diff --git a/.github/workflows/build-sandbox-image.yml b/.github/workflows/build-sandbox-image.yml index f84c3785d..a1a745a1f 100644 --- a/.github/workflows/build-sandbox-image.yml +++ b/.github/workflows/build-sandbox-image.yml @@ -1,4 +1,4 @@ -name: Build fastgpt-sandbox images and copy image to docker hub +name: Build fastgpt-sandbox images on: workflow_dispatch: push: diff --git a/.github/workflows/fastgpt-image.yml b/.github/workflows/fastgpt-image.yml index 2752e86cd..b8c200887 100644 --- a/.github/workflows/fastgpt-image.yml +++ b/.github/workflows/fastgpt-image.yml @@ -1,4 +1,4 @@ -name: Build FastGPT images and copy image to docker hub +name: Build FastGPT images on: workflow_dispatch: push: @@ -90,3 +90,86 @@ jobs: -t ${Docker_Hub_Tag} \ -t ${Docker_Hub_Latest} \ . + build-fastgpt-images-sub-route: + runs-on: ubuntu-20.04 + steps: + # install env + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 1 + - name: Install Dependencies + run: | + sudo apt update && sudo apt install -y nodejs npm + - name: Set up QEMU (optional) + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + with: + driver-opts: network=host + - name: Cache Docker layers + uses: actions/cache@v2 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + + # login docker + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GH_PAT }} + - name: Login to Ali Hub + uses: docker/login-action@v2 + with: + registry: registry.cn-hangzhou.aliyuncs.com + username: ${{ secrets.ALI_HUB_USERNAME }} + password: ${{ secrets.ALI_HUB_PASSWORD }} + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_HUB_NAME }} + password: ${{ secrets.DOCKER_HUB_PASSWORD }} + + # Set tag + - name: Set image name and tag + run: | + if [[ "${{ github.ref_name }}" == "main" ]]; then + echo "Git_Tag=ghcr.io/${{ github.repository_owner }}/fastgpt-sub-route:latest" >> $GITHUB_ENV + echo "Git_Latest=ghcr.io/${{ github.repository_owner }}/fastgpt-sub-route:latest" >> $GITHUB_ENV + echo "Ali_Tag=${{ secrets.ALI_IMAGE_NAME }}/fastgpt-sub-route:latest" >> $GITHUB_ENV + echo "Ali_Latest=${{ secrets.ALI_IMAGE_NAME }}/fastgpt-sub-route:latest" >> $GITHUB_ENV + echo "Docker_Hub_Tag=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-sub-route:latest" >> $GITHUB_ENV + echo "Docker_Hub_Latest=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-sub-route:latest" >> $GITHUB_ENV + else + echo "Git_Tag=ghcr.io/${{ github.repository_owner }}/fastgpt-sub-route:${{ github.ref_name }}" >> $GITHUB_ENV + echo "Git_Latest=ghcr.io/${{ github.repository_owner }}/fastgpt-sub-route:latest" >> $GITHUB_ENV + echo "Ali_Tag=${{ secrets.ALI_IMAGE_NAME }}/fastgpt-sub-route:${{ github.ref_name }}" >> $GITHUB_ENV + echo "Ali_Latest=${{ secrets.ALI_IMAGE_NAME }}/fastgpt-sub-route:latest" >> $GITHUB_ENV + echo "Docker_Hub_Tag=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-sub-route:${{ github.ref_name }}" >> $GITHUB_ENV + echo "Docker_Hub_Latest=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-sub-route:latest" >> $GITHUB_ENV + fi + + - name: Build and publish image for main branch or tag push event + env: + DOCKER_REPO_TAGGED: ${{ env.DOCKER_REPO_TAGGED }} + run: | + docker buildx build \ + -f projects/app/Dockerfile \ + --platform linux/amd64,linux/arm64 \ + --build-arg base_url=/fastai \ + --label "org.opencontainers.image.source=https://github.com/${{ github.repository_owner }}/FastGPT" \ + --label "org.opencontainers.image.description=fastgpt image" \ + --push \ + --cache-from=type=local,src=/tmp/.buildx-cache \ + --cache-to=type=local,dest=/tmp/.buildx-cache \ + -t ${Git_Tag} \ + -t ${Git_Latest} \ + -t ${Ali_Tag} \ + -t ${Ali_Latest} \ + -t ${Docker_Hub_Tag} \ + -t ${Docker_Hub_Latest} \ + . diff --git a/docSite/assets/imgs/bing_search_plugin1.png b/docSite/assets/imgs/bing_search_plugin1.png new file mode 100644 index 000000000..27d51f03f Binary files /dev/null and b/docSite/assets/imgs/bing_search_plugin1.png differ diff --git a/docSite/assets/imgs/bing_search_plugin2.png b/docSite/assets/imgs/bing_search_plugin2.png new file mode 100644 index 000000000..fccaa5940 Binary files /dev/null and b/docSite/assets/imgs/bing_search_plugin2.png differ diff --git a/docSite/assets/imgs/bing_search_plugin3.png b/docSite/assets/imgs/bing_search_plugin3.png new file mode 100644 index 000000000..7c83fefb6 Binary files /dev/null and b/docSite/assets/imgs/bing_search_plugin3.png differ diff --git a/docSite/assets/imgs/bing_search_plugin4.png b/docSite/assets/imgs/bing_search_plugin4.png new file mode 100644 index 000000000..b4a428bbc Binary files /dev/null and b/docSite/assets/imgs/bing_search_plugin4.png differ diff --git a/docSite/assets/imgs/bing_search_plugin5.png b/docSite/assets/imgs/bing_search_plugin5.png new file mode 100644 index 000000000..fb0afe407 Binary files /dev/null and b/docSite/assets/imgs/bing_search_plugin5.png differ diff --git a/docSite/assets/imgs/doc2x_plugin1.png b/docSite/assets/imgs/doc2x_plugin1.png new file mode 100644 index 000000000..4d6d0126f Binary files /dev/null and b/docSite/assets/imgs/doc2x_plugin1.png differ diff --git a/docSite/assets/imgs/doc2x_plugin2.png b/docSite/assets/imgs/doc2x_plugin2.png new file mode 100644 index 000000000..a30379d21 Binary files /dev/null and b/docSite/assets/imgs/doc2x_plugin2.png differ diff --git a/docSite/assets/imgs/doc2x_plugin3.png b/docSite/assets/imgs/doc2x_plugin3.png new file mode 100644 index 000000000..c9ea407a0 Binary files /dev/null and b/docSite/assets/imgs/doc2x_plugin3.png differ diff --git a/docSite/assets/imgs/doc2x_plugin4.png b/docSite/assets/imgs/doc2x_plugin4.png new file mode 100644 index 000000000..aa549ab06 Binary files /dev/null and b/docSite/assets/imgs/doc2x_plugin4.png differ diff --git a/docSite/assets/imgs/document_analysis1.png b/docSite/assets/imgs/document_analysis1.png new file mode 100644 index 000000000..a1a76deb6 Binary files /dev/null and b/docSite/assets/imgs/document_analysis1.png differ diff --git a/docSite/assets/imgs/document_analysis2.png b/docSite/assets/imgs/document_analysis2.png new file mode 100644 index 000000000..b2e662fb0 Binary files /dev/null and b/docSite/assets/imgs/document_analysis2.png differ diff --git a/docSite/assets/imgs/fileinpu-6.jpg b/docSite/assets/imgs/fileinpu-6.jpg deleted file mode 100644 index 5a649e1d3..000000000 Binary files a/docSite/assets/imgs/fileinpu-6.jpg and /dev/null differ diff --git a/docSite/assets/imgs/fileinpu-7.jpg b/docSite/assets/imgs/fileinpu-7.jpg deleted file mode 100644 index 975a5ae26..000000000 Binary files a/docSite/assets/imgs/fileinpu-7.jpg and /dev/null differ diff --git a/docSite/assets/imgs/google_search_plugin1.png b/docSite/assets/imgs/google_search_plugin1.png new file mode 100644 index 000000000..d9db7878c Binary files /dev/null and b/docSite/assets/imgs/google_search_plugin1.png differ diff --git a/docSite/assets/imgs/google_search_plugin2.png b/docSite/assets/imgs/google_search_plugin2.png new file mode 100644 index 000000000..6421bf151 Binary files /dev/null and b/docSite/assets/imgs/google_search_plugin2.png differ diff --git a/docSite/assets/imgs/google_search_plugin3.png b/docSite/assets/imgs/google_search_plugin3.png new file mode 100644 index 000000000..63542f66d Binary files /dev/null and b/docSite/assets/imgs/google_search_plugin3.png differ diff --git a/docSite/assets/imgs/google_search_plugin4.png b/docSite/assets/imgs/google_search_plugin4.png new file mode 100644 index 000000000..a6a4c7853 Binary files /dev/null and b/docSite/assets/imgs/google_search_plugin4.png differ diff --git a/docSite/assets/imgs/image-5.png b/docSite/assets/imgs/image-5.png new file mode 100644 index 000000000..40abc7cde Binary files /dev/null and b/docSite/assets/imgs/image-5.png differ diff --git a/docSite/assets/imgs/image-6.png b/docSite/assets/imgs/image-6.png new file mode 100644 index 000000000..2235b9d62 Binary files /dev/null and b/docSite/assets/imgs/image-6.png differ diff --git a/docSite/assets/imgs/image-7.png b/docSite/assets/imgs/image-7.png new file mode 100644 index 000000000..99b26e3ab Binary files /dev/null and b/docSite/assets/imgs/image-7.png differ diff --git a/docSite/assets/imgs/integration1.png b/docSite/assets/imgs/integration1.png new file mode 100644 index 000000000..c84df0175 Binary files /dev/null and b/docSite/assets/imgs/integration1.png differ diff --git a/docSite/assets/imgs/knowledge_merge1.png b/docSite/assets/imgs/knowledge_merge1.png new file mode 100644 index 000000000..fb3789f75 Binary files /dev/null and b/docSite/assets/imgs/knowledge_merge1.png differ diff --git a/docSite/assets/imgs/knowledge_merge2.png b/docSite/assets/imgs/knowledge_merge2.png new file mode 100644 index 000000000..acc4846ed Binary files /dev/null and b/docSite/assets/imgs/knowledge_merge2.png differ diff --git a/docSite/assets/imgs/knowledge_merge3.png b/docSite/assets/imgs/knowledge_merge3.png new file mode 100644 index 000000000..42e1bfc8b Binary files /dev/null and b/docSite/assets/imgs/knowledge_merge3.png differ diff --git a/docSite/assets/imgs/plugin_submission1.png b/docSite/assets/imgs/plugin_submission1.png new file mode 100644 index 000000000..07e7d89a1 Binary files /dev/null and b/docSite/assets/imgs/plugin_submission1.png differ diff --git a/docSite/assets/imgs/plugin_submission2.png b/docSite/assets/imgs/plugin_submission2.png new file mode 100644 index 000000000..5008d87ed Binary files /dev/null and b/docSite/assets/imgs/plugin_submission2.png differ diff --git a/docSite/assets/imgs/plugin_submission3.png b/docSite/assets/imgs/plugin_submission3.png new file mode 100644 index 000000000..a65cac416 Binary files /dev/null and b/docSite/assets/imgs/plugin_submission3.png differ diff --git a/docSite/assets/imgs/plugin_submission4.png b/docSite/assets/imgs/plugin_submission4.png new file mode 100644 index 000000000..bf46d5785 Binary files /dev/null and b/docSite/assets/imgs/plugin_submission4.png differ diff --git a/docSite/assets/imgs/plugin_submission5.png b/docSite/assets/imgs/plugin_submission5.png new file mode 100644 index 000000000..8aab80a34 Binary files /dev/null and b/docSite/assets/imgs/plugin_submission5.png differ diff --git a/docSite/assets/imgs/plugin_submission6.png b/docSite/assets/imgs/plugin_submission6.png new file mode 100644 index 000000000..5cb9829e7 Binary files /dev/null and b/docSite/assets/imgs/plugin_submission6.png differ diff --git a/docSite/assets/imgs/plugin_submission7.png b/docSite/assets/imgs/plugin_submission7.png new file mode 100644 index 000000000..a13d382f9 Binary files /dev/null and b/docSite/assets/imgs/plugin_submission7.png differ diff --git a/docSite/assets/imgs/points1.png b/docSite/assets/imgs/points1.png new file mode 100644 index 000000000..31714f247 Binary files /dev/null and b/docSite/assets/imgs/points1.png differ diff --git a/docSite/assets/imgs/spellcheck1.png b/docSite/assets/imgs/spellcheck1.png new file mode 100644 index 000000000..199264319 Binary files /dev/null and b/docSite/assets/imgs/spellcheck1.png differ diff --git a/docSite/assets/imgs/spellcheck2.png b/docSite/assets/imgs/spellcheck2.png new file mode 100644 index 000000000..9e6e502f5 Binary files /dev/null and b/docSite/assets/imgs/spellcheck2.png differ diff --git a/docSite/assets/imgs/spellcheck3.png b/docSite/assets/imgs/spellcheck3.png new file mode 100644 index 000000000..b7fa4eeca Binary files /dev/null and b/docSite/assets/imgs/spellcheck3.png differ diff --git a/docSite/assets/imgs/spellcheck4.png b/docSite/assets/imgs/spellcheck4.png new file mode 100644 index 000000000..4a4564e4c Binary files /dev/null and b/docSite/assets/imgs/spellcheck4.png differ diff --git a/docSite/assets/imgs/spellcheck5.png b/docSite/assets/imgs/spellcheck5.png new file mode 100644 index 000000000..4c29ada01 Binary files /dev/null and b/docSite/assets/imgs/spellcheck5.png differ diff --git a/docSite/assets/imgs/template_submission1.png b/docSite/assets/imgs/template_submission1.png new file mode 100644 index 000000000..7ea025d89 Binary files /dev/null and b/docSite/assets/imgs/template_submission1.png differ diff --git a/docSite/assets/imgs/template_submission2.png b/docSite/assets/imgs/template_submission2.png new file mode 100644 index 000000000..338d47b1a Binary files /dev/null and b/docSite/assets/imgs/template_submission2.png differ diff --git a/docSite/assets/imgs/template_submission3.png b/docSite/assets/imgs/template_submission3.png new file mode 100644 index 000000000..cc0897414 Binary files /dev/null and b/docSite/assets/imgs/template_submission3.png differ diff --git a/docSite/assets/imgs/template_submission4.png b/docSite/assets/imgs/template_submission4.png new file mode 100644 index 000000000..3ba1f0ee1 Binary files /dev/null and b/docSite/assets/imgs/template_submission4.png differ diff --git a/docSite/assets/imgs/translate1.png b/docSite/assets/imgs/translate1.png new file mode 100644 index 000000000..86d13d96d Binary files /dev/null and b/docSite/assets/imgs/translate1.png differ diff --git a/docSite/assets/imgs/translate10.png b/docSite/assets/imgs/translate10.png new file mode 100644 index 000000000..7705286c4 Binary files /dev/null and b/docSite/assets/imgs/translate10.png differ diff --git a/docSite/assets/imgs/translate11.png b/docSite/assets/imgs/translate11.png new file mode 100644 index 000000000..b8ec88ea8 Binary files /dev/null and b/docSite/assets/imgs/translate11.png differ diff --git a/docSite/assets/imgs/translate12.png b/docSite/assets/imgs/translate12.png new file mode 100644 index 000000000..c4d03b739 Binary files /dev/null and b/docSite/assets/imgs/translate12.png differ diff --git a/docSite/assets/imgs/translate13.png b/docSite/assets/imgs/translate13.png new file mode 100644 index 000000000..000719089 Binary files /dev/null and b/docSite/assets/imgs/translate13.png differ diff --git a/docSite/assets/imgs/translate14.png b/docSite/assets/imgs/translate14.png new file mode 100644 index 000000000..dc82701ec Binary files /dev/null and b/docSite/assets/imgs/translate14.png differ diff --git a/docSite/assets/imgs/translate15.png b/docSite/assets/imgs/translate15.png new file mode 100644 index 000000000..9076e97f1 Binary files /dev/null and b/docSite/assets/imgs/translate15.png differ diff --git a/docSite/assets/imgs/translate16.png b/docSite/assets/imgs/translate16.png new file mode 100644 index 000000000..366ed4bbf Binary files /dev/null and b/docSite/assets/imgs/translate16.png differ diff --git a/docSite/assets/imgs/translate17.png b/docSite/assets/imgs/translate17.png new file mode 100644 index 000000000..399935df7 Binary files /dev/null and b/docSite/assets/imgs/translate17.png differ diff --git a/docSite/assets/imgs/translate18.png b/docSite/assets/imgs/translate18.png new file mode 100644 index 000000000..6296c5a86 Binary files /dev/null and b/docSite/assets/imgs/translate18.png differ diff --git a/docSite/assets/imgs/translate19.png b/docSite/assets/imgs/translate19.png new file mode 100644 index 000000000..010160a07 Binary files /dev/null and b/docSite/assets/imgs/translate19.png differ diff --git a/docSite/assets/imgs/translate2.png b/docSite/assets/imgs/translate2.png new file mode 100644 index 000000000..55a764d21 Binary files /dev/null and b/docSite/assets/imgs/translate2.png differ diff --git a/docSite/assets/imgs/translate20.png b/docSite/assets/imgs/translate20.png new file mode 100644 index 000000000..ea7d3298f Binary files /dev/null and b/docSite/assets/imgs/translate20.png differ diff --git a/docSite/assets/imgs/translate21.png b/docSite/assets/imgs/translate21.png new file mode 100644 index 000000000..210137ac5 Binary files /dev/null and b/docSite/assets/imgs/translate21.png differ diff --git a/docSite/assets/imgs/translate3.png b/docSite/assets/imgs/translate3.png new file mode 100644 index 000000000..a4e9e1d2e Binary files /dev/null and b/docSite/assets/imgs/translate3.png differ diff --git a/docSite/assets/imgs/translate4.png b/docSite/assets/imgs/translate4.png new file mode 100644 index 000000000..7eae67077 Binary files /dev/null and b/docSite/assets/imgs/translate4.png differ diff --git a/docSite/assets/imgs/translate5.png b/docSite/assets/imgs/translate5.png new file mode 100644 index 000000000..7272932a8 Binary files /dev/null and b/docSite/assets/imgs/translate5.png differ diff --git a/docSite/assets/imgs/translate61.png b/docSite/assets/imgs/translate61.png new file mode 100644 index 000000000..4d2d781d6 Binary files /dev/null and b/docSite/assets/imgs/translate61.png differ diff --git a/docSite/assets/imgs/translate7.png b/docSite/assets/imgs/translate7.png new file mode 100644 index 000000000..44e2e8d0a Binary files /dev/null and b/docSite/assets/imgs/translate7.png differ diff --git a/docSite/assets/imgs/translate8.png b/docSite/assets/imgs/translate8.png new file mode 100644 index 000000000..9ba735bbd Binary files /dev/null and b/docSite/assets/imgs/translate8.png differ diff --git a/docSite/assets/imgs/translate9.png b/docSite/assets/imgs/translate9.png new file mode 100644 index 000000000..d2cd500e9 Binary files /dev/null and b/docSite/assets/imgs/translate9.png differ diff --git a/docSite/assets/imgs/user-selection1.png b/docSite/assets/imgs/user-selection1.png new file mode 100644 index 000000000..a629af4e8 Binary files /dev/null and b/docSite/assets/imgs/user-selection1.png differ diff --git a/docSite/assets/imgs/user-selection2.png b/docSite/assets/imgs/user-selection2.png new file mode 100644 index 000000000..ab5fede1d Binary files /dev/null and b/docSite/assets/imgs/user-selection2.png differ diff --git a/docSite/assets/imgs/user-selection3.png b/docSite/assets/imgs/user-selection3.png new file mode 100644 index 000000000..8acfb7752 Binary files /dev/null and b/docSite/assets/imgs/user-selection3.png differ diff --git a/docSite/assets/imgs/variable_update1.png b/docSite/assets/imgs/variable_update1.png new file mode 100644 index 000000000..569238d27 Binary files /dev/null and b/docSite/assets/imgs/variable_update1.png differ diff --git a/docSite/assets/imgs/variable_update2.png b/docSite/assets/imgs/variable_update2.png new file mode 100644 index 000000000..0f4a72b2f Binary files /dev/null and b/docSite/assets/imgs/variable_update2.png differ diff --git a/docSite/assets/imgs/variable_update3.png b/docSite/assets/imgs/variable_update3.png new file mode 100644 index 000000000..182d7e5ba Binary files /dev/null and b/docSite/assets/imgs/variable_update3.png differ diff --git a/docSite/assets/imgs/variable_update4.png b/docSite/assets/imgs/variable_update4.png new file mode 100644 index 000000000..cd8f35eb8 Binary files /dev/null and b/docSite/assets/imgs/variable_update4.png differ diff --git a/docSite/assets/imgs/variable_update5.png b/docSite/assets/imgs/variable_update5.png new file mode 100644 index 000000000..fdfb454e6 Binary files /dev/null and b/docSite/assets/imgs/variable_update5.png differ diff --git a/docSite/content/zh-cn/docs/development/migration/ docker_mongo.md b/docSite/content/zh-cn/docs/development/migration/docker_mongo.md similarity index 100% rename from docSite/content/zh-cn/docs/development/migration/ docker_mongo.md rename to docSite/content/zh-cn/docs/development/migration/docker_mongo.md diff --git a/docSite/content/zh-cn/docs/development/upgrading/464.md b/docSite/content/zh-cn/docs/development/upgrading/464.md index b93b3257d..125ba0bc5 100644 --- a/docSite/content/zh-cn/docs/development/upgrading/464.md +++ b/docSite/content/zh-cn/docs/development/upgrading/464.md @@ -32,7 +32,7 @@ curl --location --request POST 'https://{{host}}/api/admin/initv464' \ 4. 优化 - 历史记录模块。弃用旧的历史记录模块,直接在对应地方填写数值即可。 5. 调整 - 知识库搜索模块 topk 逻辑,采用 MaxToken 计算,兼容不同长度的文本块 6. 调整鉴权顺序,提高 apikey 的优先级,避免cookie抢占 apikey 的鉴权。 -7. 链接读取支持多选择器。参考[Web 站点同步用法](/docs/course/websync) +7. 链接读取支持多选择器。参考[Web 站点同步用法](/docs/guide/knowledge_base/websync/) 8. 修复 - 分享链接图片上传鉴权问题 9. 修复 - Mongo 连接池未释放问题。 10. 修复 - Dataset Intro 无法更新 diff --git a/docSite/content/zh-cn/docs/development/upgrading/465.md b/docSite/content/zh-cn/docs/development/upgrading/465.md index 9353384e9..a36b780c6 100644 --- a/docSite/content/zh-cn/docs/development/upgrading/465.md +++ b/docSite/content/zh-cn/docs/development/upgrading/465.md @@ -21,10 +21,10 @@ weight: 831 ## V4.6.5 功能介绍 -1. 新增 - [问题优化模块](/docs/workflow/modules/coreferenceresolution/) -2. 新增 - [文本编辑模块](/docs/workflow/modules/text_editor/) -3. 新增 - [判断器模块](/docs/workflow/modules/tfswitch/) -4. 新增 - [自定义反馈模块](/docs/workflow/modules/custom_feedback/) +1. 新增 - [问题优化模块](/docs/guide/workbench/workflow/coreferenceresolution/) +2. 新增 - [文本编辑模块](/docs/guide/workbench/workflow/text_editor/) +3. 新增 - [判断器模块](/docs/guide/workbench/workflow/tfswitch//) +4. 新增 - [自定义反馈模块](/docs/guide/workbench/workflow/custom_feedback/) 5. 新增 - 【内容提取】模块支持选择模型,以及字段枚举 6. 优化 - docx读取,兼容表格(表格转markdown) 7. 优化 - 高级编排连接线交互 diff --git a/docSite/content/zh-cn/docs/development/upgrading/466.md b/docSite/content/zh-cn/docs/development/upgrading/466.md index fc216d974..0435df94a 100644 --- a/docSite/content/zh-cn/docs/development/upgrading/466.md +++ b/docSite/content/zh-cn/docs/development/upgrading/466.md @@ -25,7 +25,7 @@ weight: 830 1. 查看 [FastGPT 2024 RoadMap](https://github.com/labring/FastGPT?tab=readme-ov-file#-%E5%9C%A8%E7%BA%BF%E4%BD%BF%E7%94%A8) 2. 新增 - Http 模块请求头支持 Json 编辑器。 -3. 新增 - [ReRank模型部署](/docs/development/custom-models/reranker/) +3. 新增 - [ReRank模型部署](/docs/development/custom-models/bge-rerank/) 4. 新增 - 搜索方式:分离向量语义检索,全文检索和重排,通过 RRF 进行排序合并。 5. 优化 - 问题分类提示词,id引导。测试国产商用 api 模型(百度阿里智谱讯飞)使用 Prompt 模式均可分类。 6. UI 优化,未来将逐步替换新的UI设计。 diff --git a/docSite/content/zh-cn/docs/development/upgrading/468.md b/docSite/content/zh-cn/docs/development/upgrading/468.md index 6df28919b..965eba55d 100644 --- a/docSite/content/zh-cn/docs/development/upgrading/468.md +++ b/docSite/content/zh-cn/docs/development/upgrading/468.md @@ -91,7 +91,7 @@ curl --location --request POST 'https://{{host}}/api/init/v468' \ 1. 新增 - 知识库搜索合并模块。 2. 新增 - 新的 Http 模块,支持更加灵活的参数传入。同时支持了输入输出自动数据类型转化,例如:接口输出的 JSON 类型会自动转成字符串类型,直接给其他模块使用。此外,还补充了一些例子,可在文档中查看。 -3. 优化 - 内容补全。将内容补全内置到【知识库搜索】中,并实现了一次内容补全,即可完成“指代消除”和“问题扩展”。FastGPT知识库搜索详细流程可查看:[知识库搜索介绍](/docs/course/data_search/) +3. 优化 - 内容补全。将内容补全内置到【知识库搜索】中,并实现了一次内容补全,即可完成“指代消除”和“问题扩展”。FastGPT知识库搜索详细流程可查看:[知识库搜索介绍](/docs/guide/workbench/workflow/dataset_search/) 4. 优化 - LLM 模型配置,不再区分对话、分类、提取模型。同时支持模型的默认参数,避免不同模型参数冲突,可通过`defaultConfig`传入默认的配置。 5. 优化 - 流响应,参考了`ChatNextWeb`的流,更加丝滑。此外,之前提到的乱码、中断,刷新后又正常了,可能会修复) 6. 修复 - 语音输入文件无法上传。 diff --git a/docSite/content/zh-cn/docs/development/upgrading/4813.md b/docSite/content/zh-cn/docs/development/upgrading/4813.md index 255d70002..9855eadc8 100644 --- a/docSite/content/zh-cn/docs/development/upgrading/4813.md +++ b/docSite/content/zh-cn/docs/development/upgrading/4813.md @@ -7,6 +7,20 @@ toc: true weight: 811 --- +## 更新指南 + +### 1. 做好数据备份 + +### 2. 修改镜像 + +- 更新 FastGPT 镜像 tag: v4.8.13-beta +- 更新 FastGPT 商业版镜像 tag: v4.8.13-beta (fastgpt-pro镜像) +- Sandbox 镜像,可以不更新 + +### 3. 调整文件上传编排 + +虽然依然兼容旧版的文件上传编排,但是未来两个版本内将会去除兼容代码,请尽快调整编排,以适应最新的文件上传逻辑。尤其是嵌套应用的文件传递,未来将不会自动传递,必须手动指定传递的文件。 + ## 更新说明 1. 新增 - 数组变量选择支持多选,可以选多个数组或对应的单一数据类型,会自动按选择顺序进行合并。 @@ -16,26 +30,15 @@ weight: 811 5. 新增 - 循环节点增加下标值。 6. 新增 - 部分对话错误提醒增加翻译。 7. 新增 - 对话输入框支持拖拽文件上传,可直接拖文件到输入框中。 -8. 优化 - 合并多个 system 提示词成 1 个,避免部分模型不支持多个 system 提示词。 -9. 优化 - 知识库上传文件,优化报错提示。 -10. 优化 - 全文检索语句,减少一轮子查询。 -11. 优化 - 修改 findLast 为 [...array].reverse().find,适配旧版浏览器。 -12. 优化 - Markdown 组件自动空格,避免分割 url 中的中文。 -13. 优化 - 工作流上下文拆分,性能优化。 -14. 优化 - 语音播报,不支持 mediaSource 的浏览器可等待完全生成语音后输出。 -15. 修复 - Dockerfile pnpm install 支持代理。 -16. 修复 - BI 图表生成无法写入文件。 - -## 更新指南 - -### 1. 做好数据备份 - -### 2. 修改镜像 - -- 更新 FastGPT 镜像 tag: v4.8.13-alpha -- 更新 FastGPT 管理端镜像 tag: v4.8.13-alpha (fastgpt-pro镜像) -- Sandbox 镜像,可以不更新 - -### 3. 调整文件上传编排 - -虽然依然兼容旧版的文件上传编排,但是未来两个版本内将会去除兼容代码,请尽快调整编排,以适应最新的文件上传逻辑。尤其是嵌套应用的文件传递,未来将不会自动传递,必须手动指定传递的文件。 +8. 新增 - 对话日志,来源可显示分享链接/API具体名称 +9. 新增 - 分享链接支持配置是否展示实时运行状态。 +10. 优化 - 合并多个 system 提示词成 1 个,避免部分模型不支持多个 system 提示词。 +11. 优化 - 知识库上传文件,优化报错提示。 +12. 优化 - 全文检索语句,减少一轮子查询。 +13. 优化 - 修改 findLast 为 [...array].reverse().find,适配旧版浏览器。 +14. 优化 - Markdown 组件自动空格,避免分割 url 中的中文。 +15. 优化 - 工作流上下文拆分,性能优化。 +16. 优化 - 语音播报,不支持 mediaSource 的浏览器可等待完全生成语音后输出。 +17. 修复 - Dockerfile pnpm install 支持代理。 +18. 修复 - BI 图表生成无法写入文件。同时优化其解析,支持数字类型数组。 +19. 修复 - 分享链接首次加载时,标题显示不正确。 diff --git a/docSite/content/zh-cn/docs/faq/_index.md b/docSite/content/zh-cn/docs/faq/_index.md index 89cec79b3..d789768bd 100644 --- a/docSite/content/zh-cn/docs/faq/_index.md +++ b/docSite/content/zh-cn/docs/faq/_index.md @@ -6,6 +6,6 @@ draft: false toc: true weight: 900 --- - + FastGPT 是一个由用户和贡献者参与推动的开源项目,如果您对产品使用存在疑问和建议,可尝试[加入社区](community)寻求支持。我们的团队与社区会竭尽所能为您提供帮助。 \ No newline at end of file diff --git a/docSite/content/zh-cn/docs/faq/app.md b/docSite/content/zh-cn/docs/faq/app.md index 3819bd4d2..41e427276 100644 --- a/docSite/content/zh-cn/docs/faq/app.md +++ b/docSite/content/zh-cn/docs/faq/app.md @@ -4,7 +4,7 @@ description: 'FastGPT 常见应用使用问题,包括简易应用、工作流 icon: 'quiz' draft: false toc: true -weight: 903 +weight: 908 --- ## 工作流中多轮对话场景中如何使连续问题被问题分类节点正确的归类 diff --git a/docSite/content/zh-cn/docs/faq/chat.md b/docSite/content/zh-cn/docs/faq/chat.md index 758905b1c..93aeb1afc 100644 --- a/docSite/content/zh-cn/docs/faq/chat.md +++ b/docSite/content/zh-cn/docs/faq/chat.md @@ -4,7 +4,7 @@ description: 'FastGPT 常见聊天框问题' icon: 'quiz' draft: false toc: true -weight: 905 +weight: 906 --- ## 我修改了工作台的应用,为什么在“聊天”时没有更新配置? diff --git a/docSite/content/zh-cn/docs/faq/dataset.md b/docSite/content/zh-cn/docs/faq/dataset.md index 3a9130636..e66ebc01e 100644 --- a/docSite/content/zh-cn/docs/faq/dataset.md +++ b/docSite/content/zh-cn/docs/faq/dataset.md @@ -4,7 +4,7 @@ description: '常见知识库使用问题' icon: 'quiz' draft: false toc: true -weight: 904 +weight: 910 --- ## 上传的文件内容出现中文乱码 diff --git a/docSite/content/zh-cn/docs/faq/docker.md b/docSite/content/zh-cn/docs/faq/docker.md index c34491cac..8b1e6e375 100644 --- a/docSite/content/zh-cn/docs/faq/docker.md +++ b/docSite/content/zh-cn/docs/faq/docker.md @@ -4,7 +4,7 @@ description: 'FastGPT Docker 部署问题' icon: '' draft: false toc: true -weight: 901 +weight: 902 type: redirect target: /docs/development/docker/#faq --- diff --git a/docSite/content/zh-cn/docs/faq/error.md b/docSite/content/zh-cn/docs/faq/error.md index 330f47f81..f0a0ee3f8 100644 --- a/docSite/content/zh-cn/docs/faq/error.md +++ b/docSite/content/zh-cn/docs/faq/error.md @@ -1,7 +1,15 @@ --- -title: '常见错误' +title: '报错' icon: 'quiz' draft: false toc: true -weight: 920 ---- \ No newline at end of file +weight: 914 +--- + +1. ### 当前分组上游负载已饱和,请稍后再试(request id:202407100753411462086782835521) + +是oneapi渠道的问题,可以换个模型用or换一家中转站 + +1. ### 使用API时在日志中报错Connection Error + +大概率是api-key填写了openapi,然后部署的服务器在国内,不能访问海外的api,可以使用中转或者反代的手段解决访问不到的问题 \ No newline at end of file diff --git a/docSite/content/zh-cn/docs/faq/external_channel_integration.md b/docSite/content/zh-cn/docs/faq/external_channel_integration.md new file mode 100644 index 000000000..2134bdc93 --- /dev/null +++ b/docSite/content/zh-cn/docs/faq/external_channel_integration.md @@ -0,0 +1,18 @@ +--- +title: "接入外部渠道" +description: "如何通过外部渠道与 FastGPT 集成,实现对多种平台的支持" +icon: "integration" +draft: false +toc: true +weight: 912 +--- + +1. ### 接入cow,图文对话无法直接显示图片 + +提示词给引导,不要以markdown格式输出。图片需要二开 cow 实现图片链接截取并发送。 + +1. ### 可以获取到用户发送问答的记录吗 + +在应用的对话日志里可以查看。 + +![](/imgs/integration1.png) \ No newline at end of file diff --git a/docSite/content/zh-cn/docs/faq/other.md b/docSite/content/zh-cn/docs/faq/other.md index baf557a2e..b8adf64f2 100644 --- a/docSite/content/zh-cn/docs/faq/other.md +++ b/docSite/content/zh-cn/docs/faq/other.md @@ -3,7 +3,7 @@ title: '其他问题' icon: 'quiz' draft: false toc: true -weight: 925 +weight: 918 --- ## oneapi 官网是哪个 diff --git a/docSite/content/zh-cn/docs/faq/points_consumption.md b/docSite/content/zh-cn/docs/faq/points_consumption.md new file mode 100644 index 000000000..2eb0f6d5c --- /dev/null +++ b/docSite/content/zh-cn/docs/faq/points_consumption.md @@ -0,0 +1,14 @@ +--- +title: "积分消耗" +description: "了解 FastGPT 中的积分消耗机制和使用场景" +icon: "points" +draft: false +toc: true +weight: 916 +--- + +1. ### 接入oneapi后,为什么还会消耗fastgpt的积分 + +矢量数据库检索会默认消耗。可以查看看绑定提示和使用记录。 + +![](/imgs/points1.png) \ No newline at end of file diff --git a/docSite/content/zh-cn/docs/faq/privateDeploy.md b/docSite/content/zh-cn/docs/faq/privateDeploy.md index 9f1643f58..1032ba989 100644 --- a/docSite/content/zh-cn/docs/faq/privateDeploy.md +++ b/docSite/content/zh-cn/docs/faq/privateDeploy.md @@ -4,7 +4,7 @@ description: "FastGPT 私有部署常见问题" icon: upgrade draft: false images: [] -weight: 902 +weight: 904 type: redirect target: /docs/development/faq/ --- \ No newline at end of file diff --git a/docSite/content/zh-cn/docs/guide/_index.md b/docSite/content/zh-cn/docs/guide/_index.md new file mode 100644 index 000000000..a6c7ada72 --- /dev/null +++ b/docSite/content/zh-cn/docs/guide/_index.md @@ -0,0 +1,9 @@ +--- +weight: 100 +title: '功能介绍' +description: 'FastGPT 的功能和使用指南' +icon: 'import_contacts' +draft: false +images: [] +--- + \ No newline at end of file diff --git a/docSite/content/zh-cn/docs/course/_index.md b/docSite/content/zh-cn/docs/guide/course/_index.md similarity index 87% rename from docSite/content/zh-cn/docs/course/_index.md rename to docSite/content/zh-cn/docs/guide/course/_index.md index 3c6b4b37a..9a49870d0 100644 --- a/docSite/content/zh-cn/docs/course/_index.md +++ b/docSite/content/zh-cn/docs/guide/course/_index.md @@ -6,4 +6,4 @@ icon: 'import_contacts' draft: false images: [] --- - \ No newline at end of file + \ No newline at end of file diff --git a/docSite/content/zh-cn/docs/course/ai_settings.md b/docSite/content/zh-cn/docs/guide/course/ai_settings.md similarity index 98% rename from docSite/content/zh-cn/docs/course/ai_settings.md rename to docSite/content/zh-cn/docs/guide/course/ai_settings.md index 3f206a5d4..8f748bfb5 100644 --- a/docSite/content/zh-cn/docs/course/ai_settings.md +++ b/docSite/content/zh-cn/docs/guide/course/ai_settings.md @@ -4,7 +4,7 @@ description: "FastGPT AI 相关参数配置说明" icon: "sign_language" draft: false toc: true -weight: 102 +weight: 104 --- 在 FastGPT 的 AI 对话模块中,有一个 AI 高级配置,里面包含了 AI 模型的参数配置,本文详细介绍这些配置的含义。 @@ -66,7 +66,7 @@ Tips: 可以通过点击上下文按键查看完整的上下文组成,便于 FastGPT 知识库采用 QA 对(不一定都是问答格式,仅代表两个变量)的格式存储,在转义成字符串时候会根据**引用模板**来进行格式化。知识库包含多个可用变量: q, a, sourceId(数据的ID), index(第n个数据), source(数据的集合名、文件名),score(距离得分,0-1) 可以通过 {{q}} {{a}} {{sourceId}} {{index}} {{source}} {{score}} 按需引入。下面一个模板例子: -可以通过 [知识库结构讲解](/docs/course/dataset_engine/) 了解详细的知识库的结构。 +可以通过 [知识库结构讲解](/docs/guide/knowledge_base/dataset_engine/) 了解详细的知识库的结构。 #### 引用模板 diff --git a/docSite/content/zh-cn/docs/course/chat_input_guide.md b/docSite/content/zh-cn/docs/guide/course/chat_input_guide.md similarity index 98% rename from docSite/content/zh-cn/docs/course/chat_input_guide.md rename to docSite/content/zh-cn/docs/guide/course/chat_input_guide.md index 07c8436b0..155cdfdb3 100644 --- a/docSite/content/zh-cn/docs/course/chat_input_guide.md +++ b/docSite/content/zh-cn/docs/guide/course/chat_input_guide.md @@ -4,7 +4,7 @@ description: "FastGPT 对话问题引导" icon: "code" draft: false toc: true -weight: 108 +weight: 106 --- ![](/imgs/questionGuide.png) diff --git a/docSite/content/zh-cn/docs/course/collection_tags.md b/docSite/content/zh-cn/docs/guide/course/collection_tags.md similarity index 100% rename from docSite/content/zh-cn/docs/course/collection_tags.md rename to docSite/content/zh-cn/docs/guide/course/collection_tags.md diff --git a/docSite/content/zh-cn/docs/course/fileInput.md b/docSite/content/zh-cn/docs/guide/course/fileInput.md similarity index 56% rename from docSite/content/zh-cn/docs/course/fileInput.md rename to docSite/content/zh-cn/docs/guide/course/fileInput.md index d9e9c91bf..5eb88b33a 100644 --- a/docSite/content/zh-cn/docs/course/fileInput.md +++ b/docSite/content/zh-cn/docs/guide/course/fileInput.md @@ -20,10 +20,12 @@ weight: 110 随后,你的调试对话框中,就会出现一个文件选择的 icon,可以点击文件选择 icon,选择你需要上传的文件。 -由于采用的是工具调用模式,所以在提问时候,可能需要加上适当的引导,让模型知道,你需要读取`文档`。 - ![打开文件上传](/imgs/fileinpu-2.png) +**工作模式** + +从 4.8.13 版本起,简易模式的文件读取将会强制解析文件并放入 system 提示词中,避免连续对话时,模型有时候不会主动调用读取文件的工具。 + ## 工作流中使用 工作流中,可以在系统配置中,找到`文件输入`配置项,点击其右侧的`开启`/`关闭`按键,即可打开配置弹窗。 @@ -32,14 +34,13 @@ weight: 110 在工作流中,使用文件的方式很多,最简单的就是类似下图中,直接通过工具调用接入文档解析,实现和简易模式一样的效果。 -![打开文件上传](/imgs/fileinpu-3.jpg) +| | | +| --------------------- | --------------------- | +| ![](/imgs/image-5.png) | ![](/imgs/image-6.png) | -也可以更简单点,强制每轮对话都携带上文档内容进行回答,这样就不需要调用两次 AI 才能读取文档内容了。 - -![打开文件上传](/imgs/fileinpu-5.jpg) - -当然,你也可以在工作流中,对文档进行内容提取、内容分析等,然后将分析的结果传递给 HTTP 或者其他模块,从而实现文件处理的 SOP。不过目前版本,`插件`中并未支持文件处理,所以在构建 SOP 的话可能还是有一些麻烦。 +当然,你也可以在工作流中,对文档进行内容提取、内容分析等,然后将分析的结果传递给 HTTP 或者其他模块,从而实现文件处理的 SOP。 +![文档解析](/imgs/image-7.png) ## 文档解析工作原理 @@ -73,23 +74,8 @@ type UserChatItemValueItemType = { 文档解析依赖文档解析节点,这个节点会接收一个`array`类型的输入,对应的是文件输入的 URL;输出的是一个`string`,对应的是文档解析后的内容。 -![打开文件上传](/imgs/fileinpu-6.jpg) - * 在文档解析节点中,只会解析`文档`类型的 URL,它是通过文件 URL 解析出来的`文名件后缀`去判断的。如果你同时选择了文档和图片,图片会被忽略。 -* 文档解析节点,除了解析本轮工作流接收的文件外,还会把历史记录中的文档 URL 进行解析。最终会解析至多 n 个文档,n 取决于你配置文件上传时,允许的最大文件数量。 - -{{% alert icon="🤖" context="success" %}} -举例: - -配置了最多允许 5 个文件上传 - -1. 第一轮对话,上传 3 个文档和 1 个图片:文档解析节点,返回 3 个文档内容。 -2. 第二轮对话,不上传任何文件:文档解析节点,返回 3 个文档内容。 -3. 第三轮对话,上传 2 个文档:文档解析节点,返回 5 个文档内容。 -4. 第四轮对话,上传 1 个文档:文档解析节点,返回 5 个文档内容,第一轮对话中的第三个文档会被过滤掉。 - -{{% /alert %}} - +* **文档解析节点,只会解析本轮工作流接收的文件,不会解析历史记录的文件。** * 多个文档内容如何拼接的 按下列的模板,对多个文件进行拼接,即文件名+文件内容的形式组成一个字符串,不同文档之间通过分隔符:`\n******\n` 进行分割。 @@ -101,32 +87,27 @@ ${content} ``` -### 工具调用如何使用文档解析 +### AI节点中如何使用文档解析 -在工具调用中,文档解析节点的调用提示词为:`解析对话中所有上传的文档,并返回对应文档内容`。 +在 AI 节点(AI对话/工具调用)中,新增了一个文档链接的输入,可以直接引用文档的地址,从而实现文档内容的引用。 -作为工具被执行后,文档解析节点会返回解析后的文档内容作为工具响应。 - -### AI对话中如何使用文档解析 - -在 AI 对话节点中,新增了一个文档引用的输入,可以直接引用文档解析节点的输出,从而实现文档内容的引用。 - -它接收一个`string`类型的输入,除了可以引用文档解析结果外,还可以实现自定义内容引用,最终会进行提示词拼接,放置在 role=system 的消息中。提示词模板如下: +它接收一个`Array`类型的输入,最终这些 url 会被解析,并进行提示词拼接,放置在 role=system 的消息中。提示词模板如下: ``` -将 中的内容作为本次对话的参考: - +将 中的内容作为本次对话的参考: + {{quote}} - + ``` -quote 为引用的内容。 +# 4.8.13版本起,关于文件上传的更新 -![打开文件上传](/imgs/fileinpu-7.jpg) +由于与 4.8.9 版本有些差异,尽管我们做了向下兼容,避免工作流立即不可用。但是请尽快的按新版本规则进行调整工作流,后续将会去除兼容性代码。 -## 文件输入后续更新 - -* 插件支持配置文件输入。 -* 子应用和插件调用,支持传递文件输入。 -* 文档解析,结构化解析结果。 -* 更多的文件类型输入以及解析器。 \ No newline at end of file +1. 简易模式中,将会强制进行文件解析,不再由模型决策是否解析,保证每次都能参考文档。 +2. 文档解析:不再解析历史记录中的文件。 +3. 工具调用:支持直接选择文档引用,不需要再挂载文档解析工具。会自动解析历史记录中的文件。 +4. AI 对话:支持直接选择文档引用,不需要进过文档解析节点。会自动解析历史记录中的文件。 +5. 插件单独运行:不再支持全局文件;插件输入支持配置文件类型,可以取代全局文件上传。 +6. **工作流调用插件:不再自动传递工作流上传的文件到插件,需要手动给插件输入指定变量。** +7. **工作流调用工作流:不再自动传递工作流上传的文件到子工作流,可以手动选择需要传递的文件链接。** \ No newline at end of file diff --git a/docSite/content/zh-cn/docs/course/quick-start.md b/docSite/content/zh-cn/docs/guide/course/quick-start.md similarity index 99% rename from docSite/content/zh-cn/docs/course/quick-start.md rename to docSite/content/zh-cn/docs/guide/course/quick-start.md index 6f6397b33..f9e0ac475 100644 --- a/docSite/content/zh-cn/docs/course/quick-start.md +++ b/docSite/content/zh-cn/docs/guide/course/quick-start.md @@ -4,7 +4,7 @@ description: '快速体验 FastGPT 基础功能' icon: 'rocket_launch' draft: false toc: true -weight: 101 +weight: 102 --- 更多使用技巧,[查看视屏教程](https://www.bilibili.com/video/BV1sH4y1T7s9) diff --git a/docSite/content/zh-cn/docs/course/RAG.md b/docSite/content/zh-cn/docs/guide/knowledge_base/RAG.md similarity index 99% rename from docSite/content/zh-cn/docs/course/RAG.md rename to docSite/content/zh-cn/docs/guide/knowledge_base/RAG.md index ef312ce0a..ad6d877f7 100644 --- a/docSite/content/zh-cn/docs/course/RAG.md +++ b/docSite/content/zh-cn/docs/guide/knowledge_base/RAG.md @@ -4,7 +4,7 @@ description: '本节详细介绍RAG模型的核心机制、应用场景及其在 icon: 'language' draft: false toc: true -weight: 106 +weight: 402 --- [RAG文档](https://huggingface.co/docs/transformers/model_doc/rag) @@ -83,7 +83,7 @@ RAG(检索增强生成)模型通过结合检索器和生成器,实现了 RAG模型的性能很大程度上取决于检索器返回的文档质量。由于生成器主要依赖检索器提供的上下文信息,如果检索到的文档片段不相关、不准确,生成的文本可能出现偏差,甚至产生误导性的结果。尤其在多模糊查询或跨领域检索的情况下,检索器可能无法找到合适的片段,这将直接影响生成内容的连贯性和准确性。 - 挑战:当知识库庞大且内容多样时,如何提高检索器在复杂问题下的精确度是一大挑战。当前的方法如BM25等在特定任务上有局限,尤其是在面对语义模糊的查询时,传统的关键词匹配方式可能无法提供语义上相关的内容。 -- 解决途径:引入混合检索技术,如结合稀疏检索(BM25)与密集检索(如向量检索)。例如,[Faiss](https://fael3z0zfze.feishu.cn/wiki/LULawsUufitGvWkDjx3cKJqHnle?from=from_copylink)的底层实现允许通过BERT等模型生成密集向量表示,显著提升语义级别的匹配效果。通过这种方式,检索器可以捕捉深层次的语义相似性,减少无关文档对生成器的负面影响。 +- 解决途径:引入混合检索技术,如结合稀疏检索(BM25)与密集检索(如向量检索)。例如,Faiss的底层实现允许通过BERT等模型生成密集向量表示,显著提升语义级别的匹配效果。通过这种方式,检索器可以捕捉深层次的语义相似性,减少无关文档对生成器的负面影响。 #### 4.2.2 生成器的计算复杂度与性能瓶颈 diff --git a/docSite/content/zh-cn/docs/guide/knowledge_base/_index.md b/docSite/content/zh-cn/docs/guide/knowledge_base/_index.md new file mode 100644 index 000000000..61d5b9279 --- /dev/null +++ b/docSite/content/zh-cn/docs/guide/knowledge_base/_index.md @@ -0,0 +1,9 @@ +--- +weight: 400 +title: '知识库' +description: '知识库的基础原理、搜索方案、Web站点同步和外部文件知识库的使用方法。' +icon: 'book' +draft: false +images: [] +--- + \ No newline at end of file diff --git a/docSite/content/zh-cn/docs/course/dataset_engine.md b/docSite/content/zh-cn/docs/guide/knowledge_base/dataset_engine.md similarity index 99% rename from docSite/content/zh-cn/docs/course/dataset_engine.md rename to docSite/content/zh-cn/docs/guide/knowledge_base/dataset_engine.md index bac049397..969e31108 100644 --- a/docSite/content/zh-cn/docs/course/dataset_engine.md +++ b/docSite/content/zh-cn/docs/guide/knowledge_base/dataset_engine.md @@ -4,7 +4,7 @@ description: '本节会详细介绍 FastGPT 知识库结构设计,理解其 QA icon: 'language' draft: false toc: true -weight: 106 +weight: 404 --- ## 理解向量 diff --git a/docSite/content/zh-cn/docs/course/externalFile.md b/docSite/content/zh-cn/docs/guide/knowledge_base/externalFile.md similarity index 99% rename from docSite/content/zh-cn/docs/course/externalFile.md rename to docSite/content/zh-cn/docs/guide/knowledge_base/externalFile.md index b1c056526..f7b8dc9b3 100644 --- a/docSite/content/zh-cn/docs/course/externalFile.md +++ b/docSite/content/zh-cn/docs/guide/knowledge_base/externalFile.md @@ -4,7 +4,7 @@ description: 'FastGPT 外部文件知识库功能介绍和使用方式' icon: 'language' draft: false toc: true -weight: 107 +weight: 408 --- 外部文件库是 FastGPT 商业版特有功能。它允许接入你现在的文件系统,无需将文件再导入一份到 FastGPT 中。 diff --git a/docSite/content/zh-cn/docs/course/websync.md b/docSite/content/zh-cn/docs/guide/knowledge_base/websync.md similarity index 99% rename from docSite/content/zh-cn/docs/course/websync.md rename to docSite/content/zh-cn/docs/guide/knowledge_base/websync.md index 8374f068c..1f4bf2bfc 100644 --- a/docSite/content/zh-cn/docs/course/websync.md +++ b/docSite/content/zh-cn/docs/guide/knowledge_base/websync.md @@ -4,7 +4,7 @@ description: 'FastGPT Web 站点同步功能介绍和使用方式' icon: 'language' draft: false toc: true -weight: 105 +weight: 406 --- ![](/imgs/webSync1.jpg) diff --git a/docSite/content/zh-cn/docs/guide/plugins/_index.md b/docSite/content/zh-cn/docs/guide/plugins/_index.md new file mode 100644 index 000000000..d192e5945 --- /dev/null +++ b/docSite/content/zh-cn/docs/guide/plugins/_index.md @@ -0,0 +1,9 @@ +--- +weight: 300 +title: '系统插件' +description: '介绍如何使用和提交系统插件,以及各插件的填写说明' +icon: 'extension' +draft: false +images: [] +--- + \ No newline at end of file diff --git a/docSite/content/zh-cn/docs/guide/plugins/bing_search_plugin.md b/docSite/content/zh-cn/docs/guide/plugins/bing_search_plugin.md new file mode 100644 index 000000000..25ae9521f --- /dev/null +++ b/docSite/content/zh-cn/docs/guide/plugins/bing_search_plugin.md @@ -0,0 +1,32 @@ +--- +title: "Bing 搜索插件填写说明" +description: "FastGPT Bing 搜索插件配置步骤详解" +icon: "bing_search" +draft: false +toc: true +weight: 306 +--- + +1. # 打开微软Azure官网,登陆账号 + +https://portal.azure.com/ + +![](/imgs/bing_search_plugin1.png) + +1. # 创建bing web搜索资源 + +搜索Bing Search v7,点击创建 + +https://portal.azure.com/#create/Microsoft.BingSearch + +![](/imgs/bing_search_plugin2.png) + +1. # 进入资源详情点击管理密钥 + +![](/imgs/bing_search_plugin3.png) + +# 4. 复制任意一个密钥填入插件输入 + +![](/imgs/bing_search_plugin4.png) + +![](/imgs/bing_search_plugin5.png) \ No newline at end of file diff --git a/docSite/content/zh-cn/docs/guide/plugins/doc2x_plugin_guide.md b/docSite/content/zh-cn/docs/guide/plugins/doc2x_plugin_guide.md new file mode 100644 index 000000000..e1e70570b --- /dev/null +++ b/docSite/content/zh-cn/docs/guide/plugins/doc2x_plugin_guide.md @@ -0,0 +1,26 @@ +--- +title: "Doc2x 插件填写说明" +description: "如何配置和使用 Doc2x 插件" +icon: "doc_transform" +draft: false +toc: true +weight: 308 +--- + +1. # 打开docx官网,创建账号,并复制 apikey + +https://doc2x.noedgeai.com/ + +![](/imgs/doc2x_plugin1.png) + +![](/imgs/doc2x_plugin2.png) + +1. # 填写apikey到fastgpt中 + +**工作流****中:** + +![](/imgs/doc2x_plugin3.png) + +**简易模式使用:** + +![](/imgs/doc2x_plugin4.png) \ No newline at end of file diff --git a/docSite/content/zh-cn/docs/guide/plugins/google_search_plugin_guide.md b/docSite/content/zh-cn/docs/guide/plugins/google_search_plugin_guide.md new file mode 100644 index 000000000..432e2cab0 --- /dev/null +++ b/docSite/content/zh-cn/docs/guide/plugins/google_search_plugin_guide.md @@ -0,0 +1,32 @@ +--- +title: "Google 搜索插件填写说明" +description: "FastGPT Google 搜索插件配置指南" +icon: "google_search" +draft: false +toc: true +weight: 304 +--- + +1. # 创建Google Custom Search Engine + +https://programmablesearchengine.google.com/ + +我们连到Custom Search Engine control panel 建立Search Engine + +![](/imgs/google_search_plugin1.png) + +取得搜索引擎的ID,即cx + +![](/imgs/google_search_plugin2.png) + +1. # 获取api key + +https://developers.google.com/custom-search/v1/overview?hl=zh-cn + +![](/imgs/google_search_plugin3.png) + +1. # 填入插件输入参数 + +将搜索引擎ID填入cx字段,api key填入key字段 + +![](/imgs/google_search_plugin4.png) \ No newline at end of file diff --git a/docSite/content/zh-cn/docs/guide/plugins/how_to_submit_system_plugin.md b/docSite/content/zh-cn/docs/guide/plugins/how_to_submit_system_plugin.md new file mode 100644 index 000000000..e8168bdee --- /dev/null +++ b/docSite/content/zh-cn/docs/guide/plugins/how_to_submit_system_plugin.md @@ -0,0 +1,118 @@ +--- +title: "如何提交系统插件" +description: "FastGPT 系统插件提交指南" +icon: "plugin_submission" +draft: false +toc: true +weight: 302 +--- + +> 如何向 FastGPT 社区提交系统插件 + +## 系统插件原则 + +- 尽可能的轻量简洁,以解决实际问题的工具为主 +- 不允许有密集 cpu 计算,不会占用大量内存占用或网络消耗 +- 不允许操作数据库 +- 不允许往固定的私人地址发送请求(不包含请求某些在线服务,例如 gapier, firecrawl等) +- 不允许使用私人包,可使用主流的开源包 + +## 什么插件可以合并 + +由于目前未采用按需安装的模式,合并进仓库的插件会全部展示给用户使用。 + +为了控制插件的质量以及避免数量过多带来的繁琐,并不是所有的插件都会被合并到开源仓库中,你可以提前 PR 与我们沟通插件的内容。 + +后续实现插件按需安装后,我们会允许更多的社区插件合入。 + +## 如何写一个系统插件 - 初步 + +FastGPT 系统插件和用户工作台的插件效果是一致的,所以你需要提前了解“插件”的定义和功能。 + +在 FastGPT 中,插件是一种特殊的工作流,它允许你将一个工作流封装起来,并自定义入口参数和出口参数,类似于代码里的 “子函数”。 + +1. ### 跑通 FastGPT dev 环境 + +需要在 dev 环境下执行下面的操作。 + +1. ### 在 FastGPT 工作台中,创建一个插件 + +选择基础模板即可。 + +![](/imgs/plugin_submission1.png) + +1. ### 创建系统插件配置 + +系统插件配置以及自定义代码,都会在 **packages/plugins** 目录下。 + +1. 在 **packages/plugins/src** 下,复制一份 **template** 目录,并修改名字。 +2. 打开目录里面的 template.json 文件,配置如下: +3. 目录还有一个 index.ts 文件,下文再提。 + +```TypeScript +{ + "author": "填写你的名字", + "version": "当前系统版本号", + "name": "插件名", + "avatar": "插件头像,需要配成 icon 格式。直接把 logo 图在 pr 评论区提交即可,我们会帮你加入。", + "intro": " 插件的描述,这个描述会影响工具调用", + "showStatus": false, // 是否在对话过程展示状态 + "weight": 10, // 排序权重,均默认 10 + + "isTool": true, // 是否作为工具调用节点 + "templateType": "tools", // 都填写 tools 即可,由官方来分类 + + "workflow": { // 这个对象先不管,待会直接粘贴导出的工作流即可 + "nodes": [], + "edges": [] + } +} +``` + +1. 打开 **packages/plugins/register** 文件,注册你的插件。在 list 数组中,加入一个你插件目录的名字,如下图的例子。如需构建插件组(带目录),可参考 DuckDuckGo 插件。 + +无需额外写代码的插件,直接放在 staticPluginList 内,需要在项目内额外写代码的,写在 packagePluginList 中。 + +![](/imgs/plugin_submission2.png) + +1. ### 完成工作流编排并测试 + +完成工作流编排后,可以点击右上角的发布,并在其他工作流中引入进行测试(此时属于团队插件)。 + +1. ### 复制配置到 template.json + +鼠标放置在左上角插件的头像和名称上,会出现对于下拉框操作,可以导出工作流配置。 + +导出的配置,会自动到剪切板,可以直接到 template.json 文件中粘贴使用,替换步骤 2 中,**workflow** 的值。 + +![](/imgs/plugin_submission3.png) + +1. ### 验证插件是否加载成功 + +刷新页面,打开系统插件,看其是否成功加载,并将其添加到工作流中使用。 + +![](/imgs/plugin_submission4.png) + +1. ### 提交 PR + +如果你觉得你的插件需要提交到开源仓库,可以通过 PR 形式向我们提交。 + +- 写清楚插件的介绍和功能 +- 配上插件运行的效果图 +- 插件参数填写说明,需要在 PR 中写清楚。例如,有些插件需要去某个提供商申请 key,需要附上对应的地址和教材,后续我们会加入到文档中。 + +## 写一个复杂的系统插件 - 进阶 + +这一章会介绍如何增加一些无法单纯通过编排实现的插件。因为可能需要用到网络请求或第三方包。 + +上一章提到,在插件的 **template** 目录下,还有一个 **index.ts** 文件,这个文件就是用来执行一些插件的代码的。你可以通过在 HTTP 节点中的 URL,填写插件的名字,即可触发该方法,下面以 **duckduckgo/search** 这个插件为例: + +![](/imgs/plugin_submission5.png) + +![](/imgs/plugin_submission6.png) + +![](/imgs/plugin_submission7.png) + +参考上面 3 张图片,当 HTTP 节点的 URL 为系统插件注册的名字时,该请求不会以 HTTP 形式发送,而是会请求到 index.ts 文件中的 main 方法。出入参则对应了 body 和自定义输出的字段名。 + +由于目前插件会默认插件输出均作为“工具调用”的结果,无法单独指定某些字段作为工具输出,所以,请避免插件的自定义输出携带大量说明字段。 \ No newline at end of file diff --git a/docSite/content/zh-cn/docs/guide/team_permissions/_index.md b/docSite/content/zh-cn/docs/guide/team_permissions/_index.md new file mode 100644 index 000000000..a85eed375 --- /dev/null +++ b/docSite/content/zh-cn/docs/guide/team_permissions/_index.md @@ -0,0 +1,9 @@ +--- +weight: 450 +title: '团队与权限' +description: '团队管理、成员组与权限设置,确保团队协作中的数据安全和权限分配合理。' +icon: 'group' +draft: false +images: [] +--- + \ No newline at end of file diff --git a/docSite/content/zh-cn/docs/guide/team_permissions/team_roles_permissions.md b/docSite/content/zh-cn/docs/guide/team_permissions/team_roles_permissions.md new file mode 100644 index 000000000..f76256a72 --- /dev/null +++ b/docSite/content/zh-cn/docs/guide/team_permissions/team_roles_permissions.md @@ -0,0 +1,8 @@ +--- +title: "团队&成员组&权限" +description: "如何管理 FastGPT 团队、成员组及权限设置" +icon: "group" +draft: false +toc: true +weight: 450 +--- \ No newline at end of file diff --git a/docSite/content/zh-cn/docs/guide/workbench/_index.md b/docSite/content/zh-cn/docs/guide/workbench/_index.md new file mode 100644 index 000000000..375fb800b --- /dev/null +++ b/docSite/content/zh-cn/docs/guide/workbench/_index.md @@ -0,0 +1,9 @@ +--- +weight: 200 +title: '工作台' +description: 'FastGPT 工作台及工作流节点的使用说明' +icon: 'dashboard' +draft: false +images: [] +--- + \ No newline at end of file diff --git a/docSite/content/zh-cn/docs/guide/workbench/basic-mode.md b/docSite/content/zh-cn/docs/guide/workbench/basic-mode.md new file mode 100644 index 000000000..8fb4353d4 --- /dev/null +++ b/docSite/content/zh-cn/docs/guide/workbench/basic-mode.md @@ -0,0 +1,8 @@ +--- +weight: 202 +title: '简易模式' +description: '快速了解 FastGPT 工作台的简易模式' +icon: 'speed' +draft: false +images: [] +--- \ No newline at end of file diff --git a/docSite/content/zh-cn/docs/use-cases/gapier.md b/docSite/content/zh-cn/docs/guide/workbench/gapier.md similarity index 99% rename from docSite/content/zh-cn/docs/use-cases/gapier.md rename to docSite/content/zh-cn/docs/guide/workbench/gapier.md index 0a38e6860..043a3a936 100644 --- a/docSite/content/zh-cn/docs/use-cases/gapier.md +++ b/docSite/content/zh-cn/docs/guide/workbench/gapier.md @@ -4,7 +4,7 @@ description: "FastGPT 使用 Gapier 快速导入Agent工具" icon: "build" draft: false toc: true -weight: 501 +weight: 300 --- FastGPT V4.7版本加入了工具调用,可以兼容 GPTs 的 Actions。这意味着,你可以直接导入兼容 GPTs 的 Agent 工具。 diff --git a/docSite/content/zh-cn/docs/workflow/intro.md b/docSite/content/zh-cn/docs/guide/workbench/intro.md similarity index 97% rename from docSite/content/zh-cn/docs/workflow/intro.md rename to docSite/content/zh-cn/docs/guide/workbench/intro.md index 43f90669c..e5d8b1c5c 100644 --- a/docSite/content/zh-cn/docs/workflow/intro.md +++ b/docSite/content/zh-cn/docs/guide/workbench/intro.md @@ -1,10 +1,10 @@ --- -title: "高级编排介绍" -description: "快速了解 FastGPT 高级编排" -icon: "circle" +title: "工作流&插件" +description: "快速了解 FastGPT 工作流和插件的使用" +icon: "extension" draft: false toc: true -weight: 301 +weight: 220 --- FastGPT 从 V4 版本开始采用新的交互方式来构建 AI 应用。使用了 Flow 节点编排(工作流)的方式来实现复杂工作流,提高可玩性和扩展性。但同时也提高了上手的门槛,有一定开发背景的用户使用起来会比较容易。 diff --git a/docSite/content/zh-cn/docs/guide/workbench/workflow/_index.md b/docSite/content/zh-cn/docs/guide/workbench/workflow/_index.md new file mode 100644 index 000000000..d361afe8a --- /dev/null +++ b/docSite/content/zh-cn/docs/guide/workbench/workflow/_index.md @@ -0,0 +1,9 @@ +--- +weight: 230 +title: '工作流节点' +description: 'FastGPT 工作流节点设置和使用指南' +icon: 'workflow' +draft: false +images: [] +--- + diff --git a/docSite/content/zh-cn/docs/workflow/modules/ai_chat.md b/docSite/content/zh-cn/docs/guide/workbench/workflow/ai_chat.md similarity index 93% rename from docSite/content/zh-cn/docs/workflow/modules/ai_chat.md rename to docSite/content/zh-cn/docs/guide/workbench/workflow/ai_chat.md index 7f982998f..8ee015a13 100644 --- a/docSite/content/zh-cn/docs/workflow/modules/ai_chat.md +++ b/docSite/content/zh-cn/docs/guide/workbench/workflow/ai_chat.md @@ -4,7 +4,7 @@ description: "FastGPT AI 对话模块介绍" icon: "chat" draft: false toc: true -weight: 351 +weight: 232 --- ## 特点 @@ -30,5 +30,5 @@ weight: 351 {{% alert icon="🍅" context="success" %}} -具体配置参数介绍可以参考: [AI参数配置说明](/docs/course/ai_settings) +具体配置参数介绍可以参考: [AI参数配置说明](/docs/guide/course/ai_settings/) {{% /alert %}} \ No newline at end of file diff --git a/docSite/content/zh-cn/docs/workflow/modules/content_extract.md b/docSite/content/zh-cn/docs/guide/workbench/workflow/content_extract.md similarity index 97% rename from docSite/content/zh-cn/docs/workflow/modules/content_extract.md rename to docSite/content/zh-cn/docs/guide/workbench/workflow/content_extract.md index 78677a1de..17eebfb2f 100644 --- a/docSite/content/zh-cn/docs/workflow/modules/content_extract.md +++ b/docSite/content/zh-cn/docs/guide/workbench/workflow/content_extract.md @@ -1,10 +1,10 @@ --- -title: "内容提取" +title: "文本内容提取" description: "FastGPT 内容提取模块介绍" icon: "content_paste_go" draft: false toc: true -weight: 352 +weight: 240 --- ## 特点 diff --git a/docSite/content/zh-cn/docs/workflow/modules/coreferenceResolution.md b/docSite/content/zh-cn/docs/guide/workbench/workflow/coreferenceResolution.md similarity index 91% rename from docSite/content/zh-cn/docs/workflow/modules/coreferenceResolution.md rename to docSite/content/zh-cn/docs/guide/workbench/workflow/coreferenceResolution.md index e8a06e4c2..108f54c3a 100644 --- a/docSite/content/zh-cn/docs/workflow/modules/coreferenceResolution.md +++ b/docSite/content/zh-cn/docs/guide/workbench/workflow/coreferenceResolution.md @@ -1,10 +1,10 @@ --- -title: "问题优化(已合并到知识库搜索)" +title: "问题优化" description: "问题优化模块介绍和使用" icon: "input" draft: false toc: true -weight: 364 +weight: 264 --- ## 特点 @@ -36,4 +36,4 @@ weight: 364 ## 示例 -- [接入谷歌搜索](/docs/workflow/examples/google_search/) \ No newline at end of file +- [接入谷歌搜索](/docs/use-cases/app-cases/google_search/) \ No newline at end of file diff --git a/docSite/content/zh-cn/docs/workflow/modules/custom_feedback.md b/docSite/content/zh-cn/docs/guide/workbench/workflow/custom_feedback.md similarity index 98% rename from docSite/content/zh-cn/docs/workflow/modules/custom_feedback.md rename to docSite/content/zh-cn/docs/guide/workbench/workflow/custom_feedback.md index c6733e95b..cec0bbf04 100644 --- a/docSite/content/zh-cn/docs/workflow/modules/custom_feedback.md +++ b/docSite/content/zh-cn/docs/guide/workbench/workflow/custom_feedback.md @@ -4,7 +4,7 @@ description: "自定义反馈模块介绍" icon: "feedback" draft: false toc: true -weight: 354 +weight: 268 --- 该模块为临时模块,后续会针对该模块进行更全面的设计。 diff --git a/docSite/content/zh-cn/docs/workflow/modules/dataset_search.md b/docSite/content/zh-cn/docs/guide/workbench/workflow/dataset_search.md similarity index 98% rename from docSite/content/zh-cn/docs/workflow/modules/dataset_search.md rename to docSite/content/zh-cn/docs/guide/workbench/workflow/dataset_search.md index ecdb88200..df8767de8 100644 --- a/docSite/content/zh-cn/docs/workflow/modules/dataset_search.md +++ b/docSite/content/zh-cn/docs/guide/workbench/workflow/dataset_search.md @@ -4,7 +4,7 @@ description: 'FastGPT AI 知识库搜索模块介绍' icon: 'chat' draft: false toc: true -weight: 357 +weight: 234 --- 知识库搜索具体参数说明,以及内部逻辑请移步:[FastGPT知识库搜索方案](/docs/course/data_search/) diff --git a/docSite/content/zh-cn/docs/guide/workbench/workflow/document_parsing.md b/docSite/content/zh-cn/docs/guide/workbench/workflow/document_parsing.md new file mode 100644 index 000000000..1d84b385a --- /dev/null +++ b/docSite/content/zh-cn/docs/guide/workbench/workflow/document_parsing.md @@ -0,0 +1,20 @@ +--- +title: "文档解析" +description: "FastGPT 文档解析模块介绍" +icon: "document_analysis" +draft: false +toc: true +weight: 250 +--- + +
+ 文档解析示例 1 + 文档解析示例 2 +
+ + +开启文件上传后,可使用文档解析组件。 + +## 功能 + +## 作用 \ No newline at end of file diff --git a/docSite/content/zh-cn/docs/guide/workbench/workflow/form_input.md b/docSite/content/zh-cn/docs/guide/workbench/workflow/form_input.md new file mode 100644 index 000000000..7fcb28c7d --- /dev/null +++ b/docSite/content/zh-cn/docs/guide/workbench/workflow/form_input.md @@ -0,0 +1,8 @@ +--- +title: "表单输入" +description: "FastGPT 表单输入模块介绍" +icon: "form_input" +draft: false +toc: true +weight: 244 +--- \ No newline at end of file diff --git a/docSite/content/zh-cn/docs/workflow/modules/http.md b/docSite/content/zh-cn/docs/guide/workbench/workflow/http.md similarity index 96% rename from docSite/content/zh-cn/docs/workflow/modules/http.md rename to docSite/content/zh-cn/docs/guide/workbench/workflow/http.md index 0285bf849..32605409e 100644 --- a/docSite/content/zh-cn/docs/workflow/modules/http.md +++ b/docSite/content/zh-cn/docs/guide/workbench/workflow/http.md @@ -1,10 +1,10 @@ --- -title: "HTTP 模块" +title: "HTTP 请求" description: "FastGPT HTTP 模块介绍" icon: "http" draft: false toc: true -weight: 355 +weight: 252 --- ## 特点 @@ -250,6 +250,6 @@ export default async function (ctx: FunctionContext) { ## 相关示例 -- [谷歌搜索](/docs/workflow/examples/google_search/) -- [发送飞书webhook](/docs/workflow/examples/feishu_webhook/) -- [实验室预约(操作数据库)](/docs/workflow/examples/lab_appointment/) +- [谷歌搜索](/docs/use-cases/app-cases/google_search/) +- [发送飞书webhook](/docs/use-cases/app-cases/feishu_webhook/) +- [实验室预约(操作数据库)](/docs/use-cases/app-cases/lab_appointment/) diff --git a/docSite/content/zh-cn/docs/guide/workbench/workflow/knowledge_base_search_merge.md b/docSite/content/zh-cn/docs/guide/workbench/workflow/knowledge_base_search_merge.md new file mode 100644 index 000000000..eafccd769 --- /dev/null +++ b/docSite/content/zh-cn/docs/guide/workbench/workflow/knowledge_base_search_merge.md @@ -0,0 +1,29 @@ +--- +title: "知识库搜索引用合并" +description: "FastGPT 知识库搜索引用合并模块介绍" +icon: "knowledge_merge" +draft: false +toc: true +weight: 262 +--- + + +![](/imgs/knowledge_merge1.png) + +## 作用 + +将多个知识库搜索结果合并成一个结果进行输出,并会通过 RRF 进行重新排序(根据排名情况),并且支持最大 tokens 过滤。 + +## 使用方法 + +AI对话只能接收一个知识库引用内容。因此,如果调用了多个知识库,无法直接引用所有知识库(如下图) + +![](/imgs/knowledge_merge2.png) + +使用**知识库搜索引用合并**,可以把多个知识库的搜索结果合在一起。 + +![](/imgs/knowledge_merge3.png) + +## 可用例子: + +1. 经过问题分类后对不同知识库进行检索,然后统一给一个 AI 进行回答,此时可以用到合并,不需要每个分支都添加一个 AI 对话。 \ No newline at end of file diff --git a/docSite/content/zh-cn/docs/workflow/modules/laf.md b/docSite/content/zh-cn/docs/guide/workbench/workflow/laf.md similarity index 99% rename from docSite/content/zh-cn/docs/workflow/modules/laf.md rename to docSite/content/zh-cn/docs/guide/workbench/workflow/laf.md index 943e7ba4e..71b2d622e 100644 --- a/docSite/content/zh-cn/docs/workflow/modules/laf.md +++ b/docSite/content/zh-cn/docs/guide/workbench/workflow/laf.md @@ -4,7 +4,7 @@ description: "FastGPT Laf 函数调用模块介绍" icon: "code" draft: false toc: true -weight: 355 +weight: 266 --- diff --git a/docSite/content/zh-cn/docs/workflow/modules/loop.md b/docSite/content/zh-cn/docs/guide/workbench/workflow/loop.md similarity index 99% rename from docSite/content/zh-cn/docs/workflow/modules/loop.md rename to docSite/content/zh-cn/docs/guide/workbench/workflow/loop.md index 41f29fd94..7b32a91cd 100644 --- a/docSite/content/zh-cn/docs/workflow/modules/loop.md +++ b/docSite/content/zh-cn/docs/guide/workbench/workflow/loop.md @@ -1,10 +1,10 @@ --- -title: "循环运行" +title: "循环执行" description: "FastGPT 循环运行节点介绍和使用" icon: "input" draft: false toc: true -weight: 366 +weight: 260 --- ## 节点概述 diff --git a/docSite/content/zh-cn/docs/workflow/modules/question_classify.md b/docSite/content/zh-cn/docs/guide/workbench/workflow/question_classify.md similarity index 99% rename from docSite/content/zh-cn/docs/workflow/modules/question_classify.md rename to docSite/content/zh-cn/docs/guide/workbench/workflow/question_classify.md index c017238e3..54db28b1e 100644 --- a/docSite/content/zh-cn/docs/workflow/modules/question_classify.md +++ b/docSite/content/zh-cn/docs/guide/workbench/workflow/question_classify.md @@ -4,7 +4,7 @@ description: "FastGPT 问题分类模块介绍" icon: "quiz" draft: false toc: true -weight: 358 +weight: 238 --- ## 特点 diff --git a/docSite/content/zh-cn/docs/workflow/modules/reply.md b/docSite/content/zh-cn/docs/guide/workbench/workflow/reply.md similarity index 97% rename from docSite/content/zh-cn/docs/workflow/modules/reply.md rename to docSite/content/zh-cn/docs/guide/workbench/workflow/reply.md index b6d4b7071..2cfdbdfae 100644 --- a/docSite/content/zh-cn/docs/workflow/modules/reply.md +++ b/docSite/content/zh-cn/docs/guide/workbench/workflow/reply.md @@ -4,7 +4,7 @@ description: "FastGPT 指定回复模块介绍" icon: "reply" draft: false toc: true -weight: 359 +weight: 248 --- ## 特点 diff --git a/docSite/content/zh-cn/docs/workflow/modules/sandbox.md b/docSite/content/zh-cn/docs/guide/workbench/workflow/sandbox.md similarity index 99% rename from docSite/content/zh-cn/docs/workflow/modules/sandbox.md rename to docSite/content/zh-cn/docs/guide/workbench/workflow/sandbox.md index 5c968412f..4a6a7aad7 100644 --- a/docSite/content/zh-cn/docs/workflow/modules/sandbox.md +++ b/docSite/content/zh-cn/docs/guide/workbench/workflow/sandbox.md @@ -4,7 +4,7 @@ description: "FastGPT 代码运行节点介绍" icon: "input" draft: false toc: true -weight: 364 +weight: 258 --- ![alt text](/imgs/image.png) diff --git a/docSite/content/zh-cn/docs/workflow/modules/text_editor.md b/docSite/content/zh-cn/docs/guide/workbench/workflow/text_editor.md similarity index 84% rename from docSite/content/zh-cn/docs/workflow/modules/text_editor.md rename to docSite/content/zh-cn/docs/guide/workbench/workflow/text_editor.md index c1ca7b761..f7bd8f7a4 100644 --- a/docSite/content/zh-cn/docs/workflow/modules/text_editor.md +++ b/docSite/content/zh-cn/docs/guide/workbench/workflow/text_editor.md @@ -1,10 +1,10 @@ --- -title: "文本加工" +title: "文本拼接" description: "FastGPT 文本加工模块介绍" icon: "input" draft: false toc: true -weight: 363 +weight: 246 --- ## 特点 @@ -29,4 +29,4 @@ weight: 363 ## 示例 -- [接入谷歌搜索](/docs/workflow/examples/google_search/) \ No newline at end of file +- [接入谷歌搜索](/docs/use-cases/app-cases/google_search/) \ No newline at end of file diff --git a/docSite/content/zh-cn/docs/workflow/modules/tfswitch.md b/docSite/content/zh-cn/docs/guide/workbench/workflow/tfswitch.md similarity index 98% rename from docSite/content/zh-cn/docs/workflow/modules/tfswitch.md rename to docSite/content/zh-cn/docs/guide/workbench/workflow/tfswitch.md index 9cb7a3983..2255a0506 100644 --- a/docSite/content/zh-cn/docs/workflow/modules/tfswitch.md +++ b/docSite/content/zh-cn/docs/guide/workbench/workflow/tfswitch.md @@ -4,7 +4,7 @@ description: "FastGPT 判断器模块介绍" icon: "input" draft: false toc: true -weight: 362 +weight: 254 --- ## 特点 diff --git a/docSite/content/zh-cn/docs/workflow/modules/tool.md b/docSite/content/zh-cn/docs/guide/workbench/workflow/tool.md similarity index 95% rename from docSite/content/zh-cn/docs/workflow/modules/tool.md rename to docSite/content/zh-cn/docs/guide/workbench/workflow/tool.md index ea3b1e82a..4508b3079 100644 --- a/docSite/content/zh-cn/docs/workflow/modules/tool.md +++ b/docSite/content/zh-cn/docs/guide/workbench/workflow/tool.md @@ -1,10 +1,10 @@ --- -title: "工具调用" +title: "工具调用&终止" description: "FastGPT 工具调用模块介绍" icon: "build" draft: false toc: true -weight: 356 +weight: 236 --- ![](/imgs/flow-tool1.png) @@ -67,5 +67,5 @@ weight: 356 ## 相关示例 -- [谷歌搜索](/docs/workflow/examples/google_search/) -- [发送飞书webhook](/docs/workflow/examples/feishu_webhook/) \ No newline at end of file +- [谷歌搜索](/docs/use-cases/app-cases/google_search/) +- [发送飞书webhook](/docs/use-cases/app-cases/feishu_webhook/) \ No newline at end of file diff --git a/docSite/content/zh-cn/docs/guide/workbench/workflow/user-selection.md b/docSite/content/zh-cn/docs/guide/workbench/workflow/user-selection.md new file mode 100644 index 000000000..ef7a6490e --- /dev/null +++ b/docSite/content/zh-cn/docs/guide/workbench/workflow/user-selection.md @@ -0,0 +1,32 @@ +--- +title: "用户选择" +description: "FastGPT 用户选择模块的使用说明" +icon: "user_check" +draft: false +toc: true +weight: 242 +--- + +## 特点 + +- 用户交互 +- 可重复添加 +- 触发执行 + +![](/imgs/user-selection1.png) + +## 功能 + +「用户选择」节点属于用户交互节点,当触发这个节点时,对话会进入“交互”状态,会记录工作流的状态,等用户完成交互后,继续向下执行工作流 + +![](/imgs/user-selection2.png) + +比如上图中的例子,当触发用户选择节点时,对话框隐藏,对话进入“交互状态” + +![](/imgs/user-selection3.png) + +当用户做出选择时,节点会判断用户的选择,执行“是”的分支 + +## 作用 + +基础的用法为提出需要用户做抉择的问题,然后根据用户的反馈设计不同的工作流流程 \ No newline at end of file diff --git a/docSite/content/zh-cn/docs/guide/workbench/workflow/variable_update.md b/docSite/content/zh-cn/docs/guide/workbench/workflow/variable_update.md new file mode 100644 index 000000000..e0c0be108 --- /dev/null +++ b/docSite/content/zh-cn/docs/guide/workbench/workflow/variable_update.md @@ -0,0 +1,38 @@ +--- +title: "变量更新" +description: "FastGPT 变量更新模块介绍" +icon: "variable_update" +draft: false +toc: true +weight: 256 +--- + +## 特点 + +- 可重复添加 +- 有外部输入 +- 触发执行 +- 手动配置 + +![](/imgs/variable_update1.png) + +## 功能 + +- 更新指定节点的输出值 + +![](/imgs/variable_update2.png) + +![](/imgs/variable_update3.png) + +- 更新全局变量 + +![](/imgs/variable_update4.png) + +![](/imgs/variable_update5.png) + +## 作用 + +最基础的使用场景为 + +- 给一个「自定义变量」类型的全局变量赋值,从而实现全局变量无需用户输入 +- 更新「变量更新」节点前的工作流节点输出,在后续使用中,使用的节点输出值为新的输出 \ No newline at end of file diff --git a/docSite/content/zh-cn/docs/commercial/_index.md b/docSite/content/zh-cn/docs/shopping_cart/_index.md similarity index 100% rename from docSite/content/zh-cn/docs/commercial/_index.md rename to docSite/content/zh-cn/docs/shopping_cart/_index.md diff --git a/docSite/content/zh-cn/docs/commercial/intro.md b/docSite/content/zh-cn/docs/shopping_cart/intro.md similarity index 99% rename from docSite/content/zh-cn/docs/commercial/intro.md rename to docSite/content/zh-cn/docs/shopping_cart/intro.md index 4012972ec..70cd02531 100644 --- a/docSite/content/zh-cn/docs/commercial/intro.md +++ b/docSite/content/zh-cn/docs/shopping_cart/intro.md @@ -4,7 +4,7 @@ description: 'FastGPT 商业版相关说明' icon: 'shopping_cart' draft: false toc: true -weight: 1001 +weight: 1104 --- ## 简介 diff --git a/docSite/content/zh-cn/docs/commercial/saas.md b/docSite/content/zh-cn/docs/shopping_cart/saas.md similarity index 95% rename from docSite/content/zh-cn/docs/commercial/saas.md rename to docSite/content/zh-cn/docs/shopping_cart/saas.md index 6dcf3070d..f3c1d29cc 100644 --- a/docSite/content/zh-cn/docs/commercial/saas.md +++ b/docSite/content/zh-cn/docs/shopping_cart/saas.md @@ -4,7 +4,7 @@ description: 'FastGPT 线上版定价' icon: 'currency_yen' draft: false toc: true -weight: 1002 +weight: 1102 type: redirect target: https://cloud.tryfastgpt.ai/price --- diff --git a/docSite/content/zh-cn/docs/use-cases/app-cases/_index.md b/docSite/content/zh-cn/docs/use-cases/app-cases/_index.md new file mode 100644 index 000000000..84e64c71d --- /dev/null +++ b/docSite/content/zh-cn/docs/use-cases/app-cases/_index.md @@ -0,0 +1,8 @@ +--- +title: "应用搭建案例" +description: "FastGPT 应用场景及功能实现的搭建案例" +icon: "construction" +draft: false +weight: 600 +--- + diff --git a/docSite/content/zh-cn/docs/workflow/examples/dalle3.md b/docSite/content/zh-cn/docs/use-cases/app-cases/dalle3.md similarity index 99% rename from docSite/content/zh-cn/docs/workflow/examples/dalle3.md rename to docSite/content/zh-cn/docs/use-cases/app-cases/dalle3.md index d3f3c9a1f..7c214f8ae 100644 --- a/docSite/content/zh-cn/docs/workflow/examples/dalle3.md +++ b/docSite/content/zh-cn/docs/use-cases/app-cases/dalle3.md @@ -4,7 +4,7 @@ description: '使用 HTTP 模块绘制图片' icon: 'image' draft: false toc: true -weight: 408 +weight: 614 --- | | | diff --git a/docSite/content/zh-cn/docs/use-cases/app-cases/english_essay_correction_bot.md b/docSite/content/zh-cn/docs/use-cases/app-cases/english_essay_correction_bot.md new file mode 100644 index 000000000..1f11c8f5b --- /dev/null +++ b/docSite/content/zh-cn/docs/use-cases/app-cases/english_essay_correction_bot.md @@ -0,0 +1,97 @@ +--- +title: "英语作文纠错机器人" +description: "使用 FastGPT 创建一个用于英语作文纠错的机器人,帮助用户检测并纠正语言错误" +icon: "spellcheck" +draft: false +toc: true +weight: 608 +--- + +FastGPT 提供了一种基于 LLM Model 搭建应用的简便方式。 + +本文通过搭建一个英语作文纠错机器人,介绍一下如何使用 **工作流** + +## 搭建过程 + +### 1. 创建工作流 + +![](/imgs/spellcheck1.png) + +可以从 *多轮翻译机器人* 开始创建。 + +> 多轮翻译机器人是 @米开朗基杨 同学创建的,同样也是一个值得学习的工作流。 + +### 2. 获取输入,使用大模型进行分析 + +我们期望让大模型处理文字,返回一个结构化的数据,由我们自己处理。 + +![](/imgs/spellcheck2.png) + +**提示词** 是最重要的一个参数,这里提供的提示词仅供参考: + +~~~Markdown +## 角色 +资深英语写作专家 + +## 任务 +对输入的原文进行分析。 找出其中的各种错误, 包括但不限于单词拼写错误、 语法错误等。 +注意: 忽略标点符号前后空格的问题。 +注意: 对于存在错误的句子, 提出修改建议是指指出这个句子中的具体部分, 然后提出将这一个部分修改替换为什么。 + +## 输出格式 +不要使用 Markdown 语法, 输入 JSON 格式的内容。 +输出的"reason"的内容使用中文。 +直接输出一个列表, 其成员为一个相同类型的对象, 定义如下 +您正在找回 FastGPT 账号 +``` +{ +“raw”: string; // 表示原文 +“reason”: string; // 表示原因 +“suggestion”: string; // 修改建议 +} +``` +~~~ + +可以在模型选择的窗口中设置禁用 AI 回复。 + +这样就看不到输出的 json 格式的内容了。 + +![](/imgs/spellcheck3.png) + +### 3. 数据处理 + +上面的大模型输出了一个 json,这里要进行数据处理。数据处理可以使用代码执行组件。 + +![](/imgs/spellcheck4.png) + +```JavaScript +function main({data}){ + const array = JSON.parse(data) + return { + content: array.map( + (item, index) => { + return ` +## 分析${index+1} +- **错误**: ${item.raw} +- **分析**: ${item.reason} +- **修改建议**: ${item.suggestion} +` + } + ).join('') + } +} +``` + +上面的代码将 JSON 解析为 Object, 然后拼接成一串 Markdown 语法的字符串。 + +FastGPT 的指定回复组件可以将 Markdown 解析为 Html 返回。 + +## 发布 + +可以使用发布渠道进行发布。 + +![](/imgs/spellcheck5.png) + +可以选择通过 URL 访问,或者是直接嵌入你的网页中。 + +> [点我使用](https://share.fastgpt.in/chat/share?shareId=b4r173wkcjae7wpnexcvmyc3) \ No newline at end of file diff --git a/docSite/content/zh-cn/docs/workflow/examples/feishu_webhook.md b/docSite/content/zh-cn/docs/use-cases/app-cases/feishu_webhook.md similarity index 99% rename from docSite/content/zh-cn/docs/workflow/examples/feishu_webhook.md rename to docSite/content/zh-cn/docs/use-cases/app-cases/feishu_webhook.md index 43b055ba6..67f588c02 100644 --- a/docSite/content/zh-cn/docs/workflow/examples/feishu_webhook.md +++ b/docSite/content/zh-cn/docs/use-cases/app-cases/feishu_webhook.md @@ -4,7 +4,7 @@ description: '利用工具调用模块,发送一个飞书webhook通知' icon: 'image' draft: false toc: true -weight: 409 +weight: 618 --- 该文章展示如何发送一个简单的飞书webhook通知,以此类推,发送其他类型的通知也可以这么操作。 diff --git a/docSite/content/zh-cn/docs/workflow/examples/fixingEvidence.md b/docSite/content/zh-cn/docs/use-cases/app-cases/fixingEvidence.md similarity index 99% rename from docSite/content/zh-cn/docs/workflow/examples/fixingEvidence.md rename to docSite/content/zh-cn/docs/use-cases/app-cases/fixingEvidence.md index ddb74d24e..6a8f08542 100644 --- a/docSite/content/zh-cn/docs/workflow/examples/fixingEvidence.md +++ b/docSite/content/zh-cn/docs/use-cases/app-cases/fixingEvidence.md @@ -4,7 +4,7 @@ description: '利用指定回复,创建固定的开头和结尾' icon: 'healing' draft: false toc: true -weight: 405 +weight: 610 --- ![](/imgs/demo-fix-evidence1.jpg) diff --git a/docSite/content/zh-cn/docs/workflow/examples/google_search.md b/docSite/content/zh-cn/docs/use-cases/app-cases/google_search.md similarity index 99% rename from docSite/content/zh-cn/docs/workflow/examples/google_search.md rename to docSite/content/zh-cn/docs/use-cases/app-cases/google_search.md index 9f01f4fd4..66f0f1152 100644 --- a/docSite/content/zh-cn/docs/workflow/examples/google_search.md +++ b/docSite/content/zh-cn/docs/use-cases/app-cases/google_search.md @@ -4,7 +4,7 @@ description: '将 FastGPT 接入谷歌搜索' icon: 'search' draft: false toc: true -weight: 406 +weight: 616 --- | | | diff --git a/docSite/content/zh-cn/docs/workflow/examples/lab_appointment.md b/docSite/content/zh-cn/docs/use-cases/app-cases/lab_appointment.md similarity index 99% rename from docSite/content/zh-cn/docs/workflow/examples/lab_appointment.md rename to docSite/content/zh-cn/docs/use-cases/app-cases/lab_appointment.md index b4b5bd5e4..e2f974e56 100644 --- a/docSite/content/zh-cn/docs/workflow/examples/lab_appointment.md +++ b/docSite/content/zh-cn/docs/use-cases/app-cases/lab_appointment.md @@ -4,7 +4,7 @@ description: '展示高级编排操作数据库的能力' icon: 'database' draft: false toc: true -weight: 407 +weight: 612 --- | | | diff --git a/docSite/content/zh-cn/docs/use-cases/app-cases/multi_turn_translation_bot.md b/docSite/content/zh-cn/docs/use-cases/app-cases/multi_turn_translation_bot.md new file mode 100644 index 000000000..a3f51303b --- /dev/null +++ b/docSite/content/zh-cn/docs/use-cases/app-cases/multi_turn_translation_bot.md @@ -0,0 +1,315 @@ +--- +title: "多轮翻译机器人" +description: "如何使用 FastGPT 构建一个多轮翻译机器人,实现连续的对话翻译功能" +icon: "translate" +draft: false +toc: true +weight: 606 +--- + +吴恩达老师提出了一种反思翻译的大语言模型(LLM)翻译工作流程——[GitHub - andrewyng/translation-agent](https://github.com/andrewyng/translation-agent),具体工作流程如下: + +1. 提示一个 LLM 将文本从 `source_language` 翻译到 `target_language`; +2. 让 LLM 反思翻译结果并提出建设性的改进建议; +3. 使用这些建议来改进翻译。 + +这个翻译流程应该是目前比较新的一种翻译方式,利用 LLM 对自己的翻译结果进行改进来获得较好的翻译效果 + +项目中展示了可以利用对长文本进行分片,然后分别进行反思翻译处理,以突破 LLM 对 tokens 数量的限制,真正实现长文本一键高效率高质量翻译。 + +项目还通过给大模型限定国家地区,已实现更精确的翻译,如美式英语、英式英语之分;同时提出一些可能能带来更好效果的优化,如对于一些 LLM 未曾训练到的术语(或有多种翻译方式的术语)建立术语表,进一步提升翻译的精确度等等 + +而这一切都能通过 Fastgpt 工作流轻松实现,本文将手把手教你如何复刻吴恩达老师的 translation-agent + +# 单文本块反思翻译 + +先从简单的开始,即不超出 LLM tokens 数量限制的单文本块翻译 + +## 初始翻译 + +第一步先让 LLM 对源文本块进行初始翻译(翻译的提示词在源项目中都有) + +![](/imgs/translate1.png) + +通过`文本拼接`模块引用 源语言、目标语言、源文本这三个参数,生成提示词,传给 LLM,让它给出第一版的翻译 + +## 反思 + +然后让 LLM 对第一步生成的初始翻译给出修改建议,称之为 反思 + +![](/imgs/translate2.png) + +这时的提示词接收 5 个参数,源文本、初始翻译、源语言、目标语言 以及限定词地区国家,这样 LLM 会对前面生成的翻译提出相当多的修改建议,为后续的提升翻译作准备 + +## 提升翻译 + +![](/imgs/translate3.png) + +在前文生成了初始翻译以及相应的反思后,将这二者输入给第三次 LLM 翻译,这样我们就能获得一个比较高质量的翻译结果 + +完整的工作流如下 + +![](/imgs/translate4.png) + +## 运行效果 + +由于考虑之后对这个反思翻译的复用,所以创建了一个插件,那么在下面我直接调用这个插件就能使用反思翻译,效果如下 + +随机挑选了一段哈利波特的文段 + +![](/imgs/translate5.png) + +![](/imgs/translate6.png) + +可以看到反思翻译后的效果还是好上不少的,其中反思的输出如下 + +![](/imgs/translate7.png) + +# 长文反思翻译 + +在掌握了对短文本块的反思翻译后,我们能轻松的通过分片和循环,实现对长文本也即多文本块的反思翻译 + +整体的逻辑是,首先对传入文本的 tokens数量做判断,如果不超过设置的 tokens 限制,那么直接调用单文本块反思翻译,如果超过设置的 tokens限制,那么切割为合理的大小,再分别进行对应的反思翻译处理 + +## 计算 tokens + +![](/imgs/translate8.png) + +首先,我使用了 Laf函数 模块来实现对输入文本的 tokens 的计算 + +laf函数的使用相当简单,即开即用,只需要在 laf 创建个应用,然后安装 tiktoken 依赖,导入如下代码即可 + +```TypeScript +const { Tiktoken } = require("tiktoken/lite"); +const cl100k_base = require("tiktoken/encoders/cl100k_base.json"); + +interface IRequestBody { + str: string +} + +interface RequestProps extends IRequestBody { + systemParams: { + appId: string, + variables: string, + histories: string, + cTime: string, + chatId: string, + responseChatItemId: string + } +} + +interface IResponse { + message: string; + tokens: number; +} + +export default async function (ctx: FunctionContext): Promise { + const { str = "" }: RequestProps = ctx.body + + const encoding = new Tiktoken( + cl100k_base.bpe_ranks, + cl100k_base.special_tokens, + cl100k_base.pat_str + ); + const tokens = encoding.encode(str); + encoding.free(); + + return { + message: 'ok', + tokens: tokens.length + }; +} +``` + +再回到 Fastgpt,点击“同步参数”,再连线将源文本传入,即可计算 tokens 数量 + +## 计算单文本块大小 + +![](/imgs/translate9.png) + +由于不涉及第三方包,只是一些数据处理,所以直接使用 代码运行 模块处理即可 + +```TypeScript +function main({tokenCount, tokenLimit}){ + const numChunks = Math.ceil(tokenCount / tokenLimit); + let chunkSize = Math.floor(tokenCount / numChunks); + + const remainingTokens = tokenCount % tokenLimit; + if (remainingTokens > 0) { + chunkSize += Math.floor(remainingTokens / numChunks); + } + + return {chunkSize}; +} +``` + +通过上面的代码,我们就能算出不超过 token限制的合理单文本块大小是多少了 + +## 获得切分后源文本块 + +![](/imgs/translate10.png) + +通过单文本块大小和源文本,我们再编写一个函数调用 langchain 的 textsplitters 包来实现文本分片,具体代码如下 + +```TypeScript +import cloud from '@lafjs/cloud' +import { TokenTextSplitter } from "@langchain/textsplitters"; + +interface IRequestBody { + text: string + chunkSize: number +} + +interface RequestProps extends IRequestBody { + systemParams: { + appId: string, + variables: string, + histories: string, + cTime: string, + chatId: string, + responseChatItemId: string + } +} + +interface IResponse { + output: string[]; +} + +export default async function (ctx: FunctionContext): Promise{ + const { text = '', chunkSize=1000 }: RequestProps = ctx.body; + + const splitter = new TokenTextSplitter({ + encodingName:"gpt2", + chunkSize: Number(chunkSize), + chunkOverlap: 0, + }); + + const output = await splitter.splitText(text); + + return { + output + } +} +``` + +这样我们就获得了切分好的文本,接下去的操作就类似单文本块反思翻译 + +## 多文本块翻译 + +这里应该还是不能直接调用前面的单文本块反思翻译,因为提示词中会涉及一些上下文的处理(或者可以修改下前面写好的插件,多传点参数进去) + +详细的和前面类似,就是提示词进行一些替换,以及需要做一些很简单的数据处理,整体效果如下 + +### 多文本块初始翻译 + +![](/imgs/translate11.png) + +### 多文本块反思 + +![](/imgs/translate12.png) + +### 多文本块提升翻译 + +![](/imgs/translate13.png) + +## 循环执行 + +长文反思翻译比较关键的一个部分,就是对多个文本块进行循环反思翻译 + +Fastgpt 提供了工作流线路可以返回去执行的功能,所以我们可以写一个很简单的判断函数,来判断结束或是接着执行 + +![](/imgs/translate14.png) + +也就是通过判断当前处理的这个文本块,是否是最后一个文本块,从而判断是否需要继续执行,就这样,我们实现了长文反思翻译的效果 + +完整工作流如下 + +![](/imgs/translate15.png) + +## 运行效果 + +首先输入全局设置 + +![](/imgs/translate16.png) + +然后输入需要翻译的文本,这里我选择了一章哈利波特的英文原文来做翻译,其文本长度通过 openai 对 tokens 数量的判断如下 + +![](/imgs/translate17.png) + +实际运行效果如下 + +![](/imgs/translate18.png) + +可以看到还是能满足阅读需求的 + +# 进一步调优 + +## 提示词调优 + +在源项目中,给 AI 的系统提示词还是比较的简略的,我们可以通过比较完善的提示词,来督促 LLM 返回更合适的翻译,进一步提升翻译的质量 + +比如初始翻译中, + +```TypeScript +# Role: 资深翻译专家 + +## Background: +你是一位经验丰富的翻译专家,精通{{source_lang}}和{{target_lang}}互译,尤其擅长将{{source_lang}}文章译成流畅易懂的{{target_lang}}。你曾多次带领团队完成大型翻译项目,译文广受好评。 + +## Attention: +- 翻译过程中要始终坚持"信、达、雅"的原则,但"达"尤为重要 +- 译文要符合{{target_lang}}的表达习惯,通俗易懂,连贯流畅 +- 避免使用过于文绉绉的表达和晦涩难懂的典故引用 + +## Constraints: +- 必须严格遵循四轮翻译流程:直译、意译、校审、定稿 +- 译文要忠实原文,准确无误,不能遗漏或曲解原意 + +## Goals: +- 通过四轮翻译流程,将{{source_lang}}原文译成高质量的{{target_lang}}译文 +- 译文要准确传达原文意思,语言表达力求浅显易懂,朗朗上口 +- 适度使用一些熟语俗语、流行网络用语等,增强译文的亲和力 +- 在直译的基础上,提供至少2个不同风格的意译版本供选择 + +## Skills: +- 精通{{source_lang}} {{target_lang}}两种语言,具有扎实的语言功底和丰富的翻译经验 +- 擅长将{{source_lang}}表达习惯转换为地道自然的{{target_lang}} +- 对当代{{target_lang}}语言的发展变化有敏锐洞察,善于把握语言流行趋势 + +## Workflow: +1. 第一轮直译:逐字逐句忠实原文,不遗漏任何信息 +2. 第二轮意译:在直译的基础上用通俗流畅的{{target_lang}}意译原文,至少提供2个不同风格的版本 +3. 第三轮校审:仔细审视译文,消除偏差和欠缺,使译文更加地道易懂 +4. 第四轮定稿:择优选取,反复修改润色,最终定稿出一个简洁畅达、符合大众阅读习惯的译文 + +## OutputFormat: +- 只需要输出第四轮定稿的回答 + +## Suggestions: +- 直译时力求忠实原文,但不要过于拘泥逐字逐句 +- 意译时在准确表达原意的基础上,用最朴实无华的{{target_lang}}来表达 +- 校审环节重点关注译文是否符合{{target_lang}}表达习惯,是否通俗易懂 +- 定稿时适度采用一些熟语谚语、网络流行语等,使译文更接地气- 善于利用{{target_lang}}的灵活性,用不同的表述方式展现同一内容,提高译文的可读性 +``` + +从而返回更准确更高质量的初始翻译,后续的反思和提升翻译也可以修改更准确的提示词,如下 + +![](/imgs/translate19.png) + +然后再让我们来看看运行效果 + +![](/imgs/translate20.png) + +给了和之前相同的一段文本进行测试,测试效果还是比较显著的,就比如红框部分,之前的翻译如下 + +![](/imgs/translate21.png) + +从“让你的猫头鹰给我写信”这样有失偏颇的翻译,变成“给我写信,你的猫头鹰会知道怎么找到我”这样较为准确的翻译 + +## 其他调优 + +比如限定词调优,源项目中已经做了示范,就是加上国家地区这个限定词,实测确实会有不少提升 + +出于 LLM 的卓越能力,我们能够通过设置不同的prompt来获取不同的翻译结果,也就是可以很轻松地通过设置特殊的限定词,来实现特定的,更精确的翻译 + +而对于一些超出 LLM 理解的术语等,也可以利用 Fastgpt 的知识库功能进行相应扩展,进一步完善翻译机器人的功能 \ No newline at end of file diff --git a/docSite/content/zh-cn/docs/use-cases/app-cases/submit_application_template.md b/docSite/content/zh-cn/docs/use-cases/app-cases/submit_application_template.md new file mode 100644 index 000000000..1df015e97 --- /dev/null +++ b/docSite/content/zh-cn/docs/use-cases/app-cases/submit_application_template.md @@ -0,0 +1,86 @@ +--- +title: "如何提交应用模板" +description: "指南:如何向 FastGPT 提交应用模板" +icon: "template_submission" +draft: false +toc: true +weight: 602 +--- + + +## 什么模板可以合并 + +目前合并进仓库的应用模板,会在「模板市场」中全部展示给用户。 + +为了控制模板的质量以及避免数量过多带来的繁琐,并不是所有的模板都会被合并到开源仓库中,你可以提前 PR 与我们沟通模板的内容。 + +预估最后总体的数量不会很多,控制在 50 个左右,一半来自 FastGPT Team,一半来自社区用户。 + +## 如何写一个应用模板 + +1. ### 跑通 FastGPT dev 环境 + +需要在 dev 环境下执行下面的操作。 + +> 可参照 [FastGPT|快速开始本地开发](https://doc.fastgpt.in/docs/development/intro/) + +1. ### 在 FastGPT 工作台中,创建一个应用 + +创建空白工作流即可。 + +![](/imgs/template_submission1.png) + +1. ### 创建应用模板 + +应用模板配置以及相关资源,都会在 **projects/app/public/appMarketTemplates** 目录下。 + +![](/imgs/template_submission2.png) + +1. 在 **projects/app/public/appMarketTemplates** 目录下,创建一个文件夹,名称为模板对应的 id。 +2. 在刚刚创建的文件夹中,再创建一个 **template.json** 文件,复制粘贴并填写如下配置: + +```JSON +{ + "name": "模板名", + "intro": "模板描述,会展示在模板市场的展示页", + "author": "填写你的名字", + "avatar": "模板头像,可以将图片文件放在同一个文件夹中,然后填写相应路径", + + "tags": ["模板标签"], // writing(文本创作),image-generation(图片生成),web-search(联网搜索), + // roleplay(角色扮演), office-services(办公服务) 暂时分为 5 类,从中选择相应的标签 + + "type": "模板类别", // simple(简易应用), advanced(工作流), plugin(插件) + + "workflow": { // 这个对象先不管,待会直接粘贴导出的工作流即可 + "nodes": [], + "edges": [], + "chatConfig": {} + } +} +``` + +1. ### 完成应用编排并测试 + +完成应用编排后,可以点击右上角的发布。 + +1. ### 复制配置到 template.json + +鼠标放置在左上角应用的头像和名称上,会出现对于下拉框操作,可以导出工作流配置。 + +导出的配置,会自动复制到剪切板,可以直接到 template.json 文件中粘贴使用,替换步骤 2 中,**workflow** 的值。 + +![](/imgs/template_submission3.png) + +1. ### 验证模板是否加载成功 + +刷新页面,打开模板市场,看其是否成功加载,并点击「使用」测试其功能。 + +![](/imgs/template_submission4.png) + +1. ### 提交 PR + +如果你觉得你的模板需要提交到开源仓库,可以通过 PR 形式向我们提交。 + +- 写清楚模板的介绍和功能 +- 配上模板运行的效果图 +- 模板参数填写说明,需要在 PR 中写清楚。例如,有些模板需要去某个提供商申请 key,需要附上对应的地址和教程,后续我们会加入到文档中。 \ No newline at end of file diff --git a/docSite/content/zh-cn/docs/workflow/examples/translate-subtitle-using-gpt.md b/docSite/content/zh-cn/docs/use-cases/app-cases/translate-subtitle-using-gpt.md similarity index 99% rename from docSite/content/zh-cn/docs/workflow/examples/translate-subtitle-using-gpt.md rename to docSite/content/zh-cn/docs/use-cases/app-cases/translate-subtitle-using-gpt.md index 5fc6f8305..fa12d31ce 100644 --- a/docSite/content/zh-cn/docs/workflow/examples/translate-subtitle-using-gpt.md +++ b/docSite/content/zh-cn/docs/use-cases/app-cases/translate-subtitle-using-gpt.md @@ -4,7 +4,7 @@ description: '利用 AI 自我反思提升翻译质量,同时循环迭代执 icon: 'translate' draft: false toc: true -weight: 401 +weight: 604 --- 直接使用 LLM 来翻译长字幕会遇到很多难点,这些难点也正是直接使用 AI 无法有效处理的问题: diff --git a/docSite/content/zh-cn/docs/use-cases/external-integration/_index.md b/docSite/content/zh-cn/docs/use-cases/external-integration/_index.md new file mode 100644 index 000000000..f9cdc5a96 --- /dev/null +++ b/docSite/content/zh-cn/docs/use-cases/external-integration/_index.md @@ -0,0 +1,9 @@ +--- +weight: 500 +title: "外部调用 FastGPT" +description: "外部应用通过多种方式调用 FastGPT 功能的教程" +icon: "cloud" +draft: false +images: [] +--- + diff --git a/docSite/content/zh-cn/docs/course/feishu.md b/docSite/content/zh-cn/docs/use-cases/external-integration/feishu.md similarity index 99% rename from docSite/content/zh-cn/docs/course/feishu.md rename to docSite/content/zh-cn/docs/use-cases/external-integration/feishu.md index 4c83499ca..2ad152467 100644 --- a/docSite/content/zh-cn/docs/course/feishu.md +++ b/docSite/content/zh-cn/docs/use-cases/external-integration/feishu.md @@ -4,7 +4,7 @@ description: "FastGPT 接入飞书机器人教程" icon: "chat" draft: false toc: true -weight: 111 +weight: 504 --- 从 4.8.10 版本起,FastGPT 商业版支持直接接入飞书机器人,无需额外的 API。 diff --git a/docSite/content/zh-cn/docs/use-cases/external-integration/iframe_integration.md b/docSite/content/zh-cn/docs/use-cases/external-integration/iframe_integration.md new file mode 100644 index 000000000..e88d55e47 --- /dev/null +++ b/docSite/content/zh-cn/docs/use-cases/external-integration/iframe_integration.md @@ -0,0 +1,8 @@ +--- +title: "iframe 接入" +description: "通过 iframe 嵌入 FastGPT 内容到其他网页或应用" +icon: "iframe" +draft: false +toc: true +weight: 512 +--- \ No newline at end of file diff --git a/docSite/content/zh-cn/docs/course/official_account.md b/docSite/content/zh-cn/docs/use-cases/external-integration/official_account.md similarity index 99% rename from docSite/content/zh-cn/docs/course/official_account.md rename to docSite/content/zh-cn/docs/use-cases/external-integration/official_account.md index 04c5fa89b..b8cef67bb 100644 --- a/docSite/content/zh-cn/docs/course/official_account.md +++ b/docSite/content/zh-cn/docs/use-cases/external-integration/official_account.md @@ -4,7 +4,7 @@ description: 'FastGPT 接入微信公众号教程' icon: 'description' draft: false toc: true -weight: 113 +weight: 506 --- 从 4.8.10 版本起,FastGPT 商业版支持直接接入微信公众号,无需额外的 API。 diff --git a/docSite/content/zh-cn/docs/use-cases/onwechat.md b/docSite/content/zh-cn/docs/use-cases/external-integration/onwechat.md similarity index 93% rename from docSite/content/zh-cn/docs/use-cases/onwechat.md rename to docSite/content/zh-cn/docs/use-cases/external-integration/onwechat.md index bc8959bdd..f3e68c9a3 100644 --- a/docSite/content/zh-cn/docs/use-cases/onwechat.md +++ b/docSite/content/zh-cn/docs/use-cases/external-integration/onwechat.md @@ -4,14 +4,14 @@ description: "FastGPT 对接 chatgpt-on-wechat" icon: "chat" draft: false toc: true -weight: 504 +weight: 509 --- # 1 分钟对接 chatgpt-on-wechat [chatgpt-on-wechat GitHub 地址](https://github.com/zhayujie/chatgpt-on-wechat) -由于 FastGPT 的 API 接口和 OpenAI 的规范一致,可以无需变更原来的应用即可使用 FastGPT 上编排好的应用。API 使用可参考 [这篇文章](/docs/course/openapi/)。编排示例,可参考 [高级编排介绍](/docs/workflow/intro) +由于 FastGPT 的 API 接口和 OpenAI 的规范一致,可以无需变更原来的应用即可使用 FastGPT 上编排好的应用。API 使用可参考 [这篇文章](/docs/use-cases/external-integration/openapi/)。编排示例,可参考 [高级编排介绍](/docs/workflow/intro) ## 1. 获取 OpenAPI 密钥 diff --git a/docSite/content/zh-cn/docs/course/openapi.md b/docSite/content/zh-cn/docs/use-cases/external-integration/openapi.md similarity index 98% rename from docSite/content/zh-cn/docs/course/openapi.md rename to docSite/content/zh-cn/docs/use-cases/external-integration/openapi.md index 898e20138..0c62d3fd2 100644 --- a/docSite/content/zh-cn/docs/course/openapi.md +++ b/docSite/content/zh-cn/docs/use-cases/external-integration/openapi.md @@ -4,7 +4,7 @@ description: "通过 API 访问 FastGPT 应用" icon: "model_training" draft: false toc: true -weight: 112 +weight: 502 --- 在 FastGPT 中,你可以为每一个应用创建多个 API 密钥,用于访问应用的 API 接口。每个密钥仅能访问一个应用。完整的接口可以[查看应用对话接口](/docs/development/openapi/chat)。 diff --git a/docSite/content/zh-cn/docs/use-cases/wechat.md b/docSite/content/zh-cn/docs/use-cases/external-integration/wechat.md similarity index 99% rename from docSite/content/zh-cn/docs/use-cases/wechat.md rename to docSite/content/zh-cn/docs/use-cases/external-integration/wechat.md index 58d74ea4a..1746507f1 100644 --- a/docSite/content/zh-cn/docs/use-cases/wechat.md +++ b/docSite/content/zh-cn/docs/use-cases/external-integration/wechat.md @@ -4,7 +4,7 @@ description: "FastGPT 接入微信和企业微信 " icon: "chat" draft: false toc: true -weight: 506 +weight: 510 --- # FastGPT 三分钟接入微信/企业微信 diff --git a/docSite/content/zh-cn/docs/workflow/_index.md b/docSite/content/zh-cn/docs/workflow/_index.md deleted file mode 100644 index 706d7e466..000000000 --- a/docSite/content/zh-cn/docs/workflow/_index.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -weight: 300 -title: '高级编排' -description: 'FastGPT 高级编排文档' -icon: 'family_history' -draft: false -images: [] ---- - diff --git a/docSite/content/zh-cn/docs/workflow/examples/_index.md b/docSite/content/zh-cn/docs/workflow/examples/_index.md deleted file mode 100644 index 70f4b3131..000000000 --- a/docSite/content/zh-cn/docs/workflow/examples/_index.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -weight: 400 -title: "编排示例" -description: "介绍 FastGPT 的高级编排实践案例" -icon: "list" -draft: false -images: [] ---- \ No newline at end of file diff --git a/docSite/content/zh-cn/docs/workflow/modules/_index.md b/docSite/content/zh-cn/docs/workflow/modules/_index.md deleted file mode 100644 index a5d5551e0..000000000 --- a/docSite/content/zh-cn/docs/workflow/modules/_index.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -weight: 350 -title: "模块介绍" -description: "介绍 FastGPT 的常用模块" -icon: "apps" -draft: false -images: [] ---- - - \ No newline at end of file diff --git a/docSite/content/zh-cn/docs/workflow/modules/input.md b/docSite/content/zh-cn/docs/workflow/modules/input.md deleted file mode 100644 index 4a439838c..000000000 --- a/docSite/content/zh-cn/docs/workflow/modules/input.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: "对话入口" -description: "FastGPT 对话入口模块介绍" -icon: "input" -draft: false -toc: true -weight: 356 ---- - -## 特点 - -- 流程入口 -- 无输入 -- 自动执行 - -![](/imgs/chatinput.png) \ No newline at end of file diff --git a/files/docker/docker-compose-milvus.yml b/files/docker/docker-compose-milvus.yml index 4703bdddf..ccd84a7d2 100644 --- a/files/docker/docker-compose-milvus.yml +++ b/files/docker/docker-compose-milvus.yml @@ -139,6 +139,8 @@ services: - OPENAI_BASE_URL=http://oneapi:3000/v1 # AI模型的API Key。(这里默认填写了OneAPI的快速默认key,测试通后,务必及时修改) - CHAT_API_KEY=sk-fastgpt + # 是否将图片转成 base64 传递给模型,本地开发和内网环境使用共有模型时候需要设置为 true + - MULTIPLE_DATA_TO_BASE64=false # 数据库最大连接数 - DB_MAX_LINK=30 # 登录凭证密钥 diff --git a/files/docker/docker-compose-pgvector.yml b/files/docker/docker-compose-pgvector.yml index 558fee914..9767d12f7 100644 --- a/files/docker/docker-compose-pgvector.yml +++ b/files/docker/docker-compose-pgvector.yml @@ -97,6 +97,8 @@ services: - OPENAI_BASE_URL=http://oneapi:3000/v1 # AI模型的API Key。(这里默认填写了OneAPI的快速默认key,测试通后,务必及时修改) - CHAT_API_KEY=sk-fastgpt + # 是否将图片转成 base64 传递给模型,本地开发和内网环境使用共有模型时候需要设置为 true + - MULTIPLE_DATA_TO_BASE64=false # 数据库最大连接数 - DB_MAX_LINK=30 # 登录凭证密钥 diff --git a/files/docker/docker-compose-zilliz.yml b/files/docker/docker-compose-zilliz.yml index 6796dc7a7..b6e781b52 100644 --- a/files/docker/docker-compose-zilliz.yml +++ b/files/docker/docker-compose-zilliz.yml @@ -77,6 +77,8 @@ services: - OPENAI_BASE_URL=http://oneapi:3000/v1 # AI模型的API Key。(这里默认填写了OneAPI的快速默认key,测试通后,务必及时修改) - CHAT_API_KEY=sk-fastgpt + # 是否将图片转成 base64 传递给模型,本地开发和内网环境使用共有模型时候需要设置为 true + - MULTIPLE_DATA_TO_BASE64=false # 数据库最大连接数 - DB_MAX_LINK=30 # 登录凭证密钥 diff --git a/packages/global/common/file/constants.ts b/packages/global/common/file/constants.ts index 23fd27a6d..4be4bab22 100644 --- a/packages/global/common/file/constants.ts +++ b/packages/global/common/file/constants.ts @@ -16,6 +16,8 @@ export const bucketNameMap = { } }; -export const ReadFileBaseUrl = `${process.env.FE_DOMAIN || ''}/api/common/file/read`; +export const ReadFileBaseUrl = `${process.env.FE_DOMAIN || ''}${process.env.NEXT_PUBLIC_BASE_URL}/api/common/file/read`; export const documentFileType = '.txt, .docx, .csv, .xlsx, .pdf, .md, .html, .pptx'; +export const imageFileType = + '.jpg, .jpeg, .png, .gif, .bmp, .webp, .svg, .tiff, .tif, .ico, .heic, .heif, .avif'; diff --git a/packages/global/common/file/tools.ts b/packages/global/common/file/tools.ts index 97e6bc7cb..8cf1d3e95 100644 --- a/packages/global/common/file/tools.ts +++ b/packages/global/common/file/tools.ts @@ -1,4 +1,7 @@ import { detect } from 'jschardet'; +import { documentFileType, imageFileType } from './constants'; +import { ChatFileTypeEnum } from '../../core/chat/constants'; +import { UserChatItemValueItemType } from '../../core/chat/type'; export const formatFileSize = (bytes: number): string => { if (bytes === 0) return '0 B'; @@ -13,3 +16,40 @@ export const formatFileSize = (bytes: number): string => { export const detectFileEncoding = (buffer: Buffer) => { return detect(buffer.slice(0, 200))?.encoding?.toLocaleLowerCase(); }; + +// Url => user upload file type +export const parseUrlToFileType = (url: string): UserChatItemValueItemType['file'] | undefined => { + if (typeof url !== 'string') return; + const parseUrl = new URL(url, 'https://locaohost:3000'); + + const filename = (() => { + // Old version file url: https://xxx.com/file/read?filename=xxx.pdf + const filenameQuery = parseUrl.searchParams.get('filename'); + if (filenameQuery) return filenameQuery; + + // Common file: https://xxx.com/xxx.pdf?xxxx=xxx + const pathname = parseUrl.pathname; + if (pathname) return pathname.split('/').pop(); + })(); + + if (!filename) return; + + const extension = filename.split('.').pop()?.toLowerCase() || ''; + + if (!extension) return; + + if (documentFileType.includes(extension)) { + return { + type: ChatFileTypeEnum.file, + name: filename, + url + }; + } + if (imageFileType.includes(extension)) { + return { + type: ChatFileTypeEnum.image, + name: filename, + url + }; + } +}; diff --git a/packages/global/common/string/time.ts b/packages/global/common/string/time.ts index 2ae2980fd..51f6b2370 100644 --- a/packages/global/common/string/time.ts +++ b/packages/global/common/string/time.ts @@ -2,6 +2,7 @@ import dayjs from 'dayjs'; import cronParser from 'cron-parser'; import utc from 'dayjs/plugin/utc'; import timezone from 'dayjs/plugin/timezone'; +import { i18nT } from '../../../web/i18n/utils'; dayjs.extend(utc); dayjs.extend(timezone); @@ -23,31 +24,51 @@ export const formatTimeToChatTime = (time: Date) => { // 如果传入时间小于60秒,返回刚刚 if (now.diff(target, 'second') < 60) { - return '刚刚'; + return i18nT('common:just_now'); } // 如果时间是今天,展示几时:几分 + //用#占位,i18n生效后replace成: if (now.isSame(target, 'day')) { - return target.format('HH : mm'); + return target.format('HH#mm'); } // 如果是昨天,展示昨天 if (now.subtract(1, 'day').isSame(target, 'day')) { - return '昨天'; - } - - // 如果是前天,展示前天 - if (now.subtract(2, 'day').isSame(target, 'day')) { - return '前天'; + return i18nT('common:yesterday'); } // 如果是今年,展示某月某日 if (now.isSame(target, 'year')) { - return target.format('MM/DD'); + return target.format('MM-DD'); } // 如果是更久之前,展示某年某月某日 - return target.format('YYYY/M/D'); + return target.format('YYYY-M-D'); +}; + +export const formatTimeToChatItemTime = (time: Date) => { + const now = dayjs(); + const target = dayjs(time); + const detailTime = target.format('HH#mm'); + + // 如果时间是今天,展示几时:几分 + if (now.isSame(target, 'day')) { + return detailTime; + } + + // 如果是昨天,展示昨天+几时:几分 + if (now.subtract(1, 'day').isSame(target, 'day')) { + return i18nT('common:yesterday_detail_time'); + } + + // 如果是今年,展示某月某日+几时:几分 + if (now.isSame(target, 'year')) { + return target.format('MM-DD') + ' ' + detailTime; + } + + // 如果是更久之前,展示某年某月某日+几时:几分 + return target.format('YYYY-M-D') + ' ' + detailTime; }; /* cron time parse */ diff --git a/packages/global/core/ai/prompt/AIChat.ts b/packages/global/core/ai/prompt/AIChat.ts index b0c47cfe9..36abbaeca 100644 --- a/packages/global/core/ai/prompt/AIChat.ts +++ b/packages/global/core/ai/prompt/AIChat.ts @@ -207,8 +207,8 @@ export const Prompt_systemQuotePromptList: PromptTemplateItem[] = [ ]; // Document quote prompt -export const Prompt_DocumentQuote = `将 中的内容作为本次对话的参考: - +export const Prompt_DocumentQuote = `将 中的内容作为本次对话的参考: + {{quote}} - + `; diff --git a/packages/global/core/chat/adapt.ts b/packages/global/core/chat/adapt.ts index 9b9c34d32..02a85fe1c 100644 --- a/packages/global/core/chat/adapt.ts +++ b/packages/global/core/chat/adapt.ts @@ -14,7 +14,6 @@ import type { ChatCompletionToolMessageParam } from '../../core/ai/type.d'; import { ChatCompletionRequestMessageRoleEnum } from '../../core/ai/constants'; - const GPT2Chat = { [ChatCompletionRequestMessageRoleEnum.System]: ChatRoleEnum.System, [ChatCompletionRequestMessageRoleEnum.User]: ChatRoleEnum.Human, @@ -61,14 +60,14 @@ export const chats2GPTMessages = ({ return { type: 'image_url', image_url: { - url: item.file?.url || '' + url: item.file.url } }; } else if (item.file?.type === ChatFileTypeEnum.file) { return { type: 'file_url', name: item.file?.name || '', - url: item.file?.url || '' + url: item.file.url }; } } @@ -91,6 +90,7 @@ export const chats2GPTMessages = ({ } } else { const aiResults: ChatCompletionMessageParam[] = []; + //AI item.value.forEach((value, i) => { if (value.type === ChatItemValueTypeEnum.tool && value.tools && reserveTool) { @@ -131,7 +131,7 @@ export const chats2GPTMessages = ({ if ( lastValue && lastValue.type === ChatItemValueTypeEnum.text && - typeof lastResult.content === 'string' + typeof lastResult?.content === 'string' ) { lastResult.content += value.text.content; } else { diff --git a/packages/global/core/chat/type.d.ts b/packages/global/core/chat/type.d.ts index d606efb8d..3d16c2653 100644 --- a/packages/global/core/chat/type.d.ts +++ b/packages/global/core/chat/type.d.ts @@ -126,6 +126,7 @@ export type ChatSiteItemType = (UserChatItemType | SystemChatItemType | AIChatIt moduleName?: string; ttsBuffer?: Uint8Array; responseData?: ChatHistoryItemResType[]; + time?: Date; } & ChatBoxInputType & ResponseTagItemType; diff --git a/packages/global/core/chat/utils.ts b/packages/global/core/chat/utils.ts index a8d32e366..3c252861c 100644 --- a/packages/global/core/chat/utils.ts +++ b/packages/global/core/chat/utils.ts @@ -30,7 +30,8 @@ export const getChatTitleFromChatMessage = (message?: ChatItemType, defaultValue // Keep the first n and last n characters export const getHistoryPreview = ( completeMessages: ChatItemType[], - size = 100 + size = 100, + useVision = false ): { obj: `${ChatRoleEnum}`; value: string; @@ -48,7 +49,8 @@ export const getHistoryPreview = ( item.value ?.map((item) => { if (item?.text?.content) return item?.text?.content; - if (item.file?.type === 'image') return 'Input an image'; + if (item.file?.type === 'image' && useVision) + return `![Input an image](${item.file.url.slice(0, 100)}...)`; return ''; }) .filter(Boolean) @@ -80,7 +82,7 @@ export const filterPublicNodeResponseData = ({ }: { flowResponses?: ChatHistoryItemResType[]; }) => { - const filedList = ['quoteList', 'moduleType', 'pluginOutput']; + const filedList = ['quoteList', 'moduleType', 'pluginOutput', 'runningTime']; const filterModuleTypeList: any[] = [ FlowNodeTypeEnum.pluginModule, FlowNodeTypeEnum.datasetSearchNode, diff --git a/packages/global/core/workflow/constants.ts b/packages/global/core/workflow/constants.ts index 7219f972f..578a81b3b 100644 --- a/packages/global/core/workflow/constants.ts +++ b/packages/global/core/workflow/constants.ts @@ -199,8 +199,10 @@ export enum NodeInputKeyEnum { childrenNodeIdList = 'childrenNodeIdList', nodeWidth = 'nodeWidth', nodeHeight = 'nodeHeight', + loopNodeInputHeight = 'loopNodeInputHeight', // loop start loopStartInput = 'loopStartInput', + loopStartIndex = 'loopStartIndex', // loop end loopEndInput = 'loopEndInput', @@ -256,9 +258,9 @@ export enum NodeOutputKeyEnum { // loop loopArray = 'loopArray', - // loop start loopStartInput = 'loopStartInput', + loopStartIndex = 'loopStartIndex', // form input formInputResult = 'formInputResult' @@ -334,3 +336,21 @@ export enum ContentTypes { xml = 'xml', raw = 'raw-text' } + +export const ArrayTypeMap: Record = { + [WorkflowIOValueTypeEnum.string]: WorkflowIOValueTypeEnum.arrayString, + [WorkflowIOValueTypeEnum.number]: WorkflowIOValueTypeEnum.arrayNumber, + [WorkflowIOValueTypeEnum.boolean]: WorkflowIOValueTypeEnum.arrayBoolean, + [WorkflowIOValueTypeEnum.object]: WorkflowIOValueTypeEnum.arrayObject, + [WorkflowIOValueTypeEnum.arrayString]: WorkflowIOValueTypeEnum.arrayString, + [WorkflowIOValueTypeEnum.arrayNumber]: WorkflowIOValueTypeEnum.arrayNumber, + [WorkflowIOValueTypeEnum.arrayBoolean]: WorkflowIOValueTypeEnum.arrayBoolean, + [WorkflowIOValueTypeEnum.arrayObject]: WorkflowIOValueTypeEnum.arrayObject, + [WorkflowIOValueTypeEnum.chatHistory]: WorkflowIOValueTypeEnum.arrayObject, + [WorkflowIOValueTypeEnum.datasetQuote]: WorkflowIOValueTypeEnum.arrayObject, + [WorkflowIOValueTypeEnum.dynamic]: WorkflowIOValueTypeEnum.arrayObject, + [WorkflowIOValueTypeEnum.selectDataset]: WorkflowIOValueTypeEnum.arrayObject, + [WorkflowIOValueTypeEnum.selectApp]: WorkflowIOValueTypeEnum.arrayObject, + [WorkflowIOValueTypeEnum.arrayAny]: WorkflowIOValueTypeEnum.arrayAny, + [WorkflowIOValueTypeEnum.any]: WorkflowIOValueTypeEnum.arrayAny +}; diff --git a/packages/global/core/workflow/node/constant.ts b/packages/global/core/workflow/node/constant.ts index 54dfa55ba..e3a90725f 100644 --- a/packages/global/core/workflow/node/constant.ts +++ b/packages/global/core/workflow/node/constant.ts @@ -27,7 +27,9 @@ export enum FlowNodeInputTypeEnum { // render ui settingDatasetQuotePrompt = 'settingDatasetQuotePrompt', hidden = 'hidden', - custom = 'custom' + custom = 'custom', + + fileSelect = 'fileSelect' } export const FlowNodeInputMap: Record< FlowNodeInputTypeEnum, @@ -85,6 +87,9 @@ export const FlowNodeInputMap: Record< }, [FlowNodeInputTypeEnum.textarea]: { icon: 'core/workflow/inputType/textarea' + }, + [FlowNodeInputTypeEnum.fileSelect]: { + icon: 'core/workflow/inputType/file' } }; @@ -137,43 +142,43 @@ export enum FlowNodeTypeEnum { // node IO value type export const FlowValueTypeMap = { [WorkflowIOValueTypeEnum.string]: { - label: 'string', + label: 'String', value: WorkflowIOValueTypeEnum.string }, [WorkflowIOValueTypeEnum.number]: { - label: 'number', + label: 'Number', value: WorkflowIOValueTypeEnum.number }, [WorkflowIOValueTypeEnum.boolean]: { - label: 'boolean', + label: 'Boolean', value: WorkflowIOValueTypeEnum.boolean }, [WorkflowIOValueTypeEnum.object]: { - label: 'object', + label: 'Object', value: WorkflowIOValueTypeEnum.object }, [WorkflowIOValueTypeEnum.arrayString]: { - label: 'array', + label: 'Array', value: WorkflowIOValueTypeEnum.arrayString }, [WorkflowIOValueTypeEnum.arrayNumber]: { - label: 'array', + label: 'Array', value: WorkflowIOValueTypeEnum.arrayNumber }, [WorkflowIOValueTypeEnum.arrayBoolean]: { - label: 'array', + label: 'Array', value: WorkflowIOValueTypeEnum.arrayBoolean }, [WorkflowIOValueTypeEnum.arrayObject]: { - label: 'array', + label: 'Array', value: WorkflowIOValueTypeEnum.arrayObject }, [WorkflowIOValueTypeEnum.arrayAny]: { - label: 'array', + label: 'Array', value: WorkflowIOValueTypeEnum.arrayAny }, [WorkflowIOValueTypeEnum.any]: { - label: 'any', + label: 'Any', value: WorkflowIOValueTypeEnum.any }, [WorkflowIOValueTypeEnum.chatHistory]: { diff --git a/packages/global/core/workflow/runtime/type.d.ts b/packages/global/core/workflow/runtime/type.d.ts index 1de03d8c3..ae0d29bda 100644 --- a/packages/global/core/workflow/runtime/type.d.ts +++ b/packages/global/core/workflow/runtime/type.d.ts @@ -135,6 +135,9 @@ export type DispatchNodeResponseType = { extensionResult?: string; extensionTokens?: number; + // dataset concat + concatLength?: number; + // cq cqList?: ClassifyQuestionAgentItemType[]; cqResult?: string; @@ -216,5 +219,7 @@ export type AIChatNodeProps = { [NodeInputKeyEnum.aiChatQuoteTemplate]?: string; [NodeInputKeyEnum.aiChatQuotePrompt]?: string; [NodeInputKeyEnum.aiChatVision]?: boolean; + [NodeInputKeyEnum.stringQuoteText]?: string; + [NodeInputKeyEnum.fileUrlList]?: string[]; }; diff --git a/packages/global/core/workflow/runtime/utils.ts b/packages/global/core/workflow/runtime/utils.ts index 9fec266e6..8ce3c8bb9 100644 --- a/packages/global/core/workflow/runtime/utils.ts +++ b/packages/global/core/workflow/runtime/utils.ts @@ -5,8 +5,8 @@ import { StoreNodeItemType } from '../type/node'; import { StoreEdgeItemType } from '../type/edge'; import { RuntimeEdgeItemType, RuntimeNodeItemType } from './type'; import { VARIABLE_NODE_ID } from '../constants'; -import { isReferenceValue } from '../utils'; -import { FlowNodeOutputItemType, ReferenceValueProps } from '../type/io'; +import { isValidReferenceValueFormat } from '../utils'; +import { FlowNodeOutputItemType, ReferenceValueType } from '../type/io'; import { ChatItemType, NodeOutputItemType } from '../../../core/chat/type'; import { ChatItemValueTypeEnum, ChatRoleEnum } from '../../../core/chat/constants'; @@ -34,7 +34,7 @@ export const getMaxHistoryLimitFromNodes = (nodes: StoreNodeItemType[]): number 2. Check that the workflow starts at the interaction node */ export const getLastInteractiveValue = (histories: ChatItemType[]) => { - const lastAIMessage = histories.findLast((item) => item.obj === ChatRoleEnum.AI); + const lastAIMessage = [...histories].reverse().find((item) => item.obj === ChatRoleEnum.AI); if (lastAIMessage) { const lastValue = lastAIMessage.value[lastAIMessage.value.length - 1]; @@ -225,37 +225,129 @@ export const checkNodeRunStatus = ({ return 'wait'; }; +/* + Get the value of the reference variable/node output + 1. [string,string] + 2. [string,string][] +*/ export const getReferenceVariableValue = ({ value, nodes, variables }: { - value: ReferenceValueProps; + value?: ReferenceValueType; nodes: RuntimeNodeItemType[]; variables: Record; }) => { - const nodeIds = nodes.map((node) => node.nodeId); - if (!isReferenceValue(value, nodeIds)) { - return value; - } - const sourceNodeId = value[0]; - const outputId = value[1]; + if (!value) return value; - if (sourceNodeId === VARIABLE_NODE_ID && outputId) { - return variables[outputId]; + // handle single reference value + if (isValidReferenceValueFormat(value)) { + const sourceNodeId = value[0]; + const outputId = value[1]; + + if (sourceNodeId === VARIABLE_NODE_ID) { + if (!outputId) return undefined; + return variables[outputId]; + } + + const node = nodes.find((node) => node.nodeId === sourceNodeId); + if (!node) { + return value; + } + + return node.outputs.find((output) => output.id === outputId)?.value; } - const node = nodes.find((node) => node.nodeId === sourceNodeId); + // handle reference array + if ( + Array.isArray(value) && + value.length > 0 && + value.every((item) => isValidReferenceValueFormat(item)) + ) { + const result = value.map((val) => { + return getReferenceVariableValue({ + value: val, + nodes, + variables + }); + }); - if (!node) { - return undefined; + return result.flat().filter((item) => item !== undefined); } - const outputValue = node.outputs.find((output) => output.id === outputId)?.value; - - return outputValue; + return value; }; +// replace {{$xx.xx$}} variables for text +export function replaceEditorVariable({ + text, + nodes, + variables, + runningNode +}: { + text: any; + nodes: RuntimeNodeItemType[]; + variables: Record; // global variables + runningNode: RuntimeNodeItemType; +}) { + if (typeof text !== 'string') return text; + + const globalVariables = Object.keys(variables).map((key) => { + return { + nodeId: VARIABLE_NODE_ID, + id: key, + value: variables[key] + }; + }); + + // Upstream node outputs + const nodeVariables = nodes + .map((node) => { + return node.outputs.map((output) => { + return { + nodeId: node.nodeId, + id: output.id, + value: output.value + }; + }); + }) + .flat(); + + // Get runningNode inputs(Will be replaced with reference) + const customInputs = runningNode.inputs.flatMap((item) => { + return [ + { + id: item.key, + value: getReferenceVariableValue({ + value: item.value, + nodes, + variables + }), + nodeId: runningNode.nodeId + } + ]; + }); + + const allVariables = [...globalVariables, ...nodeVariables, ...customInputs]; + + // Replace {{$xxx.xxx$}} to value + for (const key in allVariables) { + const variable = allVariables[key]; + const val = variable.value; + const formatVal = (() => { + if (val === undefined) return ''; + if (val === null) return 'null'; + + return typeof val === 'object' ? JSON.stringify(val) : String(val); + })(); + + const regex = new RegExp(`\\{\\{\\$(${variable.nodeId}\\.${variable.id})\\$\\}\\}`, 'g'); + text = text.replace(regex, formatVal); + } + return text || ''; +} + export const textAdaptGptResponse = ({ text, model = '', diff --git a/packages/global/core/workflow/template/input.ts b/packages/global/core/workflow/template/input.ts index 49a0788a4..72b4067c3 100644 --- a/packages/global/core/workflow/template/input.ts +++ b/packages/global/core/workflow/template/input.ts @@ -75,10 +75,17 @@ export const Input_Template_Text_Quote: FlowNodeInputItemType = { description: i18nT('app:document_quote_tip'), valueType: WorkflowIOValueTypeEnum.string }; + +export const Input_Template_File_Link_Prompt: FlowNodeInputItemType = { + key: NodeInputKeyEnum.fileUrlList, + renderTypeList: [FlowNodeInputTypeEnum.reference, FlowNodeInputTypeEnum.input], + label: i18nT('app:file_quote_link'), + debugLabel: i18nT('app:file_quote_link'), + valueType: WorkflowIOValueTypeEnum.arrayString +}; export const Input_Template_File_Link: FlowNodeInputItemType = { key: NodeInputKeyEnum.fileUrlList, renderTypeList: [FlowNodeInputTypeEnum.reference], - required: true, label: i18nT('app:workflow.user_file_input'), debugLabel: i18nT('app:workflow.user_file_input'), description: i18nT('app:workflow.user_file_input_desc'), @@ -104,7 +111,14 @@ export const Input_Template_Node_Height: FlowNodeInputItemType = { renderTypeList: [FlowNodeInputTypeEnum.hidden], valueType: WorkflowIOValueTypeEnum.number, label: '', - value: 900 + value: 600 +}; +export const Input_Template_LOOP_NODE_OFFSET: FlowNodeInputItemType = { + key: NodeInputKeyEnum.loopNodeInputHeight, + renderTypeList: [FlowNodeInputTypeEnum.hidden], + valueType: WorkflowIOValueTypeEnum.number, + label: '', + value: 320 }; export const Input_Template_Stream_MODE: FlowNodeInputItemType = { diff --git a/packages/global/core/workflow/template/system/aiChat/index.ts b/packages/global/core/workflow/template/system/aiChat/index.ts index 0bc4e19ab..a346371b2 100644 --- a/packages/global/core/workflow/template/system/aiChat/index.ts +++ b/packages/global/core/workflow/template/system/aiChat/index.ts @@ -17,7 +17,7 @@ import { Input_Template_History, Input_Template_System_Prompt, Input_Template_UserChatInput, - Input_Template_Text_Quote + Input_Template_File_Link_Prompt } from '../../input'; import { chatNodeSystemPromptTip, systemPromptTip } from '../../tip'; import { getHandleConfig } from '../../utils'; @@ -54,8 +54,8 @@ export const AiChatModule: FlowNodeTemplateType = { intro: i18nT('workflow:template.ai_chat_intro'), showStatus: true, isTool: true, - courseUrl: '/docs/workflow/modules/ai_chat/', - version: '481', + courseUrl: '/docs/guide/workbench/workflow/ai_chat/', + version: '4813', inputs: [ Input_Template_SettingAiModel, // --- settings modal @@ -89,7 +89,7 @@ export const AiChatModule: FlowNodeTemplateType = { renderTypeList: [FlowNodeInputTypeEnum.hidden], label: '', valueType: WorkflowIOValueTypeEnum.boolean, - value: false + value: true }, // settings modal --- { @@ -100,7 +100,7 @@ export const AiChatModule: FlowNodeTemplateType = { }, Input_Template_History, Input_Template_Dataset_Quote, - Input_Template_Text_Quote, + Input_Template_File_Link_Prompt, { ...Input_Template_UserChatInput, toolDescription: i18nT('workflow:user_question') } ], diff --git a/packages/global/core/workflow/template/system/assignedAnswer.ts b/packages/global/core/workflow/template/system/assignedAnswer.ts index 14f344be4..31a8eada0 100644 --- a/packages/global/core/workflow/template/system/assignedAnswer.ts +++ b/packages/global/core/workflow/template/system/assignedAnswer.ts @@ -17,7 +17,7 @@ export const AssignedAnswerModule: FlowNodeTemplateType = { avatar: 'core/workflow/template/reply', name: i18nT('workflow:assigned_reply'), intro: i18nT('workflow:intro_assigned_reply'), - courseUrl: '/docs/workflow/modules/reply/', + courseUrl: '/docs/guide/workbench/workflow/reply/', version: '481', isTool: true, inputs: [ diff --git a/packages/global/core/workflow/template/system/classifyQuestion/index.ts b/packages/global/core/workflow/template/system/classifyQuestion/index.ts index 8326d823a..3aefc9479 100644 --- a/packages/global/core/workflow/template/system/classifyQuestion/index.ts +++ b/packages/global/core/workflow/template/system/classifyQuestion/index.ts @@ -31,7 +31,7 @@ export const ClassifyQuestionModule: FlowNodeTemplateType = { intro: i18nT('workflow:intro_question_classification'), showStatus: true, version: '481', - courseUrl: '/docs/workflow/modules/question_classify/', + courseUrl: '/docs/guide/workbench/workflow/question_classify/', inputs: [ { ...Input_Template_SelectAIModel, diff --git a/packages/global/core/workflow/template/system/contextExtract/index.ts b/packages/global/core/workflow/template/system/contextExtract/index.ts index 5d0333c42..f94aa7e7e 100644 --- a/packages/global/core/workflow/template/system/contextExtract/index.ts +++ b/packages/global/core/workflow/template/system/contextExtract/index.ts @@ -26,7 +26,7 @@ export const ContextExtractModule: FlowNodeTemplateType = { intro: i18nT('workflow:intro_text_content_extraction'), showStatus: true, isTool: true, - courseUrl: '/docs/workflow/modules/content_extract/', + courseUrl: '/docs/guide/workbench/workflow/content_extract/', version: '481', inputs: [ { diff --git a/packages/global/core/workflow/template/system/customFeedback.ts b/packages/global/core/workflow/template/system/customFeedback.ts index f3d5dd9c3..c49f51b34 100644 --- a/packages/global/core/workflow/template/system/customFeedback.ts +++ b/packages/global/core/workflow/template/system/customFeedback.ts @@ -17,7 +17,7 @@ export const CustomFeedbackNode: FlowNodeTemplateType = { avatar: 'core/workflow/template/customFeedback', name: i18nT('workflow:custom_feedback'), intro: i18nT('workflow:intro_custom_feedback'), - courseUrl: '/docs/workflow/modules/custom_feedback/', + courseUrl: '/docs/guide/workbench/workflow/custom_feedback/', version: '486', inputs: [ { diff --git a/packages/global/core/workflow/template/system/datasetConcat.ts b/packages/global/core/workflow/template/system/datasetConcat.ts index 062d86f40..5a43b9a02 100644 --- a/packages/global/core/workflow/template/system/datasetConcat.ts +++ b/packages/global/core/workflow/template/system/datasetConcat.ts @@ -25,7 +25,7 @@ export const getOneQuoteInputTemplate = ({ }): FlowNodeInputItemType => ({ key, renderTypeList: [FlowNodeInputTypeEnum.reference], - label: `${i18nT('workflow:quote_num')},{ num: ${index} }`, + label: `${i18nT('workflow:quote_num')}-${index}`, debugLabel: i18nT('workflow:knowledge_base_reference'), canEdit: true, valueType: WorkflowIOValueTypeEnum.datasetQuote @@ -43,6 +43,7 @@ export const DatasetConcatModule: FlowNodeTemplateType = { showStatus: false, version: '486', + courseUrl: '/docs/guide/workbench/workflow/knowledge_base_search_merge/', inputs: [ { key: NodeInputKeyEnum.datasetMaxTokens, diff --git a/packages/global/core/workflow/template/system/datasetSearch.ts b/packages/global/core/workflow/template/system/datasetSearch.ts index d83cf2f82..476d10287 100644 --- a/packages/global/core/workflow/template/system/datasetSearch.ts +++ b/packages/global/core/workflow/template/system/datasetSearch.ts @@ -29,7 +29,7 @@ export const DatasetSearchModule: FlowNodeTemplateType = { intro: Dataset_SEARCH_DESC, showStatus: true, isTool: true, - courseUrl: '/docs/workflow/modules/dataset_search/', + courseUrl: '/docs/guide/workbench/workflow/dataset_search/', version: '481', inputs: [ { diff --git a/packages/global/core/workflow/template/system/http468.ts b/packages/global/core/workflow/template/system/http468.ts index 18df6f901..a1276dccd 100644 --- a/packages/global/core/workflow/template/system/http468.ts +++ b/packages/global/core/workflow/template/system/http468.ts @@ -27,7 +27,7 @@ export const HttpNode468: FlowNodeTemplateType = { intro: i18nT('workflow:intro_http_request'), showStatus: true, isTool: true, - courseUrl: '/docs/workflow/modules/http/', + courseUrl: '/docs/guide/workbench/workflow/http/', version: '481', inputs: [ { diff --git a/packages/global/core/workflow/template/system/ifElse/index.ts b/packages/global/core/workflow/template/system/ifElse/index.ts index b50e9989f..c20b3f331 100644 --- a/packages/global/core/workflow/template/system/ifElse/index.ts +++ b/packages/global/core/workflow/template/system/ifElse/index.ts @@ -23,7 +23,7 @@ export const IfElseNode: FlowNodeTemplateType = { name: i18nT('workflow:condition_checker'), intro: i18nT('workflow:execute_different_branches_based_on_conditions'), showStatus: true, - courseUrl: '/docs/workflow/modules/tfswitch/', + courseUrl: '/docs/guide/workbench/workflow/tfswitch/', version: '481', inputs: [ { diff --git a/packages/global/core/workflow/template/system/ifElse/type.d.ts b/packages/global/core/workflow/template/system/ifElse/type.d.ts index 1f55adcaa..6092ac0e1 100644 --- a/packages/global/core/workflow/template/system/ifElse/type.d.ts +++ b/packages/global/core/workflow/template/system/ifElse/type.d.ts @@ -1,9 +1,9 @@ -import { ReferenceValueProps } from 'core/workflow/type/io'; +import { ReferenceItemValueType } from '../../../type/io'; import { VariableConditionEnum } from './constant'; export type IfElseConditionType = 'AND' | 'OR'; export type ConditionListItemType = { - variable?: ReferenceValueProps; + variable?: ReferenceItemValueType; condition?: VariableConditionEnum; value?: string; }; diff --git a/packages/global/core/workflow/template/system/interactive/userSelect.ts b/packages/global/core/workflow/template/system/interactive/userSelect.ts index f8c170d78..753255c76 100644 --- a/packages/global/core/workflow/template/system/interactive/userSelect.ts +++ b/packages/global/core/workflow/template/system/interactive/userSelect.ts @@ -25,6 +25,7 @@ export const UserSelectNode: FlowNodeTemplateType = { intro: i18nT(`app:workflow.user_select_tip`), isTool: true, version: '489', + courseUrl: '/docs/guide/workbench/workflow/user-selection/', inputs: [ { key: NodeInputKeyEnum.description, diff --git a/packages/global/core/workflow/template/system/laf.ts b/packages/global/core/workflow/template/system/laf.ts index 61dadb191..f92fada21 100644 --- a/packages/global/core/workflow/template/system/laf.ts +++ b/packages/global/core/workflow/template/system/laf.ts @@ -32,7 +32,7 @@ export const LafModule: FlowNodeTemplateType = { intro: i18nT('workflow:intro_laf_function_call'), showStatus: true, isTool: true, - courseUrl: '/docs/workflow/modules/laf/', + courseUrl: '/docs/guide/workbench/workflow/laf/', version: '481', inputs: [ { diff --git a/packages/global/core/workflow/template/system/loop/loop.ts b/packages/global/core/workflow/template/system/loop/loop.ts index b99a1453f..4b62c449d 100644 --- a/packages/global/core/workflow/template/system/loop/loop.ts +++ b/packages/global/core/workflow/template/system/loop/loop.ts @@ -14,6 +14,7 @@ import { getHandleConfig } from '../../utils'; import { i18nT } from '../../../../../../web/i18n/utils'; import { Input_Template_Children_Node_List, + Input_Template_LOOP_NODE_OFFSET, Input_Template_Node_Height, Input_Template_Node_Width } from '../../input'; @@ -29,6 +30,7 @@ export const LoopNode: FlowNodeTemplateType = { intro: i18nT('workflow:intro_loop'), showStatus: true, version: '4811', + courseUrl: '/docs/guide/workbench/workflow/loop/', inputs: [ { key: NodeInputKeyEnum.loopInputArray, @@ -40,7 +42,8 @@ export const LoopNode: FlowNodeTemplateType = { }, Input_Template_Children_Node_List, Input_Template_Node_Width, - Input_Template_Node_Height + Input_Template_Node_Height, + Input_Template_LOOP_NODE_OFFSET ], outputs: [ { diff --git a/packages/global/core/workflow/template/system/loop/loopStart.ts b/packages/global/core/workflow/template/system/loop/loopStart.ts index 96ded8e69..2c0d97855 100644 --- a/packages/global/core/workflow/template/system/loop/loopStart.ts +++ b/packages/global/core/workflow/template/system/loop/loopStart.ts @@ -1,8 +1,13 @@ -import { FlowNodeInputTypeEnum, FlowNodeTypeEnum } from '../../../node/constant'; +import { + FlowNodeInputTypeEnum, + FlowNodeOutputTypeEnum, + FlowNodeTypeEnum +} from '../../../node/constant'; import { FlowNodeTemplateType } from '../../../type/node.d'; import { FlowNodeTemplateTypeEnum, NodeInputKeyEnum, + NodeOutputKeyEnum, WorkflowIOValueTypeEnum } from '../../../constants'; import { getHandleConfig } from '../../utils'; @@ -28,7 +33,21 @@ export const LoopStartNode: FlowNodeTemplateType = { label: '', required: true, value: '' + }, + { + key: NodeInputKeyEnum.loopStartIndex, + renderTypeList: [FlowNodeInputTypeEnum.hidden], + valueType: WorkflowIOValueTypeEnum.number, + label: i18nT('workflow:Array_element_index') } ], - outputs: [] + outputs: [ + { + id: NodeOutputKeyEnum.loopStartIndex, + key: NodeOutputKeyEnum.loopStartIndex, + label: i18nT('workflow:Array_element_index'), + type: FlowNodeOutputTypeEnum.static, + valueType: WorkflowIOValueTypeEnum.number + } + ] }; diff --git a/packages/global/core/workflow/template/system/readFiles/index.tsx b/packages/global/core/workflow/template/system/readFiles/index.tsx index 8018eda74..30f373200 100644 --- a/packages/global/core/workflow/template/system/readFiles/index.tsx +++ b/packages/global/core/workflow/template/system/readFiles/index.tsx @@ -23,8 +23,9 @@ export const ReadFilesNode: FlowNodeTemplateType = { name: i18nT('app:workflow.read_files'), intro: i18nT('app:workflow.read_files_tip'), showStatus: true, - version: '489', - isTool: true, + version: '4812', + isTool: false, + courseUrl: '/docs/guide/course/fileinput/', inputs: [ { key: NodeInputKeyEnum.fileUrlList, diff --git a/packages/global/core/workflow/template/system/sandbox/index.ts b/packages/global/core/workflow/template/system/sandbox/index.ts index b66ac4bcc..5325c5e3b 100644 --- a/packages/global/core/workflow/template/system/sandbox/index.ts +++ b/packages/global/core/workflow/template/system/sandbox/index.ts @@ -26,7 +26,7 @@ export const CodeNode: FlowNodeTemplateType = { name: i18nT('workflow:code_execution'), intro: i18nT('workflow:execute_a_simple_script_code_usually_for_complex_data_processing'), showStatus: true, - courseUrl: '/docs/workflow/modules/sandbox/', + courseUrl: '/docs/guide/workbench/workflow/sandbox/', version: '482', inputs: [ { diff --git a/packages/global/core/workflow/template/system/textEditor.ts b/packages/global/core/workflow/template/system/textEditor.ts index c7f97d2e9..511f28ba2 100644 --- a/packages/global/core/workflow/template/system/textEditor.ts +++ b/packages/global/core/workflow/template/system/textEditor.ts @@ -23,18 +23,9 @@ export const TextEditorNode: FlowNodeTemplateType = { avatar: 'core/workflow/template/textConcat', name: i18nT('workflow:text_concatenation'), intro: i18nT('workflow:intro_text_concatenation'), - courseUrl: '/docs/workflow/modules/text_editor/', - version: '486', + courseUrl: '/docs/guide/workbench/workflow/text_editor/', + version: '4813', inputs: [ - { - ...Input_Template_DynamicInput, - description: i18nT('workflow:dynamic_input_description_concat'), - customInputConfig: { - selectValueTypeList: Object.values(WorkflowIOValueTypeEnum), - showDescription: false, - showDefaultValue: false - } - }, { key: NodeInputKeyEnum.textareaInput, renderTypeList: [FlowNodeInputTypeEnum.textarea], diff --git a/packages/global/core/workflow/template/system/tools.ts b/packages/global/core/workflow/template/system/tools.ts index bd91596d7..29406a193 100644 --- a/packages/global/core/workflow/template/system/tools.ts +++ b/packages/global/core/workflow/template/system/tools.ts @@ -20,6 +20,7 @@ import { chatNodeSystemPromptTip, systemPromptTip } from '../tip'; import { LLMModelTypeEnum } from '../../../ai/constants'; import { getHandleConfig } from '../utils'; import { i18nT } from '../../../../../web/i18n/utils'; +import { Input_Template_File_Link_Prompt } from '../input'; export const ToolModule: FlowNodeTemplateType = { id: FlowNodeTypeEnum.tools, @@ -31,8 +32,8 @@ export const ToolModule: FlowNodeTemplateType = { name: i18nT('workflow:template.tool_call'), intro: i18nT('workflow:template.tool_call_intro'), showStatus: true, - courseUrl: '/docs/workflow/modules/tool/', - version: '481', + courseUrl: '/docs/guide/workbench/workflow/tool/', + version: '4813', inputs: [ { ...Input_Template_SettingAiModel, @@ -67,6 +68,7 @@ export const ToolModule: FlowNodeTemplateType = { placeholder: chatNodeSystemPromptTip }, Input_Template_History, + Input_Template_File_Link_Prompt, Input_Template_UserChatInput ], outputs: [ diff --git a/packages/global/core/workflow/template/system/variableUpdate/index.tsx b/packages/global/core/workflow/template/system/variableUpdate/index.tsx index 3ad693f67..79c479891 100644 --- a/packages/global/core/workflow/template/system/variableUpdate/index.tsx +++ b/packages/global/core/workflow/template/system/variableUpdate/index.tsx @@ -20,6 +20,7 @@ export const VariableUpdateNode: FlowNodeTemplateType = { showStatus: false, isTool: true, version: '481', + courseUrl: '/docs/guide/workbench/workflow/variable_update/', inputs: [ { key: NodeInputKeyEnum.updateList, diff --git a/packages/global/core/workflow/template/system/variableUpdate/type.d.ts b/packages/global/core/workflow/template/system/variableUpdate/type.d.ts index b46d730ee..c0ac23ddf 100644 --- a/packages/global/core/workflow/template/system/variableUpdate/type.d.ts +++ b/packages/global/core/workflow/template/system/variableUpdate/type.d.ts @@ -1,10 +1,10 @@ import { FlowNodeInputTypeEnum } from '../../../node/constant'; -import { ReferenceValueProps } from '../../..//type/io'; +import { ReferenceItemValueType, ReferenceValueType } from '../../..//type/io'; import { WorkflowIOValueTypeEnum } from '../../../constants'; export type TUpdateListItem = { - variable?: ReferenceValueProps; - value: ReferenceValueProps; + variable?: ReferenceItemValueType; + value?: ReferenceValueType; // input: ['',value], reference: [nodeId,outputId] valueType?: WorkflowIOValueTypeEnum; renderType: FlowNodeInputTypeEnum.input | FlowNodeInputTypeEnum.reference; }; diff --git a/packages/global/core/workflow/template/system/workflowStart.ts b/packages/global/core/workflow/template/system/workflowStart.ts index 930ced6ca..42d08b824 100644 --- a/packages/global/core/workflow/template/system/workflowStart.ts +++ b/packages/global/core/workflow/template/system/workflowStart.ts @@ -30,7 +30,7 @@ export const WorkflowStart: FlowNodeTemplateType = { intro: '', forbidDelete: true, unique: true, - courseUrl: '/docs/workflow/modules/input/', + courseUrl: '/docs/guide/workbench/workflow/input/', version: '481', inputs: [{ ...Input_Template_UserChatInput, toolDescription: i18nT('workflow:user_question') }], outputs: [ @@ -43,6 +43,3 @@ export const WorkflowStart: FlowNodeTemplateType = { } ] }; - -export const isWorkflowStartOutput = (key?: string) => - !!WorkflowStart.outputs.find((output) => output.key === key); diff --git a/packages/global/core/workflow/type/io.d.ts b/packages/global/core/workflow/type/io.d.ts index eb46e6dda..3653e2a12 100644 --- a/packages/global/core/workflow/type/io.d.ts +++ b/packages/global/core/workflow/type/io.d.ts @@ -56,6 +56,11 @@ export type FlowNodeInputItemType = InputComponentPropsType & { canEdit?: boolean; // dynamic inputs isPro?: boolean; // Pro version field isToolOutput?: boolean; + + // file + canSelectFile?: boolean; + canSelectImg?: boolean; + maxFiles?: number; }; export type FlowNodeOutputItemType = { @@ -75,4 +80,6 @@ export type FlowNodeOutputItemType = { customFieldConfig?: CustomFieldConfigType; }; -export type ReferenceValueProps = [string, string | undefined]; +export type ReferenceItemValueType = [string, string | undefined]; +export type ReferenceArrayValueType = ReferenceItemValueType[]; +export type ReferenceValueType = ReferenceItemValueType | ReferenceArrayValueType; diff --git a/packages/global/core/workflow/utils.ts b/packages/global/core/workflow/utils.ts index 6fbb98138..d4a5a4a29 100644 --- a/packages/global/core/workflow/utils.ts +++ b/packages/global/core/workflow/utils.ts @@ -12,7 +12,12 @@ import { VARIABLE_NODE_ID, NodeOutputKeyEnum } from './constants'; -import { FlowNodeInputItemType, FlowNodeOutputItemType, ReferenceValueProps } from './type/io.d'; +import { + FlowNodeInputItemType, + FlowNodeOutputItemType, + ReferenceArrayValueType, + ReferenceItemValueType +} from './type/io.d'; import { StoreNodeItemType } from './type/node'; import type { VariableItemType, @@ -30,8 +35,8 @@ import { } from '../app/constants'; import { IfElseResultEnum } from './template/system/ifElse/constant'; import { RuntimeNodeItemType } from './runtime/type'; -import { getReferenceVariableValue } from './runtime/utils'; import { + Input_Template_File_Link, Input_Template_History, Input_Template_Stream_MODE, Input_Template_UserChatInput @@ -261,8 +266,10 @@ export const appData2FlowNodeIO = ({ inputs: [ Input_Template_Stream_MODE, Input_Template_History, + ...(chatConfig?.fileSelectConfig?.canSelectFile || chatConfig?.fileSelectConfig?.canSelectImg + ? [Input_Template_File_Link] + : []), Input_Template_UserChatInput, - // ...(showFileLink ? [Input_Template_File_Link] : []), ...variableInput ], outputs: [ @@ -298,9 +305,37 @@ export const formatEditorVariablePickerIcon = ( })); }; -export const isReferenceValue = (value: any, nodeIds: string[]): boolean => { - const validIdList = [VARIABLE_NODE_ID, ...nodeIds]; - return Array.isArray(value) && value.length === 2 && validIdList.includes(value[0]); +// Check the value is a valid reference value format: [variableId, outputId] +export const isValidReferenceValueFormat = (value: any): value is ReferenceItemValueType => { + return Array.isArray(value) && value.length === 2 && typeof value[0] === 'string'; +}; +/* + Check whether the value([variableId, outputId]) value is a valid reference value: + 1. The value must be an array of length 2 + 2. The first item of the array must be one of VARIABLE_NODE_ID or nodeIds +*/ +export const isValidReferenceValue = ( + value: any, + nodeIds: string[] +): value is ReferenceItemValueType => { + if (!isValidReferenceValueFormat(value)) return false; + + const validIdSet = new Set([VARIABLE_NODE_ID, ...nodeIds]); + return validIdSet.has(value[0]); +}; +/* + Check whether the value([variableId, outputId][]) value is a valid reference value array: + 1. The value must be an array + 2. The array must contain at least one element + 3. Each element in the array must be a valid reference value +*/ +export const isValidArrayReferenceValue = ( + value: any, + nodeIds: string[] +): value is ReferenceArrayValueType => { + if (!Array.isArray(value)) return false; + + return value.every((item) => isValidReferenceValue(item, nodeIds)); }; export const getElseIFLabel = (i: number) => { @@ -342,79 +377,6 @@ export const updatePluginInputByVariables = ( ); }; -// replace {{$xx.xx$}} variables for text -export function replaceEditorVariable({ - text, - nodes, - variables, - runningNode -}: { - text: any; - nodes: RuntimeNodeItemType[]; - variables: Record; // global variables - runningNode: RuntimeNodeItemType; -}) { - if (typeof text !== 'string') return text; - - const globalVariables = Object.keys(variables).map((key) => { - return { - nodeId: VARIABLE_NODE_ID, - id: key, - value: variables[key] - }; - }); - - // Upstream node outputs - const nodeVariables = nodes - .map((node) => { - return node.outputs.map((output) => { - return { - nodeId: node.nodeId, - id: output.id, - value: output.value - }; - }); - }) - .flat(); - - // Get runningNode inputs(Will be replaced with reference) - const customInputs = runningNode.inputs.flatMap((item) => { - if (Array.isArray(item.value)) { - return [ - { - id: item.key, - value: getReferenceVariableValue({ - value: item.value as ReferenceValueProps, - nodes, - variables - }), - nodeId: runningNode.nodeId - } - ]; - } - return [ - { - id: item.key, - value: item.value, - nodeId: runningNode.nodeId - } - ]; - }); - - const allVariables = [...globalVariables, ...nodeVariables, ...customInputs]; - - // Replace {{$xxx.xxx$}} to value - for (const key in allVariables) { - const variable = allVariables[key]; - const val = variable.value; - const formatVal = typeof val === 'object' ? JSON.stringify(val) : String(val); - - const regex = new RegExp(`\\{\\{\\$(${variable.nodeId}\\.${variable.id})\\$\\}\\}`, 'g'); - text = text.replace(regex, formatVal); - } - return text || ''; -} - /* Get plugin runtime input user query */ export const getPluginRunUserQuery = ({ pluginInputs, diff --git a/packages/global/support/outLink/type.d.ts b/packages/global/support/outLink/type.d.ts index ec882c00b..e33179c1a 100644 --- a/packages/global/support/outLink/type.d.ts +++ b/packages/global/support/outLink/type.d.ts @@ -51,6 +51,10 @@ export type OutLinkSchema = { // whether the response content is detailed responseDetail: boolean; + // whether to hide the node status + showNodeStatus?: boolean; + // whether to show the complete quote + showRawSource?: boolean; // response when request immediateResponse?: string; @@ -79,6 +83,8 @@ export type OutLinkEditType = { _id?: string; name: string; responseDetail?: OutLinkSchema['responseDetail']; + showNodeStatus?: OutLinkSchema['showNodeStatus']; + showRawSource?: OutLinkSchema['showRawSource']; // response when request immediateResponse?: string; // response when error or other situation diff --git a/packages/plugins/src/databaseConnection/template.json b/packages/plugins/src/databaseConnection/template.json index c4561019d..6aaa4cbf3 100644 --- a/packages/plugins/src/databaseConnection/template.json +++ b/packages/plugins/src/databaseConnection/template.json @@ -1,7 +1,7 @@ { - "author": "", + "author": "silencezhang", "version": "4811", - "name": "数据源配置", + "name": "数据库连接", "avatar": "core/workflow/template/datasource", "intro": "可连接常用数据库,并执行sql", "showStatus": true, diff --git a/packages/plugins/src/drawing/baseChart/template.json b/packages/plugins/src/drawing/baseChart/template.json index 5ff6013ea..c249ced14 100644 --- a/packages/plugins/src/drawing/baseChart/template.json +++ b/packages/plugins/src/drawing/baseChart/template.json @@ -1,5 +1,5 @@ { - "author": "", + "author": "silencezhang", "version": "4812", "name": "基础图表", "avatar": "core/workflow/template/baseChart", diff --git a/packages/plugins/src/drawing/template.json b/packages/plugins/src/drawing/template.json index d7ec486b3..62a4b1297 100644 --- a/packages/plugins/src/drawing/template.json +++ b/packages/plugins/src/drawing/template.json @@ -1,5 +1,5 @@ { - "author": "silencezhang7", + "author": "silencezhang", "version": "486", "name": "BI图表功能", "avatar": "core/workflow/template/BI", diff --git a/packages/service/common/file/read/utils.ts b/packages/service/common/file/read/utils.ts index dad2d8c10..08a77eacd 100644 --- a/packages/service/common/file/read/utils.ts +++ b/packages/service/common/file/read/utils.ts @@ -9,6 +9,7 @@ import type { ReadFileResponse } from '../../../worker/readFile/type'; import axios from 'axios'; import { addLog } from '../../system/log'; import { batchRun } from '@fastgpt/global/common/fn/utils'; +import { addHours } from 'date-fns'; export type readRawTextByLocalFileParams = { teamId: string; @@ -111,6 +112,7 @@ export const readRawContentByFileBuffer = async ({ type: MongoImageTypeEnum.collectionImage, base64Img: `data:${item.mime};base64,${item.base64}`, teamId, + expiredTime: addHours(new Date(), 1), metadata: { ...metadata, mime: item.mime diff --git a/packages/service/core/ai/config.ts b/packages/service/core/ai/config.ts index e1073b4c4..3b05d0729 100644 --- a/packages/service/core/ai/config.ts +++ b/packages/service/core/ai/config.ts @@ -1,5 +1,11 @@ import type { UserModelSchema } from '@fastgpt/global/support/user/type'; import OpenAI from '@fastgpt/global/core/ai'; +import { + ChatCompletionCreateParamsNonStreaming, + ChatCompletionCreateParamsStreaming +} from '@fastgpt/global/core/ai/type'; +import { getErrText } from '@fastgpt/global/common/error/utils'; +import { addLog } from '../../common/system/log'; export const openaiBaseUrl = process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1'; @@ -34,3 +40,55 @@ export const getAxiosConfig = (props?: { userKey?: UserModelSchema['openaiAccoun authorization: `Bearer ${apiKey}` }; }; + +type CompletionsBodyType = + | ChatCompletionCreateParamsNonStreaming + | ChatCompletionCreateParamsStreaming; +type InferResponseType = + T extends ChatCompletionCreateParamsStreaming + ? OpenAI.Chat.Completions.ChatCompletionChunk + : OpenAI.Chat.Completions.ChatCompletion; + +export const createChatCompletion = async ({ + body, + userKey, + timeout, + options +}: { + body: T; + userKey?: UserModelSchema['openaiAccount']; + timeout?: number; + options?: OpenAI.RequestOptions; +}): Promise<{ + response: InferResponseType; + isStreamResponse: boolean; +}> => { + try { + const formatTimeout = timeout ? timeout : body.stream ? 60000 : 600000; + const ai = getAIApi({ + userKey, + timeout: formatTimeout + }); + const response = await ai.chat.completions.create(body, options); + + const isStreamResponse = + typeof response === 'object' && + response !== null && + ('iterator' in response || 'controller' in response); + + return { + response: response as InferResponseType, + isStreamResponse + }; + } catch (error) { + addLog.error(`LLM response error`, error); + addLog.warn(`LLM response error`, { + baseUrl: userKey?.baseUrl, + requestBody: body + }); + if (userKey?.baseUrl) { + return Promise.reject(`您的 OpenAI key 出错了: ${getErrText(error)}`); + } + return Promise.reject(error); + } +}; diff --git a/packages/service/core/ai/embedding/index.ts b/packages/service/core/ai/embedding/index.ts index d0573bf20..89bb91b1f 100644 --- a/packages/service/core/ai/embedding/index.ts +++ b/packages/service/core/ai/embedding/index.ts @@ -55,7 +55,7 @@ export async function getVectorsByText({ model, input, type }: GetVectorProps) { return result; } catch (error) { - console.log(`Embedding Error`, error); + addLog.error(`Embedding Error`, error); return Promise.reject(error); } diff --git a/packages/service/core/ai/functions/createQuestionGuide.ts b/packages/service/core/ai/functions/createQuestionGuide.ts index fb921f4ff..bb332180c 100644 --- a/packages/service/core/ai/functions/createQuestionGuide.ts +++ b/packages/service/core/ai/functions/createQuestionGuide.ts @@ -1,5 +1,5 @@ import type { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type.d'; -import { getAIApi } from '../config'; +import { createChatCompletion } from '../config'; import { countGptMessagesTokens } from '../../../common/string/tiktoken/index'; import { loadRequestMessages } from '../../chat/utils'; import { llmCompletionsBodyFormat } from '../utils'; @@ -29,11 +29,8 @@ export async function createQuestionGuide({ } ]; - const ai = getAIApi({ - timeout: 480000 - }); - const data = await ai.chat.completions.create( - llmCompletionsBodyFormat( + const { response: data } = await createChatCompletion({ + body: llmCompletionsBodyFormat( { model, temperature: 0.1, @@ -46,7 +43,7 @@ export async function createQuestionGuide({ }, model ) - ); + }); const answer = data.choices?.[0]?.message?.content || ''; diff --git a/packages/service/core/ai/functions/queryExtension.ts b/packages/service/core/ai/functions/queryExtension.ts index e9535496e..519e03f1b 100644 --- a/packages/service/core/ai/functions/queryExtension.ts +++ b/packages/service/core/ai/functions/queryExtension.ts @@ -1,8 +1,7 @@ import { replaceVariable } from '@fastgpt/global/common/string/tools'; -import { getAIApi } from '../config'; +import { createChatCompletion } from '../config'; import { ChatItemType } from '@fastgpt/global/core/chat/type'; import { countGptMessagesTokens } from '../../../common/string/tiktoken/index'; -import { ChatCompletion, ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type'; import { chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt'; import { getLLMModel } from '../model'; import { llmCompletionsBodyFormat } from '../utils'; @@ -138,10 +137,6 @@ A: ${chatBg} const modelData = getLLMModel(model); - const ai = getAIApi({ - timeout: 480000 - }); - const messages = [ { role: 'user', @@ -150,20 +145,19 @@ A: ${chatBg} histories: concatFewShot }) } - ] as ChatCompletionMessageParam[]; + ] as any; - const result = (await ai.chat.completions.create( - llmCompletionsBodyFormat( + const { response: result } = await createChatCompletion({ + body: llmCompletionsBodyFormat( { stream: false, model: modelData.model, temperature: 0.01, - // @ts-ignore messages }, modelData ) - )) as ChatCompletion; + }); let answer = result.choices?.[0]?.message?.content || ''; if (!answer) { diff --git a/packages/service/core/ai/utils.ts b/packages/service/core/ai/utils.ts index cff7c92aa..ec95b61a8 100644 --- a/packages/service/core/ai/utils.ts +++ b/packages/service/core/ai/utils.ts @@ -48,14 +48,17 @@ export const computedTemperature = ({ type CompletionsBodyType = | ChatCompletionCreateParamsNonStreaming | ChatCompletionCreateParamsStreaming; +type InferCompletionsBody = T extends { stream: true } + ? ChatCompletionCreateParamsStreaming + : ChatCompletionCreateParamsNonStreaming; export const llmCompletionsBodyFormat = ( body: T, model: string | LLMModelItemType -) => { +): InferCompletionsBody => { const modelData = typeof model === 'string' ? getLLMModel(model) : model; if (!modelData) { - return body; + return body as InferCompletionsBody; } const requestBody: T = { @@ -81,5 +84,5 @@ export const llmCompletionsBodyFormat = ( // console.log(requestBody); - return requestBody; + return requestBody as InferCompletionsBody; }; diff --git a/packages/service/core/app/plugin/utils.ts b/packages/service/core/app/plugin/utils.ts index 4cbff4b95..4ea86b06e 100644 --- a/packages/service/core/app/plugin/utils.ts +++ b/packages/service/core/app/plugin/utils.ts @@ -7,14 +7,20 @@ import { PluginSourceEnum } from '@fastgpt/global/core/plugin/constants'; 1. Commercial plugin: n points per times 2. Other plugin: sum of children points */ -export const computedPluginUsage = async ( - plugin: PluginRuntimeType, - childrenUsage: ChatNodeUsageType[] -) => { +export const computedPluginUsage = async ({ + plugin, + childrenUsage, + error +}: { + plugin: PluginRuntimeType; + childrenUsage: ChatNodeUsageType[]; + error?: boolean; +}) => { const { source } = await splitCombinePluginId(plugin.id); // Commercial plugin: n points per times if (source === PluginSourceEnum.commercial) { + if (error) return 0; return plugin.currentCost ?? 0; } diff --git a/packages/service/core/chat/chatSchema.ts b/packages/service/core/chat/chatSchema.ts index 001802f9a..4aae6eef2 100644 --- a/packages/service/core/chat/chatSchema.ts +++ b/packages/service/core/chat/chatSchema.ts @@ -52,7 +52,6 @@ const ChatSchema = new Schema({ }, source: { type: String, - enum: Object.keys(ChatSourceMap), required: true }, shareId: { @@ -90,7 +89,7 @@ try { // get chat logs; ChatSchema.index({ teamId: 1, appId: 1, updateTime: -1 }, { background: true }); // get share chat history - ChatSchema.index({ shareId: 1, outLinkUid: 1, updateTime: -1, source: 1 }, { background: true }); + ChatSchema.index({ shareId: 1, outLinkUid: 1, updateTime: -1 }, { background: true }); // timer, clear history ChatSchema.index({ teamId: 1, updateTime: -1 }, { background: true }); diff --git a/packages/service/core/chat/pushChatLog.ts b/packages/service/core/chat/pushChatLog.ts new file mode 100644 index 000000000..501edfd38 --- /dev/null +++ b/packages/service/core/chat/pushChatLog.ts @@ -0,0 +1,195 @@ +import { addLog } from '../../common/system/log'; +import { MongoChatItem } from './chatItemSchema'; +import { MongoChat } from './chatSchema'; +import axios from 'axios'; +import { AIChatItemType, ChatItemType, UserChatItemType } from '@fastgpt/global/core/chat/type'; +import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants'; + +export type Metadata = { + [key: string]: { + label: string; + value: string; + }; +}; + +export const pushChatLog = ({ + chatId, + chatItemIdHuman, + chatItemIdAi, + appId, + metadata +}: { + chatId: string; + chatItemIdHuman: string; + chatItemIdAi: string; + appId: string; + metadata?: Metadata; +}) => { + const interval = Number(process.env.CHAT_LOG_INTERVAL); + const url = process.env.CHAT_LOG_URL; + if (!isNaN(interval) && interval > 0 && url) { + addLog.debug(`[ChatLogPush] push chat log after ${interval}ms`, { + appId, + chatItemIdHuman, + chatItemIdAi + }); + setTimeout(() => { + pushChatLogInternal({ chatId, chatItemIdHuman, chatItemIdAi, appId, url, metadata }); + }, interval); + } +}; + +type ChatItem = ChatItemType & { + userGoodFeedback?: string; + userBadFeedback?: string; + chatId: string; + responseData: { + moduleType: string; + runningTime: number; //s + historyPreview: { obj: string; value: string }[]; + }[]; + time: Date; +}; + +type ChatLog = { + title: string; + feedback: 'like' | 'dislike' | null; + chatItemId: string; + uid: string; + question: string; + answer: string; + chatId: string; + responseTime: number; + metadata: string; + sourceName: string; + createdAt: number; + sourceId: string; +}; + +const pushChatLogInternal = async ({ + chatId, + chatItemIdHuman, + chatItemIdAi, + appId, + url, + metadata +}: { + chatId: string; + chatItemIdHuman: string; + chatItemIdAi: string; + appId: string; + url: string; + metadata?: Metadata; +}) => { + try { + const [chatItemHuman, chatItemAi] = await Promise.all([ + MongoChatItem.findById(chatItemIdHuman).lean() as Promise, + MongoChatItem.findById(chatItemIdAi).lean() as Promise + ]); + + if (!chatItemHuman || !chatItemAi) { + return; + } + + const chat = await MongoChat.findOne({ chatId }).lean(); + + // addLog.warn('ChatLogDebug', chat); + // addLog.warn('ChatLogDebug', { chatItemHuman, chatItemAi }); + + if (!chat) { + return; + } + + const metadataString = JSON.stringify(metadata ?? {}); + + const uid = chat.outLinkUid || chat.tmbId; + // Pop last two items + const question = chatItemHuman.value + .map((item) => { + if (item.type === ChatItemValueTypeEnum.text) { + return item.text?.content; + } else if (item.type === ChatItemValueTypeEnum.file) { + if (item.file?.type === 'image') { + return `![${item.file?.name}](${item.file?.url})`; + } + return `[${item.file?.name}](${item.file?.url})`; + } + return ''; + }) + .join('\n'); + const answer = chatItemAi.value + .map((item) => { + const text = []; + if (item.text?.content) { + text.push(item.text?.content); + } + if (item.tools) { + text.push( + item.tools.map( + (tool) => + `\`\`\`json +${JSON.stringify( + { + name: tool.toolName, + params: tool.params, + response: tool.response + }, + null, + 2 +)} +\`\`\`` + ) + ); + } + if (item.interactive) { + text.push(`\`\`\`json +${JSON.stringify(item.interactive, null, 2)} + \`\`\``); + } + return text.join('\n'); + }) + .join('\n'); + + if (!question || !answer) { + addLog.error('[ChatLogPush] question or answer is empty', { + question: chatItemHuman.value, + answer: chatItemAi.value + }); + return; + } + + // computed response time + const responseData = chatItemAi.responseData; + const responseTime = + responseData?.reduce((acc, item) => acc + (item?.runningTime ?? 0), 0) || 0; + + const sourceIdPrefix = process.env.CHAT_LOG_SOURCE_ID_PREFIX ?? 'fastgpt-'; + + const chatLog: ChatLog = { + title: chat.title, + feedback: (() => { + if (chatItemAi.userGoodFeedback) { + return 'like'; + } else if (chatItemAi.userBadFeedback) { + return 'dislike'; + } else { + return null; + } + })(), + chatItemId: `${chatItemIdHuman},${chatItemIdAi}`, + uid, + question, + answer, + chatId, + responseTime: responseTime * 1000, + metadata: metadataString, + sourceName: chat.source ?? '-', + // @ts-ignore + createdAt: new Date(chatItemAi.time).getTime(), + sourceId: `${sourceIdPrefix}${appId}` + }; + await axios.post(`${url}/api/chat/push`, chatLog); + } catch (e) { + addLog.error('[ChatLogPush] error', e); + } +}; diff --git a/packages/service/core/chat/saveChat.ts b/packages/service/core/chat/saveChat.ts index 5614b94cc..1b78e1e70 100644 --- a/packages/service/core/chat/saveChat.ts +++ b/packages/service/core/chat/saveChat.ts @@ -1,4 +1,9 @@ -import type { AIChatItemType, UserChatItemType } from '@fastgpt/global/core/chat/type.d'; +import type { + AIChatItemType, + ChatItemType, + UserChatItemType +} from '@fastgpt/global/core/chat/type.d'; +import axios from 'axios'; import { MongoApp } from '../app/schema'; import { ChatItemValueTypeEnum, @@ -13,6 +18,7 @@ import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node'; import { getAppChatConfig, getGuideModule } from '@fastgpt/global/core/workflow/utils'; import { AppChatConfigType } from '@fastgpt/global/core/app/type'; import { mergeChatResponseData } from '@fastgpt/global/core/chat/utils'; +import { pushChatLog } from './pushChatLog'; type Props = { chatId: string; @@ -24,7 +30,7 @@ type Props = { variables?: Record; isUpdateUseTime: boolean; newTitle: string; - source: `${ChatSourceEnum}`; + source: string; shareId?: string; outLinkUid?: string; content: [UserChatItemType & { dataId?: string }, AIChatItemType & { dataId?: string }]; @@ -67,7 +73,7 @@ export async function saveChat({ }); await mongoSessionRun(async (session) => { - await MongoChatItem.insertMany( + const [{ _id: chatItemIdHuman }, { _id: chatItemIdAi }] = await MongoChatItem.insertMany( content.map((item) => ({ chatId, teamId, @@ -105,6 +111,13 @@ export async function saveChat({ upsert: true } ); + + pushChatLog({ + chatId, + chatItemIdHuman: String(chatItemIdHuman), + chatItemIdAi: String(chatItemIdAi), + appId + }); }); if (isUpdateUseTime) { diff --git a/packages/service/core/chat/utils.ts b/packages/service/core/chat/utils.ts index 56b74aa47..c68d9227b 100644 --- a/packages/service/core/chat/utils.ts +++ b/packages/service/core/chat/utils.ts @@ -109,7 +109,7 @@ export const loadRequestMessages = async ({ } return Promise.all( messages.map(async (item) => { - if (item.type === 'image_url') { + if (item.type === 'image_url' && process.env.MULTIPLE_DATA_TO_BASE64 === 'true') { // Remove url origin const imgUrl = (() => { if (origin && item.image_url.url.startsWith(origin)) { @@ -118,38 +118,51 @@ export const loadRequestMessages = async ({ return item.image_url.url; })(); - // If imgUrl is a local path, load image from local, and set url to base64 - if (imgUrl.startsWith('/')) { - addLog.debug('Load image from local server', { - baseUrl: serverRequestBaseUrl, - requestUrl: imgUrl - }); - const response = await axios.get(imgUrl, { - baseURL: serverRequestBaseUrl, - responseType: 'arraybuffer', - proxy: false - }); - const base64 = Buffer.from(response.data, 'binary').toString('base64'); - const imageType = - getFileContentTypeFromHeader(response.headers['content-type']) || - guessBase64ImageType(base64); + try { + // If imgUrl is a local path, load image from local, and set url to base64 + if (imgUrl.startsWith('/')) { + addLog.debug('Load image from local server', { + baseUrl: serverRequestBaseUrl, + requestUrl: imgUrl + }); + const response = await axios.get(imgUrl, { + baseURL: serverRequestBaseUrl, + responseType: 'arraybuffer', + proxy: false + }); + const base64 = Buffer.from(response.data, 'binary').toString('base64'); + const imageType = + getFileContentTypeFromHeader(response.headers['content-type']) || + guessBase64ImageType(base64); - return { - ...item, - image_url: { - ...item.image_url, - url: `data:${imageType};base64,${base64}` - } - }; + return { + ...item, + image_url: { + ...item.image_url, + url: `data:${imageType};base64,${base64}` + } + }; + } + + // 检查下这个图片是否可以被访问,如果不行的话,则过滤掉 + const response = await axios.head(imgUrl, { + timeout: 10000 + }); + if (response.status < 200 || response.status >= 400) { + addLog.info(`Filter invalid image: ${imgUrl}`); + return; + } + } catch (error) { + return; } } return item; }) - ); + ).then((res) => res.filter(Boolean) as ChatCompletionContentPart[]); }; // Split question text and image const parseStringWithImages = (input: string): ChatCompletionContentPart[] => { - if (!useVision) { + if (!useVision || input.length > 500) { return [{ type: 'text', text: input || '' }]; } @@ -170,8 +183,8 @@ export const loadRequestMessages = async ({ }); }); - // Too many images or too long text, return text - if (httpsImages.length > 4 || input.length > 1000) { + // Too many images return text + if (httpsImages.length > 4) { return [{ type: 'text', text: input || '' }]; } @@ -179,7 +192,7 @@ export const loadRequestMessages = async ({ result.push({ type: 'text', text: input }); return result; }; - // Parse user content(text and img) + // Parse user content(text and img) Store history => api messages const parseUserContent = async (content: string | ChatCompletionContentPart[]) => { if (typeof content === 'string') { return loadImageToBase64(parseStringWithImages(content)); diff --git a/packages/service/core/dataset/search/controller.ts b/packages/service/core/dataset/search/controller.ts index 9f85231cb..c5368726c 100644 --- a/packages/service/core/dataset/search/controller.ts +++ b/packages/service/core/dataset/search/controller.ts @@ -12,7 +12,7 @@ import { DatasetDataWithCollectionType, SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type'; -import { DatasetColCollectionName, MongoDatasetCollection } from '../collection/schema'; +import { MongoDatasetCollection } from '../collection/schema'; import { reRankRecall } from '../../../core/ai/rerank'; import { countPromptTokens } from '../../../common/string/tiktoken/index'; import { datasetSearchResultConcat } from '@fastgpt/global/core/dataset/search/utils'; @@ -320,11 +320,13 @@ export async function searchDatasetData(props: SearchDatasetDataProps) { const fullTextRecall = async ({ query, limit, - filterCollectionIdList + filterCollectionIdList, + forbidCollectionIdList }: { query: string; limit: number; filterCollectionIdList?: string[]; + forbidCollectionIdList: string[]; }): Promise<{ fullTextRecallResults: SearchDataResponseItemType[]; tokenLen: number; @@ -351,6 +353,13 @@ export async function searchDatasetData(props: SearchDatasetDataProps) { $in: filterCollectionIdList.map((id) => new Types.ObjectId(id)) } } + : {}), + ...(forbidCollectionIdList && forbidCollectionIdList.length > 0 + ? { + collectionId: { + $nin: forbidCollectionIdList.map((id) => new Types.ObjectId(id)) + } + } : {}) } }, @@ -367,31 +376,6 @@ export async function searchDatasetData(props: SearchDatasetDataProps) { { $limit: limit }, - { - $lookup: { - from: DatasetColCollectionName, - let: { collectionId: '$collectionId' }, - pipeline: [ - { - $match: { - $expr: { $eq: ['$_id', '$$collectionId'] }, - forbid: { $eq: true } // 匹配被禁用的数据 - } - }, - { - $project: { - _id: 1 // 只需要_id字段来确认匹配 - } - } - ], - as: 'collection' - } - }, - { - $match: { - collection: { $eq: [] } // 没有 forbid=true 的数据 - } - }, { $project: { _id: 1, @@ -509,7 +493,8 @@ export async function searchDatasetData(props: SearchDatasetDataProps) { fullTextRecall({ query, limit: fullTextLimit, - filterCollectionIdList + filterCollectionIdList, + forbidCollectionIdList }) ]); totalTokens += tokens; diff --git a/packages/service/core/workflow/dispatch/agent/classifyQuestion.ts b/packages/service/core/workflow/dispatch/agent/classifyQuestion.ts index 3f2c78267..f350be359 100644 --- a/packages/service/core/workflow/dispatch/agent/classifyQuestion.ts +++ b/packages/service/core/workflow/dispatch/agent/classifyQuestion.ts @@ -2,7 +2,7 @@ import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt'; import { countMessagesTokens } from '../../../../common/string/tiktoken/index'; import type { ChatItemType } from '@fastgpt/global/core/chat/type.d'; import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; -import { getAIApi } from '../../../ai/config'; +import { createChatCompletion } from '../../../ai/config'; import type { ClassifyQuestionAgentItemType } from '@fastgpt/global/core/workflow/template/system/classifyQuestion/type'; import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; @@ -120,13 +120,8 @@ const completions = async ({ useVision: false }); - const ai = getAIApi({ - userKey: user.openaiAccount, - timeout: 480000 - }); - - const data = await ai.chat.completions.create( - llmCompletionsBodyFormat( + const { response: data } = await createChatCompletion({ + body: llmCompletionsBodyFormat( { model: cqModel.model, temperature: 0.01, @@ -134,8 +129,9 @@ const completions = async ({ stream: false }, cqModel - ) - ); + ), + userKey: user.openaiAccount + }); const answer = data.choices?.[0].message?.content || ''; // console.log(JSON.stringify(chats2GPTMessages({ messages, reserveId: false }), null, 2)); diff --git a/packages/service/core/workflow/dispatch/agent/extract.ts b/packages/service/core/workflow/dispatch/agent/extract.ts index 5c0f999c6..84e257b1f 100644 --- a/packages/service/core/workflow/dispatch/agent/extract.ts +++ b/packages/service/core/workflow/dispatch/agent/extract.ts @@ -6,7 +6,7 @@ import { countGptMessagesTokens } from '../../../../common/string/tiktoken/index'; import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; -import { getAIApi } from '../../../ai/config'; +import { createChatCompletion } from '../../../ai/config'; import type { ContextExtractAgentItemType } from '@fastgpt/global/core/workflow/template/system/contextExtract/type'; import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; @@ -222,13 +222,8 @@ const toolChoice = async (props: ActionProps) => { } ]; - const ai = getAIApi({ - userKey: user.openaiAccount, - timeout: 480000 - }); - - const response = await ai.chat.completions.create( - llmCompletionsBodyFormat( + const { response } = await createChatCompletion({ + body: llmCompletionsBodyFormat( { model: extractModel.model, temperature: 0.01, @@ -237,8 +232,9 @@ const toolChoice = async (props: ActionProps) => { tool_choice: { type: 'function', function: { name: agentFunName } } }, extractModel - ) - ); + ), + userKey: user.openaiAccount + }); const arg: Record = (() => { try { @@ -272,13 +268,8 @@ const functionCall = async (props: ActionProps) => { const { agentFunction, filterMessages } = await getFunctionCallSchema(props); const functions: ChatCompletionCreateParams.Function[] = [agentFunction]; - const ai = getAIApi({ - userKey: user.openaiAccount, - timeout: 480000 - }); - - const response = await ai.chat.completions.create( - llmCompletionsBodyFormat( + const { response } = await createChatCompletion({ + body: llmCompletionsBodyFormat( { model: extractModel.model, temperature: 0.01, @@ -289,8 +280,9 @@ const functionCall = async (props: ActionProps) => { functions }, extractModel - ) - ); + ), + userKey: user.openaiAccount + }); try { const arg = JSON.parse(response?.choices?.[0]?.message?.function_call?.arguments || ''); @@ -358,12 +350,8 @@ Human: ${content}` useVision: false }); - const ai = getAIApi({ - userKey: user.openaiAccount, - timeout: 480000 - }); - const data = await ai.chat.completions.create( - llmCompletionsBodyFormat( + const { response: data } = await createChatCompletion({ + body: llmCompletionsBodyFormat( { model: extractModel.model, temperature: 0.01, @@ -371,8 +359,9 @@ Human: ${content}` stream: false }, extractModel - ) - ); + ), + userKey: user.openaiAccount + }); const answer = data.choices?.[0].message?.content || ''; // parse response diff --git a/packages/service/core/workflow/dispatch/agent/runTool/functionCall.ts b/packages/service/core/workflow/dispatch/agent/runTool/functionCall.ts index 9ebcb8ddb..6084fb994 100644 --- a/packages/service/core/workflow/dispatch/agent/runTool/functionCall.ts +++ b/packages/service/core/workflow/dispatch/agent/runTool/functionCall.ts @@ -1,5 +1,4 @@ -import { LLMModelItemType } from '@fastgpt/global/core/ai/model.d'; -import { getAIApi } from '../../../../ai/config'; +import { createChatCompletion } from '../../../../ai/config'; import { filterGPTMessageByMaxTokens, loadRequestMessages } from '../../../../chat/utils'; import { ChatCompletion, @@ -22,12 +21,13 @@ import { DispatchFlowResponse, WorkflowResponseType } from '../../type'; import { countGptMessagesTokens } from '../../../../../common/string/tiktoken/index'; import { getNanoid, sliceStrStartEnd } from '@fastgpt/global/common/string/tools'; import { AIChatItemType } from '@fastgpt/global/core/chat/type'; -import { chats2GPTMessages, GPTMessages2Chats } from '@fastgpt/global/core/chat/adapt'; +import { GPTMessages2Chats } from '@fastgpt/global/core/chat/adapt'; import { formatToolResponse, initToolCallEdges, initToolNodes } from './utils'; import { computedMaxToken, llmCompletionsBodyFormat } from '../../../../ai/utils'; import { toolValueTypeList } from '@fastgpt/global/core/workflow/constants'; import { WorkflowInteractiveResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type'; -import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; +import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants'; +import { i18nT } from '../../../../../../web/i18n/utils'; type FunctionRunResponseType = { toolRunResponse: DispatchFlowResponse; @@ -44,7 +44,7 @@ export const runToolWithFunctionCall = async ( requestOrigin, runtimeNodes, runtimeEdges, - node, + user, stream, workflowStreamResponse, params: { temperature = 0, maxToken = 4000, aiChatVision } @@ -216,17 +216,18 @@ export const runToolWithFunctionCall = async ( // console.log(JSON.stringify(requestMessages, null, 2)); /* Run llm */ - const ai = getAIApi({ - timeout: 480000 - }); - const aiResponse = await ai.chat.completions.create(requestBody, { - headers: { - Accept: 'application/json, text/plain, */*' + const { response: aiResponse, isStreamResponse } = await createChatCompletion({ + body: requestBody, + userKey: user.openaiAccount, + options: { + headers: { + Accept: 'application/json, text/plain, */*' + } } }); const { answer, functionCalls } = await (async () => { - if (res && stream) { + if (res && isStreamResponse) { return streamResponse({ res, toolNodes, @@ -549,7 +550,7 @@ async function streamResponse({ } if (!textAnswer && functionCalls.length === 0) { - return Promise.reject('LLM api response empty'); + return Promise.reject(i18nT('chat:LLM_model_response_empty')); } return { answer: textAnswer, functionCalls }; diff --git a/packages/service/core/workflow/dispatch/agent/runTool/index.ts b/packages/service/core/workflow/dispatch/agent/runTool/index.ts index 74bfa25e0..b70623189 100644 --- a/packages/service/core/workflow/dispatch/agent/runTool/index.ts +++ b/packages/service/core/workflow/dispatch/agent/runTool/index.ts @@ -25,45 +25,17 @@ import { replaceVariable } from '@fastgpt/global/common/string/tools'; import { getMultiplePrompt, Prompt_Tool_Call } from './constants'; import { filterToolResponseToPreview } from './utils'; import { InteractiveNodeResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type'; +import { getFileContentFromLinks, getHistoryFileLinks } from '../../tools/readFiles'; +import { parseUrlToFileType } from '@fastgpt/global/common/file/tools'; +import { Prompt_DocumentQuote } from '@fastgpt/global/core/ai/prompt/AIChat'; +import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; +import { postTextCensor } from '../../../../../common/api/requestPlusApi'; type Response = DispatchNodeResultType<{ [NodeOutputKeyEnum.answerText]: string; [DispatchNodeResponseKeyEnum.interactive]?: InteractiveNodeResponseType; }>; -/* - Tool call, auth add file prompt to question。 - Guide the LLM to call tool. -*/ -export const toolCallMessagesAdapt = ({ - userInput -}: { - userInput: UserChatItemValueItemType[]; -}) => { - const files = userInput.filter((item) => item.type === 'file'); - - if (files.length > 0) { - return userInput.map((item) => { - if (item.type === 'text') { - const filesCount = files.filter((file) => file.file?.type === 'file').length; - const imgCount = files.filter((file) => file.file?.type === 'image').length; - const text = item.text?.content || ''; - - return { - ...item, - text: { - content: getMultiplePrompt({ fileCount: filesCount, imgCount, question: text }) - } - }; - } - - return item; - }); - } - - return userInput; -}; - export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise => { const { node: { nodeId, name, isEntry }, @@ -71,11 +43,22 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise< runtimeEdges, histories, query, - - params: { model, systemPrompt, userChatInput, history = 6 } + requestOrigin, + chatConfig, + runningAppInfo: { teamId }, + user, + params: { + model, + systemPrompt, + userChatInput, + history = 6, + fileUrlList: fileLinks, + aiChatVision + } } = props; const toolModel = getLLMModel(model); + const useVision = aiChatVision && toolModel.vision; const chatHistories = getHistories(history, histories); const toolNodeIds = filterToolNodeIdByEdges({ nodeId, edges: runtimeEdges }); @@ -109,18 +92,44 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise< } })(); props.node.isEntry = false; + const hasReadFilesTool = toolNodes.some( + (item) => item.flowNodeType === FlowNodeTypeEnum.readFiles + ); + + const globalFiles = chatValue2RuntimePrompt(query).files; + const { documentQuoteText, userFiles } = await getMultiInput({ + histories: chatHistories, + requestOrigin, + maxFiles: chatConfig?.fileSelectConfig?.maxFiles || 20, + teamId, + fileLinks, + inputFiles: globalFiles, + hasReadFilesTool + }); + + const concatenateSystemPrompt = [ + toolModel.defaultSystemChatPrompt, + systemPrompt, + documentQuoteText + ? replaceVariable(Prompt_DocumentQuote, { + quote: documentQuoteText + }) + : '' + ] + .filter(Boolean) + .join('\n\n===---===---===\n\n'); const messages: ChatItemType[] = (() => { const value: ChatItemType[] = [ - ...getSystemPrompt_ChatItemType(toolModel.defaultSystemChatPrompt), - ...getSystemPrompt_ChatItemType(systemPrompt), + ...getSystemPrompt_ChatItemType(concatenateSystemPrompt), // Add file input prompt to histories ...chatHistories.map((item) => { if (item.obj === ChatRoleEnum.Human) { return { ...item, value: toolCallMessagesAdapt({ - userInput: item.value + userInput: item.value, + skip: !hasReadFilesTool }) }; } @@ -129,9 +138,10 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise< { obj: ChatRoleEnum.Human, value: toolCallMessagesAdapt({ + skip: !hasReadFilesTool, userInput: runtimePrompt2ChatsValue({ text: userChatInput, - files: chatValue2RuntimePrompt(query).files + files: userFiles }) }) } @@ -142,6 +152,15 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise< return value; })(); + // censor model and system key + if (toolModel.censor && !user.openaiAccount?.key) { + await postTextCensor({ + text: `${systemPrompt} + ${userChatInput} + ` + }); + } + const { toolWorkflowInteractiveResponse, dispatchFlowResponse, // tool flow response @@ -177,7 +196,7 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise< } const lastMessage = adaptMessages[adaptMessages.length - 1]; - if (typeof lastMessage.content === 'string') { + if (typeof lastMessage?.content === 'string') { lastMessage.content = replaceVariable(Prompt_Tool_Call, { question: lastMessage.content }); @@ -209,13 +228,14 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise< tokens: toolNodeTokens, modelType: ModelTypeEnum.llm }); + const toolAIUsage = user.openaiAccount?.key ? 0 : totalPoints; // flat child tool response const childToolResponse = dispatchFlowResponse.map((item) => item.flowResponses).flat(); // concat tool usage const totalPointsUsage = - totalPoints + + toolAIUsage + dispatchFlowResponse.reduce((sum, item) => { const childrenTotal = item.flowUsages.reduce((sum, item) => sum + item.totalPoints, 0); return sum + childrenTotal; @@ -232,24 +252,130 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise< .join(''), [DispatchNodeResponseKeyEnum.assistantResponses]: previewAssistantResponses, [DispatchNodeResponseKeyEnum.nodeResponse]: { + // 展示的积分消耗 totalPoints: totalPointsUsage, toolCallTokens: toolNodeTokens, childTotalPoints: flatUsages.reduce((sum, item) => sum + item.totalPoints, 0), model: modelName, query: userChatInput, - historyPreview: getHistoryPreview(GPTMessages2Chats(completeMessages, false), 10000), + historyPreview: getHistoryPreview( + GPTMessages2Chats(completeMessages, false), + 10000, + useVision + ), toolDetail: childToolResponse, mergeSignId: nodeId }, [DispatchNodeResponseKeyEnum.nodeDispatchUsages]: [ + // 工具调用本身的积分消耗 { moduleName: name, - totalPoints, + totalPoints: toolAIUsage, model: modelName, tokens: toolNodeTokens }, + // 工具的消耗 ...flatUsages ], [DispatchNodeResponseKeyEnum.interactive]: toolWorkflowInteractiveResponse }; }; + +const getMultiInput = async ({ + histories, + fileLinks, + requestOrigin, + maxFiles, + teamId, + inputFiles, + hasReadFilesTool +}: { + histories: ChatItemType[]; + fileLinks?: string[]; + requestOrigin?: string; + maxFiles: number; + teamId: string; + inputFiles: UserChatItemValueItemType['file'][]; + hasReadFilesTool: boolean; +}) => { + // Not file quote + if (!fileLinks || hasReadFilesTool) { + return { + documentQuoteText: '', + userFiles: inputFiles + }; + } + + const filesFromHistories = getHistoryFileLinks(histories); + const urls = [...fileLinks, ...filesFromHistories]; + + if (urls.length === 0) { + return { + documentQuoteText: '', + userFiles: [] + }; + } + + // Get files from histories + const { text } = await getFileContentFromLinks({ + // Concat fileUrlList and filesFromHistories; remove not supported files + urls, + requestOrigin, + maxFiles, + teamId + }); + + return { + documentQuoteText: text, + userFiles: fileLinks.map((url) => parseUrlToFileType(url)) + }; +}; + +/* +Tool call, auth add file prompt to question。 +Guide the LLM to call tool. +*/ +const toolCallMessagesAdapt = ({ + userInput, + skip +}: { + userInput: UserChatItemValueItemType[]; + skip?: boolean; +}): UserChatItemValueItemType[] => { + if (skip) return userInput; + + const files = userInput.filter((item) => item.type === 'file'); + + if (files.length > 0) { + const filesCount = files.filter((file) => file.file?.type === 'file').length; + const imgCount = files.filter((file) => file.file?.type === 'image').length; + + if (userInput.some((item) => item.type === 'text')) { + return userInput.map((item) => { + if (item.type === 'text') { + const text = item.text?.content || ''; + + return { + ...item, + text: { + content: getMultiplePrompt({ fileCount: filesCount, imgCount, question: text }) + } + }; + } + return item; + }); + } + + // Every input is a file + return [ + { + type: ChatItemValueTypeEnum.text, + text: { + content: getMultiplePrompt({ fileCount: filesCount, imgCount, question: '' }) + } + } + ]; + } + + return userInput; +}; diff --git a/packages/service/core/workflow/dispatch/agent/runTool/promptCall.ts b/packages/service/core/workflow/dispatch/agent/runTool/promptCall.ts index 9c176ecc9..661117752 100644 --- a/packages/service/core/workflow/dispatch/agent/runTool/promptCall.ts +++ b/packages/service/core/workflow/dispatch/agent/runTool/promptCall.ts @@ -1,4 +1,4 @@ -import { getAIApi } from '../../../../ai/config'; +import { createChatCompletion } from '../../../../ai/config'; import { filterGPTMessageByMaxTokens, loadRequestMessages } from '../../../../chat/utils'; import { ChatCompletion, @@ -29,6 +29,7 @@ import { WorkflowResponseType } from '../../type'; import { toolValueTypeList } from '@fastgpt/global/core/workflow/constants'; import { WorkflowInteractiveResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type'; import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants'; +import { i18nT } from '../../../../../../web/i18n/utils'; type FunctionCallCompletion = { id: string; @@ -51,7 +52,7 @@ export const runToolWithPromptCall = async ( requestOrigin, runtimeNodes, runtimeEdges, - node, + user, stream, workflowStreamResponse, params: { temperature = 0, maxToken = 4000, aiChatVision } @@ -224,18 +225,15 @@ export const runToolWithPromptCall = async ( // console.log(JSON.stringify(requestMessages, null, 2)); /* Run llm */ - const ai = getAIApi({ - timeout: 480000 - }); - const aiResponse = await ai.chat.completions.create(requestBody, { - headers: { - Accept: 'application/json, text/plain, */*' + const { response: aiResponse, isStreamResponse } = await createChatCompletion({ + body: requestBody, + userKey: user.openaiAccount, + options: { + headers: { + Accept: 'application/json, text/plain, */*' + } } }); - const isStreamResponse = - typeof aiResponse === 'object' && - aiResponse !== null && - ('iterator' in aiResponse || 'controller' in aiResponse); const answer = await (async () => { if (res && isStreamResponse) { @@ -537,7 +535,7 @@ async function streamResponse({ } if (!textAnswer) { - return Promise.reject('LLM api response empty'); + return Promise.reject(i18nT('chat:LLM_model_response_empty')); } return { answer: textAnswer.trim() }; } diff --git a/packages/service/core/workflow/dispatch/agent/runTool/toolChoice.ts b/packages/service/core/workflow/dispatch/agent/runTool/toolChoice.ts index b730ae66a..fbea9acab 100644 --- a/packages/service/core/workflow/dispatch/agent/runTool/toolChoice.ts +++ b/packages/service/core/workflow/dispatch/agent/runTool/toolChoice.ts @@ -1,4 +1,4 @@ -import { getAIApi } from '../../../../ai/config'; +import { createChatCompletion } from '../../../../ai/config'; import { filterGPTMessageByMaxTokens, loadRequestMessages } from '../../../../chat/utils'; import { ChatCompletion, @@ -28,6 +28,7 @@ import { addLog } from '../../../../../common/system/log'; import { toolValueTypeList } from '@fastgpt/global/core/workflow/constants'; import { WorkflowInteractiveResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type'; import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants'; +import { i18nT } from '../../../../../../web/i18n/utils'; type ToolRunResponseType = { toolRunResponse: DispatchFlowResponse; @@ -91,6 +92,7 @@ export const runToolWithToolChoice = async ( runtimeNodes, runtimeEdges, stream, + user, workflowStreamResponse, params: { temperature = 0, maxToken = 4000, aiChatVision } } = workflowProps; @@ -268,279 +270,267 @@ export const runToolWithToolChoice = async ( }, toolModel ); - // console.log(JSON.stringify(requestMessages, null, 2), '==requestBody'); + // console.log(JSON.stringify(requestBody, null, 2), '==requestBody'); /* Run llm */ - const ai = getAIApi({ - timeout: 480000 - }); - - try { - const aiResponse = await ai.chat.completions.create(requestBody, { + const { response: aiResponse, isStreamResponse } = await createChatCompletion({ + body: requestBody, + userKey: user.openaiAccount, + options: { headers: { Accept: 'application/json, text/plain, */*' } - }); - const isStreamResponse = - typeof aiResponse === 'object' && - aiResponse !== null && - ('iterator' in aiResponse || 'controller' in aiResponse); + } + }); - const { answer, toolCalls } = await (async () => { - if (res && isStreamResponse) { - return streamResponse({ - res, - workflowStreamResponse, - toolNodes, - stream: aiResponse - }); - } else { - const result = aiResponse as ChatCompletion; - const calls = result.choices?.[0]?.message?.tool_calls || []; - const answer = result.choices?.[0]?.message?.content || ''; + const { answer, toolCalls } = await (async () => { + if (res && isStreamResponse) { + return streamResponse({ + res, + workflowStreamResponse, + toolNodes, + stream: aiResponse + }); + } else { + const result = aiResponse as ChatCompletion; + const calls = result.choices?.[0]?.message?.tool_calls || []; + const answer = result.choices?.[0]?.message?.content || ''; - // 加上name和avatar - const toolCalls = calls.map((tool) => { - const toolNode = toolNodes.find((item) => item.nodeId === tool.function?.name); - return { - ...tool, - toolName: toolNode?.name || '', - toolAvatar: toolNode?.avatar || '' - }; - }); + // 加上name和avatar + const toolCalls = calls.map((tool) => { + const toolNode = toolNodes.find((item) => item.nodeId === tool.function?.name); + return { + ...tool, + toolName: toolNode?.name || '', + toolAvatar: toolNode?.avatar || '' + }; + }); - // 不支持 stream 模式的模型的流失响应 - toolCalls.forEach((tool) => { - workflowStreamResponse?.({ - event: SseResponseEventEnum.toolCall, - data: { - tool: { - id: tool.id, - toolName: tool.toolName, - toolAvatar: tool.toolAvatar, - functionName: tool.function.name, - params: tool.function?.arguments ?? '', - response: '' - } + // 不支持 stream 模式的模型的流失响应 + toolCalls.forEach((tool) => { + workflowStreamResponse?.({ + event: SseResponseEventEnum.toolCall, + data: { + tool: { + id: tool.id, + toolName: tool.toolName, + toolAvatar: tool.toolAvatar, + functionName: tool.function.name, + params: tool.function?.arguments ?? '', + response: '' } - }); + } + }); + }); + if (answer) { + workflowStreamResponse?.({ + event: SseResponseEventEnum.fastAnswer, + data: textAdaptGptResponse({ + text: answer + }) + }); + } + + return { + answer, + toolCalls: toolCalls + }; + } + })(); + + // Run the selected tool by LLM. + const toolsRunResponse = ( + await Promise.all( + toolCalls.map(async (tool) => { + const toolNode = toolNodes.find((item) => item.nodeId === tool.function?.name); + + if (!toolNode) return; + + const startParams = (() => { + try { + return json5.parse(tool.function.arguments); + } catch (error) { + return {}; + } + })(); + + initToolNodes(runtimeNodes, [toolNode.nodeId], startParams); + const toolRunResponse = await dispatchWorkFlow({ + ...workflowProps, + isToolCall: true + }); + + const stringToolResponse = formatToolResponse(toolRunResponse.toolResponses); + + const toolMsgParams: ChatCompletionToolMessageParam = { + tool_call_id: tool.id, + role: ChatCompletionRequestMessageRoleEnum.Tool, + name: tool.function.name, + content: stringToolResponse + }; + + workflowStreamResponse?.({ + event: SseResponseEventEnum.toolResponse, + data: { + tool: { + id: tool.id, + toolName: '', + toolAvatar: '', + params: '', + response: sliceStrStartEnd(stringToolResponse, 5000, 5000) + } + } }); - if (answer) { - workflowStreamResponse?.({ - event: SseResponseEventEnum.fastAnswer, - data: textAdaptGptResponse({ - text: answer - }) - }); - } return { - answer, - toolCalls: toolCalls + toolRunResponse, + toolMsgParams }; + }) + ) + ).filter(Boolean) as ToolRunResponseType; + + const flatToolsResponseData = toolsRunResponse.map((item) => item.toolRunResponse).flat(); + // concat tool responses + const dispatchFlowResponse = response + ? response.dispatchFlowResponse.concat(flatToolsResponseData) + : flatToolsResponseData; + + if (toolCalls.length > 0 && !res?.closed) { + // Run the tool, combine its results, and perform another round of AI calls + const assistantToolMsgParams: ChatCompletionAssistantMessageParam[] = [ + ...(answer + ? [ + { + role: ChatCompletionRequestMessageRoleEnum.Assistant as 'assistant', + content: answer + } + ] + : []), + { + role: ChatCompletionRequestMessageRoleEnum.Assistant, + tool_calls: toolCalls } - })(); + ]; - // Run the selected tool by LLM. - const toolsRunResponse = ( - await Promise.all( - toolCalls.map(async (tool) => { - const toolNode = toolNodes.find((item) => item.nodeId === tool.function?.name); - - if (!toolNode) return; - - const startParams = (() => { - try { - return json5.parse(tool.function.arguments); - } catch (error) { - return {}; - } - })(); - - initToolNodes(runtimeNodes, [toolNode.nodeId], startParams); - const toolRunResponse = await dispatchWorkFlow({ - ...workflowProps, - isToolCall: true - }); - - const stringToolResponse = formatToolResponse(toolRunResponse.toolResponses); - - const toolMsgParams: ChatCompletionToolMessageParam = { - tool_call_id: tool.id, - role: ChatCompletionRequestMessageRoleEnum.Tool, - name: tool.function.name, - content: stringToolResponse - }; - - workflowStreamResponse?.({ - event: SseResponseEventEnum.toolResponse, - data: { - tool: { - id: tool.id, - toolName: '', - toolAvatar: '', - params: '', - response: sliceStrStartEnd(stringToolResponse, 5000, 5000) - } - } - }); - - return { - toolRunResponse, - toolMsgParams - }; - }) - ) - ).filter(Boolean) as ToolRunResponseType; - - const flatToolsResponseData = toolsRunResponse.map((item) => item.toolRunResponse).flat(); - // concat tool responses - const dispatchFlowResponse = response - ? response.dispatchFlowResponse.concat(flatToolsResponseData) - : flatToolsResponseData; - - if (toolCalls.length > 0 && !res?.closed) { - // Run the tool, combine its results, and perform another round of AI calls - const assistantToolMsgParams: ChatCompletionAssistantMessageParam[] = [ - ...(answer - ? [ - { - role: ChatCompletionRequestMessageRoleEnum.Assistant as 'assistant', - content: answer - } - ] - : []), - { - role: ChatCompletionRequestMessageRoleEnum.Assistant, - tool_calls: toolCalls - } - ]; - - /* + /* ... user assistant: tool data */ - const concatToolMessages = [ - ...requestMessages, - ...assistantToolMsgParams - ] as ChatCompletionMessageParam[]; + const concatToolMessages = [ + ...requestMessages, + ...assistantToolMsgParams + ] as ChatCompletionMessageParam[]; - // Only toolCall tokens are counted here, Tool response tokens count towards the next reply - const tokens = await countGptMessagesTokens(concatToolMessages, tools); - /* + // Only toolCall tokens are counted here, Tool response tokens count towards the next reply + const tokens = await countGptMessagesTokens(concatToolMessages, tools); + /* ... user assistant: tool data tool: tool response */ - const completeMessages = [ - ...concatToolMessages, - ...toolsRunResponse.map((item) => item?.toolMsgParams) - ]; + const completeMessages = [ + ...concatToolMessages, + ...toolsRunResponse.map((item) => item?.toolMsgParams) + ]; - /* + /* Get tool node assistant response history assistant current tool assistant tool child assistant */ - const toolNodeAssistant = GPTMessages2Chats([ - ...assistantToolMsgParams, - ...toolsRunResponse.map((item) => item?.toolMsgParams) - ])[0] as AIChatItemType; - const toolChildAssistants = flatToolsResponseData - .map((item) => item.assistantResponses) - .flat() - .filter((item) => item.type !== ChatItemValueTypeEnum.interactive); // 交互节点留着下次记录 - const toolNodeAssistants = [ - ...assistantResponses, - ...toolNodeAssistant.value, - ...toolChildAssistants - ]; + const toolNodeAssistant = GPTMessages2Chats([ + ...assistantToolMsgParams, + ...toolsRunResponse.map((item) => item?.toolMsgParams) + ])[0] as AIChatItemType; + const toolChildAssistants = flatToolsResponseData + .map((item) => item.assistantResponses) + .flat() + .filter((item) => item.type !== ChatItemValueTypeEnum.interactive); // 交互节点留着下次记录 + const toolNodeAssistants = [ + ...assistantResponses, + ...toolNodeAssistant.value, + ...toolChildAssistants + ]; - const runTimes = - (response?.runTimes || 0) + - flatToolsResponseData.reduce((sum, item) => sum + item.runTimes, 0); - const toolNodeTokens = response ? response.toolNodeTokens + tokens : tokens; + const runTimes = + (response?.runTimes || 0) + + flatToolsResponseData.reduce((sum, item) => sum + item.runTimes, 0); + const toolNodeTokens = response ? response.toolNodeTokens + tokens : tokens; - // Check stop signal - const hasStopSignal = flatToolsResponseData.some( - (item) => !!item.flowResponses?.find((item) => item.toolStop) - ); - // Check interactive response(Only 1 interaction is reserved) - const workflowInteractiveResponseItem = toolsRunResponse.find( - (item) => item.toolRunResponse.workflowInteractiveResponse - ); - if (hasStopSignal || workflowInteractiveResponseItem) { - // Get interactive tool data - const workflowInteractiveResponse = - workflowInteractiveResponseItem?.toolRunResponse.workflowInteractiveResponse; + // Check stop signal + const hasStopSignal = flatToolsResponseData.some( + (item) => !!item.flowResponses?.find((item) => item.toolStop) + ); + // Check interactive response(Only 1 interaction is reserved) + const workflowInteractiveResponseItem = toolsRunResponse.find( + (item) => item.toolRunResponse.workflowInteractiveResponse + ); + if (hasStopSignal || workflowInteractiveResponseItem) { + // Get interactive tool data + const workflowInteractiveResponse = + workflowInteractiveResponseItem?.toolRunResponse.workflowInteractiveResponse; - // Flashback traverses completeMessages, intercepting messages that know the first user - const firstUserIndex = completeMessages.findLastIndex((item) => item.role === 'user'); - const newMessages = completeMessages.slice(firstUserIndex + 1); + // Flashback traverses completeMessages, intercepting messages that know the first user + const firstUserIndex = completeMessages.findLastIndex((item) => item.role === 'user'); + const newMessages = completeMessages.slice(firstUserIndex + 1); - const toolWorkflowInteractiveResponse: WorkflowInteractiveResponseType | undefined = - workflowInteractiveResponse - ? { - ...workflowInteractiveResponse, - toolParams: { - entryNodeIds: workflowInteractiveResponse.entryNodeIds, - toolCallId: workflowInteractiveResponseItem?.toolMsgParams.tool_call_id, - memoryMessages: newMessages - } + const toolWorkflowInteractiveResponse: WorkflowInteractiveResponseType | undefined = + workflowInteractiveResponse + ? { + ...workflowInteractiveResponse, + toolParams: { + entryNodeIds: workflowInteractiveResponse.entryNodeIds, + toolCallId: workflowInteractiveResponseItem?.toolMsgParams.tool_call_id, + memoryMessages: newMessages } - : undefined; - - return { - dispatchFlowResponse, - toolNodeTokens, - completeMessages, - assistantResponses: toolNodeAssistants, - runTimes, - toolWorkflowInteractiveResponse - }; - } - - return runToolWithToolChoice( - { - ...props, - maxRunToolTimes: maxRunToolTimes - 1, - messages: completeMessages - }, - { - dispatchFlowResponse, - toolNodeTokens, - assistantResponses: toolNodeAssistants, - runTimes - } - ); - } else { - // No tool is invoked, indicating that the process is over - const gptAssistantResponse: ChatCompletionAssistantMessageParam = { - role: ChatCompletionRequestMessageRoleEnum.Assistant, - content: answer - }; - const completeMessages = filterMessages.concat(gptAssistantResponse); - const tokens = await countGptMessagesTokens(completeMessages, tools); - - // concat tool assistant - const toolNodeAssistant = GPTMessages2Chats([gptAssistantResponse])[0] as AIChatItemType; + } + : undefined; return { - dispatchFlowResponse: response?.dispatchFlowResponse || [], - toolNodeTokens: response ? response.toolNodeTokens + tokens : tokens, + dispatchFlowResponse, + toolNodeTokens, completeMessages, - assistantResponses: [...assistantResponses, ...toolNodeAssistant.value], - runTimes: (response?.runTimes || 0) + 1 + assistantResponses: toolNodeAssistants, + runTimes, + toolWorkflowInteractiveResponse }; } - } catch (error) { - console.log(error); - addLog.warn(`LLM response error`, { - requestBody - }); - return Promise.reject(error); + + return runToolWithToolChoice( + { + ...props, + maxRunToolTimes: maxRunToolTimes - 1, + messages: completeMessages + }, + { + dispatchFlowResponse, + toolNodeTokens, + assistantResponses: toolNodeAssistants, + runTimes + } + ); + } else { + // No tool is invoked, indicating that the process is over + const gptAssistantResponse: ChatCompletionAssistantMessageParam = { + role: ChatCompletionRequestMessageRoleEnum.Assistant, + content: answer + }; + const completeMessages = filterMessages.concat(gptAssistantResponse); + const tokens = await countGptMessagesTokens(completeMessages, tools); + + // concat tool assistant + const toolNodeAssistant = GPTMessages2Chats([gptAssistantResponse])[0] as AIChatItemType; + + return { + dispatchFlowResponse: response?.dispatchFlowResponse || [], + toolNodeTokens: response ? response.toolNodeTokens + tokens : tokens, + completeMessages, + assistantResponses: [...assistantResponses, ...toolNodeAssistant.value], + runTimes: (response?.runTimes || 0) + 1 + }; } }; @@ -656,7 +646,7 @@ async function streamResponse({ } if (!textAnswer && toolCalls.length === 0) { - return Promise.reject('LLM api response empty'); + return Promise.reject(i18nT('chat:LLM_model_response_empty')); } return { answer: textAnswer, toolCalls }; diff --git a/packages/service/core/workflow/dispatch/agent/runTool/type.d.ts b/packages/service/core/workflow/dispatch/agent/runTool/type.d.ts index 418be1aa4..ad2866c13 100644 --- a/packages/service/core/workflow/dispatch/agent/runTool/type.d.ts +++ b/packages/service/core/workflow/dispatch/agent/runTool/type.d.ts @@ -21,6 +21,7 @@ export type DispatchToolModuleProps = ModuleDispatchProps<{ [NodeInputKeyEnum.aiChatTemperature]: number; [NodeInputKeyEnum.aiChatMaxToken]: number; [NodeInputKeyEnum.aiChatVision]?: boolean; + [NodeInputKeyEnum.fileUrlList]?: string[]; }> & { messages: ChatCompletionMessageParam[]; toolNodes: ToolNodeItemType[]; diff --git a/packages/service/core/workflow/dispatch/chat/oneapi.ts b/packages/service/core/workflow/dispatch/chat/oneapi.ts index 2d28bf769..14784754d 100644 --- a/packages/service/core/workflow/dispatch/chat/oneapi.ts +++ b/packages/service/core/workflow/dispatch/chat/oneapi.ts @@ -4,12 +4,8 @@ import type { ChatItemType, UserChatItemValueItemType } from '@fastgpt/global/co import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { textAdaptGptResponse } from '@fastgpt/global/core/workflow/runtime/utils'; -import { getAIApi } from '../../../ai/config'; -import type { - ChatCompletion, - ChatCompletionMessageParam, - StreamChatType -} from '@fastgpt/global/core/ai/type.d'; +import { createChatCompletion } from '../../../ai/config'; +import type { ChatCompletion, StreamChatType } from '@fastgpt/global/core/ai/type.d'; import { formatModelChars2Points } from '../../../../support/wallet/usage/utils'; import type { LLMModelItemType } from '@fastgpt/global/core/ai/model.d'; import { postTextCensor } from '../../../../common/api/requestPlusApi'; @@ -46,6 +42,9 @@ import { WorkflowResponseType } from '../type'; import { formatTime2YMDHM } from '@fastgpt/global/common/string/time'; import { AiChatQuoteRoleType } from '@fastgpt/global/core/workflow/template/system/aiChat/type'; import { getErrText } from '@fastgpt/global/common/error/utils'; +import { getFileContentFromLinks, getHistoryFileLinks } from '../tools/readFiles'; +import { parseUrlToFileType } from '@fastgpt/global/common/file/tools'; +import { i18nT } from '../../../../../web/i18n/utils'; export type ChatProps = ModuleDispatchProps< AIChatNodeProps & { @@ -69,7 +68,9 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise { // censor model and system key if (modelConstantsData.censor && !user.openaiAccount?.key) { return postTextCensor({ text: `${systemPrompt} - ${datasetQuoteText} ${userChatInput} ` }); @@ -132,22 +145,9 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise { if (res && isStreamResponse) { // sse response @@ -195,7 +190,7 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise + if (stringQuoteText) { + return { + documentQuoteText: stringQuoteText, + userFiles: inputFiles + }; + } + + // 没有引用文件参考,但是可能用了图片识别 + if (!fileLinks) { + return { + documentQuoteText: '', + userFiles: inputFiles + }; + } + // 旧版本适配<==== + + // If fileLinks params is not empty, it means it is a new version, not get the global file. + + // Get files from histories + const filesFromHistories = getHistoryFileLinks(histories); + const urls = [...fileLinks, ...filesFromHistories]; + + if (urls.length === 0) { + return { + documentQuoteText: '', + userFiles: [] + }; + } + + const { text } = await getFileContentFromLinks({ + // Concat fileUrlList and filesFromHistories; remove not supported files + urls, + requestOrigin, + maxFiles, + teamId + }); + + return { + documentQuoteText: text, + userFiles: fileLinks.map((url) => parseUrlToFileType(url)) + }; +} + async function getChatMessages({ + model, aiChatQuoteRole, datasetQuotePrompt = '', datasetQuoteText, @@ -310,10 +372,10 @@ async function getChatMessages({ histories = [], systemPrompt, userChatInput, - inputFiles, - model, - stringQuoteText + userFiles, + documentQuoteText }: { + model: LLMModelItemType; // dataset quote aiChatQuoteRole: AiChatQuoteRoleType; // user: replace user prompt; system: replace system prompt datasetQuotePrompt?: string; @@ -323,10 +385,11 @@ async function getChatMessages({ histories: ChatItemType[]; systemPrompt: string; userChatInput: string; - inputFiles: UserChatItemValueItemType['file'][]; - model: LLMModelItemType; - stringQuoteText?: string; // file quote + + userFiles: UserChatItemValueItemType['file'][]; + documentQuoteText?: string; // document quote }) { + // Dataset prompt ====> // User role or prompt include question const quoteRole = aiChatQuoteRole === 'user' || datasetQuotePrompt.includes('{{question}}') ? 'user' : 'system'; @@ -337,6 +400,7 @@ async function getChatMessages({ ? Prompt_userQuotePromptList[0].value : Prompt_systemQuotePromptList[0].value; + // Reset user input, add dataset quote to user input const replaceInputValue = useDatasetQuote && quoteRole === 'user' ? replaceVariable(datasetQuotePromptTemplate, { @@ -344,31 +408,33 @@ async function getChatMessages({ question: userChatInput }) : userChatInput; + // Dataset prompt <==== - const replaceSystemPrompt = + // Concat system prompt + const concatenateSystemPrompt = [ + model.defaultSystemChatPrompt, + systemPrompt, useDatasetQuote && quoteRole === 'system' - ? `${systemPrompt ? systemPrompt + '\n\n------\n\n' : ''}${replaceVariable( - datasetQuotePromptTemplate, - { - quote: datasetQuoteText - } - )}` - : systemPrompt; + ? replaceVariable(datasetQuotePromptTemplate, { + quote: datasetQuoteText + }) + : '', + documentQuoteText + ? replaceVariable(Prompt_DocumentQuote, { + quote: documentQuoteText + }) + : '' + ] + .filter(Boolean) + .join('\n\n===---===---===\n\n'); const messages: ChatItemType[] = [ - ...getSystemPrompt_ChatItemType(replaceSystemPrompt), - ...(stringQuoteText // file quote - ? getSystemPrompt_ChatItemType( - replaceVariable(Prompt_DocumentQuote, { - quote: stringQuoteText - }) - ) - : []), + ...getSystemPrompt_ChatItemType(concatenateSystemPrompt), ...histories, { obj: ChatRoleEnum.Human, value: runtimePrompt2ChatsValue({ - files: inputFiles, + files: userFiles, text: replaceInputValue }) } diff --git a/packages/service/core/workflow/dispatch/dataset/concat.ts b/packages/service/core/workflow/dispatch/dataset/concat.ts index becf193b2..f11a63b42 100644 --- a/packages/service/core/workflow/dispatch/dataset/concat.ts +++ b/packages/service/core/workflow/dispatch/dataset/concat.ts @@ -1,17 +1,21 @@ import type { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type'; -import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type'; +import type { + DispatchNodeResultType, + ModuleDispatchProps +} from '@fastgpt/global/core/workflow/runtime/type'; import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { datasetSearchResultConcat } from '@fastgpt/global/core/dataset/search/utils'; import { filterSearchResultsByMaxChars } from '../../utils'; +import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; type DatasetConcatProps = ModuleDispatchProps< { [NodeInputKeyEnum.datasetMaxTokens]: number; } & { [key: string]: SearchDataResponseItemType[] } >; -type DatasetConcatResponse = { +type DatasetConcatResponse = DispatchNodeResultType<{ [NodeOutputKeyEnum.datasetQuoteQA]: SearchDataResponseItemType[]; -}; +}>; export async function dispatchDatasetConcat( props: DatasetConcatProps @@ -30,6 +34,12 @@ export async function dispatchDatasetConcat( ); return { - [NodeOutputKeyEnum.datasetQuoteQA]: await filterSearchResultsByMaxChars(rrfConcatResults, limit) + [NodeOutputKeyEnum.datasetQuoteQA]: await filterSearchResultsByMaxChars( + rrfConcatResults, + limit + ), + [DispatchNodeResponseKeyEnum.nodeResponse]: { + concatLength: rrfConcatResults.length + } }; } diff --git a/packages/service/core/workflow/dispatch/dataset/search.ts b/packages/service/core/workflow/dispatch/dataset/search.ts index c355f4c9f..dce339587 100644 --- a/packages/service/core/workflow/dispatch/dataset/search.ts +++ b/packages/service/core/workflow/dispatch/dataset/search.ts @@ -16,6 +16,7 @@ import { datasetSearchQueryExtension } from '../../../dataset/search/utils'; import { ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type'; import { checkTeamReRankPermission } from '../../../../support/permission/teamLimit'; import { MongoDataset } from '../../../dataset/schema'; +import { i18nT } from '../../../../../web/i18n/utils'; type DatasetSearchProps = ModuleDispatchProps<{ [NodeInputKeyEnum.datasetSelectList]: SelectedDatasetType; @@ -56,15 +57,15 @@ export async function dispatchDatasetSearch( } = props as DatasetSearchProps; if (!Array.isArray(datasets)) { - return Promise.reject('Quote type error'); + return Promise.reject(i18nT('chat:dataset_quote_type error')); } if (datasets.length === 0) { - return Promise.reject('core.chat.error.Select dataset empty'); + return Promise.reject(i18nT('common:core.chat.error.Select dataset empty')); } if (!userChatInput) { - return Promise.reject('core.chat.error.User input empty'); + return Promise.reject(i18nT('common:core.chat.error.User input empty')); } // query extension diff --git a/packages/service/core/workflow/dispatch/index.ts b/packages/service/core/workflow/dispatch/index.ts index 411f30e92..acbf726cc 100644 --- a/packages/service/core/workflow/dispatch/index.ts +++ b/packages/service/core/workflow/dispatch/index.ts @@ -23,7 +23,6 @@ import { } from '@fastgpt/global/core/workflow/node/constant'; import { getNanoid, replaceVariable } from '@fastgpt/global/common/string/tools'; import { getSystemTime } from '@fastgpt/global/common/time/timezone'; -import { replaceEditorVariable } from '@fastgpt/global/core/workflow/utils'; import { dispatchWorkflowStart } from './init/workflowStart'; import { dispatchChatCompletion } from './chat/oneapi'; @@ -38,11 +37,12 @@ import { dispatchQueryExtension } from './tools/queryExternsion'; import { dispatchRunPlugin } from './plugin/run'; import { dispatchPluginInput } from './plugin/runInput'; import { dispatchPluginOutput } from './plugin/runOutput'; -import { removeSystemVariable, valueTypeFormat } from './utils'; +import { formatHttpError, removeSystemVariable, valueTypeFormat } from './utils'; import { filterWorkflowEdges, checkNodeRunStatus, - textAdaptGptResponse + textAdaptGptResponse, + replaceEditorVariable } from '@fastgpt/global/core/workflow/runtime/utils'; import { ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type'; import { dispatchRunTools } from './agent/runTool/index'; @@ -72,6 +72,7 @@ import { dispatchLoopEnd } from './loop/runLoopEnd'; import { dispatchLoopStart } from './loop/runLoopStart'; import { dispatchFormInput } from './interactive/formInput'; import { dispatchToolParams } from './agent/runTool/toolParams'; +import { responseWrite } from '../../../common/response'; const callbackMap: Record = { [FlowNodeTypeEnum.workflowStart]: dispatchWorkflowStart, @@ -386,6 +387,7 @@ export async function dispatchWorkFlow(data: Props): Promise { if (status === 'run') { nodeRunBeforeHook(node); @@ -481,8 +483,16 @@ export async function dispatchWorkFlow(data: Props): Promise { + // Special input, not format if (input.key === dynamicInput?.key) return; + // Skip some special key + if (input.key === NodeInputKeyEnum.childrenNodeIdList) { + params[input.key] = input.value; + + return; + } + // replace {{xx}} variables let value = replaceVariable(input.value, variables); @@ -505,7 +515,6 @@ export async function dispatchWorkFlow(data: Props): Promise = await (async () => { if (callbackMap[node.flowNodeType]) { - return callbackMap[node.flowNodeType](dispatchData); + try { + return await callbackMap[node.flowNodeType](dispatchData); + } catch (error) { + // Get source handles of outgoing edges + const targetEdges = runtimeEdges.filter((item) => item.source === node.nodeId); + const skipHandleIds = targetEdges.map((item) => item.sourceHandle); + + // Skip all edges and return error + return { + [DispatchNodeResponseKeyEnum.nodeResponse]: { + error: formatHttpError(error) + }, + [DispatchNodeResponseKeyEnum.skipHandleId]: skipHandleIds + }; + } } return {}; })(); diff --git a/packages/service/core/workflow/dispatch/loop/runLoop.ts b/packages/service/core/workflow/dispatch/loop/runLoop.ts index 58711c834..05d3d6ee0 100644 --- a/packages/service/core/workflow/dispatch/loop/runLoop.ts +++ b/packages/service/core/workflow/dispatch/loop/runLoop.ts @@ -25,7 +25,7 @@ export const dispatchLoop = async (props: Props): Promise => { user, node: { name } } = props; - const { loopInputArray = [], childrenNodeIdList } = params; + const { loopInputArray = [], childrenNodeIdList = [] } = params; if (!Array.isArray(loopInputArray)) { return Promise.reject('Input value is not an array'); @@ -43,23 +43,24 @@ export const dispatchLoop = async (props: Props): Promise => { let totalPoints = 0; let newVariables: Record = props.variables; - for await (const item of loopInputArray) { + let index = 0; + for await (const item of loopInputArray.filter(Boolean)) { runtimeNodes.forEach((node) => { if ( childrenNodeIdList.includes(node.nodeId) && node.flowNodeType === FlowNodeTypeEnum.loopStart ) { node.isEntry = true; - node.inputs = node.inputs.map((input) => - input.key === NodeInputKeyEnum.loopStartInput - ? { - ...input, - value: item - } - : input - ); + node.inputs.forEach((input) => { + if (input.key === NodeInputKeyEnum.loopStartInput) { + input.value = item; + } else if (input.key === NodeInputKeyEnum.loopStartIndex) { + input.value = index++; + } + }); } }); + const response = await dispatchWorkFlow({ ...props, runtimeEdges: cloneDeep(runtimeEdges) @@ -69,11 +70,13 @@ export const dispatchLoop = async (props: Props): Promise => { (res) => res.moduleType === FlowNodeTypeEnum.loopEnd )?.loopOutputValue; + // Concat runtime response outputValueArr.push(loopOutputValue); loopDetail.push(...response.flowResponses); assistantResponses.push(...response.assistantResponses); + totalPoints += response.flowUsages.reduce((acc, usage) => acc + usage.totalPoints, 0); - totalPoints = response.flowUsages.reduce((acc, usage) => acc + usage.totalPoints, 0); + // Concat new variables newVariables = { ...newVariables, ...response.newVariables diff --git a/packages/service/core/workflow/dispatch/loop/runLoopStart.ts b/packages/service/core/workflow/dispatch/loop/runLoopStart.ts index c7c4cfb24..a6364a88e 100644 --- a/packages/service/core/workflow/dispatch/loop/runLoopStart.ts +++ b/packages/service/core/workflow/dispatch/loop/runLoopStart.ts @@ -7,9 +7,11 @@ import { type Props = ModuleDispatchProps<{ [NodeInputKeyEnum.loopStartInput]: any; + [NodeInputKeyEnum.loopStartIndex]: number; }>; type Response = DispatchNodeResultType<{ [NodeOutputKeyEnum.loopStartInput]: any; + [NodeOutputKeyEnum.loopStartIndex]: number; }>; export const dispatchLoopStart = async (props: Props): Promise => { @@ -18,6 +20,7 @@ export const dispatchLoopStart = async (props: Props): Promise => { [DispatchNodeResponseKeyEnum.nodeResponse]: { loopInputValue: params.loopStartInput }, - [NodeOutputKeyEnum.loopStartInput]: params.loopStartInput + [NodeOutputKeyEnum.loopStartInput]: params.loopStartInput, + [NodeOutputKeyEnum.loopStartIndex]: params.loopStartIndex }; }; diff --git a/packages/service/core/workflow/dispatch/plugin/run.ts b/packages/service/core/workflow/dispatch/plugin/run.ts index 32dad35ae..6d5fe3164 100644 --- a/packages/service/core/workflow/dispatch/plugin/run.ts +++ b/packages/service/core/workflow/dispatch/plugin/run.ts @@ -112,7 +112,11 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise; type Response = DispatchNodeResultType<{ [NodeOutputKeyEnum.answerText]: string; @@ -40,8 +42,24 @@ export const dispatchRunAppNode = async (props: Props): Promise => { variables } = props; - const { system_forbid_stream = false, userChatInput, history, ...childrenAppVariables } = params; - if (!userChatInput) { + const { + system_forbid_stream = false, + userChatInput, + history, + fileUrlList, + ...childrenAppVariables + } = params; + const { files } = chatValue2RuntimePrompt(query); + + const userInputFiles = (() => { + if (fileUrlList) { + return fileUrlList.map((url) => parseUrlToFileType(url)); + } + // Adapt version 4.8.13 upgrade + return files; + })(); + + if (!userChatInput && !userInputFiles) { return Promise.reject('Input is empty'); } if (!appId) { @@ -72,7 +90,6 @@ export const dispatchRunAppNode = async (props: Props): Promise => { } const chatHistories = getHistories(history, histories); - const { files } = chatValue2RuntimePrompt(query); // Rewrite children app variables const systemVariables = filterSystemVariables(variables); @@ -102,7 +119,7 @@ export const dispatchRunAppNode = async (props: Props): Promise => { histories: chatHistories, variables: childrenRunVariables, query: runtimePrompt2ChatsValue({ - files, + files: userInputFiles, text: userChatInput }), chatConfig diff --git a/packages/service/core/workflow/dispatch/plugin/runInput.ts b/packages/service/core/workflow/dispatch/plugin/runInput.ts index 6760d8fc3..dde187eba 100644 --- a/packages/service/core/workflow/dispatch/plugin/runInput.ts +++ b/packages/service/core/workflow/dispatch/plugin/runInput.ts @@ -1,4 +1,5 @@ import { chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt'; +import { ChatFileTypeEnum } from '@fastgpt/global/core/chat/constants'; import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type'; @@ -11,6 +12,26 @@ export const dispatchPluginInput = (props: PluginInputProps) => { const { params, query } = props; const { files } = chatValue2RuntimePrompt(query); + /* + 对 params 中文件类型数据进行处理 + * 插件单独运行时,这里会是一个特殊的数组 + * 插件调用的话,这个参数是一个 string[] 不会进行处理 + * 硬性要求:API 单独调用插件时,要避免这种特殊类型冲突 + + TODO: 需要 filter max files + */ + for (const key in params) { + const val = params[key]; + if ( + Array.isArray(val) && + val.every( + (item) => item.type === ChatFileTypeEnum.file || item.type === ChatFileTypeEnum.image + ) + ) { + params[key] = val.map((item) => item.url); + } + } + return { ...params, [DispatchNodeResponseKeyEnum.nodeResponse]: {}, diff --git a/packages/service/core/workflow/dispatch/tools/http468.ts b/packages/service/core/workflow/dispatch/tools/http468.ts index 145ded177..6f5a683f0 100644 --- a/packages/service/core/workflow/dispatch/tools/http468.ts +++ b/packages/service/core/workflow/dispatch/tools/http468.ts @@ -14,10 +14,12 @@ import { SERVICE_LOCAL_HOST } from '../../../../common/system/tools'; import { addLog } from '../../../../common/system/log'; import { DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/type'; import { getErrText } from '@fastgpt/global/common/error/utils'; -import { textAdaptGptResponse } from '@fastgpt/global/core/workflow/runtime/utils'; +import { + textAdaptGptResponse, + replaceEditorVariable +} from '@fastgpt/global/core/workflow/runtime/utils'; import { getSystemPluginCb } from '../../../../../plugins/register'; import { ContentTypes } from '@fastgpt/global/core/workflow/constants'; -import { replaceEditorVariable } from '@fastgpt/global/core/workflow/utils'; import { uploadFileFromBase64Img } from '../../../../common/file/gridfs/controller'; import { ReadFileBaseUrl } from '@fastgpt/global/common/file/constants'; import { createFileToken } from '../../../../support/permission/controller'; @@ -235,7 +237,9 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise - item.key !== NodeOutputKeyEnum.error && item.key !== NodeOutputKeyEnum.httpRawResponse + item.id !== NodeOutputKeyEnum.error && + item.id !== NodeOutputKeyEnum.httpRawResponse && + item.id !== NodeOutputKeyEnum.addOutputParam ) .forEach((item) => { const key = item.key.startsWith('$') ? item.key : `$.${item.key}`; diff --git a/packages/service/core/workflow/dispatch/tools/readFiles.ts b/packages/service/core/workflow/dispatch/tools/readFiles.ts index 31f8db7e9..702dfda93 100644 --- a/packages/service/core/workflow/dispatch/tools/readFiles.ts +++ b/packages/service/core/workflow/dispatch/tools/readFiles.ts @@ -2,16 +2,15 @@ import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runti import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type'; import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/type'; -import { documentFileType } from '@fastgpt/global/common/file/constants'; import axios from 'axios'; import { serverRequestBaseUrl } from '../../../../common/api/serverRequest'; import { MongoRawTextBuffer } from '../../../../common/buffer/rawText/schema'; import { readFromSecondary } from '../../../../common/mongo/utils'; import { getErrText } from '@fastgpt/global/common/error/utils'; -import { detectFileEncoding } from '@fastgpt/global/common/file/tools'; +import { detectFileEncoding, parseUrlToFileType } from '@fastgpt/global/common/file/tools'; import { readRawContentByFileBuffer } from '../../../../common/file/read/utils'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; -import { UserChatItemValueItemType } from '@fastgpt/global/core/chat/type'; +import { ChatItemType, UserChatItemValueItemType } from '@fastgpt/global/core/chat/type'; import { parseFileExtensionFromUrl } from '@fastgpt/global/common/string/tools'; type Props = ModuleDispatchProps<{ @@ -48,12 +47,41 @@ export const dispatchReadFiles = async (props: Props): Promise => { runningAppInfo: { teamId }, histories, chatConfig, + node: { version }, params: { fileUrlList = [] } } = props; const maxFiles = chatConfig?.fileSelectConfig?.maxFiles || 20; // Get files from histories - const filesFromHistories = histories + const filesFromHistories = version !== '489' ? [] : getHistoryFileLinks(histories); + + const { text, readFilesResult } = await getFileContentFromLinks({ + // Concat fileUrlList and filesFromHistories; remove not supported files + urls: [...fileUrlList, ...filesFromHistories], + requestOrigin, + maxFiles, + teamId + }); + + return { + [NodeOutputKeyEnum.text]: text, + [DispatchNodeResponseKeyEnum.nodeResponse]: { + readFiles: readFilesResult.map((item) => ({ + name: item?.filename || '', + url: item?.url || '' + })), + readFilesResult: readFilesResult + .map((item) => item?.nodeResponsePreviewText ?? '') + .join('\n******\n') + }, + [DispatchNodeResponseKeyEnum.toolResponses]: { + fileContent: text + } + }; +}; + +export const getHistoryFileLinks = (histories: ChatItemType[]) => { + return histories .filter((item) => { if (item.obj === ChatRoleEnum.Human) { return item.value.filter((value) => value.type === 'file'); @@ -70,28 +98,38 @@ export const dispatchReadFiles = async (props: Props): Promise => { return files; }) .flat(); +}; - // Concat fileUrlList and filesFromHistories; remove not supported files - const parseUrlList = [...fileUrlList, ...filesFromHistories] +export const getFileContentFromLinks = async ({ + urls, + requestOrigin, + maxFiles, + teamId +}: { + urls: string[]; + requestOrigin?: string; + maxFiles: number; + teamId: string; +}) => { + const parseUrlList = urls + // Remove invalid urls + .filter((url) => { + if (typeof url !== 'string') return false; + + // 检查相对路径 + const validPrefixList = ['/', 'http', 'ws']; + if (validPrefixList.some((prefix) => url.startsWith(prefix))) { + return true; + } + + return false; + }) + // Just get the document type file + .filter((url) => parseUrlToFileType(url)?.type === 'file') .map((url) => { try { - // Avoid "/api/xxx" file error. - const origin = requestOrigin ?? 'http://localhost:3000'; - // Check is system upload file if (url.startsWith('/') || (requestOrigin && url.startsWith(requestOrigin))) { - // Parse url, get filename query. Keep only documents that can be parsed - const parseUrl = new URL(url, origin); - const filenameQuery = parseUrl.searchParams.get('filename'); - - // Not document - if (filenameQuery) { - const extensionQuery = filenameQuery.split('.').pop()?.toLowerCase() || ''; - if (!documentFileType.includes(extensionQuery)) { - return ''; - } - } - // Remove the origin(Make intranet requests directly) if (requestOrigin && url.startsWith(requestOrigin)) { url = url.replace(requestOrigin, ''); @@ -123,7 +161,7 @@ export const dispatchReadFiles = async (props: Props): Promise => { } try { - // Get file buffer + // Get file buffer data const response = await axios.get(url, { baseURL: serverRequestBaseUrl, responseType: 'arraybuffer' @@ -197,18 +235,7 @@ export const dispatchReadFiles = async (props: Props): Promise => { const text = readFilesResult.map((item) => item?.text ?? '').join('\n******\n'); return { - [NodeOutputKeyEnum.text]: text, - [DispatchNodeResponseKeyEnum.nodeResponse]: { - readFiles: readFilesResult.map((item) => ({ - name: item?.filename || '', - url: item?.url || '' - })), - readFilesResult: readFilesResult - .map((item) => item?.nodeResponsePreviewText ?? '') - .join('\n******\n') - }, - [DispatchNodeResponseKeyEnum.toolResponses]: { - fileContent: text - } + text, + readFilesResult }; }; diff --git a/packages/service/core/workflow/dispatch/tools/runUpdateVar.ts b/packages/service/core/workflow/dispatch/tools/runUpdateVar.ts index bcb028a33..a1f6af8b0 100644 --- a/packages/service/core/workflow/dispatch/tools/runUpdateVar.ts +++ b/packages/service/core/workflow/dispatch/tools/runUpdateVar.ts @@ -4,11 +4,14 @@ import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/type'; -import { getReferenceVariableValue } from '@fastgpt/global/core/workflow/runtime/utils'; +import { + getReferenceVariableValue, + replaceEditorVariable +} from '@fastgpt/global/core/workflow/runtime/utils'; import { TUpdateListItem } from '@fastgpt/global/core/workflow/template/system/variableUpdate/type'; import { ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type'; import { removeSystemVariable, valueTypeFormat } from '../utils'; -import { replaceEditorVariable } from '@fastgpt/global/core/workflow/utils'; +import { isValidReferenceValue } from '@fastgpt/global/core/workflow/utils'; type Props = ModuleDispatchProps<{ [NodeInputKeyEnum.updateList]: TUpdateListItem[]; @@ -19,15 +22,24 @@ export const dispatchUpdateVariable = async (props: Props): Promise => const { params, variables, runtimeNodes, workflowStreamResponse, node } = props; const { updateList } = params; - const result = updateList.map((item) => { - const varNodeId = item.variable?.[0]; - const varKey = item.variable?.[1]; + const nodeIds = runtimeNodes.map((node) => node.nodeId); - if (!varNodeId || !varKey) { + const result = updateList.map((item) => { + const variable = item.variable; + + if (!isValidReferenceValue(variable, nodeIds)) { + return null; + } + + const varNodeId = variable[0]; + const varKey = variable[1]; + + if (!varKey) { return null; } const value = (() => { + // If first item is empty, it means it is a input value if (!item.value?.[0]) { const formatValue = valueTypeFormat(item.value?.[1], item.valueType); @@ -48,6 +60,7 @@ export const dispatchUpdateVariable = async (props: Props): Promise => } })(); + // Update node output // Global variable if (varNodeId === VARIABLE_NODE_ID) { variables[varKey] = value; @@ -72,6 +85,7 @@ export const dispatchUpdateVariable = async (props: Props): Promise => }); return { + [DispatchNodeResponseKeyEnum.newVariables]: variables, [DispatchNodeResponseKeyEnum.nodeResponse]: { updateVarResult: result } diff --git a/packages/service/core/workflow/dispatch/utils.ts b/packages/service/core/workflow/dispatch/utils.ts index 01ef6878e..3ed92a6e9 100644 --- a/packages/service/core/workflow/dispatch/utils.ts +++ b/packages/service/core/workflow/dispatch/utils.ts @@ -18,12 +18,14 @@ export const getWorkflowResponseWrite = ({ res, detail, streamResponse, - id = getNanoid(24) + id = getNanoid(24), + showNodeStatus = true }: { res?: NextApiResponse; detail: boolean; streamResponse: boolean; id?: string; + showNodeStatus?: boolean; }) => { return ({ write, @@ -40,17 +42,27 @@ export const getWorkflowResponseWrite = ({ if (!res || res.closed || !useStreamResponse) return; - const detailEvent = [ - SseResponseEventEnum.error, - SseResponseEventEnum.flowNodeStatus, - SseResponseEventEnum.flowResponses, - SseResponseEventEnum.interactive, - SseResponseEventEnum.toolCall, - SseResponseEventEnum.toolParams, - SseResponseEventEnum.toolResponse, - SseResponseEventEnum.updateVariables - ]; - if (!detail && detailEvent.includes(event)) return; + // Forbid show detail + const detailEvent: Record = { + [SseResponseEventEnum.error]: 1, + [SseResponseEventEnum.flowNodeStatus]: 1, + [SseResponseEventEnum.flowResponses]: 1, + [SseResponseEventEnum.interactive]: 1, + [SseResponseEventEnum.toolCall]: 1, + [SseResponseEventEnum.toolParams]: 1, + [SseResponseEventEnum.toolResponse]: 1, + [SseResponseEventEnum.updateVariables]: 1 + }; + if (!detail && detailEvent[event]) return; + + // Forbid show running status + const statusEvent: Record = { + [SseResponseEventEnum.flowNodeStatus]: 1, + [SseResponseEventEnum.toolCall]: 1, + [SseResponseEventEnum.toolParams]: 1, + [SseResponseEventEnum.toolResponse]: 1 + }; + if (!showNodeStatus && statusEvent[event]) return; responseWrite({ res, diff --git a/packages/service/core/workflow/utils.ts b/packages/service/core/workflow/utils.ts index 327cae656..6e5ec8cfb 100644 --- a/packages/service/core/workflow/utils.ts +++ b/packages/service/core/workflow/utils.ts @@ -1,14 +1,5 @@ import { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type'; import { countPromptTokens } from '../../common/string/tiktoken/index'; -import { getNanoid } from '@fastgpt/global/common/string/tools'; -import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; -import { - getPluginInputsFromStoreNodes, - getPluginRunContent -} from '@fastgpt/global/core/app/plugin/utils'; -import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node'; -import { RuntimeUserPromptType, UserChatItemType } from '@fastgpt/global/core/chat/type'; -import { runtimePrompt2ChatsValue } from '@fastgpt/global/core/chat/adapt'; /* filter search result */ export const filterSearchResultsByMaxChars = async ( diff --git a/packages/service/support/openapi/auth.ts b/packages/service/support/openapi/auth.ts index a66c684b7..88f90f591 100644 --- a/packages/service/support/openapi/auth.ts +++ b/packages/service/support/openapi/auth.ts @@ -11,7 +11,7 @@ export async function authOpenApiKey({ apikey }: { apikey: string }) { return Promise.reject(ERROR_ENUM.unAuthApiKey); } try { - const openApi = await MongoOpenApi.findOne({ apiKey: apikey.trim() }); + const openApi = await MongoOpenApi.findOne({ apiKey: apikey.trim() }).lean(); if (!openApi) { return Promise.reject(ERROR_ENUM.unAuthApiKey); } @@ -20,7 +20,7 @@ export async function authOpenApiKey({ apikey }: { apikey: string }) { // @ts-ignore if (global.feConfigs?.isPlus) { await POST('/support/openapi/authLimit', { - openApi: openApi.toObject() + openApi } as AuthOpenApiLimitProps); } @@ -30,7 +30,8 @@ export async function authOpenApiKey({ apikey }: { apikey: string }) { apikey, teamId: String(openApi.teamId), tmbId: String(openApi.tmbId), - appId: openApi.appId || '' + appId: openApi.appId || '', + sourceName: openApi.name }; } catch (error) { return Promise.reject(error); diff --git a/packages/service/support/outLink/schema.ts b/packages/service/support/outLink/schema.ts index d5e6dc000..51e470a94 100644 --- a/packages/service/support/outLink/schema.ts +++ b/packages/service/support/outLink/schema.ts @@ -42,10 +42,17 @@ const OutLinkSchema = new Schema({ lastTime: { type: Date }, + responseDetail: { type: Boolean, default: false }, + showNodeStatus: { + type: Boolean + }, + showRawSource: { + type: Boolean + }, limit: { maxUsagePoints: { type: Number, @@ -62,6 +69,8 @@ const OutLinkSchema = new Schema({ type: String } }, + + // Third part app config app: { type: Object // could be FeishuAppType | WecomAppType | ... }, diff --git a/packages/service/support/permission/controller.ts b/packages/service/support/permission/controller.ts index 59deba454..743759a5b 100644 --- a/packages/service/support/permission/controller.ts +++ b/packages/service/support/permission/controller.ts @@ -313,14 +313,15 @@ export async function parseHeaderCert({ })(); // auth apikey - const { teamId, tmbId, appId: apiKeyAppId = '' } = await authOpenApiKey({ apikey }); + const { teamId, tmbId, appId: apiKeyAppId = '', sourceName } = await authOpenApiKey({ apikey }); return { uid: '', teamId, tmbId, apikey, - appId: apiKeyAppId || authorizationAppid + appId: apiKeyAppId || authorizationAppid, + sourceName }; } // root user @@ -332,48 +333,50 @@ export async function parseHeaderCert({ const { cookie, token, rootkey, authorization } = (req.headers || {}) as ReqHeaderAuthType; - const { uid, teamId, tmbId, appId, openApiKey, authType, isRoot } = await (async () => { - if (authApiKey && authorization) { - // apikey from authorization - const authResponse = await parseAuthorization(authorization); - return { - uid: authResponse.uid, - teamId: authResponse.teamId, - tmbId: authResponse.tmbId, - appId: authResponse.appId, - openApiKey: authResponse.apikey, - authType: AuthUserTypeEnum.apikey - }; - } - if (authToken && (token || cookie)) { - // user token(from fastgpt web) - const res = await authCookieToken(cookie, token); - return { - uid: res.userId, - teamId: res.teamId, - tmbId: res.tmbId, - appId: '', - openApiKey: '', - authType: AuthUserTypeEnum.token, - isRoot: res.isRoot - }; - } - if (authRoot && rootkey) { - await parseRootKey(rootkey); - // root user - return { - uid: '', - teamId: '', - tmbId: '', - appId: '', - openApiKey: '', - authType: AuthUserTypeEnum.root, - isRoot: true - }; - } + const { uid, teamId, tmbId, appId, openApiKey, authType, isRoot, sourceName } = + await (async () => { + if (authApiKey && authorization) { + // apikey from authorization + const authResponse = await parseAuthorization(authorization); + return { + uid: authResponse.uid, + teamId: authResponse.teamId, + tmbId: authResponse.tmbId, + appId: authResponse.appId, + openApiKey: authResponse.apikey, + authType: AuthUserTypeEnum.apikey, + sourceName: authResponse.sourceName + }; + } + if (authToken && (token || cookie)) { + // user token(from fastgpt web) + const res = await authCookieToken(cookie, token); + return { + uid: res.userId, + teamId: res.teamId, + tmbId: res.tmbId, + appId: '', + openApiKey: '', + authType: AuthUserTypeEnum.token, + isRoot: res.isRoot + }; + } + if (authRoot && rootkey) { + await parseRootKey(rootkey); + // root user + return { + uid: '', + teamId: '', + tmbId: '', + appId: '', + openApiKey: '', + authType: AuthUserTypeEnum.root, + isRoot: true + }; + } - return Promise.reject(ERROR_ENUM.unAuthorization); - })(); + return Promise.reject(ERROR_ENUM.unAuthorization); + })(); if (!authRoot && (!teamId || !tmbId)) { return Promise.reject(ERROR_ENUM.unAuthorization); @@ -385,6 +388,7 @@ export async function parseHeaderCert({ tmbId: String(tmbId), appId, authType, + sourceName, apikey: openApiKey, isRoot: !!isRoot }; diff --git a/packages/web/common/system/utils.ts b/packages/web/common/system/utils.ts index be42ee812..1015f699f 100644 --- a/packages/web/common/system/utils.ts +++ b/packages/web/common/system/utils.ts @@ -9,3 +9,12 @@ export const getUserFingerprint = async () => { export const hasHttps = () => { return window.location.protocol === 'https:'; }; + +export const getWebReqUrl = (url: string = '') => { + if (!url) return '/'; + const baseUrl = process.env.NEXT_PUBLIC_BASE_URL; + if (!baseUrl) return url; + + if (!url.startsWith('/') || url.startsWith(baseUrl)) return url; + return `${baseUrl}${url}`; +}; diff --git a/packages/web/components/common/Avatar/index.tsx b/packages/web/components/common/Avatar/index.tsx index 141f20e47..9f398ac22 100644 --- a/packages/web/components/common/Avatar/index.tsx +++ b/packages/web/components/common/Avatar/index.tsx @@ -1,9 +1,10 @@ import React from 'react'; -import { Box, Flex, Image } from '@chakra-ui/react'; +import { Box } from '@chakra-ui/react'; import type { ImageProps } from '@chakra-ui/react'; import { LOGO_ICON } from '@fastgpt/global/common/system/constants'; import MyIcon from '../Icon'; import { iconPaths } from '../Icon/constants'; +import MyImage from '../Image/MyImage'; const Avatar = ({ w = '30px', src, ...props }: ImageProps) => { // @ts-ignore @@ -14,7 +15,7 @@ const Avatar = ({ w = '30px', src, ...props }: ImageProps) => { ) : ( - import('./icons/book.svg'), change: () => import('./icons/change.svg'), chatSend: () => import('./icons/chatSend.svg'), + check: () => import('./icons/check.svg'), closeSolid: () => import('./icons/closeSolid.svg'), collectionLight: () => import('./icons/collectionLight.svg'), collectionSolid: () => import('./icons/collectionSolid.svg'), @@ -59,7 +60,6 @@ export const iconPaths = { 'common/playFill': () => import('./icons/common/playFill.svg'), 'common/playLight': () => import('./icons/common/playLight.svg'), 'common/publishFill': () => import('./icons/common/publishFill.svg'), - 'common/questionLight': () => import('./icons/common/questionLight.svg'), 'common/refreshLight': () => import('./icons/common/refreshLight.svg'), 'common/resultLight': () => import('./icons/common/resultLight.svg'), 'common/retryLight': () => import('./icons/common/retryLight.svg'), @@ -217,6 +217,7 @@ export const iconPaths = { 'core/workflow/template/FileRead': () => import('./icons/core/workflow/template/FileRead.svg'), 'core/workflow/template/aiChat': () => import('./icons/core/workflow/template/aiChat.svg'), 'core/workflow/template/baseChart': () => import('./icons/core/workflow/template/baseChart.svg'), + 'core/workflow/template/bing': () => import('./icons/core/workflow/template/bing.svg'), 'core/workflow/template/codeRun': () => import('./icons/core/workflow/template/codeRun.svg'), 'core/workflow/template/customFeedback': () => import('./icons/core/workflow/template/customFeedback.svg'), @@ -224,18 +225,16 @@ export const iconPaths = { import('./icons/core/workflow/template/datasetConcat.svg'), 'core/workflow/template/datasetSearch': () => import('./icons/core/workflow/template/datasetSearch.svg'), + 'core/workflow/template/datasource': () => + import('./icons/core/workflow/template/datasource.svg'), 'core/workflow/template/duckduckgo': () => import('./icons/core/workflow/template/duckduckgo.svg'), 'core/workflow/template/extractJson': () => import('./icons/core/workflow/template/extractJson.svg'), - 'core/workflow/template/wiki': () => import('./icons/core/workflow/template/wiki.svg'), - 'core/workflow/template/datasource': () => - import('./icons/core/workflow/template/datasource.svg'), - 'core/workflow/template/google': () => import('./icons/core/workflow/template/google.svg'), - 'core/workflow/template/bing': () => import('./icons/core/workflow/template/bing.svg'), 'core/workflow/template/fetchUrl': () => import('./icons/core/workflow/template/fetchUrl.svg'), 'core/workflow/template/formInput': () => import('./icons/core/workflow/template/formInput.svg'), 'core/workflow/template/getTime': () => import('./icons/core/workflow/template/getTime.svg'), + 'core/workflow/template/google': () => import('./icons/core/workflow/template/google.svg'), 'core/workflow/template/httpRequest': () => import('./icons/core/workflow/template/httpRequest.svg'), 'core/workflow/template/ifelse': () => import('./icons/core/workflow/template/ifelse.svg'), @@ -271,6 +270,7 @@ export const iconPaths = { 'core/workflow/template/variable': () => import('./icons/core/workflow/template/variable.svg'), 'core/workflow/template/variableUpdate': () => import('./icons/core/workflow/template/variableUpdate.svg'), + 'core/workflow/template/wiki': () => import('./icons/core/workflow/template/wiki.svg'), 'core/workflow/template/workflowStart': () => import('./icons/core/workflow/template/workflowStart.svg'), 'core/workflow/touchTable': () => import('./icons/core/workflow/touchTable.svg'), @@ -280,6 +280,7 @@ export const iconPaths = { date: () => import('./icons/date.svg'), delete: () => import('./icons/delete.svg'), drag: () => import('./icons/drag.svg'), + edgeAdd: () => import('./icons/edgeAdd.svg'), edit: () => import('./icons/edit.svg'), empty: () => import('./icons/empty.svg'), export: () => import('./icons/export.svg'), @@ -302,6 +303,7 @@ export const iconPaths = { 'file/pdf': () => import('./icons/file/pdf.svg'), 'file/qaImport': () => import('./icons/file/qaImport.svg'), 'file/uploadFile': () => import('./icons/file/uploadFile.svg'), + help: () => import('./icons/help.svg'), history: () => import('./icons/history.svg'), infoRounded: () => import('./icons/infoRounded.svg'), kbTest: () => import('./icons/kbTest.svg'), @@ -331,7 +333,6 @@ export const iconPaths = { save: () => import('./icons/save.svg'), stop: () => import('./icons/stop.svg'), 'support/account/loginoutLight': () => import('./icons/support/account/loginoutLight.svg'), - 'support/account/passwordLogin': () => import('./icons/support/account/passwordLogin.svg'), 'support/account/plans': () => import('./icons/support/account/plans.svg'), 'support/account/promotionLight': () => import('./icons/support/account/promotionLight.svg'), 'support/bill/extraDatasetsize': () => import('./icons/support/bill/extraDatasetsize.svg'), diff --git a/packages/web/components/common/Icon/icons/check.svg b/packages/web/components/common/Icon/icons/check.svg new file mode 100644 index 000000000..5332e6086 --- /dev/null +++ b/packages/web/components/common/Icon/icons/check.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/common/editor/resizer.svg b/packages/web/components/common/Icon/icons/common/editor/resizer.svg index b7e276b17..53bdea172 100644 --- a/packages/web/components/common/Icon/icons/common/editor/resizer.svg +++ b/packages/web/components/common/Icon/icons/common/editor/resizer.svg @@ -1 +1,4 @@ - \ No newline at end of file + + + + \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/common/importLight.svg b/packages/web/components/common/Icon/icons/common/importLight.svg index b83f8efdd..4eadad39b 100644 --- a/packages/web/components/common/Icon/icons/common/importLight.svg +++ b/packages/web/components/common/Icon/icons/common/importLight.svg @@ -1,6 +1,4 @@ - - - + + + \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/common/questionLight.svg b/packages/web/components/common/Icon/icons/common/questionLight.svg deleted file mode 100644 index f09e09270..000000000 --- a/packages/web/components/common/Icon/icons/common/questionLight.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/common/select.svg b/packages/web/components/common/Icon/icons/common/select.svg index 8081fb521..22e274fb0 100644 --- a/packages/web/components/common/Icon/icons/common/select.svg +++ b/packages/web/components/common/Icon/icons/common/select.svg @@ -1,4 +1,3 @@ - - + + \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/core/workflow/template/loopEnd.svg b/packages/web/components/common/Icon/icons/core/workflow/template/loopEnd.svg index ca8fc3c91..1d56992b1 100644 --- a/packages/web/components/common/Icon/icons/core/workflow/template/loopEnd.svg +++ b/packages/web/components/common/Icon/icons/core/workflow/template/loopEnd.svg @@ -1,11 +1,10 @@ - - - + + - - - + + + diff --git a/packages/web/components/common/Icon/icons/core/workflow/template/loopStart.svg b/packages/web/components/common/Icon/icons/core/workflow/template/loopStart.svg index 546a3ecd5..c71bdc9a7 100644 --- a/packages/web/components/common/Icon/icons/core/workflow/template/loopStart.svg +++ b/packages/web/components/common/Icon/icons/core/workflow/template/loopStart.svg @@ -1,11 +1,10 @@ - - - - + + + - - - + + + diff --git a/packages/web/components/common/Icon/icons/edgeAdd.svg b/packages/web/components/common/Icon/icons/edgeAdd.svg new file mode 100644 index 000000000..2bab3a38f --- /dev/null +++ b/packages/web/components/common/Icon/icons/edgeAdd.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/web/components/common/Icon/icons/help.svg b/packages/web/components/common/Icon/icons/help.svg new file mode 100644 index 000000000..9da1bbd44 --- /dev/null +++ b/packages/web/components/common/Icon/icons/help.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/support/account/passwordLogin.svg b/packages/web/components/common/Icon/icons/support/account/passwordLogin.svg deleted file mode 100644 index 492c5c8b1..000000000 --- a/packages/web/components/common/Icon/icons/support/account/passwordLogin.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - \ No newline at end of file diff --git a/packages/web/components/common/Image/MyImage.tsx b/packages/web/components/common/Image/MyImage.tsx new file mode 100644 index 000000000..b9f7a6a0e --- /dev/null +++ b/packages/web/components/common/Image/MyImage.tsx @@ -0,0 +1,7 @@ +import React from 'react'; +import { Image, ImageProps } from '@chakra-ui/react'; +import { getWebReqUrl } from '../../../common/system/utils'; +const MyImage = (props: ImageProps) => { + return ; +}; +export default React.memo(MyImage); diff --git a/packages/web/components/common/Image/PhotoView.tsx b/packages/web/components/common/Image/PhotoView.tsx index 5d2199aa4..33cbdd615 100644 --- a/packages/web/components/common/Image/PhotoView.tsx +++ b/packages/web/components/common/Image/PhotoView.tsx @@ -1,9 +1,10 @@ import React from 'react'; import { PhotoProvider, PhotoView } from 'react-photo-view'; import 'react-photo-view/dist/react-photo-view.css'; -import { Box, Image, ImageProps } from '@chakra-ui/react'; +import { ImageProps } from '@chakra-ui/react'; import { useSystem } from '../../../hooks/useSystem'; import Loading from '../MyLoading'; +import MyImage from './MyImage'; const MyPhotoView = ({ ...props }: ImageProps) => { const { isPc } = useSystem(); @@ -15,7 +16,7 @@ const MyPhotoView = ({ ...props }: ImageProps) => { loadingElement={} > - + ); diff --git a/packages/web/components/common/Input/NumberInput/index.tsx b/packages/web/components/common/Input/NumberInput/index.tsx index 8d8587240..58b788ebf 100644 --- a/packages/web/components/common/Input/NumberInput/index.tsx +++ b/packages/web/components/common/Input/NumberInput/index.tsx @@ -7,28 +7,50 @@ import { NumberInputProps } from '@chakra-ui/react'; import React from 'react'; +import MyIcon from '../../Icon'; +import { UseFormRegister } from 'react-hook-form'; type Props = Omit & { - onChange: (e?: number) => any; + onChange?: (e?: number) => any; placeholder?: string; + register?: UseFormRegister; + name?: string; + bg?: string; }; const MyNumberInput = (props: Props) => { + const { register, name, onChange, placeholder, bg, ...restProps } = props; + return ( { + if (!onChange) return; if (isNaN(Number(e))) { - props?.onChange(); + onChange(); } else { - props?.onChange(Number(e)); + onChange(Number(e)); } }} > - + - - + + + + + + ); diff --git a/packages/web/components/common/MyBox/FormLabel.tsx b/packages/web/components/common/MyBox/FormLabel.tsx index 60dba93ff..52a121f0e 100644 --- a/packages/web/components/common/MyBox/FormLabel.tsx +++ b/packages/web/components/common/MyBox/FormLabel.tsx @@ -10,7 +10,14 @@ const FormLabel = ({ children: React.ReactNode; }) => { return ( - + {required && ( * diff --git a/packages/web/components/common/MyDrawer/CustomRightDrawer.tsx b/packages/web/components/common/MyDrawer/CustomRightDrawer.tsx index 345673d5c..8d590a251 100644 --- a/packages/web/components/common/MyDrawer/CustomRightDrawer.tsx +++ b/packages/web/components/common/MyDrawer/CustomRightDrawer.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import MyIcon from '../Icon'; -import { Flex, Image, Box, CloseButton, FlexProps } from '@chakra-ui/react'; +import { Flex, Box, CloseButton, FlexProps } from '@chakra-ui/react'; import { useLoading } from '../../../hooks/useLoading'; +import Avatar from '../Avatar'; type Props = FlexProps & { onClose: () => void; @@ -50,15 +50,7 @@ const CustomRightDrawer = ({ py={'10px'} px={5} > - {iconSrc && ( - <> - {iconSrc.startsWith('/') ? ( - - ) : ( - - )} - - )} + {iconSrc && } {title} diff --git a/packages/web/components/common/MyDrawer/MyRightDrawer.tsx b/packages/web/components/common/MyDrawer/MyRightDrawer.tsx index 94bb01232..7dc3c428b 100644 --- a/packages/web/components/common/MyDrawer/MyRightDrawer.tsx +++ b/packages/web/components/common/MyDrawer/MyRightDrawer.tsx @@ -13,6 +13,7 @@ import { Box } from '@chakra-ui/react'; import { useLoading } from '../../../hooks/useLoading'; +import Avatar from '../Avatar'; type Props = DrawerContentProps & { onClose: () => void; @@ -52,15 +53,7 @@ const MyRightDrawer = ({ py={'10px'} px={5} > - {iconSrc && ( - <> - {iconSrc.startsWith('/') ? ( - - ) : ( - - )} - - )} + {iconSrc && } {title} diff --git a/packages/web/components/common/MyMenu/index.tsx b/packages/web/components/common/MyMenu/index.tsx index 98d1442ce..c05128356 100644 --- a/packages/web/components/common/MyMenu/index.tsx +++ b/packages/web/components/common/MyMenu/index.tsx @@ -147,8 +147,6 @@ const MyMenu = ({ position={'relative'} color={isOpen ? 'primary.600' : ''} w="fit-content" - p="1" - bgColor={isOpen ? 'myGray.50' : ''} h="fit-content" borderRadius="sm" > diff --git a/packages/web/components/common/MySelect/MultipleRowSelect.tsx b/packages/web/components/common/MySelect/MultipleRowSelect.tsx index 73f630bf0..6ab271058 100644 --- a/packages/web/components/common/MySelect/MultipleRowSelect.tsx +++ b/packages/web/components/common/MySelect/MultipleRowSelect.tsx @@ -1,11 +1,11 @@ -import React, { useRef, useCallback, useState } from 'react'; -import { Button, useDisclosure, Box, Flex, useOutsideClick } from '@chakra-ui/react'; -import { ChevronDownIcon } from '@chakra-ui/icons'; -import { MultipleSelectProps } from './type'; +import React, { useRef, useCallback, useState, useMemo } from 'react'; +import { Button, useDisclosure, Box, Flex, useOutsideClick, Checkbox } from '@chakra-ui/react'; +import { ListItemType, MultipleArraySelectProps, MultipleSelectProps } from './type'; import EmptyTip from '../EmptyTip'; import { useTranslation } from 'next-i18next'; +import MyIcon from '../../common/Icon'; -const MultipleRowSelect = ({ +export const MultipleRowSelect = ({ placeholder, label, value = [], @@ -106,17 +106,25 @@ const MultipleRowSelect = ({ } - variant={'whiteBase'} + variant={'whitePrimaryOutline'} + size={'lg'} + fontSize={'sm'} + px={3} + outline={'none'} + rightIcon={} _active={{ transform: 'none' }} {...(isOpen ? { - boxShadow: '0px 0px 4px #A8DBFF', - borderColor: 'primary.500' + borderColor: 'primary.600', + color: 'primary.700', + boxShadow: '0px 0px 0px 2.4px rgba(51, 112, 255, 0.15)' } - : {})} + : { + borderColor: 'myGray.200', + boxShadow: 'none' + })} {...styles} onClick={() => (isOpen ? onClose() : onOpenSelect())} > @@ -127,10 +135,194 @@ const MultipleRowSelect = ({ position={'absolute'} {...(popDirection === 'top' ? { - bottom: '45px' + transform: 'translateY(-105%)', + top: '0' } : { - top: '45px' + transform: 'translateY(105%)', + bottom: '0' + })} + py={2} + bg={'white'} + border={'1px solid #fff'} + boxShadow={'5'} + borderRadius={'md'} + zIndex={1} + minW={'100%'} + w={'max-content'} + > + + + + + )} + + ); +}; + +export const MultipleRowArraySelect = ({ + placeholder, + label, + value = [], + list, + emptyTip, + maxH = 300, + onSelect, + popDirection = 'bottom', + styles +}: MultipleArraySelectProps) => { + const { t } = useTranslation(); + const ref = useRef(null); + const { isOpen, onOpen, onClose } = useDisclosure(); + + const [navigationPath, setNavigationPath] = useState([]); + + const formatValue = useMemo(() => { + return Array.isArray(value) ? value : []; + }, [value]); + + // Close when clicking outside + useOutsideClick({ + ref: ref, + handler: onClose + }); + + const RenderList = useCallback( + ({ index, list }: { index: number; list: MultipleSelectProps['list'] }) => { + const currentNavValue = navigationPath[index]; + const selectedIndex = list.findIndex((item) => item.value === currentNavValue); + const children = list[selectedIndex]?.children || []; + const hasChildren = list.some((item) => item.children && item.children?.length > 0); + + const handleSelect = (item: ListItemType) => { + // Has children, set parent value + if (hasChildren) { + const newPath = [...navigationPath]; + newPath[index] = item.value; + setNavigationPath(newPath); + } else { + const parentValue = navigationPath[0]; + const newValues = [...formatValue]; + const newValue = [parentValue, item.value]; + + if (newValues.some((v) => v[0] === parentValue && v[1] === item.value)) { + onSelect(newValues.filter((v) => !(v[0] === parentValue && v[1] === item.value))); + } else { + onSelect([...newValues, newValue]); + } + } + }; + + return ( + <> + + {list.map((item) => { + const isSelected = item.value === currentNavValue; + const showCheckbox = !hasChildren; + const isChecked = + showCheckbox && + formatValue.some((v) => v[1] === item.value && v[0] === navigationPath[0]); + + return ( + handleSelect(item)} + {...(isSelected ? { color: 'primary.600' } : {})} + > + {showCheckbox && ( + } + mr={1} + /> + )} + {item.label} + + ); + })} + {list.length === 0 && ( + + )} + + {children.length > 0 && } + + ); + }, + [navigationPath, formatValue, onSelect] + ); + + const onOpenSelect = useCallback(() => { + setNavigationPath([]); + onOpen(); + }, []); + + return ( + + } + iconSpacing={2} + h={'auto'} + _active={{ + transform: 'none' + }} + _hover={{ + borderColor: 'primary.500' + }} + {...(isOpen + ? { + borderColor: 'primary.600', + color: 'primary.700', + boxShadow: '0px 0px 0px 2.4px rgba(51, 112, 255, 0.15)' + } + : { + borderColor: 'myGray.200', + boxShadow: 'none' + })} + {...styles} + onClick={() => (isOpen ? onClose() : onOpenSelect())} + className="nowheel" + > + + {label ?? placeholder} + + + {isOpen && ( + ( display: 'flex', alignItems: 'center', _hover: { - backgroundColor: 'myWhite.600' + backgroundColor: 'myGray.100', + color: 'primary.700' }, _notLast: { - mb: 2 + mb: 1 } }; const { isOpen, onOpen, onClose } = useDisclosure(); @@ -107,16 +108,19 @@ const MySelect = ( ref={ButtonRef} width={width} px={3} - rightIcon={} - variant={'whitePrimary'} + rightIcon={} + variant={'whitePrimaryOutline'} + size={'lg'} + fontSize={'sm'} textAlign={'left'} _active={{ transform: 'none' }} {...(isOpen ? { - boxShadow: '0px 0px 4px #A8DBFF', - borderColor: 'primary.500' + boxShadow: '0px 0px 0px 2.4px rgba(51, 112, 255, 0.15)', + borderColor: 'primary.600', + color: 'primary.700' } : {})} {...props} @@ -157,7 +161,7 @@ const MySelect = ( {...(value === item.value ? { ref: SelectedItemRef, - color: 'primary.600', + color: 'primary.700', bg: 'myGray.100' } : { diff --git a/packages/web/components/common/MySelect/type.d.ts b/packages/web/components/common/MySelect/type.d.ts index 7e14aed50..2b4831c63 100644 --- a/packages/web/components/common/MySelect/type.d.ts +++ b/packages/web/components/common/MySelect/type.d.ts @@ -4,9 +4,9 @@ type ListItemType = { value: any; children?: ListItemType[]; }; -export type MultipleSelectProps = { +export type MultipleSelectProps = { label?: string | React.ReactNode; - value: any[]; + value?: any[]; placeholder?: string; list: ListItemType[]; emptyTip?: string; @@ -15,3 +15,7 @@ export type MultipleSelectProps = { styles?: ButtonProps; popDirection?: 'top' | 'bottom'; }; +export type MultipleArraySelectProps = Omit & { + value?: any[][]; + onSelect: (val: any[][]) => void; +}; diff --git a/packages/web/components/common/MyTooltip/QuestionTip.tsx b/packages/web/components/common/MyTooltip/QuestionTip.tsx index 57a449165..506d22c64 100644 --- a/packages/web/components/common/MyTooltip/QuestionTip.tsx +++ b/packages/web/components/common/MyTooltip/QuestionTip.tsx @@ -1,6 +1,7 @@ import React from 'react'; import MyTooltip from '.'; -import { IconProps, QuestionOutlineIcon } from '@chakra-ui/icons'; +import { IconProps } from '@chakra-ui/icons'; +import MyIcon from '../Icon'; type Props = IconProps & { label?: string | React.ReactNode; @@ -9,7 +10,7 @@ type Props = IconProps & { const QuestionTip = ({ label, maxW, ...props }: Props) => { return ( - + ); }; diff --git a/packages/web/components/common/Tabs/LightRowTabs.tsx b/packages/web/components/common/Tabs/LightRowTabs.tsx index ab743c1c7..9b517a1c6 100644 --- a/packages/web/components/common/Tabs/LightRowTabs.tsx +++ b/packages/web/components/common/Tabs/LightRowTabs.tsx @@ -72,11 +72,11 @@ const LightRowTabs = ({ _hover={{ color: activeColor }} + fontWeight={'medium'} {...(value === item.value ? { color: activeColor, cursor: 'default', - fontWeight: 'bold', borderBottomColor: activeColor } : { diff --git a/packages/web/components/common/Textarea/CodeEditor/Editor.tsx b/packages/web/components/common/Textarea/CodeEditor/Editor.tsx index 49fb0ba7a..4177ef897 100644 --- a/packages/web/components/common/Textarea/CodeEditor/Editor.tsx +++ b/packages/web/components/common/Textarea/CodeEditor/Editor.tsx @@ -2,9 +2,10 @@ import React, { useCallback, useRef, useState } from 'react'; import Editor, { Monaco, loader } from '@monaco-editor/react'; import { Box, BoxProps } from '@chakra-ui/react'; import MyIcon from '../../Icon'; +import { getWebReqUrl } from '../../../../common/system/utils'; loader.config({ - paths: { vs: '/js/monaco-editor.0.45.0/vs' } + paths: { vs: getWebReqUrl('/js/monaco-editor.0.45.0/vs') } }); type EditorVariablePickerType = { diff --git a/packages/web/components/common/Textarea/JsonEditor/index.tsx b/packages/web/components/common/Textarea/JsonEditor/index.tsx index 18c70163c..feea41284 100644 --- a/packages/web/components/common/Textarea/JsonEditor/index.tsx +++ b/packages/web/components/common/Textarea/JsonEditor/index.tsx @@ -4,9 +4,10 @@ import { Box, BoxProps } from '@chakra-ui/react'; import MyIcon from '../../Icon'; import { useToast } from '../../../../hooks/useToast'; import { useTranslation } from 'next-i18next'; +import { getWebReqUrl } from '../../../../common/system/utils'; loader.config({ - paths: { vs: '/js/monaco-editor.0.45.0/vs' } + paths: { vs: getWebReqUrl('/js/monaco-editor.0.45.0/vs') } }); type EditorVariablePickerType = { diff --git a/packages/web/components/common/Textarea/PromptEditor/Editor.tsx b/packages/web/components/common/Textarea/PromptEditor/Editor.tsx index 4fdb78351..ce85e7d65 100644 --- a/packages/web/components/common/Textarea/PromptEditor/Editor.tsx +++ b/packages/web/components/common/Textarea/PromptEditor/Editor.tsx @@ -105,10 +105,10 @@ export default function Editor({ left={0} right={0} bottom={0} - py={3} - px={4} + py={2} + px={3} pointerEvents={'none'} - overflow={'overlay'} + overflow={'hidden'} > - + )} diff --git a/packages/web/components/common/Textarea/PromptEditor/index.module.scss b/packages/web/components/common/Textarea/PromptEditor/index.module.scss index de7297616..a161224d6 100644 --- a/packages/web/components/common/Textarea/PromptEditor/index.module.scss +++ b/packages/web/components/common/Textarea/PromptEditor/index.module.scss @@ -9,6 +9,45 @@ font-size: var(--chakra-fontSizes-sm); overflow-y: auto; + + &:hover { + border-color: var(--chakra-colors-primary-300); + } + &::-webkit-scrollbar { + color: var(--chakra-colors-myGray-100); + } + &::-webkit-scrollbar-thumb { + background-color: var(--chakra-colors-myGray-200) !important; + cursor: pointer; + } + &::-webkit-scrollbar-thumb:hover { + background-color: var(--chakra-colors-myGray-250) !important; + } +} + +.contentEditable_isFlow { + position: relative; + height: 100%; + width: 100%; + border: 1px solid var(--chakra-colors-myGray-200); + border-radius: var(--chakra-radii-sm); + padding: 6px 8px; + font-size: var(--chakra-fontSizes-sm); + overflow-y: auto; + + &:hover { + border-color: var(--chakra-colors-primary-300); + } + &::-webkit-scrollbar { + color: var(--chakra-colors-myGray-100); + } + &::-webkit-scrollbar-thumb { + background-color: var(--chakra-colors-myGray-200) !important; + cursor: pointer; + } + &::-webkit-scrollbar-thumb:hover { + background-color: var(--chakra-colors-myGray-250) !important; + } } .contentEditable:focus { diff --git a/packages/web/components/common/Textarea/PromptEditor/index.tsx b/packages/web/components/common/Textarea/PromptEditor/index.tsx index 1977942c4..cf8d0cb2f 100644 --- a/packages/web/components/common/Textarea/PromptEditor/index.tsx +++ b/packages/web/components/common/Textarea/PromptEditor/index.tsx @@ -1,4 +1,4 @@ -import { Box, Button, ModalBody, ModalFooter, useDisclosure } from '@chakra-ui/react'; +import { Button, ModalBody, ModalFooter, useDisclosure } from '@chakra-ui/react'; import React from 'react'; import { editorStateToText } from './utils'; import Editor from './Editor'; diff --git a/packages/web/components/common/Textarea/PromptEditor/modules/ComfirmVar/index.tsx b/packages/web/components/common/Textarea/PromptEditor/modules/ComfirmVar/index.tsx deleted file mode 100644 index 22c669f27..000000000 --- a/packages/web/components/common/Textarea/PromptEditor/modules/ComfirmVar/index.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { Box, Button, Image } from '@chakra-ui/react'; -import { useTranslation } from 'next-i18next'; -export default function ComfirmVar({ - newVariables, - onCancel, - onConfirm -}: { - newVariables: string[]; - onCancel: () => void; - onConfirm: () => void; -}) { - const { t } = useTranslation(); - return ( - <> - - - - - - - {t('common:undefined_var')} - - - {newVariables.map((item, index) => ( - - - {`{{`} - {item} - {`}}`} - - - ))} - - - - - - - - - - ); -} diff --git a/packages/web/components/core/workflow/NodeInputSelect.tsx b/packages/web/components/core/workflow/NodeInputSelect.tsx index 044754a67..ca170e8f5 100644 --- a/packages/web/components/core/workflow/NodeInputSelect.tsx +++ b/packages/web/components/core/workflow/NodeInputSelect.tsx @@ -150,12 +150,20 @@ const NodeInputSelect = ({ trigger="click" Button={ diff --git a/packages/web/i18n/en/app.json b/packages/web/i18n/en/app.json index e93f967de..b8bdb7b98 100644 --- a/packages/web/i18n/en/app.json +++ b/packages/web/i18n/en/app.json @@ -1,5 +1,8 @@ { + "Role_setting": "Role setting", "Run": "Execute", + "Team Tags Set": "Team tags", + "Team_Tags": "Team tags", "ai_settings": "AI Configuration", "all_apps": "All Applications", "app.Version name": "Version Name", @@ -27,6 +30,7 @@ "cron.every_month": "Run Monthly", "cron.every_week": "Run Weekly", "cron.interval": "Run at Intervals", + "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.", "day": "Day", "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.", @@ -37,6 +41,7 @@ "export_config_successful": "Configuration copied, some sensitive information automatically filtered. Please check for any remaining sensitive data.", "export_configs": "Export Configurations", "feedback_count": "User Feedback", + "file_quote_link": "Files", "file_recover": "File will overwrite current content", "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.", @@ -44,7 +49,7 @@ "go_to_chat": "Go to Conversation", "go_to_run": "Go to Execution", "image_upload": "Image Upload", - "image_upload_tip": "Please ensure to select a vision model that can process images.", + "image_upload_tip": "How to activate model image recognition capabilities", "import_configs": "Import Configurations", "import_configs_failed": "Import configuration failed, please ensure the configuration is correct!", "import_configs_success": "Import Successful", @@ -58,7 +63,7 @@ "intro": "A comprehensive model application orchestration system that offers out-of-the-box data processing and model invocation capabilities. It allows for rapid Dataset construction and workflow orchestration through Flow visualization, enabling complex Dataset scenarios!", "llm_not_support_vision": "This model does not support image recognition", "llm_use_vision": "Enable Image Recognition", - "llm_use_vision_tip": "Once image recognition is enabled, this model will automatically receive images uploaded from the 'dialog box' and image links in 'user questions'.", + "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.", "logs_chat_user": "user", "logs_empty": "No logs yet~", "logs_message_total": "Total Messages", @@ -69,8 +74,10 @@ "module.type": "\"{{type}}\" type\n{{description}}", "modules.Title is required": "Module name cannot be empty", "month.unit": "Day", + "move.hint": "After moving, the selected application/folder will inherit the permission settings of the new folder, and the original permission settings will become invalid.", "move_app": "Move Application", "not_json_file": "Please select a JSON file", + "open_vision_function_tip": "Models with icon switches have image recognition capabilities. \nAfter being turned on, the model will parse the pictures in the file link and automatically parse the pictures in the user's question (user question ≤ 500 words).", "or_drag_JSON": "or drag in JSON file", "paste_config": "Paste Configuration", "permission.des.manage": "Based on write permissions, you can configure publishing channels, view conversation logs, and assign permissions to the application.", @@ -125,14 +132,14 @@ "type.Simple bot": "Simple App", "type.Workflow bot": "Workflow", "upload_file_max_amount": "Maximum File Quantity", - "upload_file_max_amount_tip": "1. The maximum number of files that can be uploaded at one time.\n2. The maximum number of files remembered by the chat window: each round of dialogue will automatically retrieve files from history, files beyond the range will be forgotten.", + "upload_file_max_amount_tip": "Maximum number of files uploaded in a single round of conversation", "variable.select type_desc": "You can define a global variable that does not need to be filled in by the user.\n\nThe value of this variable can come from the API interface, the Query of the shared link, or assigned through the [Variable Update] module.", "variable.textarea_type_desc": "Allows users to input up to 4000 characters in the dialogue box.", "version.Revert success": "Revert Successful", "version_back": "Revert to Original State", "version_copy": "Duplicate", "version_initial_copy": "Duplicate - Original State", - "vision_model_title": "Enable Image Recognition", + "vision_model_title": "Image recognition ability", "week.Friday": "Friday", "week.Monday": "Monday", "week.Saturday": "Saturday", @@ -149,7 +156,7 @@ "workflow.read_files": "Document Parsing", "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.", - "workflow.read_files_tip": "Parse all uploaded documents in the dialogue and return the corresponding document content.", + "workflow.read_files_tip": "Parse the documents uploaded in this round of dialogue and return the corresponding document content", "workflow.select_description": "Description Text", "workflow.select_description_placeholder": "For example: \nAre there tomatoes in the fridge?", "workflow.select_description_tip": "You can add a description text to explain the meaning of each option to the user.", @@ -159,4 +166,4 @@ "workflow.user_file_input_desc": "Links to documents and images uploaded by users.", "workflow.user_select": "User Selection", "workflow.user_select_tip": "This module can configure multiple options for selection during the dialogue. Different options can lead to different workflow branches." -} \ No newline at end of file +} diff --git a/packages/web/i18n/en/chat.json b/packages/web/i18n/en/chat.json index 6c5c7c5e7..2f48e8371 100644 --- a/packages/web/i18n/en/chat.json +++ b/packages/web/i18n/en/chat.json @@ -1,5 +1,7 @@ { + "AI_input_is_empty": "The content passed to the AI ​​node is empty", "Delete_all": "Clear All Lexicon", + "LLM_model_response_empty": "The model flow response is empty, please check whether the model flow output is normal.", "chat_history": "Conversation History", "chat_input_guide_lexicon_is_empty": "Lexicon not configured yet", "citations": "{{num}} References", @@ -12,6 +14,7 @@ "contextual_preview": "Contextual Preview {{num}} Items", "csv_input_lexicon_tip": "Only CSV batch import is supported, click to download the template", "custom_input_guide_url": "Custom Lexicon URL", + "dataset_quote_type error": "Knowledge base reference type is wrong, correct type: { datasetId: string }[]", "delete_all_input_guide_confirm": "Are you sure you want to clear the input guide lexicon?", "empty_directory": "This directory is empty~", "file_amount_over": "Exceeded maximum file quantity {{max}}", @@ -29,15 +32,19 @@ "multiple_AI_conversations": "Multiple AI Conversations", "new_input_guide_lexicon": "New Lexicon", "no_workflow_response": "No workflow data", + "not_select_file": "No file selected", "plugins_output": "Plugin Output", "question_tip": "From top to bottom, the response order of each module", + "response.child total points": "Sub-workflow point consumption", + "response.dataset_concat_length": "Combined total", "response.node_inputs": "Node Inputs", "select": "Select", "select_file": "Upload File", "select_file_img": "Upload file / image", "select_img": "Upload Image", "stream_output": "Stream Output", + "unsupported_file_type": "Unsupported file types", "upload": "Upload", "view_citations": "View References", "web_site_sync": "Web Site Sync" -} \ No newline at end of file +} diff --git a/packages/web/i18n/en/common.json b/packages/web/i18n/en/common.json index 8626a21ad..a042562b9 100644 --- a/packages/web/i18n/en/common.json +++ b/packages/web/i18n/en/common.json @@ -53,6 +53,7 @@ "code_error.error_code.502": "Gateway Error", "code_error.error_code.503": "Server Overloaded or Under Maintenance", "code_error.error_code.504": "Gateway Timeout", + "code_error.error_code[429]": "Requests are too frequent", "code_error.error_message.403": "Credential Error", "code_error.error_message.510": "Insufficient Account Balance", "code_error.error_message.511": "Unauthorized to Operate This Model", @@ -69,12 +70,17 @@ "code_error.system_error.community_version_num_limit": "Exceeded Open Source Version Limit, Please Upgrade to Commercial Version: https://tryfastgpt.ai", "code_error.team_error.ai_points_not_enough": "Insufficient AI Points", "code_error.team_error.app_amount_not_enough": "Application Limit Reached", + "code_error.team_error.cannot_delete_default_group": "Cannot delete default group", "code_error.team_error.dataset_amount_not_enough": "Dataset Limit Reached", "code_error.team_error.dataset_size_not_enough": "Insufficient Dataset Capacity, Please Expand", + "code_error.team_error.group_name_duplicate": "Duplicate group name", + "code_error.team_error.group_name_empty": "Group name cannot be empty", + "code_error.team_error.group_not_exist": "Group does not exist", "code_error.team_error.over_size": "error.team.overSize", "code_error.team_error.plugin_amount_not_enough": "Plugin Limit Reached", "code_error.team_error.re_rank_not_enough": "Unauthorized to Use Re-Rank", "code_error.team_error.un_auth": "Unauthorized to Operate This Team", + "code_error.team_error.user_not_active": "The user did not accept or has left the team", "code_error.team_error.website_sync_not_enough": "Unauthorized to Use Website Sync", "code_error.token_error_code.403": "Invalid Login Status, Please Re-login", "code_error.user_error.balance_not_enough": "Insufficient Account Balance", @@ -116,6 +122,7 @@ "common.Documents": "Documents", "common.Done": "Done", "common.Edit": "Edit", + "common.Error": "Error", "common.Exit": "Exit", "common.Exit Directly": "Exit Directly", "common.Expired Time": "Expiration Time", @@ -147,6 +154,7 @@ "common.Params": "Parameters", "common.Password inconsistency": "Passwords Do Not Match", "common.Permission": "Permission", + "common.Permission_tip": "Individual permissions are greater than group permissions", "common.Please Input Name": "Please Enter a Name", "common.Read document": "Read Document", "common.Read intro": "Read Introduction", @@ -184,7 +192,6 @@ "common.Update Successful": "Updated Successfully", "common.Username": "Username", "common.Waiting": "Waiting", - "common.Error": "Error", "common.Warning": "Warning", "common.Website": "Website", "common.all_result": "Full Results", @@ -542,6 +549,7 @@ "core.dataset.import.Chunk Range": "Range: {{min}}~{{max}}", "core.dataset.import.Chunk Split": "Direct Segmentation", "core.dataset.import.Chunk Split Tip": "Segment the text according to certain rules and convert it into a format that can be semantically searched. Suitable for most scenarios. No additional model processing is required, and the cost is low.", + "core.dataset.import.Continue upload": "Continue upload", "core.dataset.import.Custom process": "Custom Rules", "core.dataset.import.Custom process desc": "Customize segmentation and preprocessing rules", "core.dataset.import.Custom prompt": "Custom Prompt", @@ -570,11 +578,10 @@ "core.dataset.import.Select source": "Select Source", "core.dataset.import.Source name": "Source Name", "core.dataset.import.Sources list": "Source List", - "core.dataset.import.Continue upload": "Continue upload", - "core.dataset.import.Upload complete": "Upload complete", "core.dataset.import.Start upload": "Start Upload", "core.dataset.import.Total files": "Total {{total}} Files", "core.dataset.import.Training mode": "Training Mode", + "core.dataset.import.Upload complete": "Upload complete", "core.dataset.import.Upload data": "Confirm Upload", "core.dataset.import.Upload file progress": "File Upload Progress", "core.dataset.import.Upload status": "Status", @@ -885,7 +892,9 @@ "is_using": "In Use", "item_description": "Field Description", "item_name": "Field Name", + "just_now": "just", "key_repetition": "Key Repetition", + "move.confirm": "Confirm move", "navbar.Account": "Account", "navbar.Chat": "Chat", "navbar.Datasets": "Datasets", @@ -966,6 +975,9 @@ "support.outlink.Usage points": "Points Consumption", "support.outlink.share.Response Quote": "Return Quote", "support.outlink.share.Response Quote tips": "Return quoted content in the share link, but do not allow users to download the original document", + "support.outlink.share.running_node": "Running node", + "support.outlink.share.show_complete_quote": "View original source", + "support.outlink.share.show_complete_quote_tips": "View and download the complete citation document, or jump to the citation website", "support.permission.Permission": "Permission", "support.standard.AI Bonus Points": "AI Points", "support.standard.due_date": "Due Date", @@ -1115,7 +1127,6 @@ "tag_list": "Tag List", "team_tag": "Team Tag", "textarea_variable_picker_tip": "Enter \"/\" to select a variable", - "undefined_var": "Referenced an undefined variable, add it automatically?", "unit.character": "Character", "unit.minute": "Minute", "unusable_variable": "No Usable Variables", @@ -1197,8 +1208,12 @@ "user.team.member.waiting": "Pending Acceptance", "user.team.role.Admin": "Admin", "user.team.role.Owner": "Owner", + "user.team.role.Visitor": "visitor", + "user.team.role.writer": "writable member", "user.type": "Type", "verification": "Verification", "xx_search_result": "{{key}} Search Results", - "yes": "Yes" + "yes": "Yes", + "yesterday": "yesterday", + "yesterday_detail_time": "Yesterday {{time}}" } diff --git a/packages/web/i18n/en/dataset.json b/packages/web/i18n/en/dataset.json index 8f95e0ae8..7db8c2f9c 100644 --- a/packages/web/i18n/en/dataset.json +++ b/packages/web/i18n/en/dataset.json @@ -8,6 +8,7 @@ "confirm_to_rebuild_embedding_tip": "Are you sure you want to switch the index for the Dataset?\nSwitching the index is a significant operation that requires re-indexing all data in your Dataset, which may take a long time. Please ensure your account has sufficient remaining points.\n\nAdditionally, you need to update the applications that use this Dataset to avoid conflicts with other indexed model Datasets.", "custom_data_process_params": "Custom", "custom_data_process_params_desc": "Customize data processing rules", + "data.ideal_chunk_length": "ideal block length", "data_process_params": "Processing parameters", "data_process_setting": "Data processing configuration", "dataset.no_collections": "No datasets available", @@ -25,6 +26,7 @@ "ideal_chunk_length_tips": "Segment according to the end symbol and combine multiple segments into one block. This value determines the estimated size of the block, if there is any fluctuation.", "import.Auto mode Estimated Price Tips": "The text understanding model needs to be called, which requires more points: {{price}} points/1K tokens", "import.Embedding Estimated Price Tips": "Only use the index model and consume a small amount of AI points: {{price}} points/1K tokens", + "move.hint": "After moving, the selected knowledge base/folder will inherit the permission settings of the new folder, and the original permission settings will become invalid.", "permission.des.manage": "Can manage the entire knowledge base data and information", "permission.des.read": "View knowledge base content", "permission.des.write": "Ability to add and change knowledge base content", @@ -44,4 +46,4 @@ "training_mode": "Chunk mode", "website_dataset": "Website Sync", "website_dataset_desc": "Website sync allows you to build a Dataset directly using a web link." -} \ No newline at end of file +} diff --git a/packages/web/i18n/en/publish.json b/packages/web/i18n/en/publish.json index f0b2e00cf..c1dd0f127 100644 --- a/packages/web/i18n/en/publish.json +++ b/packages/web/i18n/en/publish.json @@ -1,6 +1,7 @@ { "app_key_tips": "These keys are already linked to the current application. Check the documentation for detailed usage.", "basic_info": "Basic Info", + "config": "Visibility configuration", "copy_link_hint": "Copy the link below to the specified location", "create_api_key": "Create New Key", "create_link": "Create Link", @@ -19,10 +20,14 @@ "official_account.edit_modal_title": "Edit WeChat Official Account Integration", "official_account.name": "WeChat Official Account Integration", "official_account.params": "WeChat Official Account Parameters", + "private_config": "Visibility configuration", "publish_name": "Name", "qpm_is_empty": "QPM cannot be empty", "qpm_tips": "Maximum number of queries per minute per IP", + "quote_content": "Quote content", "request_address": "Request URL", + "show_node": "real-time running status", + "show_origin_content": "View original source", "show_share_link_modal_title": "Get Started", "token_auth": "Token Authentication", "token_auth_tips": "Token authentication server URL. If provided, a request will be sent to the specified server for authentication before each conversation.", @@ -33,4 +38,4 @@ "wecom.create_modal_title": "Create WeCom Bot", "wecom.edit_modal_title": "Edit WeCom Bot", "wecom.title": "Publish to WeCom Bot" -} \ No newline at end of file +} diff --git a/packages/web/i18n/en/user.json b/packages/web/i18n/en/user.json index 0af708fd7..6a59c1f96 100644 --- a/packages/web/i18n/en/user.json +++ b/packages/web/i18n/en/user.json @@ -22,6 +22,8 @@ "bind_inform_account_success": "Notification Account Bound Successfully", "delete.admin_failed": "Failed to Delete Admin", "delete.admin_success": "Admin Deleted Successfully", + "delete.failed": "Delete failed", + "delete.success": "Delete successfully", "has_chosen": "Selected", "individuation": "Individuation", "login.error": "Login Error", @@ -32,6 +34,7 @@ "notification.Bind Notification Pipe Hint": "Please bind a notification receiving account to ensure you receive notifications such as plan expiration reminders, ensuring your service runs smoothly.", "notification.remind_owner_bind": "Please remind the creator to bind a notification account", "operations": "Actions", + "owner": "owner", "password.code_required": "Verification Code Required", "password.code_send_error": "Failed to Send Verification Code", "password.code_sended": "Verification Code Sent", @@ -76,6 +79,32 @@ "synchronization.title": "Enter the sync tag link and click the sync button to synchronize", "team.Add manager": "Add Admin", "team.add_collaborator": "Add Collaborator", + "team.add_writer": "Add writable members", + "team.avatar_and_name": "avatar", + "team.belong_to_group": "Member group", + "team.group.avatar": "Group avatar", + "team.group.create": "Create group", + "team.group.create_failed": "Failed to create group", + "team.group.default_group": "Default group", + "team.group.delete_confirm": "Confirm to delete group?", + "team.group.edit": "Edit group", + "team.group.edit_info": "Edit information", + "team.group.group": "group", + "team.group.keep_admin": "Keep administrator rights", + "team.group.manage_member": "Managing members", + "team.group.manage_tip": "You can invite members, delete members, create groups, manage all groups, and assign permissions to groups and members", + "team.group.members": "member", + "team.group.name": "Group name", + "team.group.permission.manage": "administrator", + "team.group.permission.write": "Workbench/knowledge base creation", + "team.group.permission_tip": "Members with individually configured permissions will follow the individual permission configuration and will no longer be affected by group permissions.\n\nIf a member is in multiple permission groups, the member's permissions are combined.", + "team.group.role.admin": "administrator", + "team.group.role.member": "member", + "team.group.role.owner": "owner", + "team.group.search_placeholder": "Search member/group name", + "team.group.set_as_admin": "Set as administrator", + "team.group.toast.can_not_delete_owner": "Owner cannot be deleted, please transfer first", + "team.group.transfer_owner": "transfer owner", "team.manage_collaborators": "Manage Collaborators", "team.no_collaborators": "No Collaborators", "team.write_role_member": "", @@ -84,4 +113,4 @@ "usage.share": "Share Link", "usage.wecom": "WeCom", "usage_record": "Usages" -} \ No newline at end of file +} diff --git a/packages/web/i18n/en/workflow.json b/packages/web/i18n/en/workflow.json index 48ded9c3c..7940b4b44 100644 --- a/packages/web/i18n/en/workflow.json +++ b/packages/web/i18n/en/workflow.json @@ -1,5 +1,6 @@ { "Array_element": "Array element", + "Array_element_index": "Index", "Code": "Code", "Confirm_sync_node": "It will be updated to the latest node configuration and fields that do not exist in the template will be deleted (including all custom fields).\n\nIf the fields are complex, it is recommended that you copy a node first and then update the original node to facilitate parameter copying.", "Node.Open_Node_Course": "Open node course", @@ -28,6 +29,7 @@ "contains": "Contains", "content_to_retrieve": "Content to Retrieve", "content_to_search": "Content to Search", + "contextMenu.addComment": "Add comment", "context_menu.add_comment": "Add comment", "create_link_error": "Error creating link", "custom_feedback": "Custom Feedback", @@ -122,13 +124,15 @@ "pass_returned_object_as_output_to_next_nodes": "Pass the object returned in the code as output to the next nodes. The variable name needs to correspond to the return key.", "plugin.Instruction_Tip": "You can configure an instruction to explain the purpose of the plugin. This instruction will be displayed each time the plugin is used. Supports standard Markdown syntax.", "plugin.Instructions": "Instructions", + "plugin.global_file_input": "File links (deprecated)", + "plugin_file_abandon_tip": "Plugin global file upload has been deprecated, please adjust it as soon as possible. \nRelated functions can be achieved through plug-in input and adding image type input.", "plugin_input": "Plugin Input", "plugin_output_tool": "When the plug-in is executed as a tool, whether this field responds as a result of the tool", "question_classification": "Question Classification", "question_optimization": "Question Optimization", "quote_content_placeholder": "The structure of the reference content can be customized to better suit different scenarios. \nSome variables can be used for template configuration\n\n{{q}} - main content\n\n{{a}} - auxiliary data\n\n{{source}} - source name\n\n{{sourceId}} - source ID\n\n{{index}} - nth reference", "quote_content_tip": "The structure of the reference content can be customized to better suit different scenarios. Some variables can be used for template configuration:\n\n{{q}} - main content\n{{a}} - auxiliary data\n{{source}} - source name\n{{sourceId}} - source ID\n{{index}} - nth reference\nThey are all optional and the following are the default values:\n\n{{default}}", - "quote_num": "Quote {{num}}", + "quote_num": "Dataset", "quote_prompt_tip": "You can use {{quote}} to insert a quote content template and {{question}} to insert a question (Role=user).\n\nThe following are the default values:\n\n{{default}}", "quote_role_system_tip": "Please note that the {{question}} variable is removed from the \"Quote Template Prompt Words\"", "quote_role_user_tip": "Please pay attention to adding the {{question}} variable in the \"Quote Template Prompt Word\"", @@ -174,6 +178,7 @@ "tool_params.params_description_placeholder": "Name/Age/SQL statement..", "tool_params.params_name": "Name", "tool_params.params_name_placeholder": "name/age/sql", + "tool_params.tool_params_result": "Parameter configuration results", "trigger_after_application_completion": "Will be triggered after the application is fully completed", "update_link_error": "Error updating link", "update_specified_node_output_or_global_variable": "Can update the output value of a specified node or update global variables", @@ -190,4 +195,4 @@ "workflow.Switch_success": "Switch Successful", "workflow.Team cloud": "Team Cloud", "workflow.exit_tips": "Your changes have not been saved. 'Exit directly' will not save your edits." -} \ No newline at end of file +} diff --git a/packages/web/i18n/zh/app.json b/packages/web/i18n/zh/app.json index 9b8fda251..a9d178419 100644 --- a/packages/web/i18n/zh/app.json +++ b/packages/web/i18n/zh/app.json @@ -1,5 +1,8 @@ { + "Role_setting": "权限设置", "Run": "运行", + "Team Tags Set": "团队标签", + "Team_Tags": "团队标签", "ai_settings": "AI 配置", "all_apps": "全部应用", "app.Version name": "版本名称", @@ -27,6 +30,7 @@ "cron.every_month": "每月执行", "cron.every_week": "每周执行", "cron.interval": "间隔执行", + "dataset_search_tool_description": "调用“语义检索”和“全文检索”能力,从“知识库”中查找可能与问题相关的参考内容。优先调用该工具来辅助回答用户的问题。", "day": "日", "document_quote": "文档引用", "document_quote_tip": "通常用于接受用户上传的文档内容(这需要文档解析),也可以用于引用其他字符串数据。", @@ -37,6 +41,7 @@ "export_config_successful": "已复制配置,自动过滤部分敏感信息,请注意检查是否仍有敏感数据", "export_configs": "导出配置", "feedback_count": "用户反馈", + "file_quote_link": "文件链接", "file_recover": "文件将覆盖当前内容", "file_upload": "文件上传", "file_upload_tip": "开启后,可以上传文档/图片。文档保留7天,图片保留15天。使用该功能可能产生较多额外费用。为保证使用体验,使用该功能时,请选择上下文长度较大的AI模型。", @@ -44,7 +49,7 @@ "go_to_chat": "去对话", "go_to_run": "去运行", "image_upload": "图片上传", - "image_upload_tip": "请确保选择可处理图片的视觉模型", + "image_upload_tip": "如何启动模型图片识别能力", "import_configs": "导入配置", "import_configs_failed": "导入配置失败,请确保配置正常!", "import_configs_success": "导入成功", @@ -57,8 +62,8 @@ "interval.per_hour": "每小时", "intro": "是一个大模型应用编排系统,提供开箱即用的数据处理、模型调用等能力,可以快速的构建知识库并通过 Flow 可视化进行工作流编排,实现复杂的知识库场景!", "llm_not_support_vision": "该模型不支持图片识别", - "llm_use_vision": "启用图片识别", - "llm_use_vision_tip": "启用图片识别后,该模型会自动接收来自“对话框上传”的图片,以及“用户问题”中的图片链接。", + "llm_use_vision": "图片识别", + "llm_use_vision_tip": "点击模型选择后,可以看到模型是否支持图片识别以及控制是否启动图片识别的能力。启动图片识别后,模型会读取文件链接里图片内容,并且如果用户问题少于 500 字,会自动解析用户问题中的图片。", "logs_chat_user": "使用者", "logs_empty": "还没有日志噢~", "logs_message_total": "消息总数", @@ -69,9 +74,10 @@ "module.type": "\"{{type}}\"类型\n{{description}}", "modules.Title is required": "模块名不能为空", "month.unit": "号", - "move_app": "移动应用", "move.hint": "移动后,所选应用/文件夹将继承新文件夹的权限设置,原先的权限设置失效。", + "move_app": "移动应用", "not_json_file": "请选择JSON文件", + "open_vision_function_tip": "有图示开关的模型即拥有图片识别能力。若开启,模型会解析文件链接里的图片,并自动解析用户问题中的图片(用户问题≤500字时生效)。", "or_drag_JSON": "或拖入JSON文件", "paste_config": "粘贴配置", "permission.des.manage": "写权限基础上,可配置发布渠道、查看对话日志、分配该应用权限", @@ -126,14 +132,14 @@ "type.Simple bot": "简易应用", "type.Workflow bot": "工作流", "upload_file_max_amount": "最大文件数量", - "upload_file_max_amount_tip": "1.单次上传文件的最大数量。\n2.对话窗口记忆的最大文件数量:每轮对话会自动获取历史中的文件,超出范围的文件会被遗忘。", + "upload_file_max_amount_tip": "单轮对话中最大上传文件数量", "variable.select type_desc": "可以为工作流定义全局变量,常用临时缓存。赋值的方式包括:\n1. 从对话页面的 query 参数获取。\n2. 通过 API 的 variables 对象传递。\n3. 通过【变量更新】节点进行赋值。", "variable.textarea_type_desc": "允许用户最多输入4000字的对话框。", "version.Revert success": "回滚成功", "version_back": "回到初始状态", "version_copy": "副本", "version_initial_copy": "副本-初始状态", - "vision_model_title": "启用图片识别", + "vision_model_title": "图片识别能力", "week.Friday": "星期五", "week.Monday": "星期一", "week.Saturday": "星期六", @@ -150,7 +156,7 @@ "workflow.read_files": "文档解析", "workflow.read_files_result": "文档解析结果", "workflow.read_files_result_desc": "文档原文,由文件名和文档内容组成,多个文件之间通过横线隔开。", - "workflow.read_files_tip": "解析对话中所有上传的文档,并返回对应文档内容", + "workflow.read_files_tip": "解析本轮对话上传的文档,并返回对应文档内容", "workflow.select_description": "说明文字", "workflow.select_description_placeholder": "例如: \n冰箱里是否有西红柿?", "workflow.select_description_tip": "你可以添加一段说明文字,用以向用户说明每个选项代表的含义。", @@ -160,4 +166,4 @@ "workflow.user_file_input_desc": "用户上传的文档和图片链接", "workflow.user_select": "用户选择", "workflow.user_select_tip": "该模块可配置多个选项,以供对话时选择。不同选项可导向不同工作流支线" -} \ No newline at end of file +} diff --git a/packages/web/i18n/zh/chat.json b/packages/web/i18n/zh/chat.json index ef327cfdf..a82ce845f 100644 --- a/packages/web/i18n/zh/chat.json +++ b/packages/web/i18n/zh/chat.json @@ -1,5 +1,7 @@ { + "AI_input_is_empty": "传入AI 节点的内容为空", "Delete_all": "清空词库", + "LLM_model_response_empty": "模型流响应为空,请检查模型流输出是否正常", "chat_history": "聊天记录", "chat_input_guide_lexicon_is_empty": "还没有配置词库", "citations": "{{num}}条引用", @@ -12,6 +14,7 @@ "contextual_preview": "上下文预览 {{num}} 条", "csv_input_lexicon_tip": "仅支持 CSV 批量导入,点击下载模板", "custom_input_guide_url": "自定义词库地址", + "dataset_quote_type error": "知识库引用类型错误,正确类型:{ datasetId: string }[]", "delete_all_input_guide_confirm": "确定要清空输入引导词库吗?", "empty_directory": "这个目录已经没东西可选了~", "file_amount_over": "超出最大文件数量 {{max}}", @@ -25,20 +28,23 @@ "insert_input_guide,_some_data_already_exists": "有重复数据,已自动过滤,共插入 {{len}} 条数据", "is_chatting": "正在聊天中...请等待结束", "items": "条", - "module_runtime_and": "模块运行时间和", + "module_runtime_and": "工作流总运行时间", "multiple_AI_conversations": "多组 AI 对话", "new_input_guide_lexicon": "新词库", "no_workflow_response": "没有运行数据", + "not_select_file": "未选择文件", "plugins_output": "插件输出", "question_tip": "从上到下,为各个模块的响应顺序", "response.child total points": "子工作流积分消耗", + "response.dataset_concat_length": "合并后总数", "response.node_inputs": "节点输入", "select": "选择", "select_file": "上传文件", "select_file_img": "上传文件/图片", "select_img": "上传图片", "stream_output": "流输出", + "unsupported_file_type": "不支持的文件类型", "upload": "上传", "view_citations": "查看引用", "web_site_sync": "Web站点同步" -} \ No newline at end of file +} diff --git a/packages/web/i18n/zh/common.json b/packages/web/i18n/zh/common.json index 39762c250..d963a4198 100644 --- a/packages/web/i18n/zh/common.json +++ b/packages/web/i18n/zh/common.json @@ -18,6 +18,9 @@ "FAQ.switch_package_a": "套餐使用规则为优先使用更高级的套餐,因此,购买的新套餐若比当前套餐更高级,则新套餐立即生效:否则将继续使用当前套餐。", "FAQ.switch_package_q": "是否切换订阅套餐?", "Folder": "文件夹", + "just_now": "刚刚", + "yesterday": "昨天", + "yesterday_detail_time": "昨天 {{time}}", "Login": "登录", "Move": "移动", "Name": "名称", @@ -971,8 +974,11 @@ "support.outlink.Max usage points": "积分上限", "support.outlink.Max usage points tip": "该链接最多允许使用多少积分,超出后将无法使用。-1 代表无限制。", "support.outlink.Usage points": "积分消耗", - "support.outlink.share.Response Quote": "返回引用", - "support.outlink.share.Response Quote tips": "在分享链接中返回引用内容,但不会允许用户下载原文档", + "support.outlink.share.Response Quote": "引用内容", + "support.outlink.share.Response Quote tips": "查看知识库搜索的引用内容,不可查看完整引用文档或跳转引用网站", + "support.outlink.share.running_node": "运行节点", + "support.outlink.share.show_complete_quote": "查看来源原文", + "support.outlink.share.show_complete_quote_tips": "查看及下载完整引用文档,或跳转引用网站", "support.permission.Permission": "权限", "support.standard.AI Bonus Points": "AI 积分", "support.standard.due_date": "到期时间", @@ -1122,7 +1128,6 @@ "tag_list": "标签列表", "team_tag": "团队标签", "textarea_variable_picker_tip": "输入\"/\"可选择变量", - "undefined_var": "引用了未定义的变量,是否自动添加?", "unit.character": "字符", "unit.minute": "分钟", "unusable_variable": "无可用变量", diff --git a/packages/web/i18n/zh/publish.json b/packages/web/i18n/zh/publish.json index e36bf6a37..e24230697 100644 --- a/packages/web/i18n/zh/publish.json +++ b/packages/web/i18n/zh/publish.json @@ -1,6 +1,7 @@ { "app_key_tips": "这些 key 已有当前应用标识,具体使用可参考文档", "basic_info": "基本信息", + "config": "可见度配置", "copy_link_hint": "将下面链接复制到指定位置", "create_api_key": "创建新 key", "create_link": "创建链接", @@ -19,13 +20,17 @@ "official_account.edit_modal_title": "编辑微信公众号接入", "official_account.name": "微信公众号接入", "official_account.params": "微信公众号参数", + "private_config": "可见度配置", "publish_name": "名称", "qpm_is_empty": "QPM 不能为空", "qpm_tips": "每个 IP 每分钟最多提问多少次", + "quote_content": "知识库引用", "request_address": "请求地址", + "show_node": "实时运行状态", + "show_origin_content": "查看来源原文", "show_share_link_modal_title": "开始使用", "token_auth": "身份验证", - "token_auth_tips": "身份校验服务器地址,如填写该值,每次对话前都会向指定服务器发送一个请求,进行身份校验", + "token_auth_tips": "身份校验服务器地址", "token_auth_use_cases": "查看身份验证使用说明", "wecom.api": "企微 API", "wecom.bot": "企业微信机器人", @@ -33,4 +38,4 @@ "wecom.create_modal_title": "创建企微机器人", "wecom.edit_modal_title": "编辑企微机器人", "wecom.title": "发布到企业微信机器人" -} \ No newline at end of file +} diff --git a/packages/web/i18n/zh/workflow.json b/packages/web/i18n/zh/workflow.json index f1b6a116e..74310d681 100644 --- a/packages/web/i18n/zh/workflow.json +++ b/packages/web/i18n/zh/workflow.json @@ -1,5 +1,6 @@ { "Array_element": "数组元素", + "Array_element_index": "下标", "Code": "代码", "Confirm_sync_node": "将会更新至最新的节点配置,不存在模板中的字段将会被删除(包括所有自定义字段)。\n如果字段较为复杂,建议您先复制一份节点,再更新原来的节点,便于参数复制。", "Node.Open_Node_Course": "查看节点教程", @@ -123,13 +124,15 @@ "pass_returned_object_as_output_to_next_nodes": "将代码中 return 的对象作为输出,传递给后续的节点。变量名需要对应 return 的 key", "plugin.Instruction_Tip": "可以配置一段说明,以解释该插件的用途。每次使用插件前,会显示该段说明。支持标准 Markdown 语法。", "plugin.Instructions": "使用说明", + "plugin.global_file_input": "文件链接(弃用)", + "plugin_file_abandon_tip": "插件全局文件上传已弃用,请尽快调整。可以通过插件输入,添加图片类型输入来实现相关功能。", "plugin_input": "插件输入", "plugin_output_tool": "插件作为工具执行时,该字段是否作为工具响应结果", "question_classification": "问题分类", "question_optimization": "问题优化", "quote_content_placeholder": "可以自定义引用内容的结构,以更好的适配不同场景。可以使用一些变量来进行模板配置\n{{q}} - 主要内容\n{{a}} - 辅助数据\n{{source}} - 来源名\n{{sourceId}} - 来源ID\n{{index}} - 第 n 个引用", "quote_content_tip": "可以自定义引用内容的结构,以更好的适配不同场景。可以使用一些变量来进行模板配置\n{{q}} - 主要内容\n{{a}} - 辅助数据\n{{source}} - 来源名\n{{sourceId}} - 来源ID\n{{index}} - 第 n 个引用\n他们都是可选的,下面是默认值:\n{{default}}", - "quote_num": "引用{{num}}", + "quote_num": "引用", "quote_prompt_tip": "可以用 {{quote}} 来插入引用内容模板,使用 {{question}} 来插入问题(Role=user)。\n下面是默认值:\n{{default}}", "quote_role_system_tip": "请注意从“引用模板提示词”中移除 {{question}} 变量", "quote_role_user_tip": "请注意在“引用模板提示词”中添加 {{question}} 变量", @@ -192,4 +195,4 @@ "workflow.Switch_success": "切换成功", "workflow.Team cloud": "团队云端", "workflow.exit_tips": "您的更改尚未保存,「直接退出」将不会保存您的编辑记录。" -} \ No newline at end of file +} diff --git a/packages/web/styles/theme.ts b/packages/web/styles/theme.ts index 1f5b49227..0e6258c02 100644 --- a/packages/web/styles/theme.ts +++ b/packages/web/styles/theme.ts @@ -46,7 +46,8 @@ const Button = defineStyleConfig({ px: '2', py: '0', h: '24px', - fontWeight: 'normal', + minH: '24px', + fontWeight: 'medium', borderRadius: 'sm' }, xsSquare: { @@ -54,24 +55,27 @@ const Button = defineStyleConfig({ px: '0', py: '0', h: '24px', + minH: '24px', w: '24px', - fontWeight: 'normal', + fontWeight: 'medium', borderRadius: 'sm' }, sm: { fontSize: 'sm', px: '3', py: 0, - fontWeight: 'normal', + fontWeight: 'medium', h: '30px', + minH: '30px', borderRadius: 'sm' }, smSquare: { fontSize: 'sm', px: '0', py: 0, - fontWeight: 'normal', + fontWeight: 'medium', h: '30px', + minH: '30px', w: '30px', borderRadius: 'sm' }, @@ -80,34 +84,38 @@ const Button = defineStyleConfig({ px: '4', py: 0, h: '34px', - fontWeight: 'normal', - borderRadius: 'md' + minH: '34px', + fontWeight: 'medium', + borderRadius: 'sm' }, mdSquare: { fontSize: 'sm', px: '0', py: 0, h: '34px', + minH: '34px', w: '34px', - fontWeight: 'normal', - borderRadius: 'md' + fontWeight: 'medium', + borderRadius: 'sm' }, lg: { fontSize: 'md', px: '4', py: 0, h: '40px', - fontWeight: 'normal', - borderRadius: 'lg' + minH: '40px', + fontWeight: 'medium', + borderRadius: 'md' }, lgSquare: { fontSize: 'md', px: '0', py: 0, h: '40px', + minH: '40px', w: '40px', - fontWeight: 'normal', - borderRadius: 'lg' + fontWeight: 'medium', + borderRadius: 'md' } }, variants: { @@ -175,6 +183,16 @@ const Button = defineStyleConfig({ color: 'myGray.600 !important' } }, + whitePrimaryOutline: { + border: '1px solid', + borderColor: 'myGray.250', + bg: 'white', + transition: 'background 0.1s', + _hover: { + color: 'primary.600', + borderColor: 'primary.300' + } + }, whitePrimary: { color: 'myGray.600', border: '1px solid', @@ -288,12 +306,18 @@ const Input: ComponentStyleConfig = { sm: defineStyle({ field: { h: '32px', - borderRadius: 'md' + borderRadius: 'sm' } }), md: defineStyle({ field: { - h: '34px', + h: '36px', + borderRadius: 'sm' + } + }), + lg: defineStyle({ + field: { + h: '40px', borderRadius: 'md' } }) @@ -303,11 +327,15 @@ const Input: ComponentStyleConfig = { field: { border: '1px solid', borderColor: 'borderColor.low', + px: 3, _focus: { borderColor: 'primary.500', boxShadow: shadowLight, bg: 'white' }, + _hover: { + borderColor: 'primary.300' + }, _disabled: { color: 'myGray.400', bg: 'myWhite.300' @@ -326,14 +354,14 @@ const NumberInput = numInputMultiStyle({ sm: defineStyle({ field: { h: '32px', - borderRadius: 'md', + borderRadius: 'sm', fontsize: 'sm' } }), - md: defineStyle({ + lg: defineStyle({ field: { h: '40px', - borderRadius: 'md', + borderRadius: 'sm', fontsize: 'sm' } }) @@ -347,7 +375,7 @@ const NumberInput = numInputMultiStyle({ _focus: { borderColor: 'primary.500 !important', boxShadow: `${shadowLight} !important`, - bg: 'transparent' + bg: 'white' }, _disabled: { color: 'myGray.400 !important', @@ -356,10 +384,12 @@ const NumberInput = numInputMultiStyle({ }, stepper: { bg: 'transparent', - border: 'none', color: 'myGray.600', _active: { color: 'primary.500' + }, + _hover: { + bg: 'myGray.100' } } }) @@ -373,16 +403,24 @@ const Textarea: ComponentStyleConfig = { variants: { outline: { border: '1px solid', + px: 3, borderRadius: 'md', borderColor: 'myGray.200', fontSize: 'sm', _hover: { - borderColor: '' + borderColor: 'primary.300' }, _focus: { borderColor: 'primary.500', boxShadow: shadowLight, bg: 'white' + }, + '&::-webkit-resizer': { + background: "url('/icon/resizer.svg') no-repeat", + backgroundSize: '11px', + backgroundPosition: 'right bottom', + backgroundPositionX: 'right 12px', + backgroundPositionY: 'bottom 12px' } } }, diff --git a/projects/app/.env.template b/projects/app/.env.template index 0bc4cbbc1..60d4a3752 100644 --- a/projects/app/.env.template +++ b/projects/app/.env.template @@ -16,6 +16,9 @@ OPENAI_BASE_URL=https://api.openai.com/v1 # 通用key。可以是 openai 的也可以是 oneapi 的。 # 此处逻辑:优先走 ONEAPI_URL,如果填写了 ONEAPI_URL,key 也需要是 ONEAPI 的 key CHAT_API_KEY=sk-xxxx +# 是否将图片转成 base64 传递给模型,本地开发和内网环境使用共有模型时候需要设置为 true +MULTIPLE_DATA_TO_BASE64=true + # mongo 数据库连接参数,本地开发连接远程数据库时,可能需要增加 directConnection=true 参数,才能连接上。 MONGODB_URI=mongodb://username:password@0.0.0.0:27017/fastgpt?authSource=admin @@ -32,6 +35,8 @@ SANDBOX_URL=http://localhost:3001 PRO_URL= # 页面的地址,用于自动补全相对路径资源的 domain FE_DOMAIN=http://localhost:3000 +# 二级路由,需要打包时候就确定 +# NEXT_PUBLIC_BASE_URL=/fastai # 日志等级: debug, info, warn, error LOG_LEVEL=debug @@ -41,4 +46,12 @@ STORE_LOG_LEVEL=warn # 工作流最大运行次数,避免极端的死循环情况 WORKFLOW_MAX_RUN_TIMES=500 # 循环最大运行次数,避免极端的死循环情况 -WORKFLOW_MAX_LOOP_TIMES=50 \ No newline at end of file +WORKFLOW_MAX_LOOP_TIMES=50 + +# 对话日志推送服务 +# # 日志服务地址 +# CHAT_LOG_URL=http://localhost:8080 +# # 日志推送间隔 +# CHAT_LOG_INTERVAL=10000 +# # 日志来源ID前缀 +# CHAT_LOG_SOURCE_ID_PREFIX=fastgpt- diff --git a/projects/app/Dockerfile b/projects/app/Dockerfile index 3970f3795..1417f8ae4 100644 --- a/projects/app/Dockerfile +++ b/projects/app/Dockerfile @@ -6,8 +6,6 @@ ARG proxy RUN [ -z "$proxy" ] || sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories RUN apk add --no-cache libc6-compat && npm install -g pnpm@9.4.0 -# if proxy exists, set proxy -RUN [ -z "$proxy" ] || pnpm config set registry https://registry.npmmirror.com # copy packages and one project COPY pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./ @@ -16,13 +14,19 @@ COPY ./projects/app/package.json ./projects/app/package.json RUN [ -f pnpm-lock.yaml ] || (echo "Lockfile not found." && exit 1) -RUN pnpm i +# if proxy exists, set proxy +RUN if [ -z "$proxy" ]; then \ + pnpm i; \ + else \ + pnpm i --registry=https://registry.npmmirror.com; \ + fi # --------- builder ----------- FROM node:20.14.0-alpine AS builder WORKDIR /app ARG proxy +ARG base_url # copy common node_modules and one project node_modules COPY package.json pnpm-workspace.yaml .npmrc tsconfig.json ./ @@ -36,6 +40,7 @@ RUN [ -z "$proxy" ] || sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' / RUN apk add --no-cache libc6-compat && npm install -g pnpm@9.4.0 ENV NODE_OPTIONS="--max-old-space-size=4096" +ENV NEXT_PUBLIC_BASE_URL=$base_url RUN pnpm --filter=app build # --------- runner ----------- @@ -43,6 +48,7 @@ FROM node:20.14.0-alpine AS runner WORKDIR /app ARG proxy +ARG base_url # create user and use it RUN addgroup --system --gid 1001 nodejs @@ -78,6 +84,7 @@ RUN chown -R nextjs:nodejs /app/data ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED=1 ENV PORT=3000 +ENV NEXT_PUBLIC_BASE_URL=$base_url EXPOSE 3000 diff --git a/projects/app/next.config.js b/projects/app/next.config.js index 1e455e044..73a53cd4e 100644 --- a/projects/app/next.config.js +++ b/projects/app/next.config.js @@ -6,6 +6,7 @@ const isDev = process.env.NODE_ENV === 'development'; /** @type {import('next').NextConfig} */ const nextConfig = { + basePath: process.env.NEXT_PUBLIC_BASE_URL, i18n, output: 'standalone', reactStrictMode: isDev ? false : true, diff --git a/projects/app/package.json b/projects/app/package.json index 51b42cf00..344763d75 100644 --- a/projects/app/package.json +++ b/projects/app/package.json @@ -1,6 +1,6 @@ { "name": "app", - "version": "4.8.12", + "version": "4.8.13", "private": false, "scripts": { "dev": "next dev", diff --git a/projects/app/public/icon/logo.svg b/projects/app/public/icon/logo.svg index ec5a08663..8141202cd 100644 --- a/projects/app/public/icon/logo.svg +++ b/projects/app/public/icon/logo.svg @@ -1,12 +1,37 @@ - - - + + + + + + + + - + - + + + + + + + + + + + + + + + + + + + + + diff --git a/projects/app/public/icon/resizer.svg b/projects/app/public/icon/resizer.svg new file mode 100644 index 000000000..260d9451b --- /dev/null +++ b/projects/app/public/icon/resizer.svg @@ -0,0 +1,4 @@ + + + + diff --git a/projects/app/public/imgs/app/visionModel.png b/projects/app/public/imgs/app/visionModel.png deleted file mode 100644 index 16cc046aa..000000000 Binary files a/projects/app/public/imgs/app/visionModel.png and /dev/null differ diff --git a/projects/app/public/imgs/app/visionModel.svg b/projects/app/public/imgs/app/visionModel.svg new file mode 100644 index 000000000..0f6ba2ed9 --- /dev/null +++ b/projects/app/public/imgs/app/visionModel.svg @@ -0,0 +1,207 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/projects/app/src/components/Markdown/codeBlock/Iframe.tsx b/projects/app/src/components/Markdown/codeBlock/Iframe.tsx new file mode 100644 index 000000000..9a09f5dff --- /dev/null +++ b/projects/app/src/components/Markdown/codeBlock/Iframe.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { Box } from '@chakra-ui/react'; +import { useMarkdownWidth } from '../hooks'; + +const IframeBlock = ({ code }: { code: string }) => { + const { width, Ref } = useMarkdownWidth(); + return ( + + + + ); +}; + +export default IframeBlock; diff --git a/projects/app/src/components/Markdown/codeBlock/iframe-html.tsx b/projects/app/src/components/Markdown/codeBlock/iframe-html.tsx new file mode 100644 index 000000000..0674335fb --- /dev/null +++ b/projects/app/src/components/Markdown/codeBlock/iframe-html.tsx @@ -0,0 +1,25 @@ +import React, { useEffect, useRef, useCallback, useState } from 'react'; +import { Box } from '@chakra-ui/react'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import { useMarkdownWidth } from '../hooks'; + +const MermaidBlock = ({ code }: { code: string }) => { + const { width, Ref } = useMarkdownWidth(); + return ( + + + + ); +}; + +export default MermaidBlock; diff --git a/projects/app/src/components/Markdown/hooks.ts b/projects/app/src/components/Markdown/hooks.ts new file mode 100644 index 000000000..754e1971f --- /dev/null +++ b/projects/app/src/components/Markdown/hooks.ts @@ -0,0 +1,34 @@ +import { useScreen } from '@fastgpt/web/hooks/useScreen'; +import { useSystem } from '@fastgpt/web/hooks/useSystem'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +export const useMarkdownWidth = () => { + const Ref = useRef(null); + const [width, setWidth] = useState(400); + const { screenWidth } = useScreen(); + const { isPc } = useSystem(); + + const findMarkdownDom = useCallback(() => { + if (!Ref.current) return; + + // 一直找到 parent = markdown 的元素 + let parent = Ref.current?.parentElement; + while (parent && !parent.className.includes('chat-box-card')) { + parent = parent.parentElement; + } + + const ChatItemDom = parent?.parentElement; + const clientWidth = ChatItemDom?.clientWidth ? ChatItemDom.clientWidth - (isPc ? 90 : 60) : 500; + setWidth(clientWidth); + return parent?.parentElement; + }, [isPc]); + + useEffect(() => { + findMarkdownDom(); + }, [findMarkdownDom, screenWidth, Ref.current]); + + return { + Ref, + width + }; +}; diff --git a/projects/app/src/components/Markdown/index.tsx b/projects/app/src/components/Markdown/index.tsx index 9d0c9b7cf..703ebbf90 100644 --- a/projects/app/src/components/Markdown/index.tsx +++ b/projects/app/src/components/Markdown/index.tsx @@ -22,6 +22,7 @@ const CodeLight = dynamic(() => import('./CodeLight'), { ssr: false }); const MermaidCodeBlock = dynamic(() => import('./img/MermaidCodeBlock'), { ssr: false }); const MdImage = dynamic(() => import('./img/Image'), { ssr: false }); const EChartsCodeBlock = dynamic(() => import('./img/EChartsCodeBlock'), { ssr: false }); +const IframeCodeBlock = dynamic(() => import('./codeBlock/Iframe'), { ssr: false }); const ChatGuide = dynamic(() => import('./chat/Guide'), { ssr: false }); const QuestionGuide = dynamic(() => import('./chat/QuestionGuide'), { ssr: false }); @@ -29,11 +30,13 @@ const QuestionGuide = dynamic(() => import('./chat/QuestionGuide'), { ssr: false const Markdown = ({ source = '', showAnimation = false, - isDisabled = false + isDisabled = false, + forbidZhFormat = false }: { source?: string; showAnimation?: boolean; isDisabled?: boolean; + forbidZhFormat?: boolean; }) => { const components = useMemo( () => ({ @@ -46,15 +49,35 @@ const Markdown = ({ ); const formatSource = useMemo(() => { - const formatSource = source + if (showAnimation || forbidZhFormat) return source; + + // 保护 URL 格式:https://, http://, /api/xxx + const urlPlaceholders: string[] = []; + const textWithProtectedUrls = source.replace( + /(https?:\/\/[^\s<]+[^<.,:;"')\]\s]|\/api\/[^\s]+)(?=\s|$)/g, + (match) => { + urlPlaceholders.push(match); + return `__URL_${urlPlaceholders.length - 1}__`; + } + ); + + // 处理中文与英文数字之间的分词 + const textWithSpaces = textWithProtectedUrls .replace( /([\u4e00-\u9fa5\u3000-\u303f])([a-zA-Z0-9])|([a-zA-Z0-9])([\u4e00-\u9fa5\u3000-\u303f])/g, '$1$3 $2$4' - ) // Chinese and english chars separated by space + ) + // 处理引用标记 .replace(/\n*(\[QUOTE SIGN\]\(.*\))/g, '$1'); - return formatSource; - }, [source]); + // 还原 URL + const finalText = textWithSpaces.replace( + /__URL_(\d+)__/g, + (_, index) => urlPlaceholders[parseInt(index)] + ); + + return finalText; + }, [forbidZhFormat, showAnimation, source]); const urlTransform = useCallback((val: string) => { return val; @@ -101,6 +124,9 @@ function Code(e: any) { if (codeType === CodeClassNameEnum.echarts) { return ; } + if (codeType === CodeClassNameEnum.iframe) { + return ; + } return ( diff --git a/projects/app/src/components/Markdown/utils.ts b/projects/app/src/components/Markdown/utils.ts index b0b907542..50d0b5c40 100644 --- a/projects/app/src/components/Markdown/utils.ts +++ b/projects/app/src/components/Markdown/utils.ts @@ -5,7 +5,8 @@ export enum CodeClassNameEnum { echarts = 'echarts', quote = 'quote', files = 'files', - latex = 'latex' + latex = 'latex', + iframe = 'iframe' } function htmlTableToLatex(html: string) { diff --git a/projects/app/src/components/MyImage/index.tsx b/projects/app/src/components/MyImage/index.tsx index c595a888d..46d49c99d 100644 --- a/projects/app/src/components/MyImage/index.tsx +++ b/projects/app/src/components/MyImage/index.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react'; -import { Image, Skeleton, ImageProps } from '@chakra-ui/react'; +import { Skeleton, ImageProps } from '@chakra-ui/react'; +import CustomImage from '@fastgpt/web/components/common/Image/MyImage'; export const MyImage = (props: ImageProps) => { const [isLoading, setIsLoading] = useState(true); @@ -13,7 +14,7 @@ export const MyImage = (props: ImageProps) => { justifyContent={'center'} my={1} > - } - rightIcon={} - pl={4} + rightIcon={} + px={3} + pr={2} onClick={onOpenAIChatSetting} > diff --git a/projects/app/src/components/core/app/FileSelect.tsx b/projects/app/src/components/core/app/FileSelect.tsx index a4b8ac1d8..7f7361d9c 100644 --- a/projects/app/src/components/core/app/FileSelect.tsx +++ b/projects/app/src/components/core/app/FileSelect.tsx @@ -59,7 +59,9 @@ const FileSelect = ({ return ( - {t('app:file_upload')} + + {t('app:file_upload')} + @@ -68,6 +70,7 @@ const FileSelect = ({ iconSpacing={1} size={'sm'} mr={'-5px'} + color={'myGray.600'} onClick={onOpen} > {formLabel} diff --git a/projects/app/src/components/core/app/InputGuideConfig.tsx b/projects/app/src/components/core/app/InputGuideConfig.tsx index 8d9b7347d..f9ee1b1d3 100644 --- a/projects/app/src/components/core/app/InputGuideConfig.tsx +++ b/projects/app/src/components/core/app/InputGuideConfig.tsx @@ -87,7 +87,7 @@ const InputGuideConfig = ({ - {chatT('input_guide')} + {chatT('input_guide')} @@ -97,6 +97,7 @@ const InputGuideConfig = ({ iconSpacing={1} size={'sm'} mr={'-5px'} + color={'myGray.600'} onClick={onOpen} > {formLabel} @@ -145,7 +146,7 @@ const InputGuideConfig = ({ {chatT('custom_input_guide_url')} window.open(getDocPath('/docs/course/chat_input_guide'))} + onClick={() => window.open(getDocPath('/docs/guide/course/chat_input_guide/'))} color={'primary.700'} alignItems={'center'} cursor={'pointer'} diff --git a/projects/app/src/components/core/app/QGSwitch.tsx b/projects/app/src/components/core/app/QGSwitch.tsx index ef2d1a71e..a54688010 100644 --- a/projects/app/src/components/core/app/QGSwitch.tsx +++ b/projects/app/src/components/core/app/QGSwitch.tsx @@ -11,7 +11,7 @@ const QGSwitch = (props: SwitchProps) => { return ( - {t('common:core.app.Question Guide')} + {t('common:core.app.Question Guide')} diff --git a/projects/app/src/components/core/app/ScheduledTriggerConfig.tsx b/projects/app/src/components/core/app/ScheduledTriggerConfig.tsx index ddef626ec..8072e7235 100644 --- a/projects/app/src/components/core/app/ScheduledTriggerConfig.tsx +++ b/projects/app/src/components/core/app/ScheduledTriggerConfig.tsx @@ -270,7 +270,7 @@ const ScheduledTriggerConfig = ({ - {t('common:core.app.Interval timer run')} + {t('common:core.app.Interval timer run')} @@ -279,6 +279,7 @@ const ScheduledTriggerConfig = ({ iconSpacing={1} size={'sm'} mr={'-5px'} + color={'myGray.600'} onClick={onOpen} > {formatLabel} diff --git a/projects/app/src/components/core/app/TTSSelect.tsx b/projects/app/src/components/core/app/TTSSelect.tsx index 7cb506c65..677e154fe 100644 --- a/projects/app/src/components/core/app/TTSSelect.tsx +++ b/projects/app/src/components/core/app/TTSSelect.tsx @@ -13,6 +13,7 @@ import MySelect from '@fastgpt/web/components/common/MySelect'; import { defaultTTSConfig } from '@fastgpt/global/core/app/constants'; import ChatFunctionTip from './Tip'; import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; +import MyImage from '@fastgpt/web/components/common/Image/MyImage'; const TTSSelect = ({ value = defaultTTSConfig, @@ -82,7 +83,7 @@ const TTSSelect = ({ return ( - {t('common:core.app.TTS')} + {t('common:core.app.TTS')} @@ -92,6 +93,7 @@ const TTSSelect = ({ size={'sm'} mr={'-5px'} onClick={onOpen} + color={'myGray.600'} > {formLabel} @@ -132,7 +134,7 @@ const TTSSelect = ({ {audioPlaying ? ( - + - +
+ + + - - - + px={4} + fontWeight={'medium'} + > + {t('common:common.Operation')} + {formatVariables.map((item) => ( - - - - + ))} @@ -337,11 +359,7 @@ const VariableEdit = ({ type={'variable'} isEdit={!!value.key} inputType={type} - valueType={valueType} - defaultValue={defaultValue} defaultValueType={defaultValueType} - max={max} - min={min} onClose={() => reset({})} onSubmitSuccess={onSubmitSuccess} onSubmitError={onSubmitError} diff --git a/projects/app/src/components/core/app/WelcomeTextConfig.tsx b/projects/app/src/components/core/app/WelcomeTextConfig.tsx index 1859948c3..67a3139ff 100644 --- a/projects/app/src/components/core/app/WelcomeTextConfig.tsx +++ b/projects/app/src/components/core/app/WelcomeTextConfig.tsx @@ -13,17 +13,20 @@ const WelcomeTextConfig = (props: TextareaProps) => { <> - {t('common:core.app.Welcome Text')} + + {t('common:core.app.Welcome Text')} + - {t('common:core.app.Whisper')} + {t('common:core.app.Whisper')}
+ {t('workflow:Variable_name')} + + {t('common:common.Require Input')} + - {t('workflow:Variable_name')}{t('app:global_variables_desc')}{t('common:common.Require Input')}
- - {item.key} - {item.description || '-'} + + + {item.key} + {item.required ? '✔' : '-'} - { - const formattedItem = { - ...item, - list: item.enums || [] - }; - reset(formattedItem); - }} - /> - - onChange(variables.filter((variable) => variable.id !== item.id)) - } - /> + + + {item.required ? ( + + ) : ( + '' + )} + + + + { + const formattedItem = { + ...item, + list: item.enums || [] + }; + reset(formattedItem); + }} + /> + + onChange(variables.filter((variable) => variable.id !== item.id)) + } + /> +