diff --git a/CLAUDE.md b/.claude/CLAUDE.md similarity index 94% rename from CLAUDE.md rename to .claude/CLAUDE.md index 12d4c2bb0556..29318078803d 100644 --- a/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -118,4 +118,9 @@ FastGPT 是一个 AI Agent 构建平台,通过 Flow 提供开箱即用的数据 ## 代码规范 -- 尽可能使用 type 进行类型声明,而不是 interface。 \ No newline at end of file +- 尽可能使用 type 进行类型声明,而不是 interface。 + +## Agent 设计规范 + +1. 对于功能的实习和复杂问题修复,优先进行文档设计,并于让用户确认后,再进行执行修复。 +2. 采用"设计文档-测试示例-代码编写-测试运行-修正代码/文档"的工作模式,以测试为核心来确保设计的正确性。 \ No newline at end of file diff --git a/.claude/design/projects_app_performance_stability_analysis.md b/.claude/design/projects_app_performance_stability_analysis.md index 9e4b4107d9c9..6ff0a0db9025 100644 --- a/.claude/design/projects_app_performance_stability_analysis.md +++ b/.claude/design/projects_app_performance_stability_analysis.md @@ -80,7 +80,7 @@ addActiveNode(nodeId: string) { --- -### 🔴 H2. MongoDB 连接池配置缺失 +### 🔴 H2. MongoDB 连接池配置缺失(已解决) **位置**: - `packages/service/common/mongo/index.ts:12-24` @@ -147,7 +147,7 @@ connectionMongo.connection.on('connectionPoolClosed', () => { --- -### 🔴 H3. SSE 流式响应未处理客户端断开 +### 🔴 H3. SSE 流式响应未处理客户端断开(已解决) **位置**: `packages/service/core/workflow/dispatch/index.ts:105-129` diff --git a/.eslintignore b/.eslintignore index a574aa61b09b..587ce90eebb8 100644 --- a/.eslintignore +++ b/.eslintignore @@ -23,4 +23,6 @@ vitest.config.mts bin/ scripts/ deploy/ -document/ \ No newline at end of file +document/ + +projects/marketplace diff --git a/.github/workflows/marketplace-image.yml b/.github/workflows/marketplace-image.yml new file mode 100644 index 000000000000..2dba410ef0fa --- /dev/null +++ b/.github/workflows/marketplace-image.yml @@ -0,0 +1,139 @@ +name: Build Marketplace images + +on: + workflow_dispatch: + +jobs: + build-marketplace-images: + permissions: + packages: write + contents: read + attestations: write + id-token: write + strategy: + matrix: + archs: + - arch: amd64 + - arch: arm64 + runs-on: ubuntu-24.04-arm + runs-on: ${{ matrix.archs.runs-on || 'ubuntu-24.04' }} + steps: + # install env + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + driver-opts: network=host + + - name: Cache Docker layers + uses: actions/cache@v4 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-${{ matrix.archs.arch }}-marketplace-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-${{ matrix.archs.arch }}-marketplace-buildx- + + # login docker + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Login to Ali Hub + uses: docker/login-action@v3 + with: + registry: registry.cn-hangzhou.aliyuncs.com + username: ${{ secrets.ALI_HUB_USERNAME }} + password: ${{ secrets.ALI_HUB_PASSWORD }} + + - name: Build for ${{ matrix.archs.arch }} + id: build + uses: docker/build-push-action@v6 + with: + context: . + file: projects/marketplace/Dockerfile + platforms: linux/${{ matrix.archs.arch }} + labels: | + org.opencontainers.image.source=https://github.com/${{ github.repository }} + org.opencontainers.image.description=marketplace image + outputs: type=image,"name=ghcr.io/${{ github.repository_owner }}/marketplace,${{ secrets.ALI_IMAGE_NAME }}/marketplace",push-by-digest=true,push=true + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache + + - name: Export digest + run: | + mkdir -p ${{ runner.temp }}/digests/marketplace + digest="${{ steps.build.outputs.digest }}" + touch "${{ runner.temp }}/digests/marketplace/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digests-marketplace-${{ github.sha }}-${{ matrix.archs.arch }} + path: ${{ runner.temp }}/digests/marketplace/* + if-no-files-found: error + retention-days: 1 + + release-marketplace-images: + permissions: + packages: write + contents: read + attestations: write + id-token: write + needs: build-marketplace-images + runs-on: ubuntu-24.04 + steps: + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Login to Ali Hub + uses: docker/login-action@v3 + with: + registry: registry.cn-hangzhou.aliyuncs.com + username: ${{ secrets.ALI_HUB_USERNAME }} + password: ${{ secrets.ALI_HUB_PASSWORD }} + + - name: Download digests + uses: actions/download-artifact@v4 + with: + path: ${{ runner.temp }}/digests + pattern: digests-marketplace-${{ github.sha }}-* + merge-multiple: true + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Generate random tag + id: tag + run: | + # Generate random hash tag (8 characters) + TAG=$(echo $RANDOM | md5sum | head -c 8) + echo "RANDOM_TAG=$TAG" >> $GITHUB_ENV + echo "Generated tag: $TAG" + + - name: Set image name and tag + run: | + echo "Git_IMAGE=ghcr.io/${{ github.repository_owner }}/marketplace:${{ env.RANDOM_TAG }}" >> $GITHUB_ENV + echo "Ali_IMAGE=${{ secrets.ALI_IMAGE_NAME }}/marketplace:${{ env.RANDOM_TAG }}" >> $GITHUB_ENV + + - name: Create manifest list and push + working-directory: ${{ runner.temp }}/digests + run: | + echo "Pushing image with tag: ${{ env.RANDOM_TAG }}" + TAGS="$(echo -e "${Git_Tag}\n${Ali_Tag}")" + for TAG in $TAGS; do + docker buildx imagetools create -t $TAG \ + $(printf 'ghcr.io/${{ github.repository_owner }}/marketplace@sha256:%s ' *) + sleep 5 + done + echo "✅ Successfully pushed images:" + echo " - ${{ env.Git_IMAGE }}" + echo " - ${{ env.Ali_IMAGE }}" diff --git a/document/content/docs/introduction/development/design/design_plugin.mdx b/document/content/docs/introduction/development/design/design_plugin.mdx index ae70473ebd25..0453629309b6 100644 --- a/document/content/docs/introduction/development/design/design_plugin.mdx +++ b/document/content/docs/introduction/development/design/design_plugin.mdx @@ -32,7 +32,7 @@ description: FastGPT 系统插件设计方案 1. 使用 ts-rest 作为 RPC 框架进行交互,提供 sdk 供 FastGPT 主项目调用 2. 使用 zod 进行类型验证 -3. 用 bun 进行编译,每个工具编译为单一的 `.js` 文件,支持热插拔。 +3. 用 bun 进行编译,每个工具编译为单一的 `.pkg` 文件,支持热插拔。 ## 项目结构 @@ -48,7 +48,8 @@ description: FastGPT 系统插件设计方案 - **model** 模型预设 - **scripts** 脚本(编译、创建新工具) - **sdk**: SDK 定义,供外部调用,发布到了 npm -- **src**: 运行时,express 服务 +- **runtime**: 运行时,express 服务 +- **lib**: 库文件,提供工具函数和类库 - **test**: 测试相关 系统工具的结构可以参考 [如何开发系统工具](/docs/introduction/guide/plugins/dev_system_tool)。 @@ -78,7 +79,7 @@ zod 可以实现在运行时的类型校验,也可以提供更高级的功能 ### 使用 bun 进行打包 -将插件 bundle 为一个单一的 `.js` 文件是一个重要的设计。这样可以将插件发布出来直接通过网络挂载等的形式使用。 +将插件 bundle 为一个单一的 `.pkg` 文件是一个重要的设计。这样可以将插件发布出来直接通过网络挂载等的形式使用。 ## 未来规划 diff --git a/document/content/docs/introduction/guide/plugins/dev_system_tool.mdx b/document/content/docs/introduction/guide/plugins/dev_system_tool.mdx index 775ad7a50ed7..df4cbdeb5cd7 100644 --- a/document/content/docs/introduction/guide/plugins/dev_system_tool.mdx +++ b/document/content/docs/introduction/guide/plugins/dev_system_tool.mdx @@ -5,7 +5,9 @@ description: FastGPT 系统工具开发指南 ## 介绍 -FastGPT 系统工具项目从 4.10.0 版本后移动到独立的`fastgpt-plugin`项目中,采用纯代码的模式进行工具编写。你可以在`fastgpt-plugin`项目中进行独立开发和调试好插件后,直接向 FastGPT 官方提交 PR 即可,无需运行 FastGPT 主服务。 +FastGPT 系统工具项目从 4.10.0 版本后移动到独立的`fastgpt-plugin`项目中,采用纯代码的模式进行工具编写。 +在 4.14.0 版本插件市场更新后,系统工具开发流程有所改变,请依照最新文档贡献代码。 +你可以在`fastgpt-plugin`项目中进行独立开发和调试好插件后,直接向 FastGPT 官方提交 PR 即可,无需运行 FastGPT 主服务。 ## 概念 @@ -14,29 +16,74 @@ FastGPT 系统工具项目从 4.10.0 版本后移动到独立的`fastgpt-plugin` 在`fastgpt-plugin`中,你可以每次创建一个工具/工具集,每次提交时,仅接收一个工具/工具集。如需开发多个,可以创建多个 PR 进行提交。 -## 1. 准备工作 +## 1. 准备开发环境 -- Fork [fastgpt-plugin 项目](https://github.com/labring/fastgpt-plugin) -- 安装 [Bun](https://bun.sh/) -- 部署一套 Minio,也可以直接使用 FastGPT 的 `docker-compose.yml` 中的 Minio。 -- 本地 clone 项目 `git clone git@github.com:[your-github-username]/fastgpt-plugin.git` -- 拷贝示例环境变量文件,并修改连接到开发环境的 Minio `cp .env.example .env.local` -- 安装依赖 `bun install` -- 运行开发环境 `bun run dev` +### 1.1 安装 Bun -在 dev 环境下,Bun 将监听修改并热更新。 +- 安装 [Bun](https://bun.sh/), FastGPT-plugin 使用 Bun 作为包管理器 -## 2. 初始化一个新的工具/工具集 +### 1.2 Fork FastGPT-plugin 仓库 -### 2.1 执行创建命令 +Fork 本仓库 `https://github.com/labring/fastgpt-plugin` +### 1.3 搭建开发脚手架 + + + +注意:由于使用了 bun 特有的 API,必须使用 bunx 进行安装,使用 npx/npx 等会报错 + +创建一个新的目录,在该目录下执行: +```bash +bunx @fastgpt-sdk/plugin-cli +``` + +上述命令会在当前目录下创建 fastgpt-plugin 目录,并且添加两个 remote: +- upstream 指向官方仓库 +- origin 指向你自己的仓库 + +默认使用 sparse-checkout 避免拉取所有的官方插件代码 + + + +- 本地在一个新建目录下初始化一个 `git`: + +```bash +git init +``` + +如果配置了 Git SSH Key, 则可以: +```bash +git remote add origin git@github.com:[your-name]/fastgpt-plugin.git +git remote add upstream git@github.com:labring/fastgpt-plugin.git +``` + +否则使用 https: +```bash +git remote add origin https://github.com/[your-name]/fastgpt-plugin.git +git remote add upstream https://github.com/labring/fastgpt-plugin.git +``` + +- (可选)使用稀疏检出 (Sparse-checkout) 以避免拉取所有插件代码,如果不进行稀疏检出,则会拉取所有官方插件 +```bash +git sparse-checkout init --no-cone +git sparse-checkout add "/*" "!/modules/tool/packages/*" +git pull +``` + +使用命令创建新工具 ```bash +bun i bun run new:tool ``` -依据提示分别选择创建工具/工具集,以及目录名(使用 camelCase 小驼峰法命名)。 + + + +## 2. 编写工具代码 -执行完后,系统会在 `modules/tool/packages/[your-tool-name]` 下生成一个工具/工具集的目录。 +### 2.1 工具代码结构 + +依据提示分别选择创建工具/工具集,以及目录名(使用 camelCase 小驼峰法命名)。 系统工具 (Tool) 文件结构如下: @@ -48,6 +95,8 @@ test // 测试样例 config.ts // 配置,配置工具的名称、描述、类型、图标等 index.ts // 入口,不要改这个文件 logo.svg // Logo,替换成你的工具的 Logo +README.md // (可选)README 文件,用于展示工具的使用说明和示例 +assets/ // (可选)assets 目录,用于存放工具的资源文件,如图片、音频等 package.json // npm 包 ``` @@ -55,19 +104,21 @@ package.json // npm 包 ```plaintext children -└── tool // 这个里面的结构就和上面的 tool 基本一致 +└── tool // 这个里面的结构就和上面的 tool 一致,但是没有 README 和 assets 目录 config.ts index.ts logo.svg +README.md +assets/ package.json ``` ### 2.2 修改 config.ts - **name** 和 **description** 字段为中文和英文两种语言 -- **courseUrl** 密钥获取链接,或官网链接,教程链接等。 +- **courseUrl**(可选) 密钥获取链接,或官网链接,教程链接等,如果提供 README.md,则可以写到 README 里面 - **author** 开发者名 -- **type** 为枚举类型,目前有: +- **tags** 工具默认的标签,有如下可选标签(枚举类型) - tools: 工具 - search: 搜索 - multimodal: 多模态 @@ -204,7 +255,7 @@ dalle3 的 outputs 参数格式如下: } ``` -## 2. 编写处理逻辑 +### 2.3 编写处理逻辑 在 `[your-tool-name]/src/index.ts` 为入口编写处理逻辑,需要注意: @@ -234,15 +285,51 @@ export async function tool(props: z.infer): Promise 注意:不要把你的 secret 密钥等写到测试样例中 +> +> 使用 Agent 工具编写测试样例时,可能 Agent 工具会修改您的处理逻辑甚至修改整个测试框架的逻辑。 -### 从 Scalar 进行测试 +### 5.2 查看测试样例覆盖率(coverage) -浏览器打开`localhost:3000/openapi`可进入`fastgpt-plugin`的 OpenAPI 页面,进行 API 调试。 +浏览器打开 coverage/index.html 可以插件各个模块的覆盖率 + +提交插件给官方仓库,必须编写单元测试样例,并且达到: +- 90% 以上代码覆盖率 +- 100% 函数覆盖率 +- 100% 分支条件覆盖率 + +## 6. E2E (端到端)测试 + +对于简单的工具,可能并不需要进行 E2E 测试,而如果工具过于复杂,官方人员可能会要求您完成 E2E 测试。 + +### 6.1 部署 E2E 测试环境 + +1. 参考 [快速开始本地开发](/docs/introduction/development/intro),在本地部署一套 FastGPT 开发环境 +2. `cd runtime && cp .env.template .env.local` 复制环境变量样例文件,连接到上一步部署的 Minio, Mongo, Redis 中 +3. `bun run dev` 运行开发环境,修改 FastGPT 的环境变量,连接到你刚刚启动的 fastgpt-plugin + +### 6.2 从 Scalar 进行测试 + +运行 fastgpt-plugin 开发环境 + +浏览器打开`http://localhost:PORT/openapi`可进入`fastgpt-plugin`的 OpenAPI 页面,进行 API 调试。 +PORT 为你的 fastgpt-plugin 的端口 ![](/imgs/plugin-openapi.png) @@ -250,14 +337,18 @@ export async function tool(props: z.infer): Promise 从 FastGPT 4.14.0 版本开始,系统管理员可以通过 Web 界面直接上传和更新系统工具,无需重新部署服务 +> 从 FastGPT 4.14.0 版本开始,系统管理员可以通过 Web 界面直接上传和更新系统工具进行热更新 ## 权限要求 ⚠️ **重要提示**:只有 **root 用户** 才能使用在线上传系统工具功能。 - 确保您已使用 `root` 账户登录 FastGPT -- 普通用户无法看到"导入/更新"按钮和删除功能 ## 支持的文件格式 -- **文件类型**:`.js` 文件 -- **文件大小**:最大 10MB -- **文件数量**:每次只能上传一个文件 +- **文件类型**:`.pkg` 文件 +- **文件大小**:最大 100 MB +- **文件数量**:每次最多上传 15 个文件 ## 上传步骤 -### 1. 进入系统工具页面 +### 1. 进入配置页面 -1. 登录 FastGPT 管理后台 -2. 导航到:**工作台** → **系统工具** -3. 确认页面右上角显示"导入/更新"按钮(只有 root 用户可见) ![](/imgs/plugins/entry.png) ### 2. 准备工具文件 -在上传之前,请确保您的 `.js` 文件是从 fastgpt-plugin 项目中通过 `bun run build` 命令打包后的 dist/tools/built-in 文件夹下得到的 +在上传之前,请确保您的 `.pkg` 文件是从 fastgpt-plugin 项目中通过 `bun run build:pkg` 命令打包后的 `dist/pkgs` 文件夹下得到的 -![](/imgs/plugins/file.png) +![](/imgs/plugins/files.png) ### 3. 执行上传 1. 点击 **"导入/更新"** 按钮 2. 在弹出的对话框中,点击文件选择区域 -3. 选择您准备好的 `.js` 工具文件 +3. 选择您准备好的 `.pkg` 工具文件 4. 确认文件信息无误后,点击 **"确认导入"** ### 4. 上传过程 @@ -54,70 +50,10 @@ description: FastGPT 系统工具在线上传指南 - **上传工具**:仅 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 bdb137127b33..d1591c83d003 100644 --- a/document/content/docs/toc.mdx +++ b/document/content/docs/toc.mdx @@ -110,6 +110,7 @@ description: FastGPT 文档目录 - [/docs/upgrading/4-13/4130](/docs/upgrading/4-13/4130) - [/docs/upgrading/4-13/4131](/docs/upgrading/4-13/4131) - [/docs/upgrading/4-13/4132](/docs/upgrading/4-13/4132) +- [/docs/upgrading/4-14/4140](/docs/upgrading/4-14/4140) - [/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-14/4140.mdx b/document/content/docs/upgrading/4-14/4140.mdx new file mode 100644 index 000000000000..b4e1f0809b5b --- /dev/null +++ b/document/content/docs/upgrading/4-14/4140.mdx @@ -0,0 +1,37 @@ +--- +title: 'V4.14.0(进行中)' +description: 'FastGPT V4.14.0 更新说明' +--- + +## 更新指南 + +### 1. 更新镜像: + +- 更新 FastGPT 镜像tag: v4.14.0 +- 更新 FastGPT 商业版镜像tag: v4.14.0 +- 更新 fastgpt-plugin 镜像 tag: v0.3.0 +- mcp_server 无需更新 +- Sandbox 无需更新 +- AIProxy 无需更新 + +## 🚀 新增内容 + +1. 增加插件市场,同时移除自定义工具分类,仅支持自定义标签。本期支持系统工具,可以从 FastGPT Marketplace 统一安装系统工具。后续将支持更多插件类型:工作流触发器,数据源解析方式,数据分块,索引增强策略等。 +2. 对话框上传文件移动存储至 S3,并且不会自动过期,完全跟随对话记录删除。安全性更高,签发预览连接仅 1 小时生效,而不是长期。 +3. 全局变量支持时间点/时间范围/对话模型选择类型。 +4. 插件输入支持密码类型。 + +## ⚙️ 优化 + +1. 匹配 Markdown 中 Base64 图片正则性能。 + +## 🐛 修复 + +1. Claude 工具调用,如果下标从 1 开始会导致参数异常。 +2. S3 删除头像,如果 key 为空时,会抛错,导致流程阻塞。 +3. 工作流前置IO 变更时,依赖未及时刷新。 +4. 导出对话日志,缺少反馈记录。 +5. 工作流欢迎语输入框输入时,光标会偏移到最后一位。 +6. 存在交互节点和连续批量执行时,会导致工作流运行逻辑错误。 +7. 工作流 Redo 操作后,编辑记录无法再继续推送快照。 + diff --git a/document/content/docs/upgrading/meta.json b/document/content/docs/upgrading/meta.json index 89e4c054a7ff..a91b2bb8a3ac 100644 --- a/document/content/docs/upgrading/meta.json +++ b/document/content/docs/upgrading/meta.json @@ -4,6 +4,8 @@ "description": "FastGPT 版本更新介绍及升级操作", "pages": [ "index", + "---4.14.x---", + "...4-14", "---4.13.x---", "...4-13", "---4.12.x---", diff --git a/document/data/doc-last-modified.json b/document/data/doc-last-modified.json index 8ab17e251297..52fdbaf6f142 100644 --- a/document/data/doc-last-modified.json +++ b/document/data/doc-last-modified.json @@ -19,7 +19,7 @@ "document/content/docs/introduction/development/custom-models/ollama.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/introduction/development/custom-models/xinference.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/introduction/development/design/dataset.mdx": "2025-07-23T21:35:03+08:00", - "document/content/docs/introduction/development/design/design_plugin.mdx": "2025-08-20T19:00:48+08:00", + "document/content/docs/introduction/development/design/design_plugin.mdx": "2025-10-30T22:14:07+08:00", "document/content/docs/introduction/development/docker.mdx": "2025-09-29T11:34:11+08:00", "document/content/docs/introduction/development/faq.mdx": "2025-08-12T22:22:18+08:00", "document/content/docs/introduction/development/intro.mdx": "2025-09-29T11:34:11+08:00", @@ -38,7 +38,7 @@ "document/content/docs/introduction/development/proxy/cloudflare.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/development/proxy/http_proxy.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/development/proxy/nginx.mdx": "2025-07-23T21:35:03+08:00", - "document/content/docs/introduction/development/quick-start.mdx": "2025-09-29T11:34:11+08:00", + "document/content/docs/introduction/development/quick-start.mdx": "2025-10-21T11:58:25+08:00", "document/content/docs/introduction/development/sealos.mdx": "2025-09-29T11:52:39+08:00", "document/content/docs/introduction/development/signoz.mdx": "2025-09-17T22:29:56+08:00", "document/content/docs/introduction/guide/DialogBoxes/htmlRendering.mdx": "2025-07-23T21:35:03+08:00", @@ -84,11 +84,11 @@ "document/content/docs/introduction/guide/knowledge_base/websync.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/introduction/guide/knowledge_base/yuque_dataset.mdx": "2025-09-17T22:29:56+08:00", "document/content/docs/introduction/guide/plugins/bing_search_plugin.mdx": "2025-07-23T21:35:03+08:00", - "document/content/docs/introduction/guide/plugins/dev_system_tool.mdx": "2025-08-20T19:00:48+08:00", + "document/content/docs/introduction/guide/plugins/dev_system_tool.mdx": "2025-10-30T22:14:07+08:00", "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-24T22:40:31+08:00", + "document/content/docs/introduction/guide/plugins/upload_system_tool.mdx": "2025-10-30T22:14:07+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", @@ -101,7 +101,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-10-09T15:10:19+08:00", + "document/content/docs/toc.mdx": "2025-10-23T19:11:11+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", @@ -113,7 +113,8 @@ "document/content/docs/upgrading/4-12/4124.mdx": "2025-09-17T22:29:56+08:00", "document/content/docs/upgrading/4-13/4130.mdx": "2025-09-30T16:00:10+08:00", "document/content/docs/upgrading/4-13/4131.mdx": "2025-09-30T15:47:06+08:00", - "document/content/docs/upgrading/4-13/4132.mdx": "2025-10-21T11:32:22+08:00", + "document/content/docs/upgrading/4-13/4132.mdx": "2025-10-21T11:46:53+08:00", + "document/content/docs/upgrading/4-14/4140.mdx": "2025-10-30T22:49:32+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 index aaadd6fad6cf..b3e8c34d74c0 100644 Binary files a/document/public/imgs/plugins/entry.png 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 deleted file mode 100644 index 8843823d1c05..000000000000 Binary files a/document/public/imgs/plugins/file.png and /dev/null differ diff --git a/document/public/imgs/plugins/files.png b/document/public/imgs/plugins/files.png new file mode 100644 index 000000000000..d4f38c9d2b5f Binary files /dev/null and b/document/public/imgs/plugins/files.png differ diff --git a/packages/global/common/error/s3.ts b/packages/global/common/error/s3.ts new file mode 100644 index 000000000000..c5121fa78940 --- /dev/null +++ b/packages/global/common/error/s3.ts @@ -0,0 +1,65 @@ +import { formatFileSize } from '../file/tools'; + +/** + * Parse S3 upload error and return user-friendly error message key + * @param error - The error from S3 upload + * @param maxFileSize - Maximum allowed file size in bytes + * @returns i18n error message key and parameters + */ +export function parseS3UploadError({ + t, + error, + maxSize +}: { + t: any; + error: any; + maxSize?: number; +}): string { + const maxSizeStr = maxSize ? formatFileSize(maxSize) : '-'; + // Handle S3 XML error response + if (typeof error === 'string' && error.includes('EntityTooLarge')) { + return t('common:error:s3_upload_file_too_large', { max: maxSizeStr }); + } + + // Handle axios error response + if (error?.response?.data) { + const data = error.response.data; + + // Try to parse XML error response + if (typeof data === 'string') { + if (data.includes('EntityTooLarge')) { + return t('common:error:s3_upload_file_too_large', { max: maxSizeStr }); + } + if (data.includes('AccessDenied')) { + return t('common:error:s3_upload_auth_failed'); + } + if (data.includes('InvalidAccessKeyId') || data.includes('SignatureDoesNotMatch')) { + return t('common:error:s3_upload_auth_failed'); + } + if (data.includes('NoSuchBucket')) { + return t('common:error:s3_upload_bucket_not_found'); + } + if (data.includes('RequestTimeout')) { + return t('common:error:s3_upload_timeout'); + } + } + } + + // Handle network errors + if (error?.code === 'ECONNREFUSED' || error?.code === 'ETIMEDOUT') { + return t('common:error:s3_upload_network_error'); + } + + // Handle axios timeout + if (error?.code === 'ECONNABORTED' || error?.message?.includes('timeout')) { + return t('common:error:s3_upload_timeout'); + } + + // Handle file size validation error (client-side) + if (error?.message?.includes('file size') || error?.message?.includes('too large')) { + return t('common:error:s3_upload_file_too_large', { max: maxSizeStr }); + } + + // Default error + return t('common:error:s3_upload_network_error'); +} diff --git a/packages/global/common/file/icon.ts b/packages/global/common/file/icon.ts index f2baf7613fdd..0d268e509ec0 100644 --- a/packages/global/common/file/icon.ts +++ b/packages/global/common/file/icon.ts @@ -1,19 +1,19 @@ -export const fileImgs = [ - { suffix: 'pdf', src: 'file/fill/pdf' }, - { suffix: 'ppt', src: 'file/fill/ppt' }, - { suffix: 'xlsx', src: 'file/fill/xlsx' }, - { suffix: 'csv', src: 'file/fill/csv' }, - { suffix: '(doc|docs)', src: 'file/fill/doc' }, - { suffix: 'txt', src: 'file/fill/txt' }, - { suffix: 'md', src: 'file/fill/markdown' }, - { suffix: 'html', src: 'file/fill/html' }, - { suffix: '(jpg|jpeg|png|gif|bmp|webp|svg|ico|tiff|tif)', src: 'image' } +export const getFileIcon = (name = '', defaultImg = 'file/fill/file') => { + const fileImgs = [ + { suffix: 'pdf', src: 'file/fill/pdf' }, + { suffix: 'ppt', src: 'file/fill/ppt' }, + { suffix: 'xlsx', src: 'file/fill/xlsx' }, + { suffix: 'csv', src: 'file/fill/csv' }, + { suffix: '(doc|docs)', src: 'file/fill/doc' }, + { suffix: 'txt', src: 'file/fill/txt' }, + { suffix: 'md', src: 'file/fill/markdown' }, + { suffix: 'html', src: 'file/fill/html' }, + { suffix: '(jpg|jpeg|png|gif|bmp|webp|svg|ico|tiff|tif)', src: 'image' }, + { suffix: '(mp3|wav|ogg|m4a|amr|mpga)', src: 'file/fill/audio' }, + { suffix: '(mp4|mov|avi|mpeg|webm)', src: 'file/fill/video' } + ]; - // { suffix: '.', src: '/imgs/files/file.svg' } -]; - -export function getFileIcon(name = '', defaultImg = 'file/fill/file') { return ( fileImgs.find((item) => new RegExp(`\.${item.suffix}`, 'gi').test(name))?.src || defaultImg ); -} +}; diff --git a/packages/global/common/file/tools.ts b/packages/global/common/file/tools.ts index 262cbe3476b1..585ea4af02f9 100644 --- a/packages/global/common/file/tools.ts +++ b/packages/global/common/file/tools.ts @@ -1,7 +1,7 @@ import { detect } from 'jschardet'; import { documentFileType } from './constants'; import { ChatFileTypeEnum } from '../../core/chat/constants'; -import { type UserChatItemValueItemType } from '../../core/chat/type'; +import { type UserChatItemFileItemType } from '../../core/chat/type'; import * as fs from 'fs'; export const formatFileSize = (bytes: number): string => { @@ -36,7 +36,7 @@ export const detectFileEncodingByPath = async (path: string) => { }; // Url => user upload file type -export const parseUrlToFileType = (url: string): UserChatItemValueItemType['file'] | undefined => { +export const parseUrlToFileType = (url: string): UserChatItemFileItemType | undefined => { if (typeof url !== 'string') return; // Handle base64 image @@ -74,13 +74,13 @@ export const parseUrlToFileType = (url: string): UserChatItemValueItemType['file // Default to image type for non-document files return { type: ChatFileTypeEnum.image, - name: filename || 'null.png', + name: filename || 'null', url }; } catch (error) { return { - type: ChatFileTypeEnum.image, - name: 'invalid.png', + type: ChatFileTypeEnum.file, + name: url, url }; } diff --git a/packages/global/common/i18n/utils.ts b/packages/global/common/i18n/utils.ts index ac271bcabdf8..16a65c528d45 100644 --- a/packages/global/common/i18n/utils.ts +++ b/packages/global/common/i18n/utils.ts @@ -2,5 +2,17 @@ import type { I18nStringType, localeType } from './type'; export const parseI18nString = (str: I18nStringType | string = '', lang = 'en') => { if (!str || typeof str === 'string') return str; - return str[lang as localeType] ?? str['en']; + + // 尝试使用当前语言 + if (str[lang as localeType]) { + return str[lang as localeType] || ''; + } + + // 如果当前语言是繁体中文但没有对应翻译,优先回退到简体中文 + if (lang === 'zh-Hant' && !str['zh-Hant'] && str['zh-CN']) { + return str['zh-CN']; + } + + // 最后回退到英文 + return str['en'] || ''; }; diff --git a/packages/global/common/parentFolder/type.d.ts b/packages/global/common/parentFolder/type.ts similarity index 87% rename from packages/global/common/parentFolder/type.d.ts rename to packages/global/common/parentFolder/type.ts index b962435a4766..801d2b966671 100644 --- a/packages/global/common/parentFolder/type.d.ts +++ b/packages/global/common/parentFolder/type.ts @@ -1,3 +1,8 @@ +import z from 'zod'; + +export const ParentIdSchema = z.string().nullish(); +export type ParentIdType = string | null | undefined; + export type GetPathProps = { sourceId?: ParentIdType; type: 'current' | 'parent'; @@ -7,7 +12,6 @@ export type ParentTreePathItemType = { parentId: string; parentName: string; }; -export type ParentIdType = string | null | undefined; export type GetResourceFolderListProps = { parentId: ParentIdType; diff --git a/packages/global/common/string/markdown.ts b/packages/global/common/string/markdown.ts index b171a626a5ca..bfaab2f36a3c 100644 --- a/packages/global/common/string/markdown.ts +++ b/packages/global/common/string/markdown.ts @@ -173,18 +173,20 @@ export const markdownProcess = async ({ }; export const matchMdImg = (text: string) => { - const base64Regex = /!\[([^\]]*)\]\((data:image\/[^;]+;base64[^)]+)\)/g; + // 优化后的正则: + // 1. 使用 [^\]]* 匹配 alt 文本(更精确) + // 2. 使用 [A-Za-z0-9+/=]+ 匹配 base64 数据(避免回溯) + // 3. 明确匹配 data:image/ 前缀 + const base64Regex = /!\[([^\]]*)\]\((data:image\/([^;]+);base64,([A-Za-z0-9+/=]+))\)/g; const imageList: ImageType[] = []; - text = text.replace(base64Regex, (match, altText, base64Url) => { + text = text.replace(base64Regex, (_match, altText, _fullDataUrl, mime, base64Data) => { const uuid = `IMAGE_${getNanoid(12)}_IMAGE`; - const mime = base64Url.split(';')[0].split(':')[1]; - const base64 = base64Url.split(',')[1]; imageList.push({ uuid, - base64, - mime + base64: base64Data, + mime: `image/${mime}` }); // 保持原有的 alt 文本,只替换 base64 部分 diff --git a/packages/global/common/string/textSplitter.ts b/packages/global/common/string/textSplitter.ts index e44341ccb466..803d51468795 100644 --- a/packages/global/common/string/textSplitter.ts +++ b/packages/global/common/string/textSplitter.ts @@ -82,9 +82,10 @@ const markdownTableSplit = (props: SplitProps): SplitResponse => { .join(' | ')} |`; const chunks: string[] = []; - let chunk = `${header} + const defaultChunk = `${header} ${mdSplitString} `; + let chunk = defaultChunk; for (let i = 2; i < splitText2Lines.length; i++) { const chunkLength = getTextValidLength(chunk); @@ -93,9 +94,7 @@ ${mdSplitString} // Over size if (chunkLength + nextLineLength > chunkSize) { chunks.push(chunk); - chunk = `${header} -${mdSplitString} -`; + chunk = defaultChunk; } chunk += `${splitText2Lines[i]}\n`; } diff --git a/packages/global/common/system/types/index.d.ts b/packages/global/common/system/types/index.d.ts index 4fd196276fa2..379623b8cc43 100644 --- a/packages/global/common/system/types/index.d.ts +++ b/packages/global/common/system/types/index.d.ts @@ -79,7 +79,7 @@ export type FastGPTFeConfigsType = { concatMd?: string; docUrl?: string; openAPIDocUrl?: string; - systemPluginCourseUrl?: string; + submitPluginRequestUrl?: string; appTemplateCourse?: string; customApiDomain?: string; customSharePageDomain?: string; diff --git a/packages/global/common/time/timezone.ts b/packages/global/common/time/timezone.ts index f7eb2f9dee2e..079585e0df6d 100644 --- a/packages/global/common/time/timezone.ts +++ b/packages/global/common/time/timezone.ts @@ -73,8 +73,8 @@ export const getTimeZoneList = () => { }; export const timeZoneList = getTimeZoneList(); -export const getMongoTimezoneCode = (timeString: string) => { - if (!timeString.includes(':')) { +export const getTimezoneCodeFromStr = (timeString: string | Date) => { + if (typeof timeString !== 'string' || !timeString.includes(':')) { return '+00:00'; } diff --git a/packages/global/core/ai/provider.ts b/packages/global/core/ai/provider.ts index 96047dca8387..c25a13fcd19a 100644 --- a/packages/global/core/ai/provider.ts +++ b/packages/global/core/ai/provider.ts @@ -1,4 +1,4 @@ -import type { I18nStringStrictType } from '@fastgpt-sdk/plugin'; +import type { I18nStringStrictType } from '../../sdk/fastgpt-plugin'; export type ModelProviderItemType = { id: string; diff --git a/packages/global/core/ai/type.d.ts b/packages/global/core/ai/type.d.ts index 39373a40fd87..5ce40bab199e 100644 --- a/packages/global/core/ai/type.d.ts +++ b/packages/global/core/ai/type.d.ts @@ -19,10 +19,11 @@ export type ChatCompletionContentPartFile = { type: 'file_url'; name: string; url: string; + key?: string; }; // Rewrite ChatCompletionContentPart, Add file type export type ChatCompletionContentPart = - | SdkChatCompletionContentPart + | (SdkChatCompletionContentPart & { key?: string }) | ChatCompletionContentPartFile; type CustomChatCompletionUserMessageParam = Omit & { role: 'user'; diff --git a/packages/global/core/app/constants.ts b/packages/global/core/app/constants.ts index e44d1fedf147..a4bfe86a2140 100644 --- a/packages/global/core/app/constants.ts +++ b/packages/global/core/app/constants.ts @@ -50,7 +50,11 @@ export const defaultChatInputGuideConfig = { export const defaultAppSelectFileConfig: AppFileSelectConfigType = { canSelectFile: false, canSelectImg: false, - maxFiles: 10 + maxFiles: 10, + canSelectVideo: false, + canSelectAudio: false, + canSelectCustomFileExtension: false, + customFileExtensionList: [] }; export enum AppTemplateTypeEnum { @@ -64,3 +68,45 @@ export enum AppTemplateTypeEnum { // special type contribute = 'contribute' } + +export const defaultFileExtensionTypes = { + canSelectFile: ['.pdf', '.docx', '.pptx', '.xlsx', '.txt', '.md', '.html', '.csv'], + canSelectImg: ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'], + canSelectVideo: ['.mp4', '.mov', '.avi', '.mpeg', '.webm'], + canSelectAudio: ['.mp3', '.wav', '.ogg', '.m4a', '.amr', '.mpga'], + canSelectCustomFileExtension: [] +}; +export type FileExtensionKeyType = keyof typeof defaultFileExtensionTypes; +export const getUploadFileType = ({ + canSelectFile, + canSelectImg, + canSelectVideo, + canSelectAudio, + canSelectCustomFileExtension, + customFileExtensionList +}: { + canSelectFile?: boolean; + canSelectImg?: boolean; + canSelectVideo?: boolean; + canSelectAudio?: boolean; + canSelectCustomFileExtension?: boolean; + customFileExtensionList?: string[]; +}) => { + const types: string[] = []; + if (canSelectFile) { + types.push(...defaultFileExtensionTypes.canSelectFile); + } + if (canSelectImg) { + types.push(...defaultFileExtensionTypes.canSelectImg); + } + if (canSelectVideo) { + types.push(...defaultFileExtensionTypes.canSelectVideo); + } + if (canSelectAudio) { + types.push(...defaultFileExtensionTypes.canSelectAudio); + } + if (canSelectCustomFileExtension && customFileExtensionList) { + types.push(...customFileExtensionList); + } + return types.join(', '); +}; diff --git a/packages/global/core/app/jsonschema.ts b/packages/global/core/app/jsonschema.ts index ecb01b4bfb2c..cc3061b62d45 100644 --- a/packages/global/core/app/jsonschema.ts +++ b/packages/global/core/app/jsonschema.ts @@ -4,7 +4,7 @@ import type { FlowNodeInputItemType, FlowNodeOutputItemType } from '../workflow/ import SwaggerParser from '@apidevtools/swagger-parser'; import yaml from 'js-yaml'; import type { OpenAPIV3 } from 'openapi-types'; -import type { OpenApiJsonSchema } from './httpTools/type'; +import type { OpenApiJsonSchema } from './tool/httpTool/type'; import { i18nT } from '../../../web/i18n/utils'; type SchemaInputValueType = 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object'; diff --git a/packages/global/core/app/mcpTools/type.d.ts b/packages/global/core/app/mcpTools/type.d.ts deleted file mode 100644 index 8234a532dff9..000000000000 --- a/packages/global/core/app/mcpTools/type.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { McpToolConfigType } from '../type'; - -export type McpToolSetDataType = { - url: string; - headerSecret?: StoreSecretValueType; - toolList: McpToolConfigType[]; -}; - -export type McpToolDataType = McpToolConfigType & { - url: string; - headerSecret?: StoreSecretValueType; -}; diff --git a/packages/global/core/app/plugin/utils.ts b/packages/global/core/app/plugin/utils.ts deleted file mode 100644 index a7301eff244e..000000000000 --- a/packages/global/core/app/plugin/utils.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { type StoreNodeItemType } from '../../workflow/type/node'; -import { type FlowNodeInputItemType } from '../../workflow/type/io'; -import { FlowNodeTypeEnum } from '../../workflow/node/constant'; -import { PluginSourceEnum } from './constants'; - -export const getPluginInputsFromStoreNodes = (nodes: StoreNodeItemType[]) => { - return nodes.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput)?.inputs || []; -}; -export const getPluginRunContent = ({ - pluginInputs, - variables -}: { - pluginInputs: FlowNodeInputItemType[]; - variables: Record; -}) => { - const pluginInputsWithValue = pluginInputs.map((input) => { - const { key } = input; - const value = variables?.hasOwnProperty(key) ? variables[key] : input.defaultValue; - return { - ...input, - value - }; - }); - return JSON.stringify(pluginInputsWithValue); -}; - -/** - plugin id rule: - - personal: ObjectId - - commercial: commercial-ObjectId - - systemtool: systemTool-id - - mcp tool: mcp-parentId/toolName - (deprecated) community: community-id -*/ -export function splitCombinePluginId(id: string) { - const splitRes = id.split('-'); - if (splitRes.length === 1) { - // app id - return { - source: PluginSourceEnum.personal, - pluginId: id - }; - } - - const [source, ...rest] = id.split('-') as [PluginSourceEnum, string | undefined]; - const pluginId = rest.join('-'); - if (!source || !pluginId) throw new Error('pluginId not found'); - - // 兼容4.10.0 之前的插件 - if (source === 'community' || id === 'commercial-dalle3') { - return { - source: PluginSourceEnum.systemTool, - pluginId: `${PluginSourceEnum.systemTool}-${pluginId}` - }; - } - - if (source === 'mcp') { - return { - source: PluginSourceEnum.mcp, - pluginId - }; - } - if (source === 'http') { - return { - source: PluginSourceEnum.http, - pluginId - }; - } - return { source, pluginId: id }; -} diff --git a/packages/global/core/app/systemTool/constants.ts b/packages/global/core/app/systemTool/constants.ts deleted file mode 100644 index 8b13b4610813..000000000000 --- a/packages/global/core/app/systemTool/constants.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { i18nT } from '../../../../web/i18n/utils'; - -export enum SystemToolInputTypeEnum { - system = 'system', - team = 'team', - manual = 'manual' -} -export const SystemToolInputTypeMap = { - [SystemToolInputTypeEnum.system]: { - text: i18nT('common:System') - }, - [SystemToolInputTypeEnum.team]: { - text: i18nT('common:Team') - }, - [SystemToolInputTypeEnum.manual]: { - text: i18nT('common:Manual') - } -}; diff --git a/packages/global/core/app/plugin/constants.ts b/packages/global/core/app/tool/constants.ts similarity index 92% rename from packages/global/core/app/plugin/constants.ts rename to packages/global/core/app/tool/constants.ts index 3dc350f9fe56..014d404cf612 100644 --- a/packages/global/core/app/plugin/constants.ts +++ b/packages/global/core/app/tool/constants.ts @@ -1,4 +1,4 @@ -export enum PluginSourceEnum { +export enum AppToolSourceEnum { personal = 'personal', // this is a app. systemTool = 'systemTool', // FastGPT-plugin tools, pure code. commercial = 'commercial', // configured in Pro, with associatedPluginId. Specially, commercial-dalle3 is a systemTool diff --git a/packages/global/core/app/httpTools/type.d.ts b/packages/global/core/app/tool/httpTool/type.d.ts similarity index 100% rename from packages/global/core/app/httpTools/type.d.ts rename to packages/global/core/app/tool/httpTool/type.d.ts diff --git a/packages/global/core/app/httpTools/utils.ts b/packages/global/core/app/tool/httpTool/utils.ts similarity index 89% rename from packages/global/core/app/httpTools/utils.ts rename to packages/global/core/app/tool/httpTool/utils.ts index eac7f4cc1b87..ffb813def4f5 100644 --- a/packages/global/core/app/httpTools/utils.ts +++ b/packages/global/core/app/tool/httpTool/utils.ts @@ -1,14 +1,14 @@ -import { getNanoid } from '../../../common/string/tools'; +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'; +import { type RuntimeNodeItemType } from '../../../workflow/runtime/type'; +import { FlowNodeOutputTypeEnum, FlowNodeTypeEnum } from '../../../workflow/node/constant'; +import { type HttpToolConfigType } from '../../type'; +import { AppToolSourceEnum } from '../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, @@ -66,7 +66,7 @@ export const getHTTPToolRuntimeNode = ({ intro: tool.description, toolConfig: { httpTool: { - toolId: `${PluginSourceEnum.http}-${parentId}/${tool.name}` + toolId: `${AppToolSourceEnum.http}-${parentId}/${tool.name}` } }, inputs: jsonSchema2NodeInput(tool.inputSchema), diff --git a/packages/global/core/app/tool/mcpTool/type.d.ts b/packages/global/core/app/tool/mcpTool/type.d.ts new file mode 100644 index 000000000000..cdea9b0f3f7b --- /dev/null +++ b/packages/global/core/app/tool/mcpTool/type.d.ts @@ -0,0 +1,19 @@ +import type { StoreSecretValueType } from '../../../../common/secret/type'; +import type { JSONSchemaInputType } from '../../jsonschema'; + +export type McpToolConfigType = { + name: string; + description: string; + inputSchema: JSONSchemaInputType; +}; + +export type McpToolSetDataType = { + url: string; + headerSecret?: StoreSecretValueType; + toolList: McpToolConfigType[]; +}; + +export type McpToolDataType = McpToolConfigType & { + url: string; + headerSecret?: StoreSecretValueType; +}; diff --git a/packages/global/core/app/mcpTools/utils.ts b/packages/global/core/app/tool/mcpTool/utils.ts similarity index 76% rename from packages/global/core/app/mcpTools/utils.ts rename to packages/global/core/app/tool/mcpTool/utils.ts index 9d387081e2aa..c5e5efeb2f95 100644 --- a/packages/global/core/app/mcpTools/utils.ts +++ b/packages/global/core/app/tool/mcpTool/utils.ts @@ -1,12 +1,12 @@ -import { NodeOutputKeyEnum, WorkflowIOValueTypeEnum } from '../../workflow/constants'; -import { i18nT } from '../../../../web/i18n/utils'; -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'; -import { jsonSchema2NodeInput } from '../jsonschema'; -import { getNanoid } from '../../../common/string/tools'; -import { PluginSourceEnum } from '../plugin/constants'; +import { NodeOutputKeyEnum, WorkflowIOValueTypeEnum } from '../../../workflow/constants'; +import { i18nT } from '../../../../../web/i18n/utils'; +import { FlowNodeOutputTypeEnum, FlowNodeTypeEnum } from '../../../workflow/node/constant'; +import { type McpToolConfigType } from '../../tool/mcpTool/type'; +import { type RuntimeNodeItemType } from '../../../workflow/runtime/type'; +import { type StoreSecretValueType } from '../../../../common/secret/type'; +import { jsonSchema2NodeInput } from '../../jsonschema'; +import { getNanoid } from '../../../../common/string/tools'; +import { AppToolSourceEnum } from '../constants'; export const getMCPToolSetRuntimeNode = ({ url, @@ -59,7 +59,7 @@ export const getMCPToolRuntimeNode = ({ intro: tool.description, toolConfig: { mcpTool: { - toolId: `${PluginSourceEnum.mcp}-${parentId}/${tool.name}` + toolId: `${AppToolSourceEnum.mcp}-${parentId}/${tool.name}` } }, inputs: jsonSchema2NodeInput(tool.inputSchema), diff --git a/packages/global/core/app/tool/systemTool/constants.ts b/packages/global/core/app/tool/systemTool/constants.ts new file mode 100644 index 000000000000..0b9e6a2b47e0 --- /dev/null +++ b/packages/global/core/app/tool/systemTool/constants.ts @@ -0,0 +1,18 @@ +import { i18nT } from '../../../../../web/i18n/utils'; + +export enum SystemToolSecretInputTypeEnum { + system = 'system', + team = 'team', + manual = 'manual' +} +export const SystemToolSecretInputTypeMap = { + [SystemToolSecretInputTypeEnum.system]: { + text: i18nT('common:System') + }, + [SystemToolSecretInputTypeEnum.team]: { + text: i18nT('common:Team') + }, + [SystemToolSecretInputTypeEnum.manual]: { + text: i18nT('common:Manual') + } +}; diff --git a/packages/global/core/app/plugin/type.d.ts b/packages/global/core/app/tool/type.d.ts similarity index 68% rename from packages/global/core/app/plugin/type.d.ts rename to packages/global/core/app/tool/type.d.ts index 1849beef75c5..c396fa8cf494 100644 --- a/packages/global/core/app/plugin/type.d.ts +++ b/packages/global/core/app/tool/type.d.ts @@ -6,8 +6,12 @@ import type { FlowNodeTemplateType } from '../../workflow/type/node'; import type { WorkflowTemplateType } from '../../workflow/type'; import type { FlowNodeInputItemType, FlowNodeOutputItemType } from '../../workflow/type/io'; import type { ParentIdType } from 'common/parentFolder/type'; +import type { I18nStringStrictType } from '../../../common/i18n/type'; +import type { I18nStringType } from '../../../common/i18n/type'; +import type { ToolSimpleType, ToolDetailType } from '../../../sdk/fastgpt-plugin'; +import type { PluginStatusType, SystemPluginToolTagType } from '../../plugin/type'; -export type PluginRuntimeType = { +export type AppToolRuntimeType = { id: string; teamId?: string; tmbId?: string; @@ -23,10 +27,9 @@ export type PluginRuntimeType = { hasTokenFee?: boolean; }; -// system plugin -export type SystemPluginTemplateItemType = WorkflowTemplateType & { - templateType: string; - +// System tool +export type AppToolTemplateItemType = WorkflowTemplateType & { + status?: PluginStatusType; // FastGPT-plugin tool inputs?: FlowNodeInputItemType[]; outputs?: FlowNodeOutputItemType[]; @@ -49,7 +52,8 @@ export type SystemPluginTemplateItemType = WorkflowTemplateType & { hasTokenFee?: boolean; pluginOrder?: number; - isActive?: boolean; + toolTags?: string[] | null; + defaultInstalled?: boolean; isOfficial?: boolean; // Admin config @@ -57,14 +61,16 @@ export type SystemPluginTemplateItemType = WorkflowTemplateType & { inputListVal?: Record; hasSystemSecret?: boolean; - // Plugin source type - toolSource?: 'uploaded' | 'built-in'; + // @deprecated use toolTags instead + isActive?: boolean; + templateType?: string; }; -export type SystemPluginTemplateListItemType = Omit< - SystemPluginTemplateItemType, - 'name' | 'intro' +export type AppToolTemplateListItemType = Omit< + AppToolTemplateItemType, + 'name' | 'intro' | 'workflow' > & { name: string; intro: string; + tags?: SystemPluginToolTagType[]; }; diff --git a/packages/global/core/app/tool/utils.ts b/packages/global/core/app/tool/utils.ts new file mode 100644 index 000000000000..1838a1d3affe --- /dev/null +++ b/packages/global/core/app/tool/utils.ts @@ -0,0 +1,46 @@ +import { AppToolSourceEnum } from '../tool/constants'; + +/** + Tool id rule: + - personal: ObjectId + - commercial: commercial-ObjectId + - systemtool: systemTool-id + - mcp tool: mcp-parentId/toolName + (deprecated) community: community-id +*/ +export function splitCombineToolId(id: string) { + const splitRes = id.split('-'); + if (splitRes.length === 1) { + // app id + return { + source: AppToolSourceEnum.personal, + pluginId: id + }; + } + + const [source, ...rest] = id.split('-') as [AppToolSourceEnum, string | undefined]; + const pluginId = rest.join('-'); + if (!source || !pluginId) throw new Error('pluginId not found'); + + // 兼容4.10.0 之前的插件 + if (source === 'community' || id === 'commercial-dalle3') { + return { + source: AppToolSourceEnum.systemTool, + pluginId: `${AppToolSourceEnum.systemTool}-${pluginId}` + }; + } + + if (source === 'mcp') { + return { + source: AppToolSourceEnum.mcp, + pluginId + }; + } + if (source === 'http') { + return { + source: AppToolSourceEnum.http, + pluginId + }; + } + return { source, pluginId: id }; +} diff --git a/packages/global/core/app/tool/workflowTool/utils.ts b/packages/global/core/app/tool/workflowTool/utils.ts new file mode 100644 index 000000000000..a8f4ff8ab9de --- /dev/null +++ b/packages/global/core/app/tool/workflowTool/utils.ts @@ -0,0 +1,6 @@ +import { type StoreNodeItemType } from '../../../workflow/type/node'; +import { FlowNodeTypeEnum } from '../../../workflow/node/constant'; + +export const getWorkflowToolInputsFromStoreNodes = (nodes: StoreNodeItemType[]) => { + return nodes.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput)?.inputs || []; +}; diff --git a/packages/global/core/app/type.d.ts b/packages/global/core/app/type.d.ts index 40ff44c8b433..9a2a92ca4ab6 100644 --- a/packages/global/core/app/type.d.ts +++ b/packages/global/core/app/type.d.ts @@ -7,7 +7,7 @@ import type { VariableInputEnum, WorkflowIOValueTypeEnum } from '../workflow/constants'; -import type { SelectedDatasetType } from '../workflow/type/io'; +import type { InputComponentPropsType, SelectedDatasetType } from '../workflow/type/io'; import type { DatasetSearchModeEnum } from '../dataset/constants'; import { TeamTagSchema as TeamTagsSchemaType } from '@fastgpt/global/support/user/team/type.d'; import type { StoreEdgeItemType } from '../workflow/type/edge'; @@ -115,12 +115,6 @@ export type AppSimpleEditFormType = { chatConfig: AppChatConfigType; }; -export type McpToolConfigType = { - name: string; - description: string; - inputSchema: JSONSchemaInputType; -}; - export type HttpToolConfigType = { name: string; description: string; @@ -170,38 +164,11 @@ export type SettingAIDataType = { }; // variable -export type VariableItemType = { - // id: string; - key: string; - label: string; - type: VariableInputEnum; - required: boolean; - description: string; - valueType?: WorkflowIOValueTypeEnum; - defaultValue?: any; - - // input - maxLength?: number; - // password - minLength?: number; - // numberInput - max?: number; - min?: number; - // select - list?: { label: string; value: string }[]; - // file - canSelectFile?: boolean; - canSelectImg?: boolean; - maxFiles?: number; - // timeSelect - timeGranularity?: 'second' | 'minute' | 'hour' | 'day'; - timeType?: 'point' | 'range'; - timeRangeStart?: string; - timeRangeEnd?: string; - - // @deprecated - enums?: { value: string; label: string }[]; -}; +export type VariableItemType = AppFileSelectConfigType & + InputComponentPropsType & { + type: VariableInputEnum; + description: string; + }; // tts export type AppTTSConfigType = { type: 'none' | 'web' | 'model'; @@ -241,16 +208,14 @@ export type AppAutoExecuteConfigType = { }; // File export type AppFileSelectConfigType = { - canSelectFile: boolean; + maxFiles?: number; + canSelectFile?: boolean; customPdfParse?: boolean; - canSelectImg: boolean; - maxFiles: number; -}; - -export type SystemPluginListItemType = { - _id: string; - name: string; - avatar: string; + canSelectImg?: boolean; + canSelectVideo?: boolean; + canSelectAudio?: boolean; + canSelectCustomFileExtension?: boolean; + customFileExtensionList?: string[]; }; export type AppTemplateSchemaType = { diff --git a/packages/global/core/chat/adapt.ts b/packages/global/core/chat/adapt.ts index 31dde95440e5..326467f1e16d 100644 --- a/packages/global/core/chat/adapt.ts +++ b/packages/global/core/chat/adapt.ts @@ -17,6 +17,7 @@ import type { ChatCompletionToolMessageParam } from '../../core/ai/type.d'; import { ChatCompletionRequestMessageRoleEnum } from '../../core/ai/constants'; + const GPT2Chat = { [ChatCompletionRequestMessageRoleEnum.System]: ChatRoleEnum.System, [ChatCompletionRequestMessageRoleEnum.User]: ChatRoleEnum.Human, @@ -71,6 +72,7 @@ export const chats2GPTMessages = ({ if (item.file?.type === ChatFileTypeEnum.image) { return { type: 'image_url', + key: item.file.key, image_url: { url: item.file.url } @@ -79,7 +81,8 @@ export const chats2GPTMessages = ({ return { type: 'file_url', name: item.file?.name || '', - url: item.file.url + url: item.file.url, + key: item.file.key }; } } @@ -171,6 +174,7 @@ export const chats2GPTMessages = ({ return results; }; + export const GPTMessages2Chats = ({ messages, reserveTool = true, @@ -238,7 +242,8 @@ export const GPTMessages2Chats = ({ file: { type: ChatFileTypeEnum.image, name: '', - url: item.image_url.url + url: item.image_url.url, + key: item.key } }); } else if (item.type === 'file_url') { @@ -248,7 +253,8 @@ export const GPTMessages2Chats = ({ file: { type: ChatFileTypeEnum.file, name: item.name, - url: item.url + url: item.url, + key: item.key } }); } diff --git a/packages/global/core/chat/api.d.ts b/packages/global/core/chat/api.d.ts deleted file mode 100644 index 3c98979aab1e..000000000000 --- a/packages/global/core/chat/api.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { OutLinkChatAuthProps } from '../../support/permission/chat'; - -export type UpdateChatFeedbackProps = OutLinkChatAuthProps & { - appId: string; - chatId: string; - dataId: string; - userBadFeedback?: string; - userGoodFeedback?: string; -}; diff --git a/packages/global/core/chat/api.ts b/packages/global/core/chat/api.ts new file mode 100644 index 000000000000..3d66d389083e --- /dev/null +++ b/packages/global/core/chat/api.ts @@ -0,0 +1,68 @@ +import type { OutLinkChatAuthType } from '../../support/permission/chat/type'; +import { OutLinkChatAuthSchema } from '../../support/permission/chat/type'; +import { ObjectIdSchema } from '../../common/type/mongo'; +import z from 'zod'; + +export const PresignChatFileGetUrlSchema = z + .object({ + key: z.string().min(1), + appId: ObjectIdSchema, + outLinkAuthData: OutLinkChatAuthSchema.optional() + }) + .meta({ + description: '获取对话文件预览链接', + example: { + key: '1234567890', + appId: '1234567890', + outLinkAuthData: { + shareId: '1234567890', + outLinkUid: '1234567890' + } + } + }); +export type PresignChatFileGetUrlParams = z.infer & { + outLinkAuthData?: OutLinkChatAuthType; +}; + +export const PresignChatFilePostUrlSchema = z + .object({ + filename: z.string().min(1), + appId: ObjectIdSchema, + chatId: ObjectIdSchema, + outLinkAuthData: OutLinkChatAuthSchema.optional() + }) + .meta({ + description: '获取上传对话文件预签名 URL', + example: { + filename: '1234567890', + appId: '1234567890', + chatId: '1234567890', + outLinkAuthData: { + shareId: '1234567890', + outLinkUid: '1234567890' + } + } + }); +export type PresignChatFilePostUrlParams = z.infer & { + outLinkAuthData?: OutLinkChatAuthType; +}; + +export const UpdateChatFeedbackSchema = z + .object({ + appId: z.string().min(1), + chatId: z.string().min(1), + dataId: z.string().min(1), + userBadFeedback: z.string().optional(), + userGoodFeedback: z.string().optional() + }) + .meta({ + description: '更新对话反馈', + example: { + appId: '1234567890', + chatId: '1234567890', + dataId: '1234567890', + userBadFeedback: '1234567890', + userGoodFeedback: '1234567890' + } + }); +export type UpdateChatFeedbackProps = z.infer; diff --git a/packages/global/core/chat/type.d.ts b/packages/global/core/chat/type.d.ts index 3a69f7d91923..bf8eea596659 100644 --- a/packages/global/core/chat/type.d.ts +++ b/packages/global/core/chat/type.d.ts @@ -51,16 +51,18 @@ export type ChatWithAppSchema = Omit & { }; /* --------- chat item ---------- */ +export type UserChatItemFileItemType = { + type: `${ChatFileTypeEnum}`; + name?: string; + key?: string; + url: string; +}; export type UserChatItemValueItemType = { type: ChatItemValueTypeEnum.text | ChatItemValueTypeEnum.file; text?: { content: string; }; - file?: { - type: `${ChatFileTypeEnum}`; - name?: string; - url: string; - }; + file?: UserChatItemFileItemType; }; export type UserChatItemType = { obj: ChatRoleEnum.Human; diff --git a/packages/global/core/chat/utils.ts b/packages/global/core/chat/utils.ts index e6fcfb5300b5..259184d14334 100644 --- a/packages/global/core/chat/utils.ts +++ b/packages/global/core/chat/utils.ts @@ -171,7 +171,8 @@ export const removeEmptyUserInput = (input?: UserChatItemValueItemType[]) => { if (item.type === ChatItemValueTypeEnum.text && !item.text?.content?.trim()) { return false; } - if (item.type === ChatItemValueTypeEnum.file && !item.file?.url) { + // type 为 'file' 时 key 和 url 不能同时为空 + if (item.type === ChatItemValueTypeEnum.file && !item.file?.key && !item.file?.url) { return false; } return true; @@ -204,7 +205,7 @@ export const getChatSourceByPublishChannel = (publishChannel: PublishChannelEnum } }; -/* +/* Merge chat responseData 1. Same tool mergeSignId (Interactive tool node) 2. Recursively merge plugin details with same mergeSignId diff --git a/packages/global/core/plugin/admin/tool/type.ts b/packages/global/core/plugin/admin/tool/type.ts new file mode 100644 index 000000000000..724299f03896 --- /dev/null +++ b/packages/global/core/plugin/admin/tool/type.ts @@ -0,0 +1,38 @@ +import { ParentIdSchema } from '../../../../common/parentFolder/type'; +import { SystemToolBasicConfigSchema, ToolSecretInputItemSchema } from '../../tool/type'; +import z from 'zod'; + +export const AdminSystemToolListItemSchema = SystemToolBasicConfigSchema.extend({ + id: z.string(), + parentId: ParentIdSchema, + name: z.string(), + intro: z.string().optional(), + author: z.string().optional(), + avatar: z.string().optional(), + tags: z.array(z.string()).nullish(), + + hasSystemSecret: z.boolean().optional(), + + // App tool + associatedPluginId: z.string().optional(), + + isFolder: z.boolean().optional(), + hasSecretInput: z.boolean() +}); +export type AdminSystemToolListItemType = z.infer; + +// Child config schema for update +export const ToolsetChildSchema = z.object({ + pluginId: z.string(), + name: z.string(), + systemKeyCost: z.number().optional() +}); +export const AdminSystemToolDetailSchema = AdminSystemToolListItemSchema.omit({ + hasSecretInput: true +}).extend({ + userGuide: z.string().nullish(), + inputList: z.array(ToolSecretInputItemSchema).optional(), + inputListVal: z.record(z.string(), z.any()).nullish(), + childTools: z.array(ToolsetChildSchema).optional() +}); +export type AdminSystemToolDetailType = z.infer; diff --git a/packages/global/core/plugin/schema/type.ts b/packages/global/core/plugin/schema/type.ts new file mode 100644 index 000000000000..5318ccf46e77 --- /dev/null +++ b/packages/global/core/plugin/schema/type.ts @@ -0,0 +1,9 @@ +import z from 'zod'; + +export const TeamInstalledPluginSchema = z.object({ + _id: z.string(), + teamId: z.string(), + pluginId: z.string(), + installed: z.boolean() +}); +export type TeamInstalledPluginSchemaType = z.infer; diff --git a/packages/global/core/plugin/tool/type.ts b/packages/global/core/plugin/tool/type.ts new file mode 100644 index 000000000000..2c0b294c1755 --- /dev/null +++ b/packages/global/core/plugin/tool/type.ts @@ -0,0 +1,57 @@ +import z from 'zod'; +import { PluginStatusEnum, PluginStatusSchema } from '../type'; + +// 无论哪种 Tool,都会有这一层配置 +export const SystemToolBasicConfigSchema = z.object({ + defaultInstalled: z.boolean().optional(), + status: PluginStatusSchema.optional().default(PluginStatusEnum.Normal), + originCost: z.number().optional(), + currentCost: z.number().optional(), + hasTokenFee: z.boolean().optional(), + systemKeyCost: z.number().optional(), + pluginOrder: z.number().optional() +}); + +export const SystemPluginToolCollectionSchema = SystemToolBasicConfigSchema.extend({ + pluginId: z.string(), + customConfig: z + .object({ + name: z.string(), + avatar: z.string().optional(), + intro: z.string().optional(), + toolDescription: z.string().optional(), + version: z.string(), + toolTags: z.array(z.string()).nullish(), + associatedPluginId: z.string().optional(), + userGuide: z.string().optional(), + author: z.string().optional() + }) + .optional(), + inputListVal: z.record(z.string(), z.any()).optional(), + + // @deprecated + isActive: z.boolean().optional(), + inputConfig: z + .array( + z.object({ + key: z.string(), + label: z.string(), + description: z.string().optional(), + value: z.any().optional() + }) + ) + .optional() +}); +export type SystemPluginToolCollectionType = z.infer; + +// TODO: 移动到 plugin sdk 里 +export const ToolSecretInputItemSchema = z.object({ + key: z.string(), + label: z.string(), + description: z.string().optional(), + required: z.boolean().optional(), + inputType: z.enum(['input', 'numberInput', 'secret', 'switch', 'select']), + value: z.any().optional(), + list: z.array(z.object({ label: z.string(), value: z.string() })).optional() +}); +export type ToolSecretInputItemType = z.infer; diff --git a/packages/global/core/plugin/type.ts b/packages/global/core/plugin/type.ts new file mode 100644 index 000000000000..87e658b53988 --- /dev/null +++ b/packages/global/core/plugin/type.ts @@ -0,0 +1,37 @@ +import { z } from 'zod'; +import { i18nT } from '../../../web/i18n/utils'; + +export const I18nStringSchema = z.object({ + en: z.string(), + 'zh-CN': z.string().optional(), + 'zh-Hant': z.string().optional() +}); +// I18nStringType can be either an object with language keys or a plain string +export const I18nUnioStringSchema = z.union([I18nStringSchema, z.string()]); + +export const PluginToolTagSchema = z.object({ + tagId: z.string(), + tagName: I18nUnioStringSchema, + tagOrder: z.number(), + isSystem: z.boolean() +}); +export type SystemPluginToolTagType = z.infer; + +export const PluginStatusSchema = z.union([z.literal(1), z.literal(2), z.literal(3)]); +export type PluginStatusType = z.infer; +export enum PluginStatusEnum { + Normal = 1, + SoonOffline = 2, + Offline = 3 +} +export const PluginStatusMap = { + [PluginStatusEnum.Normal]: { + label: i18nT('app:toolkit_status_normal') + }, + [PluginStatusEnum.SoonOffline]: { + label: i18nT('app:toolkit_status_soon_offline') + }, + [PluginStatusEnum.Offline]: { + label: i18nT('app:toolkit_status_offline') + } +}; diff --git a/packages/global/core/workflow/constants.ts b/packages/global/core/workflow/constants.ts index 166d6dd74457..fd5625d38fd7 100644 --- a/packages/global/core/workflow/constants.ts +++ b/packages/global/core/workflow/constants.ts @@ -328,7 +328,7 @@ export enum VariableInputEnum { password = 'password', file = 'file', - modelSelect = 'modelSelect', + llmSelect = 'llmSelect', datasetSelect = 'datasetSelect', custom = 'custom', @@ -386,20 +386,26 @@ export const variableConfigs: VariableConfigType[][] = [ label: i18nT('common:core.workflow.inputType.switch'), value: VariableInputEnum.switch, defaultValueType: WorkflowIOValueTypeEnum.boolean + }, + { + icon: 'core/workflow/inputType/timePointSelect', + label: i18nT('common:core.workflow.inputType.timePointSelect'), + value: VariableInputEnum.timePointSelect, + defaultValueType: WorkflowIOValueTypeEnum.string + }, + { + icon: 'core/workflow/inputType/timeRangeSelect', + label: i18nT('common:core.workflow.inputType.timeRangeSelect'), + value: VariableInputEnum.timeRangeSelect, + defaultValueType: WorkflowIOValueTypeEnum.arrayString + }, + { + icon: 'core/workflow/inputType/model', + label: i18nT('common:core.workflow.inputType.modelSelect'), + value: VariableInputEnum.llmSelect, + defaultValueType: WorkflowIOValueTypeEnum.string } // { - // icon: 'core/workflow/inputType/timePointSelect', - // label: i18nT('common:core.workflow.inputType.timePointSelect'), - // value: VariableInputEnum.timePointSelect, - // defaultValueType: WorkflowIOValueTypeEnum.string - // }, - // { - // icon: 'core/workflow/inputType/timeRangeSelect', - // label: i18nT('common:core.workflow.inputType.timeRangeSelect'), - // value: VariableInputEnum.timeRangeSelect, - // defaultValueType: WorkflowIOValueTypeEnum.arrayString - // } - // { // icon: 'core/workflow/inputType/file', // label: i18nT('common:core.workflow.inputType.file'), // value: VariableInputEnum.file, @@ -410,14 +416,14 @@ export const variableConfigs: VariableConfigType[][] = [ // { // icon: 'core/workflow/inputType/model', // label: i18nT('common:core.workflow.inputType.modelSelect'), - // value: VariableInputEnum.modelSelect, + // value: VariableInputEnum.llmSelect, // defaultValueType: WorkflowIOValueTypeEnum.string // }, // { // icon: 'core/workflow/inputType/dataset', // label: i18nT('common:core.workflow.inputType.datasetSelect'), // value: VariableInputEnum.datasetSelect, - // defaultValueType: WorkflowIOValueTypeEnum.arrayString + // defaultValueType: WorkflowIOValueTypeEnum.selectDataset // } // ], [ diff --git a/packages/global/core/workflow/node/constant.ts b/packages/global/core/workflow/node/constant.ts index 27e5eedd1824..84b3de4a5ecf 100644 --- a/packages/global/core/workflow/node/constant.ts +++ b/packages/global/core/workflow/node/constant.ts @@ -13,11 +13,9 @@ export enum FlowNodeInputTypeEnum { // render ui JSONEditor = 'JSONEditor', addInputParam = 'addInputParam', // params input + customVariable = 'customVariable', // 外部变量 - // special input selectApp = 'selectApp', - customVariable = 'customVariable', - // ai model select selectLLMModel = 'selectLLMModel', settingLLMModel = 'settingLLMModel', @@ -28,7 +26,7 @@ export enum FlowNodeInputTypeEnum { // render ui settingDatasetQuotePrompt = 'settingDatasetQuotePrompt', hidden = 'hidden', - custom = 'custom', + custom = 'custom', // 自定义渲染 fileSelect = 'fileSelect', timePointSelect = 'timePointSelect', diff --git a/packages/global/core/workflow/runtime/type.d.ts b/packages/global/core/workflow/runtime/type.d.ts index 0d200873827c..7702817f7c2c 100644 --- a/packages/global/core/workflow/runtime/type.d.ts +++ b/packages/global/core/workflow/runtime/type.d.ts @@ -30,6 +30,8 @@ import type { } from '../template/system/interactive/type'; import type { SearchDataResponseItemType } from '../../dataset/type'; import type { localeType } from '../../../common/i18n/type'; +import { type UserChatItemValueItemType } from '../../chat/type'; + export type ExternalProviderType = { openaiAccount?: OpenaiAccountType; externalWorkflowVariables?: Record; @@ -102,7 +104,7 @@ export type SystemVariablesType = { export type RuntimeNodeItemType = { nodeId: StoreNodeItemType['nodeId']; name: StoreNodeItemType['name']; - avatar: StoreNodeItemType['avatar']; + avatar?: StoreNodeItemType['avatar']; intro?: StoreNodeItemType['intro']; toolDescription?: StoreNodeItemType['toolDescription']; flowNodeType: StoreNodeItemType['flowNodeType']; diff --git a/packages/global/core/workflow/template/input.ts b/packages/global/core/workflow/template/input.ts index 357437f2ab95..192019f9c3be 100644 --- a/packages/global/core/workflow/template/input.ts +++ b/packages/global/core/workflow/template/input.ts @@ -52,7 +52,8 @@ export const Input_Template_SettingAiModel: FlowNodeInputItemType = { export const Input_Template_System_Prompt: FlowNodeInputItemType = { key: NodeInputKeyEnum.aiSystemPrompt, renderTypeList: [FlowNodeInputTypeEnum.textarea, FlowNodeInputTypeEnum.reference], - max: 3000, + maxLength: 100000, + isRichText: true, valueType: WorkflowIOValueTypeEnum.string, label: i18nT('common:core.ai.Prompt'), description: systemPromptTip, diff --git a/packages/global/core/workflow/template/system/assignedAnswer.ts b/packages/global/core/workflow/template/system/assignedAnswer.ts index f8fb4484106f..48c3f2f1a519 100644 --- a/packages/global/core/workflow/template/system/assignedAnswer.ts +++ b/packages/global/core/workflow/template/system/assignedAnswer.ts @@ -24,6 +24,8 @@ export const AssignedAnswerModule: FlowNodeTemplateType = { renderTypeList: [FlowNodeInputTypeEnum.textarea, FlowNodeInputTypeEnum.reference], valueType: WorkflowIOValueTypeEnum.any, required: true, + isRichText: false, + maxLength: 100000, label: i18nT('common:core.module.input.label.Response content'), description: i18nT('common:core.module.input.description.Response content'), placeholder: i18nT('common:core.module.input.description.Response content') diff --git a/packages/global/core/workflow/type/index.d.ts b/packages/global/core/workflow/type/index.d.ts index a9f34ad0a45a..0938f02cead0 100644 --- a/packages/global/core/workflow/type/index.d.ts +++ b/packages/global/core/workflow/type/index.d.ts @@ -34,7 +34,7 @@ export type WorkflowTemplateType = { parentId?: ParentIdType; isFolder?: boolean; - avatar: string; + avatar?: string; name: I18nStringType | string; intro?: I18nStringType | string; toolDescription?: string; diff --git a/packages/global/core/workflow/type/io.d.ts b/packages/global/core/workflow/type/io.d.ts index 382b212da267..d2fdbdf0ee3c 100644 --- a/packages/global/core/workflow/type/io.d.ts +++ b/packages/global/core/workflow/type/io.d.ts @@ -15,9 +15,18 @@ export type CustomFieldConfigType = { showDescription?: boolean; }; export type InputComponentPropsType = { + key: `${NodeInputKeyEnum}` | string; + label: string; + + valueType?: WorkflowIOValueTypeEnum; // data type + required?: boolean; + defaultValue?: any; + referencePlaceholder?: string; + isRichText?: boolean; placeholder?: string; // input,textarea maxLength?: number; // input,textarea + minLength?: number; // password list?: { label: string; value: string }[]; // select @@ -27,12 +36,29 @@ export type InputComponentPropsType = { min?: number; // slider, number input precision?: number; // number input - defaultValue?: string; - llmModelType?: `${LLMModelTypeEnum}`; + // file + canSelectFile?: boolean; + canSelectImg?: boolean; + canSelectVideo?: boolean; + canSelectAudio?: boolean; + canSelectCustomFileExtension?: boolean; + customFileExtensionList?: string[]; + canLocalUpload?: boolean; + canUrlUpload?: boolean; + maxFiles?: number; + + // Time + timeGranularity?: 'day' | 'hour' | 'minute' | 'second'; + timeRangeStart?: string; + timeRangeEnd?: string; + // dynamic input customInputConfig?: CustomFieldConfigType; + + // @deprecated + enums?: { value: string; label: string }[]; }; export type InputConfigType = { key: string; @@ -50,30 +76,20 @@ export type FlowNodeInputItemType = InputComponentPropsType & { selectedTypeIndex?: number; renderTypeList: FlowNodeInputTypeEnum[]; // Node Type. Decide on a render style - key: `${NodeInputKeyEnum}` | string; - valueType?: WorkflowIOValueTypeEnum; // data type valueDesc?: string; // data desc value?: any; - label: string; debugLabel?: string; description?: string; // field desc - required?: boolean; - enum?: string; + toolDescription?: string; // If this field is not empty, it is entered as a tool + enum?: string; inputList?: InputConfigType[]; // when key === 'system_input_config', this field is used - toolDescription?: string; // If this field is not empty, it is entered as a tool - // render components params canEdit?: boolean; // dynamic inputs isPro?: boolean; // Pro version field isToolOutput?: boolean; - // file - canSelectFile?: boolean; - canSelectImg?: boolean; - maxFiles?: number; - deprecated?: boolean; }; diff --git a/packages/global/core/workflow/type/node.d.ts b/packages/global/core/workflow/type/node.d.ts index efc24aa418fe..d823b54df9e7 100644 --- a/packages/global/core/workflow/type/node.d.ts +++ b/packages/global/core/workflow/type/node.d.ts @@ -19,16 +19,13 @@ 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, - HttpToolConfigType -} from '../../app/type'; -import type { ParentIdType } from 'common/parentFolder/type'; +import type { AppDetailType, AppSchema, HttpToolConfigType } from '../../app/type'; +import type { McpToolConfigType } from '../../app/tool/mcpTool/type'; +import type { ParentIdType } from '../../../common/parentFolder/type'; import { AppTypeEnum } from '../../app/constants'; import type { WorkflowInteractiveResponseType } from '../template/system/interactive/type'; import type { StoreSecretValueType } from '../../../common/secret/type'; +import type { PluginStatusType } from '../../plugin/type'; export type NodeToolConfigType = { mcpToolSet?: { @@ -105,6 +102,8 @@ export type PluginDataType = { name?: string; avatar?: string; error?: string; + status?: PluginStatusType; + // toolTags?: string[]; }; type HandleType = { @@ -117,6 +116,7 @@ type HandleType = { export type FlowNodeTemplateType = FlowNodeCommonType & { id: string; // node id, unique templateType: string; + status?: PluginStatusType; showSourceHandle?: boolean; showTargetHandle?: boolean; @@ -131,9 +131,9 @@ export type FlowNodeTemplateType = FlowNodeCommonType & { diagram?: string; // diagram url courseUrl?: string; // course url userGuide?: string; // user guide + toolTags?: string[] | null; // @deprecated - // show handle sourceHandle?: HandleType; targetHandle?: HandleType; }; @@ -143,7 +143,8 @@ export type NodeTemplateListItemType = { flowNodeType: FlowNodeTypeEnum; // render node card parentId?: ParentIdType; isFolder?: boolean; - templateType: string; + templateType?: string; + toolTags?: string[] | null; avatar?: string; name: string; intro?: string; // template list intro diff --git a/packages/global/core/workflow/utils.ts b/packages/global/core/workflow/utils.ts index 99b61225e7d7..8060a4db1f54 100644 --- a/packages/global/core/workflow/utils.ts +++ b/packages/global/core/workflow/utils.ts @@ -39,7 +39,6 @@ import { defaultWhisperConfig } from '../app/constants'; import { IfElseResultEnum } from './template/system/ifElse/constant'; -import { type RuntimeNodeItemType } from './runtime/type'; import { Input_Template_File_Link, Input_Template_History, @@ -51,7 +50,6 @@ import { type RuntimeUserPromptType, type UserChatItemType } from '../../core/ch import { getNanoid } from '../../common/string/tools'; import { ChatRoleEnum } from '../../core/chat/constants'; import { runtimePrompt2ChatsValue } from '../../core/chat/adapt'; -import { getPluginRunContent } from '../../core/app/plugin/utils'; export const getHandleId = ( nodeId: string, @@ -262,7 +260,7 @@ export const appData2FlowNodeIO = ({ [VariableInputEnum.switch]: [FlowNodeInputTypeEnum.switch], [VariableInputEnum.password]: [FlowNodeInputTypeEnum.password], [VariableInputEnum.file]: [FlowNodeInputTypeEnum.fileSelect], - [VariableInputEnum.modelSelect]: [FlowNodeInputTypeEnum.selectLLMModel], + [VariableInputEnum.llmSelect]: [FlowNodeInputTypeEnum.selectLLMModel], [VariableInputEnum.datasetSelect]: [FlowNodeInputTypeEnum.selectDataset], [VariableInputEnum.internal]: [FlowNodeInputTypeEnum.hidden], [VariableInputEnum.custom]: [FlowNodeInputTypeEnum.input, FlowNodeInputTypeEnum.reference] @@ -385,43 +383,8 @@ export const getElseIFLabel = (i: number) => { return i === 0 ? IfElseResultEnum.IF : `${IfElseResultEnum.ELSE_IF} ${i}`; }; -// add value to plugin input node when run plugin -export const updatePluginInputByVariables = ( - nodes: RuntimeNodeItemType[], - variables: Record -) => { - return nodes.map((node) => - node.flowNodeType === FlowNodeTypeEnum.pluginInput - ? { - ...node, - inputs: node.inputs.map((input) => { - const parseValue = (() => { - try { - if ( - input.valueType === WorkflowIOValueTypeEnum.string || - input.valueType === WorkflowIOValueTypeEnum.number || - input.valueType === WorkflowIOValueTypeEnum.boolean - ) - return variables[input.key]; - - return JSON.parse(variables[input.key]); - } catch (e) { - return variables[input.key]; - } - })(); - - return { - ...input, - value: parseValue ?? input.value - }; - }) - } - : node - ); -}; - /* Get plugin runtime input user query */ -export const getPluginRunUserQuery = ({ +export const clientGetWorkflowToolRunUserQuery = ({ pluginInputs, variables, files = [] @@ -430,6 +393,25 @@ export const getPluginRunUserQuery = ({ variables: Record; files?: RuntimeUserPromptType['files']; }): UserChatItemType & { dataId: string } => { + const getPluginRunContent = ({ + pluginInputs, + variables + }: { + pluginInputs: FlowNodeInputItemType[]; + variables: Record; + }) => { + const pluginInputsWithValue = pluginInputs.map((input) => { + const { key } = input; + let value = variables?.hasOwnProperty(key) ? variables[key] : input.defaultValue; + + return { + ...input, + value + }; + }); + return JSON.stringify(pluginInputsWithValue); + }; + return { dataId: getNanoid(24), obj: ChatRoleEnum.Human, diff --git a/packages/global/openapi/api.ts b/packages/global/openapi/api.ts new file mode 100644 index 000000000000..c8be011d3f41 --- /dev/null +++ b/packages/global/openapi/api.ts @@ -0,0 +1,7 @@ +import { z } from 'zod'; + +export const PaginationSchema = z.object({ + pageSize: z.union([z.number(), z.string()]), + offset: z.union([z.number(), z.string()]).optional(), + pageNum: z.union([z.number(), z.string()]).optional() +}); diff --git a/packages/global/openapi/core/chat/favourite/index.ts b/packages/global/openapi/core/chat/favourite/index.ts index 5d3990bda17c..3e6aad5911bf 100644 --- a/packages/global/openapi/core/chat/favourite/index.ts +++ b/packages/global/openapi/core/chat/favourite/index.ts @@ -7,13 +7,14 @@ import { UpdateFavouriteAppTagsParamsSchema } from './api'; import { ObjectIdSchema } from '../../../../common/type/mongo'; +import { TagsMap } from '../../../tag'; export const ChatFavouriteAppPath: OpenAPIPath = { '/proApi/core/chat/setting/favourite/list': { get: { summary: '获取精选应用列表', description: '获取团队配置的精选应用列表,支持按名称和标签筛选', - tags: ['对话页配置'], + tags: [TagsMap.chatSetting], requestParams: { query: GetChatFavouriteListParamsSchema }, @@ -33,7 +34,7 @@ export const ChatFavouriteAppPath: OpenAPIPath = { post: { summary: '更新精选应用', description: '批量创建或更新精选应用配置,包括应用 ID、标签和排序信息', - tags: ['对话页配置'], + tags: [TagsMap.chatSetting], requestBody: { content: { 'application/json': { @@ -57,7 +58,7 @@ export const ChatFavouriteAppPath: OpenAPIPath = { put: { summary: '更新精选应用排序', description: '批量更新精选应用的显示顺序', - tags: ['对话页配置'], + tags: [TagsMap.chatSetting], requestBody: { content: { 'application/json': { @@ -89,7 +90,7 @@ export const ChatFavouriteAppPath: OpenAPIPath = { put: { summary: '更新精选应用标签', description: '批量更新精选应用的标签分类', - tags: ['对话页配置'], + tags: [TagsMap.chatSetting], requestBody: { content: { 'application/json': { @@ -113,7 +114,7 @@ export const ChatFavouriteAppPath: OpenAPIPath = { delete: { summary: '删除精选应用', description: '根据 ID 删除指定的精选应用配置', - tags: ['对话页配置'], + tags: [TagsMap.chatSetting], requestParams: { query: z.object({ id: ObjectIdSchema diff --git a/packages/global/openapi/core/chat/index.ts b/packages/global/openapi/core/chat/index.ts index cefa5df2f42f..b977869820ba 100644 --- a/packages/global/openapi/core/chat/index.ts +++ b/packages/global/openapi/core/chat/index.ts @@ -1,7 +1,61 @@ +import type { OpenAPIPath } from '../../type'; import { ChatSettingPath } from './setting'; import { ChatFavouriteAppPath } from './favourite/index'; +import { z } from 'zod'; +import { CreatePostPresignedUrlResultSchema } from '../../../../service/common/s3/type'; +import { PresignChatFileGetUrlSchema, PresignChatFilePostUrlSchema } from '../../../core/chat/api'; +import { TagsMap } from '../../tag'; -export const ChatPath = { +export const ChatPath: OpenAPIPath = { ...ChatSettingPath, - ...ChatFavouriteAppPath + ...ChatFavouriteAppPath, + + '/core/chat/presignChatFileGetUrl': { + post: { + summary: '获取对话文件预签名 URL', + description: '获取对话文件的预签名 URL', + tags: [TagsMap.chatPage], + requestBody: { + content: { + 'application/json': { + schema: PresignChatFileGetUrlSchema + } + } + }, + responses: { + 200: { + description: '成功获取对话文件预签名 URL', + content: { + 'application/json': { + schema: z.string() + } + } + } + } + } + }, + '/core/chat/presignChatFilePostUrl': { + post: { + summary: '上传对话文件预签名 URL', + description: '上传对话文件的预签名 URL', + tags: [TagsMap.chatPage], + requestBody: { + content: { + 'application/json': { + schema: PresignChatFilePostUrlSchema + } + } + }, + responses: { + 200: { + description: '成功上传对话文件预签名 URL', + content: { + 'application/json': { + schema: CreatePostPresignedUrlResultSchema + } + } + } + } + } + } }; diff --git a/packages/global/openapi/core/chat/setting/index.ts b/packages/global/openapi/core/chat/setting/index.ts index 8abd322ad43f..a3db7bbcea9e 100644 --- a/packages/global/openapi/core/chat/setting/index.ts +++ b/packages/global/openapi/core/chat/setting/index.ts @@ -1,5 +1,6 @@ import type { OpenAPIPath } from '../../../type'; import { ChatSettingSchema, ChatSettingModelSchema } from '../../../../core/chat/setting/type'; +import { TagsMap } from '../../../tag'; export const ChatSettingPath: OpenAPIPath = { '/proApi/core/chat/setting/detail': { @@ -7,7 +8,7 @@ export const ChatSettingPath: OpenAPIPath = { summary: '获取对话页设置', description: '获取当前团队的对话页设置,包括 slogan、对话提示、Logo、快捷应用、已选工具和精选应用标签等配置信息', - tags: ['对话页配置'], + tags: [TagsMap.chatSetting], responses: { 200: { description: '成功返回对话页设置信息', @@ -25,7 +26,7 @@ export const ChatSettingPath: OpenAPIPath = { summary: '更新对话页设置', description: '更新团队的对话页设置配置,包括 slogan、对话提示、Logo、快捷应用、已选工具和精选应用标签等信息', - tags: ['对话页配置'], + tags: [TagsMap.chatSetting], requestBody: { content: { 'application/json': { diff --git a/packages/global/openapi/core/plugin/admin/api.ts b/packages/global/openapi/core/plugin/admin/api.ts new file mode 100644 index 000000000000..399ef7c979e2 --- /dev/null +++ b/packages/global/openapi/core/plugin/admin/api.ts @@ -0,0 +1,52 @@ +import { I18nStringSchema, I18nUnioStringSchema } from '../../../../core/plugin/type'; +import { z } from 'zod'; + +/* ============ Pkg Plugin ============== */ +// 1. Get Pkg Plugin Upload URL Schema +export const GetPkgPluginUploadURLQuerySchema = z.object({ + filename: z.string() +}); +export type GetPkgPluginUploadURLQueryType = z.infer; +export const GetPkgPluginUploadURLResponseSchema = z.object({ + postURL: z.string(), + formData: z.record(z.string(), z.string()), + objectName: z.string() +}); +export type GetPkgPluginUploadURLResponseType = z.infer; + +// 2. Parse Uploaded Pkg Plugin Schema +export const ParseUploadedPkgPluginQuerySchema = z.object({ + objectName: z.string() +}); +export type ParseUploadedPkgPluginQueryType = z.infer; +export const ParseUploadedPkgPluginResponseSchema = z.array( + z.object({ + toolId: z.string(), + name: I18nUnioStringSchema, + description: I18nStringSchema, + icon: z.string(), + parentId: z.string().optional(), + tags: z.array(z.string()).nullish() + }) +); +export type ParseUploadedPkgPluginResponseType = z.infer< + typeof ParseUploadedPkgPluginResponseSchema +>; + +// 3. Confirm Uploaded Pkg Plugin Schema +export const ConfirmUploadPkgPluginBodySchema = z.object({ + toolIds: z.array(z.string()) +}); +export type ConfirmUploadPkgPluginBodyType = z.infer; + +// 4. Delete Pkg Plugin Schema +export const DeletePkgPluginQuerySchema = z.object({ + toolId: z.string() +}); +export type DeletePkgPluginQueryType = z.infer; + +// Install plugin from url +export const InstallPluginFromUrlBodySchema = z.object({ + downloadUrls: z.array(z.string()) +}); +export type InstallPluginFromUrlBodyType = z.infer; diff --git a/packages/global/openapi/core/plugin/admin/index.ts b/packages/global/openapi/core/plugin/admin/index.ts new file mode 100644 index 000000000000..1e1368b01122 --- /dev/null +++ b/packages/global/openapi/core/plugin/admin/index.ts @@ -0,0 +1,127 @@ +import type { OpenAPIPath } from '../../../type'; +import { + GetPkgPluginUploadURLQuerySchema, + GetPkgPluginUploadURLResponseSchema, + ParseUploadedPkgPluginQuerySchema, + ParseUploadedPkgPluginResponseSchema, + ConfirmUploadPkgPluginBodySchema, + DeletePkgPluginQuerySchema, + InstallPluginFromUrlBodySchema +} from './api'; +import { TagsMap } from '../../../tag'; +import { z } from 'zod'; +import { AdminPluginToolPath } from './tool'; + +export const PluginAdminPath: OpenAPIPath = { + ...AdminPluginToolPath, + + // Pkg Plugin + '/core/plugin/admin/pkg/presign': { + get: { + summary: '获取插件包上传预签名URL', + description: '获取插件包上传到存储服务的预签名URL,需要系统管理员权限', + tags: [TagsMap.pluginAdmin], + requestParams: { + query: GetPkgPluginUploadURLQuerySchema + }, + responses: { + 200: { + description: '成功获取上传URL', + content: { + 'application/json': { + schema: GetPkgPluginUploadURLResponseSchema + } + } + } + } + } + }, + '/core/plugin/admin/pkg/parse': { + get: { + summary: '解析已上传的插件包', + description: '解析已上传的插件包,返回插件包中包含的工具信息,需要系统管理员权限', + tags: [TagsMap.pluginAdmin], + requestParams: { + query: ParseUploadedPkgPluginQuerySchema + }, + responses: { + 200: { + description: '成功解析插件包', + content: { + 'application/json': { + schema: ParseUploadedPkgPluginResponseSchema + } + } + } + } + } + }, + '/core/plugin/admin/pkg/confirm': { + post: { + summary: '确认上传插件包', + description: '确认上传插件包,将解析的工具添加到系统中,需要系统管理员权限', + tags: [TagsMap.pluginAdmin], + requestBody: { + content: { + 'application/json': { + schema: ConfirmUploadPkgPluginBodySchema + } + } + }, + responses: { + 200: { + description: '成功确认上传', + content: { + 'application/json': { + schema: z.object({}) + } + } + } + } + } + }, + '/core/plugin/admin/pkg/delete': { + delete: { + summary: '删除插件包', + description: '删除指定的插件包工具,需要系统管理员权限', + tags: [TagsMap.pluginAdmin], + requestParams: { + query: DeletePkgPluginQuerySchema + }, + responses: { + 200: { + description: '成功删除插件包', + content: { + 'application/json': { + schema: z.object({}) + } + } + } + } + } + }, + '/core/plugin/admin/installWithUrl': { + post: { + summary: '从URL安装插件', + description: '从URL安装插件,需要系统管理员权限', + tags: [TagsMap.pluginAdmin], + requestBody: { + content: { + 'application/json': { + schema: InstallPluginFromUrlBodySchema + } + } + }, + responses: { + 200: { + description: '成功安装插件', + content: { + 'application/json': { + schema: z.object({}) + } + } + } + } + } + } +}; diff --git a/packages/global/openapi/core/plugin/admin/tool/api.ts b/packages/global/openapi/core/plugin/admin/tool/api.ts new file mode 100644 index 000000000000..a6a3b46935c2 --- /dev/null +++ b/packages/global/openapi/core/plugin/admin/tool/api.ts @@ -0,0 +1,87 @@ +import type { AdminSystemToolDetailSchema } from '../../../../../core/plugin/admin/tool/type'; +import { + AdminSystemToolListItemSchema, + ToolsetChildSchema +} from '../../../../../core/plugin/admin/tool/type'; +import z from 'zod'; +import { ParentIdSchema } from '../../../../../common/parentFolder/type'; +import { PluginStatusSchema } from '../../../../../core/plugin/type'; + +// Admin tool list +export const GetAdminSystemToolsQuery = z.object({ + parentId: ParentIdSchema +}); +export type GetAdminSystemToolsQueryType = z.infer; +export const GetAdminSystemToolsResponseSchema = z.array(AdminSystemToolListItemSchema); +export type GetAdminSystemToolsResponseType = z.infer; + +// Admin tool detail +export const GetAdminSystemToolDetailQuerySchema = z.object({ + toolId: z.string() +}); +export type GetAdminSystemToolDetailQueryType = z.infer; +export type GetAdminSystemToolDetailResponseType = z.infer; + +// Update Tool Order Schema +export const UpdateToolOrderBodySchema = z.object({ + plugins: z.array( + z.object({ + pluginId: z.string(), + pluginOrder: z.number() + }) + ) +}); +export type UpdateToolOrderBodyType = z.infer; + +// Update system tool Schema +const UpdateChildToolSchema = ToolsetChildSchema.omit({ + name: true +}); +export const UpdateToolBodySchema = z.object({ + pluginId: z.string(), + status: PluginStatusSchema.optional(), + defaultInstalled: z.boolean().optional(), + originCost: z.number().optional(), + currentCost: z.number().nullish(), + systemKeyCost: z.number().optional(), + hasTokenFee: z.boolean().optional(), + inputListVal: z.record(z.string(), z.any()).nullish(), + childTools: z.array(UpdateChildToolSchema).optional(), + + // App tool fields + name: z.string().optional(), + avatar: z.string().optional(), + intro: z.string().optional(), + tagIds: z.array(z.string()).nullish(), + associatedPluginId: z.string().optional(), + userGuide: z.string().nullish(), + author: z.string().optional() +}); +export type UpdateToolBodyType = z.infer; + +// Delete system Tool +export const DeleteSystemToolQuerySchema = z.object({ + toolId: z.string() +}); +export type DeleteSystemToolQueryType = z.infer; + +/* ======= App type tool ====== */ +// Get all system plugin apps +export const GetAllSystemAppsBodySchema = z.object({ + searchKey: z.string().optional() +}); +export type GetAllSystemAppsBodyType = z.infer; +export const GetAllSystemAppsResponseSchema = z.array( + z.object({ + _id: z.string(), + avatar: z.string(), + name: z.string() + }) +); +export type GetAllSystemAppTypeToolsResponse = z.infer; + +// Create app type tool +export const CreateAppToolBodySchema = UpdateToolBodySchema.omit({ + childTools: true +}); +export type CreateAppToolBodyType = z.infer; diff --git a/packages/global/openapi/core/plugin/admin/tool/index.ts b/packages/global/openapi/core/plugin/admin/tool/index.ts new file mode 100644 index 000000000000..5c459070141b --- /dev/null +++ b/packages/global/openapi/core/plugin/admin/tool/index.ts @@ -0,0 +1,177 @@ +import type { OpenAPIPath } from '../../../../type'; +import { + CreateAppToolBodySchema, + DeleteSystemToolQuerySchema, + GetAdminSystemToolDetailQuerySchema, + GetAdminSystemToolsQuery, + GetAdminSystemToolsResponseSchema, + GetAllSystemAppsBodySchema, + GetAllSystemAppsResponseSchema, + UpdateToolBodySchema, + UpdateToolOrderBodySchema +} from './api'; +import { TagsMap } from '../../../../tag'; +import { z } from 'zod'; +import { AdminSystemToolDetailSchema } from '../../../../../core/plugin/admin/tool/type'; +import { SystemToolTagPath } from './tag'; + +export const AdminPluginToolPath: OpenAPIPath = { + '/core/plugin/admin/tool/list': { + get: { + summary: '获取系统工具列表', + description: '获取系统工具列表,需要系统管理员权限', + tags: [TagsMap.pluginToolAdmin], + requestParams: { + query: GetAdminSystemToolsQuery + }, + responses: { + '200': { + description: '成功获取系统工具列表', + content: { + 'application/json': { + schema: GetAdminSystemToolsResponseSchema + } + } + } + } + } + }, + '/core/plugin/admin/tool/detail': { + get: { + summary: '获取系统工具详情', + description: '获取系统工具详情,需要系统管理员权限', + tags: [TagsMap.pluginToolAdmin], + requestParams: { + query: GetAdminSystemToolDetailQuerySchema + }, + responses: { + '200': { + description: '成功获取系统工具详情', + content: { + 'application/json': { + schema: AdminSystemToolDetailSchema + } + } + } + } + } + }, + '/core/plugin/admin/tool/update': { + put: { + summary: '更新系统工具', + description: + '更新系统工具配置,包括基础字段和自定义字段,支持递归更新子配置,需要系统管理员权限', + tags: [TagsMap.pluginToolAdmin], + requestBody: { + content: { + 'application/json': { + schema: UpdateToolBodySchema + } + } + }, + responses: { + 200: { + description: '成功更新系统工具', + content: { + 'application/json': { + schema: z.object({}) + } + } + } + } + } + }, + '/core/plugin/admin/tool/updateOrder': { + put: { + summary: '更新系统工具顺序', + description: '批量更新系统工具的排序顺序,需要系统管理员权限', + tags: [TagsMap.pluginToolAdmin], + requestBody: { + content: { + 'application/json': { + schema: UpdateToolOrderBodySchema + } + } + }, + responses: { + 200: { + description: '成功更新系统工具顺序', + content: { + 'application/json': { + schema: z.object({}) + } + } + } + } + } + }, + '/core/plugin/admin/tool/delete': { + delete: { + summary: '删除系统工具', + description: '根据ID删除系统工具,需要系统管理员权限', + tags: [TagsMap.pluginToolAdmin], + requestParams: { + query: DeleteSystemToolQuerySchema + }, + responses: { + 200: { + description: '成功删除系统工具', + content: { + 'application/json': { + schema: z.object({}) + } + } + } + } + } + }, + '/core/plugin/admin/tool/app/systemApps': { + post: { + summary: '获取所有系统工具类型应用', + description: '获取所有系统工具类型应用,用于选择系统上的应用作为系统工具。需要系统管理员权限', + tags: [TagsMap.pluginToolAdmin], + requestBody: { + content: { + 'application/json': { + schema: GetAllSystemAppsBodySchema + } + } + }, + responses: { + 200: { + description: '成功获取系统工具类型应用', + content: { + 'application/json': { + schema: GetAllSystemAppsResponseSchema + } + } + } + } + } + }, + '/core/plugin/admin/tool/app/create': { + post: { + summary: '将系统应用设置成系统工具', + description: '将系统应用设置成系统工具,需要系统管理员权限', + tags: [TagsMap.pluginToolAdmin], + requestBody: { + content: { + 'application/json': { + schema: CreateAppToolBodySchema + } + } + }, + responses: { + 200: { + description: '成功将系统应用设置成系统工具', + content: { + 'application/json': { + schema: z.object({}) + } + } + } + } + } + }, + ...SystemToolTagPath +}; diff --git a/packages/global/openapi/core/plugin/admin/tool/tag/api.ts b/packages/global/openapi/core/plugin/admin/tool/tag/api.ts new file mode 100644 index 000000000000..c67b064e59b7 --- /dev/null +++ b/packages/global/openapi/core/plugin/admin/tool/tag/api.ts @@ -0,0 +1,23 @@ +import { PluginToolTagSchema } from '../../../../../../core/plugin/type'; +import { z } from 'zod'; + +export const CreatePluginToolTagBodySchema = z.object({ + tagName: z.string() +}); +export type CreatePluginToolTagBody = z.infer; + +export const DeletePluginToolTagQuerySchema = z.object({ + tagId: z.string() +}); +export type DeletePluginToolTagQuery = z.infer; + +export const UpdatePluginToolTagBodySchema = z.object({ + tagId: z.string(), + tagName: z.string() +}); +export type UpdatePluginToolTagBody = z.infer; + +export const UpdatePluginToolTagOrderBodySchema = z.object({ + tags: z.array(PluginToolTagSchema) +}); +export type UpdatePluginToolTagOrderBody = z.infer; diff --git a/packages/global/openapi/core/plugin/admin/tool/tag/index.ts b/packages/global/openapi/core/plugin/admin/tool/tag/index.ts new file mode 100644 index 000000000000..cc1f5f94d32d --- /dev/null +++ b/packages/global/openapi/core/plugin/admin/tool/tag/index.ts @@ -0,0 +1,104 @@ +import type { OpenAPIPath } from '../../../../../type'; +import { TagsMap } from '../../../../../tag'; +import { z } from 'zod'; +import { + CreatePluginToolTagBodySchema, + DeletePluginToolTagQuerySchema, + UpdatePluginToolTagBodySchema, + UpdatePluginToolTagOrderBodySchema +} from './api'; + +export const SystemToolTagPath: OpenAPIPath = { + '/core/plugin/toolTag/config/create': { + post: { + summary: '创建工具标签', + description: '创建新的工具标签,需要系统管理员权限', + tags: [TagsMap.pluginToolAdmin], + requestBody: { + content: { + 'application/json': { + schema: CreatePluginToolTagBodySchema + } + } + }, + responses: { + 200: { + description: '成功创建工具标签', + content: { + 'application/json': { + schema: z.object({}) + } + } + } + } + } + }, + '/core/plugin/toolTag/config/delete': { + delete: { + summary: '删除工具标签', + description: '根据标签ID删除工具标签,需要系统管理员权限', + tags: [TagsMap.pluginToolAdmin], + requestParams: { + query: DeletePluginToolTagQuerySchema + }, + responses: { + 200: { + description: '成功删除工具标签', + content: { + 'application/json': { + schema: z.object({}) + } + } + } + } + } + }, + '/core/plugin/toolTag/config/update': { + put: { + summary: '更新工具标签', + description: '更新工具标签的名称,需要系统管理员权限', + tags: [TagsMap.pluginToolAdmin], + requestBody: { + content: { + 'application/json': { + schema: UpdatePluginToolTagBodySchema + } + } + }, + responses: { + 200: { + description: '成功更新工具标签', + content: { + 'application/json': { + schema: z.object({}) + } + } + } + } + } + }, + '/core/plugin/toolTag/config/updateOrder': { + put: { + summary: '更新工具标签顺序', + description: '批量更新工具标签的排序顺序,需要系统管理员权限', + tags: [TagsMap.pluginToolAdmin], + requestBody: { + content: { + 'application/json': { + schema: UpdatePluginToolTagOrderBodySchema + } + } + }, + responses: { + 200: { + description: '成功更新工具标签顺序', + content: { + 'application/json': { + schema: z.object({}) + } + } + } + } + } + } +}; diff --git a/packages/global/openapi/core/plugin/index.ts b/packages/global/openapi/core/plugin/index.ts new file mode 100644 index 000000000000..6510f71f02c0 --- /dev/null +++ b/packages/global/openapi/core/plugin/index.ts @@ -0,0 +1,12 @@ +import type { OpenAPIPath } from '../../type'; +import { MarketplacePath } from './marketplace'; +import { PluginToolTagPath } from './toolTag'; +import { PluginAdminPath } from './admin'; +import { PluginTeamPath } from './team'; + +export const PluginPath: OpenAPIPath = { + ...MarketplacePath, + ...PluginToolTagPath, + ...PluginAdminPath, + ...PluginTeamPath +}; diff --git a/packages/global/openapi/core/plugin/marketplace/api.ts b/packages/global/openapi/core/plugin/marketplace/api.ts new file mode 100644 index 000000000000..56e5f5dfe073 --- /dev/null +++ b/packages/global/openapi/core/plugin/marketplace/api.ts @@ -0,0 +1,64 @@ +import { z } from 'zod'; +import { type ToolSimpleType } from '../../../../sdk/fastgpt-plugin'; +import { PaginationSchema } from '../../../api'; +import { PluginToolTagSchema } from '../../../../core/plugin/type'; + +const formatToolDetailSchema = z.object({}); +const formatToolSimpleSchema = z.object({}); + +// Create intersection types for extended schemas +export const MarketplaceToolListItemSchema = formatToolSimpleSchema.extend({ + downloadUrl: z.string() +}); +export type MarketplaceToolListItemType = ToolSimpleType & { + downloadUrl: string; +}; + +export const MarketplaceToolDetailItemSchema = formatToolDetailSchema.extend({ + readme: z.string().optional() +}); +export const MarketplaceToolDetailSchema = z.object({ + tools: z.array(MarketplaceToolDetailItemSchema), + downloadUrl: z.string() +}); + +// List +export const GetMarketplaceToolsBodySchema = PaginationSchema.extend({ + searchKey: z.string().optional(), + tags: z.array(z.string()).nullish() +}); +export type GetMarketplaceToolsBodyType = z.infer; + +export const MarketplaceToolsResponseSchema = z.object({ + total: z.number(), + list: z.array(MarketplaceToolListItemSchema) +}); +export type MarketplaceToolsResponseType = z.infer; + +// Detail +export const GetMarketplaceToolDetailQuerySchema = z.object({ + toolId: z.string() +}); +export type GetMarketplaceToolDetailQueryType = z.infer; + +export type GetMarketplaceToolDetailResponseType = z.infer; + +// Tags +export const GetMarketplaceToolTagsResponseSchema = z.array(PluginToolTagSchema); +export type GetMarketplaceToolTagsResponseType = z.infer< + typeof GetMarketplaceToolTagsResponseSchema +>; + +// Get installed plugins +export const GetSystemInstalledPluginsQuerySchema = z.object({ + type: z.enum(['tool']).optional() +}); +export type GetSystemInstalledPluginsQueryType = z.infer< + typeof GetSystemInstalledPluginsQuerySchema +>; +export const GetSystemInstalledPluginsResponseSchema = z.object({ + ids: z.array(z.string()) +}); +export type GetSystemInstalledPluginsResponseType = z.infer< + typeof GetSystemInstalledPluginsResponseSchema +>; diff --git a/packages/global/openapi/core/plugin/marketplace/index.ts b/packages/global/openapi/core/plugin/marketplace/index.ts new file mode 100644 index 000000000000..23242634da51 --- /dev/null +++ b/packages/global/openapi/core/plugin/marketplace/index.ts @@ -0,0 +1,87 @@ +import { type OpenAPIPath } from '../../../type'; +import { + GetMarketplaceToolDetailQuerySchema, + GetMarketplaceToolsBodySchema, + MarketplaceToolDetailSchema, + MarketplaceToolsResponseSchema, + GetMarketplaceToolTagsResponseSchema, + GetSystemInstalledPluginsQuerySchema, + GetSystemInstalledPluginsResponseSchema +} from './api'; +import { TagsMap } from '../../../tag'; + +export const MarketplacePath: OpenAPIPath = { + '/core/plugin/admin/marketplace/installed': { + get: { + summary: '获取系统已安装插件的 ID 列表(管理员视角)', + tags: [TagsMap.pluginMarketplace], + requestParams: { + query: GetSystemInstalledPluginsQuerySchema + }, + responses: { + 200: { + description: '获取系统已安装插件的 ID 列表成功', + content: { + 'application/json': { + schema: GetSystemInstalledPluginsResponseSchema + } + } + } + } + } + }, + '/marketplace/api/tool/list': { + get: { + summary: '获取工具列表', + tags: [TagsMap.pluginMarketplace], + requestParams: { + query: GetMarketplaceToolsBodySchema + }, + responses: { + 200: { + description: '获取市场工具列表成功', + content: { + 'application/json': { + schema: MarketplaceToolsResponseSchema + } + } + } + } + } + }, + '/marketplace/api/tool/detail': { + get: { + summary: '获取单个工具详情', + tags: [TagsMap.pluginMarketplace], + requestParams: { + query: GetMarketplaceToolDetailQuerySchema + }, + responses: { + 200: { + description: '获取市场工具详情成功', + content: { + 'application/json': { + schema: MarketplaceToolDetailSchema + } + } + } + } + } + }, + '/marketplace/api/tool/tags': { + get: { + summary: '获取工具标签', + tags: [TagsMap.pluginMarketplace], + responses: { + 200: { + description: '获取市场工具标签成功', + content: { + 'application/json': { + schema: GetMarketplaceToolTagsResponseSchema + } + } + } + } + } + } +}; diff --git a/packages/global/openapi/core/plugin/team/api.ts b/packages/global/openapi/core/plugin/team/api.ts new file mode 100644 index 000000000000..157501f35082 --- /dev/null +++ b/packages/global/openapi/core/plugin/team/api.ts @@ -0,0 +1,27 @@ +import { PluginStatusEnum, PluginStatusSchema } from '../../../../core/plugin/type'; +import z from 'zod'; + +export const GetTeamSystemPluginListQuerySchema = z.object({ + type: z.enum(['tool']) +}); +export type GetTeamSystemPluginListQueryType = z.infer; +export const TeamPluginListItemSchema = z.object({ + id: z.string(), + avatar: z.string().optional(), + name: z.string(), + intro: z.string().optional(), + author: z.string().optional(), + tags: z.array(z.string()).nullish(), + status: PluginStatusSchema.optional().default(PluginStatusEnum.Normal), + installed: z.boolean(), + associatedPluginId: z.string().optional() +}); +export const GetTeamPluginListResponseSchema = z.array(TeamPluginListItemSchema); +export type GetTeamPluginListResponseType = z.infer; + +export const ToggleInstallPluginBodySchema = z.object({ + pluginId: z.string(), + type: z.enum(['tool']), + installed: z.boolean() +}); +export type ToggleInstallPluginBodyType = z.infer; diff --git a/packages/global/openapi/core/plugin/team/index.ts b/packages/global/openapi/core/plugin/team/index.ts new file mode 100644 index 000000000000..d76b7d7c84db --- /dev/null +++ b/packages/global/openapi/core/plugin/team/index.ts @@ -0,0 +1,43 @@ +import type { OpenAPIPath } from '../../../type'; +import { GetTeamPluginListResponseSchema, ToggleInstallPluginBodySchema } from './api'; +import { TagsMap } from '../../../tag'; + +export const PluginTeamPath: OpenAPIPath = { + '/core/plugin/team/list': { + get: { + summary: '获取团队插件列表', + description: '获取团队插件列表', + tags: [TagsMap.pluginTeam], + responses: { + 200: { + description: '获取团队插件列表成功', + content: { + 'application/json': { + schema: GetTeamPluginListResponseSchema + } + } + } + } + } + }, + '/core/plugin/team/toggleInstall': { + post: { + summary: '切换插件安装状态', + description: '切换团队插件的安装状态,支持安装或卸载插件', + tags: [TagsMap.pluginTeam], + requestBody: { + content: { + 'application/json': { + schema: ToggleInstallPluginBodySchema + } + } + }, + responses: { + 200: { + description: '请求成功', + content: {} + } + } + } + } +}; diff --git a/packages/global/openapi/core/plugin/toolTag/api.ts b/packages/global/openapi/core/plugin/toolTag/api.ts new file mode 100644 index 000000000000..7a597ef2dbb4 --- /dev/null +++ b/packages/global/openapi/core/plugin/toolTag/api.ts @@ -0,0 +1,5 @@ +import { PluginToolTagSchema } from '../../../../core/plugin/type'; +import { z } from 'zod'; + +export const GetPluginToolTagsResponseSchema = z.array(PluginToolTagSchema); +export type GetPluginTagListResponse = z.infer; diff --git a/packages/global/openapi/core/plugin/toolTag/index.ts b/packages/global/openapi/core/plugin/toolTag/index.ts new file mode 100644 index 000000000000..1fc265f574d4 --- /dev/null +++ b/packages/global/openapi/core/plugin/toolTag/index.ts @@ -0,0 +1,24 @@ +import type { OpenAPIPath } from '../../../type'; +import { GetPluginToolTagsResponseSchema } from './api'; +import { TagsMap } from '../../../tag'; +import { z } from 'zod'; + +export const PluginToolTagPath: OpenAPIPath = { + '/core/plugin/toolTag/list': { + get: { + summary: '获取工具标签列表', + description: '获取所有工具标签列表,按排序顺序返回', + tags: [TagsMap.pluginToolTag], + responses: { + 200: { + description: '成功获取工具标签列表', + content: { + 'application/json': { + schema: GetPluginToolTagsResponseSchema + } + } + } + } + } + } +}; diff --git a/packages/global/openapi/index.ts b/packages/global/openapi/index.ts index 7c4ed9cb4347..323e3148f65f 100644 --- a/packages/global/openapi/index.ts +++ b/packages/global/openapi/index.ts @@ -1,6 +1,8 @@ import { createDocument } from 'zod-openapi'; import { ChatPath } from './core/chat'; import { ApiKeyPath } from './support/openapi'; +import { TagsMap } from './tag'; +import { PluginPath } from './core/plugin'; export const openAPIDocument = createDocument({ openapi: '3.1.0', @@ -11,7 +13,26 @@ export const openAPIDocument = createDocument({ }, paths: { ...ChatPath, - ...ApiKeyPath + ...ApiKeyPath, + ...PluginPath }, - servers: [{ url: '/api' }] + servers: [{ url: '/api' }], + 'x-tagGroups': [ + { + name: '对话', + tags: [TagsMap.chatSetting, TagsMap.chatPage] + }, + { + name: '插件相关', + tags: [TagsMap.pluginToolTag, TagsMap.pluginTeam] + }, + { + name: '插件-管理员', + tags: [TagsMap.pluginAdmin, TagsMap.pluginMarketplace, TagsMap.pluginToolAdmin] + }, + { + name: 'ApiKey', + tags: [TagsMap.apiKey] + } + ] }); diff --git a/packages/global/openapi/support/openapi/index.ts b/packages/global/openapi/support/openapi/index.ts index 5583328b11a8..84b9bf72e7bb 100644 --- a/packages/global/openapi/support/openapi/index.ts +++ b/packages/global/openapi/support/openapi/index.ts @@ -1,12 +1,13 @@ import { z } from 'zod'; -import { formatSuccessResponse, getErrorResponse, type OpenAPIPath } from '../../type'; +import { type OpenAPIPath } from '../../type'; import { ApiKeyHealthParamsSchema, ApiKeyHealthResponseSchema } from './api'; +import { TagsMap } from '../../tag'; export const ApiKeyPath: OpenAPIPath = { '/support/openapi/health': { get: { summary: '检查 API Key 是否健康', - tags: ['APIKey'], + tags: [TagsMap.apiKey], requestParams: { query: ApiKeyHealthParamsSchema }, diff --git a/packages/global/openapi/tag.ts b/packages/global/openapi/tag.ts new file mode 100644 index 000000000000..ddc4d8ce6d41 --- /dev/null +++ b/packages/global/openapi/tag.ts @@ -0,0 +1,10 @@ +export const TagsMap = { + chatPage: '对话页', + chatSetting: '对话页配置', + pluginMarketplace: '插件市场(管理员视角)', + pluginToolTag: '工具标签', + pluginAdmin: '管理员插件管理', + pluginToolAdmin: '管理员系统工具管理', + pluginTeam: '团队插件管理', + apiKey: 'APIKey' +}; diff --git a/packages/global/package.json b/packages/global/package.json index 70c0757c18c7..3e5ec84ddeda 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.19", + "@fastgpt-sdk/plugin": "0.2.13", "@apidevtools/swagger-parser": "^10.1.0", "@bany/curl-to-json": "^1.2.8", "axios": "^1.12.1", diff --git a/packages/global/support/permission/chat/type.ts b/packages/global/support/permission/chat/type.ts new file mode 100644 index 000000000000..026f7b00e016 --- /dev/null +++ b/packages/global/support/permission/chat/type.ts @@ -0,0 +1,29 @@ +import { z } from 'zod'; + +export const OutLinkChatAuthSchema = z.union([ + z + .object({ + shareId: z.string().optional(), + outLinkUid: z.string().optional() + }) + .meta({ + description: '分享链接鉴权', + example: { + shareId: '1234567890', + outLinkUid: '1234567890' + } + }), + z + .object({ + teamId: z.string().optional(), + teamToken: z.string().optional() + }) + .meta({ + description: '团队鉴权', + example: { + teamId: '1234567890', + teamToken: '1234567890' + } + }) +]); +export type OutLinkChatAuthType = z.infer; diff --git a/packages/service/common/api/pagination.ts b/packages/service/common/api/pagination.ts index f8b2149d97e1..b2d1118f86df 100644 --- a/packages/service/common/api/pagination.ts +++ b/packages/service/common/api/pagination.ts @@ -1,7 +1,7 @@ import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { type ApiRequestProps } from '../../type/next'; -export function parsePaginationRequest(req: ApiRequestProps) { +export const parsePaginationRequest = (req: ApiRequestProps) => { const { pageSize = 10, pageNum = 1, @@ -18,4 +18,4 @@ export function parsePaginationRequest(req: ApiRequestProps) { pageSize: Number(pageSize), offset: offset ? Number(offset) : (Number(pageNum) - 1) * Number(pageSize) }; -} +}; diff --git a/packages/service/common/bullmq/index.ts b/packages/service/common/bullmq/index.ts index ddd6f2b8f224..17c66bd0af1b 100644 --- a/packages/service/common/bullmq/index.ts +++ b/packages/service/common/bullmq/index.ts @@ -21,6 +21,7 @@ const defaultWorkerOpts: Omit = { export enum QueueNames { datasetSync = 'datasetSync', evaluation = 'evaluation', + s3FileDelete = 's3FileDelete', // abondoned websiteSync = 'websiteSync' } diff --git a/packages/service/common/cache/init.ts b/packages/service/common/cache/init.ts index bcc25ad867af..555e63bcf1ce 100644 --- a/packages/service/common/cache/init.ts +++ b/packages/service/common/cache/init.ts @@ -1,5 +1,5 @@ import { SystemCacheKeyEnum } from './type'; -import { refreshSystemTools } from '../../core/app/plugin/controller'; +import { refreshSystemTools } from '../../core/app/tool/controller'; export const initCache = () => { global.systemCache = { diff --git a/packages/service/common/cache/type.ts b/packages/service/common/cache/type.ts index 5aae88139e2b..9f3d51b05e78 100644 --- a/packages/service/common/cache/type.ts +++ b/packages/service/common/cache/type.ts @@ -1,4 +1,4 @@ -import type { SystemPluginTemplateItemType } from '@fastgpt/global/core/app/plugin/type'; +import type { AppToolTemplateItemType } from '@fastgpt/global/core/app/tool/type'; export enum SystemCacheKeyEnum { systemTool = 'systemTool', @@ -6,7 +6,7 @@ export enum SystemCacheKeyEnum { } export type SystemCacheDataType = { - [SystemCacheKeyEnum.systemTool]: SystemPluginTemplateItemType[]; + [SystemCacheKeyEnum.systemTool]: AppToolTemplateItemType[]; [SystemCacheKeyEnum.modelPermission]: null; }; diff --git a/packages/service/common/mongo/init.ts b/packages/service/common/mongo/init.ts index 4fc57d42ec9c..c67a395e2288 100644 --- a/packages/service/common/mongo/init.ts +++ b/packages/service/common/mongo/init.ts @@ -53,20 +53,20 @@ export async function connectMongo(props: { } catch (error) {} }); - const options = { + await db.connect(url, { bufferCommands: true, - maxConnecting: maxConnecting, - maxPoolSize: maxConnecting, - minPoolSize: 20, - connectTimeoutMS: 60000, - waitQueueTimeoutMS: 60000, - socketTimeoutMS: 60000, - maxIdleTimeMS: 300000, - retryWrites: true, - retryReads: true - }; - - await db.connect(url, options); + maxConnecting: maxConnecting, // 最大连接数: 防止连接数过多时无法满足需求 + maxPoolSize: maxConnecting, // 最大连接池大小: 防止连接池过大时无法满足需求 + minPoolSize: 20, // 最小连接数: 20,防止连接数过少时无法满足需求 + connectTimeoutMS: 60000, // 连接超时: 60秒,防止连接失败时长时间阻塞 + waitQueueTimeoutMS: 60000, // 等待队列超时: 60秒,防止等待队列长时间阻塞 + socketTimeoutMS: 60000, // Socket 超时: 60秒,防止Socket连接失败时长时间阻塞 + maxIdleTimeMS: 300000, // 空闲连接超时: 5分钟,防止空闲连接长时间占用资源 + retryWrites: true, // 重试写入: 重试写入失败的操作 + retryReads: true, // 重试读取: 重试读取失败的操作 + serverSelectionTimeoutMS: 10000, // 服务器选择超时: 10秒,防止副本集故障时长时间阻塞 + w: 'majority' // 写入确认策略: 多数节点确认后返回,保证数据安全性 + }); console.log('mongo connected'); connectedCb?.(); diff --git a/packages/service/common/s3/buckets/base.ts b/packages/service/common/s3/buckets/base.ts index 330abceed55a..4d11835849cf 100644 --- a/packages/service/common/s3/buckets/base.ts +++ b/packages/service/common/s3/buckets/base.ts @@ -1,16 +1,19 @@ -import { Client, type RemoveOptions, type CopyConditions } from 'minio'; +import { Client, type RemoveOptions, type CopyConditions, InvalidObjectNameError } from 'minio'; import { type CreatePostPresignedUrlOptions, type CreatePostPresignedUrlParams, type CreatePostPresignedUrlResult, - type S3OptionsType + type S3OptionsType, + type createPreviewUrlParams, + CreateGetPresignedUrlParamsSchema } from '../type'; -import { defaultS3Options, Mimes } from '../constants'; +import { defaultS3Options, getSystemMaxFileSize, Mimes } from '../constants'; import path from 'node:path'; import { MongoS3TTL } from '../schema'; import { getNanoid } from '@fastgpt/global/common/string/tools'; import { addHours } from 'date-fns'; import { addLog } from '../../system/log'; +import { addS3DelJob } from '../mq'; export class S3BaseBucket { private _client: Client; @@ -84,8 +87,27 @@ export class S3BaseBucket { return this.client.bucketExists(this.name); } - delete(objectKey: string, options?: RemoveOptions): Promise { - return this.client.removeObject(this.name, objectKey, options); + async delete(objectKey: string, options?: RemoveOptions): Promise { + try { + if (!objectKey) return Promise.resolve(); + return await this.client.removeObject(this.name, objectKey, options); + } catch (error) { + if (error instanceof InvalidObjectNameError) { + addLog.warn(`${this.name} delete object not found: ${objectKey}`, error); + return Promise.resolve(); + } + return Promise.reject(error); + } + } + + addDeleteJob({ prefix, key }: { prefix?: string; key?: string }): Promise { + return addS3DelJob({ prefix, key, bucketName: this.name }); + } + + listObjectsV2( + ...params: Parameters extends [string, ...infer R] ? R : never + ) { + return this.client.listObjectsV2(this.name, ...params); } async createPostPresignedUrl( @@ -93,11 +115,11 @@ export class S3BaseBucket { options: CreatePostPresignedUrlOptions = {} ): Promise { try { - const { expiredHours } = options; + const { expiredHours, maxFileSize = getSystemMaxFileSize() } = options; + const formatMaxFileSize = maxFileSize * 1024 * 1024; const filename = params.filename; const ext = path.extname(filename).toLowerCase(); const contentType = Mimes[ext as keyof typeof Mimes] ?? 'application/octet-stream'; - const maxFileSize = this.options.maxFileSize; const key = (() => { if ('rawKey' in params) return params.rawKey; @@ -109,8 +131,8 @@ export class S3BaseBucket { policy.setKey(key); policy.setBucket(this.name); policy.setContentType(contentType); - if (maxFileSize) { - policy.setContentLengthRange(1, maxFileSize); + if (formatMaxFileSize) { + policy.setContentLengthRange(1, formatMaxFileSize); } policy.setExpires(new Date(Date.now() + 10 * 60 * 1000)); policy.setUserMetaData({ @@ -131,11 +153,29 @@ export class S3BaseBucket { return { url: postURL, - fields: formData + fields: formData, + maxSize: formatMaxFileSize }; } catch (error) { addLog.error('Failed to create post presigned url', error); return Promise.reject('Failed to create post presigned url'); } } + + async createExtenalUrl(params: createPreviewUrlParams) { + const parsed = CreateGetPresignedUrlParamsSchema.parse(params); + + const { key, expiredHours } = parsed; + const expires = expiredHours ? expiredHours * 60 * 60 : 30 * 60; // expires 的单位是秒 默认 30 分钟 + + return await this.externalClient.presignedGetObject(this.name, key, expires); + } + async createPreviewlUrl(params: createPreviewUrlParams) { + const parsed = CreateGetPresignedUrlParamsSchema.parse(params); + + const { key, expiredHours } = parsed; + const expires = expiredHours ? expiredHours * 60 * 60 : 30 * 60; // expires 的单位是秒 默认 30 分钟 + + return await this.client.presignedGetObject(this.name, key, expires); + } } diff --git a/packages/service/common/s3/constants.ts b/packages/service/common/s3/constants.ts index 64458fa1e87d..c6ca270bce74 100644 --- a/packages/service/common/s3/constants.ts +++ b/packages/service/common/s3/constants.ts @@ -1,5 +1,3 @@ -import type { S3PrivateBucket } from './buckets/private'; -import type { S3PublicBucket } from './buckets/public'; import { HttpProxyAgent } from 'http-proxy-agent'; import { HttpsProxyAgent } from 'https-proxy-agent'; import type { ClientOptions } from 'minio'; @@ -29,11 +27,8 @@ export const Mimes = { export const defaultS3Options: { externalBaseURL?: string; - maxFileSize?: number; afterInit?: () => Promise | void; } & ClientOptions = { - maxFileSize: 1024 ** 3, // 1GB - useSSL: process.env.S3_USE_SSL === 'true', endPoint: process.env.S3_ENDPOINT || 'localhost', externalBaseURL: process.env.S3_EXTERNAL_BASE_URL, @@ -51,3 +46,8 @@ export const S3Buckets = { public: process.env.S3_PUBLIC_BUCKET || 'fastgpt-public', private: process.env.S3_PRIVATE_BUCKET || 'fastgpt-private' } as const; + +export const getSystemMaxFileSize = () => { + const config = global.feConfigs?.uploadFileMaxSize || 1024; // MB, default 1024MB + return config; // bytes +}; diff --git a/packages/service/common/s3/index.ts b/packages/service/common/s3/index.ts index f879e5eef838..6e6729efd6ff 100644 --- a/packages/service/common/s3/index.ts +++ b/packages/service/common/s3/index.ts @@ -1,5 +1,7 @@ import { S3PublicBucket } from './buckets/public'; import { S3PrivateBucket } from './buckets/private'; +import { addLog } from '../system/log'; +import { startS3DelWorker } from './mq'; export function initS3Buckets() { const publicBucket = new S3PublicBucket(); @@ -10,3 +12,8 @@ export function initS3Buckets() { [privateBucket.name]: privateBucket }; } + +export const initS3MQWorker = async () => { + addLog.info('Init S3 Delete Worker...'); + await startS3DelWorker(); +}; diff --git a/packages/service/common/s3/mq.ts b/packages/service/common/s3/mq.ts new file mode 100644 index 000000000000..9c6ac5a5ab84 --- /dev/null +++ b/packages/service/common/s3/mq.ts @@ -0,0 +1,77 @@ +import { getQueue, getWorker, QueueNames } from '../bullmq'; +import pLimit from 'p-limit'; +import { retryFn } from '@fastgpt/global/common/system/utils'; + +export type S3MQJobData = { + key?: string; + prefix?: string; + bucketName: string; +}; + +export const addS3DelJob = async (data: S3MQJobData): Promise => { + const queue = getQueue(QueueNames.s3FileDelete); + + await queue.add( + 'delete-s3-files', + { ...data }, + { + attempts: 3, + removeOnFail: false, + removeOnComplete: true, + backoff: { + delay: 2000, + type: 'exponential' + } + } + ); +}; +export const startS3DelWorker = async () => { + return getWorker( + QueueNames.s3FileDelete, + async (job) => { + const { prefix, bucketName, key } = job.data; + const limit = pLimit(10); + const tasks: Promise[] = []; + const bucket = s3BucketMap[bucketName]; + if (!bucket) { + return Promise.reject(`Bucket not found: ${bucketName}`); + } + + if (key) { + await bucket.delete(key); + } + if (prefix) { + return new Promise(async (resolve, reject) => { + const stream = bucket.listObjectsV2(prefix, true); + stream.on('data', async (file) => { + if (!file.name) return; + + const p = limit(() => retryFn(() => bucket.delete(file.name))); + tasks.push(p); + }); + + stream.on('end', async () => { + try { + const results = await Promise.allSettled(tasks); + const failed = results.filter((r) => r.status === 'rejected'); + if (failed.length > 0) { + reject('Some deletes failed'); + } + resolve(); + } catch (err) { + reject(err); + } + }); + + stream.on('error', (err) => { + console.error('listObjects stream error', err); + reject(err); + }); + }); + } + }, + { + concurrency: 1 + } + ); +}; diff --git a/packages/service/common/s3/sources/avatar.ts b/packages/service/common/s3/sources/avatar.ts index 9d1614da58f2..d8c3efb564fe 100644 --- a/packages/service/common/s3/sources/avatar.ts +++ b/packages/service/common/s3/sources/avatar.ts @@ -31,7 +31,10 @@ class S3AvatarSource { }) { return this.bucket.createPostPresignedUrl( { filename, teamId, source: S3Sources.avatar }, - { expiredHours: autoExpired ? 1 : undefined } // 1 Hourse + { + expiredHours: autoExpired ? 1 : undefined, // 1 Hours + maxFileSize: 5 // 5MB + } ); } diff --git a/packages/service/common/s3/sources/chat/index.ts b/packages/service/common/s3/sources/chat/index.ts new file mode 100644 index 000000000000..cbe0124a27ac --- /dev/null +++ b/packages/service/common/s3/sources/chat/index.ts @@ -0,0 +1,59 @@ +import { getNanoid } from '@fastgpt/global/common/string/tools'; +import { S3PrivateBucket } from '../../buckets/private'; +import { S3Sources } from '../../type'; +import { + type CheckChatFileKeys, + type DelChatFileByPrefixParams, + ChatFileUploadSchema, + DelChatFileByPrefixSchema +} from './type'; +import { MongoS3TTL } from '../../schema'; +import { addHours } from 'date-fns'; + +class S3ChatSource { + private bucket: S3PrivateBucket; + private static instance: S3ChatSource; + + constructor() { + this.bucket = new S3PrivateBucket(); + } + + static getInstance() { + return (this.instance ??= new S3ChatSource()); + } + + async createGetChatFileURL(params: { key: string; expiredHours?: number; external: boolean }) { + const { key, expiredHours = 1, external = false } = params; // 默认一个小时 + + if (external) { + return await this.bucket.createExtenalUrl({ key, expiredHours }); + } + return await this.bucket.createPreviewlUrl({ key, expiredHours }); + } + + async createUploadChatFileURL(params: CheckChatFileKeys) { + const { appId, chatId, uId, filename } = ChatFileUploadSchema.parse(params); + const rawKey = [S3Sources.chat, appId, uId, chatId, `${getNanoid(6)}-${filename}`].join('/'); + await MongoS3TTL.create({ + minioKey: rawKey, + bucketName: this.bucket.name, + expiredTime: addHours(new Date(), 24) + }); + return await this.bucket.createPostPresignedUrl({ rawKey, filename }); + } + + deleteChatFilesByPrefix(params: DelChatFileByPrefixParams) { + const { appId, chatId, uId } = DelChatFileByPrefixSchema.parse(params); + + const prefix = [S3Sources.chat, appId, uId, chatId].filter(Boolean).join('/'); + return this.bucket.addDeleteJob({ prefix }); + } + + deleteChatFileByKey(key: string) { + return this.bucket.addDeleteJob({ key }); + } +} + +export function getS3ChatSource() { + return S3ChatSource.getInstance(); +} diff --git a/packages/service/common/s3/sources/chat/type.ts b/packages/service/common/s3/sources/chat/type.ts new file mode 100644 index 000000000000..a306eb172f50 --- /dev/null +++ b/packages/service/common/s3/sources/chat/type.ts @@ -0,0 +1,17 @@ +import { z } from 'zod'; +import { ObjectIdSchema } from '@fastgpt/global/common/type/mongo'; + +export const ChatFileUploadSchema = z.object({ + appId: ObjectIdSchema, + chatId: z.string().length(24), + uId: z.string().nonempty(), + filename: z.string().nonempty() +}); +export type CheckChatFileKeys = z.infer; + +export const DelChatFileByPrefixSchema = z.object({ + appId: ObjectIdSchema, + chatId: z.string().length(24).optional(), + uId: z.string().nonempty().optional() +}); +export type DelChatFileByPrefixParams = z.infer; diff --git a/packages/service/common/s3/type.ts b/packages/service/common/s3/type.ts index 04cfdbc54115..885a0081742b 100644 --- a/packages/service/common/s3/type.ts +++ b/packages/service/common/s3/type.ts @@ -17,7 +17,7 @@ export type ExtensionType = keyof typeof Mimes; export type S3OptionsType = typeof defaultS3Options; -export const S3SourcesSchema = z.enum(['avatar']); +export const S3SourcesSchema = z.enum(['avatar', 'chat']); export const S3Sources = S3SourcesSchema.enum; export type S3SourceType = z.infer; @@ -37,16 +37,24 @@ export const CreatePostPresignedUrlParamsSchema = z.union([ export type CreatePostPresignedUrlParams = z.infer; export const CreatePostPresignedUrlOptionsSchema = z.object({ - expiredHours: z.number().positive().optional() // TTL in Hours, default 7 * 24 + expiredHours: z.number().positive().optional(), // TTL in Hours, default 7 * 24 + maxFileSize: z.number().positive().optional() // MB }); export type CreatePostPresignedUrlOptions = z.infer; export const CreatePostPresignedUrlResultSchema = z.object({ - url: z.string().min(1), - fields: z.record(z.string(), z.string()) + url: z.string().nonempty(), + fields: z.record(z.string(), z.string()), + maxSize: z.number().positive().optional() // bytes }); export type CreatePostPresignedUrlResult = z.infer; +export const CreateGetPresignedUrlParamsSchema = z.object({ + key: z.string().nonempty(), + expiredHours: z.number().positive().optional() +}); +export type createPreviewUrlParams = z.infer; + declare global { var s3BucketMap: { [key: string]: S3BaseBucket; diff --git a/packages/service/common/secret/utils.ts b/packages/service/common/secret/utils.ts index 8e65ff713788..f54fe0c7de6e 100644 --- a/packages/service/common/secret/utils.ts +++ b/packages/service/common/secret/utils.ts @@ -5,6 +5,7 @@ import { HeaderSecretTypeEnum } from '@fastgpt/global/common/secret/constants'; import { isSecretValue } from '../../../global/common/secret/utils'; export const encryptSecretValue = (value: SecretValueType): SecretValueType => { + if (typeof value !== 'object' || value === null) return value; if (!value.value) { return value; } @@ -51,7 +52,19 @@ export const getSecretValue = ({ }; export const anyValueDecrypt = (value: any) => { - if (!isSecretValue(value)) return value; + const val = (() => { + try { + return JSON.parse(value); + } catch (error) { + return value; + } + })(); + + if (typeof val === 'object' && val !== null && val.value) { + return val.value; + } + + if (!isSecretValue(val)) return val; - return decryptSecret(value.secret); + return decryptSecret(val.secret); }; diff --git a/packages/service/core/ai/llm/utils.ts b/packages/service/core/ai/llm/utils.ts index ef80ae6ebecd..6fe9efd60aee 100644 --- a/packages/service/core/ai/llm/utils.ts +++ b/packages/service/core/ai/llm/utils.ts @@ -12,6 +12,8 @@ import { ChatCompletionRequestMessageRoleEnum } from '@fastgpt/global/core/ai/co import { i18nT } from '../../../../web/i18n/utils'; import { addLog } from '../../../common/system/log'; import { getImageBase64 } from '../../../common/file/image/utils'; +import { getS3ChatSource } from '../../../common/s3/sources/chat'; +import { isInternalAddress } from '../../../common/system/utils'; export const filterGPTMessageByMaxContext = async ({ messages = [], @@ -80,7 +82,7 @@ export const filterGPTMessageByMaxContext = async ({ return [...systemPrompts, ...chats]; }; -/* +/* Format requested messages 1. If not useVision, only retain text. 2. Remove file_url @@ -150,12 +152,7 @@ export const loadRequestMessages = async ({ content.map(async (item) => { if (item.type === 'image_url') { // Remove url origin - const imgUrl = (() => { - if (origin && item.image_url.url.startsWith(origin)) { - return item.image_url.url.replace(origin, ''); - } - return item.image_url.url; - })(); + const imgUrl = item.image_url.url; // base64 image if (imgUrl.startsWith('data:image/')) { @@ -164,8 +161,23 @@ export const loadRequestMessages = async ({ try { // If imgUrl is a local path, load image from local, and set url to base64 - if (imgUrl.startsWith('/') || process.env.MULTIPLE_DATA_TO_BASE64 === 'true') { - const { completeBase64: base64 } = await getImageBase64(imgUrl); + if ( + imgUrl.startsWith('/') || + process.env.MULTIPLE_DATA_TO_BASE64 === 'true' || + isInternalAddress(imgUrl) + ) { + const url = await (async () => { + if (item.key) { + try { + return await getS3ChatSource().createGetChatFileURL({ + key: item.key, + external: false + }); + } catch (error) {} + } + return imgUrl; + })(); + const { completeBase64: base64 } = await getImageBase64(url); return { ...item, @@ -185,7 +197,7 @@ export const loadRequestMessages = async ({ return; } } catch (error: any) { - if (error?.response?.status === 405) { + if (error?.response?.status === 405 || error?.response?.status === 403) { return item; } addLog.warn(`Filter invalid image: ${imgUrl}`, { error }); diff --git a/packages/service/core/app/controller.ts b/packages/service/core/app/controller.ts index 4821b344d412..9039fda63622 100644 --- a/packages/service/core/app/controller.ts +++ b/packages/service/core/app/controller.ts @@ -1,11 +1,14 @@ import { type AppSchema } from '@fastgpt/global/core/app/type'; import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; -import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; +import { + FlowNodeInputTypeEnum, + FlowNodeTypeEnum +} from '@fastgpt/global/core/workflow/node/constant'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { MongoApp } from './schema'; import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node'; import { encryptSecretValue, storeSecretValue } from '../../common/secret/utils'; -import { SystemToolInputTypeEnum } from '@fastgpt/global/core/app/systemTool/constants'; +import { SystemToolSecretInputTypeEnum } from '@fastgpt/global/core/app/tool/systemTool/constants'; import { type ClientSession } from '../../common/mongo'; import { MongoEvaluation } from './evaluation/evalSchema'; import { removeEvaluationJob } from './evaluation/mq'; @@ -24,6 +27,7 @@ import { removeImageByPath } from '../../common/file/image/controller'; import { mongoSessionRun } from '../../common/mongo/sessionRun'; import { MongoAppLogKeys } from './logs/logkeysSchema'; import { MongoChatItemResponse } from '../chat/chatItemResponseSchema'; +import { getS3ChatSource } from '../../common/s3/sources/chat'; export const beforeUpdateAppFormat = ({ nodes }: { nodes?: StoreNodeItemType[] }) => { if (!nodes) return; @@ -34,11 +38,14 @@ export const beforeUpdateAppFormat = ({ nodes }: { nodes?: StoreNodeItemType[] } if (input.key === NodeInputKeyEnum.headerSecret && typeof input.value === 'object') { input.value = storeSecretValue(input.value); } + if (input.renderTypeList.includes(FlowNodeInputTypeEnum.password)) { + input.value = encryptSecretValue(input.value); + } if (input.key === NodeInputKeyEnum.systemInputConfig && typeof input.value === 'object') { input.inputList?.forEach((inputItem) => { if ( inputItem.inputType === 'secret' && - input.value?.type === SystemToolInputTypeEnum.manual && + input.value?.type === SystemToolSecretInputTypeEnum.manual && input.value?.value ) { input.value.value[inputItem.key] = encryptSecretValue(input.value.value[inputItem.key]); @@ -158,80 +165,84 @@ export const onDelOneApp = async ({ ).lean(); await Promise.all(evalJobs.map((evalJob) => removeEvaluationJob(evalJob._id))); + const del = async (app: AppSchema, session: ClientSession) => { + const appId = String(app._id); + + // 删除分享链接 + await MongoOutLink.deleteMany({ + appId + }).session(session); + // Openapi + await MongoOpenApi.deleteMany({ + appId + }).session(session); + + // delete version + await MongoAppVersion.deleteMany({ + appId + }).session(session); + + await MongoChatInputGuide.deleteMany({ + appId + }).session(session); + + // 删除精选应用记录 + await MongoChatFavouriteApp.deleteMany({ + teamId, + appId + }).session(session); + + // 从快捷应用中移除对应应用 + await MongoChatSetting.updateMany( + { teamId }, + { $pull: { quickAppIds: { id: String(appId) } } } + ).session(session); + + // Del permission + await MongoResourcePermission.deleteMany({ + resourceType: PerResourceTypeEnum.app, + teamId, + resourceId: appId + }).session(session); + + await MongoAppLogKeys.deleteMany({ + appId + }).session(session); + + // delete app + await MongoApp.deleteOne( + { + _id: appId + }, + { session } + ); + + await removeImageByPath(app.avatar, session); + }; + // Delete chats - await deleteChatFiles({ appId }); - await MongoChatItemResponse.deleteMany({ - appId - }); - await MongoChatItem.deleteMany({ - appId - }); - await MongoChat.deleteMany({ - appId - }); + for await (const app of apps) { + const appId = String(app._id); + await deleteChatFiles({ appId }); + await MongoChatItemResponse.deleteMany({ + appId + }); + await MongoChatItem.deleteMany({ + appId + }); + await MongoChat.deleteMany({ + appId + }); + await getS3ChatSource().deleteChatFilesByPrefix({ appId }); + } - const del = async (session: ClientSession) => { - for await (const app of apps) { - const appId = app._id; - - // 删除分享链接 - await MongoOutLink.deleteMany({ - appId - }).session(session); - // Openapi - await MongoOpenApi.deleteMany({ - appId - }).session(session); - - // delete version - await MongoAppVersion.deleteMany({ - appId - }).session(session); - - await MongoChatInputGuide.deleteMany({ - appId - }).session(session); - - // 删除精选应用记录 - await MongoChatFavouriteApp.deleteMany({ - teamId, - appId - }).session(session); - - // 从快捷应用中移除对应应用 - await MongoChatSetting.updateMany( - { teamId }, - { $pull: { quickAppIds: { id: String(appId) } } } - ).session(session); - - // Del permission - await MongoResourcePermission.deleteMany({ - resourceType: PerResourceTypeEnum.app, - teamId, - resourceId: appId - }).session(session); - - await MongoAppLogKeys.deleteMany({ - appId - }).session(session); - - // delete app - await MongoApp.deleteOne( - { - _id: appId - }, - { session } - ); - - await removeImageByPath(app.avatar, session); + for await (const app of apps) { + if (session) { + await del(app, session); + return deletedAppIds; } - }; - if (session) { - await del(session); + await mongoSessionRun((session) => del(app, session)); return deletedAppIds; } - - await mongoSessionRun(del); - return deletedAppIds; }; diff --git a/packages/service/core/app/mcp.ts b/packages/service/core/app/mcp.ts index 1fd37869d103..220a32b4362b 100644 --- a/packages/service/core/app/mcp.ts +++ b/packages/service/core/app/mcp.ts @@ -2,12 +2,12 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; import type { AppSchema } from '@fastgpt/global/core/app/type'; -import { type McpToolConfigType } from '@fastgpt/global/core/app/type'; +import { type McpToolConfigType } from '@fastgpt/global/core/app/tool/mcpTool/type'; import { addLog } from '../../common/system/log'; import { retryFn } from '@fastgpt/global/common/system/utils'; -import { PluginSourceEnum } from '@fastgpt/global/core/app/plugin/constants'; +import { AppToolSourceEnum } from '@fastgpt/global/core/app/tool/constants'; import { MongoApp } from './schema'; -import type { McpToolDataType } from '@fastgpt/global/core/app/mcpTools/type'; +import type { McpToolDataType } from '@fastgpt/global/core/app/tool/mcpTool/type'; import { UserError } from '@fastgpt/global/common/error/utils'; export class MCPClient { @@ -142,7 +142,7 @@ export const getMCPChildren = async (app: AppSchema) => { return ( app.modules[0].toolConfig?.mcpToolSet?.toolList.map((item) => ({ ...item, - id: `${PluginSourceEnum.mcp}-${id}/${item.name}`, + id: `${AppToolSourceEnum.mcp}-${id}/${item.name}`, avatar: app.avatar })) ?? [] ); @@ -159,7 +159,7 @@ export const getMCPChildren = async (app: AppSchema) => { return { avatar: app.avatar, - id: `${PluginSourceEnum.mcp}-${id}/${item.name}`, + id: `${AppToolSourceEnum.mcp}-${id}/${item.name}`, ...toolData }; }); diff --git a/packages/service/core/app/plugin/type.d.ts b/packages/service/core/app/plugin/type.d.ts deleted file mode 100644 index 5d117fc38a35..000000000000 --- a/packages/service/core/app/plugin/type.d.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { SystemPluginListItemType } from '@fastgpt/global/core/app/type'; -import { FlowNodeTemplateTypeEnum } from '@fastgpt/global/core/workflow/constants'; -import type { WorkflowTemplateBasicType } from '@fastgpt/global/core/workflow/type'; -import type { InputConfigType } from '@fastgpt/global/core/workflow/type/io'; -import type { I18nStringStrictType } from '@fastgpt/global/sdk/fastgpt-plugin'; - -export type SystemPluginConfigSchemaType = { - pluginId: string; - - originCost: number; // n points/one time - currentCost: number; - hasTokenFee: boolean; - isActive: boolean; - pluginOrder?: number; - systemKeyCost?: number; - - customConfig?: { - name: string; - avatar: string; - intro?: string; - toolDescription?: string; - version: string; - weight?: number; - templateType: string; - associatedPluginId: string; - userGuide: string; - author?: string; - }; - inputListVal?: Record; - - // @deprecated - inputConfig?: { - // Render config input form. Find the corresponding node and replace the variable directly - key: string; - label: string; - description: string; - value?: string; - }[]; -}; - -export type TGroupType = { - typeName: I18nStringStrictType | string; - typeId: string; -}; - -export type SystemToolGroupSchemaType = { - groupId: string; - groupAvatar: string; - groupName: string; - groupTypes: TGroupType[]; - groupOrder: number; -}; diff --git a/packages/service/core/app/templates/register.ts b/packages/service/core/app/templates/register.ts index 23daf81d171c..2a6d936b8b4d 100644 --- a/packages/service/core/app/templates/register.ts +++ b/packages/service/core/app/templates/register.ts @@ -1,5 +1,5 @@ import { isProduction } from '@fastgpt/global/common/system/constants'; -import { PluginSourceEnum } from '@fastgpt/global/core/app/plugin/constants'; +import { AppToolSourceEnum } from '@fastgpt/global/core/app/tool/constants'; import { type AppTemplateSchemaType } from '@fastgpt/global/core/app/type'; import { MongoAppTemplate } from './templateSchema'; import { pluginClient } from '../../../thirdProvider/fastgptPlugin'; @@ -61,7 +61,7 @@ export const getAppTemplatesAndLoadThem = async (refresh = false) => { }; export const isCommercialTemaplte = (templateId: string) => { - return templateId.startsWith(PluginSourceEnum.commercial); + return templateId.startsWith(AppToolSourceEnum.commercial); }; declare global { diff --git a/packages/service/core/app/tool/api.ts b/packages/service/core/app/tool/api.ts index 5890515bfb99..aacf9fb74058 100644 --- a/packages/service/core/app/tool/api.ts +++ b/packages/service/core/app/tool/api.ts @@ -1,6 +1,5 @@ -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 { AppToolSourceEnum } from '@fastgpt/global/core/app/tool/constants'; import { pluginClient, PLUGIN_BASE_URL, PLUGIN_TOKEN } from '../../../thirdProvider/fastgptPlugin'; import { addLog } from '../../../common/system/log'; import { retryFn } from '@fastgpt/global/common/system/utils'; @@ -12,12 +11,9 @@ export async function APIGetSystemToolList() { return res.body.map((item) => { return { ...item, - id: `${PluginSourceEnum.systemTool}-${item.id}`, - parentId: item.parentId ? `${PluginSourceEnum.systemTool}-${item.parentId}` : undefined, - avatar: - item.avatar && item.avatar.startsWith('/imgs/tools/') - ? `/api/system/plugin/tools/${item.avatar.replace('/imgs/tools/', '')}` - : item.avatar + id: `${AppToolSourceEnum.systemTool}-${item.toolId}`, + parentId: item.parentId ? `${AppToolSourceEnum.systemTool}-${item.parentId}` : undefined, + avatar: item.icon }; }); } @@ -31,15 +27,9 @@ const runToolInstance = new RunToolWithStream({ }); export const APIRunSystemTool = runToolInstance.run.bind(runToolInstance); -// Tool Types Cache -type SystemToolTypeItem = { - type: ToolTypeEnum; - name: I18nStringStrictType; -}; - -export const getSystemToolTypes = (): Promise => { +export const getSystemToolTags = () => { return retryFn(async () => { - const res = await pluginClient.tool.getType(); + const res = await pluginClient.tool.getTags(); if (res.status === 200) { const toolTypes = res.body || []; diff --git a/packages/service/core/app/plugin/controller.ts b/packages/service/core/app/tool/controller.ts similarity index 74% rename from packages/service/core/app/plugin/controller.ts rename to packages/service/core/app/tool/controller.ts index c4e3928749a4..394f90751e2f 100644 --- a/packages/service/core/app/plugin/controller.ts +++ b/packages/service/core/app/tool/controller.ts @@ -17,41 +17,44 @@ import { MongoApp } from '../schema'; import type { localeType } from '@fastgpt/global/common/i18n/type'; import { parseI18nString } from '@fastgpt/global/common/i18n/utils'; import type { WorkflowTemplateBasicType } from '@fastgpt/global/core/workflow/type'; -import { type SystemPluginTemplateItemType } from '@fastgpt/global/core/app/plugin/type'; import { checkIsLatestVersion, getAppLatestVersion, getAppVersionById } from '../version/controller'; -import { type PluginRuntimeType } from '@fastgpt/global/core/app/plugin/type'; -import { MongoSystemPlugin } from './systemPluginSchema'; +import type { + AppToolRuntimeType, + AppToolTemplateItemType +} from '@fastgpt/global/core/app/tool/type'; +import { MongoSystemTool } from '../../plugin/tool/systemToolSchema'; import { PluginErrEnum } from '@fastgpt/global/common/error/code/plugin'; -import { PluginSourceEnum } from '@fastgpt/global/core/app/plugin/constants'; +import { AppToolSourceEnum } from '@fastgpt/global/core/app/tool/constants'; import { FlowNodeTemplateTypeEnum, NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { getNanoid } from '@fastgpt/global/common/string/tools'; -import { APIGetSystemToolList } from '../tool/api'; +import { APIGetSystemToolList } from './api'; import { Types } from '../../../common/mongo'; -import type { SystemPluginConfigSchemaType } from './type'; +import type { SystemPluginToolCollectionType } from '@fastgpt/global/core/plugin/tool/type'; import type { FlowNodeInputItemType, FlowNodeOutputItemType } from '@fastgpt/global/core/workflow/type/io'; -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 { splitCombineToolId } from '@fastgpt/global/core/app/tool/utils'; +import { getMCPToolRuntimeNode } from '@fastgpt/global/core/app/tool/mcpTool/utils'; +import { getHTTPToolRuntimeNode } from '@fastgpt/global/core/app/tool/httpTool/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'; +import { PluginStatusEnum } from '@fastgpt/global/core/plugin/type'; +import { MongoTeamInstalledPlugin } from '../../plugin/schema/teamInstalledPluginSchema'; -type ChildAppType = SystemPluginTemplateItemType & { +type ChildAppType = AppToolTemplateItemType & { teamId?: string; tmbId?: string; workflow?: WorkflowTemplateBasicType; @@ -61,41 +64,87 @@ type ChildAppType = SystemPluginTemplateItemType & { export const getSystemTools = () => getCachedData(SystemCacheKeyEnum.systemTool); -export const getSystemPluginByIdAndVersionId = async ( +export const getSystemToolsWithInstalled = async ({ + teamId, + isRoot +}: { + teamId: string; + isRoot: boolean; +}) => { + const [tools, { installedSet, uninstalledSet }] = await Promise.all([ + getSystemTools(), + MongoTeamInstalledPlugin.find({ teamId, pluginType: 'tool' }, 'pluginId installed') + .lean() + .then((res) => { + const installedSet = new Set(); + const uninstalledSet = new Set(); + res.forEach((item) => { + if (item.installed) { + installedSet.add(item.pluginId); + } else { + uninstalledSet.add(item.pluginId); + } + }); + return { installedSet, uninstalledSet }; + }) + ]); + + return tools.map((tool) => { + const installed = (() => { + if (installedSet.has(tool.id)) { + return true; + } + if (isRoot && !uninstalledSet.has(tool.id)) { + return true; + } + if (tool.defaultInstalled && !uninstalledSet.has(tool.id)) { + return true; + } + return false; + })(); + + return { + ...tool, + installed + }; + }); +}; + +export const getSystemToolByIdAndVersionId = async ( pluginId: string, versionId?: string ): Promise => { - const plugin = await getSystemToolById(pluginId); + const tool = await getSystemToolById(pluginId); - // Admin selected system tool - if (plugin.associatedPluginId) { - // The verification plugin is set as a system plugin - const systemPlugin = await MongoSystemPlugin.findOne( - { pluginId: plugin.id, 'customConfig.associatedPluginId': plugin.associatedPluginId }, + // App type system tool + if (tool.associatedPluginId) { + // The verification tool is set as a system tool + const systemPlugin = await MongoSystemTool.findOne( + { pluginId: tool.id, 'customConfig.associatedPluginId': tool.associatedPluginId }, 'associatedPluginId' ).lean(); if (!systemPlugin) return Promise.reject(PluginErrEnum.unExist); - const app = await MongoApp.findById(plugin.associatedPluginId).lean(); + const app = await MongoApp.findById(tool.associatedPluginId).lean(); if (!app) return Promise.reject(PluginErrEnum.unExist); const version = versionId ? await getAppVersionById({ - appId: plugin.associatedPluginId, + appId: tool.associatedPluginId, versionId, app }) - : await getAppLatestVersion(plugin.associatedPluginId, app); + : await getAppLatestVersion(tool.associatedPluginId, app); if (!version.versionId) return Promise.reject(new UserError('App version not found')); const isLatest = version.versionId ? await checkIsLatestVersion({ - appId: plugin.associatedPluginId, + appId: tool.associatedPluginId, versionId: version.versionId }) : true; return { - ...plugin, + ...tool, workflow: { nodes: version.nodes, edges: version.edges, @@ -110,22 +159,22 @@ export const getSystemPluginByIdAndVersionId = async ( } // System toolset - if (plugin.isFolder) { + if (tool.isFolder) { return { - ...plugin, + ...tool, inputs: [], outputs: [], - inputList: plugin.inputList, + inputList: tool.inputList, version: '', isLatestVersion: true }; } // System tool - const versionList = (plugin.versionList as SystemPluginTemplateItemType['versionList']) || []; + const versionList = (tool.versionList as AppToolTemplateItemType['versionList']) || []; if (versionList.length === 0) { - return Promise.reject(new UserError('Can not find plugin version list')); + return Promise.reject(new UserError('Can not find tool version list')); } const version = versionId @@ -134,7 +183,7 @@ export const getSystemPluginByIdAndVersionId = async ( const lastVersion = versionList[0]; // concat parent (if exists) input config - const parent = plugin.parentId ? await getSystemToolById(plugin.parentId) : undefined; + const parent = tool.parentId ? await getSystemToolById(tool.parentId) : undefined; if (parent?.inputList) { version?.inputs?.unshift({ key: NodeInputKeyEnum.systemInputConfig, @@ -145,7 +194,7 @@ export const getSystemPluginByIdAndVersionId = async ( } return { - ...plugin, + ...tool, inputs: version.inputs ?? [], outputs: version.outputs ?? [], version: versionId ? version?.value : '', @@ -170,12 +219,12 @@ export async function getChildAppPreviewNode({ versionId?: string; lang?: localeType; }): Promise { - const { source, pluginId } = splitCombinePluginId(appId); + const { source, pluginId } = splitCombineToolId(appId); const app: ChildAppType = await (async () => { // 1. App // 2. MCP ToolSets - if (source === PluginSourceEnum.personal) { + if (source === AppToolSourceEnum.personal) { const item = await MongoApp.findById(pluginId).lean(); if (!item) return Promise.reject(PluginErrEnum.unExist); @@ -225,7 +274,7 @@ export async function getChildAppPreviewNode({ }; } // mcp tool - else if (source === PluginSourceEnum.mcp) { + else if (source === AppToolSourceEnum.mcp) { const [parentId, toolName] = pluginId.split('/'); // 1. get parentApp const item = await MongoApp.findById(parentId).lean(); @@ -266,7 +315,7 @@ export async function getChildAppPreviewNode({ }; } // http tool - else if (source === PluginSourceEnum.http) { + else if (source === AppToolSourceEnum.http) { const [parentId, toolName] = pluginId.split('/'); const item = await MongoApp.findById(parentId).lean(); if (!item) return Promise.reject(PluginErrEnum.unExist); @@ -307,7 +356,7 @@ export async function getChildAppPreviewNode({ // 1. System Tools // 2. System Plugins configured in Pro (has associatedPluginId) else { - return getSystemPluginByIdAndVersionId(pluginId, versionId); + return getSystemToolByIdAndVersionId(pluginId, versionId); } })(); @@ -321,7 +370,7 @@ export async function getChildAppPreviewNode({ showTargetHandle?: boolean; }; }> => { - if (source === PluginSourceEnum.systemTool) { + if (source === AppToolSourceEnum.systemTool) { // system Tool or Toolsets const children = app.isFolder ? (await getSystemTools()).filter((item) => item.parentId === pluginId) @@ -350,7 +399,7 @@ export async function getChildAppPreviewNode({ systemToolSet: { toolId: app.id, toolList: children - .filter((item) => item.isActive !== false) + .filter((item) => item.status === 1 || item.status === undefined) .map((item) => ({ toolId: item.id, name: parseI18nString(item.name, lang), @@ -407,7 +456,7 @@ export async function getChildAppPreviewNode({ return { id: getNanoid(), pluginId: app.id, - templateType: app.templateType, + templateType: app.templateType ?? FlowNodeTemplateTypeEnum.tools, flowNodeType, avatar: app.avatar, name: parseI18nString(app.name, lang), @@ -451,11 +500,11 @@ export async function getChildAppRuntimeById({ id: string; versionId?: string; lang?: localeType; -}): Promise { +}): Promise { const app = await (async () => { - const { source, pluginId } = splitCombinePluginId(id); + const { source, pluginId } = splitCombineToolId(id); - if (source === PluginSourceEnum.personal) { + if (source === AppToolSourceEnum.personal) { const item = await MongoApp.findById(pluginId).lean(); if (!item) return Promise.reject(PluginErrEnum.unExist); @@ -487,7 +536,7 @@ export async function getChildAppRuntimeById({ pluginOrder: 0 }; } else { - return getSystemPluginByIdAndVersionId(pluginId, versionId); + return getSystemToolByIdAndVersionId(pluginId, versionId); } })(); @@ -506,59 +555,60 @@ export async function getChildAppRuntimeById({ }; } -const dbPluginFormat = (item: SystemPluginConfigSchemaType): SystemPluginTemplateItemType => { - const { - name, - avatar, - intro, - toolDescription, - version, - weight, - templateType, - associatedPluginId, - userGuide - } = item.customConfig!; +/* FastsGPT-tool api: */ +export const refreshSystemTools = async (): Promise => { + const workflowToolFormat = (item: SystemPluginToolCollectionType): AppToolTemplateItemType => { + const { + name, + avatar, + intro, + toolDescription, + version, + associatedPluginId, + userGuide, + author = '', + toolTags + } = item.customConfig!; - return { - id: item.pluginId, - isActive: item.isActive, - isFolder: false, - parentId: null, - author: item.customConfig?.author || '', - version, - name, - avatar, - intro, - toolDescription, - weight, - templateType, - originCost: item.originCost, - currentCost: item.currentCost, - hasTokenFee: item.hasTokenFee, - pluginOrder: item.pluginOrder, - systemKeyCost: item.systemKeyCost, - associatedPluginId, - userGuide, - workflow: { - nodes: [], - edges: [] - } + return { + id: item.pluginId, + status: item.status ?? PluginStatusEnum.Normal, + defaultInstalled: item.defaultInstalled ?? false, + isFolder: false, + parentId: null, + author, + version, + name, + avatar, + intro, + toolDescription, + toolTags, + templateType: FlowNodeTemplateTypeEnum.tools, + originCost: item.originCost, + currentCost: item.currentCost, + hasTokenFee: item.hasTokenFee, + pluginOrder: item.pluginOrder, + systemKeyCost: item.systemKeyCost, + associatedPluginId, + userGuide, + workflow: { + nodes: [], + edges: [] + } + }; }; -}; -/* FastsGPT-Pluign api: */ -export const refreshSystemTools = async (): Promise => { const tools = await APIGetSystemToolList(); // 从数据库里加载插件配置进行替换 - const systemToolsArray = await MongoSystemPlugin.find({}).lean(); + const systemToolsArray = await MongoSystemTool.find({}).lean(); const systemTools = new Map(systemToolsArray.map((plugin) => [plugin.pluginId, plugin])); - const formatTools = tools.map((item) => { + 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']) || []; + const versionList = (item.versionList as AppToolTemplateItemType['versionList']) || []; return { id: item.id, @@ -571,16 +621,15 @@ export const refreshSystemTools = async (): Promise item.customConfig?.associatedPluginId) - .map((item) => dbPluginFormat(item)); + .map((item) => workflowToolFormat(item)); const concatTools = [...formatTools, ...dbPlugins]; concatTools.sort((a, b) => (a.pluginOrder ?? 999) - (b.pluginOrder ?? 999)); - global.systemToolsTypeCache = {}; - concatTools.forEach((item) => { - global.systemToolsTypeCache[item.templateType] = 1; - }); - return concatTools; }; -export const getSystemToolById = async (id: string): Promise => { - const { source, pluginId } = splitCombinePluginId(id); - if (source === PluginSourceEnum.systemTool) { - const tools = await getSystemTools(); - const tool = tools.find((item) => item.id === pluginId); - if (tool) { - return cloneDeep(tool); - } - return Promise.reject(PluginErrEnum.unExist); +export const getSystemToolById = async (id: string): Promise => { + const { pluginId } = splitCombineToolId(id); + const tools = await getSystemTools(); + const tool = tools.find((item) => item.id === pluginId); + if (tool) { + return cloneDeep(tool); } - - const dbPlugin = await MongoSystemPlugin.findOne({ pluginId }).lean(); - if (!dbPlugin) return Promise.reject(PluginErrEnum.unExist); - return dbPluginFormat(dbPlugin); + return Promise.reject(PluginErrEnum.unExist); }; - -declare global { - var systemToolsTypeCache: Record; -} diff --git a/packages/service/core/app/plugin/utils.ts b/packages/service/core/app/tool/runtime/utils.ts similarity index 63% rename from packages/service/core/app/plugin/utils.ts rename to packages/service/core/app/tool/runtime/utils.ts index 848a925099d2..7ac7c7b39c1e 100644 --- a/packages/service/core/app/plugin/utils.ts +++ b/packages/service/core/app/tool/runtime/utils.ts @@ -1,29 +1,29 @@ import { type ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type'; -import { type PluginRuntimeType } from '@fastgpt/global/core/app/plugin/type'; -import { PluginSourceEnum } from '@fastgpt/global/core/app/plugin/constants'; -import { splitCombinePluginId } from '@fastgpt/global/core/app/plugin/utils'; +import { type AppToolRuntimeType } from '@fastgpt/global/core/app/tool/type'; +import { AppToolSourceEnum } from '@fastgpt/global/core/app/tool/constants'; +import { splitCombineToolId } from '@fastgpt/global/core/app/tool/utils'; /* - Plugin points calculation: + Tool points calculation: 1. 系统插件/商业版插件: - 有错误:返回 0 - 无错误:返回 单次积分 + 子流程积分(可配置) 2. 个人插件 - 返回 子流程积分 */ -export const computedPluginUsage = async ({ +export const computedAppToolUsage = async ({ plugin, childrenUsage, error }: { - plugin: PluginRuntimeType; + plugin: AppToolRuntimeType; childrenUsage: ChatNodeUsageType[]; error?: boolean; }) => { - const { source } = splitCombinePluginId(plugin.id); + const { source } = splitCombineToolId(plugin.id); const childrenUsages = childrenUsage.reduce((sum, item) => sum + (item.totalPoints || 0), 0); - if (source !== PluginSourceEnum.personal) { + if (source !== AppToolSourceEnum.personal) { if (error) return 0; const pluginCurrentCost = plugin.currentCost ?? 0; diff --git a/packages/service/core/app/tool/workflowTool/utils.ts b/packages/service/core/app/tool/workflowTool/utils.ts new file mode 100644 index 000000000000..8b5825fb1b01 --- /dev/null +++ b/packages/service/core/app/tool/workflowTool/utils.ts @@ -0,0 +1,95 @@ +import { anyValueDecrypt, encryptSecretValue } from '../../../../common/secret/utils'; +import type { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type'; +import { + FlowNodeInputTypeEnum, + FlowNodeTypeEnum +} from '@fastgpt/global/core/workflow/node/constant'; +import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants'; +import type { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io'; +import type { RuntimeUserPromptType, UserChatItemType } from '@fastgpt/global/core/chat/type'; +import { getNanoid } from '@fastgpt/global/common/string/tools'; +import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; +import { runtimePrompt2ChatsValue } from '@fastgpt/global/core/chat/adapt'; + +// add value to tool input node when run tool +export const updateWorkflowToolInputByVariables = ( + nodes: RuntimeNodeItemType[], + variables: Record +) => { + return nodes.map((node) => + node.flowNodeType === FlowNodeTypeEnum.pluginInput + ? { + ...node, + inputs: node.inputs.map((input) => { + const parseValue = (() => { + try { + if (input.renderTypeList.includes(FlowNodeInputTypeEnum.password)) { + return anyValueDecrypt(variables[input.key]); + } + if ( + input.valueType === WorkflowIOValueTypeEnum.string || + input.valueType === WorkflowIOValueTypeEnum.number || + input.valueType === WorkflowIOValueTypeEnum.boolean + ) + return variables[input.key]; + return JSON.parse(variables[input.key]); + } catch (e) { + return variables[input.key]; + } + })(); + + return { + ...input, + value: parseValue ?? input.value + }; + }) + } + : node + ); +}; + +/* Get tool runtime input user query */ +export const serverGetWorkflowToolRunUserQuery = ({ + pluginInputs, + variables, + files = [] +}: { + pluginInputs: FlowNodeInputItemType[]; + variables: Record; + files?: RuntimeUserPromptType['files']; +}): UserChatItemType & { dataId: string } => { + const getRunContent = ({ + pluginInputs, + variables + }: { + pluginInputs: FlowNodeInputItemType[]; + variables: Record; + }) => { + const pluginInputsWithValue = pluginInputs.map((input) => { + const { key } = input; + let value = variables?.hasOwnProperty(key) ? variables[key] : input.defaultValue; + + if (input.renderTypeList.includes(FlowNodeInputTypeEnum.password)) { + value = encryptSecretValue(value); + } + + return { + ...input, + value + }; + }); + return JSON.stringify(pluginInputsWithValue); + }; + + return { + dataId: getNanoid(24), + obj: ChatRoleEnum.Human, + value: runtimePrompt2ChatsValue({ + text: getRunContent({ + pluginInputs: pluginInputs, + variables + }), + files + }) + }; +}; diff --git a/packages/service/core/app/utils.ts b/packages/service/core/app/utils.ts index 70ffaea5c198..a48728cb229c 100644 --- a/packages/service/core/app/utils.ts +++ b/packages/service/core/app/utils.ts @@ -3,12 +3,12 @@ import { getEmbeddingModel } from '../ai/model'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node'; -import { getChildAppPreviewNode } from './plugin/controller'; -import { PluginSourceEnum } from '@fastgpt/global/core/app/plugin/constants'; +import { getChildAppPreviewNode } from './tool/controller'; +import { AppToolSourceEnum } from '@fastgpt/global/core/app/tool/constants'; import { authAppByTmbId } from '../../support/permission/app/auth'; import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { getErrText } from '@fastgpt/global/common/error/utils'; -import { splitCombinePluginId } from '@fastgpt/global/core/app/plugin/utils'; +import { splitCombineToolId } from '@fastgpt/global/core/app/tool/utils'; import type { localeType } from '@fastgpt/global/common/i18n/type'; export async function listAppDatasetDataByTeamIdAndDatasetIds({ @@ -50,7 +50,7 @@ export async function rewriteAppWorkflowToDetail({ await Promise.all( nodes.map(async (node) => { if (!node.pluginId) return; - const { source, pluginId } = splitCombinePluginId(node.pluginId); + const { source, pluginId } = splitCombineToolId(node.pluginId); try { const [preview] = await Promise.all([ @@ -59,7 +59,7 @@ export async function rewriteAppWorkflowToDetail({ versionId: node.version, lang }), - ...(source === PluginSourceEnum.personal + ...(source === AppToolSourceEnum.personal ? [ authAppByTmbId({ tmbId: ownerTmbId, @@ -71,11 +71,12 @@ export async function rewriteAppWorkflowToDetail({ ]); node.pluginData = { + name: preview.name, + avatar: preview.avatar, + status: preview.status, diagram: preview.diagram, userGuide: preview.userGuide, - courseUrl: preview.courseUrl, - name: preview.name, - avatar: preview.avatar + courseUrl: preview.courseUrl }; node.versionLabel = preview.versionLabel; node.isLatestVersion = preview.isLatestVersion; diff --git a/packages/service/core/chat/saveChat.ts b/packages/service/core/chat/saveChat.ts index 11be542015c9..2b0793e83265 100644 --- a/packages/service/core/chat/saveChat.ts +++ b/packages/service/core/chat/saveChat.ts @@ -17,6 +17,8 @@ import { MongoAppChatLog } from '../app/logs/chatLogsSchema'; import { writePrimary } from '../../common/mongo/utils'; import { MongoChatItemResponse } from './chatItemResponseSchema'; import { chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt'; +import { MongoS3TTL } from '../../common/s3/schema'; +import type { ClientSession } from '../../common/mongo'; type Props = { chatId: string; @@ -39,6 +41,40 @@ type Props = { errorMsg?: string; }; +const beforProcess = (props: Props) => { + // Remove url + props.userContent.value.forEach((item) => { + if (item.type === ChatItemValueTypeEnum.file && item.file?.key) { + item.file.url = ''; + } + }); +}; +const afterProcess = async ({ + contents, + session +}: { + contents: (UserChatItemType | AIChatItemType)[]; + session: ClientSession; +}) => { + // Remove ttl + const fileKeys = contents + .map((item) => { + if (item.value && Array.isArray(item.value)) { + return item.value.map((valueItem) => { + if (valueItem.type === ChatItemValueTypeEnum.file && valueItem.file?.key) { + return valueItem.file.key; + } + }); + } + return []; + }) + .flat() + .filter(Boolean) as string[]; + if (fileKeys.length > 0) { + await MongoS3TTL.deleteMany({ minioKey: { $in: fileKeys } }, { session }); + } +}; + const formatAiContent = ({ aiContent, durationSeconds, @@ -106,6 +142,8 @@ const getChatDataLog = async ({ }; export async function saveChat(props: Props) { + beforProcess(props); + const { chatId, appId, @@ -216,6 +254,8 @@ export async function saveChat(props: Props) { } ); + await afterProcess({ contents: processedContent, session }); + pushChatLog({ chatId, chatItemIdHuman: String(chatItemIdHuman), @@ -293,16 +333,11 @@ export async function saveChat(props: Props) { } } -export const updateInteractiveChat = async ({ - teamId, - chatId, - appId, - userContent, - aiContent, - variables, - durationSeconds, - errorMsg -}: Props) => { +export const updateInteractiveChat = async (props: Props) => { + beforProcess(props); + + const { teamId, chatId, appId, userContent, aiContent, variables, durationSeconds, errorMsg } = + props; if (!chatId) return; const chatItem = await MongoChatItem.findOne({ appId, chatId, obj: ChatRoleEnum.AI }).sort({ @@ -422,6 +457,8 @@ export const updateInteractiveChat = async ({ { session, ordered: true, ...writePrimary } ); } + + await afterProcess({ contents: [userContent, aiContent], session }); }); // Push chat data logs diff --git a/packages/service/core/chat/utils.ts b/packages/service/core/chat/utils.ts new file mode 100644 index 000000000000..4c3bb7ee936a --- /dev/null +++ b/packages/service/core/chat/utils.ts @@ -0,0 +1,18 @@ +import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants'; +import type { ChatItemType } from '@fastgpt/global/core/chat/type'; +import { getS3ChatSource } from '../../common/s3/sources/chat'; + +export const addPreviewUrlToChatItems = async (histories: ChatItemType[]) => { + // Presign file urls + const s3ChatSource = getS3ChatSource(); + for await (const item of histories) { + for await (const value of item.value) { + if (value.type === ChatItemValueTypeEnum.file && value.file && value.file.key) { + value.file.url = await s3ChatSource.createGetChatFileURL({ + key: value.file.key, + external: true + }); + } + } + } +}; diff --git a/packages/service/core/dataset/training/controller.ts b/packages/service/core/dataset/training/controller.ts index a2daac822208..fec4f1bc886e 100644 --- a/packages/service/core/dataset/training/controller.ts +++ b/packages/service/core/dataset/training/controller.ts @@ -1,18 +1,14 @@ import { MongoDatasetTraining } from './schema'; -import type { - PushDatasetDataChunkProps, - PushDatasetDataResponse -} from '@fastgpt/global/core/dataset/api.d'; +import type { PushDatasetDataResponse } from '@fastgpt/global/core/dataset/api.d'; import { TrainingModeEnum } from '@fastgpt/global/core/dataset/constants'; -import { simpleText } from '@fastgpt/global/common/string/tools'; import { type ClientSession } from '../../../common/mongo'; import { getLLMModel, getEmbeddingModel, getVlmModel } from '../../ai/model'; import { addLog } from '../../../common/system/log'; -import { getCollectionWithDataset } from '../controller'; import { mongoSessionRun } from '../../../common/mongo/sessionRun'; import { type PushDataToTrainingQueueProps } from '@fastgpt/global/core/dataset/training/type'; import { i18nT } from '../../../../web/i18n/utils'; import { getLLMMaxChunkSize } from '../../../../global/core/dataset/training/utils'; +import { retryFn } from '@fastgpt/global/common/system/utils'; export const lockTrainingDataByTeamId = async (teamId: string): Promise => { try { @@ -101,22 +97,26 @@ export async function pushDataListToTrainingQueue({ }); // insert data to db - const insertLen = data.length; + const batchSize = 500; // Batch insert size + const maxBatchesPerTransaction = 20; // Every session can insert at most 20 batches - // 使用 insertMany 批量插入 - const batchSize = 500; - const insertData = async (startIndex: number, session: ClientSession) => { - const list = data.slice(startIndex, startIndex + batchSize); + const insertDataIterative = async ( + dataToInsert: typeof data, + session: ClientSession + ): Promise => { + let insertedCount = 0; - if (list.length === 0) return; + for (let i = 0; i < dataToInsert.length; i += batchSize) { + const batch = dataToInsert.slice(i, i + batchSize); + + if (batch.length === 0) continue; - try { const result = await MongoDatasetTraining.insertMany( - list.map((item) => ({ + batch.map((item) => ({ teamId, tmbId, - datasetId: datasetId, - collectionId: collectionId, + datasetId, + collectionId, billId, mode, ...(item.q && { q: item.q }), @@ -130,34 +130,58 @@ export async function pushDataListToTrainingQueue({ })), { session, - ordered: false, + ordered: true, // 改为 true: 任何失败立即停止,事务回滚 rawResult: true, - includeResultMetadata: false // 进一步减少返回数据 + includeResultMetadata: false } ); - if (result.insertedCount !== list.length) { - return Promise.reject(`Insert data error, ${JSON.stringify(result)}`); - } - } catch (error: any) { - addLog.error(`Insert error`, error); - return Promise.reject(error); + // ordered: true 模式下,成功必定等于批次大小 + insertedCount += result.insertedCount; + + addLog.debug(`Training data insert progress: ${insertedCount}/${dataToInsert.length}`); } - return insertData(startIndex + batchSize, session); + return insertedCount; }; + // 大数据量分段事务处理 (避免事务超时) + const chunkSize = maxBatchesPerTransaction * batchSize; // 10,000 条 + let start = Date.now(); + + if (data.length > chunkSize) { + addLog.info(`Large dataset detected (${data.length} items), using chunked transactions`); + + let totalInserted = 0; + + for (let i = 0; i < data.length; i += chunkSize) { + const chunk = data.slice(i, i + chunkSize); + + await retryFn(async () => { + const inserted = await mongoSessionRun(async (chunkSession) => { + return insertDataIterative(chunk, chunkSession); + }); + totalInserted += inserted; + }); + } + + addLog.info(`Chunked transactions completed in ${Date.now() - start}ms`); + + return { insertLen: totalInserted }; + } + + // 小数据量单事务处理 if (session) { - await insertData(0, session); + const insertedCount = await insertDataIterative(data, session); + addLog.info(`Single transaction completed in ${Date.now() - start}ms`); + return { insertLen: insertedCount }; } else { - await mongoSessionRun(async (session) => { - await insertData(0, session); + const insertedCount = await mongoSessionRun(async (session) => { + return insertDataIterative(data, session); }); + addLog.info(`Single transaction completed in ${Date.now() - start}ms`); + return { insertLen: insertedCount }; } - - return { - insertLen - }; } export const pushDatasetToParseQueue = async ({ diff --git a/packages/service/core/plugin/schema/teamInstalledPluginSchema.ts b/packages/service/core/plugin/schema/teamInstalledPluginSchema.ts new file mode 100644 index 000000000000..c4481314d712 --- /dev/null +++ b/packages/service/core/plugin/schema/teamInstalledPluginSchema.ts @@ -0,0 +1,34 @@ +import { TeamCollectionName } from '@fastgpt/global/support/user/team/constant'; +import { connectionMongo, getMongoModel } from '../../../common/mongo/index'; +import type { TeamInstalledPluginSchemaType } from '@fastgpt/global/core/plugin/schema/type'; + +const { Schema } = connectionMongo; + +export const collectionName = 'team_installed_plugins'; + +const TeamInstalledPluginSchema = new Schema({ + teamId: { + type: Schema.Types.ObjectId, + ref: TeamCollectionName, + required: true + }, + pluginType: { + type: String, + default: 'tool' + }, + pluginId: { + type: String, + required: true + }, + installed: { + type: Boolean, + required: true + } +}); + +TeamInstalledPluginSchema.index({ teamId: 1, pluginId: 1 }, { unique: true }); + +export const MongoTeamInstalledPlugin = getMongoModel( + collectionName, + TeamInstalledPluginSchema +); diff --git a/packages/service/core/app/plugin/pluginGroupSchema.ts b/packages/service/core/plugin/tool/pluginGroupSchema.ts similarity index 66% rename from packages/service/core/app/plugin/pluginGroupSchema.ts rename to packages/service/core/plugin/tool/pluginGroupSchema.ts index eaf14988818c..a18417de21a3 100644 --- a/packages/service/core/app/plugin/pluginGroupSchema.ts +++ b/packages/service/core/plugin/tool/pluginGroupSchema.ts @@ -1,9 +1,22 @@ +// 已弃用 +import type { I18nStringStrictType } from '@fastgpt/global/sdk/fastgpt-plugin'; import { connectionMongo, getMongoModel } from '../../../common/mongo/index'; -import { type SystemToolGroupSchemaType, type TGroupType } from './type'; const { Schema } = connectionMongo; export const collectionName = 'app_plugin_groups'; +export type TGroupType = { + typeName: I18nStringStrictType | string; + typeId: string; +}; +export type SystemToolGroupSchemaType = { + groupId: string; + groupAvatar: string; + groupName: string; + groupTypes: TGroupType[]; + groupOrder: number; +}; + const PluginGroupSchema = new Schema({ groupId: { type: String, diff --git a/packages/service/core/app/plugin/systemPluginSchema.ts b/packages/service/core/plugin/tool/systemToolSchema.ts similarity index 53% rename from packages/service/core/app/plugin/systemPluginSchema.ts rename to packages/service/core/plugin/tool/systemToolSchema.ts index c83bdc521a55..e8476458d113 100644 --- a/packages/service/core/app/plugin/systemPluginSchema.ts +++ b/packages/service/core/plugin/tool/systemToolSchema.ts @@ -1,16 +1,21 @@ import { connectionMongo, getMongoModel } from '../../../common/mongo/index'; const { Schema } = connectionMongo; -import type { SystemPluginConfigSchemaType } from './type'; +import type { SystemPluginToolCollectionType } from '@fastgpt/global/core/plugin/tool/type'; -export const collectionName = 'app_system_plugins'; +export const collectionName = 'system_plugin_tools'; -const SystemPluginSchema = new Schema({ +const SystemToolSchema = new Schema({ pluginId: { type: String, required: true }, - isActive: { - type: Boolean + status: { + type: Number, + default: 1 + }, + defaultInstalled: { + type: Boolean, + default: false }, originCost: { type: Number, @@ -35,12 +40,13 @@ const SystemPluginSchema = new Schema({ inputListVal: Object, // @deprecated - inputConfig: Array + inputConfig: Array, + isActive: Boolean }); -SystemPluginSchema.index({ pluginId: 1 }); +SystemToolSchema.index({ pluginId: 1 }); -export const MongoSystemPlugin = getMongoModel( +export const MongoSystemTool = getMongoModel( collectionName, - SystemPluginSchema + SystemToolSchema ); diff --git a/packages/service/core/plugin/tool/tagSchema.ts b/packages/service/core/plugin/tool/tagSchema.ts new file mode 100644 index 000000000000..46070c2e37d2 --- /dev/null +++ b/packages/service/core/plugin/tool/tagSchema.ts @@ -0,0 +1,32 @@ +import { connectionMongo, getMongoModel } from '../../../common/mongo/index'; +import type { SystemPluginToolTagType } from '@fastgpt/global/core/plugin/type'; +const { Schema } = connectionMongo; + +export const collectionName = 'system_plugin_tool_tags'; + +const SystemPluginToolTagSchema = new Schema({ + tagId: { + type: String, + required: true + }, + tagName: { + type: Schema.Types.Mixed, + required: true + }, + tagOrder: { + type: Number, + default: 0 + }, + isSystem: { + type: Boolean, + default: false + } +}); + +SystemPluginToolTagSchema.index({ tagId: 1 }, { unique: true }); +SystemPluginToolTagSchema.index({ tagOrder: 1 }); + +export const MongoPluginToolTag = getMongoModel( + collectionName, + SystemPluginToolTagSchema +); diff --git a/packages/service/core/workflow/dispatch/ai/agent/index.ts b/packages/service/core/workflow/dispatch/ai/agent/index.ts index 55e553b8f641..7ae5e94776eb 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/index.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/index.ts @@ -29,7 +29,7 @@ import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { getDocumentQuotePrompt } from '@fastgpt/global/core/ai/prompt/AIChat'; import { postTextCensor } from '../../../../chat/postTextCensor'; import type { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io'; -import type { McpToolDataType } from '@fastgpt/global/core/app/mcpTools/type'; +import type { McpToolDataType } from '@fastgpt/global/core/app/tool/mcpTool/type'; import type { JSONSchemaInputType } from '@fastgpt/global/core/app/jsonschema'; type Response = DispatchNodeResultType<{ diff --git a/packages/service/core/workflow/dispatch/child/runTool.ts b/packages/service/core/workflow/dispatch/child/runTool.ts index 3f6738f23d68..16fc45ed6fe2 100644 --- a/packages/service/core/workflow/dispatch/child/runTool.ts +++ b/packages/service/core/workflow/dispatch/child/runTool.ts @@ -9,22 +9,23 @@ import { 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 { McpToolDataType } from '@fastgpt/global/core/app/tool/mcpTool/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'; +import { MongoSystemTool } from '../../../plugin/tool/systemToolSchema'; +import { SystemToolSecretInputTypeEnum } from '@fastgpt/global/core/app/tool/systemTool/constants'; import type { StoreSecretValueType } from '@fastgpt/global/common/secret/type'; -import { getSystemToolById } from '../../../app/plugin/controller'; +import { getSystemToolById } from '../../../app/tool/controller'; import { textAdaptGptResponse } from '@fastgpt/global/core/workflow/runtime/utils'; import { pushTrack } from '../../../../common/middle/tracks/utils'; import { getNodeErrResponse } from '../utils'; -import { splitCombinePluginId } from '@fastgpt/global/core/app/plugin/utils'; +import { splitCombineToolId } from '@fastgpt/global/core/app/tool/utils'; import { getAppVersionById } from '../../../../core/app/version/controller'; import { runHTTPTool } from '../../../app/http'; +import { i18nT } from '../../../../../web/i18n/utils'; type SystemInputConfigType = { - type: SystemToolInputTypeEnum; + type: SystemToolSecretInputTypeEnum; value: StoreSecretValueType; }; @@ -61,17 +62,17 @@ export const dispatchRunTool = async (props: RunToolProps): Promise { switch (params.system_input_config?.type) { - case SystemToolInputTypeEnum.team: + case SystemToolSecretInputTypeEnum.team: return Promise.reject(new Error('This is not supported yet')); - case SystemToolInputTypeEnum.manual: + case SystemToolSecretInputTypeEnum.manual: const val = params.system_input_config.value || {}; return getSecretValue({ storeSecret: val }); - case SystemToolInputTypeEnum.system: + case SystemToolSecretInputTypeEnum.system: default: // read from mongo - const dbPlugin = await MongoSystemPlugin.findOne({ + const dbPlugin = await MongoSystemTool.findOne({ pluginId: toolConfig.systemTool?.toolId }).lean(); return dbPlugin?.inputListVal || {}; @@ -156,8 +157,8 @@ export const dispatchRunTool = async (props: RunToolProps): Promise { if ( - params.system_input_config?.type === SystemToolInputTypeEnum.team || - params.system_input_config?.type === SystemToolInputTypeEnum.manual + params.system_input_config?.type === SystemToolSecretInputTypeEnum.team || + params.system_input_config?.type === SystemToolSecretInputTypeEnum.manual ) { return 0; } @@ -191,7 +192,7 @@ export const dispatchRunTool = async (props: RunToolProps): Promise & { runtimeNodes: RuntimeNodeItemType[]; @@ -77,7 +79,7 @@ export async function dispatchWorkFlow({ concatUsage, ...data }: Props & WorkflowUsageProps): Promise { - const { res, stream, runningUserInfo, runningAppInfo, lastInteractive } = data; + const { res, stream, runningUserInfo, runningAppInfo, lastInteractive, histories, query } = data; await checkTeamAIPoints(runningUserInfo.teamId); const [{ timezone, externalProvider }, newUsageId] = await Promise.all([ @@ -128,18 +130,33 @@ export async function dispatchWorkFlow({ } } + // Add preview url to chat items + await addPreviewUrlToChatItems(histories); + for (const item of query) { + if (item.type !== ChatItemValueTypeEnum.file || !item.file?.key) continue; + item.file.url = await getS3ChatSource().createGetChatFileURL({ + key: item.file.key, + external: true + }); + } + // Get default variables + const defaultVariables = { ...externalProvider.externalWorkflowVariables, - ...getSystemVariables({ + ...(await getSystemVariables({ ...data, + query, + histories, timezone - }) + })) }; // Init some props return runWorkflow({ ...data, + query, + histories, timezone, externalProvider, defaultSkipNodeQueue: data.lastInteractive?.skipNodeQueue || data.defaultSkipNodeQueue, @@ -195,11 +212,11 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise> = { ...data, + lastInteractive: data.lastInteractive?.entryNodeIds.includes(node.nodeId) + ? data.lastInteractive + : undefined, variables, histories, retainDatasetCite, @@ -735,8 +759,8 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise 0 ? workflowQueue.system_memories @@ -993,7 +1015,7 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise { +}): Promise => { // Get global variables(Label -> key; Key -> key) const variablesConfig = chatConfig?.variables || []; - const variablesMap = variablesConfig.reduce>((acc, item) => { + const variablesMap: Record = {}; + for await (const item of variablesConfig) { // For internal variables, ignore external input and use default value if (item.type === VariableInputEnum.password) { const val = variables[item.label] || variables[item.key] || item.defaultValue; const actualValue = anyValueDecrypt(val); - acc[item.key] = valueTypeFormat(actualValue, item.valueType); + variablesMap[item.key] = valueTypeFormat(actualValue, item.valueType); } - // API else if (variables[item.label] !== undefined) { - acc[item.key] = valueTypeFormat(variables[item.label], item.valueType); + variablesMap[item.key] = valueTypeFormat(variables[item.label], item.valueType); } // Web else if (variables[item.key] !== undefined) { - acc[item.key] = valueTypeFormat(variables[item.key], item.valueType); + variablesMap[item.key] = valueTypeFormat(variables[item.key], item.valueType); } else { - acc[item.key] = valueTypeFormat(item.defaultValue, item.valueType); + variablesMap[item.key] = valueTypeFormat(item.defaultValue, item.valueType); } - return acc; - }, {}); + } return { ...variablesMap, diff --git a/packages/service/core/workflow/dispatch/plugin/run.ts b/packages/service/core/workflow/dispatch/plugin/run.ts index 2e950fd74829..fbcdfc37b8da 100644 --- a/packages/service/core/workflow/dispatch/plugin/run.ts +++ b/packages/service/core/workflow/dispatch/plugin/run.ts @@ -1,10 +1,11 @@ -import { - getPluginInputsFromStoreNodes, - splitCombinePluginId -} from '@fastgpt/global/core/app/plugin/utils'; +import { splitCombineToolId } from '@fastgpt/global/core/app/tool/utils'; +import { getWorkflowToolInputsFromStoreNodes } from '@fastgpt/global/core/app/tool/workflowTool/utils'; import { chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt'; -import { PluginSourceEnum } from '@fastgpt/global/core/app/plugin/constants'; -import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; +import { AppToolSourceEnum } from '@fastgpt/global/core/app/tool/constants'; +import { + FlowNodeInputTypeEnum, + FlowNodeTypeEnum +} from '@fastgpt/global/core/workflow/node/constant'; import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type'; import { @@ -15,15 +16,16 @@ import { import { type DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/type'; import { authPluginByTmbId } from '../../../../support/permission/app/auth'; import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; -import { computedPluginUsage } from '../../../app/plugin/utils'; +import { computedAppToolUsage } from '../../../app/tool/runtime/utils'; import { filterSystemVariables, getNodeErrResponse } from '../utils'; -import { getPluginRunUserQuery } from '@fastgpt/global/core/workflow/utils'; +import { serverGetWorkflowToolRunUserQuery } from '../../../app/tool/workflowTool/utils'; import type { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants'; -import { getChildAppRuntimeById } from '../../../app/plugin/controller'; +import { getChildAppRuntimeById } from '../../../app/tool/controller'; import { runWorkflow } from '../index'; import { getUserChatInfo } from '../../../../support/user/team/utils'; import { dispatchRunTool } from '../child/runTool'; -import type { PluginRuntimeType } from '@fastgpt/global/core/app/plugin/type'; +import type { AppToolRuntimeType } from '@fastgpt/global/core/app/tool/type'; +import { anyValueDecrypt } from '../../../../common/secret/utils'; type RunPluginProps = ModuleDispatchProps<{ [NodeInputKeyEnum.forbidStream]?: boolean; @@ -49,12 +51,12 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise ({ - ...input, - value: data[input.key] ?? input.value - })) + inputs: node.inputs.map((input) => { + let val = data[input.key] ?? input.value; + if (input.renderTypeList.includes(FlowNodeInputTypeEnum.password)) { + val = anyValueDecrypt(val); + } + + return { + ...input, + value: val + }; + }) }; } return { @@ -137,8 +146,8 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise item.moduleType === FlowNodeTypeEnum.pluginOutput); - const usagePoints = await computedPluginUsage({ + const usagePoints = await computedAppToolUsage({ plugin, childrenUsage: flowUsages, error: !!output?.pluginOutput?.error diff --git a/packages/service/core/workflow/dispatch/tools/runUpdateVar.ts b/packages/service/core/workflow/dispatch/tools/runUpdateVar.ts index 15df8efd9ca6..c0fc0373956a 100644 --- a/packages/service/core/workflow/dispatch/tools/runUpdateVar.ts +++ b/packages/service/core/workflow/dispatch/tools/runUpdateVar.ts @@ -11,7 +11,7 @@ import { } from '@fastgpt/global/core/workflow/runtime/utils'; import { type TUpdateListItem } from '@fastgpt/global/core/workflow/template/system/variableUpdate/type'; import { type ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type'; -import { removeSystemVariable } from '../utils'; +import { runtimeSystemVar2StoreType } from '../utils'; import { isValidReferenceValue } from '@fastgpt/global/core/workflow/utils'; import { valueTypeFormat } from '@fastgpt/global/core/workflow/runtime/utils'; @@ -92,11 +92,11 @@ export const dispatchUpdateVariable = async (props: Props): Promise => if (!runningAppInfo.isChildApp) { workflowStreamResponse?.({ event: SseResponseEventEnum.updateVariables, - data: removeSystemVariable( + data: runtimeSystemVar2StoreType({ variables, - externalProvider.externalWorkflowVariables, - chatConfig?.variables - ) + removeObj: externalProvider.externalWorkflowVariables, + userVariablesConfigs: chatConfig?.variables + }) }); } diff --git a/packages/service/core/workflow/dispatch/utils.ts b/packages/service/core/workflow/dispatch/utils.ts index 3200561d4f84..56e8b399ab84 100644 --- a/packages/service/core/workflow/dispatch/utils.ts +++ b/packages/service/core/workflow/dispatch/utils.ts @@ -17,8 +17,8 @@ import { } from '@fastgpt/global/core/workflow/runtime/constants'; 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 { getMCPToolRuntimeNode } from '@fastgpt/global/core/app/tool/mcpTool/utils'; +import { getHTTPToolRuntimeNode } from '@fastgpt/global/core/app/tool/httpTool/utils'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { MongoApp } from '../../../core/app/schema'; import { getMCPChildren } from '../../../core/app/mcp'; @@ -117,11 +117,15 @@ export const checkQuoteQAValue = (quoteQA?: SearchDataResponseItemType[]) => { }; /* remove system variable */ -export const removeSystemVariable = ( - variables: Record, - removeObj: Record = {}, - userVariablesConfigs: VariableItemType[] = [] -) => { +export const runtimeSystemVar2StoreType = ({ + variables, + removeObj = {}, + userVariablesConfigs = [] +}: { + variables: Record; + removeObj?: Record; + userVariablesConfigs?: VariableItemType[]; +}) => { const copyVariables = { ...variables }; // Delete system variables @@ -140,11 +144,13 @@ export const removeSystemVariable = ( // Encrypt password variables userVariablesConfigs.forEach((item) => { const val = copyVariables[item.key]; - if (item.type === VariableInputEnum.password && typeof val === 'string') { - copyVariables[item.key] = { - value: '', - secret: encryptSecret(val) - }; + if (item.type === VariableInputEnum.password) { + if (typeof val === 'string') { + copyVariables[item.key] = { + value: '', + secret: encryptSecret(val) + }; + } } }); diff --git a/packages/service/core/workflow/utils.ts b/packages/service/core/workflow/utils.ts index 11e70791ad67..19cb98c8c09c 100644 --- a/packages/service/core/workflow/utils.ts +++ b/packages/service/core/workflow/utils.ts @@ -1,7 +1,7 @@ import { type SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type'; import { countPromptTokens } from '../../common/string/tiktoken/index'; import type { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type'; -import { getSystemPluginByIdAndVersionId, getSystemTools } from '../app/plugin/controller'; +import { getSystemToolByIdAndVersionId, getSystemTools } from '../app/tool/controller'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { parseI18nString } from '@fastgpt/global/common/i18n/utils'; @@ -44,7 +44,7 @@ export async function getSystemToolRunTimeNodeFromSystemToolset({ ); const tools = await getSystemTools(); const children = tools.filter( - (item) => item.parentId === systemToolId && item.isActive !== false + (item) => item.parentId === systemToolId && (item.status === 1 || item.status === undefined) ); const nodes = await Promise.all( children.map(async (child, index) => { @@ -52,7 +52,7 @@ export async function getSystemToolRunTimeNodeFromSystemToolset({ (item) => item.toolId === child.id ); - const tool = await getSystemPluginByIdAndVersionId(child.id); + const tool = await getSystemToolByIdAndVersionId(child.id); const inputs = tool.inputs ?? []; if (toolsetInputConfig?.value) { diff --git a/packages/service/package.json b/packages/service/package.json index 47f000ea3c03..8a17b9367b0d 100644 --- a/packages/service/package.json +++ b/packages/service/package.json @@ -45,6 +45,7 @@ "nextjs-cors": "^2.2.0", "node-cron": "^3.0.3", "node-xlsx": "^0.24.0", + "p-limit": "^7.2.0", "papaparse": "5.4.1", "pdfjs-dist": "4.10.38", "pg": "^8.10.0", @@ -55,9 +56,10 @@ "tunnel": "^0.0.6", "turndown": "^7.1.2", "winston": "^3.17.0", - "zod": "^3.24.2" + "zod": "^4.1.12" }, "devDependencies": { + "@types/async-retry": "^1.4.9", "@types/cookie": "^0.5.2", "@types/decompress": "^4.2.7", "@types/jsonwebtoken": "^9.0.3", diff --git a/packages/service/support/permission/app/auth.ts b/packages/service/support/permission/app/auth.ts index 55e478316ff4..909a8831d489 100644 --- a/packages/service/support/permission/app/auth.ts +++ b/packages/service/support/permission/app/auth.ts @@ -14,9 +14,9 @@ import { AppPermission } from '@fastgpt/global/support/permission/app/controller import { type PermissionValueType } from '@fastgpt/global/support/permission/type'; import { AppFolderTypeList, AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { type ParentIdType } from '@fastgpt/global/common/parentFolder/type'; -import { PluginSourceEnum } from '@fastgpt/global/core/app/plugin/constants'; +import { AppToolSourceEnum } from '@fastgpt/global/core/app/tool/constants'; import { type AuthModeType, type AuthResponseType } from '../type'; -import { splitCombinePluginId } from '@fastgpt/global/core/app/plugin/utils'; +import { splitCombineToolId } from '@fastgpt/global/core/app/tool/utils'; import { AppReadChatLogPerVal } from '@fastgpt/global/support/permission/app/constant'; import { parseHeaderCert } from '../auth/common'; import { sumPer } from '@fastgpt/global/support/permission/utils'; @@ -30,8 +30,8 @@ export const authPluginByTmbId = async ({ appId: string; per: PermissionValueType; }) => { - const { source } = splitCombinePluginId(appId); - if (source === PluginSourceEnum.personal) { + const { source } = splitCombineToolId(appId); + if (source === AppToolSourceEnum.personal) { const { app } = await authAppByTmbId({ appId, tmbId, diff --git a/packages/service/support/permission/user/auth.ts b/packages/service/support/permission/user/auth.ts index 88dd38555991..ea13bf655d5d 100644 --- a/packages/service/support/permission/user/auth.ts +++ b/packages/service/support/permission/user/auth.ts @@ -45,7 +45,7 @@ export const authSystemAdmin = async ({ req }: { req: ApiRequestProps }) => { _id: result.userId }); - if (user && user.username !== 'root') { + if (!user || user.username !== 'root') { return Promise.reject(ERROR_ENUM.unAuthorization); } return result; diff --git a/packages/service/worker/htmlStr2Md/utils.ts b/packages/service/worker/htmlStr2Md/utils.ts index 4445f11430e4..0f0e8046d4b8 100644 --- a/packages/service/worker/htmlStr2Md/utils.ts +++ b/packages/service/worker/htmlStr2Md/utils.ts @@ -1,15 +1,20 @@ import TurndownService from 'turndown'; import { type ImageType } from '../readFile/type'; -import { matchMdImg } from '@fastgpt/global/common/string/markdown'; import { getNanoid } from '@fastgpt/global/common/string/tools'; // @ts-ignore const turndownPluginGfm = require('joplin-turndown-plugin-gfm'); +const MAX_HTML_SIZE = 100 * 1000; // 100k characters limit + const processBase64Images = (htmlContent: string) => { - const base64Regex = /src="data:([^;]+);base64,([^"]+)"/g; + // 优化后的正则: + // 1. 使用精确的 base64 字符集 [A-Za-z0-9+/=]+ 避免回溯 + // 2. 明确捕获 mime 类型和 base64 数据 + // 3. 减少不必要的捕获组 + const base64Regex = /src="data:([^;]+);base64,([A-Za-z0-9+/=]+)"/g; const images: ImageType[] = []; - const processedHtml = htmlContent.replace(base64Regex, (match, mime, base64Data) => { + const processedHtml = htmlContent.replace(base64Regex, (_match, mime, base64Data) => { const uuid = `IMAGE_${getNanoid(12)}_IMAGE`; images.push({ uuid, @@ -63,12 +68,18 @@ export const html2md = ( // Base64 img to id, otherwise it will occupy memory when going to md const { processedHtml, images } = processBase64Images(html); + + // if html is too large, return the original html + if (processedHtml.length > MAX_HTML_SIZE) { + return { rawText: processedHtml, imageList: [] }; + } + const md = turndownService.turndown(processedHtml); - const { text, imageList } = matchMdImg(md); + // const { text, imageList } = matchMdImg(md); return { - rawText: text, - imageList: [...images, ...imageList] + rawText: md, + imageList: images }; } catch (error) { console.log('html 2 markdown error', error); diff --git a/packages/web/common/system/utils.ts b/packages/web/common/system/utils.ts index cb7e7a3e7b0b..bc6bf645e5c1 100644 --- a/packages/web/common/system/utils.ts +++ b/packages/web/common/system/utils.ts @@ -6,7 +6,7 @@ export const getUserFingerprint = async () => { console.log(result.visitorId); }; -export const subRoute = process.env.NEXT_PUBLIC_BASE_URL; +export const subRoute = process.env.NEXT_PUBLIC_BASE_URL || ''; export const getWebReqUrl = (url: string = '') => { if (!url) return '/'; diff --git a/packages/web/components/common/DateTimePicker/index.tsx b/packages/web/components/common/DateTimePicker/index.tsx index c22a8c913709..be6784f8b084 100644 --- a/packages/web/components/common/DateTimePicker/index.tsx +++ b/packages/web/components/common/DateTimePicker/index.tsx @@ -11,12 +11,12 @@ import MyIcon from '../Icon'; const DateTimePicker = ({ onChange, popPosition = 'bottom', - defaultDate = new Date(), + defaultDate, selectedDateTime, disabled, ...props }: { - onChange?: (dateTime: Date) => void; + onChange?: (dateTime: Date | undefined) => void; popPosition?: 'bottom' | 'top'; defaultDate?: Date; selectedDateTime?: Date; @@ -29,9 +29,7 @@ const DateTimePicker = ({ const [showSelected, setShowSelected] = useState(false); useEffect(() => { - if (selectedDateTime) { - setSelectedDate(selectedDateTime); - } + setSelectedDate(selectedDateTime); }, [selectedDateTime]); const formatSelected = useMemo(() => { @@ -39,7 +37,7 @@ const DateTimePicker = ({ const dateStr = format(selectedDate, 'y/MM/dd'); return dateStr; } - return format(new Date(), 'y/MM/dd'); + return ''; }, [selectedDate]); useOutsideClick({ @@ -60,7 +58,7 @@ const DateTimePicker = ({ cursor={'pointer'} bg={'myGray.50'} fontSize={'sm'} - onClick={() => setShowSelected(true)} + onClick={() => setShowSelected((state) => !state)} alignItems={'center'} {...props} > @@ -72,7 +70,7 @@ const DateTimePicker = ({ {showSelected && ( { - if (date) { - setSelectedDate(date); - onChange?.(date); - setShowSelected(false); - } + setSelectedDate(date); + onChange?.(date); + setShowSelected(false); }} /> diff --git a/packages/web/components/common/Icon/constants.ts b/packages/web/components/common/Icon/constants.ts index 1dec87576f71..1b865d9ff323 100644 --- a/packages/web/components/common/Icon/constants.ts +++ b/packages/web/components/common/Icon/constants.ts @@ -140,6 +140,8 @@ export const iconPaths = { 'core/app/inputGuides': () => import('./icons/core/app/inputGuides.svg'), 'core/app/logsLight': () => import('./icons/core/app/logsLight.svg'), 'core/app/markLight': () => import('./icons/core/app/markLight.svg'), + 'core/app/pluginFill': () => import('./icons/core/app/pluginFill.svg'), + 'core/app/pluginLight': () => import('./icons/core/app/pluginLight.svg'), 'core/app/publish/lark': () => import('./icons/core/app/publish/lark.svg'), 'core/app/publish/offiaccount': () => import('./icons/core/app/publish/offiaccount.svg'), 'core/app/publish/wechat': () => import('./icons/core/app/publish/wechat.svg'), @@ -176,7 +178,6 @@ export const iconPaths = { 'core/app/type/mcpToolsFill': () => import('./icons/core/app/type/mcpToolsFill.svg'), 'core/app/type/plugin': () => import('./icons/core/app/type/plugin.svg'), 'core/app/type/pluginFill': () => import('./icons/core/app/type/pluginFill.svg'), - 'core/app/type/pluginLight': () => import('./icons/core/app/type/pluginLight.svg'), 'core/app/type/simple': () => import('./icons/core/app/type/simple.svg'), 'core/app/type/simpleFill': () => import('./icons/core/app/type/simpleFill.svg'), 'core/app/type/workflow': () => import('./icons/core/app/type/workflow.svg'), @@ -273,10 +274,6 @@ export const iconPaths = { 'core/workflow/inputType/customVariable': () => import('./icons/core/workflow/inputType/customVariable.svg'), 'core/workflow/inputType/dataset': () => import('./icons/core/workflow/inputType/dataset.svg'), - 'core/workflow/inputType/timePointSelect': () => - import('./icons/core/workflow/inputType/timePointSelect.svg'), - 'core/workflow/inputType/timeRangeSelect': () => - import('./icons/core/workflow/inputType/timeRangeSelect.svg'), 'core/workflow/inputType/dynamic': () => import('./icons/core/workflow/inputType/dynamic.svg'), 'core/workflow/inputType/external': () => import('./icons/core/workflow/inputType/external.svg'), 'core/workflow/inputType/file': () => import('./icons/core/workflow/inputType/file.svg'), @@ -302,6 +299,10 @@ export const iconPaths = { import('./icons/core/workflow/inputType/selectLLM.svg'), 'core/workflow/inputType/switch': () => import('./icons/core/workflow/inputType/switch.svg'), 'core/workflow/inputType/textarea': () => import('./icons/core/workflow/inputType/textarea.svg'), + 'core/workflow/inputType/timePointSelect': () => + import('./icons/core/workflow/inputType/timePointSelect.svg'), + 'core/workflow/inputType/timeRangeSelect': () => + import('./icons/core/workflow/inputType/timeRangeSelect.svg'), 'core/workflow/mouse': () => import('./icons/core/workflow/mouse.svg'), 'core/workflow/publish': () => import('./icons/core/workflow/publish.svg'), 'core/workflow/redo': () => import('./icons/core/workflow/redo.svg'), @@ -386,6 +387,7 @@ export const iconPaths = { export: () => import('./icons/export.svg'), feedback: () => import('./icons/feedback.svg'), 'file/csv': () => import('./icons/file/csv.svg'), + 'file/fill/audio': () => import('./icons/file/fill/audio.svg'), 'file/fill/csv': () => import('./icons/file/fill/csv.svg'), 'file/fill/doc': () => import('./icons/file/fill/doc.svg'), 'file/fill/file': () => import('./icons/file/fill/file.svg'), @@ -396,6 +398,7 @@ export const iconPaths = { 'file/fill/pdf': () => import('./icons/file/fill/pdf.svg'), 'file/fill/ppt': () => import('./icons/file/fill/ppt.svg'), 'file/fill/txt': () => import('./icons/file/fill/txt.svg'), + 'file/fill/video': () => import('./icons/file/fill/video.svg'), 'file/fill/xlsx': () => import('./icons/file/fill/xlsx.svg'), 'file/html': () => import('./icons/file/html.svg'), 'file/indexImport': () => import('./icons/file/indexImport.svg'), @@ -434,6 +437,14 @@ export const iconPaths = { 'model/huggingface': () => import('./icons/model/huggingface.svg'), more: () => import('./icons/more.svg'), moreLine: () => import('./icons/moreLine.svg'), + 'navbar/chatFill': () => import('./icons/navbar/chatFill.svg'), + 'navbar/chatLight': () => import('./icons/navbar/chatLight.svg'), + 'navbar/dashboardFill': () => import('./icons/navbar/dashboardFill.svg'), + 'navbar/dashboardLight': () => import('./icons/navbar/dashboardLight.svg'), + 'navbar/datasetFill': () => import('./icons/navbar/datasetFill.svg'), + 'navbar/datasetLight': () => import('./icons/navbar/datasetLight.svg'), + 'navbar/userFill': () => import('./icons/navbar/userFill.svg'), + 'navbar/userLight': () => import('./icons/navbar/userLight.svg'), optimizer: () => import('./icons/optimizer.svg'), out: () => import('./icons/out.svg'), paragraph: () => import('./icons/paragraph.svg'), @@ -465,6 +476,8 @@ export const iconPaths = { 'support/bill/priceLight': () => import('./icons/support/bill/priceLight.svg'), 'support/bill/shoppingCart': () => import('./icons/support/bill/shoppingCart.svg'), 'support/bill/wallet': () => import('./icons/support/bill/wallet.svg'), + 'support/config/configFill': () => import('./icons/support/config/configFill.svg'), + 'support/config/configLight': () => import('./icons/support/config/configLight.svg'), 'support/outlink/apikeyFill': () => import('./icons/support/outlink/apikeyFill.svg'), 'support/outlink/iframeLight': () => import('./icons/support/outlink/iframeLight.svg'), 'support/outlink/share': () => import('./icons/support/outlink/share.svg'), diff --git a/packages/web/components/common/Icon/icons/common/link.svg b/packages/web/components/common/Icon/icons/common/link.svg index 9855cd94a8a3..8cefd15270e8 100644 --- a/packages/web/components/common/Icon/icons/common/link.svg +++ b/packages/web/components/common/Icon/icons/common/link.svg @@ -1,3 +1,3 @@ - + diff --git a/packages/web/components/common/Icon/icons/core/app/pluginFill.svg b/packages/web/components/common/Icon/icons/core/app/pluginFill.svg new file mode 100644 index 000000000000..f7ff6d393252 --- /dev/null +++ b/packages/web/components/common/Icon/icons/core/app/pluginFill.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/core/app/pluginLight.svg b/packages/web/components/common/Icon/icons/core/app/pluginLight.svg new file mode 100644 index 000000000000..aec2faefd2c9 --- /dev/null +++ b/packages/web/components/common/Icon/icons/core/app/pluginLight.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/core/app/type/pluginLight.svg b/packages/web/components/common/Icon/icons/core/app/type/pluginLight.svg deleted file mode 100644 index 2a866c8758e2..000000000000 --- a/packages/web/components/common/Icon/icons/core/app/type/pluginLight.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/file/fill/audio.svg b/packages/web/components/common/Icon/icons/file/fill/audio.svg new file mode 100644 index 000000000000..88035258050a --- /dev/null +++ b/packages/web/components/common/Icon/icons/file/fill/audio.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/file/fill/video.svg b/packages/web/components/common/Icon/icons/file/fill/video.svg new file mode 100644 index 000000000000..dbc3283d6fd1 --- /dev/null +++ b/packages/web/components/common/Icon/icons/file/fill/video.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/navbar/chatFill.svg b/packages/web/components/common/Icon/icons/navbar/chatFill.svg new file mode 100644 index 000000000000..1e5d0e7aca08 --- /dev/null +++ b/packages/web/components/common/Icon/icons/navbar/chatFill.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/navbar/chatLight.svg b/packages/web/components/common/Icon/icons/navbar/chatLight.svg new file mode 100644 index 000000000000..0d94d8ef321b --- /dev/null +++ b/packages/web/components/common/Icon/icons/navbar/chatLight.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/web/components/common/Icon/icons/navbar/dashboardFill.svg b/packages/web/components/common/Icon/icons/navbar/dashboardFill.svg new file mode 100644 index 000000000000..2edf5bbe4a12 --- /dev/null +++ b/packages/web/components/common/Icon/icons/navbar/dashboardFill.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/web/components/common/Icon/icons/navbar/dashboardLight.svg b/packages/web/components/common/Icon/icons/navbar/dashboardLight.svg new file mode 100644 index 000000000000..a8908d531bdf --- /dev/null +++ b/packages/web/components/common/Icon/icons/navbar/dashboardLight.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/web/components/common/Icon/icons/navbar/datasetFill.svg b/packages/web/components/common/Icon/icons/navbar/datasetFill.svg new file mode 100644 index 000000000000..681e744914a1 --- /dev/null +++ b/packages/web/components/common/Icon/icons/navbar/datasetFill.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/web/components/common/Icon/icons/navbar/datasetLight.svg b/packages/web/components/common/Icon/icons/navbar/datasetLight.svg new file mode 100644 index 000000000000..3e0130601ee7 --- /dev/null +++ b/packages/web/components/common/Icon/icons/navbar/datasetLight.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/web/components/common/Icon/icons/navbar/userFill.svg b/packages/web/components/common/Icon/icons/navbar/userFill.svg new file mode 100644 index 000000000000..a6998f60a880 --- /dev/null +++ b/packages/web/components/common/Icon/icons/navbar/userFill.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/web/components/common/Icon/icons/navbar/userLight.svg b/packages/web/components/common/Icon/icons/navbar/userLight.svg new file mode 100644 index 000000000000..642467989150 --- /dev/null +++ b/packages/web/components/common/Icon/icons/navbar/userLight.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/web/components/common/Icon/icons/support/config/configFill.svg b/packages/web/components/common/Icon/icons/support/config/configFill.svg new file mode 100644 index 000000000000..fc3f1fa64f26 --- /dev/null +++ b/packages/web/components/common/Icon/icons/support/config/configFill.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/support/config/configLight.svg b/packages/web/components/common/Icon/icons/support/config/configLight.svg new file mode 100644 index 000000000000..b5cd7029dc02 --- /dev/null +++ b/packages/web/components/common/Icon/icons/support/config/configLight.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/packages/web/components/common/Markdown/index.tsx b/packages/web/components/common/Markdown/index.tsx new file mode 100644 index 000000000000..e5bfb8bfe174 --- /dev/null +++ b/packages/web/components/common/Markdown/index.tsx @@ -0,0 +1,164 @@ +import React, { useMemo } from 'react'; +import ReactMarkdown from 'react-markdown'; +import RemarkGfm from 'remark-gfm'; +import RehypeExternalLinks from 'rehype-external-links'; +import { Box, Code as ChakraCode, Image, Link } from '@chakra-ui/react'; + +type MarkdownProps = { + source: string; + className?: string; +}; + +/** + * 简化版 Markdown 组件 + * 用于渲染基础的 Markdown 内容(README 等) + */ +const Markdown = ({ source, className }: MarkdownProps) => { + const components = useMemo( + () => ({ + // 图片 + img: ({ src, alt }: any) => ( + + ), + // 链接 + a: ({ href, children }: any) => ( + + {children} + + ), + // 行内代码 + code: ({ children, className }: any) => { + // 如果有 className,说明是代码块,保留原样 + if (className) { + return ( + + {children} + + ); + } + // 行内代码 + return ( + + {children} + + ); + }, + // 标题 + h1: ({ children }: any) => ( + + {children} + + ), + h2: ({ children }: any) => ( + + {children} + + ), + h3: ({ children }: any) => ( + + {children} + + ), + // 段落 + p: ({ children }: any) => ( + + {children} + + ), + // 列表 + ul: ({ children }: any) => ( + + {children} + + ), + ol: ({ children }: any) => ( + + {children} + + ), + li: ({ children }: any) => ( + + {children} + + ), + // 引用 + blockquote: ({ children }: any) => ( + + {children} + + ), + // 水平线 + hr: () => , + // 表格 + table: ({ children }: any) => ( + + + {children} + + + ), + thead: ({ children }: any) => ( + + {children} + + ), + tbody: ({ children }: any) => {children}, + tr: ({ children }: any) => ( + + {children} + + ), + th: ({ children }: any) => ( + + {children} + + ), + td: ({ children }: any) => ( + + {children} + + ) + }), + [] + ); + + return ( + + + {source} + + + ); +}; + +export default React.memo(Markdown); diff --git a/packages/web/components/common/MyDrawer/MyRightDrawer.tsx b/packages/web/components/common/MyDrawer/MyRightDrawer.tsx index e2182964d04a..1ff08c136828 100644 --- a/packages/web/components/common/MyDrawer/MyRightDrawer.tsx +++ b/packages/web/components/common/MyDrawer/MyRightDrawer.tsx @@ -37,9 +37,9 @@ const MyRightDrawer = ({ diff --git a/packages/web/components/common/MySelect/MultipleSelect.tsx b/packages/web/components/common/MySelect/MultipleSelect.tsx index 4ba33c691fed..e0aca4665b9b 100644 --- a/packages/web/components/common/MySelect/MultipleSelect.tsx +++ b/packages/web/components/common/MySelect/MultipleSelect.tsx @@ -21,7 +21,7 @@ import type { useScrollPagination } from '../../../hooks/useScrollPagination'; import MyDivider from '../MyDivider'; import { shadowLight } from '../../../styles/theme'; import { isArray } from 'lodash'; -import { useMount } from 'ahooks'; +import { useMemoEnhance } from '../../../hooks/useMemoEnhance'; const menuItemStyles: MenuItemProps = { borderRadius: 'sm', @@ -43,7 +43,7 @@ export type SelectProps = { value: T; }[]; value: T[]; - isSelectAll: boolean; + isSelectAll?: boolean; setIsSelectAll?: React.Dispatch>; placeholder?: string; @@ -62,8 +62,14 @@ export type SelectProps = { tagStyle?: FlexProps; } & Omit; +type SelectedItemType = { + icon?: string; + label: string | React.ReactNode; + value: T; +}; + const MultipleSelect = ({ - value = [], + value: initialValue = [], placeholder, list = [], onSelect, @@ -90,32 +96,29 @@ const MultipleSelect = ({ const { isOpen, onOpen, onClose } = useDisclosure(); const canInput = setInputValue !== undefined; - type SelectedItemType = { - icon?: string; - label: string | React.ReactNode; - value: T; - }; + const [visibleItems, setVisibleItems] = useState[]>([]); + const [overflowItems, setOverflowItems] = useState[]>([]); - const [visibleItems, setVisibleItems] = useState([]); - const [overflowItems, setOverflowItems] = useState([]); + const formatValue = useMemoEnhance(() => { + return Array.isArray(initialValue) ? initialValue : []; + }, [initialValue]); const selectedItems = useMemo(() => { - if (!value || !isArray(value)) return []; - return value.map((val) => { + return formatValue.map((val) => { const listItem = list.find((item) => item.value === val); return listItem || { value: val, label: String(val) }; }); - }, [value, list]); + }, [formatValue, list]); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === 'Backspace' && (!inputValue || inputValue === '')) { - const newValue = [...value]; + const newValue = [...formatValue]; newValue.pop(); onSelect(newValue); } }, - [inputValue, value, onSelect] + [inputValue, formatValue, onSelect] ); useEffect(() => { if (!isOpen) { @@ -131,13 +134,13 @@ const MultipleSelect = ({ return; } - if (value.includes(val)) { - onSelect(value.filter((i) => i !== val)); + if (formatValue.includes(val)) { + onSelect(formatValue.filter((i) => i !== val)); } else { - onSelect([...value, val]); + onSelect([...formatValue, val]); } }, - [isSelectAll, value, onSelect, list, setIsSelectAll] + [isSelectAll, formatValue, onSelect, list, setIsSelectAll] ); const onSelectAll = useCallback(() => { @@ -264,7 +267,7 @@ const MultipleSelect = ({ return ( <> {list.map((item, i) => { - const isSelected = isSelectAll || value.includes(item.value); + const isSelected = isSelectAll || formatValue.includes(item.value); return ( ({ })} ); - }, [list, isSelectAll, value, onclickItem]); + }, [list, isSelectAll, formatValue, onclickItem]); return ( @@ -339,7 +342,7 @@ const MultipleSelect = ({ )} - {value.length === 0 && placeholder ? ( + {formatValue.length === 0 && placeholder ? ( {placeholder} @@ -440,24 +443,28 @@ const MultipleSelect = ({ maxH={'40vh'} overflowY={'auto'} > - { - e.stopPropagation(); - e.preventDefault(); - onSelectAll(); - }} - whiteSpace={'pre-wrap'} - fontSize={'sm'} - gap={2} - mb={1} - {...menuItemStyles} - > - - {t('common:All')} - - - + {setIsSelectAll && ( + <> + { + e.stopPropagation(); + e.preventDefault(); + onSelectAll(); + }} + whiteSpace={'pre-wrap'} + fontSize={'sm'} + gap={2} + mb={1} + {...menuItemStyles} + > + + {t('common:All')} + + + + + )} {ScrollData ? {ListRender} : ListRender} diff --git a/packages/web/components/common/Textarea/JsonEditor/index.tsx b/packages/web/components/common/Textarea/JsonEditor/index.tsx index 2c8adf1e5766..cc43302cbaf6 100644 --- a/packages/web/components/common/Textarea/JsonEditor/index.tsx +++ b/packages/web/components/common/Textarea/JsonEditor/index.tsx @@ -51,7 +51,6 @@ const options = { }; const JSONEditor = ({ - defaultValue, value, onChange, resize, @@ -70,6 +69,7 @@ const JSONEditor = ({ const completionRegisterRef = useRef(); const monaco = useMonaco(); const triggerChar = useRef(); + const monarchProviderRegistered = useRef(false); useEffect(() => { if (!monaco) return; @@ -130,22 +130,6 @@ const JSONEditor = ({ } }); - // 自定义语法高亮 - monaco.languages.setMonarchTokensProvider('json', { - tokenizer: { - root: [ - // 匹配variables里的变量 - [new RegExp(`{{(${variables.map((item) => item.key).join('|')})}}`), 'variable'], - [/".*?"/, 'string'], // 匹配字符串 - [/[{}\[\]]/, '@brackets'], // 匹配括号 - [/[0-9]+/, 'number'], // 匹配数字 - [/true|false/, 'keyword'], // 匹配布尔值 - [/:/, 'delimiter'], // 匹配冒号 - [/,/, 'delimiter.comma'] // 匹配逗号 - ] - } - }); - return () => { completionRegisterRef.current?.dispose(); }; @@ -199,34 +183,62 @@ const JSONEditor = ({ } }, [formatedValue, toast, t]); - const beforeMount = useCallback((monaco: Monaco) => { - monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ - validate: false, - allowComments: false, - schemas: [ - { - uri: 'http://myserver/foo-schema.json', // 一个假设的 URI - fileMatch: ['*'], // 匹配所有文件 - schema: {} // 空的 Schema + const beforeMount = useCallback( + (monaco: Monaco) => { + // 配置 JSON 语言诊断选项 + monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ + validate: false, + allowComments: false, + schemas: [ + { + uri: 'http://myserver/foo-schema.json', // 一个假设的 URI + fileMatch: ['*'], // 匹配所有文件 + schema: {} // 空的 Schema + } + ] + }); + + // 定义自定义主题 + monaco.editor.defineTheme('JSONEditorTheme', { + base: 'vs', // 可以基于已有的主题进行定制 + inherit: true, // 继承基础主题的设置 + rules: [{ token: 'variable', foreground: '2B5FD9' }], + colors: { + 'editor.background': '#ffffff00', + 'editorLineNumber.foreground': '#aaa', + 'editorOverviewRuler.border': '#ffffff00', + 'editor.lineHighlightBackground': '#F7F8FA', + 'scrollbarSlider.background': '#E8EAEC', + 'editorIndentGuide.activeBackground': '#ddd', + 'editorIndentGuide.background': '#eee' } - ] - }); + }); - monaco.editor.defineTheme('JSONEditorTheme', { - base: 'vs', // 可以基于已有的主题进行定制 - inherit: true, // 继承基础主题的设置 - rules: [{ token: 'variable', foreground: '2B5FD9' }], - colors: { - 'editor.background': '#ffffff00', - 'editorLineNumber.foreground': '#aaa', - 'editorOverviewRuler.border': '#ffffff00', - 'editor.lineHighlightBackground': '#F7F8FA', - 'scrollbarSlider.background': '#E8EAEC', - 'editorIndentGuide.activeBackground': '#ddd', - 'editorIndentGuide.background': '#eee' + // 注册自定义语法高亮(仅注册一次) + if (!monarchProviderRegistered.current) { + try { + monaco.languages.setMonarchTokensProvider('json', { + tokenizer: { + root: [ + // 匹配variables里的变量 + [new RegExp(`{{(${variables.map((item) => item.key).join('|')})}}`), 'variable'], + [/".*?"/, 'string'], // 匹配字符串 + [/[{}\[\]]/, '@brackets'], // 匹配括号 + [/[0-9]+/, 'number'], // 匹配数字 + [/true|false/, 'keyword'], // 匹配布尔值 + [/:/, 'delimiter'], // 匹配冒号 + [/,/, 'delimiter.comma'] // 匹配逗号 + ] + } + }); + monarchProviderRegistered.current = true; + } catch (error) { + console.warn('Failed to register Monaco Monarch token provider:', error); + } } - }); - }, []); + }, + [variables] + ); return ( { onChange?.(e || ''); diff --git a/packages/web/components/common/Textarea/PromptEditor/Editor.tsx b/packages/web/components/common/Textarea/PromptEditor/Editor.tsx index 2277f6b5c82b..606d9b54cb0d 100644 --- a/packages/web/components/common/Textarea/PromptEditor/Editor.tsx +++ b/packages/web/components/common/Textarea/PromptEditor/Editor.tsx @@ -125,12 +125,10 @@ export default function Editor({ nodes: [ VariableNode, VariableLabelNode, - HeadingNode, - ListNode, - ListItemNode, - QuoteNode, - CodeNode, - CodeHighlightNode + // Only register rich text nodes when in rich text mode + ...(isRichText + ? [HeadingNode, ListNode, ListItemNode, QuoteNode, CodeNode, CodeHighlightNode] + : []) ], editorState: textToEditorState(value, isRichText), onError: (error: Error) => { @@ -218,14 +216,18 @@ export default function Editor({ - {variableLabels.length > 0 && ( <> )} - {variableLabels.length > 0 && } + {variables.length > 0 && ( + <> + + {/* */} + + )} { diff --git a/packages/web/components/core/plugin/tool/TagFilterBox.tsx b/packages/web/components/core/plugin/tool/TagFilterBox.tsx new file mode 100644 index 000000000000..0a1ec44681d4 --- /dev/null +++ b/packages/web/components/core/plugin/tool/TagFilterBox.tsx @@ -0,0 +1,94 @@ +import { Box, Flex } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import { parseI18nString } from '@fastgpt/global/common/i18n/utils'; +import type { SystemPluginToolTagType } from '@fastgpt/global/core/plugin/type'; +import React, { useMemo } from 'react'; + +const ToolTagFilterBox = ({ + tags, + selectedTagIds, + onTagSelect, + size = 'base' +}: { + tags: SystemPluginToolTagType[]; + selectedTagIds: string[]; + onTagSelect: (tagIds: string[]) => void; + size?: 'base' | 'sm'; +}) => { + const { t, i18n } = useTranslation(); + + const toggleTag = (tagId: string) => { + if (selectedTagIds.includes(tagId)) { + onTagSelect(selectedTagIds.filter((id) => id !== tagId)); + } else { + onTagSelect([...selectedTagIds, tagId]); + } + }; + + const tagBaseStyles = useMemo(() => { + const sizeStyles = { + base: { + px: 3, + py: 1.5, + fontSize: 'sm' + }, + sm: { + px: 2, + py: 1, + fontSize: '11px' + } + }; + + return { + ...sizeStyles[size], + fontWeight: 'medium', + color: 'myGray.700', + border: '1px solid', + borderColor: 'myGray.200', + whiteSpace: 'nowrap', + flexShrink: 0, + cursor: 'pointer' + }; + }, [size]); + + return ( + + onTagSelect([])} + > + {t('common:All')} + + + + {tags.map((tag) => { + const isSelected = selectedTagIds.includes(tag.tagId); + return ( + toggleTag(tag.tagId)} + > + {t(parseI18nString(tag.tagName, i18n.language))} + + ); + })} + + + ); +}; + +export default React.memo(ToolTagFilterBox); diff --git a/packages/web/components/core/plugin/tool/ToolCard.tsx b/packages/web/components/core/plugin/tool/ToolCard.tsx new file mode 100644 index 000000000000..af5556fedcd4 --- /dev/null +++ b/packages/web/components/core/plugin/tool/ToolCard.tsx @@ -0,0 +1,232 @@ +import { Box, Button, Flex, HStack } from '@chakra-ui/react'; +import Avatar from '../../../common/Avatar'; +import MyBox from '../../../common/MyBox'; +import React, { useMemo, useRef, useState, useEffect } from 'react'; +import { useTranslation } from 'next-i18next'; +import MyIcon from '../../../common/Icon'; +import { parseI18nString } from '@fastgpt/global/common/i18n/utils'; +import { PluginStatusEnum } from '@fastgpt/global/core/plugin/type'; + +/* + 3 种使用场景: + 1. admin 视角插件市场:显示是否安装,无状态,显示安装/卸载 + 2. team 视角资源库:显示是否安装,状态文本,以及安装/卸载 + 3. 开放的插件市场:不显示任何状态,只显示下载按钮 +*/ +export type ToolCardItemType = { + id: string; + name: string; + description?: string; + icon?: string; + author?: string; + tags?: string[] | null; + downloadUrl?: string; + status?: number; + installed?: boolean; +}; + +const ToolCard = ({ + item, + systemTitle, + isLoading, + mode, + onClickButton, + onClickCard +}: { + item: ToolCardItemType; + systemTitle?: string; + isLoading?: boolean; + mode: 'admin' | 'team' | 'marketplace'; + onClickButton: (installed: boolean) => void; + onClickCard?: () => void; +}) => { + const { t, i18n } = useTranslation(); + const tagsContainerRef = useRef(null); + const [visibleTagsCount, setVisibleTagsCount] = useState(item.tags?.length || 0); + + useEffect(() => { + const calculate = () => { + const container = tagsContainerRef.current; + if (!container || !item.tags?.length) return; + + const containerWidth = container.offsetWidth; + const tagElements = container.querySelectorAll('[data-tag-item]'); + if (!containerWidth || !tagElements.length) return; + + let totalWidth = 0; + let count = 0; + + for (let i = 0; i < tagElements.length; i++) { + const width = totalWidth + (tagElements[i] as HTMLElement).offsetWidth + (i > 0 ? 4 : 0); + if (width + (i < tagElements.length - 1 ? 54 : 0) > containerWidth) break; + totalWidth = width; + count++; + } + + setVisibleTagsCount(Math.max(1, count)); + }; + + const timer = setTimeout(calculate, 0); + const observer = new ResizeObserver(calculate); + if (tagsContainerRef.current) observer.observe(tagsContainerRef.current); + + return () => { + clearTimeout(timer); + observer.disconnect(); + }; + }, [item.tags]); + + const statusMap = useMemo(() => { + if (mode === 'marketplace') return null; + + const pluginStatusMap: Record = + { + [PluginStatusEnum.Offline]: { + label: t('app:toolkit_status_offline'), + color: 'red.600' + }, + [PluginStatusEnum.SoonOffline]: { + label: t('app:toolkit_status_soon_offline'), + color: 'yellow.600' + } + }; + + const installedStatusMap = item.installed + ? { + label: t('app:toolkit_installed'), + color: 'myGray.500', + icon: 'common/check' + } + : null; + + if (mode === 'admin') { + return installedStatusMap; + } + + if (mode === 'team') { + if (item.status && pluginStatusMap[item.status]) { + return pluginStatusMap[item.status]; + } + return installedStatusMap; + } + }, [item.installed, item.status]); + + return ( + + + + + {parseI18nString(item.name, i18n.language)} + + {statusMap && ( + + {statusMap.icon && } + {statusMap.label} + + )} + + + + {parseI18nString(item.description || '', i18n.language) || + t('app:templateMarket.no_intro')} + + + + {item.tags?.slice(0, visibleTagsCount).map((tag) => { + return ( + + {tag} + + ); + })} + {item.tags && item.tags.length > visibleTagsCount && ( + + +{item.tags.length - visibleTagsCount} + + )} + + + + {`by ${item.author || systemTitle || 'FastGPT'}`} + {mode === 'marketplace' ? ( + + ) : ( + + )} + + + ); +}; + +export default React.memo(ToolCard); diff --git a/packages/web/components/core/plugin/tool/ToolDetailDrawer.tsx b/packages/web/components/core/plugin/tool/ToolDetailDrawer.tsx new file mode 100644 index 000000000000..690637116c40 --- /dev/null +++ b/packages/web/components/core/plugin/tool/ToolDetailDrawer.tsx @@ -0,0 +1,466 @@ +import React, { useMemo, useState, useEffect } from 'react'; +import { + Box, + Button, + Drawer, + DrawerBody, + DrawerContent, + DrawerHeader, + DrawerOverlay, + Flex, + VStack, + Accordion, + AccordionItem, + AccordionButton, + AccordionPanel, + AccordionIcon +} from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import Avatar from '../../../common/Avatar'; +import { parseI18nString } from '@fastgpt/global/common/i18n/utils'; +import MyIconButton from '../../../common/Icon/button'; +import LightRowTabs from '../../../common/Tabs/LightRowTabs'; +import type { + FlowNodeInputItemType, + FlowNodeOutputItemType +} from '@fastgpt/global/core/workflow/type/io'; +import { type ToolCardItemType } from './ToolCard'; +import MyBox from '../../../common/MyBox'; +import Markdown from '../../../common/Markdown'; +import type { ToolDetailType } from '@fastgpt/global/sdk/fastgpt-plugin'; +import MyIcon from '../../../common/Icon'; +import { FlowValueTypeMap } from '@fastgpt/global/core/workflow/node/constant'; + +type toolDetailType = ToolDetailType & { + versionList?: Array<{ + value: string; + description?: string; + inputs?: Array; + outputs?: Array; + }>; + courseUrl?: string; + readme?: string; + userGuide?: string; + currentCost?: number; + hasSystemSecret?: boolean; + secretInputConfig?: Array<{}>; + inputList?: Array; +}; + +const ParamSection = ({ + title, + params +}: { + title: string; + params: (FlowNodeInputItemType | FlowNodeOutputItemType)[]; +}) => { + const { i18n } = useTranslation(); + + return ( + + + + + {title} + + + {params.map((param, index) => { + const isInput = 'required' in param; + return ( + + + {isInput && param.required && ( + + * + + )} + + {parseI18nString(param.label || param.key, i18n.language)} + + + {/* @ts-ignore */} + {FlowValueTypeMap[param.valueType]?.label || 'String'} + + + {param.description && ( + + {parseI18nString(param.description, i18n.language)} + + )} + {index !== params.length - 1 && } + + ); + })} + + ); +}; + +const SubToolAccordionItem = ({ tool }: { tool: any }) => { + const { t, i18n } = useTranslation(); + + return ( + + + + + + {parseI18nString(tool.name, i18n.language)} + + + {tool.intro || parseI18nString(tool.description, i18n.language)} + + + + + + + + + + {!!tool?.currentCost ? ( + + {t('app:toolkit_call_points_label')} + {tool?.currentCost} + + ) : ( + t('app:toolkit_no_call_points') + )} + + {tool.versionList && tool.versionList.length > 0 && ( + + {tool.versionList[0]?.inputs && tool.versionList[0].inputs.length > 0 && ( + + )} + {tool.versionList[0]?.outputs && tool.versionList[0].outputs.length > 0 && ( + + )} + + )} + + + ); +}; + +const ToolDetailDrawer = ({ + onClose, + selectedTool, + onToggleInstall, + systemTitle, + onFetchDetail, + isLoading, + showPoint +}: { + onClose: () => void; + selectedTool: ToolCardItemType; + onToggleInstall: (installed: boolean) => void; + systemTitle?: string; + onFetchDetail?: ( + toolId: string + ) => Promise<{ tools: Array; downloadUrl: string }>; + isLoading?: boolean; + showPoint: boolean; +}) => { + const { t, i18n } = useTranslation(); + const [activeTab, setActiveTab] = useState<'guide' | 'params'>('params'); + const [toolDetail, setToolDetail] = useState< + { tools: Array; downloadUrl: string } | undefined + >(undefined); + const [loading, setLoading] = useState(false); + const [readmeContent, setReadmeContent] = useState(''); + + const isInstalled = useMemo(() => { + return selectedTool.status === 3; + }, [selectedTool.status]); + const isDownload = useMemo(() => { + return false; + // return selectedTool.status === ToolStatusEnum.Download; + }, [selectedTool.status]); + + useEffect(() => { + const fetchToolDetail = async () => { + if (onFetchDetail && selectedTool?.id) { + setLoading(true); + try { + const detail = await onFetchDetail(selectedTool.id); + setToolDetail(detail as any); + } finally { + setLoading(false); + } + } + }; + + fetchToolDetail(); + }, []); + + const isToolSet = useMemo(() => { + if (!toolDetail?.tools || !Array.isArray(toolDetail?.tools) || toolDetail?.tools.length === 0) { + return false; + } + const subTools = toolDetail?.tools.filter((subTool: any) => subTool.parentId); + return subTools.length > 0; + }, [toolDetail?.tools]); + + const parentTool = useMemo(() => { + const parentTool = toolDetail?.tools.find((tool: toolDetailType) => !tool.parentId); + return { + ...parentTool, + tags: selectedTool.tags + }; + }, [selectedTool.tags, toolDetail?.tools]); + const subTools = useMemo(() => { + if (!isToolSet || !toolDetail?.tools) return []; + return toolDetail?.tools.filter((subTool: toolDetailType) => !!subTool.parentId); + }, [isToolSet, toolDetail?.tools]); + + useEffect(() => { + const fetchReadme = async () => { + if (!toolDetail) return; + const readmeUrl = parentTool?.readme; + if (!readmeUrl) return; + + try { + const response = await fetch(readmeUrl); + if (!response.ok) { + throw new Error(`Failed to fetch README: ${response.status}`); + } + let content = await response.text(); + + const baseUrl = readmeUrl.substring(0, readmeUrl.lastIndexOf('/') + 1); + + content = content.replace( + /!\[([^\]]*)\]\(\.\/([^)]+)\)/g, + (match, alt, path) => `![${alt}](${baseUrl}${path})` + ); + content = content.replace( + /!\[([^\]]*)\]\((?!http|https|\/\/)([^)]+)\)/g, + (match, alt, path) => `![${alt}](${baseUrl}${path})` + ); + console.log(content); + + setReadmeContent(content); + } catch (error) { + console.error('Failed to fetch README:', error); + setReadmeContent(''); + } + }; + + fetchReadme(); + }, [parentTool?.readme]); + + return ( + + + + + + + + {parseI18nString(parentTool?.name || '', i18n.language)} + + + + + + + + + + {parentTool?.tags?.map((tag: string) => ( + + {tag} + + ))} + + + {parseI18nString(parentTool?.description || '', i18n.language)} + + + {`by ${parentTool?.author || systemTitle || 'FastGPT'}`} + + + + + + {showPoint && ( + + + {t('app:toolkit_call_points_label')} + + + {!!parentTool?.currentCost + ? parentTool?.currentCost + : t('app:toolkit_no_call_points')} + + + )} + + + + {t('app:toolkit_activation_label')} + + + {parentTool?.hasSystemSecret || + (parentTool?.secretInputConfig && parentTool?.secretInputConfig.length > 0) || + (parentTool?.inputList && parentTool?.inputList.length > 0) + ? t('app:toolkit_activation_required') + : t('app:toolkit_activation_not_required')} + + + + + { + if (value === 'guide' && parentTool?.courseUrl) { + window.open(parentTool?.courseUrl, '_blank'); + } else { + setActiveTab(value as 'guide' | 'params'); + } + }} + gap={4} + /> + + + + + {activeTab === 'guide' && ( + + {(readmeContent || parentTool?.userGuide) && ( + + + + )} + + )} + + {activeTab === 'params' && ( + + {isToolSet && subTools.length > 0 && ( + + {subTools.map((subTool: ToolDetailType) => ( + + ))} + + )} + + {!isToolSet && ( + <> + {parentTool?.versionList?.[0]?.inputs && + parentTool?.versionList?.[0]?.inputs.length > 0 && ( + + )} + {parentTool?.versionList?.[0]?.outputs && + parentTool?.versionList?.[0]?.outputs.length > 0 && ( + + )} + + )} + + )} + + + + + + ); +}; + +export default React.memo(ToolDetailDrawer); diff --git a/packages/web/core/workflow/constants.ts b/packages/web/core/workflow/constants.ts index bc1227bef155..4d6b1f2e9dfb 100644 --- a/packages/web/core/workflow/constants.ts +++ b/packages/web/core/workflow/constants.ts @@ -1,6 +1,5 @@ import { FlowNodeTemplateTypeEnum } from '@fastgpt/global/core/workflow/constants'; import { i18nT } from '../../i18n/utils'; -import type { SystemToolGroupSchemaType } from '../../../service/core/app/plugin/type'; import { AppTemplateTypeEnum } from '@fastgpt/global/core/app/constants'; import { type TemplateTypeSchemaType } from '@fastgpt/global/core/app/type'; @@ -31,14 +30,6 @@ export const workflowSystemNodeTemplateList: { } ]; -export const defaultGroup: SystemToolGroupSchemaType = { - groupId: 'systemPlugin', - groupAvatar: 'core/app/type/pluginLight', - groupName: i18nT('app:core.module.template.System Tools'), - groupOrder: 0, - groupTypes: [] // from getPluginGroups -}; - export const defaultTemplateTypes: TemplateTypeSchemaType[] = [ { typeName: i18nT('common:templateTags.Writing'), diff --git a/packages/web/hooks/useI18n.ts b/packages/web/hooks/useI18n.ts index 016fbbda4bed..ac26bcd0bd15 100644 --- a/packages/web/hooks/useI18n.ts +++ b/packages/web/hooks/useI18n.ts @@ -37,7 +37,17 @@ export const useI18nLng = () => { }; const onChangeLng = async (lng: string) => { - const lang = languageMap[lng] || 'en'; + let lang = languageMap[lng]; + + // 如果没有直接映射,尝试智能回退 + if (!lang) { + const langPrefix = lng.split('-')[0]; + // 中文相关语言优先回退到简体中文 + if (langPrefix === 'zh') { + lang = LangEnum.zh_CN; + } + } + const prevLang = getLang(); setLang(lang); @@ -54,7 +64,17 @@ export const useI18nLng = () => { if (getLang() && !forceGetDefaultLng) return onChangeLng(getLang() as string); - const lang = languageMap[navigator.language] || 'en'; + // 尝试精确匹配浏览器语言 + let lang = languageMap[navigator.language]; + + // 如果没有精确匹配,尝试匹配语言前缀 + if (!lang) { + const browserLangPrefix = navigator.language.split('-')[0]; + // 中文语言环境下优先回退到简体中文 + if (browserLangPrefix === 'zh') { + lang = LangEnum.zh_CN; + } + } // currentLng not in userLang return onChangeLng(lang); diff --git a/packages/web/hooks/usePagination.tsx b/packages/web/hooks/usePagination.tsx index e8f5f87e110c..545a271536a6 100644 --- a/packages/web/hooks/usePagination.tsx +++ b/packages/web/hooks/usePagination.tsx @@ -68,6 +68,7 @@ export function usePagination( const { t } = useTranslation(); const [isLoading, { setTrue, setFalse }] = useBoolean(false); + const [error, setError] = useState(null); const [pageNum, setPageNum] = useState(numPage); const [pageSize, setPageSize] = useState(defaultPageSize); @@ -88,6 +89,7 @@ export function usePagination( if (noMore && num !== 1) return; setTrue(); + setError(null); try { const res = await api({ @@ -139,6 +141,7 @@ export function usePagination( onChange?.(num); } catch (error: any) { + setError(error); if (error.code !== 'ERR_CANCELED') { toast({ title: getErrText(error, t('common:core.chat.error.data_error')), @@ -372,6 +375,7 @@ export function usePagination( data, setData, isLoading, + error, Pagination, ScrollData, getData: fetchData, diff --git a/packages/web/hooks/useStep.tsx b/packages/web/hooks/useStep.tsx index 2d33c8c24a1a..251ca195adf1 100644 --- a/packages/web/hooks/useStep.tsx +++ b/packages/web/hooks/useStep.tsx @@ -1,9 +1,7 @@ import { Box, Flex, - IconButton, Step, - StepDescription, StepIcon, StepIndicator, StepNumber, diff --git a/packages/web/i18n/en/app.json b/packages/web/i18n/en/app.json index 13e74b501362..1a8abc5bc2bf 100644 --- a/packages/web/i18n/en/app.json +++ b/packages/web/i18n/en/app.json @@ -60,6 +60,7 @@ "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.", + "click_to_config": "Go to configuration", "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", @@ -68,6 +69,7 @@ "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!", + "confirm_delete_tool": "Confirm to delete this tool?", "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", @@ -86,10 +88,40 @@ "cron.every_month": "Run Monthly", "cron.every_week": "Run Weekly", "cron.interval": "Run at Intervals", + "custom_plugin": "Custom Plugin", + "custom_plugin_associated_plugin_label": "Associated Plugin", + "custom_plugin_associated_plugin_placeholder": "Enter plugin name or appId to search", + "custom_plugin_associated_plugin_required": "Associated applications cannot be empty", + "custom_plugin_author_label": "Author Name", + "custom_plugin_author_placeholder": "Default to system name", + "custom_plugin_call_price_label": "Call Price (n points/time)", + "custom_plugin_click_upload_avatar": "Click to upload avatar", + "custom_plugin_config_success": "Configuration successful", + "custom_plugin_config_title": "{{name}} Configuration", + "custom_plugin_create": "Create Plugin", + "custom_plugin_default_installed_label": "Default Installed", + "custom_plugin_delete_success": "Delete successful", + "custom_plugin_duplicate": "Duplicate ID", + "custom_plugin_has_token_fee_label": "Charge Token Fee", + "custom_plugin_intro_label": "Introduction", + "custom_plugin_intro_placeholder": "Add an introduction for this application", + "custom_plugin_name_label": "Name", + "custom_plugin_name_required": "Application name cannot be empty", + "custom_plugin_plugin_status_label": "Plugin Status", + "custom_plugin_tags_label": "Tags", + "custom_plugin_tags_max_limit": "You can only select up to 3 tags", + "custom_plugin_update": "Update", + "custom_plugin_upload_failed": "Upload failed", + "custom_plugin_uploaded": "Uploaded", + "custom_plugin_uploading": "Uploading...", + "custom_plugin_user_guide_label": "User Guide", + "custom_plugin_user_guide_placeholder": "Use markdown syntax", "dataset": "dataset", "dataset_search_tool_description": "Call the \"Semantic Search\" and \"Full-text Search\" capabilities to find reference content that may be related to the problem from the \"Knowledge Base\". \nPrioritize calling this tool to assist in answering user questions.", + "dataset_select": "Optional knowledge base", "day": "Day", "deleted": "App deleted", + "document": "document", "document_quote": "Document Reference", "document_quote_tip": "Usually used to accept user-uploaded document content (requires document parsing), and can also be used to reference other string data.", "document_upload": "Document Upload", @@ -99,15 +131,19 @@ "execute_time": "Execution Time", "export_config_successful": "Configuration copied, some sensitive information automatically filtered. Please check for any remaining sensitive data.", "export_configs": "Export", + "fastgpt_marketplace": "FastGPT plug-in market", "feedback_count": "User Feedback", "file_quote_link": "Files", "file_recover": "File will overwrite current content", + "file_types": "Optional file types", "file_upload": "File Upload", "file_upload_tip": "Once enabled, documents/images can be uploaded. Documents are retained for 7 days, images for 15 days. Using this feature may incur additional costs. To ensure a good experience, please choose an AI model with a larger context length when using this feature.", + "find_more_tools": "Explore more", "go_to_chat": "Go to Conversation", "go_to_run": "Go to Execution", "http_toolset_add_tips": "Click the \"Add\" button to add tools", "http_toolset_config_tips": "Click \"Start Configuration\" to add tools", + "image": "picture", "image_upload": "Image Upload", "image_upload_tip": "How to activate model image recognition capabilities", "import_configs": "Import", @@ -126,6 +162,7 @@ "llm_not_support_vision": "This model does not support image recognition", "llm_use_vision": "Vision", "llm_use_vision_tip": "After clicking on the model selection, you can see whether the model supports image recognition and the ability to control whether to start image recognition. \nAfter starting image recognition, the model will read the image content in the file link, and if the user question is less than 500 words, it will automatically parse the image in the user question.", + "local_upload": "Local upload", "log_chat_logs": "Dialogue log", "log_detail": "Log details", "logs_app_data": "Data board", @@ -153,7 +190,7 @@ "logs_keys_customFeedback": "Custom Feedback", "logs_keys_errorCount": "Error Count", "logs_keys_feedback": "User Feedback", - "logs_keys_lastConversationTime": "Last Conversation Time", + "logs_keys_lastConversationTime": "last conversation time", "logs_keys_messageCount": "Message Count", "logs_keys_points": "Points Consumed", "logs_keys_responseTime": "Average Response Time", @@ -223,6 +260,8 @@ "plugin_cost_per_times": "{{cost}} points/time", "plugin_dispatch": "Plugin Invocation", "plugin_dispatch_tip": "Adds extra capabilities to the model. The specific plugins to be invoked will be autonomously decided by the model.\nIf a plugin is selected, the Dataset invocation will automatically be treated as a special plugin.", + "plugin_offline_tips": "Your system does not have direct access to plugin market data,\n\nPlease manually copy the URL and go to get more plugins", + "plugin_offline_url": "URL", "pro_modal_feature_1": "External organization structure integration and multi-tenancy", "pro_modal_feature_2": "Team-exclusive application showcase page", "pro_modal_feature_3": "Knowledge base enhanced indexing", @@ -293,10 +332,76 @@ "tool_detail": "Tool details", "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_offset_tips": "This tool is no longer available and will interrupt application operation. Please replace it immediately.", "tool_params_description_tips": "The description of parameter functions, if used as tool invocation parameters, affects the model tool invocation effect.", "tool_run_free": "This tool runs without points consumption", + "tool_soon_offset_tips": "This tool will be offline in the future. For the sake of your business stability, please replace it as soon as possible.", "tool_tip": "When executed as a tool, is this field used as a tool response result?", "tool_type_tools": "tool", + "toolkit": "Resource library", + "toolkit_activation_label": "Key Activation", + "toolkit_activation_not_required": "No Activation", + "toolkit_activation_required": "Activation Required", + "toolkit_add_resource": "Add resources", + "toolkit_author": "Author", + "toolkit_basic_config": "Basic Configuration", + "toolkit_basic_info": "Basic Information", + "toolkit_call_points": "Call Points", + "toolkit_call_points_label": "Call Points", + "toolkit_config_system_key": "Configure System Key", + "toolkit_contribute_resource": "Contribute Resource", + "toolkit_default_install": "Default Install", + "toolkit_import_resource": "Import/update resources", + "toolkit_inputs": "Input Parameters", + "toolkit_install": "Install", + "toolkit_installed": "Installed", + "toolkit_key_price": "Key Price", + "toolkit_marketplace_download": "Download", + "toolkit_marketplace_download_count": "{{count}} downloads", + "toolkit_marketplace_faq": "FAQ", + "toolkit_marketplace_search_placeholder": "Search tools", + "toolkit_marketplace_submit_request": "Submit Request", + "toolkit_marketplace_title": "Explore more plugins", + "toolkit_name": "Name", + "toolkit_no_call_points": "This tool does not require call points", + "toolkit_no_params_info": "No parameter information available", + "toolkit_no_plugins": "No plugins", + "toolkit_no_user_guide": "No user guide available", + "toolkit_official": "Official", + "toolkit_open_marketplace": "Open Marketplace", + "toolkit_outputs": "Output Parameters", + "toolkit_params_description": "Parameters", + "toolkit_plugin_status": "Plugin Status", + "toolkit_select_app": "Select an existing app", + "toolkit_status": "Status", + "toolkit_status_normal": "Normal", + "toolkit_status_offline": "Offline", + "toolkit_status_soon_offline": "Soon Offline", + "toolkit_system_key": "System Key", + "toolkit_system_key_configured": "Configured", + "toolkit_system_key_cost": "System Key Price (n points/time)", + "toolkit_system_key_enable": "Enable System Key", + "toolkit_system_key_not_configured": "Not Configured", + "toolkit_system_key_tip": "For tools that require a key, you can configure a system key. Users can use the system key by paying points.", + "toolkit_system_tool_config": "System Tool Configuration", + "toolkit_tags": "Tags", + "toolkit_tags_add": "Add Tag", + "toolkit_tags_delete_confirm": "Confirm delete this tag?", + "toolkit_tags_duplicate_name": "Duplicate tag name", + "toolkit_tags_enter_name": "Enter tag name", + "toolkit_tags_manage": "tag management", + "toolkit_tags_manage_title": "Plugin Tag Management", + "toolkit_tags_name": "Tag name", + "toolkit_tags_total": "Total {{count}} tags", + "toolkit_token_fee": "Token Points", + "toolkit_token_fee_tip": "After enabling this switch, users need to pay the Token points in the plugin when using it, and call points will also be charged at the same time", + "toolkit_tool_config": "{{name}} Configuration", + "toolkit_tool_list": "Tool List", + "toolkit_tool_name": "Tool Name", + "toolkit_uninstall": "Uninstall", + "toolkit_uninstalled": "Uninstalled", + "toolkit_update_failed": "Update failed", + "toolkit_user_guide": "User Guide", "tools_no_description": "This tool has not been introduced ~", "transition_to_workflow": "Convert to Workflow", "transition_to_workflow_create_new_placeholder": "Create a new app instead of modifying the current app", @@ -337,8 +442,18 @@ "type.hidden": "Hide app", "type_not_recognized": "App type not recognized", "un_auth": "No permission", + "upload_file_exists_filtered": "Duplicate files have been automatically filtered.", + "upload_file_extension_type_canSelectAudio": "Audio", + "upload_file_extension_type_canSelectCustomFileExtension": "Custom file extension type", + "upload_file_extension_type_canSelectCustomFileExtension_placeholder": "file extension name", + "upload_file_extension_type_canSelectFile": "Documents", + "upload_file_extension_type_canSelectImg": "Image", + "upload_file_extension_type_canSelectVideo": "Video", + "upload_file_extension_types": "Supported file types", "upload_file_max_amount": "Maximum File Quantity", "upload_file_max_amount_tip": "Maximum number of files uploaded in a single round of conversation", + "upload_method": "Upload method", + "url_upload": "File link", "variable.internal_type_desc": "Use only inside the workflow and will not appear in the dialog box", "variable.select type_desc": "The input box will be displayed in the site conversation and run preview, and this variable will not be displayed in the sharing link.", "variable.textarea_type_desc": "Allows users to input up to 4000 characters in the dialogue box.", @@ -362,6 +477,8 @@ "workflow.form_input_description_placeholder": "For example: \nAdd your information", "workflow.form_input_tip": " This module can configure multiple inputs to guide users in entering specific content.", "workflow.input_description_tip": "You can add a description to explain to users what they need to input", + "workflow.plugin_offline_error": "This plugin is offline and cannot be run", + "workflow.plugin_soon_offline_warning": "This plugin will be offline soon, please replace it as soon as possible", "workflow.read_files": "Document Parse", "workflow.read_files_result": "Document Parsing Result", "workflow.read_files_result_desc": "Original document text, consisting of file names and document content, separated by hyphens between multiple files.", diff --git a/packages/web/i18n/en/chat.json b/packages/web/i18n/en/chat.json index ea59e6ec2a9d..2e708049a48b 100644 --- a/packages/web/i18n/en/chat.json +++ b/packages/web/i18n/en/chat.json @@ -12,6 +12,7 @@ "chat_test_app": "Debug-{{name}}", "citations": "{{num}} References", "click_contextual_preview": "Click to see contextual preview", + "click_to_add_url": "Click to add link", "completion_finish_close": "Disconnection", "completion_finish_content_filter": "Trigger safe wind control", "completion_finish_function_call": "Function Calls", @@ -23,6 +24,7 @@ "config_input_guide": "Set Up Input Guide", "config_input_guide_lexicon": "Set Up Lexicon", "config_input_guide_lexicon_title": "Set Up Lexicon", + "confirm_to_clear_share_chat_history": "Are you sure you want to clear all chat history?", "content_empty": "No Content", "contextual": "{{num}} Contexts", "contextual_preview": "Contextual Preview {{num}} Items", @@ -75,6 +77,7 @@ "response.child total points": "Sub-workflow point consumption", "response.dataset_concat_length": "Combined total", "response.node_inputs": "Node Inputs", + "response.node_name": "Node name", "response_embedding_model": "Vector model", "response_embedding_model_tokens": "Vector Model Tokens", "response_hybrid_weight": "Embedding : Full text = {{emb}} : {{text}}", diff --git a/packages/web/i18n/en/common.json b/packages/web/i18n/en/common.json index 0a11bc546d54..f7ad51b87dc7 100644 --- a/packages/web/i18n/en/common.json +++ b/packages/web/i18n/en/common.json @@ -824,6 +824,11 @@ "error.inheritPermissionError": "Inherit permission Error", "error.invalid_params": "Invalid parameter", "error.missingParams": "Insufficient parameters", + "error.s3_upload_auth_failed": "No permission to upload file", + "error.s3_upload_bucket_not_found": "bucket not found", + "error.s3_upload_file_too_large": "File must be smaller than {{max}}", + "error.s3_upload_network_error": "Network abnormality", + "error.s3_upload_timeout": "Upload timed out", "error.send_auth_code_too_frequently": "Please do not obtain verification code frequently", "error.too_many_request": "Too many request", "error.unKnow": "An Unexpected Error Occurred", @@ -870,6 +875,7 @@ "input.Repeat Value": "Duplicate Value", "input_name": "Enter a Name", "invalid_time": "Validity period", + "invalid_url": "Invalid URL format", "invalid_variable": "Invalid Variable", "is_open": "Is Open", "is_requesting": "Requesting...", @@ -914,10 +920,12 @@ "name_is_empty": "Name Cannot Be Empty", "navbar.Account": "Account", "navbar.Chat": "Chat", + "navbar.Config": "administrator", "navbar.Datasets": "Dataset", "navbar.Studio": "Studio", "navbar.Toolkit": "Toolkit", "navbar.Tools": "Tools", + "navbar.toolkit": "Plug-in library", "new_create": "Create New", "next_step": "Next", "no": "No", @@ -1220,7 +1228,7 @@ "support.wallet.usage.Time": "Generation Time", "support.wallet.usage.Token Length": "Token Length", "support.wallet.usage.Total": "Total Amount", - "support.wallet.usage.Total points": "AI Points Consumption", + "support.wallet.usage.Total points": "Total credit", "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", diff --git a/packages/web/i18n/zh-CN/app.json b/packages/web/i18n/zh-CN/app.json index b41320f1e704..8af072f1848c 100644 --- a/packages/web/i18n/zh-CN/app.json +++ b/packages/web/i18n/zh-CN/app.json @@ -62,6 +62,7 @@ "chat_debug": "调试预览", "chat_logs": "对话日志", "chat_logs_tips": "日志会记录该应用的在线、分享和 API(需填写 chatId)对话记录", + "click_to_config": "去配置", "code_applied_successfully": "代码应用成功", "code_function_describe": "描述你的代码功能,输入“/”可选择变量", "config_ai_model_params": "点击配置 AI 模型相关属性", @@ -70,6 +71,7 @@ "confirm_copy_app_tip": "系统将为您创建一个相同配置应用,但权限不会进行复制,请确认!", "confirm_del_app_tip": "确认删除 【{{name}}】 及其所有聊天记录?", "confirm_delete_folder_tip": "确认删除该文件夹?将会删除它下面所有应用及对应的聊天记录,请确认!", + "confirm_delete_tool": "确认删除该工具?", "copilot_config_message": "`当前节点配置信息: \n代码类型:{{codeType}} \n当前代码: \\`\\`\\`{{codeType}} \n{{code}} \\`\\`\\` \n输入参数: {{inputs}} \n输出参数: {{outputs}}`", "copilot_confirm_message": "已接收到原始配置,了解当前代码结构和输入输出参数。请说明您的优化需求。", "copy_one_app": "创建副本", @@ -88,10 +90,40 @@ "cron.every_month": "每月执行", "cron.every_week": "每周执行", "cron.interval": "间隔执行", + "custom_plugin": "自定义插件", + "custom_plugin_associated_plugin_label": "关联插件", + "custom_plugin_associated_plugin_placeholder": "输入插件名或 appId 查找插件", + "custom_plugin_associated_plugin_required": "关联应用不能为空", + "custom_plugin_author_label": "作者名称", + "custom_plugin_author_placeholder": "默认为系统名", + "custom_plugin_call_price_label": "调用价格 (n积分/次)", + "custom_plugin_click_upload_avatar": "点击上传头像", + "custom_plugin_config_success": "配置成功", + "custom_plugin_config_title": "{{name}}配置", + "custom_plugin_create": "新建插件", + "custom_plugin_default_installed_label": "默认安装", + "custom_plugin_delete_success": "删除成功", + "custom_plugin_has_token_fee_label": "是否收取 Token 费用", + "custom_plugin_intro_label": "介绍", + "custom_plugin_intro_placeholder": "为这个应用添加一个介绍", + "custom_plugin_name_label": "取个名字", + "custom_plugin_name_required": "应用名不能为空", + "custom_plugin_parsing": "解析中...", + "custom_plugin_plugin_status_label": "插件状态", + "custom_plugin_tags_label": "标签", + "custom_plugin_tags_max_limit": "最多只能选择3个标签", + "custom_plugin_update": "更新", + "custom_plugin_upload_failed": "上传失败", + "custom_plugin_uploaded": "已上传", + "custom_plugin_uploading": "上传中...", + "custom_plugin_user_guide_label": "使用说明", + "custom_plugin_user_guide_placeholder": "使用 markdown 语法", "dataset": "知识库", "dataset_search_tool_description": "调用“语义检索”和“全文检索”能力,从“知识库”中查找可能与问题相关的参考内容。优先调用该工具来辅助回答用户的问题。", + "dataset_select": "可选知识库", "day": "日", "deleted": "应用已删除", + "document": "文档", "document_quote": "文档引用", "document_quote_tip": "通常用于接受用户上传的文档内容(这需要文档解析),也可以用于引用其他字符串数据。", "document_upload": "文档上传", @@ -102,15 +134,19 @@ "execute_time": "执行时间", "export_config_successful": "已复制配置,自动过滤部分敏感信息,请注意检查是否仍有敏感数据", "export_configs": "导出配置", + "fastgpt_marketplace": "FastGPT 插件市场", "feedback_count": "用户反馈", "file_quote_link": "文件链接", "file_recover": "文件将覆盖当前内容", + "file_types": "可选文件类型", "file_upload": "文件上传", "file_upload_tip": "开启后,可以上传文档/图片。文档保留7天,图片保留15天。使用该功能可能产生较多额外费用。为保证使用体验,使用该功能时,请选择上下文长度较大的AI模型。", + "find_more_tools": "探索更多", "go_to_chat": "去对话", "go_to_run": "去运行", "http_toolset_add_tips": "点击添加按钮来添加工具", "http_toolset_config_tips": "点击开始配置来添加工具", + "image": "图片", "image_upload": "图片上传", "image_upload_tip": "如何启动模型图片识别能力", "import_configs": "导入配置", @@ -129,6 +165,7 @@ "llm_not_support_vision": "该模型不支持图片识别", "llm_use_vision": "图片识别", "llm_use_vision_tip": "点击模型选择后,可以看到模型是否支持图片识别以及控制是否启动图片识别的能力。启动图片识别后,模型会读取文件链接里图片内容,并且如果用户问题少于 500 字,会自动解析用户问题中的图片。", + "local_upload": "本地上传", "log_chat_logs": "对话日志", "log_detail": "日志详情", "logs_app_data": "数据看板", @@ -157,7 +194,7 @@ "logs_keys_customFeedback": "自定义反馈", "logs_keys_errorCount": "报错数量", "logs_keys_feedback": "用户反馈", - "logs_keys_lastConversationTime": "上次对话时间", + "logs_keys_lastConversationTime": "最后对话时间", "logs_keys_messageCount": "消息总数", "logs_keys_points": "积分消耗", "logs_keys_responseTime": "平均响应时长", @@ -235,6 +272,8 @@ "plugin_cost_per_times": "{{cost}} 积分/次", "plugin_dispatch": "插件调用", "plugin_dispatch_tip": "给模型附加获取外部数据的能力,具体调用哪些插件,将由模型自主决定,所有插件都将以非流模式运行。\n若选择了插件,知识库调用将自动作为一个特殊的插件。", + "plugin_offline_tips": "您的系统无法直接访问插件市场数据,\n请手动复制网址并前往,以获得更多插件", + "plugin_offline_url": "网址", "pro_modal_feature_1": "外部组织架构接入与多租户", "pro_modal_feature_2": "团队专属的应用展示页", "pro_modal_feature_3": "知识库增强索引", @@ -309,10 +348,76 @@ "tool_detail": "工具详情", "tool_input_param_tip": "该插件正常运行需要配置相关信息", "tool_not_active": "该工具尚未激活", + "tool_offset_tips": "该工具已无法使用,将中断应用运行,请立即替换", "tool_params_description_tips": "参数功能的描述,若作为工具调用参数,影响模型工具调用效果", "tool_run_free": "该工具运行无积分消耗", + "tool_soon_offset_tips": "该工具将在后续下线,为了您的业务稳定,请尽快替换", "tool_tip": "作为工具执行时,该字段是否作为工具响应结果", "tool_type_tools": "工具", + "toolkit": "资源库", + "toolkit_activation_label": "密钥激活", + "toolkit_activation_not_required": "免激活", + "toolkit_activation_required": "需要激活", + "toolkit_add_resource": "添加插件", + "toolkit_author": "作者", + "toolkit_basic_config": "基础配置", + "toolkit_basic_info": "基本信息", + "toolkit_call_points": "调用积分", + "toolkit_call_points_label": "调用积分", + "toolkit_config_system_key": "是否配置系统密钥", + "toolkit_contribute_resource": "贡献插件", + "toolkit_default_install": "默认安装", + "toolkit_import_resource": "导入/更新插件", + "toolkit_inputs": "输入参数", + "toolkit_install": "安装", + "toolkit_installed": "已安装", + "toolkit_key_price": "密钥价格", + "toolkit_marketplace_download": "下载", + "toolkit_marketplace_download_count": "{{count}} 次下载", + "toolkit_marketplace_faq": "常见问题", + "toolkit_marketplace_search_placeholder": "搜索插件", + "toolkit_marketplace_submit_request": "提交需求", + "toolkit_marketplace_title": "探索更多插件", + "toolkit_name": "名称", + "toolkit_no_call_points": "该工具无需调用积分", + "toolkit_no_params_info": "暂无参数信息", + "toolkit_no_plugins": "暂无插件", + "toolkit_no_user_guide": "暂无使用说明", + "toolkit_official": "官方", + "toolkit_open_marketplace": "打开插件市场", + "toolkit_outputs": "输出参数", + "toolkit_params_description": "参数说明", + "toolkit_plugin_status": "插件状态", + "toolkit_select_app": "选择现有应用", + "toolkit_status": "状态", + "toolkit_status_normal": "正常", + "toolkit_status_offline": "已下线", + "toolkit_status_soon_offline": "即将下线", + "toolkit_system_key": "系统密钥", + "toolkit_system_key_configured": "已配置", + "toolkit_system_key_cost": "系统密钥价格(n积分/次)", + "toolkit_system_key_enable": "是否配置系统密钥", + "toolkit_system_key_not_configured": "未配置", + "toolkit_system_key_tip": "对于需要密钥的工具,您可为其配置系统密钥,用户可通过支付积分的方式使用系统密钥。", + "toolkit_system_tool_config": "系统工具配置", + "toolkit_tags": "标签", + "toolkit_tags_add": "添加标签", + "toolkit_tags_delete_confirm": "确认删除该标签?", + "toolkit_tags_duplicate_name": "标签名重复", + "toolkit_tags_enter_name": "输入标签名称", + "toolkit_tags_manage": "标签管理", + "toolkit_tags_manage_title": "工具标签管理", + "toolkit_tags_name": "标签名称", + "toolkit_tags_total": "共 {{count}} 个标签", + "toolkit_token_fee": "Token 积分", + "toolkit_token_fee_tip": "开启该开关后,用户使用该插件,需要支付插件中Token的积分,并且同时会收取调用积分", + "toolkit_tool_config": "{{name}}配置", + "toolkit_tool_list": "工具列表", + "toolkit_tool_name": "工具名", + "toolkit_uninstall": "卸载", + "toolkit_uninstalled": "未安装", + "toolkit_update_failed": "更新失败", + "toolkit_user_guide": "使用说明", "tools_no_description": "这个工具没有介绍~", "transition_to_workflow": "转成工作流", "transition_to_workflow_create_new_placeholder": "创建一个新的应用,而不是修改当前应用", @@ -354,8 +459,18 @@ "type.hidden": "隐藏应用", "type_not_recognized": "未识别到应用类型", "un_auth": "无权限", + "upload_file_exists_filtered": "已自动过滤重复文件", + "upload_file_extension_type_canSelectAudio": "音频", + "upload_file_extension_type_canSelectCustomFileExtension": "自定义文件扩展类型", + "upload_file_extension_type_canSelectCustomFileExtension_placeholder": "文件扩展名", + "upload_file_extension_type_canSelectFile": "文档", + "upload_file_extension_type_canSelectImg": "图片", + "upload_file_extension_type_canSelectVideo": "视频", + "upload_file_extension_types": "支持上传的类型", "upload_file_max_amount": "最大文件数量", "upload_file_max_amount_tip": "单轮对话中最大上传文件数量", + "upload_method": "上传方式", + "url_upload": "文件链接", "variable.internal_type_desc": "仅在工作流内部使用,不会出现在对话框中", "variable.select type_desc": "会在站内对话和运行预览中显示输入框,在分享链接中不会显示此变量", "variable.textarea_type_desc": "允许用户最多输入4000字的对话框。", @@ -379,6 +494,8 @@ "workflow.form_input_description_placeholder": "例如:\n补充您的信息", "workflow.form_input_tip": "该模块可以配置多种输入,引导用户输入特定内容。", "workflow.input_description_tip": "你可以添加一段说明文字,用以向用户说明需要输入的内容", + "workflow.plugin_offline_error": "该插件已下线,无法运行", + "workflow.plugin_soon_offline_warning": "该插件即将下线,建议尽快更换", "workflow.read_files": "文档解析", "workflow.read_files_result": "文档解析结果", "workflow.read_files_result_desc": "文档原文,由文件名和文档内容组成,多个文件之间通过横线隔开。", diff --git a/packages/web/i18n/zh-CN/chat.json b/packages/web/i18n/zh-CN/chat.json index 780c686ebf27..3123d35761e0 100644 --- a/packages/web/i18n/zh-CN/chat.json +++ b/packages/web/i18n/zh-CN/chat.json @@ -12,6 +12,7 @@ "chat_test_app": "调试-{{name}}", "citations": "{{num}}条引用", "click_contextual_preview": "点击查看上下文预览", + "click_to_add_url": "点击添加链接", "completion_finish_close": "连接断开", "completion_finish_content_filter": "触发安全风控", "completion_finish_function_call": "函数调用", @@ -23,6 +24,7 @@ "config_input_guide": "配置输入引导", "config_input_guide_lexicon": "配置词库", "config_input_guide_lexicon_title": "配置词库", + "confirm_to_clear_share_chat_history": "确认清空所有聊天记录?", "content_empty": "内容为空", "contextual": "{{num}}条上下文", "contextual_preview": "上下文预览 {{num}} 条", @@ -75,6 +77,7 @@ "response.child total points": "子工作流积分消耗", "response.dataset_concat_length": "合并后总数", "response.node_inputs": "节点输入", + "response.node_name": "节点名", "response_embedding_model": "向量模型", "response_embedding_model_tokens": "向量模型 Tokens", "response_hybrid_weight": "语义检索 : 全文检索 = {{emb}} : {{text}}", diff --git a/packages/web/i18n/zh-CN/common.json b/packages/web/i18n/zh-CN/common.json index 9d5590285153..57c4b1a3363b 100644 --- a/packages/web/i18n/zh-CN/common.json +++ b/packages/web/i18n/zh-CN/common.json @@ -206,6 +206,7 @@ "confirm_update": "确认更新", "contact_way": "通知接收", "contribute_app_template": "贡献模板", + "copy_link": "复制链接", "copy_successful": "复制成功", "copy_to_clipboard": "复制到剪贴板", "core.Chat": "对话", @@ -342,7 +343,7 @@ "core.chat.Feedback Submit": "提交反馈", "core.chat.Feedback Success": "反馈成功!", "core.chat.Finish Speak": "语音输入完成", - "core.chat.History": "记录", + "core.chat.History": "历史记录", "core.chat.History Amount": "{{amount}} 条记录", "core.chat.Mark": "标注预期回答", "core.chat.Mark Description": "当前标注功能为测试版。\n\n点击添加标注后,需要选择一个知识库,以便存储标注数据。你可以通过该功能快速的标注问题和预期回答,以便引导模型下次的回答。\n\n目前,标注功能同知识库其他数据一样,受模型的影响,不代表标注后 100% 符合预期。\n\n标注数据仅单向与知识库同步,如果知识库修改了该标注数据,日志展示的标注数据无法同步。", @@ -825,6 +826,11 @@ "error.inheritPermissionError": "权限继承错误", "error.invalid_params": "参数无效", "error.missingParams": "参数缺失", + "error.s3_upload_auth_failed": "无权上传文件", + "error.s3_upload_bucket_not_found": "找不到存储桶", + "error.s3_upload_file_too_large": "文件需小于 {{max}}", + "error.s3_upload_network_error": "网络异常", + "error.s3_upload_timeout": "上传超时", "error.send_auth_code_too_frequently": "请勿频繁获取验证码", "error.too_many_request": "请求太频繁了,请稍后重试", "error.unKnow": "出现了点意外~", @@ -871,6 +877,7 @@ "input.Repeat Value": "有重复的值", "input_name": "取个名字", "invalid_time": "有效期", + "invalid_url": "无效的 URL 格式", "invalid_variable": "无效变量", "is_open": "是否开启", "is_requesting": "请求中……", @@ -915,10 +922,12 @@ "name_is_empty": "名称不能为空", "navbar.Account": "账号", "navbar.Chat": "聊天", + "navbar.Config": "管理员", "navbar.Datasets": "知识库", "navbar.Studio": "工作台", "navbar.Toolkit": "工具箱", "navbar.Tools": "工具", + "navbar.toolkit": "插件库", "new_create": "新建", "next_step": "下一步", "no": "否", @@ -1222,7 +1231,7 @@ "support.wallet.usage.Time": "生成时间", "support.wallet.usage.Token Length": "token 长度", "support.wallet.usage.Total": "总金额", - "support.wallet.usage.Total points": "AI 积分消耗", + "support.wallet.usage.Total points": "AI 积分总消耗", "support.wallet.usage.Usage Detail": "使用详情", "support.wallet.usage.Whisper": "语音输入", "sure_delete_tool_cannot_undo": "是否确认删除该工具?该操作无法撤回", diff --git a/packages/web/i18n/zh-Hant/app.json b/packages/web/i18n/zh-Hant/app.json index a13316d8d062..23d462959c92 100644 --- a/packages/web/i18n/zh-Hant/app.json +++ b/packages/web/i18n/zh-Hant/app.json @@ -60,6 +60,7 @@ "chat_debug": "聊天預覽", "chat_logs": "對話紀錄", "chat_logs_tips": "紀錄會記錄此應用程式的線上、分享和 API(需填寫 chatId)對話紀錄", + "click_to_config": "去配置", "code_applied_successfully": "代碼應用成功", "code_function_describe": "描述你的代碼功能,輸入“/”可選擇變量", "config_ai_model_params": "點選設定 AI 模型相關屬性", @@ -68,6 +69,7 @@ "confirm_copy_app_tip": "系統將為您建立一個相同設定的應用程式,但權限不會複製,請確認!", "confirm_del_app_tip": "確認刪除【{{name}}】及其所有聊天紀錄?", "confirm_delete_folder_tip": "確認刪除這個資料夾?將會刪除它底下所有應用程式及對應的對話紀錄,請確認!", + "confirm_delete_tool": "確認刪除該工具?", "copilot_config_message": "當前節點配置信息: \n代碼類型:{{codeType}} \n當前代碼: \\\\`\\\\`\\\\`{{codeType}} \n{{code}} \\\\`\\\\`\\\\` \n輸入參數: {{inputs}} \n輸出參數: {{outputs}}", "copilot_confirm_message": "已接收到原始配置,了解當前代碼結構和輸入輸出參數。\n請說明您的優化需求。", "copy_one_app": "建立副本", @@ -86,10 +88,39 @@ "cron.every_month": "每月執行", "cron.every_week": "每週執行", "cron.interval": "間隔執行", + "custom_plugin": "自訂外掛", + "custom_plugin_associated_plugin_label": "關聯外掛", + "custom_plugin_associated_plugin_placeholder": "輸入外掛名稱或 appId 尋找外掛", + "custom_plugin_associated_plugin_required": "關聯應用不能為空", + "custom_plugin_author_label": "作者名稱", + "custom_plugin_author_placeholder": "預設為系統名稱", + "custom_plugin_call_price_label": "呼叫價格 (n積分/次)", + "custom_plugin_click_upload_avatar": "點擊上傳頭像", + "custom_plugin_config_success": "配置成功", + "custom_plugin_config_title": "{{name}}配置", + "custom_plugin_create": "新建外掛", + "custom_plugin_default_installed_label": "預設安裝", + "custom_plugin_delete_success": "刪除成功", + "custom_plugin_has_token_fee_label": "是否收取 Token 費用", + "custom_plugin_intro_label": "介紹", + "custom_plugin_intro_placeholder": "為這個應用程式新增一個介紹", + "custom_plugin_name_label": "取個名字", + "custom_plugin_name_required": "應用程式名稱不能為空", + "custom_plugin_plugin_status_label": "外掛狀態", + "custom_plugin_tags_label": "標籤", + "custom_plugin_tags_max_limit": "最多只能選擇3個標籤", + "custom_plugin_update": "更新", + "custom_plugin_upload_failed": "上傳失敗", + "custom_plugin_uploaded": "已上傳", + "custom_plugin_uploading": "上傳中...", + "custom_plugin_user_guide_label": "使用說明", + "custom_plugin_user_guide_placeholder": "使用 markdown 語法", "dataset": "知識庫", "dataset_search_tool_description": "呼叫「語意搜尋」和「全文搜尋」功能,從「知識庫」中尋找可能與問題相關的參考內容。優先呼叫這個工具來協助回答使用者的問題。", + "dataset_select": "可選知識庫", "day": "日", "deleted": "應用已刪除", + "document": "文件", "document_quote": "文件引用", "document_quote_tip": "通常用於接受使用者上傳的文件內容(這需要文件解析),也可以用於引用其他字串資料。", "document_upload": "文件上傳", @@ -99,15 +130,19 @@ "execute_time": "執行時間", "export_config_successful": "已複製設定,自動過濾部分敏感資訊,請注意檢查是否仍有敏感資料", "export_configs": "匯出設定", + "fastgpt_marketplace": "FastGPT 插件市場", "feedback_count": "使用者回饋", "file_quote_link": "檔案連結", "file_recover": "檔案將會覆蓋目前內容", + "file_types": "可選文件類型", "file_upload": "檔案上傳", "file_upload_tip": "開啟後,可以上傳文件/圖片。文件保留 7 天,圖片保留 15 天。使用這個功能可能產生較多額外費用。為了確保使用體驗,使用這個功能時,請選擇上下文長度較大的 AI 模型。", + "find_more_tools": "探索更多", "go_to_chat": "前往對話", "go_to_run": "前往執行", "http_toolset_add_tips": "點擊添加按鈕來添加工具", "http_toolset_config_tips": "點擊開始配置來添加工具", + "image": "圖片", "image_upload": "圖片上傳", "image_upload_tip": "如何啟用模型圖片辨識功能", "import_configs": "匯入設定", @@ -126,6 +161,7 @@ "llm_not_support_vision": "這個模型不支援圖片辨識", "llm_use_vision": "圖片辨識", "llm_use_vision_tip": "點選模型選擇後,可以看到模型是否支援圖片辨識以及控制是否啟用圖片辨識的功能。啟用圖片辨識後,模型會讀取檔案連結中的圖片內容,並且如果使用者問題少於 500 字,會自動解析使用者問題中的圖片。", + "local_upload": "本地上傳", "log_chat_logs": "對話日誌", "log_detail": "日誌詳情", "logs_app_data": "數據看板", @@ -153,7 +189,7 @@ "logs_keys_customFeedback": "自訂回饋", "logs_keys_errorCount": "錯誤數量", "logs_keys_feedback": "使用者回饋", - "logs_keys_lastConversationTime": "上次對話時間", + "logs_keys_lastConversationTime": "最後對話時間", "logs_keys_messageCount": "訊息總數", "logs_keys_points": "積分消耗", "logs_keys_responseTime": "平均回應時長", @@ -223,6 +259,8 @@ "plugin_cost_per_times": "{{cost}} 積分/次", "plugin_dispatch": "外掛呼叫", "plugin_dispatch_tip": "賦予模型取得外部資料的能力,具體呼叫哪些外掛,將由模型自主決定,所有外掛都將以非串流模式執行。\n若選擇了外掛,知識庫呼叫將自動作為一個特殊的外掛。", + "plugin_offline_tips": "您的系統無法直接訪問插件市場數據,\n請手動複製網址並前往,以獲得更多插件", + "plugin_offline_url": "網址", "pro_modal_feature_1": "外部組織架構接入與多租戶", "pro_modal_feature_2": "團隊專屬的應用展示頁", "pro_modal_feature_3": "知識庫增強索引", @@ -293,10 +331,75 @@ "tool_detail": "工具詳情", "tool_input_param_tip": "這個外掛正常執行需要設定相關資訊", "tool_not_active": "該工具尚未激活", + "tool_offset_tips": "該工具已無法使用,將中斷應用運行,請立即替換", "tool_params_description_tips": "參數功能的描述,若作為工具調用參數,影響模型工具調用效果", "tool_run_free": "該工具運行無積分消耗", + "tool_soon_offset_tips": "該工具將在後續下線,為了您的業務穩定,請盡快替換", "tool_tip": "作為工具執行時,該字段是否作為工具響應結果", "tool_type_tools": "工具", + "toolkit": "資源庫", + "toolkit_activation_label": "密鑰激活", + "toolkit_activation_not_required": "免激活", + "toolkit_activation_required": "需要激活", + "toolkit_add_resource": "添加資源", + "toolkit_author": "作者", + "toolkit_basic_config": "基礎配置", + "toolkit_basic_info": "基本信息", + "toolkit_call_points": "調用積分", + "toolkit_call_points_label": "調用積分", + "toolkit_config_system_key": "是否配置系統密鑰", + "toolkit_contribute_resource": "貢獻資源", + "toolkit_default_install": "默認安裝", + "toolkit_import_resource": "導入/更新資源", + "toolkit_inputs": "輸入參數", + "toolkit_install": "安裝", + "toolkit_installed": "已安裝", + "toolkit_key_price": "密鑰價格", + "toolkit_marketplace_download": "下載", + "toolkit_marketplace_download_count": "{{count}} 次下載", + "toolkit_marketplace_faq": "常見問題", + "toolkit_marketplace_search_placeholder": "搜索插件", + "toolkit_marketplace_submit_request": "提交需求", + "toolkit_marketplace_title": "探索更多資源", + "toolkit_name": "名稱", + "toolkit_no_call_points": "該工具無需調用積分", + "toolkit_no_params_info": "暫無參數信息", + "toolkit_no_plugins": "暫無插件", + "toolkit_no_user_guide": "暫無使用說明", + "toolkit_official": "官方", + "toolkit_open_marketplace": "打開資源市場", + "toolkit_outputs": "輸出參數", + "toolkit_params_description": "參數說明", + "toolkit_plugin_status": "插件狀態", + "toolkit_select_app": "選擇現有應用", + "toolkit_status": "狀態", + "toolkit_status_normal": "正常", + "toolkit_status_offline": "下線", + "toolkit_status_soon_offline": "即將下線", + "toolkit_system_key": "系統密鑰", + "toolkit_system_key_configured": "已配置", + "toolkit_system_key_cost": "系統密鑰價格(n積分/次)", + "toolkit_system_key_enable": "是否配置系統密鑰", + "toolkit_system_key_not_configured": "未配置", + "toolkit_system_key_tip": "對於需要密鑰的工具,您可為其配置系統密鑰,用戶可通過支付積分的方式使用系統密鑰。", + "toolkit_system_tool_config": "系統工具配置", + "toolkit_tags": "標籤", + "toolkit_tags_add": "添加標籤", + "toolkit_tags_delete_confirm": "確認刪除該標籤?", + "toolkit_tags_enter_name": "輸入標籤名稱", + "toolkit_tags_manage": "標籤管理", + "toolkit_tags_manage_title": "工具標籤管理", + "toolkit_tags_name": "標籤名稱", + "toolkit_tags_total": "共 {{count}} 個標籤", + "toolkit_token_fee": "Token 積分", + "toolkit_token_fee_tip": "開啟該開關後,用戶使用該插件,需要支付插件中Token的積分,並且同時會收取調用積分", + "toolkit_tool_config": "{{name}}配置", + "toolkit_tool_list": "工具列表", + "toolkit_tool_name": "工具名", + "toolkit_uninstall": "卸載", + "toolkit_uninstalled": "未安裝", + "toolkit_update_failed": "更新失敗", + "toolkit_user_guide": "使用說明", "tools_no_description": "這個工具沒有介紹~", "transition_to_workflow": "轉換成工作流程", "transition_to_workflow_create_new_placeholder": "建立新的應用程式,而不是修改目前應用程式", @@ -337,8 +440,18 @@ "type.hidden": "隱藏應用", "type_not_recognized": "未識別到應用程式類型", "un_auth": "無權限", + "upload_file_exists_filtered": "已自動過濾重複檔案", + "upload_file_extension_type_canSelectAudio": "音頻", + "upload_file_extension_type_canSelectCustomFileExtension": "自定義文件擴展類型", + "upload_file_extension_type_canSelectCustomFileExtension_placeholder": "文件擴展名", + "upload_file_extension_type_canSelectFile": "文檔", + "upload_file_extension_type_canSelectImg": "圖片", + "upload_file_extension_type_canSelectVideo": "視頻", + "upload_file_extension_types": "支持上傳的類型", "upload_file_max_amount": "最大檔案數量", "upload_file_max_amount_tip": "單輪對話中最大上傳檔案數量", + "upload_method": "上傳方式", + "url_upload": "文件鏈接", "variable.internal_type_desc": "僅在工作流內部使用,不會出現在對話框中", "variable.select type_desc": "會在站內對話和運行預覽中顯示輸入框,在分享鏈接中不會顯示此變量", "variable.textarea_type_desc": "允許使用者最多輸入 4000 字的對話框。", @@ -362,6 +475,8 @@ "workflow.form_input_description_placeholder": "例如:\n補充您的資訊", "workflow.form_input_tip": "這個模組可以設定多種輸入,引導使用者輸入特定內容。", "workflow.input_description_tip": "您可以新增一段說明文字,用來向使用者說明需要輸入的內容", + "workflow.plugin_offline_error": "該插件已下線,無法運行", + "workflow.plugin_soon_offline_warning": "該插件即將下線,建議儘快更換", "workflow.read_files": "檔案解析", "workflow.read_files_result": "檔案解析結果", "workflow.read_files_result_desc": "檔案原文,由檔案名稱和檔案內容組成,多個檔案之間透過橫線分隔。", diff --git a/packages/web/i18n/zh-Hant/chat.json b/packages/web/i18n/zh-Hant/chat.json index b67743ff0af5..ff04f633df24 100644 --- a/packages/web/i18n/zh-Hant/chat.json +++ b/packages/web/i18n/zh-Hant/chat.json @@ -12,6 +12,7 @@ "chat_test_app": "除錯-{{name}}", "citations": "{{num}} 筆引用", "click_contextual_preview": "點選檢視上下文預覽", + "click_to_add_url": "點擊添加鏈接", "completion_finish_close": "連接斷開", "completion_finish_content_filter": "觸發安全風控", "completion_finish_function_call": "函式呼叫", @@ -23,6 +24,7 @@ "config_input_guide": "設定輸入導引", "config_input_guide_lexicon": "設定詞彙庫", "config_input_guide_lexicon_title": "設定詞彙庫", + "confirm_to_clear_share_chat_history": "確認清空所有聊天記錄?", "content_empty": "無內容", "contextual": "{{num}} 筆上下文", "contextual_preview": "上下文預覽 {{num}} 筆", @@ -75,6 +77,7 @@ "response.child total points": "子工作流程點數消耗", "response.dataset_concat_length": "合併總數", "response.node_inputs": "節點輸入", + "response.node_name": "節點名", "response_embedding_model": "向量模型", "response_embedding_model_tokens": "向量模型 Tokens", "response_hybrid_weight": "語義檢索 : 全文檢索 = {{emb}} : {{text}}", diff --git a/packages/web/i18n/zh-Hant/common.json b/packages/web/i18n/zh-Hant/common.json index 0a3ee744ad26..7a1b638c98e1 100644 --- a/packages/web/i18n/zh-Hant/common.json +++ b/packages/web/i18n/zh-Hant/common.json @@ -823,6 +823,11 @@ "error.inheritPermissionError": "繼承權限錯誤", "error.invalid_params": "參數無效", "error.missingParams": "參數不足", + "error.s3_upload_auth_failed": "無權上傳文件", + "error.s3_upload_bucket_not_found": "找不到存儲桶", + "error.s3_upload_file_too_large": "文件需小於 {{max}}", + "error.s3_upload_network_error": "網絡異常", + "error.s3_upload_timeout": "上傳超時", "error.send_auth_code_too_frequently": "請勿頻繁取得驗證碼", "error.too_many_request": "請求太頻繁了,請稍後重試", "error.unKnow": "發生未預期的錯誤", @@ -869,6 +874,7 @@ "input.Repeat Value": "重複的值", "input_name": "輸入名稱", "invalid_time": "有效期", + "invalid_url": "無效的 URL 格式", "invalid_variable": "無效變數", "is_open": "是否開啟", "is_requesting": "請求中...", @@ -917,6 +923,7 @@ "navbar.Studio": "工作區", "navbar.Toolkit": "工具箱", "navbar.Tools": "工具", + "navbar.toolkit": "插件庫", "new_create": "建立新項目", "next_step": "下一步", "no": "否", @@ -1219,7 +1226,7 @@ "support.wallet.usage.Time": "產生時間", "support.wallet.usage.Token Length": "Token 長度", "support.wallet.usage.Total": "總金額", - "support.wallet.usage.Total points": "AI 點數消耗", + "support.wallet.usage.Total points": "AI 積分總消耗", "support.wallet.usage.Usage Detail": "使用詳細資訊", "support.wallet.usage.Whisper": "語音輸入", "sure_delete_tool_cannot_undo": "是否確認刪除該工具?\n該操作無法撤回", diff --git a/packages/web/package.json b/packages/web/package.json index 72e2d09d9e15..9f3e6ea2106f 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -41,7 +41,10 @@ "react-photo-view": "^1.2.6", "recharts": "^2.15.0", "use-context-selector": "^1.4.4", - "zustand": "^4.3.5" + "zustand": "^4.3.5", + "react-markdown": "^9.0.1", + "rehype-external-links": "^3.0.0", + "remark-gfm": "^4.0.1" }, "devDependencies": { "@types/js-cookie": "^3.0.5", diff --git a/packages/web/styles/theme.ts b/packages/web/styles/theme.ts index 73e134b57198..ba50380ef549 100644 --- a/packages/web/styles/theme.ts +++ b/packages/web/styles/theme.ts @@ -80,6 +80,24 @@ const Button = defineStyleConfig({ w: '30px', borderRadius: 'sm' }, + base: { + fontSize: 'sm', + px: '4', + py: 0, + h: '34px', + minH: '34px', + fontWeight: 'medium', + borderRadius: 'sm' + }, + baseSquare: { + fontSize: 'sm', + px: '0', + py: 0, + h: '34px', + w: '34px', + fontWeight: 'medium', + borderRadius: 'sm' + }, md: { fontSize: 'sm', px: '4', @@ -306,7 +324,7 @@ const Button = defineStyleConfig({ } }, defaultProps: { - size: 'md', + size: 'base', variant: 'primary' } }); @@ -876,7 +894,7 @@ export const theme = extendTheme({ xxl: '1.25rem' }, shadows: { - 1: '0px 1px 2px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)', + 1: '0 1px 2px 0 rgba(19, 51, 107, 0.05), 0 0 1px 0 rgba(19, 51, 107, 0.08)', 1.5: '0px 1px 2px 0px rgba(19, 51, 107, 0.10), 0px 0px 1px 0px rgba(19, 51, 107, 0.15)', 2: '0px 4px 4px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)', 3: '0px 4px 10px 0px rgba(19, 51, 107, 0.08), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 694b01e7e37b..ffba76559a1f 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.19 - version: 0.1.19(@types/node@20.14.0) + specifier: 0.2.13 + version: 0.2.13(@types/node@20.14.0) axios: specifier: ^1.12.1 version: 1.12.1 @@ -258,6 +258,9 @@ importers: node-xlsx: specifier: ^0.24.0 version: 0.24.0 + p-limit: + specifier: ^7.2.0 + version: 7.2.0 papaparse: specifier: 5.4.1 version: 5.4.1 @@ -289,9 +292,12 @@ importers: specifier: ^3.17.0 version: 3.17.0 zod: - specifier: ^3.24.2 - version: 3.25.51 + specifier: ^4.1.12 + version: 4.1.12 devDependencies: + '@types/async-retry': + specifier: ^1.4.9 + version: 1.4.9 '@types/cookie': specifier: ^0.5.2 version: 0.5.4 @@ -436,12 +442,21 @@ importers: react-i18next: specifier: 14.1.2 version: 14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-markdown: + specifier: ^9.0.1 + version: 9.1.0(@types/react@18.3.1)(react@18.3.1) react-photo-view: specifier: ^1.2.6 version: 1.2.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) recharts: specifier: ^2.15.0 version: 2.15.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rehype-external-links: + specifier: ^3.0.0 + version: 3.0.0 + remark-gfm: + specifier: ^4.0.1 + version: 4.0.1 use-context-selector: specifier: ^1.4.4 version: 1.4.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(scheduler@0.26.0) @@ -645,8 +660,8 @@ importers: specifier: ^1.4.4 version: 1.4.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(scheduler@0.26.0) zod: - specifier: ^3.24.2 - version: 3.24.2 + specifier: ^4.1.12 + version: 4.1.12 devDependencies: '@next/bundle-analyzer': specifier: ^15.5.6 @@ -706,6 +721,85 @@ importers: specifier: ^3.0.2 version: 3.1.1(@types/debug@4.1.12)(@types/node@20.17.24)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.1) + projects/marketplace: + dependencies: + '@chakra-ui/anatomy': + specifier: 2.2.1 + version: 2.2.1 + '@chakra-ui/icons': + specifier: 2.1.1 + version: 2.1.1(@chakra-ui/system@2.6.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(react@18.3.1))(react@18.3.1) + '@chakra-ui/next-js': + specifier: 2.4.2 + version: 2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.33(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1) + '@chakra-ui/react': + specifier: 2.10.7 + version: 2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@chakra-ui/styled-system': + specifier: 2.9.1 + version: 2.9.1 + '@chakra-ui/system': + specifier: 2.6.1 + version: 2.6.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(react@18.3.1) + '@fastgpt/global': + specifier: workspace:* + version: link:../../packages/global + '@fastgpt/service': + specifier: workspace:* + version: link:../../packages/service + '@fastgpt/web': + specifier: workspace:* + version: link:../../packages/web + i18next: + specifier: 23.16.8 + version: 23.16.8 + next: + specifier: 14.2.33 + version: 14.2.33(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) + next-i18next: + specifier: 15.4.2 + version: 15.4.2(i18next@23.16.8)(next@14.2.33(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + react: + specifier: 18.3.1 + version: 18.3.1 + react-dom: + specifier: 18.3.1 + version: 18.3.1(react@18.3.1) + react-i18next: + specifier: 14.1.2 + version: 14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + zod: + specifier: ^4.1.12 + version: 4.1.12 + devDependencies: + '@svgr/webpack': + specifier: ^6.5.1 + version: 6.5.1 + '@types/node': + specifier: ^20 + version: 20.17.24 + '@types/react': + specifier: ^18 + version: 18.3.1 + '@types/react-dom': + specifier: ^18 + version: 18.3.0 + eslint: + specifier: ^8 + version: 8.57.1 + eslint-config-next: + specifier: 14.2.33 + version: 14.2.33(eslint@8.57.1)(typescript@5.8.2) + postcss: + specifier: ^8 + version: 8.5.6 + tailwindcss: + specifier: ^3.4.1 + version: 3.4.18(tsx@4.20.6)(yaml@2.8.1) + typescript: + specifier: ^5 + version: 5.8.2 + projects/mcp_server: dependencies: '@fastgpt/global': @@ -830,6 +924,10 @@ importers: packages: + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + '@ampproject/remapping@2.3.0': resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} @@ -2234,8 +2332,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.19': - resolution: {integrity: sha512-1UY7fcTy9Ve/SoH8dxeYOX+0uyvqVLwQDrQGpqRt8QbCdCsnH7ohqBbgZLITuq2UbgF4s9EN0BUNGtTEtEmNCw==} + '@fastgpt-sdk/plugin@0.2.13': + resolution: {integrity: sha512-P0Rq3rYNr3HC6Op68YUFSF0un7ZwgwwLxtygITQfy4XqWTWzMogb4npXJPMWiD89f3WvkdqPkVZfFEZzg2wzJQ==} '@fastify/accept-negotiator@1.1.0': resolution: {integrity: sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==} @@ -2989,18 +3087,30 @@ packages: '@next/env@14.2.32': resolution: {integrity: sha512-n9mQdigI6iZ/DF6pCTwMKeWgF2e8lg7qgt5M7HXMLtyhZYMnf/u905M18sSpPmHL9MKp9JHo56C6jrD2EvWxng==} + '@next/env@14.2.33': + resolution: {integrity: sha512-CgVHNZ1fRIlxkLhIX22flAZI/HmpDaZ8vwyJ/B0SDPTBuLZ1PJ+DWMjCHhqnExfmSQzA/PbZi8OAc7PAq2w9IA==} + '@next/env@15.3.5': resolution: {integrity: sha512-7g06v8BUVtN2njAX/r8gheoVffhiKFVt4nx74Tt6G4Hqw9HCLYQVx/GkH2qHvPtAHZaUNZ0VXAa0pQP6v1wk7g==} '@next/eslint-plugin-next@14.2.26': resolution: {integrity: sha512-SPEj1O5DAVTPaWD9XPupelfT2APNIgcDYD2OzEm328BEmHaglhmYNUvxhzfJYDr12AgAfW4V3UHSV93qaeELJA==} + '@next/eslint-plugin-next@14.2.33': + resolution: {integrity: sha512-DQTJFSvlB+9JilwqMKJ3VPByBNGxAGFTfJ7BuFj25cVcbBy7jm88KfUN+dngM4D3+UxZ8ER2ft+WH9JccMvxyg==} + '@next/swc-darwin-arm64@14.2.32': resolution: {integrity: sha512-osHXveM70zC+ilfuFa/2W6a1XQxJTvEhzEycnjUaVE8kpUS09lDpiDDX2YLdyFCzoUbvbo5r0X1Kp4MllIOShw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] + '@next/swc-darwin-arm64@14.2.33': + resolution: {integrity: sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + '@next/swc-darwin-arm64@15.3.5': resolution: {integrity: sha512-lM/8tilIsqBq+2nq9kbTW19vfwFve0NR7MxfkuSUbRSgXlMQoJYg+31+++XwKVSXk4uT23G2eF/7BRIKdn8t8w==} engines: {node: '>= 10'} @@ -3013,6 +3123,12 @@ packages: cpu: [x64] os: [darwin] + '@next/swc-darwin-x64@14.2.33': + resolution: {integrity: sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + '@next/swc-darwin-x64@15.3.5': resolution: {integrity: sha512-WhwegPQJ5IfoUNZUVsI9TRAlKpjGVK0tpJTL6KeiC4cux9774NYE9Wu/iCfIkL/5J8rPAkqZpG7n+EfiAfidXA==} engines: {node: '>= 10'} @@ -3025,6 +3141,12 @@ packages: cpu: [arm64] os: [linux] + '@next/swc-linux-arm64-gnu@14.2.33': + resolution: {integrity: sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + '@next/swc-linux-arm64-gnu@15.3.5': resolution: {integrity: sha512-LVD6uMOZ7XePg3KWYdGuzuvVboxujGjbcuP2jsPAN3MnLdLoZUXKRc6ixxfs03RH7qBdEHCZjyLP/jBdCJVRJQ==} engines: {node: '>= 10'} @@ -3037,6 +3159,12 @@ packages: cpu: [arm64] os: [linux] + '@next/swc-linux-arm64-musl@14.2.33': + resolution: {integrity: sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + '@next/swc-linux-arm64-musl@15.3.5': resolution: {integrity: sha512-k8aVScYZ++BnS2P69ClK7v4nOu702jcF9AIHKu6llhHEtBSmM2zkPGl9yoqbSU/657IIIb0QHpdxEr0iW9z53A==} engines: {node: '>= 10'} @@ -3049,6 +3177,12 @@ packages: cpu: [x64] os: [linux] + '@next/swc-linux-x64-gnu@14.2.33': + resolution: {integrity: sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + '@next/swc-linux-x64-gnu@15.3.5': resolution: {integrity: sha512-2xYU0DI9DGN/bAHzVwADid22ba5d/xrbrQlr2U+/Q5WkFUzeL0TDR963BdrtLS/4bMmKZGptLeg6282H/S2i8A==} engines: {node: '>= 10'} @@ -3061,6 +3195,12 @@ packages: cpu: [x64] os: [linux] + '@next/swc-linux-x64-musl@14.2.33': + resolution: {integrity: sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + '@next/swc-linux-x64-musl@15.3.5': resolution: {integrity: sha512-TRYIqAGf1KCbuAB0gjhdn5Ytd8fV+wJSM2Nh2is/xEqR8PZHxfQuaiNhoF50XfY90sNpaRMaGhF6E+qjV1b9Tg==} engines: {node: '>= 10'} @@ -3073,6 +3213,12 @@ packages: cpu: [arm64] os: [win32] + '@next/swc-win32-arm64-msvc@14.2.33': + resolution: {integrity: sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + '@next/swc-win32-arm64-msvc@15.3.5': resolution: {integrity: sha512-h04/7iMEUSMY6fDGCvdanKqlO1qYvzNxntZlCzfE8i5P0uqzVQWQquU1TIhlz0VqGQGXLrFDuTJVONpqGqjGKQ==} engines: {node: '>= 10'} @@ -3085,12 +3231,24 @@ packages: cpu: [ia32] os: [win32] + '@next/swc-win32-ia32-msvc@14.2.33': + resolution: {integrity: sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + '@next/swc-win32-x64-msvc@14.2.32': resolution: {integrity: sha512-2N0lSoU4GjfLSO50wvKpMQgKd4HdI2UHEhQPPPnlgfBJlOgJxkjpkYBqzk08f1gItBB6xF/n+ykso2hgxuydsA==} engines: {node: '>= 10'} cpu: [x64] os: [win32] + '@next/swc-win32-x64-msvc@14.2.33': + resolution: {integrity: sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + '@next/swc-win32-x64-msvc@15.3.5': resolution: {integrity: sha512-5fhH6fccXxnX2KhllnGhkYMndhOiLOLEiVGYjP2nizqeGWkN10sA9taATlXwake2E2XMvYZjjz0Uj7T0y+z1yw==} engines: {node: '>= 10'} @@ -3936,6 +4094,9 @@ packages: '@tybys/wasm-util@0.9.0': resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} + '@types/async-retry@1.4.9': + resolution: {integrity: sha512-s1ciZQJzRh3708X/m3vPExr5KJlzlZJvXsKpbtE2luqNcbROr64qU+3KpJsYHqWMeaxI839OvXf9PrUSw1Xtyg==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -4224,6 +4385,9 @@ packages: '@types/request-ip@0.0.37': resolution: {integrity: sha512-uw6/i3rQnpznxD7LtLaeuZytLhKZK6bRoTS6XVJlwxIOoOpEBU7bgKoVXDNtOg4Xl6riUKHa9bjMVrL6ESqYlQ==} + '@types/retry@0.12.5': + resolution: {integrity: sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==} + '@types/semver@7.5.8': resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} @@ -4757,6 +4921,9 @@ packages: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} @@ -4767,6 +4934,9 @@ packages: arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -5077,6 +5247,10 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + camelcase@5.3.1: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} @@ -5476,6 +5650,11 @@ packages: resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} engines: {node: '>= 6'} + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + csso@4.2.0: resolution: {integrity: sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==} engines: {node: '>=8.0.0'} @@ -5851,6 +6030,9 @@ packages: dezalgo@1.0.4: resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -5873,6 +6055,9 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -6100,6 +6285,15 @@ packages: typescript: optional: true + eslint-config-next@14.2.33: + resolution: {integrity: sha512-e2W+waB+I5KuoALAtKZl3WVDU4Q1MS6gF/gdcwHh0WOAkHf4TZI6dPjd25wKhlZFAsFrVKy24Z7/IwOhn8dHBw==} + peerDependencies: + eslint: ^7.23.0 || ^8.0.0 + typescript: '>=3.3.1' + peerDependenciesMeta: + typescript: + optional: true + eslint-import-resolver-node@0.3.9: resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} @@ -7457,6 +7651,10 @@ packages: node-notifier: optional: true + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + jiti@2.6.0: resolution: {integrity: sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ==} hasBin: true @@ -7735,6 +7933,10 @@ packages: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -8411,6 +8613,9 @@ packages: resolution: {integrity: sha512-M6DIQjTqKeqXH5HLbLMxwcK5XfXHw30u5ap6EZmu7QVmcF/gnh2wS/EOiQ4MTbXz/vQeoXrmycPlVRM00WSslg==} engines: {node: '>= 8.0'} + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + named-placeholders@1.1.3: resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==} engines: {node: '>=12.0.0'} @@ -8487,6 +8692,24 @@ packages: sass: optional: true + next@14.2.33: + resolution: {integrity: sha512-GiKHLsD00t4ACm1p00VgrI0rUFAC9cRDGReKyERlM57aeEZkOQGcZTpIbsGn0b562FTPJWmYfKwplfO9EaT6ng==} + engines: {node: '>=18.17.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.41.2 + react: ^18.2.0 + react-dom: ^18.2.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + sass: + optional: true + next@15.3.5: resolution: {integrity: sha512-RkazLBMMDJSJ4XZQ81kolSpwiCt907l0xcgcpF4xC2Vml6QVcPNXW0NQRwQ80FFtSn7UM52XN0anaw8TEJXaiw==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} @@ -8602,6 +8825,10 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -8731,6 +8958,10 @@ packages: resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} engines: {node: '>=18'} + p-limit@7.2.0: + resolution: {integrity: sha512-ATHLtwoTNDloHRFFxFJdHnG6n2WUeFjaR8XQMFdKIv0xkXjrER8/iG9iu265jOM95zXHAfv9oTkqhrfbIzosrQ==} + engines: {node: '>=20'} + p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} @@ -8978,14 +9209,53 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.1.0: + resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + postcss@8.4.31: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.3: - resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} - engines: {node: ^10 || ^12 || >=14} - postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} @@ -9342,6 +9612,9 @@ packages: react: '>=17' react-dom: '>=17' + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} @@ -10063,6 +10336,11 @@ packages: stylis@4.3.6: resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} + sucrase@3.35.0: + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + superagent@8.1.2: resolution: {integrity: sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==} engines: {node: '>=6.4.0 <13 || >=14'} @@ -10114,6 +10392,11 @@ packages: tailwind-merge@2.6.0: resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==} + tailwindcss@3.4.18: + resolution: {integrity: sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==} + engines: {node: '>=14.0.0'} + hasBin: true + tailwindcss@4.1.14: resolution: {integrity: sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==} @@ -10177,6 +10460,13 @@ packages: text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + thread-stream@3.1.0: resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} @@ -10303,6 +10593,9 @@ packages: resolution: {integrity: sha512-Du/ZW2RfwV/D4cmA5rXafYjBQVuvu4qGiEEla4EmEHVHgRdx68Gftx7i66jn2bzHPwSVZY36Ae6OuDn9el4ZKA==} engines: {node: '>=14.13.1'} + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-jest@29.2.6: resolution: {integrity: sha512-yTNZVZqc8lSixm+QGVFcPe6+yj7+TWZwIesuOWvfcn4B9bz5x4NDzVCQQjOs7Hfouu36aEqfEbo9Qpo+gq8dDg==} engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} @@ -11136,6 +11429,10 @@ packages: resolution: {integrity: sha512-KHBC7z61OJeaMGnF3wqNZj+GGNXOyypZviiKpQeiHirG5Ib1ImwcLBH70rbMSkKfSmUNBsdf2PwaEJtKvgmkNw==} engines: {node: '>=12.20'} + yocto-queue@1.2.1: + resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} + engines: {node: '>=12.20'} + zhead@2.2.4: resolution: {integrity: sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag==} @@ -11163,11 +11460,8 @@ packages: zod@3.24.1: resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==} - zod@3.24.2: - resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==} - - zod@3.25.51: - resolution: {integrity: sha512-TQSnBldh+XSGL+opiSIq0575wvDPqu09AqWe1F7JhUMKY+M91/aGlK4MhpVNO7MgYfHcVCB1ffwAUTJzllKJqg==} + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} zod@4.1.11: resolution: {integrity: sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==} @@ -11201,6 +11495,8 @@ packages: snapshots: + '@alloc/quick-lru@5.2.0': {} + '@ampproject/remapping@2.3.0': dependencies: '@jridgewell/gen-mapping': 0.3.8 @@ -12128,6 +12424,14 @@ snapshots: next: 14.2.32(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) react: 18.3.1 + '@chakra-ui/next-js@2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.33(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1)': + dependencies: + '@chakra-ui/react': 2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@emotion/cache': 11.14.0 + '@emotion/react': 11.11.1(@types/react@18.3.1)(react@18.3.1) + next: 14.2.33(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) + react: 18.3.1 + '@chakra-ui/object-utils@2.1.0': {} '@chakra-ui/react-use-safe-layout-effect@2.1.0(react@18.3.1)': @@ -12730,11 +13034,11 @@ snapshots: '@eslint/js@8.57.1': {} - '@fastgpt-sdk/plugin@0.1.19(@types/node@20.14.0)': + '@fastgpt-sdk/plugin@0.2.13(@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) - zod: 3.25.51 + '@ts-rest/core': 3.52.1(@types/node@20.14.0)(zod@3.25.76) + zod: 3.25.76 transitivePeerDependencies: - '@types/node' @@ -13433,8 +13737,8 @@ snapshots: express-rate-limit: 7.5.0(express@5.1.0) pkce-challenge: 5.0.0 raw-body: 3.0.0 - zod: 3.25.51 - zod-to-json-schema: 3.24.5(zod@3.25.51) + zod: 3.25.76 + zod-to-json-schema: 3.24.5(zod@3.25.76) transitivePeerDependencies: - supports-color @@ -13640,60 +13944,93 @@ snapshots: '@next/env@14.2.32': {} + '@next/env@14.2.33': {} + '@next/env@15.3.5': {} '@next/eslint-plugin-next@14.2.26': dependencies: glob: 10.3.10 + '@next/eslint-plugin-next@14.2.33': + dependencies: + glob: 10.3.10 + '@next/swc-darwin-arm64@14.2.32': optional: true + '@next/swc-darwin-arm64@14.2.33': + optional: true + '@next/swc-darwin-arm64@15.3.5': optional: true '@next/swc-darwin-x64@14.2.32': optional: true + '@next/swc-darwin-x64@14.2.33': + optional: true + '@next/swc-darwin-x64@15.3.5': optional: true '@next/swc-linux-arm64-gnu@14.2.32': optional: true + '@next/swc-linux-arm64-gnu@14.2.33': + optional: true + '@next/swc-linux-arm64-gnu@15.3.5': optional: true '@next/swc-linux-arm64-musl@14.2.32': optional: true + '@next/swc-linux-arm64-musl@14.2.33': + optional: true + '@next/swc-linux-arm64-musl@15.3.5': optional: true '@next/swc-linux-x64-gnu@14.2.32': optional: true + '@next/swc-linux-x64-gnu@14.2.33': + optional: true + '@next/swc-linux-x64-gnu@15.3.5': optional: true '@next/swc-linux-x64-musl@14.2.32': optional: true + '@next/swc-linux-x64-musl@14.2.33': + optional: true + '@next/swc-linux-x64-musl@15.3.5': optional: true '@next/swc-win32-arm64-msvc@14.2.32': optional: true + '@next/swc-win32-arm64-msvc@14.2.33': + optional: true + '@next/swc-win32-arm64-msvc@15.3.5': optional: true '@next/swc-win32-ia32-msvc@14.2.32': optional: true + '@next/swc-win32-ia32-msvc@14.2.33': + optional: true + '@next/swc-win32-x64-msvc@14.2.32': optional: true + '@next/swc-win32-x64-msvc@14.2.33': + optional: true + '@next/swc-win32-x64-msvc@15.3.5': optional: true @@ -14714,10 +15051,10 @@ snapshots: '@trysound/sax@0.2.0': {} - '@ts-rest/core@3.52.1(@types/node@20.14.0)(zod@3.25.51)': + '@ts-rest/core@3.52.1(@types/node@20.14.0)(zod@3.25.76)': optionalDependencies: '@types/node': 20.14.0 - zod: 3.25.51 + zod: 3.25.76 '@tsconfig/node10@1.0.11': {} @@ -14732,6 +15069,10 @@ snapshots: tslib: 2.8.1 optional: true + '@types/async-retry@1.4.9': + dependencies: + '@types/retry': 0.12.5 + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.26.10 @@ -15090,6 +15431,8 @@ snapshots: dependencies: '@types/node': 20.17.24 + '@types/retry@0.12.5': {} + '@types/semver@7.5.8': {} '@types/send@0.17.4': @@ -15457,7 +15800,7 @@ snapshots: '@vue/shared': 3.5.13 estree-walker: 2.0.2 magic-string: 0.30.17 - postcss: 8.5.3 + postcss: 8.5.6 source-map-js: 1.2.1 '@vue/compiler-sfc@3.5.22': @@ -15816,6 +16159,8 @@ snapshots: ansi-styles@6.2.1: {} + any-promise@1.3.0: {} + anymatch@3.1.3: dependencies: normalize-path: 3.0.0 @@ -15825,6 +16170,8 @@ snapshots: arg@4.1.3: {} + arg@5.0.2: {} + argparse@1.0.10: dependencies: sprintf-js: 1.0.3 @@ -16250,6 +16597,8 @@ snapshots: callsites@3.1.0: {} + camelcase-css@2.0.1: {} + camelcase@5.3.1: {} camelcase@6.3.0: {} @@ -16663,6 +17012,8 @@ snapshots: css-what@6.1.0: {} + cssesc@3.0.0: {} + csso@4.2.0: dependencies: css-tree: 1.1.3 @@ -17029,6 +17380,8 @@ snapshots: asap: 2.0.6 wrappy: 1.0.2 + didyoumean@1.2.2: {} + diff-sequences@29.6.3: {} diff@4.0.2: {} @@ -17043,6 +17396,8 @@ snapshots: dependencies: path-type: 4.0.0 + dlv@1.1.3: {} + doctrine@2.1.0: dependencies: esutils: 2.0.3 @@ -17417,6 +17772,26 @@ snapshots: - eslint-plugin-import-x - supports-color + eslint-config-next@14.2.33(eslint@8.57.1)(typescript@5.8.2): + dependencies: + '@next/eslint-plugin-next': 14.2.33 + '@rushstack/eslint-patch': 1.11.0 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1)(typescript@5.8.2) + '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.8.2) + eslint: 8.57.1 + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.9.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) + eslint-plugin-react: 7.37.4(eslint@8.57.1) + eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1) + optionalDependencies: + typescript: 5.8.2 + transitivePeerDependencies: + - eslint-import-resolver-webpack + - eslint-plugin-import-x + - supports-color + eslint-import-resolver-node@0.3.9: dependencies: debug: 3.2.7 @@ -17425,6 +17800,21 @@ snapshots: transitivePeerDependencies: - supports-color + eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.0 + eslint: 8.57.1 + get-tsconfig: 4.10.0 + is-bun-module: 1.3.0 + oxc-resolver: 5.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.12 + optionalDependencies: + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + transitivePeerDependencies: + - supports-color + eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0)(eslint@8.56.0): dependencies: '@nolyfill/is-core-module': 1.0.39 @@ -17466,6 +17856,17 @@ 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(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.8.2) + eslint: 8.57.1 + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.9.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1) + 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): dependencies: debug: 3.2.7 @@ -17506,6 +17907,35 @@ snapshots: - eslint-import-resolver-webpack - supports-color + eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.8 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + 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(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.8.2) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.9.0)(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 @@ -19400,6 +19830,8 @@ snapshots: - supports-color - ts-node + jiti@1.21.7: {} + jiti@2.6.0: optional: true @@ -19543,9 +19975,9 @@ snapshots: langbase@1.1.44(encoding@0.1.13)(react@19.1.1): dependencies: dotenv: 16.4.7 - openai: 4.87.3(encoding@0.1.13)(zod@3.25.51) - zod: 3.25.51 - zod-validation-error: 3.4.0(zod@3.25.51) + openai: 4.87.3(encoding@0.1.13)(zod@3.25.76) + zod: 3.25.76 + zod-validation-error: 3.4.0(zod@3.25.76) optionalDependencies: react: 19.1.1 transitivePeerDependencies: @@ -19645,6 +20077,8 @@ snapshots: lilconfig@2.1.0: {} + lilconfig@3.1.3: {} + lines-and-columns@1.2.4: {} lint-staged@13.3.0: @@ -20702,6 +21136,12 @@ snapshots: seq-queue: 0.0.5 sqlstring: 2.3.3 + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + named-placeholders@1.1.3: dependencies: lru-cache: 7.18.3 @@ -20756,6 +21196,18 @@ snapshots: react: 18.3.1 react-i18next: 14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next-i18next@15.4.2(i18next@23.16.8)(next@14.2.33(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.10 + '@types/hoist-non-react-statics': 3.3.6 + core-js: 3.41.0 + hoist-non-react-statics: 3.3.2 + i18next: 23.16.8 + i18next-fs-backend: 2.6.0 + next: 14.2.33(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) + react: 18.3.1 + react-i18next: 14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next-i18next@15.4.2(i18next@23.16.8)(next@15.3.5(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1): dependencies: '@babel/runtime': 7.26.10 @@ -20849,6 +21301,33 @@ snapshots: - '@babel/core' - babel-plugin-macros + next@14.2.33(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1): + dependencies: + '@next/env': 14.2.33 + '@swc/helpers': 0.5.5 + busboy: 1.6.0 + caniuse-lite: 1.0.30001751 + graceful-fs: 4.2.11 + postcss: 8.4.31 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + styled-jsx: 5.1.1(@babel/core@7.26.10)(react@18.3.1) + optionalDependencies: + '@next/swc-darwin-arm64': 14.2.33 + '@next/swc-darwin-x64': 14.2.33 + '@next/swc-linux-arm64-gnu': 14.2.33 + '@next/swc-linux-arm64-musl': 14.2.33 + '@next/swc-linux-x64-gnu': 14.2.33 + '@next/swc-linux-x64-musl': 14.2.33 + '@next/swc-win32-arm64-msvc': 14.2.33 + '@next/swc-win32-ia32-msvc': 14.2.33 + '@next/swc-win32-x64-msvc': 14.2.33 + '@opentelemetry/api': 1.9.0 + sass: 1.85.1 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + next@15.3.5(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass@1.85.1): dependencies: '@next/env': 15.3.5 @@ -20964,6 +21443,8 @@ snapshots: object-assign@4.1.1: {} + object-hash@3.0.0: {} + object-inspect@1.13.4: {} object-keys@1.1.1: {} @@ -21046,7 +21527,7 @@ snapshots: transitivePeerDependencies: - encoding - openai@4.87.3(encoding@0.1.13)(zod@3.25.51): + openai@4.87.3(encoding@0.1.13)(zod@3.25.76): dependencies: '@types/node': 18.19.80 '@types/node-fetch': 2.6.12 @@ -21056,7 +21537,7 @@ snapshots: formdata-node: 4.4.1 node-fetch: 2.7.0(encoding@0.1.13) optionalDependencies: - zod: 3.25.51 + zod: 3.25.76 transitivePeerDependencies: - encoding @@ -21146,6 +21627,10 @@ snapshots: dependencies: yocto-queue: 1.2.0 + p-limit@7.2.0: + dependencies: + yocto-queue: 1.2.1 + p-locate@4.1.0: dependencies: p-limit: 2.3.0 @@ -21383,13 +21868,40 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss@8.4.31: + postcss-import@15.1.0(postcss@8.5.6): dependencies: - nanoid: 3.3.9 - picocolors: 1.1.1 - source-map-js: 1.2.1 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.10 + + postcss-js@4.1.0(postcss@8.5.6): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.5.6 + + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.20.6)(yaml@2.8.1): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 1.21.7 + postcss: 8.5.6 + tsx: 4.20.6 + yaml: 2.8.1 - postcss@8.5.3: + postcss-nested@6.2.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.4.31: dependencies: nanoid: 3.3.9 picocolors: 1.1.1 @@ -21802,6 +22314,10 @@ snapshots: - '@types/react' - immer + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + readable-stream@2.3.8: dependencies: core-util-is: 1.0.3 @@ -22699,6 +23215,16 @@ snapshots: stylis@4.3.6: {} + sucrase@3.35.0: + dependencies: + '@jridgewell/gen-mapping': 0.3.8 + commander: 4.1.1 + glob: 10.4.5 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.6 + ts-interface-checker: 0.1.13 + superagent@8.1.2: dependencies: component-emitter: 1.3.1 @@ -22757,6 +23283,34 @@ snapshots: tailwind-merge@2.6.0: {} + tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1): + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 3.1.3 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-import: 15.1.0(postcss@8.5.6) + postcss-js: 4.1.0(postcss@8.5.6) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.20.6)(yaml@2.8.1) + postcss-nested: 6.2.0(postcss@8.5.6) + postcss-selector-parser: 6.1.2 + resolve: 1.22.10 + sucrase: 3.35.0 + transitivePeerDependencies: + - tsx + - yaml + tailwindcss@4.1.14: {} tapable@2.2.1: {} @@ -22837,6 +23391,14 @@ snapshots: text-table@0.2.0: {} + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + thread-stream@3.1.0: dependencies: real-require: 0.2.0 @@ -22925,6 +23487,8 @@ snapshots: ts-deepmerge@7.0.3: {} + ts-interface-checker@0.1.13: {} + ts-jest@29.2.6(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(jest@29.7.0(@types/node@20.17.24)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.24)(typescript@5.8.2)))(typescript@5.8.2): dependencies: bs-logger: 0.2.6 @@ -23451,7 +24015,7 @@ snapshots: vite@5.4.14(@types/node@24.0.13)(lightningcss@1.30.1)(sass@1.85.1)(terser@5.39.0): dependencies: esbuild: 0.21.5 - postcss: 8.5.3 + postcss: 8.5.6 rollup: 4.35.0 optionalDependencies: '@types/node': 24.0.13 @@ -23463,7 +24027,7 @@ snapshots: vite@6.2.2(@types/node@20.17.24)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: esbuild: 0.25.11 - postcss: 8.5.3 + postcss: 8.5.6 rollup: 4.35.0 optionalDependencies: '@types/node': 20.17.24 @@ -23478,7 +24042,7 @@ snapshots: vite@6.2.2(@types/node@24.0.13)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: esbuild: 0.25.11 - postcss: 8.5.3 + postcss: 8.5.6 rollup: 4.35.0 optionalDependencies: '@types/node': 24.0.13 @@ -23932,6 +24496,8 @@ snapshots: yocto-queue@1.2.0: {} + yocto-queue@1.2.1: {} + zhead@2.2.4: {} zhlint@0.7.4(@types/node@24.0.13)(lightningcss@1.30.1)(sass@1.85.1)(terser@5.39.0)(typescript@5.8.2): @@ -23966,19 +24532,17 @@ snapshots: dependencies: zod: 4.1.12 - zod-to-json-schema@3.24.5(zod@3.25.51): + zod-to-json-schema@3.24.5(zod@3.25.76): dependencies: - zod: 3.25.51 + zod: 3.25.76 - zod-validation-error@3.4.0(zod@3.25.51): + zod-validation-error@3.4.0(zod@3.25.76): dependencies: - zod: 3.25.51 + zod: 3.25.76 zod@3.24.1: {} - zod@3.24.2: {} - - zod@3.25.51: {} + zod@3.25.76: {} zod@4.1.11: {} diff --git a/projects/app/.env.template b/projects/app/.env.template index 21af4f6ced7f..0fbeda94a027 100644 --- a/projects/app/.env.template +++ b/projects/app/.env.template @@ -76,9 +76,10 @@ SIGNOZ_BASE_URL= SIGNOZ_SERVICE_NAME= SIGNOZ_STORE_LEVEL=warn +# 插件市场 +MARKETPLACE_URL=https://marketplace.fastgpt.cn + # 安全配置 -# 对话文件 n 天过期 -CHAT_FILE_EXPIRE_TIME=7 # 启动 IP 限流(true),部分接口增加了 ip 限流策略,防止非正常请求操作。 USE_IP_LIMIT=false # 工作流最大运行次数,避免极端的死循环情况 diff --git a/projects/app/next.config.js b/projects/app/next.config.js index a8f63a7647bb..6f8e2bf93a93 100644 --- a/projects/app/next.config.js +++ b/projects/app/next.config.js @@ -115,7 +115,7 @@ const nextConfig = { // 启用持久化缓存 config.cache = { type: 'filesystem', - name: isServer ? 'server' : 'client', + name: 'client', buildDependencies: { config: [__filename] }, diff --git a/projects/app/package.json b/projects/app/package.json index aa6b143cfac0..e1ae1069c098 100644 --- a/projects/app/package.json +++ b/projects/app/package.json @@ -1,6 +1,6 @@ { "name": "app", - "version": "4.13.2", + "version": "4.14.0", "private": false, "scripts": { "dev": "npm run build:workers && next dev", @@ -69,7 +69,7 @@ "request-ip": "^3.3.0", "sass": "^1.58.3", "use-context-selector": "^1.4.4", - "zod": "^3.24.2" + "zod": "^4.1.12" }, "devDependencies": { "@next/bundle-analyzer": "^15.5.6", diff --git a/projects/app/src/components/Layout/navbar.tsx b/projects/app/src/components/Layout/navbar.tsx index a36233179294..139b4b754011 100644 --- a/projects/app/src/components/Layout/navbar.tsx +++ b/projects/app/src/components/Layout/navbar.tsx @@ -3,7 +3,6 @@ import { Box, type BoxProps, Flex, Link, type LinkProps } from '@chakra-ui/react import { useRouter } from 'next/router'; import { useUserStore } from '@/web/support/user/useUserStore'; import { useChatStore } from '@/web/core/chat/context/useChatStore'; -import { HUMAN_ICON } from '@fastgpt/global/common/system/constants'; import NextLink from 'next/link'; import Badge from '../Badge'; import Avatar from '@fastgpt/web/components/common/Avatar'; @@ -47,15 +46,15 @@ const Navbar = ({ unread }: { unread: number }) => { () => [ { label: t('common:navbar.Chat'), - icon: 'core/chat/chatLight', - activeIcon: 'core/chat/chatFill', + icon: 'navbar/chatLight', + activeIcon: 'navbar/chatFill', link: `/chat?appId=${lastChatAppId}&pane=${lastPane}`, activeLink: ['/chat'] }, { label: t('common:navbar.Studio'), - icon: 'core/app/aiLight', - activeIcon: 'core/app/aiFill', + icon: 'navbar/dashboardLight', + activeIcon: 'navbar/dashboardFill', link: `/dashboard/apps`, activeLink: [ '/dashboard/apps', @@ -69,15 +68,22 @@ const Navbar = ({ unread }: { unread: number }) => { }, { label: t('common:navbar.Datasets'), - icon: 'core/dataset/datasetLight', - activeIcon: 'core/dataset/datasetFill', + icon: 'navbar/datasetLight', + activeIcon: 'navbar/datasetFill', link: `/dataset/list`, activeLink: ['/dataset/list', '/dataset/detail'] }, + { + label: t('common:navbar.toolkit'), + icon: 'core/app/pluginLight', + activeIcon: 'core/app/pluginFill', + link: '/toolkit/tools', + activeLink: ['/toolkit/tools'] + }, { label: t('common:navbar.Account'), - icon: 'support/user/userLight', - activeIcon: 'support/user/userFill', + icon: 'navbar/userLight', + activeIcon: 'navbar/userFill', link: '/account/info', activeLink: [ '/account/bill', @@ -91,9 +97,20 @@ const Navbar = ({ unread }: { unread: number }) => { '/account/promotion', '/account/model' ] - } + }, + ...(userInfo?.username === 'root' + ? [ + { + label: t('common:navbar.Config'), + icon: 'support/config/configLight', + activeIcon: 'support/config/configFill', + link: '/config/tool', + activeLink: ['/config/tool', '/config/tool/marketplace'] + } + ] + : []) ], - [lastChatAppId, lastPane, t] + [lastChatAppId, lastPane, t, userInfo?.username] ); const isSecondNavbarPage = useMemo(() => { @@ -166,8 +183,8 @@ const Navbar = ({ unread }: { unread: number }) => { name: item.icon as any, color: 'myGray.400' })} - width={'20px'} - height={'20px'} + width={'24px'} + height={'24px'} /> { const router = useRouter(); + const { userInfo } = useUserStore(); const { t } = useTranslation(); const { lastChatAppId, lastPane } = useChatStore(); @@ -45,6 +47,13 @@ const NavbarPhone = ({ unread }: { unread: number }) => { activeLink: ['/dataset/list', '/dataset/detail'], unread: 0 }, + { + label: t('common:navbar.toolkit'), + icon: 'core/app/type/pluginLight', + activeIcon: 'core/app/pluginFill', + link: '/toolkit/tools', + activeLink: ['/toolkit/tools'] + }, { label: t('common:navbar.Account'), icon: 'support/user/userLight', @@ -62,9 +71,20 @@ const NavbarPhone = ({ unread }: { unread: number }) => { '/account/model' ], unread - } + }, + ...(userInfo?.username === 'root' + ? [ + { + label: t('common:navbar.Config'), + icon: 'support/config/configLight', + activeIcon: 'support/config/configFill', + link: '/config/tool', + activeLink: ['/config/tool', '/config/tool/marketplace'] + } + ] + : []) ], - [t, lastChatAppId, lastPane, unread] + [lastChatAppId, lastPane, t, userInfo?.username] ); return ( diff --git a/projects/app/src/components/Select/AIModelSelector.tsx b/projects/app/src/components/Select/AIModelSelector.tsx index 8a21e2bd1320..104f8cb3beb4 100644 --- a/projects/app/src/components/Select/AIModelSelector.tsx +++ b/projects/app/src/components/Select/AIModelSelector.tsx @@ -13,9 +13,17 @@ import React, { useCallback, useMemo, useState } from 'react'; type Props = SelectProps & { disableTip?: string; noOfLines?: ResponsiveValue; + cacheModel?: boolean; }; -const OneRowSelector = ({ list, onChange, disableTip, noOfLines, ...props }: Props) => { +const OneRowSelector = ({ + list, + onChange, + disableTip, + noOfLines, + cacheModel = true, + ...props +}: Props) => { const { t } = useTranslation(); const { llmModelList, @@ -30,7 +38,9 @@ const OneRowSelector = ({ list, onChange, disableTip, noOfLines, ...props }: Pro const { data: myModels } = useRequest2( async () => { const set = await getMyModelList(); - set.add(props.value); + if (cacheModel) { + set.add(props.value); + } return set; }, { diff --git a/projects/app/src/components/Select/FileSelector.tsx b/projects/app/src/components/Select/FileSelector.tsx deleted file mode 100644 index bf10bd09c7b7..000000000000 --- a/projects/app/src/components/Select/FileSelector.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { useTranslation } from 'next-i18next'; -import type { UseFormReturn } from 'react-hook-form'; -import { useFieldArray } from 'react-hook-form'; -import { useFileUpload } from '../core/chat/ChatContainer/ChatBox/hooks/useFileUpload'; -import { useEffect } from 'react'; -import { isEqual } from 'lodash'; -import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; -import { Button, Flex } from '@chakra-ui/react'; -import MyIcon from '@fastgpt/web/components/common/Icon'; -import FilePreview from '../core/chat/ChatContainer/components/FilePreview'; -import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; -import { useChatStore } from '@/web/core/chat/context/useChatStore'; - -const FileSelector = ({ - onChange, - value, - - form, - fieldName, - - canSelectFile = true, - canSelectImg = false, - maxFiles = 5, - setUploading, - - isDisabled = false -}: { - onChange: (...event: any[]) => void; - value: any; - - form?: UseFormReturn; - fieldName?: string; - - canSelectFile?: boolean; - canSelectImg?: boolean; - maxFiles?: number; - setUploading?: (uploading: boolean) => void; - - isDisabled?: boolean; -}) => { - const { t } = useTranslation(); - - const { appId, chatId, outLinkAuthData } = useChatStore(); - const fileCtrl = useFieldArray({ - control: form?.control, - name: fieldName as any - }); - const { - File, - fileList, - selectFileIcon, - uploadFiles, - onOpenSelectFile, - onSelectFile, - removeFiles, - replaceFiles, - hasFileUploading - } = useFileUpload({ - fileSelectConfig: { - canSelectFile, - canSelectImg, - maxFiles - }, - outLinkAuthData, - appId, - chatId, - fileCtrl: fileCtrl as any - }); - - useEffect(() => { - if (!Array.isArray(value)) { - replaceFiles([]); - return; - } - - // compare file names and update if different - const valueFileNames = value.map((item) => item.name); - const currentFileNames = fileList.map((item) => item.name); - if (!isEqual(valueFileNames, currentFileNames)) { - replaceFiles(value); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [value]); - - useRequest2(uploadFiles, { - manual: false, - errorToast: t('common:upload_file_error'), - refreshDeps: [fileList] - }); - - useEffect(() => { - setUploading?.(hasFileUploading); - onChange( - fileList.map((item) => ({ - type: item.type, - name: item.name, - url: item.url, - icon: item.icon - })) - ); - }, [fileList, hasFileUploading, onChange, setUploading]); - - return ( - <> - - - - - {fileList.length === 0 && } - - onSelectFile({ files })} /> - - ); -}; - -export default FileSelector; diff --git a/projects/app/src/components/common/Textarea/MyTextarea/index.tsx b/projects/app/src/components/common/Textarea/MyTextarea/index.tsx index 219e4a2f8207..4108261f3910 100644 --- a/projects/app/src/components/common/Textarea/MyTextarea/index.tsx +++ b/projects/app/src/components/common/Textarea/MyTextarea/index.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState } from 'react'; +import React, { useCallback, useLayoutEffect, useRef } from 'react'; import { Box, @@ -100,7 +100,27 @@ const Editor = React.memo(function Editor({ showResize?: boolean; }) { const { t } = useTranslation(); - const [scrollHeight, setScrollHeight] = useState(0); + const cursorPositionRef = useRef(null); + + // 使用 useRef 保存 onChange,避免依赖变化导致 handleChange 重新创建 + const onChangeRef = useRef(onChange); + onChangeRef.current = onChange; + + // 使用 useLayoutEffect 同步恢复光标位置,避免闪烁 + useLayoutEffect(() => { + if (textareaRef.current && cursorPositionRef.current !== null) { + const pos = cursorPositionRef.current; + textareaRef.current.setSelectionRange(pos, pos); + cursorPositionRef.current = null; + } + }); + + // 移除 onChange 依赖,使 handleChange 引用永远稳定 + const handleChange = useCallback((e: React.ChangeEvent) => { + // 保存光标位置 + cursorPositionRef.current = e.target.selectionStart; + onChangeRef.current?.(e); + }, []); return ( @@ -120,25 +140,25 @@ const Editor = React.memo(function Editor({ {...props} maxH={`${maxH}px`} minH={`${minH}px`} - onChange={(e) => { - setScrollHeight(e.target.scrollHeight); - onChange?.(e); - }} + onChange={handleChange} /> - {onOpenModal && maxH && scrollHeight > Number(maxH) && ( - - - - - - )} + {onOpenModal && + maxH && + textareaRef.current && + textareaRef.current.scrollHeight > Number(maxH) && ( + + + + + + )} ); }); diff --git a/projects/app/src/components/core/app/FileSelect.tsx b/projects/app/src/components/core/app/FileSelect.tsx index 6ba44f6b2f6a..18ca6c328adc 100644 --- a/projects/app/src/components/core/app/FileSelect.tsx +++ b/projects/app/src/components/core/app/FileSelect.tsx @@ -7,12 +7,13 @@ import { ModalBody, useDisclosure, HStack, - Switch, ModalFooter, type BoxProps, - Checkbox + Checkbox, + VStack, + Input } from '@chakra-ui/react'; -import React, { useMemo } from 'react'; +import React, { useState } from 'react'; import { useTranslation } from 'next-i18next'; import type { AppFileSelectConfigType } from '@fastgpt/global/core/app/type.d'; import MyModal from '@fastgpt/web/components/common/MyModal'; @@ -23,8 +24,13 @@ import { useMount } from 'ahooks'; import { useSystemStore } from '@/web/common/system/useSystemStore'; import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; import MyTag from '@fastgpt/web/components/common/Tag/index'; -import MyDivider from '@fastgpt/web/components/common/MyDivider'; -import { defaultAppSelectFileConfig } from '@fastgpt/global/core/app/constants'; +import type { FileExtensionKeyType } from '@fastgpt/global/core/app/constants'; +import { + defaultAppSelectFileConfig, + defaultFileExtensionTypes +} from '@fastgpt/global/core/app/constants'; +import InputSlider from '@fastgpt/web/components/common/MySlider/InputSlider'; +import { shadowLight } from '@fastgpt/web/styles/theme'; const FileSelect = ({ forbidVision = false, @@ -41,13 +47,61 @@ const FileSelect = ({ const { isOpen, onOpen, onClose } = useDisclosure(); const maxSelectFiles = Math.min(feConfigs?.uploadFileMaxAmount ?? 20, 30); - const formLabel = useMemo( - () => - value.canSelectFile || value.canSelectImg - ? t('common:core.app.whisper.Open') - : t('common:core.app.whisper.Close'), - [t, value.canSelectFile, value.canSelectImg] - ); + const [localValue, setLocalValue] = useState(value); + const [isAddingCustomFileExtension, setIsAddingCustomFileExtension] = useState(false); + const [customFileExtension, setCustomFileExtension] = useState('.'); + + const canUploadFile = + value.canSelectFile || + value.canSelectImg || + value.canSelectVideo || + value.canSelectAudio || + value.canSelectCustomFileExtension; + const formLabel = canUploadFile + ? t('common:core.app.whisper.Open') + : t('common:core.app.whisper.Close'); + + const handleCheckboxChange = (type: FileExtensionKeyType, checked: boolean) => { + if (type === 'canSelectFile') { + setLocalValue((state) => ({ + ...state, + canSelectFile: checked + })); + } else if (type === 'canSelectImg') { + setLocalValue((state) => ({ + ...state, + canSelectImg: checked + })); + } else if (type === 'canSelectVideo') { + setLocalValue((state) => ({ + ...state, + canSelectVideo: checked + })); + } else if (type === 'canSelectAudio') { + setLocalValue((state) => ({ + ...state, + canSelectAudio: checked + })); + } else if (type === 'canSelectCustomFileExtension') { + setLocalValue((state) => ({ + ...state, + canSelectCustomFileExtension: checked + })); + } + }; + + const handleConfirmCustomFileExtension = () => { + const exists = localValue?.customFileExtensionList?.includes(customFileExtension); + if (customFileExtension !== '.' && !exists) { + setLocalValue((state) => ({ + ...state, + customFileExtensionList: [...(state.customFileExtensionList || []), customFileExtension] + })); + handleCheckboxChange('canSelectCustomFileExtension', true); + } + setCustomFileExtension('.'); + setIsAddingCustomFileExtension(false); + }; // Close select img switch when vision is forbidden useMount(() => { @@ -74,7 +128,10 @@ const FileSelect = ({ size={'sm'} mr={'-5px'} color={'myGray.600'} - onClick={onOpen} + onClick={() => { + setLocalValue(value); + onOpen(); + }} > {formLabel} @@ -84,110 +141,256 @@ const FileSelect = ({ title={t('app:file_upload')} isOpen={isOpen} onClose={onClose} + w={'500px'} > - - {t('app:document_upload')} - { - onChange({ - ...value, - canSelectFile: e.target.checked - }); - }} - /> - - {value.canSelectFile && feConfigs.showCustomPdfParse && ( - <> - - { - onChange({ - ...value, - customPdfParse: e.target.checked - }); - }} - > - {t('app:pdf_enhance_parse')} - - - {feConfigs?.show_pay && ( - - {t('app:pdf_enhance_parse_price', { - price: feConfigs.customPdfParsePrice || 0 - })} - - )} - - - - )} - - {t('app:image_upload')} - {forbidVision ? ( - - {t('app:llm_not_support_vision')} - - ) : ( - { - onChange({ - ...value, - canSelectImg: e.target.checked - }); - }} - /> - )} - - {!forbidVision && ( - - {t('app:image_upload_tip')} - - - )} - - + {t('app:upload_file_max_amount')} - - + { - onChange({ - ...value, + setLocalValue((state) => ({ + ...state, maxFiles: e - }); + })); }} /> + + + {t('app:upload_file_extension_types')} + + + {Object.entries(defaultFileExtensionTypes).map(([type, exts]) => + type === 'canSelectCustomFileExtension' ? ( + + { + handleCheckboxChange('canSelectCustomFileExtension', e.target.checked); + }} + > + + {t('app:upload_file_extension_type_canSelectCustomFileExtension')} + + + {localValue.customFileExtensionList?.map((ext) => ( + + {ext} + { + e.stopPropagation(); + e.preventDefault(); + setLocalValue((state) => ({ + ...state, + customFileExtensionList: ( + state.customFileExtensionList || [] + ).filter((prev) => prev !== ext) + })); + }} + > + + + + ))} + + { + e.stopPropagation(); + e.preventDefault(); + setIsAddingCustomFileExtension(true); + }} + > + {isAddingCustomFileExtension ? ( + + setCustomFileExtension( + `.${e.target.value.replace(/^\./, '').trim()}` + ) + } + onBlur={handleConfirmCustomFileExtension} + onKeyDown={(e) => { + if (e.key.toLowerCase() !== 'enter') return; + handleConfirmCustomFileExtension(); + }} + /> + ) : ( + + + + {t( + 'app:upload_file_extension_type_canSelectCustomFileExtension_placeholder' + )} + + + )} + + + + + ) : ( + + + handleCheckboxChange(type as FileExtensionKeyType, e.target.checked) + } + > + + {t(`app:upload_file_extension_type_${type}`)} + + + {exts.map((ext) => ext.slice(1)).join('/')} + + + + ) + )} + + + + {localValue.canSelectFile && feConfigs?.showCustomPdfParse && ( + + { + setLocalValue((state) => ({ + ...state, + customPdfParse: e.target.checked + })); + }} + > + {t('app:pdf_enhance_parse')} + + + {feConfigs?.show_pay && ( + + {t('app:pdf_enhance_parse_price', { + price: feConfigs.customPdfParsePrice || 0 + })} + + )} + + )} - diff --git a/projects/app/src/components/core/app/VariableEdit.tsx b/projects/app/src/components/core/app/VariableEdit.tsx index 1c7704dfd331..0978f07eb414 100644 --- a/projects/app/src/components/core/app/VariableEdit.tsx +++ b/projects/app/src/components/core/app/VariableEdit.tsx @@ -49,11 +49,8 @@ export const defaultVariable: VariableItemType = { canSelectImg: true, maxFiles: 5, timeGranularity: 'day', - timeType: 'point', - timeRangeStart: new Date( - new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).setHours(0, 0, 0, 0) - ).toISOString(), - timeRangeEnd: new Date(new Date().setHours(0, 0, 0, 0)).toISOString() + timeRangeStart: undefined, + timeRangeEnd: undefined }; export const addVariable = () => { @@ -102,14 +99,6 @@ const VariableEdit = ({ const handleTypeChange = useCallback( (newType: VariableInputEnum) => { const defaultValIsNumber = !isNaN(Number(value.defaultValue)); - const currentType = value.type; - - const isCurrentTimeType = - currentType === VariableInputEnum.timePointSelect || - currentType === VariableInputEnum.timeRangeSelect; - const isNewTimeType = - newType === VariableInputEnum.timePointSelect || - newType === VariableInputEnum.timeRangeSelect; if ( newType === VariableInputEnum.select || @@ -119,27 +108,9 @@ const VariableEdit = ({ setValue('defaultValue', ''); } - // Set time-related default values when switching from non-time type to time type - if (!isCurrentTimeType && isNewTimeType) { - setValue('defaultValue', ''); - setValue('timeGranularity', 'day'); - setValue( - 'timeRangeStart', - new Date( - new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).setHours(0, 0, 0, 0) - ).toISOString() - ); - setValue('timeRangeEnd', new Date(new Date().setHours(0, 0, 0, 0)).toISOString()); - } - - // Clear default value when switching from time type to other types - if (isCurrentTimeType && !isNewTimeType) { - setValue('defaultValue', ''); - } - setValue('type', newType); }, - [setValue, value.defaultValue, value.type] + [setValue, value.defaultValue] ); const formatVariables = useMemo(() => { @@ -191,39 +162,6 @@ const VariableEdit = ({ return; } - if ( - data.type !== VariableInputEnum.select && - data.type !== VariableInputEnum.multipleSelect && - data.list - ) { - delete data.list; - } - - if (data.type !== VariableInputEnum.file) { - delete data.canSelectFile; - delete data.canSelectImg; - delete data.maxFiles; - } - - if ( - data.type !== VariableInputEnum.timePointSelect && - data.type !== VariableInputEnum.timeRangeSelect - ) { - delete data.timeGranularity; - delete data.timeRangeStart; - delete data.timeRangeEnd; - } else if (data.type === VariableInputEnum.timePointSelect) { - data.defaultValue = new Date(new Date().setHours(0, 0, 0, 0)).toISOString(); - } else if (data.type === VariableInputEnum.timeRangeSelect) { - data.defaultValue = [ - data.timeRangeStart || - new Date( - new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).setHours(0, 0, 0, 0) - ).toISOString(), - data.timeRangeEnd || new Date(new Date().setHours(0, 0, 0, 0)).toISOString() - ]; - } - if (data.type === VariableInputEnum.custom || data.type === VariableInputEnum.internal) { data.required = false; } else { @@ -232,6 +170,14 @@ const VariableEdit = ({ .find((item) => item.value === data.type)?.defaultValueType; } + // Remove undefined keys + Object.keys(data).forEach((key) => { + if (data[key as keyof VariableItemType] === undefined) { + delete data[key as keyof VariableItemType]; + } + }); + + console.log(data); const onChangeVariable = (() => { if (data.key) { return variables.map((item) => { diff --git a/projects/app/src/components/core/app/formRender/FileSelector.tsx b/projects/app/src/components/core/app/formRender/FileSelector.tsx new file mode 100644 index 000000000000..1d1f16037996 --- /dev/null +++ b/projects/app/src/components/core/app/formRender/FileSelector.tsx @@ -0,0 +1,506 @@ +import type { DragEvent } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; +import type { UserInputFileItemType } from '../../chat/ChatContainer/ChatBox/type'; +import { + Box, + CircularProgress, + HStack, + IconButton, + Input, + InputGroup, + VStack +} from '@chakra-ui/react'; +import MyBox from '@fastgpt/web/components/common/MyBox'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import { ChatFileTypeEnum } from '@fastgpt/global/core/chat/constants'; +import { getFileIcon } from '@fastgpt/global/common/file/icon'; +import type { AppFileSelectConfigType } from '@fastgpt/global/core/app/type'; +import { useSystemStore } from '@/web/common/system/useSystemStore'; +import { getUploadFileType } from '@fastgpt/global/core/app/constants'; +import { useToast } from '@fastgpt/web/hooks/useToast'; +import { useTranslation } from 'next-i18next'; +import { useSelectFile } from '@/web/common/file/hooks/useSelectFile'; +import { getNanoid } from '@fastgpt/global/common/string/tools'; +import MyDivider from '@fastgpt/web/components/common/MyDivider'; +import MyAvatar from '@fastgpt/web/components/common/Avatar'; +import { z } from 'zod'; +import { clone } from 'lodash'; +import { getPresignedChatFileGetUrl, getUploadChatFilePresignedUrl } from '@/web/common/file/api'; +import { useContextSelector } from 'use-context-selector'; +import { ChatBoxContext } from '../../chat/ChatContainer/ChatBox/Provider'; +import { POST } from '@/web/common/api/request'; +import { getErrText } from '@fastgpt/global/common/error/utils'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { useDebounceEffect } from 'ahooks'; +import { formatFileSize, parseUrlToFileType } from '@fastgpt/global/common/file/tools'; + +const FileSelector = ({ + fileUrls, + onChange, + maxFiles, + canSelectFile, + canSelectImg, + canSelectVideo, + canSelectAudio, + canSelectCustomFileExtension, + customFileExtensionList, + canLocalUpload, + canUrlUpload +}: AppFileSelectConfigType & { + fileUrls: string[]; + onChange: (e: string[]) => void; + canLocalUpload?: boolean; + canUrlUpload?: boolean; +}) => { + const { feConfigs } = useSystemStore(); + const { toast } = useToast(); + const { t } = useTranslation(); + + const outLinkAuthData = useContextSelector(ChatBoxContext, (v) => v.outLinkAuthData); + const appId = useContextSelector(ChatBoxContext, (v) => v.appId); + const chatId = useContextSelector(ChatBoxContext, (v) => v.chatId); + + const [cloneFiles, setCloneFiles] = useState( + fileUrls + .map((url) => { + const fileType = parseUrlToFileType(url); + if (!fileType) return null as unknown as UserInputFileItemType; + + return { + id: getNanoid(6), + name: fileType.name || url, + type: fileType.type, + icon: getFileIcon(fileType.name || url), + url: fileType.url, + status: 1, + key: url.startsWith('chat/') ? url : undefined + }; + }) + .filter(Boolean) as UserInputFileItemType[] + ); + // 采用异步更新顶层的方式 + useDebounceEffect( + () => { + onChange(cloneFiles.map((file) => file.key || file.url || '').filter(Boolean)); + }, + [cloneFiles], + { + wait: 1000 + } + ); + + const fileType = useMemo(() => { + return getUploadFileType({ + canSelectFile, + canSelectImg, + canSelectVideo, + canSelectAudio, + canSelectCustomFileExtension, + customFileExtensionList + }); + }, [ + canSelectFile, + canSelectImg, + canSelectVideo, + canSelectAudio, + canSelectCustomFileExtension, + customFileExtensionList + ]); + const maxSelectFiles = maxFiles ?? 10; + const maxSize = (feConfigs?.uploadFileMaxSize || 1024) * 1024 * 1024; // nkb + const canSelectFileAmount = maxSelectFiles - cloneFiles.length; + const isMaxSelected = canSelectFileAmount <= 0; + + // Selector props + const [isDragging, setIsDragging] = useState(false); + const onSelectFile = useCallback( + async (files: File[]) => { + if (files.length > maxSelectFiles) { + files = files.slice(0, maxSelectFiles); + toast({ + status: 'warning', + title: t('chat:file_amount_over', { max: maxSelectFiles }) + }); + } + const filterFilesByMaxSize = files.filter((file) => file.size <= maxSize); + if (filterFilesByMaxSize.length < files.length) { + toast({ + status: 'warning', + title: t('file:some_file_size_exceeds_limit', { maxSize: formatFileSize(maxSize) }) + }); + } + + const loadFiles = await Promise.all( + filterFilesByMaxSize.map( + (file) => + new Promise((resolve, reject) => { + if (file.type.includes('image')) { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => { + const item: UserInputFileItemType = { + id: getNanoid(6), + rawFile: file, + type: ChatFileTypeEnum.image, + name: file.name, + icon: reader.result as string, + status: 0 + }; + resolve(item); + }; + reader.onerror = () => { + reject(reader.error); + }; + } else { + resolve({ + id: getNanoid(6), + rawFile: file, + type: ChatFileTypeEnum.file, + name: file.name, + icon: getFileIcon(file.name), + status: 0 + }); + } + }) + ) + ); + setCloneFiles((state) => [...loadFiles, ...state]); + }, + [maxSelectFiles, maxSize, t, toast] + ); + const handleDragEnter = (e: DragEvent) => { + e.preventDefault(); + setIsDragging(true); + }; + const handleDragLeave = (e: DragEvent) => { + e.preventDefault(); + setIsDragging(false); + }; + const handleDrop = async (e: DragEvent) => { + e.preventDefault(); + setIsDragging(false); + + const filterTypeReg = new RegExp( + `(${fileType + .split(',') + .map((item) => item.trim()) + .join('|')})$`, + 'i' + ); + const items = e.dataTransfer.items; + + const firstEntry = items[0].webkitGetAsEntry(); + + if (firstEntry?.isDirectory && items.length === 1) { + { + const readFile = (entry: any) => { + return new Promise((resolve) => { + entry.file((file: File) => { + if (filterTypeReg.test(file.name)) { + onSelectFile([file]); + } + resolve(file); + }); + }); + }; + const traverseFileTree = (dirReader: any) => { + return new Promise((resolve) => { + let fileNum = 0; + dirReader.readEntries(async (entries: any[]) => { + for await (const entry of entries) { + if (entry.isFile) { + await readFile(entry); + fileNum++; + } else if (entry.isDirectory) { + await traverseFileTree(entry.createReader()); + } + } + + // chrome: readEntries will return 100 entries at most + if (fileNum === 100) { + await traverseFileTree(dirReader); + } + resolve(''); + }); + }); + }; + + for await (const item of items) { + const entry = item.webkitGetAsEntry(); + if (entry) { + if (entry.isFile) { + await readFile(entry); + } else if (entry.isDirectory) { + //@ts-ignore + await traverseFileTree(entry.createReader()); + } + } + } + } + } else if (firstEntry?.isFile) { + const files = Array.from(e.dataTransfer.files); + + onSelectFile(files.filter((item) => filterTypeReg.test(item.name))); + } else { + return toast({ + title: t('file:upload_error_description'), + status: 'error' + }); + } + }; + const { File, onOpen } = useSelectFile({ + fileType, + multiple: canSelectFileAmount > 1, + maxCount: canSelectFileAmount + }); + const uploadFiles = useCallback(async () => { + const filterFiles = cloneFiles.filter((item) => item.status === 0); + if (filterFiles.length === 0) return; + + setCloneFiles((state) => state.map((item) => ({ ...item, status: 1 }))); + + await Promise.allSettled( + filterFiles.map(async (file) => { + const copyFile = clone(file); + if (!copyFile.rawFile) return; + copyFile.status = 1; + + try { + // Get Upload Post Presigned URL + const { url, fields } = await getUploadChatFilePresignedUrl({ + filename: copyFile.rawFile.name, + appId, + chatId, + outLinkAuthData + }); + + // Upload File to S3 + const formData = new FormData(); + Object.entries(fields).forEach(([k, v]) => formData.set(k, v)); + formData.set('file', copyFile.rawFile); + await POST(url, formData, { + headers: { + 'Content-Type': 'multipart/form-data; charset=utf-8' + }, + onUploadProgress: (e) => { + if (!e.total) return; + const percent = Math.round((e.loaded / e.total) * 100); + copyFile.process = percent; + setCloneFiles((state) => + state.map((item) => (item.id === file.id ? { ...item, process: percent } : item)) + ); + } + }); + const previewUrl = await getPresignedChatFileGetUrl({ + key: fields.key, + appId, + outLinkAuthData + }); + + // Update file url and key + copyFile.url = previewUrl; + copyFile.key = fields.key; + console.log(previewUrl, 232); + setCloneFiles((state) => + state.map((item) => + item.id === file.id ? { ...item, url: previewUrl, key: fields.key } : item + ) + ); + } catch (error) { + setCloneFiles((state) => + state.map((item) => + item.id === file.id ? { ...item, error: getErrText(error) } : item + ) + ); + } + }) + ); + }, [appId, chatId, cloneFiles, outLinkAuthData]); + // Watch newfiles,and upload + useRequest2(uploadFiles, { + refreshDeps: [cloneFiles], + manual: false + }); + + // Url upload props + const [urlInput, setUrlInput] = useState(''); + const handleAddUrl = useCallback( + (url: string) => { + if (!url) return; + + const urlSchema = z.string().url(); + const result = urlSchema.safeParse(url); + if (!result.success) { + return toast({ + title: t('common:invalid_url'), + status: 'error' + }); + } + + const trimmedUrl = url.trim(); + if (trimmedUrl) { + setCloneFiles((state) => [ + ...state, + { + id: getNanoid(6), + status: 1, + type: 'file', + url: trimmedUrl, + name: trimmedUrl, + icon: 'common/link' + } + ]); + } + + setUrlInput(''); + }, + [t, toast] + ); + + const handleDeleteFile = useCallback((id: string) => { + setCloneFiles((state) => state.filter((file) => file.id !== id)); + }, []); + + return ( + <> + {/* Selector */} + + {canLocalUpload && ( + e.preventDefault(), + onDragLeave: handleDragLeave, + onDrop: handleDrop, + onClick: onOpen + })} + > + + {isMaxSelected ? ( + <> + + {t('file:reached_max_file_count')} + + + ) : ( + <> + + {isDragging + ? t('file:release_the_mouse_to_upload_the_file') + : t('file:select_and_drag_file_tip')} + + onSelectFile(files)} /> + + )} + + )} + {canUrlUpload && ( + + + + setUrlInput(e.target.value)} + onBlur={(e) => handleAddUrl(e.target.value)} + border={'1.5px dashed'} + borderColor={'myGray.250'} + borderRadius={'md'} + pl={8} + py={1.5} + placeholder={ + isMaxSelected ? t('file:reached_max_file_count') : t('chat:click_to_add_url') + } + /> + + + )} + + + {/* Preview */} + {cloneFiles.length > 0 && ( + <> + + + {cloneFiles.map((file) => { + return ( + + + + + {file.name} + + + {/* Status icon */} + {!!file.url || !!file.error ? ( + } + onClick={() => handleDeleteFile(file.id)} + /> + ) : ( + + + + )} + + {file.error && ( + + {file.error} + + )} + + ); + })} + + + )} + + ); +}; + +export default React.memo(FileSelector); diff --git a/projects/app/src/components/core/app/formRender/LabelAndForm.tsx b/projects/app/src/components/core/app/formRender/LabelAndForm.tsx index 7de4ce2ded98..3ddbcde6ffc9 100644 --- a/projects/app/src/components/core/app/formRender/LabelAndForm.tsx +++ b/projects/app/src/components/core/app/formRender/LabelAndForm.tsx @@ -44,6 +44,7 @@ const LabelAndFormRender = ({ form: UseFormReturn; fieldName: string; + isDisabled?: boolean; minLength?: number; } & SpecificProps & BoxProps) => { @@ -62,8 +63,13 @@ const LabelAndFormRender = ({ name={props.fieldName} rules={{ validate: (value) => { - if (!required) return true; if (typeof value === 'number' || typeof value === 'boolean') return true; + if (inputType === InputTypeEnum.password && props.minLength) { + if (!value || typeof value !== 'object' || !value.value) return false; + return value.value.length >= props.minLength; + } + if (!required) return true; + return !!value; }, ...(!!props?.minLength @@ -79,6 +85,7 @@ const LabelAndFormRender = ({ return ( void; + onDateTimeChange: (date: Date | undefined) => void; popPosition?: 'top' | 'bottom'; timeGranularity?: 'day' | 'hour' | 'minute' | 'second'; minDate?: Date; @@ -14,17 +14,25 @@ type TimeInputProps = { }; const TimeInput: React.FC = ({ - value, + value: initialValue, onDateTimeChange, popPosition = 'bottom', timeGranularity = 'second', minDate, maxDate }) => { + const formatValue = useMemo(() => { + const val = initialValue ? new Date(initialValue) : undefined; + // 判断有效性 + if (!val) return undefined; + if (isNaN(val.getTime())) return undefined; + return val; + }, [initialValue]); + const { t } = useTranslation(); - const hour = value ? value.getHours() : 0; - const minute = value ? value.getMinutes() : 0; - const second = value ? value.getSeconds() : 0; + const hour = formatValue ? formatValue.getHours() : 0; + const minute = formatValue ? formatValue.getMinutes() : 0; + const second = formatValue ? formatValue.getSeconds() : 0; const validateAndSetDateTime = (newDate: Date) => { if (minDate && newDate < minDate) { @@ -38,26 +46,31 @@ const TimeInput: React.FC = ({ onDateTimeChange(newDate); }; - const handleDateChange = (date: Date) => { + const handleDateChange = (date: Date | undefined) => { + if (!date) { + onDateTimeChange(undefined); + return; + } + const newDate = new Date(date); newDate.setHours(hour, minute, second); validateAndSetDateTime(newDate); }; const handleHourChange = (newHour?: number) => { - const newDate = value ? new Date(value) : new Date(); + const newDate = formatValue ? formatValue : new Date(); newDate.setHours(newHour || 0); validateAndSetDateTime(newDate); }; const handleMinuteChange = (newMinute?: number) => { - const newDate = value ? new Date(value) : new Date(); + const newDate = formatValue ? formatValue : new Date(); newDate.setMinutes(newMinute || 0); validateAndSetDateTime(newDate); }; const handleSecondChange = (newSecond?: number) => { - const newDate = value ? new Date(value) : new Date(); + const newDate = formatValue ? formatValue : new Date(); newDate.setSeconds(newSecond || 0); validateAndSetDateTime(newDate); }; @@ -69,7 +82,7 @@ const TimeInput: React.FC = ({ return ( { const { @@ -26,13 +29,11 @@ const InputRender = (props: InputRenderProps) => { bg = 'white' } = props; - const [isPasswordEditing, setIsPasswordEditing] = useState(false); - - if (customRender) { - return <>{customRender(props)}; - } - const { t } = useSafeTranslation(); + const { llmModelList } = useSystemStore(); + + // Password + const [isPasswordEditing, setIsPasswordEditing] = useState(false); const isSelectAll = useMemo(() => { return ( @@ -43,85 +44,94 @@ const InputRender = (props: InputRenderProps) => { // @ts-ignore }, [inputType, value, props.list?.length]); - const commonProps = { - value, - onChange, - isDisabled, - isInvalid, - placeholder: t(placeholder as any), - bg - }; + const commonProps = useMemoEnhance( + () => ({ + value, + onChange, + isDisabled, + isInvalid, + placeholder: t(placeholder as any), + bg + }), + [bg, isDisabled, isInvalid, onChange, placeholder, t, value] + ); + + if (customRender) { + return <>{customRender(props)}; + } - const renderInput = () => { - if (inputType === InputTypeEnum.input) { - return ( - - ); - } + if (inputType === InputTypeEnum.input) { + return ( + + ); + } + + if (inputType === InputTypeEnum.textarea) { + return ( + + ); + } - if (inputType === InputTypeEnum.textarea) { - return ( - - ); - } + if (inputType === InputTypeEnum.password) { + const isPasswordConfigured = isSecretValue(value); + const val = typeof value === 'object' && value !== null ? value.value : ''; - if (inputType === InputTypeEnum.password) { - const isPasswordConfigured = isSecretValue(value); - return !isPasswordConfigured || isPasswordEditing ? ( - setIsPasswordEditing(false)} - autoComplete="new-password" - data-form-type="other" - /> - ) : ( - - - - - {t('common:had_auth_value')} - - + return !isPasswordConfigured || isPasswordEditing ? ( + { + const val = e.target.value; + onChange({ + value: val, + secret: '' + }); + }} + value={val} + type="password" + autoFocus={isPasswordEditing} + onBlur={() => setIsPasswordEditing(false)} + autoComplete="new-password" + data-form-type="other" + data-lpignore="true" + /> + ) : ( + + + + + {t('common:had_auth_value')} + + + {!isDisabled && ( } @@ -129,157 +139,197 @@ const InputRender = (props: InputRenderProps) => { variant="ghost" color={'myGray.500'} _hover={{ color: 'primary.600' }} - isDisabled={isDisabled} onClick={() => setIsPasswordEditing(true)} /> - - ); - } + )} + + ); + } - if (inputType === InputTypeEnum.numberInput) { - return ( - - ); - } + if (inputType === InputTypeEnum.numberInput) { + return ( + + ); + } - if (inputType === InputTypeEnum.switch) { - return ( - onChange(e.target.checked)} - isDisabled={isDisabled} - /> - ); - } + if (inputType === InputTypeEnum.switch) { + return ( + onChange(e.target.checked)} + isDisabled={isDisabled} + /> + ); + } + + if (inputType === InputTypeEnum.select) { + const list = + props.list || props.enums?.map((item) => ({ label: item.value, value: item.value })) || []; + return ; + } - if (inputType === InputTypeEnum.select) { - const list = - props.list || props.enums?.map((item) => ({ label: item.value, value: item.value })) || []; - return ; - } + if (inputType === InputTypeEnum.multipleSelect) { + const list = + props.list || props.enums?.map((item) => ({ label: item.value, value: item.value })) || []; + return ( + + {...commonProps} + h={10} + list={list} + value={value} + onSelect={onChange} + isSelectAll={isSelectAll} + itemWrap + /> + ); + } - if (inputType === InputTypeEnum.multipleSelect) { - const list = - props.list || props.enums?.map((item) => ({ label: item.value, value: item.value })) || []; - return ( - - {...commonProps} - h={10} - list={list} - value={value} - onSelect={onChange} - isSelectAll={isSelectAll} - itemWrap - /> - ); - } + if (inputType === InputTypeEnum.JSONEditor) { + return ; + } - if (inputType === InputTypeEnum.JSONEditor) { - return ; - } + if (inputType === InputTypeEnum.selectLLMModel) { + return ( + ({ + value: item.model, + label: item.name + })) || [] + } + /> + ); + } - if (inputType === InputTypeEnum.selectLLMModel) { - return ( - ({ - value: item.model, - label: item.name - })) || [] - } - /> - ); - } + if (inputType === InputTypeEnum.fileSelect) { + return ( + + ); + } - if (inputType === InputTypeEnum.fileSelect) { - return ( - - ); - } + if (inputType === InputTypeEnum.selectDataset) { + const list = props.dataset?.map((item: any) => ({ + label: item.name, + value: item.datasetId, + icon: item.avatar, + iconSize: '1.5rem' + })); - if (inputType === InputTypeEnum.timePointSelect) { - const { timeRangeStart, timeRangeEnd } = props; - return ( - onChange(date.toISOString())} - timeGranularity={props.timeGranularity} - minDate={timeRangeStart ? new Date(timeRangeStart) : undefined} - maxDate={timeRangeEnd ? new Date(timeRangeEnd) : undefined} - /> - ); - } + const selectedValues = Array.isArray(value) + ? value.map((item: any) => (typeof item === 'string' ? item : item.datasetId)) + : []; - if (inputType === InputTypeEnum.timeRangeSelect) { - const { timeRangeStart, timeRangeEnd } = props; - const rangeArray = Array.isArray(value) ? value : [null, null]; - const [startDate, endDate] = rangeArray; - return ( - - - - {t('app:time_range_start')} - - { - const newArray = [...rangeArray]; - newArray[0] = date.toISOString(); - onChange(newArray); - }} - timeGranularity={props.timeGranularity} - maxDate={ - endDate ? new Date(endDate) : timeRangeEnd ? new Date(timeRangeEnd) : undefined - } - minDate={timeRangeStart ? new Date(timeRangeStart) : undefined} - /> + return ( + + {...commonProps} + h={10} + list={list ?? []} + value={selectedValues} + onSelect={(selectedVals) => { + onChange( + selectedVals.map((val) => { + const item = list?.find((l) => l.value === val); + return item + ? { + name: item.label, + datasetId: item.value, + icon: item.icon + } + : { name: val, datasetId: val, icon: '' }; + }) + ); + }} + isSelectAll={selectedValues.length === list?.length && list?.length > 0} + itemWrap + /> + ); + } + + if (inputType === InputTypeEnum.timePointSelect) { + const { timeRangeStart, timeRangeEnd, defaultValue } = props; + const val = value || defaultValue; + return ( + onChange(date ? date.toISOString() : undefined)} + timeGranularity={props.timeGranularity} + minDate={timeRangeStart ? new Date(timeRangeStart) : undefined} + maxDate={timeRangeEnd ? new Date(timeRangeEnd) : undefined} + /> + ); + } + + if (inputType === InputTypeEnum.timeRangeSelect) { + const { timeRangeStart, timeRangeEnd } = props; + const rangeArray = Array.isArray(value) ? value : [null, null]; + const [startDate, endDate] = rangeArray; + return ( + + + + {t('app:time_range_start')} - - - {t('app:time_range_end')} - - { - const newArray = [...rangeArray]; - newArray[1] = date.toISOString(); - onChange(newArray); - }} - timeGranularity={props.timeGranularity} - minDate={ - startDate - ? new Date(startDate) - : timeRangeStart - ? new Date(timeRangeStart) - : undefined - } - maxDate={timeRangeEnd ? new Date(timeRangeEnd) : undefined} - /> + { + const newArray = [...rangeArray]; + newArray[0] = date ? date.toISOString() : undefined; + onChange(newArray); + }} + timeGranularity={props.timeGranularity} + maxDate={ + endDate ? new Date(endDate) : timeRangeEnd ? new Date(timeRangeEnd) : undefined + } + minDate={timeRangeStart ? new Date(timeRangeStart) : undefined} + /> + + + + {t('app:time_range_end')} + { + const newArray = [...rangeArray]; + newArray[1] = date ? date.toISOString() : undefined; + onChange(newArray); + }} + timeGranularity={props.timeGranularity} + minDate={ + startDate + ? new Date(startDate) + : timeRangeStart + ? new Date(timeRangeStart) + : undefined + } + maxDate={timeRangeEnd ? new Date(timeRangeEnd) : undefined} + /> - ); - } - - return null; - }; + + ); + } - return {renderInput()}; + return null; }; export default InputRender; diff --git a/projects/app/src/components/core/app/formRender/type.d.ts b/projects/app/src/components/core/app/formRender/type.d.ts index 951f03e38e46..98d03beef87c 100644 --- a/projects/app/src/components/core/app/formRender/type.d.ts +++ b/projects/app/src/components/core/app/formRender/type.d.ts @@ -7,6 +7,7 @@ import type { VariableInputEnum } from '@fastgpt/global/core/workflow/constants' import type { UseFormReturn } from 'react-hook-form'; import type { BoxProps } from '@chakra-ui/react'; import type { EditorProps } from '@fastgpt/web/components/common/Textarea/PromptEditor/Editor'; +import type { AppFileSelectConfigType } from '@fastgpt/global/core/app/type'; type CommonRenderProps = { placeholder?: string; @@ -27,12 +28,14 @@ type SpecificProps = variableLabels?: EditorVariableLabelPickerType[]; title?: string; maxLength?: number; + isRichText?: boolean; } & { ExtensionPopover?: EditorProps['ExtensionPopover']; }) | { // password inputType: InputTypeEnum.password; + minLength?: number; } | { // numberInput @@ -52,6 +55,12 @@ type SpecificProps = // old version enums?: { value: string }[]; } + | { + // selectDataset + inputType: InputTypeEnum.selectDataset; + list?: { label: string; value: string }[]; + dataset?: { name: string; datasetId: string; avatar: string }[]; + } | { // JSONEditor inputType: InputTypeEnum.JSONEditor; @@ -61,16 +70,15 @@ type SpecificProps = inputType: InputTypeEnum.selectLLMModel; modelList?: { model: string; name: string }[]; } - | { + | ({ // fileSelect inputType: InputTypeEnum.fileSelect; - canSelectFile?: boolean; - canSelectImg?: boolean; - maxFiles?: number; setUploading?: React.Dispatch>; form?: UseFormReturn; fieldName?: string; - } + canLocalUpload?: boolean; + canUrlUpload?: boolean; + } & AppFileSelectConfigType) | { // timePointSelect inputType: InputTypeEnum.timePointSelect; diff --git a/projects/app/src/components/core/app/formRender/utils.ts b/projects/app/src/components/core/app/formRender/utils.ts index 55606ab41774..0ed8ae317d6b 100644 --- a/projects/app/src/components/core/app/formRender/utils.ts +++ b/projects/app/src/components/core/app/formRender/utils.ts @@ -20,6 +20,8 @@ export const variableInputTypeToInputType = ( if (inputType === VariableInputEnum.file) return InputTypeEnum.fileSelect; if (inputType === VariableInputEnum.timePointSelect) return InputTypeEnum.timePointSelect; if (inputType === VariableInputEnum.timeRangeSelect) return InputTypeEnum.timeRangeSelect; + if (inputType === VariableInputEnum.datasetSelect) return InputTypeEnum.selectDataset; + if (inputType === VariableInputEnum.llmSelect) return InputTypeEnum.selectLLMModel; if (inputType === VariableInputEnum.custom || inputType === VariableInputEnum.internal) return valueTypeToInputType(valueType); return InputTypeEnum.JSONEditor; @@ -31,6 +33,7 @@ export const nodeInputTypeToInputType = (inputTypes: FlowNodeInputTypeEnum[] = [ if (inputType === FlowNodeInputTypeEnum.input) return InputTypeEnum.input; if (inputType === FlowNodeInputTypeEnum.textarea) return InputTypeEnum.textarea; + if (inputType === FlowNodeInputTypeEnum.password) return InputTypeEnum.password; if (inputType === FlowNodeInputTypeEnum.numberInput) return InputTypeEnum.numberInput; if (inputType === FlowNodeInputTypeEnum.switch) return InputTypeEnum.switch; if (inputType === FlowNodeInputTypeEnum.select) return InputTypeEnum.select; diff --git a/projects/app/src/components/core/app/plugin/CostTooltip.tsx b/projects/app/src/components/core/app/tool/CostTooltip.tsx similarity index 100% rename from projects/app/src/components/core/app/plugin/CostTooltip.tsx rename to projects/app/src/components/core/app/tool/CostTooltip.tsx diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/Input/ChatInput.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/Input/ChatInput.tsx index 0e87d496d280..3fa6508d889b 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/Input/ChatInput.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/Input/ChatInput.tsx @@ -80,6 +80,9 @@ const ChatInput = ({ selectFileLabel, showSelectFile, showSelectImg, + showSelectVideo, + showSelectAudio, + showSelectCustomFileExtension, removeFiles, replaceFiles, hasFileUploading @@ -92,6 +95,12 @@ const ChatInput = ({ }); const havInput = !!inputValue || fileList.length > 0; const canSendMessage = havInput && !hasFileUploading; + const canUploadFile = + showSelectFile || + showSelectImg || + showSelectVideo || + showSelectAudio || + showSelectCustomFileExtension; // Upload files useRequest2(uploadFiles, { @@ -201,7 +210,7 @@ const ChatInput = ({ }} onPaste={(e) => { const clipboardData = e.clipboardData; - if (clipboardData && (showSelectFile || showSelectImg)) { + if (clipboardData && canUploadFile) { const items = clipboardData.items; const files = Array.from(items) .map((item) => (item.kind === 'file' ? item.getAsFile() : undefined)) @@ -233,8 +242,7 @@ const ChatInput = ({ offFocus, setValue, handleSend, - showSelectFile, - showSelectImg, + canUploadFile, onSelectFile ] ); @@ -266,7 +274,7 @@ const ChatInput = ({ {/* Attachment and Voice Group */} {/* file selector button */} - {(showSelectFile || showSelectImg) && ( + {canUploadFile && ( {/* Divider Container */} - {((whisperConfig?.open && !inputValue) || showSelectFile || showSelectImg) && ( + {((whisperConfig?.open && !inputValue) || canUploadFile) && ( @@ -353,8 +361,8 @@ const ChatInput = ({ ); }, [ isPc, - showSelectFile, - showSelectImg, + InputLeftComponent, + canUploadFile, selectFileLabel, selectFileIcon, File, @@ -366,8 +374,7 @@ const ChatInput = ({ onOpenSelectFile, onSelectFile, handleSend, - onStop, - InputLeftComponent + onStop ]); const activeStyles: FlexProps = { @@ -381,7 +388,7 @@ const ChatInput = ({ onDrop={(e) => { e.preventDefault(); - if (!(showSelectFile || showSelectImg)) return; + if (!canUploadFile) return; const files = Array.from(e.dataTransfer.files); const droppedFiles = files.filter((file) => fileTypeFilter(file)); diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/ChatItem.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/ChatItem.tsx index 50733e5a0d6c..894ed8b3894c 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/ChatItem.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/ChatItem.tsx @@ -177,7 +177,7 @@ const ChatItem = (props: Props) => { return colorMap[statusBoxData.status]; }, [statusBoxData?.status]); - /* + /* 1. The interactive node is divided into n dialog boxes. 2. Auto-complete the last textnode */ diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/VariableInputForm.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/VariableInputForm.tsx index e1e9cb0b667b..bd2fdfbb6931 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/VariableInputForm.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/VariableInputForm.tsx @@ -14,7 +14,7 @@ import LabelAndFormRender from '@/components/core/app/formRender/LabelAndForm'; import { variableInputTypeToInputType } from '@/components/core/app/formRender/utils'; import type { VariableItemType } from '@fastgpt/global/core/app/type'; -const VariableInput = ({ +const VariableInputForm = ({ chatForm, chatStarted, chatType @@ -71,6 +71,8 @@ const VariableInput = ({ internalVariableList.length > 0 || externalVariableList.length > 0; + const isDisabled = chatType === ChatTypeEnum.log; + return hasVariables ? ( @@ -101,6 +103,7 @@ const VariableInput = ({ return ( { const showSelectFile = fileSelectConfig?.canSelectFile; const showSelectImg = fileSelectConfig?.canSelectImg; + const showSelectVideo = fileSelectConfig?.canSelectVideo; + const showSelectAudio = fileSelectConfig?.canSelectAudio; + const showSelectCustomFileExtension = fileSelectConfig?.canSelectCustomFileExtension; + const canUploadFile = + showSelectFile || + showSelectImg || + showSelectVideo || + showSelectAudio || + showSelectCustomFileExtension; const maxSelectFiles = fileSelectConfig?.maxFiles ?? 10; const maxSize = (feConfigs?.uploadFileMaxSize || 1024) * 1024 * 1024; // nkb const canSelectFileAmount = maxSelectFiles - fileList.length; const { icon: selectFileIcon, label: selectFileLabel } = useMemo(() => { - if (showSelectFile && showSelectImg) { - return { - icon: 'core/chat/fileSelect', - label: t('chat:select_file_img') - }; - } else if (showSelectFile) { + if (canUploadFile) { return { icon: 'core/chat/fileSelect', label: t('chat:select_file') }; - } else if (showSelectImg) { - return { - icon: 'core/chat/imgSelect', - label: t('chat:select_img') - }; } return {}; - }, [showSelectFile, showSelectImg, t]); + }, [canUploadFile, t]); + + const fileType = useMemo(() => { + return getUploadFileType({ + canSelectFile: showSelectFile, + canSelectImg: showSelectImg, + canSelectVideo: showSelectVideo, + canSelectAudio: showSelectAudio, + canSelectCustomFileExtension: showSelectCustomFileExtension, + customFileExtensionList: fileSelectConfig?.customFileExtensionList + }); + }, [ + fileSelectConfig?.customFileExtensionList, + showSelectAudio, + showSelectCustomFileExtension, + showSelectFile, + showSelectImg, + showSelectVideo + ]); const { File, onOpen: onOpenSelectFile } = useSelectFile({ - fileType: `${showSelectImg ? 'image/*,' : ''} ${showSelectFile ? documentFileType : ''}`, + fileType, multiple: true, maxCount: canSelectFileAmount }); @@ -156,27 +175,39 @@ export const useFileUpload = (props: UseFileUploadOptions) => { try { const fileIndex = fileList.findIndex((item) => item.id === file.id)!; - // Start upload and update process - const { previewUrl } = await uploadFile2DB({ - file: copyFile.rawFile, - bucketName: 'chat', - data: { - appId, - ...outLinkAuthData - }, - metadata: { - chatId + // Get Upload Post Presigned URL + const { url, fields, maxSize } = await getUploadChatFilePresignedUrl({ + filename: copyFile.rawFile.name, + appId, + chatId, + outLinkAuthData + }); + + // Upload File to S3 + const formData = new FormData(); + Object.entries(fields).forEach(([k, v]) => formData.set(k, v)); + formData.set('file', copyFile.rawFile); + await POST(url, formData, { + headers: { + 'Content-Type': 'multipart/form-data; charset=utf-8' }, - percentListen(e) { - copyFile.process = e; - if (!copyFile.url) { - updateFiles(fileIndex, copyFile); - } + onUploadProgress: (e) => { + if (!e.total) return; + const percent = Math.round((e.loaded / e.total) * 100); + copyFile.process = percent; + updateFiles(fileIndex, copyFile); } + }).catch((error) => Promise.reject(parseS3UploadError({ t, error, maxSize }))); + + const previewUrl = await getPresignedChatFileGetUrl({ + key: fields.key, + appId, + outLinkAuthData }); - // Update file url + // Update file url and key copyFile.url = previewUrl; + copyFile.key = fields.key; updateFiles(fileIndex, copyFile); } catch (error) { errorFileIndex.push(fileList.findIndex((item) => item.id === file.id)!); @@ -216,6 +247,9 @@ export const useFileUpload = (props: UseFileUploadOptions) => { selectFileLabel, showSelectFile, showSelectImg, + showSelectVideo, + showSelectAudio, + showSelectCustomFileExtension, removeFiles, replaceFiles, hasFileUploading 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 afe90eb8cdb0..48ea038144b9 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx @@ -63,6 +63,7 @@ import TimeBox from './components/TimeBox'; import MyBox from '@fastgpt/web/components/common/MyBox'; import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants'; import { valueTypeFormat } from '@fastgpt/global/core/workflow/runtime/utils'; +import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time'; const FeedbackModal = dynamic(() => import('./components/FeedbackModal')); const ReadFeedbackModal = dynamic(() => import('./components/ReadFeedbackModal')); @@ -179,7 +180,7 @@ const ChatBox = ({ (item) => item.type !== VariableInputEnum.custom && item.type !== VariableInputEnum.internal ); - /* + /* 对话已经开始的标记: 1. 保证 appId 一致。 2. 有对话记录/手动点了开始/默认没有需要填写的变量。 @@ -462,12 +463,19 @@ const ChatBox = ({ // Only declared variables are kept const requestVariables: Record = {}; variableList?.forEach((item) => { - const val = + let val = variables[item.key] === '' || variables[item.key] === undefined || variables[item.key] === null ? item.defaultValue : variables[item.key]; + + if (item.type === VariableInputEnum.timePointSelect && val) { + val = formatTime2YMDHMS(new Date(val)); + } else if (item.type === VariableInputEnum.timeRangeSelect && val) { + val = val.map((item: string) => (item ? formatTime2YMDHMS(new Date(item)) : '')); + } + requestVariables[item.key] = valueTypeFormat(val, item.valueType); }); @@ -493,7 +501,8 @@ const ChatBox = ({ type: file.type, name: file.name, url: file.url || '', - icon: file.icon || '' + icon: file.icon || '', + key: file.key || '' } })), ...(text @@ -544,7 +553,14 @@ const ChatBox = ({ // 这里,无论是否为交互模式,最后都是 Human 的消息。 const messages = chats2GPTMessages({ - messages: newChatList.slice(0, -1), + messages: newChatList.slice(0, -1).map((item) => { + if (item.obj === ChatRoleEnum.Human) { + item.files?.forEach((file) => { + file.url = ''; + }); + } + return item; + }), reserveId: true, reserveTool: true }); diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/type.d.ts b/projects/app/src/components/core/chat/ChatContainer/ChatBox/type.d.ts index 4a274c78ecff..122a7a625496 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/type.d.ts +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/type.d.ts @@ -12,7 +12,9 @@ export type UserInputFileItemType = { icon: string; // img is base64 status: 0 | 1; // 0: uploading, 1: success url?: string; + key?: string; // S3 key for the file process?: number; + error?: string; }; export type ChatBoxInputFormType = { diff --git a/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/components/RenderInput.tsx b/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/components/RenderInput.tsx index 110d784ee0b1..7b659289645f 100644 --- a/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/components/RenderInput.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/components/RenderInput.tsx @@ -253,10 +253,17 @@ const RenderInput = () => { name={inputKey} rules={{ validate: (value) => { - if (!input.required) return true; - if (input.valueType === WorkflowIOValueTypeEnum.boolean) { - return value !== undefined; + if (isDisabledInput) return true; + if ( + input.renderTypeList.includes(FlowNodeInputTypeEnum.password) && + input.minLength + ) { + if (!value || typeof value !== 'object' || !value.value) return false; + return value.value.length >= input.minLength; } + if (typeof value === 'number' || typeof value === 'boolean') return true; + if (!input.required) return true; + return !!value; } }} @@ -274,6 +281,7 @@ const RenderInput = () => { form={variablesForm} fieldName={inputKey} modelList={llmModelList} + isRichText={false} /> ); }} diff --git a/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/context.tsx b/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/context.tsx index d8c677d3fc6b..207aa8a6e343 100644 --- a/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/context.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/context.tsx @@ -15,7 +15,7 @@ import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/cons import { useTranslation } from 'next-i18next'; import { type ChatBoxInputFormType } from '../ChatBox/type'; import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt'; -import { getPluginRunUserQuery } from '@fastgpt/global/core/workflow/utils'; +import { clientGetWorkflowToolRunUserQuery } from '@fastgpt/global/core/workflow/utils'; import { cloneDeep } from 'lodash'; import { ChatItemContext } from '@/web/core/chat/context/chatItemContext'; import { ChatRecordContext } from '@/web/core/chat/context/chatRecordContext'; @@ -193,7 +193,7 @@ const PluginRunContextProvider = ({ setChatRecords([ { - ...getPluginRunUserQuery({ + ...clientGetWorkflowToolRunUserQuery({ pluginInputs, variables, files: files as RuntimeUserPromptType['files'] diff --git a/projects/app/src/components/core/chat/components/Interactive/InteractiveComponents.tsx b/projects/app/src/components/core/chat/components/Interactive/InteractiveComponents.tsx index de086a48ceab..422b4c3818fe 100644 --- a/projects/app/src/components/core/chat/components/Interactive/InteractiveComponents.tsx +++ b/projects/app/src/components/core/chat/components/Interactive/InteractiveComponents.tsx @@ -100,6 +100,7 @@ export const FormInputComponent = React.memo(function FormInputComponent({ min={input.min} max={input.max} list={input.list} + isRichText={false} /> ); }} diff --git a/projects/app/src/components/core/chat/components/WholeResponseModal.tsx b/projects/app/src/components/core/chat/components/WholeResponseModal.tsx index 31240d672000..fd067d98a9dd 100644 --- a/projects/app/src/components/core/chat/components/WholeResponseModal.tsx +++ b/projects/app/src/components/core/chat/components/WholeResponseModal.tsx @@ -136,10 +136,7 @@ export const WholeResponseContent = ({ > {/* common info */} <> - + {activeModule?.totalPoints !== undefined && ( startMongoWatch() - }); + await Promise.all([ + connectMongo({ + db: connectionMongo, + url: MONGO_URL, + connectedCb: () => startMongoWatch() + }), + initBullMQWorkers() + ]); connectMongo({ db: connectionLogMongo, url: MONGO_LOG_URL @@ -68,7 +73,7 @@ export async function register() { await Promise.all([ preLoadWorker().catch(), getSystemTools(), - initSystemPluginGroups(), + initSystemPluginTags(), initAppTemplateTypes() ]); diff --git a/projects/app/src/pageComponents/app/detail/HTTPTools/ChatTest.tsx b/projects/app/src/pageComponents/app/detail/HTTPTools/ChatTest.tsx index ca2781705669..c1d814324401 100644 --- a/projects/app/src/pageComponents/app/detail/HTTPTools/ChatTest.tsx +++ b/projects/app/src/pageComponents/app/detail/HTTPTools/ChatTest.tsx @@ -11,7 +11,7 @@ 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 { postRunHTTPTool } from '@/web/core/app/api/tool'; 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'; diff --git a/projects/app/src/pageComponents/app/detail/HTTPTools/EditForm.tsx b/projects/app/src/pageComponents/app/detail/HTTPTools/EditForm.tsx index 75a28401370a..661096d994db 100644 --- a/projects/app/src/pageComponents/app/detail/HTTPTools/EditForm.tsx +++ b/projects/app/src/pageComponents/app/detail/HTTPTools/EditForm.tsx @@ -20,7 +20,7 @@ import { type HttpToolConfigType } from '@fastgpt/global/core/app/type'; import MyModal from '@fastgpt/web/components/common/MyModal'; import Avatar from '@fastgpt/web/components/common/Avatar'; import MyBox from '@fastgpt/web/components/common/MyBox'; -import { putUpdateHttpPlugin } from '@/web/core/app/api/plugin'; +import { putUpdateHttpPlugin } from '@/web/core/app/api/tool'; import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; import SchemaConfigModal from './SchemaConfigModal'; import ManualToolModal from './ManualToolModal'; diff --git a/projects/app/src/pageComponents/app/detail/HTTPTools/ManualToolModal.tsx b/projects/app/src/pageComponents/app/detail/HTTPTools/ManualToolModal.tsx index ba0f9f29d910..96924d40fbc5 100644 --- a/projects/app/src/pageComponents/app/detail/HTTPTools/ManualToolModal.tsx +++ b/projects/app/src/pageComponents/app/detail/HTTPTools/ManualToolModal.tsx @@ -46,7 +46,7 @@ import HeaderAuthForm from '@/components/common/secret/HeaderAuthForm'; import type { StoreSecretValueType } from '@fastgpt/global/common/secret/type'; import MyIcon from '@fastgpt/web/components/common/Icon'; import HttpInput from '@fastgpt/web/components/common/Input/HttpInput'; -import { putUpdateHttpPlugin } from '@/web/core/app/api/plugin'; +import { putUpdateHttpPlugin } from '@/web/core/app/api/tool'; import type { HttpToolConfigType } from '@fastgpt/global/core/app/type'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import CurlImportModal from './CurlImportModal'; diff --git a/projects/app/src/pageComponents/app/detail/HTTPTools/SchemaConfigModal.tsx b/projects/app/src/pageComponents/app/detail/HTTPTools/SchemaConfigModal.tsx index c4cd9fe98e67..324b04089332 100644 --- a/projects/app/src/pageComponents/app/detail/HTTPTools/SchemaConfigModal.tsx +++ b/projects/app/src/pageComponents/app/detail/HTTPTools/SchemaConfigModal.tsx @@ -19,14 +19,14 @@ import { } 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 { getApiSchemaByUrl, putUpdateHttpPlugin } from '@/web/core/app/api/tool'; 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 { pathData2ToolList } from '@fastgpt/global/core/app/httpTools/utils'; +import { pathData2ToolList } from '@fastgpt/global/core/app/tool/httpTool/utils'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { str2OpenApiSchema } from '@fastgpt/global/core/app/jsonschema'; import { diff --git a/projects/app/src/pageComponents/app/detail/Logs/LogChart.tsx b/projects/app/src/pageComponents/app/detail/Logs/LogChart.tsx index 0edad7986852..e3188860b7da 100644 --- a/projects/app/src/pageComponents/app/detail/Logs/LogChart.tsx +++ b/projects/app/src/pageComponents/app/detail/Logs/LogChart.tsx @@ -782,7 +782,6 @@ const HeaderControl = ({ [t] ); - console.log(showSourceSelector); return ( {showSourceSelector && ( diff --git a/projects/app/src/pageComponents/app/detail/Logs/LogTable.tsx b/projects/app/src/pageComponents/app/detail/Logs/LogTable.tsx index 1ad9ef2e9df2..368ad7a37d4c 100644 --- a/projects/app/src/pageComponents/app/detail/Logs/LogTable.tsx +++ b/projects/app/src/pageComponents/app/detail/Logs/LogTable.tsx @@ -149,8 +149,8 @@ const LogTable = ({ filename: 'chat_logs.csv', body: { appId, - dateStart: dateRange.from || new Date(), - dateEnd: addDays(dateRange.to || new Date(), 1), + dateStart: dayjs(dateRange.from || new Date()).format(), + dateEnd: dayjs(addDays(dateRange.to || new Date(), 1)).format(), sources: isSelectAllSource ? undefined : chatSources, tmbIds: isSelectAllTmb ? undefined : selectTmbIds, chatSearch, diff --git a/projects/app/src/pageComponents/app/detail/MCPTools/ChatTest.tsx b/projects/app/src/pageComponents/app/detail/MCPTools/ChatTest.tsx index f88096d62e04..2064d0fbd9e6 100644 --- a/projects/app/src/pageComponents/app/detail/MCPTools/ChatTest.tsx +++ b/projects/app/src/pageComponents/app/detail/MCPTools/ChatTest.tsx @@ -7,11 +7,11 @@ 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 McpToolConfigType } from '@fastgpt/global/core/app/type'; +import { type McpToolConfigType } from '@fastgpt/global/core/app/tool/mcpTool/type'; import { useForm } from 'react-hook-form'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import Markdown from '@/components/Markdown'; -import { postRunMCPTool } from '@/web/core/app/api/plugin'; +import { postRunMCPTool } from '@/web/core/app/api/tool'; 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'; diff --git a/projects/app/src/pageComponents/app/detail/MCPTools/Edit.tsx b/projects/app/src/pageComponents/app/detail/MCPTools/Edit.tsx index 5f9b5dd745a7..5d2f90a577c8 100644 --- a/projects/app/src/pageComponents/app/detail/MCPTools/Edit.tsx +++ b/projects/app/src/pageComponents/app/detail/MCPTools/Edit.tsx @@ -7,7 +7,7 @@ import AppCard from './AppCard'; import ChatTest from './ChatTest'; import MyBox from '@fastgpt/web/components/common/MyBox'; import EditForm from './EditForm'; -import { type McpToolConfigType } from '@fastgpt/global/core/app/type'; +import { type McpToolConfigType } from '@fastgpt/global/core/app/tool/mcpTool/type'; import { type StoreSecretValueType } from '@fastgpt/global/common/secret/type'; const Edit = ({ diff --git a/projects/app/src/pageComponents/app/detail/MCPTools/EditForm.tsx b/projects/app/src/pageComponents/app/detail/MCPTools/EditForm.tsx index 172e002fa13c..2bf63f695cb8 100644 --- a/projects/app/src/pageComponents/app/detail/MCPTools/EditForm.tsx +++ b/projects/app/src/pageComponents/app/detail/MCPTools/EditForm.tsx @@ -7,12 +7,12 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { AppContext } from '../context'; import { useContextSelector } from 'use-context-selector'; import MyIconButton from '@fastgpt/web/components/common/Icon/button'; -import { type McpToolConfigType } from '@fastgpt/global/core/app/type'; +import { type McpToolConfigType } from '@fastgpt/global/core/app/tool/mcpTool/type'; import MyModal from '@fastgpt/web/components/common/MyModal'; import Avatar from '@fastgpt/web/components/common/Avatar'; import MyBox from '@fastgpt/web/components/common/MyBox'; import type { getMCPToolsBody } from '@/pages/api/support/mcp/client/getTools'; -import { getMCPTools } from '@/web/core/app/api/plugin'; +import { getMCPTools } from '@/web/core/app/api/tool'; import HeaderAuthConfig from '@/components/common/secret/HeaderAuthConfig'; import { type StoreSecretValueType } from '@fastgpt/global/common/secret/type'; diff --git a/projects/app/src/pageComponents/app/detail/MCPTools/Header.tsx b/projects/app/src/pageComponents/app/detail/MCPTools/Header.tsx index cf7ad7fd4f5c..783f1422891a 100644 --- a/projects/app/src/pageComponents/app/detail/MCPTools/Header.tsx +++ b/projects/app/src/pageComponents/app/detail/MCPTools/Header.tsx @@ -8,8 +8,8 @@ import { getAppFolderPath } from '@/web/core/app/api/app'; import { useCallback } from 'react'; import { useRouter } from 'next/router'; import { useSystemStore } from '@/web/common/system/useSystemStore'; -import { type McpToolConfigType } from '@fastgpt/global/core/app/type'; -import { postUpdateMCPTools } from '@/web/core/app/api/plugin'; +import { type McpToolConfigType } from '@fastgpt/global/core/app/tool/mcpTool/type'; +import { postUpdateMCPTools } from '@/web/core/app/api/tool'; import { type StoreSecretValueType } from '@fastgpt/global/common/secret/type'; const Header = ({ diff --git a/projects/app/src/pageComponents/app/detail/MCPTools/index.tsx b/projects/app/src/pageComponents/app/detail/MCPTools/index.tsx index 81fe6709cd99..e647f9edd494 100644 --- a/projects/app/src/pageComponents/app/detail/MCPTools/index.tsx +++ b/projects/app/src/pageComponents/app/detail/MCPTools/index.tsx @@ -5,7 +5,7 @@ import Edit from './Edit'; import { useContextSelector } from 'use-context-selector'; import { AppContext } from '../context'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; -import { type McpToolConfigType } from '@fastgpt/global/core/app/type'; +import { type McpToolConfigType } from '@fastgpt/global/core/app/tool/mcpTool/type'; import { type StoreSecretValueType } from '@fastgpt/global/common/secret/type'; const MCPTools = () => { diff --git a/projects/app/src/pageComponents/app/detail/Plugin/Header.tsx b/projects/app/src/pageComponents/app/detail/Plugin/Header.tsx index e83e8627b717..e3c2fba1fae5 100644 --- a/projects/app/src/pageComponents/app/detail/Plugin/Header.tsx +++ b/projects/app/src/pageComponents/app/detail/Plugin/Header.tsx @@ -106,6 +106,10 @@ const Header = () => { ) ); } + }, + { + manual: true, + refreshDeps: [onSaveApp, setPast, flowData2StoreData, appDetail.chatConfig] } ); @@ -157,8 +161,7 @@ const Header = () => { } aria-label={''} - size={'xs'} - w={'1rem'} + size={'xsSquare'} variant={'ghost'} onClick={isSaved ? onBack : onOpenBackConfirm} /> diff --git a/projects/app/src/pageComponents/app/detail/SimpleApp/components/ConfigToolModal.tsx b/projects/app/src/pageComponents/app/detail/SimpleApp/components/ConfigToolModal.tsx index aa3b5c73b529..407df20de5df 100644 --- a/projects/app/src/pageComponents/app/detail/SimpleApp/components/ConfigToolModal.tsx +++ b/projects/app/src/pageComponents/app/detail/SimpleApp/components/ConfigToolModal.tsx @@ -17,8 +17,8 @@ import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; import SecretInputModal, { type ToolParamsFormType -} from '@/pageComponents/app/plugin/SecretInputModal'; -import { SystemToolInputTypeMap } from '@fastgpt/global/core/app/systemTool/constants'; +} from '@/pageComponents/app/tool/SecretInputModal'; +import { SystemToolSecretInputTypeMap } from '@fastgpt/global/core/app/tool/systemTool/constants'; import { useBoolean } from 'ahooks'; const ConfigToolModal = ({ @@ -119,7 +119,7 @@ const ConfigToolModal = ({ } return t('workflow:tool_active_config_type', { - type: t(SystemToolInputTypeMap[val.type]?.text as any) + type: t(SystemToolSecretInputTypeMap[val.type]?.text as any) }); })()} @@ -164,6 +164,7 @@ const ConfigToolModal = ({ return ( void }) => { const { t } = useTranslation(); const { appDetail } = useContextSelector(AppContext, (v) => v); - const [templateType, setTemplateType] = useState(TemplateTypeEnum.systemPlugin); + const [templateType, setTemplateType] = useState(TemplateTypeEnum.appTool); const [parentId, setParentId] = useState(''); const [searchKey, setSearchKey] = useState(''); + const [selectedTagIds, setSelectedTagIds] = useState([]); const { - data: templates = [], + data: rawTemplates = [], runAsync: loadTemplates, loading: isLoading } = useRequest2( @@ -95,10 +80,10 @@ const ToolSelectModal = ({ onClose, ...props }: Props & { onClose: () => void }) parentId?: ParentIdType; searchVal?: string; }) => { - if (type === TemplateTypeEnum.systemPlugin) { - return getSystemPlugTemplates({ parentId, searchKey: searchVal }); - } else if (type === TemplateTypeEnum.teamPlugin) { - return getTeamPlugTemplates({ + if (type === TemplateTypeEnum.appTool) { + return getAppToolTemplates({ parentId, searchKey: searchVal }); + } else if (type === TemplateTypeEnum.teamApp) { + return getTeamAppTemplates({ parentId, searchKey: searchVal }).then((res) => res.filter((app) => app.id !== appDetail._id)); @@ -114,11 +99,21 @@ const ToolSelectModal = ({ onClose, ...props }: Props & { onClose: () => void }) } ); + const templates = useMemo(() => { + if (selectedTagIds.length === 0 || templateType !== TemplateTypeEnum.appTool) { + return rawTemplates; + } + return rawTemplates.filter((template) => { + // @ts-ignore + return template.toolTags?.some((toolTag) => selectedTagIds.includes(toolTag)); + }); + }, [rawTemplates, selectedTagIds, templateType]); + const { data: paths = [] } = useRequest2( () => { - if (templateType === TemplateTypeEnum.teamPlugin) + if (templateType === TemplateTypeEnum.teamApp) return getAppFolderPath({ sourceId: parentId, type: 'current' }); - return getSystemPluginPaths({ sourceId: parentId, type: 'current' }); + return getAppToolPaths({ sourceId: parentId, type: 'current' }); }, { manual: false, @@ -126,6 +121,10 @@ const ToolSelectModal = ({ onClose, ...props }: Props & { onClose: () => void }) } ); + const { data: allTags = [] } = useRequest2(getPluginToolTags, { + manual: false + }); + const onUpdateParentId = useCallback( (parentId: ParentIdType) => { loadTemplates({ @@ -158,12 +157,12 @@ const ToolSelectModal = ({ onClose, ...props }: Props & { onClose: () => void }) { icon: 'phoneTabbar/tool', label: t('common:navbar.Toolkit'), - value: TemplateTypeEnum.systemPlugin + value: TemplateTypeEnum.appTool }, { - icon: 'core/modules/teamPlugin', + icon: 'core/modules/teamApp', label: t('common:core.module.template.Team app'), - value: TemplateTypeEnum.teamPlugin + value: TemplateTypeEnum.teamApp } ]} py={'5px'} @@ -181,13 +180,23 @@ const ToolSelectModal = ({ onClose, ...props }: Props & { onClose: () => void }) value={searchKey} onChange={(e) => setSearchKey(e.target.value)} placeholder={ - templateType === TemplateTypeEnum.systemPlugin + templateType === TemplateTypeEnum.appTool ? t('common:search_tool') : t('app:search_app') } /> + {templateType === TemplateTypeEnum.appTool && allTags.length > 0 && ( + + + + )} {/* route components */} {!searchKey && parentId && ( @@ -217,8 +226,7 @@ const RenderList = React.memo(function RenderList({ onRemoveTool, setParentId, selectedTools, - chatConfig, - selectedModel + chatConfig }: Props & { templates: NodeTemplateListItemType[]; type: TemplateTypeEnum; @@ -234,7 +242,7 @@ const RenderList = React.memo(function RenderList({ const { runAsync: onClickAdd, loading: isLoading } = useRequest2( async (template: NodeTemplateListItemType) => { - const res = await getPreviewPluginNode({ appId: template.id }); + const res = await getToolPreviewNode({ appId: template.id }); /* Invalid plugin check 1. Reference type. but not tool description; @@ -321,273 +329,149 @@ const RenderList = React.memo(function RenderList({ } ); - const { data: pluginGroups = [] } = useRequest2(getPluginGroups, { - manual: false - }); - - const formatTemplatesArray = useMemo(() => { - const data = (() => { - if (type === TemplateTypeEnum.systemPlugin) { - return pluginGroups.map((group) => { - const map = group.groupTypes.reduce< - Record< - string, - { - list: NodeTemplateListItemType[]; - label: string; - } - > - >((acc, item) => { - acc[item.typeId] = { - list: [], - label: t(parseI18nString(item.typeName, i18n.language)) - }; - return acc; - }, {}); - - templates.forEach((item) => { - if (map[item.templateType]) { - map[item.templateType].list.push({ - ...item, - name: t(parseI18nString(item.name, i18n.language)), - intro: t(parseI18nString(item.intro, i18n.language)) - }); - } - }); - return { - label: group.groupName, - list: Object.entries(map) - .map(([type, { list, label }]) => ({ - type, - label, - list - })) - .filter((item) => item.list.length > 0) - }; - }); - } - - // Team apps - return [ - { - list: [ - { - list: templates, - type: '', - label: '' - } - ], - label: '' - } - ]; - })(); - - return data.filter(({ list }) => list.length > 0); - }, [i18n.language, pluginGroups, t, templates, type]); - - const gridStyle = useMemo(() => { - if (type === TemplateTypeEnum.teamPlugin) { - return { - gridTemplateColumns: ['1fr', '1fr'], - py: 2, - avatarSize: '2rem' - }; - } - - return { - gridTemplateColumns: ['1fr', '1fr 1fr'], - py: 3, - avatarSize: '1.75rem' - }; - }, [type]); - - const PluginListRender = useMemoizedFn(({ list = [] }: { list: NodeTemplateListType }) => { - const isSystemTool = type === TemplateTypeEnum.systemPlugin; + const PluginListRender = useMemoizedFn(() => { + const isSystemTool = type === TemplateTypeEnum.appTool; return ( <> - {list.map((item, i) => { - return ( - - - - {t(item.label as any)} - - - - {item.list.map((template) => { - const selected = selectedTools.some((tool) => tool.pluginId === template.id); - - return ( - - - - - {t(template.name as any)} - - {isSystemTool && ( - - By {template.author || feConfigs?.systemTitle} - - )} - - - {t(template.intro as any) || t('common:core.workflow.Not intro')} - - {isSystemTool && ( - - )} - - } - > - + {templates.length > 0 ? ( + + {templates.map((template) => { + const selected = selectedTools.some((tool) => tool.pluginId === template.id); + return ( + + - - {t(template.name as any)} + + {t(parseI18nString(template.name, i18n.language))} - - {selected ? ( - - ) : template.flowNodeType === 'toolSet' ? ( - - - - - ) : template.isFolder ? ( - - ) : ( - + {isSystemTool && ( + + By {template.author || feConfigs?.systemTitle} + )} - - ); - })} - - - ); - })} + + {t(parseI18nString(template.intro || '', i18n.language)) || + t('common:core.workflow.Not intro')} + + {isSystemTool && ( + + )} + + } + > + + + + + {t(parseI18nString(template.name, i18n.language))} + + + + {selected ? ( + + ) : template.flowNodeType === 'toolSet' ? ( + + + + + ) : template.isFolder ? ( + + ) : ( + + )} + + + ); + })} + + ) : ( + + )} ); }); - return templates.length === 0 ? ( - - ) : ( + return ( <> - - {formatTemplatesArray.length > 1 ? ( - <> - {formatTemplatesArray.map(({ list, label }, index) => ( - - - {t(label as any)} - - - - - - - ))} - - ) : ( - - )} - + {!!configTool && ( { versionName?: string; }) => { const data = flowData2StoreData(); - if (data) { await onSaveApp({ ...data, @@ -106,6 +105,10 @@ const Header = () => { ) ); } + }, + { + manual: true, + refreshDeps: [onSaveApp, setPast, flowData2StoreData, appDetail.chatConfig] } ); diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/NodeTemplatesModal.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/NodeTemplatesModal.tsx index 515d82ea9c58..8cb45319c901 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/NodeTemplatesModal.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/NodeTemplatesModal.tsx @@ -29,7 +29,10 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => { templatesIsLoading, templates, onUpdateTemplateType, - onUpdateParentId + onUpdateParentId, + selectedTagIds, + setSelectedTagIds, + toolTags } = useNodeTemplates(); const onAddNode = useMemoizedFn(async ({ newNodes }: { newNodes: Node[] }) => { @@ -86,6 +89,9 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => { searchKey={searchKey} setSearchKey={setSearchKey} onUpdateParentId={onUpdateParentId} + selectedTagIds={selectedTagIds} + setSelectedTagIds={setSelectedTagIds} + toolTags={toolTags} /> { templatesIsLoading, templates, onUpdateTemplateType, - onUpdateParentId + onUpdateParentId, + toolTags, + selectedTagIds, + setSelectedTagIds } = useNodeTemplates(); const onAddNode = useMemoizedFn(async ({ newNodes }: { newNodes: Node[] }) => { @@ -121,6 +124,9 @@ const NodeTemplatesPopover = () => { onUpdateParentId={onUpdateParentId} searchKey={searchKey} setSearchKey={setSearchKey} + toolTags={toolTags} + selectedTagIds={selectedTagIds} + setSelectedTagIds={setSelectedTagIds} /> >; onUpdateTemplateType: (type: TemplateTypeEnum) => void; onUpdateParentId: (parentId: string) => void; + + selectedTagIds: string[]; + setSelectedTagIds: (e: string[]) => any; + toolTags: SystemPluginToolTagType[]; }; const NodeTemplateListHeader = ({ @@ -37,7 +43,10 @@ const NodeTemplateListHeader = ({ searchKey, setSearchKey, onUpdateTemplateType, - onUpdateParentId + onUpdateParentId, + selectedTagIds, + setSelectedTagIds, + toolTags }: NodeTemplateListHeaderProps) => { const { t } = useTranslation(); const { feConfigs } = useSystemStore(); @@ -46,9 +55,9 @@ const NodeTemplateListHeader = ({ // Get paths const { data: paths = [] } = useRequest2( () => { - if (templateType === TemplateTypeEnum.teamPlugin) + if (templateType === TemplateTypeEnum.teamApp) return getAppFolderPath({ sourceId: parentId, type: 'current' }); - return getSystemPluginPaths({ sourceId: parentId, type: 'current' }); + return getAppToolPaths({ sourceId: parentId, type: 'current' }); }, { manual: false, @@ -71,12 +80,12 @@ const NodeTemplateListHeader = ({ { icon: 'phoneTabbar/tool', label: t('common:navbar.Toolkit'), - value: TemplateTypeEnum.systemPlugin + value: TemplateTypeEnum.appTool }, { - icon: 'core/modules/teamPlugin', + icon: 'core/modules/teamApp', label: t('common:core.module.template.Team app'), - value: TemplateTypeEnum.teamPlugin + value: TemplateTypeEnum.teamApp } ]} width={'100%'} @@ -113,8 +122,7 @@ const NodeTemplateListHeader = ({ )} {/* Search */} - {(templateType === TemplateTypeEnum.teamPlugin || - templateType === TemplateTypeEnum.systemPlugin) && ( + {(templateType === TemplateTypeEnum.teamApp || templateType === TemplateTypeEnum.appTool) && ( @@ -124,7 +132,7 @@ const NodeTemplateListHeader = ({ h={'full'} bg={'myGray.50'} placeholder={ - templateType === TemplateTypeEnum.teamPlugin + templateType === TemplateTypeEnum.teamApp ? t('common:plugin.Search_app') : t('common:search_tool') } @@ -133,7 +141,7 @@ const NodeTemplateListHeader = ({ /> - {!isPopover && templateType === TemplateTypeEnum.teamPlugin && ( + {!isPopover && templateType === TemplateTypeEnum.teamApp && ( )} - {!isPopover && - templateType === TemplateTypeEnum.systemPlugin && - feConfigs.systemPluginCourseUrl && ( - window.open(feConfigs.systemPluginCourseUrl)} - gap={1} - ml={4} - > - {t('common:plugin.contribute')} - - - )} + {templateType === TemplateTypeEnum.appTool && ( + router.push('/toolkit/tools')} + gap={1} + ml={4} + > + {t('app:find_more_tools')} + + + )} )} + {/* Tag filter */} + {templateType === TemplateTypeEnum.appTool && + selectedTagIds !== undefined && + setSelectedTagIds && ( + + + + )} {/* paths */} - {(templateType === TemplateTypeEnum.teamPlugin || - templateType === TemplateTypeEnum.systemPlugin) && + {(templateType === TemplateTypeEnum.teamApp || templateType === TemplateTypeEnum.appTool) && !searchKey && parentId && ( diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/components/NodeTemplates/list.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/components/NodeTemplates/list.tsx index 5b894fa75454..17b83d12a2cc 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/components/NodeTemplates/list.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/components/NodeTemplates/list.tsx @@ -12,9 +12,7 @@ import { css } from '@chakra-ui/react'; import { useTranslation } from 'next-i18next'; -import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; -import { getPluginGroups, getPreviewPluginNode } from '@/web/core/app/api/plugin'; -import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; +import { getToolPreviewNode } from '@/web/core/app/api/tool'; import type { FlowNodeItemType, NodeTemplateListItemType, @@ -25,7 +23,7 @@ import { useMemoizedFn } from 'ahooks'; import MyIcon from '@fastgpt/web/components/common/Icon'; import MyAvatar from '@fastgpt/web/components/common/Avatar'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; -import CostTooltip from '@/components/core/app/plugin/CostTooltip'; +import CostTooltip from '@/components/core/app/tool/CostTooltip'; import { FlowNodeTypeEnum, AppNodeFlowNodeTypeMap @@ -45,7 +43,6 @@ import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workfl import { nodeTemplate2FlowNode } from '@/web/core/workflow/utils'; import { useToast } from '@fastgpt/web/hooks/useToast'; import { parseI18nString } from '@fastgpt/global/common/i18n/utils'; -import { useSafeTranslation } from '@fastgpt/web/hooks/useSafeTranslation'; import { useSystemStore } from '@/web/common/system/useSystemStore'; import { WorkflowModalContext } from '../../../context/workflowModalContext'; @@ -73,13 +70,13 @@ const NodeTemplateListItem = ({ isPopover?: boolean; onUpdateParentId: (parentId: string) => void; }) => { - const { t } = useSafeTranslation(); + const { t } = useTranslation(); const { feConfigs } = useSystemStore(); const { screenToFlowPosition } = useReactFlow(); const handleParams = useContextSelector(WorkflowModalContext, (v) => v.handleParams); const isToolHandle = handleParams?.handleId === 'selectedTools'; - const isSystemTool = templateType === TemplateTypeEnum.systemPlugin; + const isSystemTool = templateType === TemplateTypeEnum.appTool; return ( - - {t(template.name as any)} + + + {t(template.name as any)} + {/* Folder right arrow */} {template.isFolder && ( @@ -224,10 +221,6 @@ const NodeTemplateList = ({ const { getNodeList, getNodeById } = useContextSelector(WorkflowBufferDataContext, (v) => v); const handleParams = useContextSelector(WorkflowModalContext, (v) => v.handleParams); - const { data: pluginGroups = [] } = useRequest2(getPluginGroups, { - manual: false - }); - const handleAddNode = useCallback( async ({ template, @@ -240,7 +233,7 @@ const NodeTemplateList = ({ const templateNode = await (async () => { try { if (AppNodeFlowNodeTypeMap[template.flowNodeType]) { - return await getPreviewPluginNode({ appId: template.id }); + return await getToolPreviewNode({ appId: template.id }); } const baseTemplate = moduleTemplatesFlat.find((item) => item.id === template.id); @@ -370,7 +363,7 @@ const NodeTemplateList = ({ }, {}); templates.forEach((item) => { - if (map[item.templateType]) { + if (item.templateType && map[item.templateType]) { map[item.templateType].list.push({ ...item, name: t(item.name as any), @@ -393,48 +386,6 @@ const NodeTemplateList = ({ ]; } - if (templateType === TemplateTypeEnum.systemPlugin) { - console.log(pluginGroups, 222); - return pluginGroups.map((group) => { - const map = group.groupTypes.reduce< - Record< - string, - { - list: NodeTemplateListItemType[]; - label: string; - } - > - >((acc, item) => { - acc[item.typeId] = { - list: [], - label: t(parseI18nString(item.typeName, i18n.language)) - }; - return acc; - }, {}); - - templates.forEach((item) => { - if (map[item.templateType]) { - map[item.templateType].list.push({ - ...item, - name: t(parseI18nString(item.name, i18n.language)), - intro: t(parseI18nString(item.intro, i18n.language)) - }); - } - }); - return { - label: group.groupName, - list: Object.entries(map) - .map(([type, { list, label }]) => ({ - type, - label, - list - })) - .filter((item) => item.list.length > 0) - }; - }); - } - - // Team apps return [ { label: '', @@ -442,16 +393,20 @@ const NodeTemplateList = ({ { type: '', label: '', - list: templates + list: templates.map((item) => ({ + ...item, + name: t(parseI18nString(item.name, i18n.language)), + intro: t(parseI18nString(item.intro || '', i18n.language)) + })) } ] } ]; })(); return data.filter(({ list }) => list.length > 0); - }, [templateType, templates, t, pluginGroups, i18n.language]); + }, [templateType, templates, t, i18n.language]); - const PluginListRender = useMemoizedFn(({ list = [] }: { list: NodeTemplateListType }) => { + const NodeListRender = useMemoizedFn(({ list = [] }: { list: NodeTemplateListType }) => { return ( <> {list.map((item) => { @@ -478,7 +433,7 @@ const NodeTemplateList = ({ @@ -500,9 +455,7 @@ const NodeTemplateList = ({ ); }); - return templates.length === 0 ? ( - - ) : ( + return ( 1 ? 2 : 5}> {formatTemplatesArrayData.length > 1 ? ( @@ -522,13 +475,13 @@ const NodeTemplateList = ({ - + ))} ) : ( - + )} diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/components/NodeTemplates/useNodeTemplates.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/components/NodeTemplates/useNodeTemplates.tsx index 0348db493474..695bb358cedb 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/components/NodeTemplates/useNodeTemplates.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/components/NodeTemplates/useNodeTemplates.tsx @@ -3,13 +3,14 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useSystemStore } from '@/web/common/system/useSystemStore'; import type { NodeTemplateListItemType } from '@fastgpt/global/core/workflow/type/node'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; -import { getTeamPlugTemplates, getSystemPlugTemplates } from '@/web/core/app/api/plugin'; +import { getTeamAppTemplates, getAppToolTemplates } from '@/web/core/app/api/tool'; import { TemplateTypeEnum } from './header'; import { useContextSelector } from 'use-context-selector'; import { WorkflowBufferDataContext } from '../../../context/workflowInitContext'; import type { ParentIdType } from '@fastgpt/global/common/parentFolder/type'; import { useDebounceEffect } from 'ahooks'; import { AppContext } from '@/pageComponents/app/detail/context'; +import { getPluginToolTags } from '@/web/core/plugin/toolTag/api'; export const useNodeTemplates = () => { const { feConfigs } = useSystemStore(); @@ -26,6 +27,11 @@ export const useNodeTemplates = () => { (v) => v ); + const [selectedTagIds, setSelectedTagIds] = useState([]); + const { data: toolTags = [] } = useRequest2(getPluginToolTags, { + manual: false + }); + const { data: basicNodes } = useRequest2( async () => { if (templateType === TemplateTypeEnum.basic) { @@ -72,31 +78,34 @@ export const useNodeTemplates = () => { ); const { - data: teamAndSystemApps, + data: teamAndSystemTools, loading: templatesIsLoading, runAsync: loadNodeTemplates } = useRequest2( async ({ parentId, type = templateType, - searchVal + searchVal, + tags }: { parentId?: ParentIdType; type?: TemplateTypeEnum; searchVal?: string; + tags?: string[]; }) => { - if (type === TemplateTypeEnum.teamPlugin) { + if (type === TemplateTypeEnum.teamApp) { // app, workflow-plugin, mcp - return getTeamPlugTemplates({ + return getTeamAppTemplates({ parentId, searchKey: searchVal }).then((res) => res.filter((app) => app.id !== appId)); } - if (type === TemplateTypeEnum.systemPlugin) { + if (type === TemplateTypeEnum.appTool) { // systemTool - return getSystemPlugTemplates({ + return getAppToolTemplates({ searchKey: searchVal, - parentId + parentId, + tags }); } }, @@ -113,7 +122,7 @@ export const useNodeTemplates = () => { return; } - loadNodeTemplates({ parentId, searchVal: searchKey }); + loadNodeTemplates({ parentId, searchVal: searchKey, tags: selectedTagIds }); }, [searchKey], { @@ -135,18 +144,26 @@ export const useNodeTemplates = () => { searchKeyLock.current = true; setSearchKey(''); setParentId(''); + setSelectedTagIds([]); setTemplateType(type); loadNodeTemplates({ type }); }, [loadNodeTemplates] ); + const onUpdateSelectedTagIds = useCallback( + (tags: string[]) => { + setSelectedTagIds(tags); + loadNodeTemplates({ parentId, searchVal: searchKey, tags }); + }, + [loadNodeTemplates, parentId, searchKey] + ); const templates = useMemo(() => { if (templateType === TemplateTypeEnum.basic) { return basicNodes || []; } - return teamAndSystemApps || []; - }, [basicNodes, teamAndSystemApps, templateType]); + return teamAndSystemTools || []; + }, [basicNodes, teamAndSystemTools, templateType]); return { templateType, @@ -156,6 +173,9 @@ export const useNodeTemplates = () => { onUpdateParentId, onUpdateTemplateType, searchKey, - setSearchKey + setSearchKey, + selectedTagIds, + setSelectedTagIds: onUpdateSelectedTagIds, + toolTags }; }; diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/components/ToolParamConfig.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/components/ToolParamConfig.tsx index a83352ace876..5045ba1fdad2 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/components/ToolParamConfig.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/components/ToolParamConfig.tsx @@ -6,17 +6,16 @@ import { Box, Button } from '@chakra-ui/react'; import { useBoolean } from 'ahooks'; import { useContextSelector } from 'use-context-selector'; import { WorkflowBufferDataContext } from '../../context/workflowInitContext'; -import { SystemToolInputTypeMap } from '@fastgpt/global/core/app/systemTool/constants'; +import { SystemToolSecretInputTypeMap } from '@fastgpt/global/core/app/tool/systemTool/constants'; import SecretInputModal, { type ToolParamsFormType -} from '@/pageComponents/app/plugin/SecretInputModal'; +} from '@/pageComponents/app/tool/SecretInputModal'; import { WorkflowActionsContext } from '../../context/workflowActionsContext'; const ToolConfig = ({ nodeId, inputs }: { nodeId?: string; inputs?: FlowNodeInputItemType[] }) => { const { t } = useTranslation(); const onChangeNode = useContextSelector(WorkflowActionsContext, (v) => v.onChangeNode); - const getNodeById = useContextSelector(WorkflowBufferDataContext, (v) => v.getNodeById); - const node = getNodeById(nodeId); + const node = useContextSelector(WorkflowBufferDataContext, (v) => v.getNodeById(nodeId)); const inputConfig = inputs?.find((item) => item.key === NodeInputKeyEnum.systemInputConfig); const inputList = inputConfig?.inputList; @@ -29,7 +28,7 @@ const ToolConfig = ({ nodeId, inputs }: { nodeId?: string; inputs?: FlowNodeInpu } return t('workflow:tool_active_config_type', { - type: t(SystemToolInputTypeMap[val.type]?.text as any) + type: t(SystemToolSecretInputTypeMap[val.type]?.text as any) }); }, [inputConfig?.value, t]); diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/Loop/NodeLoop.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/Loop/NodeLoop.tsx index 6a5fa8d4e19d..18a98e1f0b67 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/Loop/NodeLoop.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/Loop/NodeLoop.tsx @@ -48,7 +48,7 @@ const NodeLoop = ({ data, selected }: NodeProps) => { WorkflowLayoutContext, (v) => v.resetParentNodeSizeAndPosition ); - const computedResult = useMemo(() => { + const computedResult = useMemoEnhance(() => { return { nodeWidth: Math.round( Number(inputs.find((input) => input.key === NodeInputKeyEnum.nodeWidth)?.value) || 500 @@ -146,6 +146,8 @@ const NodeLoop = ({ data, selected }: NodeProps) => { setTimeout(() => { resetParentNodeSizeAndPosition(nodeId); }, 50); + + // eslint-disable-next-line react-hooks/exhaustive-deps }, [size?.height]); return ( diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/Loop/NodeLoopStart.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/Loop/NodeLoopStart.tsx index 54f672587046..9af4c4afd082 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/Loop/NodeLoopStart.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/Loop/NodeLoopStart.tsx @@ -85,50 +85,56 @@ const NodeLoopStart = ({ data, selected }: NodeProps) => { } }, [loopStartNode?.outputs, nodeId, onChangeNode, loopItemInputType, t]); - return ( - - - - - - - - - - - - - {outputs.map((output) => ( - - - {output.valueType && } + const Render = useMemo(() => { + return ( + + + + +
{t('workflow:Variable_name')}{t('common:core.workflow.Value type')}
- - - {t(output.label as any)} - - {FlowValueTypeMap[output.valueType]?.label}
+ + + + - ))} - -
+ {t('workflow:Variable_name')} + {t('common:core.workflow.Value type')}
-
+ + + {outputs.map((output) => ( + + + + + {t(output.label as any)} + + + {output.valueType && {FlowValueTypeMap[output.valueType]?.label}} + + ))} + + + +
-
- - ); + + ); + }, [data, outputs, selected, t]); + + return Render; }; export default React.memo(NodeLoopStart); diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeLaf.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeLaf.tsx index f807d452551e..79e80cb7cf96 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeLaf.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeLaf.tsx @@ -8,7 +8,7 @@ import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { useTranslation } from 'next-i18next'; import { getLafAppDetail } from '@/web/support/laf/api'; import MySelect from '@fastgpt/web/components/common/MySelect'; -import { getApiSchemaByUrl } from '@/web/core/app/api/plugin'; +import { getApiSchemaByUrl } from '@/web/core/app/api/tool'; import { useUserStore } from '@/web/support/user/useUserStore'; import { useSystemStore } from '@/web/common/system/useSystemStore'; import { ChevronRightIcon } from '@chakra-ui/icons'; diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodePluginIO/InputEditModal.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodePluginIO/InputEditModal.tsx index 3037901a4c91..5a9946403d1a 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodePluginIO/InputEditModal.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodePluginIO/InputEditModal.tsx @@ -61,9 +61,9 @@ const FieldEditModal = ({ defaultValueType: WorkflowIOValueTypeEnum.string }, { - icon: 'core/workflow/inputType/jsonEditor', - label: t('common:core.workflow.inputType.JSON Editor'), - value: [FlowNodeInputTypeEnum.JSONEditor, FlowNodeInputTypeEnum.reference], + icon: 'core/workflow/inputType/password', + label: t('common:core.workflow.inputType.password'), + value: [FlowNodeInputTypeEnum.password], defaultValueType: WorkflowIOValueTypeEnum.string }, { @@ -212,6 +212,13 @@ const FieldEditModal = ({ data.key = data.label; + // Remove undefined keys + Object.keys(data).forEach((key) => { + if (data[key as keyof FlowNodeInputItemType] === undefined) { + delete data[key as keyof FlowNodeInputItemType]; + } + }); + if (action === 'confirm') { onSubmit(data); onClose(); @@ -224,7 +231,7 @@ const FieldEditModal = ({ reset(defaultInput); } }, - [defaultValue.key, defaultValueType, isEdit, keys, onSubmit, t, toast, onClose, reset] + [defaultValue.key, keys, toast, t, defaultValueType, isEdit, onSubmit, onClose, reset] ); const onSubmitError = useCallback( (e: Object) => { diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodePluginIO/InputTypeConfig.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodePluginIO/InputTypeConfig.tsx index 73c8564378c4..1718cea61495 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodePluginIO/InputTypeConfig.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodePluginIO/InputTypeConfig.tsx @@ -1,8 +1,10 @@ import { Box, Button, + Checkbox, Flex, FormControl, + Grid, HStack, Input, Stack, @@ -23,20 +25,26 @@ import MultipleSelect, { } from '@fastgpt/web/components/common/MySelect/MultipleSelect'; import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; import JsonEditor from '@fastgpt/web/components/common/Textarea/JsonEditor'; -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { useFieldArray, type UseFormReturn } from 'react-hook-form'; import { useTranslation } from 'next-i18next'; import MyIcon from '@fastgpt/web/components/common/Icon'; +import Avatar from '@fastgpt/web/components/common/Avatar'; import DndDrag, { Draggable } from '@fastgpt/web/components/common/DndDrag'; import MyTextarea from '@/components/common/Textarea/MyTextarea'; import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput'; import TimeInput from '@/components/core/app/formRender/TimeInput'; -import ChatFunctionTip from '@/components/core/app/Tip'; import MySlider from '@/components/Slider'; import { useSystemStore } from '@/web/common/system/useSystemStore'; import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; import RadioGroup from '@fastgpt/web/components/common/Radio/RadioGroup'; +import { DatasetSelectModal } from '@/components/core/app/DatasetSelectModal'; +import type { EmbeddingModelItemType } from '@fastgpt/global/core/ai/model.d'; +import AIModelSelector from '@/components/Select/AIModelSelector'; +import { useMemoEnhance } from '@fastgpt/web/hooks/useMemoEnhance'; +import type { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io'; +import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time'; const InputTypeConfig = ({ form, @@ -64,20 +72,29 @@ const InputTypeConfig = ({ }) => { const { t } = useTranslation(); const defaultListValue = { label: t('common:None'), value: '' }; - const { feConfigs } = useSystemStore(); + const { feConfigs, llmModelList } = useSystemStore(); - const typeLabels = { - name: { - formInput: t('common:core.module.input_name'), - plugin: t('common:core.module.Field Name'), - variable: t('workflow:Variable_name') - }, - description: { - formInput: t('common:core.module.input_description'), - plugin: t('workflow:field_description'), - variable: t('workflow:variable_description') - } - }; + const availableModels = useMemoEnhance(() => { + return llmModelList.map((model) => ({ + value: model.model, + label: model.name + })); + }, [llmModelList]); + + const typeLabels = useMemo(() => { + return { + name: { + formInput: t('common:core.module.input_name'), + plugin: t('common:core.module.Field Name'), + variable: t('workflow:Variable_name') + }, + description: { + formInput: t('common:core.module.input_description'), + plugin: t('workflow:field_description'), + variable: t('workflow:variable_description') + } + }; + }, [t]); const { register, setValue, handleSubmit, control, watch } = form; const maxLength = watch('maxLength'); @@ -88,12 +105,28 @@ const InputTypeConfig = ({ const valueType = watch('valueType'); const timeGranularity = watch('timeGranularity'); - const timeType = watch('timeType'); const timeRangeStart = watch('timeRangeStart'); const timeRangeEnd = watch('timeRangeEnd'); + const timeRangeStartDefault = + inputType === VariableInputEnum.timeRangeSelect && Array.isArray(defaultValue) + ? defaultValue?.[0] + : undefined; + const timeRangeEndDefault = + inputType === VariableInputEnum.timeRangeSelect && Array.isArray(defaultValue) + ? defaultValue?.[1] + : undefined; const maxFiles = watch('maxFiles'); const maxSelectFiles = Math.min(feConfigs?.uploadFileMaxAmount ?? 20, 50); + const canSelectFile = watch('canSelectFile'); + const canSelectImg = watch('canSelectImg'); + const canLocalUpload = watch('canLocalUpload'); + const canUrlUpload = watch('canUrlUpload'); + + const [isDatasetSelectOpen, setIsDatasetSelectOpen] = useState(false); + const [datasetList, setDatasetList] = useState< + { name: string; datasetId: string; avatar: string }[] + >([]); const selectValueTypeList = watch('customInputConfig.selectValueTypeList'); const { isSelectAll: isSelectAllValueType, setIsSelectAll: setIsSelectAllValueType } = @@ -153,18 +186,21 @@ const InputTypeConfig = ({ }, [inputType]); const showDefaultValue = useMemo(() => { - const list = [ - FlowNodeInputTypeEnum.input, - FlowNodeInputTypeEnum.JSONEditor, - FlowNodeInputTypeEnum.numberInput, - FlowNodeInputTypeEnum.switch, - FlowNodeInputTypeEnum.select, - FlowNodeInputTypeEnum.multipleSelect, - VariableInputEnum.custom, - VariableInputEnum.internal - ]; - - return list.includes(inputType as FlowNodeInputTypeEnum); + const map = { + [FlowNodeInputTypeEnum.input]: true, + [FlowNodeInputTypeEnum.JSONEditor]: true, + [FlowNodeInputTypeEnum.numberInput]: true, + [FlowNodeInputTypeEnum.switch]: true, + [FlowNodeInputTypeEnum.select]: true, + [FlowNodeInputTypeEnum.multipleSelect]: true, + [VariableInputEnum.custom]: true, + [VariableInputEnum.internal]: true, + [VariableInputEnum.timePointSelect]: true, + [VariableInputEnum.timeRangeSelect]: true, + [VariableInputEnum.llmSelect]: true + }; + + return map[inputType as keyof typeof map]; }, [inputType]); const showIsToolInput = useMemo(() => { @@ -180,9 +216,84 @@ const InputTypeConfig = ({ return type === 'plugin' && list.includes(inputType as FlowNodeInputTypeEnum); }, [inputType, type]); + const filterValidField = useCallback( + (data: Record) => { + const commonData: Record = { + renderTypeList: data.renderTypeList, + type: data.type, + + key: data.key, + label: data.label, + valueType: data.valueType, + valueDesc: data.valueDesc, + description: data.description, + toolDescription: data.toolDescription, + required: data.required, + defaultValue: data.defaultValue + }; + + switch (inputType) { + case FlowNodeInputTypeEnum.input: + case FlowNodeInputTypeEnum.textarea: + commonData.maxLength = data.maxLength; + break; + case FlowNodeInputTypeEnum.numberInput: + commonData.max = data.max; + commonData.min = data.min; + break; + case FlowNodeInputTypeEnum.select: + case FlowNodeInputTypeEnum.multipleSelect: + commonData.list = data.list; + break; + case FlowNodeInputTypeEnum.addInputParam: + commonData.customInputConfig = data.customInputConfig; + break; + case FlowNodeInputTypeEnum.fileSelect: + commonData.canSelectFile = data.canSelectFile; + commonData.canSelectImg = data.canSelectImg; + commonData.canSelectVideo = data.canSelectVideo; + commonData.canSelectAudio = data.canSelectAudio; + commonData.canSelectCustomFileExtension = data.canSelectCustomFileExtension; + commonData.customFileExtensionList = data.customFileExtensionList; + commonData.canLocalUpload = data.canLocalUpload; + commonData.canUrlUpload = data.canUrlUpload; + commonData.maxFiles = data.maxFiles; + break; + case FlowNodeInputTypeEnum.timePointSelect: + case FlowNodeInputTypeEnum.timeRangeSelect: + commonData.timeGranularity = data.timeGranularity; + commonData.timeRangeStart = data.timeRangeStart; + commonData.timeRangeEnd = data.timeRangeEnd; + case FlowNodeInputTypeEnum.password: + commonData.minLength = data.minLength; + break; + } + + if (commonData.timeRangeStart) { + commonData.timeRangeStart = formatTime2YMDHMS(new Date(commonData.timeRangeStart)); + } + if (commonData.timeRangeEnd) { + commonData.timeRangeEnd = formatTime2YMDHMS(new Date(commonData.timeRangeEnd)); + } + if (inputType === FlowNodeInputTypeEnum.timePointSelect && commonData.defaultValue) { + commonData.defaultValue = formatTime2YMDHMS(new Date(commonData.defaultValue)); + } else if ( + inputType === FlowNodeInputTypeEnum.timeRangeSelect && + Array.isArray(commonData.defaultValue) + ) { + commonData.defaultValue = commonData.defaultValue.map((item) => + item ? formatTime2YMDHMS(new Date(item)) : '' + ); + } + + return commonData; + }, + [inputType] + ); + return ( - + {typeLabels.name[type] || typeLabels.name.formInput} @@ -409,7 +520,7 @@ const InputTypeConfig = ({ onChange={(e) => { setValue('defaultValue', e); }} - defaultValue={defaultValue} + value={defaultValue} /> )} {(inputType === FlowNodeInputTypeEnum.switch || @@ -460,6 +571,63 @@ const InputTypeConfig = ({ } /> )} + {inputType === VariableInputEnum.timePointSelect && ( + { + setValue('defaultValue', date); + }} + popPosition="top" + timeGranularity={timeGranularity} + minDate={timeRangeStart ? new Date(timeRangeStart) : undefined} + maxDate={timeRangeEnd ? new Date(timeRangeEnd) : undefined} + /> + )} + {inputType === VariableInputEnum.timeRangeSelect && ( + + + + {t('app:time_range_start')} + + { + setValue('defaultValue', [date, timeRangeEndDefault]); + }} + popPosition="top" + timeGranularity={timeGranularity} + minDate={timeRangeStart ? new Date(timeRangeStart) : undefined} + maxDate={timeRangeEndDefault ? new Date(timeRangeEndDefault) : undefined} + /> + + + + {t('app:time_range_end')} + + { + setValue('defaultValue', [timeRangeStartDefault, date]); + }} + popPosition="top" + timeGranularity={timeGranularity} + minDate={timeRangeStartDefault ? new Date(timeRangeStartDefault) : undefined} + maxDate={timeRangeEnd ? new Date(timeRangeEnd) : undefined} + /> + + + )} + {inputType === VariableInputEnum.llmSelect && ( + + { + setValue('defaultValue', model); + }} + /> + + )} )} @@ -612,28 +780,100 @@ const InputTypeConfig = ({ )} - - {(inputType === FlowNodeInputTypeEnum.fileSelect || - inputType === VariableInputEnum.file) && ( + {/* TODO: 适配新的文件上传 */} + {inputType === VariableInputEnum.file && ( <> - {t('app:document_upload')} + {t('app:file_types')} - - - - - - {t('app:image_upload')} - - + + { + if (e.target.checked) { + setValue('canSelectFile', true); + } else { + setValue('canSelectFile', false); + } + }} + > + {t('app:document')} + + + { + if (e.target.checked) { + setValue('canSelectImg', true); + } else { + setValue('canSelectImg', false); + } + }} + > + {t('app:image')} + - - {t('app:image_upload_tip')} - + + + + {t('app:upload_method')} + + + { + if (e.target.checked) { + setValue('canLocalUpload', true); + } else { + setValue('canLocalUpload', false); + } + }} + > + {t('app:local_upload')} + + { + if (e.target.checked) { + setValue('canUrlUpload', true); + } else { + setValue('canUrlUpload', false); + } + }} + > + {t('app:url_upload')} + - + {t('app:upload_file_max_amount')} @@ -660,6 +900,69 @@ const InputTypeConfig = ({ )} + {inputType === VariableInputEnum.datasetSelect && ( + <> + + + {t('app:dataset_select')} + + + + {datasetList.length > 0 && datasetList?.[0].datasetId !== '' && ( + + {datasetList.map((item) => ( + + + {item.name} + + ))} + + )} + + + 0 + ? datasetList + .filter((item) => item.datasetId === defaultValue) + .map((item) => ({ + datasetId: item.datasetId, + name: item.name, + avatar: item.avatar, + vectorModel: {} as EmbeddingModelItemType + })) + : [] + } + onChange={(selectedDatasets) => { + const newDatasetList = selectedDatasets.map((item: any) => ({ + name: item.name, + datasetId: item.datasetId, + avatar: item.avatar + })); + setDatasetList(newDatasetList); + setValue('dataset', newDatasetList); + }} + onClose={() => setIsDatasetSelectOpen(false)} + /> + + )} + {inputType === VariableInputEnum.password && ( @@ -677,14 +980,17 @@ const InputTypeConfig = ({ )} - + - - - - ); -} - -export default UploadSystemToolModal; diff --git a/projects/app/src/pageComponents/app/plugin/SecretInputModal.tsx b/projects/app/src/pageComponents/app/tool/SecretInputModal.tsx similarity index 94% rename from projects/app/src/pageComponents/app/plugin/SecretInputModal.tsx rename to projects/app/src/pageComponents/app/tool/SecretInputModal.tsx index 348a58c7e2bb..49bd0bc432b0 100644 --- a/projects/app/src/pageComponents/app/plugin/SecretInputModal.tsx +++ b/projects/app/src/pageComponents/app/tool/SecretInputModal.tsx @@ -8,7 +8,7 @@ import { ModalFooter, useDisclosure } from '@chakra-ui/react'; -import { SystemToolInputTypeEnum } from '@fastgpt/global/core/app/systemTool/constants'; +import { SystemToolSecretInputTypeEnum } from '@fastgpt/global/core/app/tool/systemTool/constants'; import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; import LeftRadio from '@fastgpt/web/components/common/Radio/LeftRadio'; import { useTranslation } from 'next-i18next'; @@ -22,13 +22,13 @@ import IconButton from '@/pageComponents/account/team/OrgManage/IconButton'; import MyModal from '@fastgpt/web/components/common/MyModal'; import InputRender from '@/components/core/app/formRender'; import { secretInputTypeToInputType } from '@/components/core/app/formRender/utils'; -import { getSystemPlugTemplates } from '@/web/core/app/api/plugin'; +import { getAppToolTemplates } from '@/web/core/app/api/tool'; import type { NodeTemplateListItemType } from '@fastgpt/global/core/workflow/type/node'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { InputTypeEnum } from '@/components/core/app/formRender/constant'; export type ToolParamsFormType = { - type: SystemToolInputTypeEnum; + type: SystemToolSecretInputTypeEnum; value?: StoreSecretValueType; }; @@ -64,7 +64,9 @@ const SecretInputModal = ({ const defaultValue = inputConfig.value; return ( defaultValue || { - type: hasSystemSecret ? SystemToolInputTypeEnum.system : SystemToolInputTypeEnum.manual, + type: hasSystemSecret + ? SystemToolSecretInputTypeEnum.system + : SystemToolSecretInputTypeEnum.manual, value: inputList?.reduce( (acc, item) => { @@ -82,7 +84,7 @@ const SecretInputModal = ({ const { data: childTools = [] } = useRequest2( async () => { if (!isFolder) return []; - return getSystemPlugTemplates({ parentId }); + return getAppToolTemplates({ parentId }); }, { manual: false, @@ -121,9 +123,9 @@ const SecretInputModal = ({ { title: t('app:system_secret'), desc: t('app:tool_active_system_config_desc'), - value: SystemToolInputTypeEnum.system, + value: SystemToolSecretInputTypeEnum.system, children: - configType === SystemToolInputTypeEnum.system && hasCost ? ( + configType === SystemToolSecretInputTypeEnum.system && hasCost ? ( {isFolder ? ( <> @@ -205,9 +207,9 @@ const SecretInputModal = ({ t('app:manual_secret') ), desc: t('app:tool_active_manual_config_desc'), - value: SystemToolInputTypeEnum.manual, + value: SystemToolSecretInputTypeEnum.manual, children: - configType === SystemToolInputTypeEnum.manual ? ( + configType === SystemToolSecretInputTypeEnum.manual ? ( <> {inputList.map((item, i) => { const inputKey = `value.${item.key}.value` as any; diff --git a/projects/app/src/pageComponents/chat/ChatSetting/ToolSelectModal.tsx b/projects/app/src/pageComponents/chat/ChatSetting/ToolSelectModal.tsx index 2734721687e3..50b82d47eadf 100644 --- a/projects/app/src/pageComponents/chat/ChatSetting/ToolSelectModal.tsx +++ b/projects/app/src/pageComponents/chat/ChatSetting/ToolSelectModal.tsx @@ -2,35 +2,16 @@ import React, { useCallback, useMemo, useState } from 'react'; import MyModal from '@fastgpt/web/components/common/MyModal'; import { useTranslation } from 'next-i18next'; -import { - Accordion, - AccordionButton, - AccordionIcon, - AccordionItem, - AccordionPanel, - Box, - Button, - css, - Flex, - Grid -} from '@chakra-ui/react'; +import { Box, Button, Flex, Grid } from '@chakra-ui/react'; import { parseI18nString } from '@fastgpt/global/common/i18n/utils'; -import type { localeType } from '@fastgpt/global/common/i18n/type'; -import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; import { type FlowNodeTemplateType, - type NodeTemplateListItemType, - type NodeTemplateListType + type NodeTemplateListItemType } from '@fastgpt/global/core/workflow/type/node.d'; import MyIcon from '@fastgpt/web/components/common/Icon'; -import { - getPluginGroups, - getPreviewPluginNode, - getSystemPlugTemplates, - getSystemPluginPaths -} from '@/web/core/app/api/plugin'; +import { getToolPreviewNode, getAppToolTemplates, getAppToolPaths } from '@/web/core/app/api/tool'; import MyBox from '@fastgpt/web/components/common/MyBox'; import { type ParentIdType } from '@fastgpt/global/common/parentFolder/type'; import FolderPath from '@/components/common/folder/Path'; @@ -45,8 +26,10 @@ import { useToast } from '@fastgpt/web/hooks/useToast'; import { workflowStartNodeId } from '@/web/core/app/constants'; import ConfigToolModal from '@/pageComponents/app/detail/SimpleApp/components/ConfigToolModal'; import type { ChatSettingType } from '@fastgpt/global/core/chat/setting/type'; -import CostTooltip from '@/components/core/app/plugin/CostTooltip'; +import CostTooltip from '@/components/core/app/tool/CostTooltip'; import { useSystemStore } from '@/web/common/system/useSystemStore'; +import ToolTagFilterBox from '@fastgpt/web/components/core/plugin/tool/TagFilterBox'; +import { getPluginToolTags } from '@/web/core/plugin/toolTag/api'; type Props = { selectedTools: ChatSettingType['selectedTools']; @@ -66,9 +49,10 @@ const ToolSelectModal = ({ onClose, ...props }: Props & { onClose: () => void }) const { t } = useTranslation(); const [parentId, setParentId] = useState(''); const [searchKey, setSearchKey] = useState(''); + const [selectedTagIds, setSelectedTagIds] = useState([]); const { - data: templates = [], + data: rawTemplates = [], runAsync: loadTemplates, loading: isLoading } = useRequest2( @@ -79,7 +63,7 @@ const ToolSelectModal = ({ onClose, ...props }: Props & { onClose: () => void }) parentId?: ParentIdType; searchVal?: string; }) => { - return getSystemPlugTemplates({ parentId, searchKey: searchVal }); + return getAppToolTemplates({ parentId, searchKey: searchVal }); }, { onSuccess(_, [{ parentId = '' }]) { @@ -90,9 +74,22 @@ const ToolSelectModal = ({ onClose, ...props }: Props & { onClose: () => void }) } ); + const { data: allTags = [] } = useRequest2(getPluginToolTags, { + manual: false + }); + + const templates = useMemo(() => { + if (selectedTagIds.length === 0) { + return rawTemplates; + } + return rawTemplates.filter((template) => { + return template.toolTags?.some((toolTag) => selectedTagIds.includes(toolTag)); + }); + }, [rawTemplates, selectedTagIds]); + const { data: paths = [] } = useRequest2( () => { - return getSystemPluginPaths({ sourceId: parentId, type: 'current' }); + return getAppToolPaths({ sourceId: parentId, type: 'current' }); }, { manual: false, @@ -135,6 +132,17 @@ const ToolSelectModal = ({ onClose, ...props }: Props & { onClose: () => void }) /> + {/* Tag filter */} + {allTags.length > 0 && ( + + + + )} {/* route components */} {!searchKey && parentId && ( @@ -143,7 +151,12 @@ const ToolSelectModal = ({ onClose, ...props }: Props & { onClose: () => void }) )} - + @@ -158,10 +171,12 @@ const RenderList = React.memo(function RenderList({ onRemoveTool, setParentId, selectedTools, - chatConfig = {} + chatConfig = {}, + allTags }: Props & { templates: NodeTemplateListItemType[]; setParentId: (parentId: ParentIdType) => any; + allTags: Array<{ tagId: string; tagName: any }>; }) { const { t, i18n } = useTranslation(); const { feConfigs } = useSystemStore(); @@ -172,7 +187,7 @@ const RenderList = React.memo(function RenderList({ const { runAsync: onClickAdd, loading: isLoading } = useRequest2( async (template: NodeTemplateListItemType) => { - const res = await getPreviewPluginNode({ appId: template.id }); + const res = await getToolPreviewNode({ appId: template.id }); /* Invalid plugin check 1. Reference type. but not tool description; @@ -259,239 +274,148 @@ const RenderList = React.memo(function RenderList({ } ); - const { data: pluginGroups = [] } = useRequest2(getPluginGroups, { - manual: false - }); - - const formatTemplatesArray = useMemo(() => { - return pluginGroups.map((group) => { - const map = group.groupTypes.reduce< - Record< - string, - { - list: NodeTemplateListItemType[]; - label: string; - } - > - >((acc, item) => { - acc[item.typeId] = { - list: [], - label: t(parseI18nString(item.typeName, i18n.language)) - }; - return acc; - }, {}); - - templates.forEach((item) => { - if (map[item.templateType]) { - map[item.templateType].list.push({ - ...item, - name: t(parseI18nString(item.name, i18n.language)), - intro: t(parseI18nString(item.intro, i18n.language)) - }); - } - }); - return { - label: group.groupName, - list: Object.entries(map) - .map(([type, { list, label }]) => ({ - type, - label, - list - })) - .filter((item) => item.list.length > 0) - }; - }); - }, [i18n.language, pluginGroups, t, templates]); - const gridStyle = { gridTemplateColumns: ['1fr', '1fr 1fr'], py: 3, avatarSize: '1.75rem' }; - const PluginListRender = useMemoizedFn(({ list = [] }: { list: NodeTemplateListType }) => { + const PluginListRender = useMemoizedFn(() => { return ( <> - {list.map((item, i) => { - return ( - - - - {t(item.label as any)} - - - - {item.list.map((template) => { - const selected = selectedTools.some((tool) => tool.pluginId === template.id); + {templates.length > 0 ? ( + + {templates.map((template) => { + const selected = selectedTools.some((tool) => tool.pluginId === template.id); - return ( - - - - - {template.name} - - - By {template.author || feConfigs?.systemTitle} - - - - {template.intro || t('common:core.workflow.Not intro')} - - - - } - > - + return ( + + - - {t(template.name as any)} + + {template.name} + + + By {template.author || feConfigs?.systemTitle} + + + {template.intro || t('common:core.workflow.Not intro')} + + +
+ } + > + + + + + {t(parseI18nString(template.name, i18n.language))} + + - {selected ? ( - - ) : template.flowNodeType === 'toolSet' ? ( - - - - - ) : template.isFolder ? ( - - ) : ( - - )} + {selected ? ( + + ) : template.flowNodeType === 'toolSet' ? ( + + + - - ); - })} - -
- ); - })} + ) : template.isFolder ? ( + + ) : ( + + )} + + + ); + })} + + ) : ( + + )} ); }); - return templates.length === 0 ? ( - - ) : ( + return ( <> - - {formatTemplatesArray.length > 1 ? ( - <> - {formatTemplatesArray.map(({ list, label }, index) => ( - - - {t(label as any)} - - - - - - - ))} - - ) : ( - - )} - + {!!configTool && ( { () => llmModelList.map((model) => ({ value: model.model, label: model.name })), [llmModelList] ); - const [selectedModel, setSelectedModel] = useLocalStorageState('chat_home_model', { + const [selectedModel, setSelectedModel] = useLocalStorageState('chat_home_model', { defaultValue: defaultModels.llm?.model }); @@ -237,7 +240,7 @@ const HomeChatWindow = ({ myApps }: Props) => { const tools: FlowNodeTemplateType[] = await Promise.all( selectedToolIds.map(async (toolId) => { - const node = await getPreviewPluginNode({ appId: toolId }); + const node = await getToolPreviewNode({ appId: toolId }); node.inputs = node.inputs.map((input) => { const tool = availableTools.find((tool) => tool.pluginId === toolId); const value = tool?.inputs?.[input.key]; @@ -288,6 +291,7 @@ const HomeChatWindow = ({ myApps }: Props) => { {availableModels.length > 0 && ( { const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); const pane = useContextSelector(ChatSettingContext, (v) => v.pane); + const isAdmin = !!userInfo?.team.permission.hasManagePer; const isSettingPane = pane === ChatSidebarPaneEnum.SETTING; return ( @@ -26,12 +27,12 @@ const ChatSliderFooter = () => { - {userInfo?.username} + {userInfo?.team?.memberName} - {feConfigs.isPlus && ( + {feConfigs.isPlus && isAdmin && ( { const pane = useContextSelector(ChatSettingContext, (v) => v.pane); const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); + const enableHome = useContextSelector(ChatSettingContext, (v) => v.chatSettings?.enableHome); const appName = useContextSelector(ChatItemContext, (v) => v.chatBoxData?.app.name); const appAvatar = useContextSelector(ChatItemContext, (v) => v.chatBoxData?.app.avatar); @@ -61,33 +62,35 @@ const ChatSliderHeader = ({ title, banner }: Props) => { - { - handlePaneChange(ChatSidebarPaneEnum.HOME); - onCloseSlider(); - setChatId(); - }} - > - { + handlePaneChange(ChatSidebarPaneEnum.HOME); + onCloseSlider(); + setChatId(); }} > - - - {t('chat:sidebar.home')} - - - + + + + {t('chat:sidebar.home')} + + + + )} { diff --git a/projects/app/src/pageComponents/config/ImportPluginModal.tsx b/projects/app/src/pageComponents/config/ImportPluginModal.tsx new file mode 100644 index 000000000000..092b24766be0 --- /dev/null +++ b/projects/app/src/pageComponents/config/ImportPluginModal.tsx @@ -0,0 +1,364 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { Box, Button, Flex, HStack, VStack } from '@chakra-ui/react'; +import MyRightDrawer from '@fastgpt/web/components/common/MyDrawer/MyRightDrawer'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import { useTranslation } from 'react-i18next'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import FileSelectorBox, { type SelectFileItemType } from '@/components/Select/FileSelectorBox'; +import { postS3UploadFile } from '@/web/common/file/api'; +import { + getPkgPluginUploadURL, + parseUploadedPkgPlugin, + confirmPkgPluginUpload +} from '@/web/core/plugin/admin/api'; +import { parseI18nString } from '@fastgpt/global/common/i18n/utils'; +import Avatar from '@fastgpt/web/components/common/Avatar'; +import { getDocPath } from '@/web/common/system/doc'; +import { getMarketPlaceToolTags } from '@/web/core/plugin/marketplace/api'; +import { useToast } from '@fastgpt/web/hooks/useToast'; + +type UploadedPluginFile = SelectFileItemType & { + status: 'uploading' | 'parsing' | 'success' | 'error'; + errorMsg?: string; + toolId?: string; + toolName?: string; + toolIntro?: string; + toolTags?: string[]; +}; + +const ImportPluginModal = ({ + onClose, + onSuccess +}: { + onClose: () => void; + onSuccess?: () => void; +}) => { + const { t, i18n } = useTranslation(); + const { toast } = useToast(); + + const [selectFiles, setSelectFiles] = useState([]); + const [uploadedFiles, setUploadedFiles] = useState([]); + + const { data: allTags = [] } = useRequest2(getMarketPlaceToolTags, { + manual: false + }); + + const uploadAndParseFile = async (file: UploadedPluginFile) => { + try { + setUploadedFiles((prev) => + prev.map((f) => + f.name === file.name ? { ...f, status: 'uploading', errorMsg: undefined } : f + ) + ); + + const presignedData = await getPkgPluginUploadURL({ filename: file.name }); + + const formData = new FormData(); + Object.entries(presignedData.formData).forEach(([key, value]) => { + formData.append(key, value); + }); + formData.append('file', file.file); + + await postS3UploadFile(presignedData.postURL, formData); + + setUploadedFiles((prev) => + prev.map((f) => (f.name === file.name ? { ...f, status: 'parsing' } : f)) + ); + + const parseResult = await parseUploadedPkgPlugin({ objectName: presignedData.objectName }); + + const parentId = parseResult.find((item) => !item.parentId)?.toolId; + if (!parentId) { + return Promise.reject(new Error(`未找到插件 ID`)); + } + const toolDetail = parseResult.find((item) => item.toolId === parentId); + + setUploadedFiles((prev) => + prev.map((f) => + f.name === file.name + ? { + ...f, + status: 'success', + toolId: parentId, + toolName: parseI18nString(toolDetail?.name || '', i18n.language), + icon: toolDetail?.icon || '', + toolIntro: parseI18nString(toolDetail?.description || '', i18n.language) || '', + toolTags: + toolDetail?.tags?.map((tag) => { + const currentTag = allTags.find((item) => item.tagId === tag); + return parseI18nString(currentTag?.tagName || '', i18n.language) || ''; + }) || [] + } + : f + ) + ); + } catch (error: any) { + setUploadedFiles((prev) => + prev.map((f) => + f.name === file.name ? { ...f, status: 'error', errorMsg: error.message } : f + ) + ); + } + }; + + const { runAsync: handleBatchUpload, loading: uploadLoading } = useRequest2( + async (files: SelectFileItemType[]) => { + const newUploadedFiles: UploadedPluginFile[] = files.map((f) => ({ + ...f, + status: 'uploading' as const + })); + setUploadedFiles((prev) => [...prev, ...newUploadedFiles]); + + for (const file of newUploadedFiles) { + await uploadAndParseFile(file); + } + }, + { + manual: true + } + ); + + const onSelectFiles = useCallback( + (files: SelectFileItemType[]) => { + const filteredFiles = files + .filter((file, index, self) => self.findIndex((f) => f.name === file.name) === index) + .filter((file) => !uploadedFiles.some((f) => f.name === file.name)); + + if (filteredFiles.length !== files.length) { + toast({ + title: t('app:upload_file_exists_filtered'), + status: 'info' + }); + } + setSelectFiles(filteredFiles); + + if (filteredFiles.length > 0) { + handleBatchUpload(filteredFiles); + } + }, + [handleBatchUpload, t, toast, uploadedFiles] + ); + + const handleRetry = async (file: UploadedPluginFile) => { + try { + await uploadAndParseFile(file); + } catch (error) { + console.error(`重试上传文件 ${file.name} 失败:`, error); + } + }; + + const handleDelete = (file: UploadedPluginFile) => { + setUploadedFiles((prev) => prev.filter((f) => f.name !== file.name)); + setSelectFiles((prev) => prev.filter((f) => f.name !== file.name)); + }; + + const { runAsync: handleConfirmImport, loading: confirmLoading } = useRequest2( + async () => { + const successToolIds = uploadedFiles + .filter((file) => file.status === 'success' && file.toolId) + .map((file) => file.toolId!); + + await confirmPkgPluginUpload({ toolIds: successToolIds }); + }, + { + manual: true, + onSuccess: () => { + setUploadedFiles([]); + onSuccess?.(); + onClose(); + } + } + ); + + return ( + + + + + + + + + + + {t('common:name')} + + + {t('app:toolkit_tags')} + + + {t('common:Intro')} + + + {t('common:Status')} + + + {t('common:Action')} + + + + {uploadedFiles.length > 0 && ( + + {uploadedFiles.map((item, index) => ( + + + + + {item.status === 'success' && item.toolName ? item.toolName : item.name} + + + + {item.status === 'success' && item.toolTags && item.toolTags.length > 0 ? ( + item.toolTags.map((tag, tagIndex) => ( + + {tag} + + )) + ) : ( + - + )} + + + {item.status === 'success' && item.toolIntro ? item.toolIntro : '-'} + + + {(item.status === 'uploading' || item.status === 'parsing') && ( + + {t('app:custom_plugin_uploading')} + + )} + {item.status === 'success' && ( + + {t('app:custom_plugin_uploaded')} + + )} + {item.status === 'error' && ( + + {t('app:custom_plugin_upload_failed')} + + )} + + + handleRetry(item)} + cursor={'pointer'} + _hover={{ + bg: 'myGray.100', + rounded: 'md', + color: 'primary.600' + }} + > + + + handleDelete(item)} + cursor={'pointer'} + _hover={{ + bg: 'myGray.100', + rounded: 'md', + color: 'red.600' + }} + > + + + + + ))} + + )} + + + + + + + + ); +}; + +export default ImportPluginModal; diff --git a/projects/app/src/pageComponents/config/TagManageModal.tsx b/projects/app/src/pageComponents/config/TagManageModal.tsx new file mode 100644 index 000000000000..a01f520d4df4 --- /dev/null +++ b/projects/app/src/pageComponents/config/TagManageModal.tsx @@ -0,0 +1,315 @@ +import { Box, Button, Flex, Input, ModalBody, ModalFooter } from '@chakra-ui/react'; +import type { SystemPluginToolTagType } from '@fastgpt/global/core/plugin/type'; +import MyModal from '@fastgpt/web/components/common/MyModal'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { useEffect, useRef, useState } from 'react'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import PopoverConfirm from '@fastgpt/web/components/common/MyPopover/PopoverConfirm'; +import DndDrag, { Draggable } from '@fastgpt/web/components/common/DndDrag/index'; +import { useTranslation } from 'next-i18next'; +import { parseI18nString } from '@fastgpt/global/common/i18n/utils'; +import { nanoid } from 'nanoid'; +import { + createPluginToolTag, + deletePluginToolTag, + updatePluginToolTag, + updatePluginToolTagOrder +} from '@/web/core/plugin/admin/tool/api'; +import { getPluginToolTags } from '@/web/core/plugin/toolTag/api'; +import { useToast } from '@fastgpt/web/hooks/useToast'; + +const TagManageModal = ({ onClose }: { onClose: () => void }) => { + const { t, i18n } = useTranslation(); + const { toast } = useToast(); + const newTagInputRef = useRef(null); + + const [localTags, setLocalTags] = useState([]); + const [editingTagId, setEditingTagId] = useState(null); + const [inputValue, setInputValue] = useState(''); + + const checkTagNameDuplicate = (tagName: string, excludeTagId?: string): boolean => { + return localTags.some((tag) => { + if (excludeTagId && tag.tagId === excludeTagId) { + return false; + } + const existingName = parseI18nString(tag.tagName, i18n.language); + return existingName === tagName; + }); + }; + + const { + data: tags = [], + run: loadTags, + loading + } = useRequest2(getPluginToolTags, { + manual: false + }); + useEffect(() => { + setLocalTags(tags); + }, [tags]); + + useEffect(() => { + if (editingTagId && newTagInputRef.current) { + newTagInputRef.current?.focus(); + } + }, [editingTagId]); + + const { runAsync: handleAddTag } = useRequest2( + async (tagName: string) => { + await createPluginToolTag({ tagName }); + }, + { + onSuccess: () => { + setEditingTagId(null); + setInputValue(''); + loadTags(); + } + } + ); + + const { runAsync: handleUpdateTag } = useRequest2( + async (tagId: string, tagName: string) => { + await updatePluginToolTag({ tagId, tagName }); + }, + { + onSuccess: () => { + setEditingTagId(null); + setInputValue(''); + loadTags(); + } + } + ); + + const { runAsync: handleDeleteTag } = useRequest2( + async (tag: SystemPluginToolTagType) => { + await deletePluginToolTag({ tagId: tag.tagId }); + }, + { + onSuccess: () => { + loadTags(); + } + } + ); + + const { runAsync: handleUpdateOrder } = useRequest2( + async (newList: SystemPluginToolTagType[]) => { + await updatePluginToolTagOrder({ tags: newList }); + }, + { + onSuccess: () => { + loadTags(); + } + } + ); + + return ( + + + + + + {t('app:toolkit_tags_total', { count: localTags.length })} + + + + + + {editingTagId && !localTags.find((tag) => tag.tagId === editingTagId) && ( + + setInputValue(e.target.value)} + onBlur={() => { + const trimmedValue = inputValue.trim(); + if (trimmedValue) { + if (checkTagNameDuplicate(trimmedValue)) { + toast({ + title: t('app:toolkit_tags_duplicate_name'), + status: 'warning' + }); + setEditingTagId(null); + setInputValue(''); + } else { + handleAddTag(trimmedValue); + } + } else { + setEditingTagId(null); + setInputValue(''); + } + }} + /> + + )} + + + onDragEndCb={async (tags: SystemPluginToolTagType[]) => { + const newList = tags.map((item, index) => ({ + ...item, + tagOrder: index + })); + setLocalTags(newList); + await handleUpdateOrder(newList); + }} + dataList={localTags} + > + {({ provided }) => ( + + {localTags.map((tag, index) => { + const isEditing = editingTagId === tag.tagId; + const displayName = parseI18nString(tag.tagName, i18n.language); + + return ( + + {(provided, snapshot) => ( + + {isEditing ? ( + setInputValue(e.target.value)} + onBlur={() => { + const trimmedValue = inputValue.trim(); + if (editingTagId && trimmedValue) { + if (checkTagNameDuplicate(trimmedValue, editingTagId)) { + toast({ + title: t('app:toolkit_tags_duplicate_name'), + status: 'warning' + }); + setEditingTagId(null); + setInputValue(''); + } else { + handleUpdateTag(editingTagId, trimmedValue); + } + } else { + setEditingTagId(null); + setInputValue(''); + } + }} + /> + ) : ( + + + + + + {t(displayName)} + + + + {!tag.isSystem && ( + <> + { + setEditingTagId(tag.tagId); + setInputValue(displayName); + }} + > + + + + + + } + onConfirm={() => handleDeleteTag(tag)} + /> + + )} + + )} + + )} + + ); + })} + + )} + + + + + + + + ); +}; + +export default TagManageModal; diff --git a/projects/app/src/pageComponents/config/tool/SystemToolConfigModal.tsx b/projects/app/src/pageComponents/config/tool/SystemToolConfigModal.tsx new file mode 100644 index 000000000000..b1deb39e66c8 --- /dev/null +++ b/projects/app/src/pageComponents/config/tool/SystemToolConfigModal.tsx @@ -0,0 +1,384 @@ +import React from 'react'; +import { + Box, + Button, + HStack, + Input, + ModalBody, + ModalFooter, + Switch, + Flex, + Text, + Table, + Thead, + Tr, + Th, + Tbody, + Td, + TableContainer +} from '@chakra-ui/react'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import MyModal from '@fastgpt/web/components/common/MyModal'; +import { useForm } from 'react-hook-form'; +import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; +import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput'; +import PopoverConfirm from '@fastgpt/web/components/common/MyPopover/PopoverConfirm'; +import { deletePkgPlugin } from '@/web/core/plugin/admin/api'; +import { getAdminSystemToolDetail, putAdminUpdateTool } from '@/web/core/plugin/admin/tool/api'; +import type { AdminSystemToolDetailType } from '@fastgpt/global/core/plugin/admin/tool/type'; +import { parseI18nString } from '@fastgpt/global/common/i18n/utils'; +import type { InputConfigType } from '@fastgpt/global/core/workflow/type/io'; +import MyDivider from '@fastgpt/web/components/common/MyDivider'; +import { PluginStatusEnum } from '@fastgpt/global/core/plugin/type'; +import MySelect from '@fastgpt/web/components/common/MySelect'; +import { useTranslation } from 'next-i18next'; + +const COST_LIMITS = { max: 1000, min: 0, step: 0.1 }; + +const SystemToolConfigModal = ({ + toolId, + onSuccess, + onClose +}: { + toolId: string; + onSuccess: () => void; + onClose: () => void; +}) => { + const { t } = useTranslation(); + const { register, reset, handleSubmit, setValue, watch, control } = + useForm(); + + const { data: tool, loading } = useRequest2(() => getAdminSystemToolDetail({ toolId }), { + onSuccess(res) { + reset(res); + }, + manual: false + }); + + const [inputList, status, defaultInstalled, inputListVal, childTools] = watch([ + 'inputList', + 'status', + 'defaultInstalled', + 'inputListVal', + 'childTools' + ]); + + // 是否显示系统密钥配置 + const showSystemSecretInput = !!inputList && inputList.length > 0; + + const { runAsync: onSubmit, loading: submitting } = useRequest2( + (formData: AdminSystemToolDetailType) => + putAdminUpdateTool({ + ...formData, + pluginId: toolId, + childTools: formData.childTools?.map((tool) => { + return { + pluginId: tool.pluginId, + systemKeyCost: tool.systemKeyCost + }; + }) + }), + { + successToast: t('common:Config') + t('common:Success'), + onSuccess() { + onSuccess(); + onClose(); + } + } + ); + + const { runAsync: onDelete, loading: deleteLoading } = useRequest2( + () => deletePkgPlugin({ toolId: toolId.split('-')[1] }), + { + onSuccess() { + onSuccess(); + onClose(); + } + } + ); + + // Secret input render + const renderInputField = (item: InputConfigType) => { + const labelSection = ( + + + {item.required && ( + + * + + )} + {item.label} + + {item.description && } + + ); + + if (item.inputType === 'switch') { + return ( + + {labelSection} + + + + + ); + } + + return ( + + {labelSection} + + + + + ); + }; + + const systemConfigSection = showSystemSecretInput && !!inputListVal && ( + <> + + + {!tool?.isFolder && ( + + + {t('app:toolkit_system_key_cost')} + + + + )} + {tool?.inputList?.map(renderInputField)} + + ); + + return ( + + + {tool?.isFolder ? ( + + + + {t('app:toolkit_basic_config')} + + + + + {t('app:toolkit_plugin_status')} + + + width={'120px'} + value={status} + list={[ + { label: t('app:toolkit_status_normal'), value: PluginStatusEnum.Normal }, + { + label: t('app:toolkit_status_soon_offline'), + value: PluginStatusEnum.SoonOffline + }, + { label: t('app:toolkit_status_offline'), value: PluginStatusEnum.Offline } + ]} + onChange={(e) => { + setValue('status', e); + if (e !== PluginStatusEnum.Normal) { + setValue('defaultInstalled', false); + } + }} + /> + + + + + {t('app:toolkit_default_install')} + + { + const newDefaultInstalled = e.target.checked; + setValue('defaultInstalled', newDefaultInstalled); + if (newDefaultInstalled && status !== PluginStatusEnum.Normal) { + setValue('status', PluginStatusEnum.Normal); + } + }} + /> + + + {showSystemSecretInput && ( + <> + + + {t('app:toolkit_config_system_key')} + + { + const val = e.target.checked; + if (val) { + setValue('inputListVal', {}); + } else { + setValue('inputListVal', null); + } + }} + /> + + {systemConfigSection} + + )} + + + + + {t('app:toolkit_tool_list')} + + + + + + + {/* */} + + + + + {childTools?.map((tool, index) => { + return ( + + + + + ); + })} + +
+ {t('app:toolkit_tool_name')} + + {t('common:Status')} + + {t('app:toolkit_key_price')} +
+ + {parseI18nString(tool.name)} + + + +
+
+
+
+ ) : ( + + + + {t('app:toolkit_plugin_status')} + + + width={'120px'} + value={status} + list={[ + { label: t('app:toolkit_status_normal'), value: PluginStatusEnum.Normal }, + { + label: t('app:toolkit_status_soon_offline'), + value: PluginStatusEnum.SoonOffline + }, + { label: t('app:toolkit_status_offline'), value: PluginStatusEnum.Offline } + ]} + onChange={(e) => { + setValue('status', e); + if (e !== PluginStatusEnum.Normal) { + setValue('defaultInstalled', false); + } + }} + /> + + + + + {t('app:toolkit_default_install')} + + { + const newDefaultInstalled = e.target.checked; + setValue('defaultInstalled', newDefaultInstalled); + if (newDefaultInstalled && status !== PluginStatusEnum.Normal) { + setValue('status', PluginStatusEnum.Normal); + } + }} + /> + + + {showSystemSecretInput && ( + <> + + + {t('app:toolkit_config_system_key')} + + { + const val = e.target.checked; + if (val) { + // @ts-ignore + setValue('inputListVal', {}); + } else { + setValue('inputListVal', undefined); + } + }} + /> + + {systemConfigSection} + + )} + + )} +
+ + + {t('common:Delete')} + + } + /> + + + + + + +
+ ); +}; + +export default SystemToolConfigModal; diff --git a/projects/app/src/pageComponents/config/tool/ToolRow.tsx b/projects/app/src/pageComponents/config/tool/ToolRow.tsx new file mode 100644 index 000000000000..1cfd1ca21d2a --- /dev/null +++ b/projects/app/src/pageComponents/config/tool/ToolRow.tsx @@ -0,0 +1,214 @@ +import { Box, Flex, Switch, Checkbox } from '@chakra-ui/react'; +import Avatar from '@fastgpt/web/components/common/Avatar'; +import type { + DraggableProvided, + DraggableStateSnapshot +} from '@fastgpt/web/components/common/DndDrag'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import MyBox from '@fastgpt/web/components/common/MyBox'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { useTranslation } from 'next-i18next'; +import { putAdminUpdateTool } from '@/web/core/plugin/admin/tool/api'; +import React, { useRef, useState, useEffect } from 'react'; +import { PluginStatusEnum } from '@fastgpt/global/core/plugin/type'; +import type { AdminSystemToolListItemType } from '@fastgpt/global/core/plugin/admin/tool/type'; +import type { GetAdminSystemToolsResponseType } from '@fastgpt/global/openapi/core/plugin/admin/tool/api'; + +const ToolRow = ({ + tool, + setEditingToolId, + setLocalTools, + provided, + snapshot +}: { + tool: AdminSystemToolListItemType; + setEditingToolId: (toolId: string) => void; + setLocalTools: React.Dispatch>; + provided: DraggableProvided; + snapshot: DraggableStateSnapshot; +}) => { + const { t, i18n } = useTranslation(); + + const { runAsync: updateSystemTool, loading } = useRequest2( + async (updateFields: { + defaultInstalled?: boolean; + hasTokenFee?: boolean; + status?: PluginStatusEnum; + }) => { + return putAdminUpdateTool({ + ...tool, + pluginId: tool.id, + defaultInstalled: updateFields.defaultInstalled, + hasTokenFee: updateFields.hasTokenFee, + status: updateFields.status + }); + }, + { + onSuccess: (_, updateFields) => { + setLocalTools((prev) => + prev.map((item) => (item.id === tool.id ? { ...item, ...updateFields[0] } : item)) + ); + }, + errorToast: t('app:toolkit_update_failed') + } + ); + + return ( + { + setEditingToolId(tool.id); + }} + > + + { + e.stopPropagation(); + }} + _hover={{ bg: 'myGray.05' }} + {...provided.dragHandleProps} + > + + + + + {tool?.name} + + {/* {tool?.isOfficial && ( + + {t('app:toolkit_official')} + + )} */} + + + {tool.tags && tool.tags.length > 0 ? ( + + {tool.tags.map((tag, index) => ( + + {tag} + + ))} + + ) : ( + + - + + )} + + + {tool?.intro || '-'} + + + + {tool.status === PluginStatusEnum.Offline + ? t('app:toolkit_status_offline') + : tool.status === PluginStatusEnum.SoonOffline + ? t('app:toolkit_status_soon_offline') + : t('app:toolkit_status_normal')} + + + + { + e.stopPropagation(); + e.preventDefault(); + const newDefaultInstalled = !tool?.defaultInstalled; + const updateFields: { + defaultInstalled: boolean; + status?: number; + } = { + defaultInstalled: newDefaultInstalled + }; + if (newDefaultInstalled && tool.status !== PluginStatusEnum.Normal) { + updateFields.status = PluginStatusEnum.Normal; + } + updateSystemTool(updateFields); + }} + > + + + + + {tool?.associatedPluginId ? ( + { + e.stopPropagation(); + e.preventDefault(); + updateSystemTool({ + hasTokenFee: !tool?.hasTokenFee + }); + }} + pl={2} + > + + + ) : ( + - + )} + + + {!!tool?.hasSecretInput ? ( + + {tool?.hasSystemSecret + ? t('app:toolkit_system_key_configured') + : t('app:toolkit_system_key_not_configured')} + + ) : ( + - + )} + + + ); +}; + +export default React.memo(ToolRow); diff --git a/projects/app/src/pageComponents/config/tool/WorkflowToolConfigModal.tsx b/projects/app/src/pageComponents/config/tool/WorkflowToolConfigModal.tsx new file mode 100644 index 000000000000..13e8882531e5 --- /dev/null +++ b/projects/app/src/pageComponents/config/tool/WorkflowToolConfigModal.tsx @@ -0,0 +1,489 @@ +import React, { useMemo, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { + Box, + Button, + Flex, + HStack, + Input, + ModalBody, + ModalFooter, + Switch, + Textarea, + useDisclosure +} from '@chakra-ui/react'; +import MyModal from '@fastgpt/web/components/common/MyModal'; +import Avatar from '@fastgpt/web/components/common/Avatar'; +import { useUploadAvatar } from '@fastgpt/web/common/file/hooks/useUploadAvatar'; +import { getUploadAvatarPresignedUrl } from '@/web/common/file/api'; +import { useToast } from '@fastgpt/web/hooks/useToast'; +import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; +import { getPluginToolTags } from '@/web/core/plugin/toolTag/api'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import PopoverConfirm from '@fastgpt/web/components/common/MyPopover/PopoverConfirm'; +import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput'; +import { PluginStatusEnum } from '@fastgpt/global/core/plugin/type'; +import MySelect from '@fastgpt/web/components/common/MySelect'; +import MultipleSelect, { + useMultipleSelect +} from '@fastgpt/web/components/common/MySelect/MultipleSelect'; +import { useTranslation } from 'next-i18next'; +import type { UpdateToolBodyType } from '@fastgpt/global/openapi/core/plugin/admin/tool/api'; +import { + delAdminSystemTool, + getAdminAllSystemAppTool, + getAdminSystemToolDetail, + postAdminCreateAppTypeTool, + putAdminUpdateTool +} from '@/web/core/plugin/admin/tool/api'; +import { parseI18nString } from '@fastgpt/global/common/i18n/utils'; + +export const defaultForm: UpdateToolBodyType = { + pluginId: '', + defaultInstalled: false, + name: '', + avatar: 'core/app/type/pluginFill', + intro: '', + status: PluginStatusEnum.Normal, + hasTokenFee: false, + originCost: 0, + currentCost: 0, + userGuide: '', + author: '', + associatedPluginId: '' +}; + +const WorkflowToolConfigModal = ({ + toolId, + onSuccess, + onClose +}: { + toolId: string; + onSuccess: () => void; + onClose: () => void; +}) => { + const { t, i18n } = useTranslation(); + const { toast } = useToast(); + + const { value: selectedTags, setValue: setSelectedTags } = useMultipleSelect([], false); + + const { register, reset, setValue, watch, handleSubmit } = useForm({ + defaultValues: defaultForm + }); + const name = watch('name'); + const avatar = watch('avatar'); + const associatedPluginId = watch('associatedPluginId'); + const currentCost = watch('currentCost'); + const status = watch('status'); + const defaultInstalled = watch('defaultInstalled'); + + React.useEffect(() => { + setValue('tagIds', selectedTags); + }, [selectedTags, setValue]); + + useRequest2( + async () => { + if (toolId) { + const res = await getAdminSystemToolDetail({ toolId }); + const form: UpdateToolBodyType = { + pluginId: res.id, + status: res.status, + defaultInstalled: res.defaultInstalled, + originCost: res.originCost, + currentCost: res.currentCost, + systemKeyCost: res.systemKeyCost, + hasTokenFee: res.hasTokenFee, + inputListVal: res.inputListVal, + name: res.name, + avatar: res.avatar, + intro: res.intro, + tagIds: res.tags || [], + associatedPluginId: res.associatedPluginId, + userGuide: res.userGuide || '', + author: res.author + }; + setSelectedTags(res.tags || []); + return form; + } + return defaultForm; + }, + { + onSuccess(res) { + reset(res); + }, + manual: false + } + ); + + const isEdit = !!toolId; + + const [searchKey, setSearchKey] = useState(''); + const [lastPluginId, setLastPluginId] = useState(''); + + const { data: apps = [], loading: loadingPlugins } = useRequest2( + () => getAdminAllSystemAppTool({ searchKey }), + { + manual: false, + refreshDeps: [searchKey] + } + ); + + const { data: tags = [], loading: loadingTags } = useRequest2(getPluginToolTags, { + manual: false + }); + const pluginTypeSelectList = useMemo( + () => + tags?.map((tag) => ({ + label: parseI18nString(tag.tagName, i18n.language), + value: tag.tagId + })) || [], + [i18n.language, tags] + ); + + const currentApp = useMemo(() => { + return apps.find((item) => item._id === associatedPluginId); + }, [apps, associatedPluginId]); + + const { + isOpen: isOpenAppListMenu, + onClose: onCloseAppListMenu, + onOpen: onOpenAppListMenu + } = useDisclosure(); + + const { + Component: AvatarUploader, + handleFileSelectorOpen: handleAvatarSelectorOpen, + uploading: isUploadingAvatar + } = useUploadAvatar(getUploadAvatarPresignedUrl, { + onSuccess(avatarUrl) { + setValue('avatar', avatarUrl); + } + }); + + const { runAsync: onSubmit, loading: isSubmitting } = useRequest2( + (data: UpdateToolBodyType) => { + if (!data.associatedPluginId) { + return Promise.reject(t('app:custom_plugin_associated_plugin_required')); + } + + const formatData: UpdateToolBodyType = { + ...data, + pluginId: toolId + }; + + if (formatData.pluginId) { + return putAdminUpdateTool(formatData); + } + + return postAdminCreateAppTypeTool(formatData); + }, + { + manual: true, + successToast: t('app:custom_plugin_config_success'), + onSuccess: () => { + onSuccess(); + onClose(); + }, + onError() {}, + refreshDeps: [toolId] + } + ); + + const { runAsync: onDelete, loading: isDeleting } = useRequest2(delAdminSystemTool, { + onSuccess() { + toast({ + title: t('app:custom_plugin_delete_success'), + status: 'success' + }); + onSuccess(); + onClose(); + } + }); + + return ( + + + + + + {t('app:custom_plugin_name_label')} + + + + + + + + + + {t('app:custom_plugin_intro_label')} + +