Compare commits
135 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcf9491999 | ||
|
|
d0041a98b4 | ||
|
|
29d152784f | ||
|
|
cd7214ba8d | ||
|
|
6a84e73a82 | ||
|
|
98ce5103a0 | ||
|
|
c65a36d3ab | ||
|
|
b6e49da288 | ||
|
|
45998f9cf5 | ||
|
|
4197f63751 | ||
|
|
ace8134a16 | ||
|
|
7f1fecb84e | ||
|
|
bf172fab81 | ||
|
|
36f5648cae | ||
|
|
ab57bfcc4a | ||
|
|
11848b8f44 | ||
|
|
a11e0bd9c3 | ||
|
|
f6552d0d4f | ||
|
|
38d4db5d5f | ||
|
|
63cd379682 | ||
|
|
9136c9306a | ||
|
|
c9db9f33ea | ||
|
|
3d7178d06f | ||
|
|
a4ff5a3f73 | ||
|
|
814c5b3d3c | ||
|
|
e7e0677291 | ||
|
|
823f4b7ad1 | ||
|
|
a3c77480f7 | ||
|
|
e367265dbb | ||
|
|
7e0deb29e0 | ||
|
|
0d94db4331 | ||
|
|
177482b33a | ||
|
|
63b183a9fe | ||
|
|
858117f8c0 | ||
|
|
ac4355d2e1 | ||
|
|
ce7da2db66 | ||
|
|
0a4a1def1e | ||
|
|
35f4deca76 | ||
|
|
ba1451a0e9 | ||
|
|
40d69e6e20 | ||
|
|
b8ba947ba8 | ||
|
|
06be57815e | ||
|
|
81e37a5736 | ||
|
|
b8ea546b3f | ||
|
|
0bb31b985d | ||
|
|
453824260f | ||
|
|
a8fdffc3e9 | ||
|
|
24164d9454 | ||
|
|
4365a94ea9 | ||
|
|
7c1ec04380 | ||
|
|
09b6365321 | ||
|
|
eb2e383cc7 | ||
|
|
ae4c479f37 | ||
|
|
6a996272da | ||
|
|
1bf76ebe7a | ||
|
|
a19afca148 | ||
|
|
be3b680bc6 | ||
|
|
31dbcfde9f | ||
|
|
6d438aafdf | ||
|
|
1aaafcf631 | ||
|
|
7521bce77e | ||
|
|
c8dee29dc4 | ||
|
|
8f953d1fc4 | ||
|
|
970b62be25 | ||
|
|
b2b3aa651d | ||
|
|
b0e7d25464 | ||
|
|
b46048609c | ||
|
|
ae2887e956 | ||
|
|
7917766024 | ||
|
|
a1a63260dd | ||
|
|
6f2d556a87 | ||
|
|
565f9c8113 | ||
|
|
975e011e03 | ||
|
|
19ce6f66ca | ||
|
|
da6e26f95c | ||
|
|
71abe08f05 | ||
|
|
45ba5e1e01 | ||
|
|
139d0be52b | ||
|
|
1ba3d72a8a | ||
|
|
cd455b2a79 | ||
|
|
fa3f3e6264 | ||
|
|
9bf5a3ec76 | ||
|
|
95389e31f7 | ||
|
|
ea65d9b34b | ||
|
|
2dd2976efa | ||
|
|
64fde42c87 | ||
|
|
7a926b7086 | ||
|
|
562fd2692d | ||
|
|
935287a95a | ||
|
|
bd419a22f4 | ||
|
|
32f482b232 | ||
|
|
5d596bd3d5 | ||
|
|
ae88d79d6f | ||
|
|
1207e3e566 | ||
|
|
3449024678 | ||
|
|
8dba2c39e1 | ||
|
|
94c53804ce | ||
|
|
a1bcd798e1 | ||
|
|
6d51b3babe | ||
|
|
a5fe671ffe | ||
|
|
44e772f0fd | ||
|
|
a3c6d6800b | ||
|
|
19b1ff5a8d | ||
|
|
8d55587cf4 | ||
|
|
a754ceaf3b | ||
|
|
086ea83fac | ||
|
|
1ace8fb9a3 | ||
|
|
e0b23a26f2 | ||
|
|
7c16d08ec0 | ||
|
|
5157e62fed | ||
|
|
1fe2c49204 | ||
|
|
b9b50a0f5a | ||
|
|
23cc2f81e9 | ||
|
|
68cdf50cb6 | ||
|
|
2ae8d43216 | ||
|
|
0ea464f30f | ||
|
|
7cb035ba24 | ||
|
|
7231a847f7 | ||
|
|
4f0f950dd4 | ||
|
|
c1f4785392 | ||
|
|
b22c878cf9 | ||
|
|
3420f677b6 | ||
|
|
baee8cfe82 | ||
|
|
0b0570fa54 | ||
|
|
299409aa7b | ||
|
|
5284312eb3 | ||
|
|
86a0e7ce23 | ||
|
|
e0de04dddb | ||
|
|
19d7edb585 | ||
|
|
fbb75c97d0 | ||
|
|
7e9cac3478 | ||
|
|
be937956af | ||
|
|
c5c3826714 | ||
|
|
42fec3a95c | ||
|
|
64b9367ca1 |
19
.github/ISSUE_TEMPLATE/bugs.md
vendored
@@ -4,21 +4,22 @@ about: 详细清晰的描述你遇到的问题
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**例行检查**
|
||||
|
||||
[//]: # (方框内删除已有的空格,填 x 号)
|
||||
+ [ ] 我已确认目前没有类似 issue
|
||||
+ [ ] 我已完整查看过项目 README,以及[项目文档](https://doc.fastgpt.run/docs/intro/)
|
||||
+ [ ] 我使用了自己的key,并确认我的 key 是可正常使用的
|
||||
+ [ ] 我理解并愿意跟进此 issue,协助测试和提供反馈
|
||||
+ [ ] 我理解并认可上述内容,并理解项目维护者精力有限,**不遵循规则的 issue 可能会被无视或直接关闭**
|
||||
[//]: # '方框内填 x 表示打钩'
|
||||
|
||||
- [ ] 我已确认目前没有类似 issue
|
||||
- [ ] 我已完整查看过项目 README,以及[项目文档](https://doc.fastgpt.run/docs/intro/)
|
||||
- [ ] 我使用了自己的 key,并确认我的 key 是可正常使用的
|
||||
- [ ] 我理解并愿意跟进此 issue,协助测试和提供反馈
|
||||
- [x] 我理解并认可上述内容,并理解项目维护者精力有限,**不遵循规则的 issue 可能会被无视或直接关闭**
|
||||
|
||||
**你的版本**
|
||||
+ [ ] 公有云版本
|
||||
+ [ ] 私有部署版本
|
||||
|
||||
- [ ] 公有云版本
|
||||
- [ ] 私有部署版本
|
||||
|
||||
**问题描述**
|
||||
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/features.md
vendored
@@ -8,13 +8,13 @@ assignees: ''
|
||||
|
||||
**例行检查**
|
||||
|
||||
[//]: # '方框内删除已有的空格,填 x 号'
|
||||
[//]: # '方框内填 x 表示打钩'
|
||||
|
||||
- [ ] 我已确认目前没有类似 features
|
||||
- [ ] 我已确认我已升级到最新版本
|
||||
- [ ] 我已完整查看过项目 README,已确定现有版本无法满足需求
|
||||
- [ ] 我理解并愿意跟进此 features,协助测试和提供反馈
|
||||
- [ ] 我理解并认可上述内容,并理解项目维护者精力有限,**不遵循规则的 features 可能会被无视或直接关闭**
|
||||
- [x] 我理解并认可上述内容,并理解项目维护者精力有限,**不遵循规则的 features 可能会被无视或直接关闭**
|
||||
|
||||
**功能描述**
|
||||
|
||||
|
||||
30
.github/gh-bot.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
version: v1
|
||||
debug: true
|
||||
action:
|
||||
printConfig: false
|
||||
release:
|
||||
retry: 15s
|
||||
actionName: Release
|
||||
allowOps:
|
||||
- cuisongliu
|
||||
bot:
|
||||
prefix: /
|
||||
spe: _
|
||||
allowOps:
|
||||
- sealos-ci-robot
|
||||
- sealos-release-robot
|
||||
email: sealos-ci-robot@sealos.io
|
||||
username: sealos-ci-robot
|
||||
repo:
|
||||
org: false
|
||||
|
||||
message:
|
||||
success: |
|
||||
🤖 says: Hooray! The action {{.Body}} has been completed successfully. 🎉
|
||||
format_error: |
|
||||
🤖 says: ‼️ There is a formatting issue with the action, kindly verify the action's format.
|
||||
permission_error: |
|
||||
🤖 says: ‼️ The action doesn't have permission to trigger.
|
||||
release_error: |
|
||||
🤖 says: ‼️ Release action failed.
|
||||
Error details: {{.Error}}
|
||||
15
.github/imgs/logo-left.svg
vendored
Normal file
|
After Width: | Height: | Size: 10 KiB |
19
.github/workflows/bot-issues-translator.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: 'Github Rebot for issues-translator'
|
||||
on:
|
||||
issues:
|
||||
types: [ opened ]
|
||||
issue_comment:
|
||||
types: [ created ]
|
||||
jobs:
|
||||
translate:
|
||||
permissions:
|
||||
issues: write
|
||||
discussions: write
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: usthe/issues-translate-action@v2.7
|
||||
with:
|
||||
IS_MODIFY_TITLE: true
|
||||
BOT_GITHUB_TOKEN: ${{ secrets.GH_PAT }}
|
||||
CUSTOM_BOT_NOTE: Bot detected the issue body's language is not English, translate it automatically. 👯👭🏻🧑🤝🧑👫🧑🏿🤝🧑🏻👩🏾🤝👨🏿👬🏿
|
||||
3
.github/workflows/deploy-docs.yml
vendored
@@ -55,8 +55,6 @@ jobs:
|
||||
# Step 4 - Builds the site using Hugo
|
||||
- name: Build
|
||||
run: cd docSite && hugo mod get -u github.com/colinwilson/lotusdocs && hugo -v --minify
|
||||
env:
|
||||
HUGO_BASEURL: ${{ vars.BASE_URL }}
|
||||
|
||||
# Step 5 - Push our generated site to Vercel
|
||||
- name: Deploy to Vercel
|
||||
@@ -69,3 +67,4 @@ jobs:
|
||||
github-comment: false
|
||||
vercel-args: '--prod --local-config ../vercel.json' # Optional
|
||||
working-directory: docSite/public
|
||||
|
||||
|
||||
94
.github/workflows/deploy-preview.yml
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
name: deploy-docs-preview
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
paths:
|
||||
- 'docSite/**'
|
||||
branches:
|
||||
- 'main'
|
||||
workflow_dispatch:
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
# This workflow contains jobs "deploy-production"
|
||||
deploy-preview:
|
||||
# The environment this job references
|
||||
environment:
|
||||
name: Preview
|
||||
url: ${{ steps.vercel-action.outputs.preview-url }}
|
||||
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
# Job outputs
|
||||
outputs:
|
||||
url: ${{ steps.vercel-action.outputs.preview-url }}
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
# Step 1 - Checks-out your repository under $GITHUB_WORKSPACE
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
submodules: recursive # Fetch submodules
|
||||
fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod
|
||||
|
||||
# Step 2 Detect changes to Docs Content
|
||||
- name: Detect changes in doc content
|
||||
uses: dorny/paths-filter@v2
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
docs:
|
||||
- 'docSite/content/docs/**'
|
||||
base: main
|
||||
|
||||
# Step 3 - Install Hugo (specific version)
|
||||
- name: Install Hugo
|
||||
uses: peaceiris/actions-hugo@v2
|
||||
with:
|
||||
hugo-version: '0.117.0'
|
||||
extended: true
|
||||
|
||||
# Step 4 - Builds the site using Hugo
|
||||
- name: Build
|
||||
run: cd docSite && hugo mod get -u github.com/colinwilson/lotusdocs && hugo -v --minify
|
||||
|
||||
# Step 5 - Push our generated site to Vercel
|
||||
- name: Deploy to Vercel
|
||||
uses: amondnet/vercel-action@v25
|
||||
id: vercel-action
|
||||
with:
|
||||
vercel-token: ${{ secrets.VERCEL_TOKEN }} # Required
|
||||
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} #Required
|
||||
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }} #Required
|
||||
github-comment: false
|
||||
vercel-args: '--local-config ../vercel.json' # Optional
|
||||
working-directory: docSite/public
|
||||
alias-domains: | #Optional
|
||||
fastgpt-staging.vercel.app
|
||||
docsOutput:
|
||||
needs: [ deploy-preview ]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
- name: Write md
|
||||
run: |
|
||||
echo "# 🤖 Generated by deploy action" > report.md
|
||||
echo "[👀 Visit Preview](${{ needs.deploy-preview.outputs.url }})" >> report.md
|
||||
cat report.md
|
||||
- name: Gh Rebot for Sealos
|
||||
uses: labring/gh-rebot@v0.0.6
|
||||
if: ${{ (github.event_name == 'pull_request_target') }}
|
||||
with:
|
||||
version: v0.0.6
|
||||
env:
|
||||
GH_TOKEN: "${{ secrets.GH_PAT }}"
|
||||
SEALOS_TYPE: "pr_comment"
|
||||
SEALOS_FILENAME: "report.md"
|
||||
SEALOS_REPLACE_TAG: "DEFAULT_REPLACE_DEPLOY"
|
||||
98
.github/workflows/docs-image.yml
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
name: Build FastGPT docs images and copy image to docker hub
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- 'docSite/**'
|
||||
branches:
|
||||
- 'main'
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
jobs:
|
||||
build-fastgpt-docs-images:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- 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-
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GH_PAT }}
|
||||
- name: Set DOCKER_REPO_TAGGED based on branch or tag
|
||||
run: |
|
||||
if [[ "${{ github.ref_name }}" == "main" ]]; then
|
||||
echo "DOCKER_REPO_TAGGED=ghcr.io/${{ github.repository_owner }}/fastgpt-docs:latest" >> $GITHUB_ENV
|
||||
else
|
||||
echo "DOCKER_REPO_TAGGED=ghcr.io/${{ github.repository_owner }}/fastgpt-docs:${{ github.ref_name }}" >> $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 \
|
||||
--build-arg name=app \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--label "org.opencontainers.image.source= https://github.com/ ${{ github.repository_owner }}/FastGPT" \
|
||||
--label "org.opencontainers.image.description=fastgpt image" \
|
||||
--label "org.opencontainers.image.licenses=Apache" \
|
||||
--push \
|
||||
--cache-from=type=local,src=/tmp/.buildx-cache \
|
||||
--cache-to=type=local,dest=/tmp/.buildx-cache \
|
||||
-t ${DOCKER_REPO_TAGGED} \
|
||||
-f docSite/Dockerfile \
|
||||
.
|
||||
push-to-docker-hub:
|
||||
needs: build-fastgpt-docs-images
|
||||
runs-on: ubuntu-20.04
|
||||
if: github.repository == 'labring/FastGPT'
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_NAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
- name: Set DOCKER_REPO_TAGGED based on branch or tag
|
||||
run: |
|
||||
if [[ "${{ github.ref_name }}" == "main" ]]; then
|
||||
echo "IMAGE_TAG=latest" >> $GITHUB_ENV
|
||||
else
|
||||
echo "IMAGE_TAG=${{ github.ref_name }}" >> $GITHUB_ENV
|
||||
fi
|
||||
- name: Pull image from GitHub Container Registry
|
||||
run: docker pull ghcr.io/${{ github.repository_owner }}/fastgpt-docs:${{env.IMAGE_TAG}}
|
||||
- name: Tag image with Docker Hub repository name and version tag
|
||||
run: docker tag ghcr.io/${{ github.repository_owner }}/fastgpt-docs:${{env.IMAGE_TAG}} ${{ secrets.DOCKER_IMAGE_NAME }}:${{env.IMAGE_TAG}}
|
||||
- name: Push image to Docker Hub
|
||||
run: docker push ${{ secrets.DOCKER_IMAGE_NAME }}:${{env.IMAGE_TAG}}
|
||||
|
||||
update-docs-image:
|
||||
needs: build-fastgpt-docs-images
|
||||
runs-on: ubuntu-20.04
|
||||
if: github.repository == 'labring/FastGPT'
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions-hub/kubectl@master
|
||||
env:
|
||||
KUBE_CONFIG: ${{ secrets.KUBE_CONFIG }}
|
||||
with:
|
||||
args: rollout restart deployment fastgpt-docs
|
||||
21
.github/workflows/fastgpt-image.yml
vendored
@@ -1,9 +1,10 @@
|
||||
name: Build fastgpt images and copy image to docker hub
|
||||
name: Build FastGPT images and copy image to docker hub
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- 'client/**'
|
||||
- 'projects/app/**'
|
||||
- 'packages/**'
|
||||
branches:
|
||||
- 'main'
|
||||
tags:
|
||||
@@ -25,6 +26,13 @@ jobs:
|
||||
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-
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
@@ -38,24 +46,26 @@ jobs:
|
||||
else
|
||||
echo "DOCKER_REPO_TAGGED=ghcr.io/${{ github.repository_owner }}/fastgpt:${{ github.ref_name }}" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Build and publish image for main branch or tag push event
|
||||
env:
|
||||
DOCKER_REPO_TAGGED: ${{ env.DOCKER_REPO_TAGGED }}
|
||||
run: |
|
||||
cd client && \
|
||||
docker buildx build \
|
||||
--build-arg name=app \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--label "org.opencontainers.image.source= https://github.com/ ${{ github.repository_owner }}/FastGPT" \
|
||||
--label "org.opencontainers.image.description=fastgpt image" \
|
||||
--label "org.opencontainers.image.licenses=MIT" \
|
||||
--label "org.opencontainers.image.licenses=Apache" \
|
||||
--push \
|
||||
--cache-from=type=local,src=/tmp/.buildx-cache \
|
||||
--cache-to=type=local,dest=/tmp/.buildx-cache \
|
||||
-t ${DOCKER_REPO_TAGGED} \
|
||||
-f Dockerfile \
|
||||
.
|
||||
push-to-docker-hub:
|
||||
needs: build-fastgpt-images
|
||||
runs-on: ubuntu-20.04
|
||||
if: github.repository == 'labring/FastGPT'
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
@@ -79,6 +89,7 @@ jobs:
|
||||
run: docker push ${{ secrets.DOCKER_IMAGE_NAME }}:${{env.IMAGE_TAG}}
|
||||
push-to-ali-hub:
|
||||
needs: build-fastgpt-images
|
||||
if: github.repository == 'labring/FastGPT'
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
||||
52
.github/workflows/preview-image.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
name: Preview FastGPT images
|
||||
on:
|
||||
pull_request_target:
|
||||
paths:
|
||||
- 'projects/app/**'
|
||||
- 'packages/**'
|
||||
branches:
|
||||
- 'main'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-fastgpt-images:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- 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-
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GH_PAT }}
|
||||
- name: Set DOCKER_REPO_TAGGED based on branch or tag
|
||||
run: |
|
||||
echo "DOCKER_REPO_TAGGED=ghcr.io/${{ github.repository_owner }}/fastgpt-pr:${{ github.event.pull_request.number }}" >> $GITHUB_ENV
|
||||
- name: Build image for PR
|
||||
env:
|
||||
DOCKER_REPO_TAGGED: ${{ env.DOCKER_REPO_TAGGED }}
|
||||
run: |
|
||||
docker buildx build \
|
||||
--build-arg name=app \
|
||||
--label "org.opencontainers.image.source= https://github.com/ ${{ github.repository_owner }}/FastGPT" \
|
||||
--label "org.opencontainers.image.description=fastgpt-pr image" \
|
||||
--label "org.opencontainers.image.licenses=Apache" \
|
||||
--cache-from=type=local,src=/tmp/.buildx-cache \
|
||||
--cache-to=type=local,dest=/tmp/.buildx-cache \
|
||||
-t ${DOCKER_REPO_TAGGED} \
|
||||
-f Dockerfile \
|
||||
.
|
||||
4
.gitignore
vendored
@@ -33,4 +33,6 @@ dist/
|
||||
|
||||
# hugo
|
||||
**/.hugo_build.lock
|
||||
docSite/public/
|
||||
docSite/public/
|
||||
docSite/resources/_gen/
|
||||
docSite/.vercel
|
||||
@@ -1,4 +1,5 @@
|
||||
dist
|
||||
.vscode
|
||||
**/.DS_Store
|
||||
node_modules
|
||||
node_modules
|
||||
docSite/
|
||||
4
.vscode/settings.json
vendored
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.mouseWheelZoom": true,
|
||||
"typescript.tsdk": "client/node_modules/typescript/lib",
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"prettier.prettierPath": "./node_modules/prettier",
|
||||
"i18n-ally.localesPaths": [
|
||||
"client/public/locales"
|
||||
"projects/app/public/locales"
|
||||
],
|
||||
"i18n-ally.enabledParsers": ["json"],
|
||||
"i18n-ally.keystyle": "nested",
|
||||
|
||||
72
Dockerfile
Normal file
@@ -0,0 +1,72 @@
|
||||
# Install dependencies only when needed
|
||||
FROM node:current-alpine AS deps
|
||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||
RUN apk add --no-cache libc6-compat && npm install -g pnpm
|
||||
WORKDIR /app
|
||||
|
||||
ARG name
|
||||
|
||||
# copy packages and one project
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||
COPY ./packages ./packages
|
||||
COPY ./projects/$name/package.json ./projects/$name/package.json
|
||||
COPY ./projects/$name/pnpm-lock.yaml ./projects/$name/pnpm-lock.yaml
|
||||
|
||||
RUN \
|
||||
[ -f pnpm-lock.yaml ] && pnpm install || \
|
||||
(echo "Lockfile not found." && exit 1)
|
||||
|
||||
RUN pnpm prune
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM node:current-alpine AS builder
|
||||
WORKDIR /app
|
||||
|
||||
ARG name
|
||||
|
||||
# copy common node_modules and one project node_modules
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY --from=deps /app/packages ./packages
|
||||
COPY ./projects/$name ./projects/$name
|
||||
COPY --from=deps /app/projects/$name/node_modules ./projects/$name/node_modules
|
||||
COPY pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||
COPY ./packages ./packages
|
||||
|
||||
# Uncomment the following line in case you want to disable telemetry during the build.
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
RUN npm install -g pnpm
|
||||
RUN pnpm --filter=$name run build
|
||||
|
||||
FROM node:current-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ARG name
|
||||
|
||||
# create user and use it
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
RUN sed -i 's/https/http/' /etc/apk/repositories
|
||||
RUN apk add curl \
|
||||
&& apk add ca-certificates \
|
||||
&& update-ca-certificates
|
||||
|
||||
# copy running files
|
||||
COPY --from=builder /app/projects/$name/public ./projects/$name/public
|
||||
COPY --from=builder /app/projects/$name/next.config.js ./projects/$name/next.config.js
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/projects/$name/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/projects/$name/.next/static ./projects/$name/.next/static
|
||||
# copy package.json to version file
|
||||
COPY --from=builder /app/projects/$name/package.json ./package.json
|
||||
|
||||
ENV NODE_ENV production
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
ENV PORT=3000
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
USER nextjs
|
||||
|
||||
ENV serverPath=./projects/$name/server.js
|
||||
|
||||
ENTRYPOINT ["sh","-c","node ${serverPath}"]
|
||||
90
README.md
@@ -4,21 +4,36 @@
|
||||
|
||||
# FastGPT
|
||||
|
||||
<p align="center">
|
||||
<a href="./README_en.md">English</a> |
|
||||
<a href="./README.md">简体中文</a>
|
||||
</p>
|
||||
|
||||
FastGPT 是一个基于 LLM 大语言模型的知识库问答系统,提供开箱即用的数据处理、模型调用等能力。同时可以通过 Flow 可视化进行工作流编排,从而实现复杂的问答场景!
|
||||
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://fastgpt.run/">线上体验</a>
|
||||
·
|
||||
<a href="https://doc.fastgpt.run/docs/intro">相关文档</a>
|
||||
·
|
||||
<a href="https://doc.fastgpt.run/docs/development">本地开发</a>
|
||||
·
|
||||
<a href="https://github.com/labring/FastGPT#-%E7%9B%B8%E5%85%B3%E9%A1%B9%E7%9B%AE">相关项目</a>
|
||||
<a href="https://fastgpt.run/">
|
||||
<img height="21" src="https://img.shields.io/badge/在线使用-fff?style=flat-square&logo=spoj&logoColor=7d09f1" alt="cloud">
|
||||
</a>
|
||||
<a href="https://doc.fastgpt.run/docs/intro">
|
||||
<img height="21" src="https://img.shields.io/badge/相关文档-7d09f1?style=flat-square" alt="document">
|
||||
</a>
|
||||
<a href="https://doc.fastgpt.run/docs/development">
|
||||
<img height="21" src="https://img.shields.io/badge/本地开发-%23fff?style=flat-square&logo=xcode&logoColor=7d09f1" alt="development">
|
||||
</a>
|
||||
<a href="/#-%E7%9B%B8%E5%85%B3%E9%A1%B9%E7%9B%AE">
|
||||
<img height="21" src="https://img.shields.io/badge/相关项目-7d09f1?style=flat-square" alt="project">
|
||||
</a>
|
||||
<a href="https://github.com/labring/FastGPT/blob/main/LICENSE">
|
||||
<img height="21" src="https://img.shields.io/badge/License-Apache--2.0-ffffff?style=flat-square&labelColor=fff&color=7d09f1" alt="license">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## 🛸 在线体验
|
||||
https://github.com/labring/FastGPT/assets/15308462/7d3a38df-eb0e-4388-9250-2409bd33f6d4
|
||||
|
||||
## 🛸 在线使用
|
||||
|
||||
[fastgpt.run](https://fastgpt.run/)(服务器在新加坡,部分地区可能无法直连)
|
||||
|
||||
@@ -31,38 +46,36 @@ FastGPT 是一个基于 LLM 大语言模型的知识库问答系统,提供开
|
||||
|
||||
1. 强大的可视化编排,轻松构建 AI 应用
|
||||
- [x] 提供简易模式,无需操作编排
|
||||
- [x] 用户对话前引导
|
||||
- [x] 全局变量
|
||||
- [x] 用户对话前引导, 全局字符串变量
|
||||
- [x] 知识库搜索
|
||||
- [x] 多 LLM 模型对话
|
||||
- [x] 文本内容提取成结构化数据
|
||||
- [x] HTTP 扩展
|
||||
- [ ] 沙盒 JS 运行模块
|
||||
- [ ] 连续对话引导
|
||||
- [ ] 嵌入 Laf,实现在线编写 HTTP 模块
|
||||
- [x] 对话下一步指引
|
||||
- [ ] 对话多路线选择
|
||||
- [ ] 源文件引用追踪
|
||||
- [x] 源文件引用追踪
|
||||
- [ ] 自定义文件阅读器
|
||||
2. 丰富的知识库预处理
|
||||
- [x] 多库复用,混用
|
||||
- [x] chunk 记录修改和删除
|
||||
- [x] 支持直接分段导入
|
||||
- [x] 支持 QA 拆分导入
|
||||
- [x] 支持手动输入内容
|
||||
- [x] 支持 url 读取导入
|
||||
- [x] 支持 CSV 批量导入问答对
|
||||
- [ ] 支持知识库单独设置向量模型
|
||||
- [ ] 源文件存储
|
||||
- [x] 支持 手动输入, 直接分段, QA 拆分导入
|
||||
- [x] 支持 url 读取、 CSV 批量导入
|
||||
- [x] 支持知识库单独设置向量模型
|
||||
- [x] 源文件存储
|
||||
- [ ] 文件学习 Agent
|
||||
3. 多种效果测试渠道
|
||||
- [x] 知识库单点搜索测试
|
||||
- [x] 对话时反馈引用并可修改与删除
|
||||
- [x] 完整上下文呈现
|
||||
- [ ] 完整模块中间值呈现
|
||||
- [x] 完整模块中间值呈现
|
||||
4. OpenAPI
|
||||
- [x] completions 接口(对齐 GPT 接口)
|
||||
- [ ] 知识库 CRUD
|
||||
5. 运营功能
|
||||
- [x] 免登录分享窗口
|
||||
- [x] Iframe 一键嵌入
|
||||
- [ ] 统一查阅对话记录
|
||||
- [x] 统一查阅对话记录,并对数据进行标注
|
||||
|
||||
## 👨💻 开发
|
||||
|
||||
@@ -76,25 +89,18 @@ FastGPT 是一个基于 LLM 大语言模型的知识库问答系统,提供开
|
||||
|
||||
由于需要部署数据库,部署完后需要等待 2~4 分钟才能正常访问。默认用了最低配置,首次访问时会有些慢。
|
||||
|
||||
* [快开始本地开发](https://doc.fastgpt.run/docs/development)
|
||||
* [快开始本地开发](https://doc.fastgpt.run/docs/development/intro/)
|
||||
* [部署 FastGPT](https://doc.fastgpt.run/docs/installation)
|
||||
* [系统配置文件说明](https://doc.fastgpt.run/docs/installation/reference)
|
||||
* [多模型配置](https://doc.fastgpt.run/docs/installation/reference/models)
|
||||
* [版本升级](https://doc.fastgpt.run/docs/installation/upgrading)
|
||||
* [API 文档](https://kjqvjse66l.feishu.cn/docx/DmLedTWtUoNGX8xui9ocdUEjnNh?pre_pathname=%2Fdrive%2Fhome%2F)
|
||||
* [系统配置文件说明](https://doc.fastgpt.run/docs/development/configuration/)
|
||||
* [多模型配置](https://doc.fastgpt.run/docs/installation/one-api/)
|
||||
* [版本更新/升级介绍](https://doc.fastgpt.run/docs/installation/upgrading)
|
||||
* [API 文档](https://doc.fastgpt.run/docs/development/openapi/)
|
||||
|
||||
## 🏘️ 社区交流群
|
||||
|
||||
| 交流群 | 小助手 |
|
||||
| ----------------------------------------------------- | ---------------------------------------------- |
|
||||
|  |  |
|
||||
添加 wx 小助手加入:
|
||||
|
||||
## 👀 其他
|
||||
|
||||
- [FastGpt 常见问题](https://kjqvjse66l.feishu.cn/docx/HtrgdT0pkonP4kxGx8qcu6XDnGh)
|
||||
- [docker 部署教程视频](https://www.bilibili.com/video/BV1jo4y147fT/)
|
||||
- [公众号接入视频教程](https://www.bilibili.com/video/BV1xh4y1t7fy/)
|
||||
- [FastGpt 知识库演示](https://www.bilibili.com/video/BV1Wo4y1p7i1/)
|
||||

|
||||
|
||||
## 💪 相关项目
|
||||
|
||||
@@ -103,9 +109,15 @@ FastGPT 是一个基于 LLM 大语言模型的知识库问答系统,提供开
|
||||
- [One API: 多模型管理,支持 Azure、文心一言等](https://github.com/songquanpeng/one-api)
|
||||
- [TuShan: 5 分钟搭建后台管理系统](https://github.com/msgbyte/tushan)
|
||||
|
||||
## 👀 其他
|
||||
|
||||
- [保姆级 FastGPT 教程](https://www.bilibili.com/video/BV1n34y1A7Bo/?spm_id_from=333.999.0.0)
|
||||
- [接入飞书](https://www.bilibili.com/video/BV1Su4y1r7R3/?spm_id_from=333.999.0.0)
|
||||
- [接入企微](https://www.bilibili.com/video/BV1Tp4y1n72T/?spm_id_from=333.999.0.0)
|
||||
|
||||
## 🤝 第三方生态
|
||||
|
||||
- [luolinAI: 企微机器人,开箱即用](https://github.com/luolin-ai/FastGPT-Enterprise-WeChatbot)
|
||||
- [OnWeChat 个人微信/企微机器人](https://doc.fastgpt.run/docs/use-cases/onwechat/)
|
||||
|
||||
## 🌟 Star History
|
||||
|
||||
@@ -115,7 +127,7 @@ FastGPT 是一个基于 LLM 大语言模型的知识库问答系统,提供开
|
||||
|
||||
本仓库遵循 [FastGPT Open Source License](./LICENSE) 开源协议。
|
||||
|
||||
1. 允许作为后台服务直接商用,但不允许直接使用 saas 服务商用。
|
||||
1. 允许作为后台服务直接商用,但不允许直接使用 SaaS 服务商用。
|
||||
2. 需保留相关版权信息。
|
||||
3. 完整请查看 [FastGPT Open Source License](./LICENSE)
|
||||
4. 联系方式:yujinlong@sealos.io, [点击查看定价策略](https://fael3z0zfze.feishu.cn/docx/F155dbirfo8vDDx2WgWc6extnwf)
|
||||
4. 联系方式:yujinlong@sealos.io, [点击查看定价策略](https://doc.fastgpt.run/docs/commercial)
|
||||
|
||||
93
README_en.md
@@ -1,25 +1,39 @@
|
||||
<div align="center">
|
||||
|
||||
<a href="https://fastgpt.run/"><img src="/.github/imgs/logo.svg" width="120" height="120" alt="fastgpt logo"></a>
|
||||
|
||||
# FastGPT
|
||||
|
||||
FastGPT is a knowledge-based question answering system based on the LLM language model, providing out-of-the-box capabilities for data processing, model invocation, and more. It also allows for complex question answering scenarios through visual workflow orchestration using Flow!
|
||||
<p align="center">
|
||||
<a href="./README_en.md">English</a> |
|
||||
<a href="./README.md">简体中文</a>
|
||||
</p>
|
||||
|
||||
FastGPT is a knowledge-based Q&A system built on the LLM, offers out-of-the-box data processing and model invocation capabilities, allows for workflow orchestration through Flow visualization!
|
||||
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://fastgpt.run/">Online</a>
|
||||
·
|
||||
<a href="https://doc.fastgpt.run/docs/intro">Document</a>
|
||||
·
|
||||
<a href="https://doc.fastgpt.run/docs/development">Development</a>
|
||||
·
|
||||
<a href="https://doc.fastgpt.run/docs/installation">Deploy</a>
|
||||
·
|
||||
<a href="#powered-by">Power By</a>
|
||||
<a href="https://fastgpt.run/">
|
||||
<img height="21" src="https://img.shields.io/badge/Website-fff?style=flat-square&logo=spoj&logoColor=7d09f1" alt="cloud">
|
||||
</a>
|
||||
<a href="https://doc.fastgpt.run/docs/intro">
|
||||
<img height="21" src="https://img.shields.io/badge/Docs-7d09f1?style=flat-square" alt="document">
|
||||
</a>
|
||||
<a href="https://doc.fastgpt.run/docs/development">
|
||||
<img height="21" src="https://img.shields.io/badge/Development-%23fff?style=flat-square&logo=xcode&logoColor=7d09f1" alt="development">
|
||||
</a>
|
||||
<a href="/#-%E7%9B%B8%E5%85%B3%E9%A1%B9%E7%9B%AE">
|
||||
<img height="21" src="https://img.shields.io/badge/Related Projects-7d09f1?style=flat-square" alt="project">
|
||||
</a>
|
||||
<a href="https://github.com/labring/FastGPT/blob/main/LICENSE">
|
||||
<img height="21" src="https://img.shields.io/badge/License-Apache--2.0-ffffff?style=flat-square&labelColor=fff&color=7d09f1" alt="license">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## 🛸 Online
|
||||
https://github.com/labring/FastGPT/assets/15308462/7d3a38df-eb0e-4388-9250-2409bd33f6d4
|
||||
|
||||
## 🛸 Use Cloud Services
|
||||
|
||||
[fastgpt.run](https://fastgpt.run/)
|
||||
| | |
|
||||
@@ -29,35 +43,34 @@ FastGPT is a knowledge-based question answering system based on the LLM language
|
||||
|
||||
## 💡 Features
|
||||
|
||||
1. Powerful visual orchestration for easy AI application building
|
||||
1. Powerful visual workflows: Effortlessly craft AI applications
|
||||
|
||||
- [x] Provides a simple mode without the need for orchestration operations
|
||||
- [x] Simple mode on deck - no need for manual arrangement
|
||||
- [x] User dialogue pre-guidance
|
||||
- [x] Global variables
|
||||
- [x] Knowledge base search
|
||||
- [x] Multi-LLM model dialogue
|
||||
- [x] Extraction of text content into structured data
|
||||
- [x] HTTP extension
|
||||
- [ ] Sandbox JS runtime module
|
||||
- [ ] Continuous dialogue guidance
|
||||
- [ ] Dialogue multi-path selection
|
||||
- [ ] Source file reference tracking
|
||||
- [x] Dialogue via multiple LLM models
|
||||
- [x] Text magic - convert to structured data
|
||||
- [x] Extend with HTTP
|
||||
- [ ] Embed Laf for on-the-fly HTTP module crafting
|
||||
- [x] Directions for the next dialogue steps
|
||||
- [ ] Multiple dialogue paths selection
|
||||
- [x] Tracking source file references
|
||||
- [ ] Custom file reader
|
||||
|
||||
2. Rich knowledge base preprocessing
|
||||
2. Extensive knowledge base preprocessing
|
||||
|
||||
- [x] Multiple library reuse and mixing
|
||||
- [x] Chunk record modification and deletion
|
||||
- [x] Supports direct segment import
|
||||
- [x] Supports QA split import
|
||||
- [x] Supports manual input content
|
||||
- [ ] Supports URL import reading
|
||||
- [x] Supports batch import of Q&A pairs in CSV format
|
||||
- [ ] Supports separate vector model settings for knowledge bases
|
||||
- [ ] Source file storage
|
||||
- [x] Reuse and mix multiple knowledge bases
|
||||
- [x] Track chunk modifications and deletions
|
||||
- [x] Supports manual entries, direct segmentation, and QA split imports
|
||||
- [x] Supports URL fetching and batch CSV imports
|
||||
- [x] Supports Set unique vector models for knowledge bases
|
||||
- [x] Store original files
|
||||
- [ ] File learning Agent
|
||||
|
||||
3. Multiple effect testing channels
|
||||
|
||||
- [x] Knowledge base single point search testing
|
||||
- [x] Single-point knowledge base search test
|
||||
- [x] Feedback references and ability to modify and delete during dialogue
|
||||
- [x] Complete context presentation
|
||||
- [ ] Complete module intermediate value presentation
|
||||
@@ -77,14 +90,20 @@ FastGPT is a knowledge-based question answering system based on the LLM language
|
||||
|
||||
Project tech stack: NextJs + TS + ChakraUI + Mongo + Postgres (Vector plugin)
|
||||
|
||||
- **⚡ Deployment**
|
||||
|
||||
[](https://cloud.sealos.io/?openapp=system-fastdeploy%3FtemplateName%3Dfastgpt)
|
||||
|
||||
Give it a 2-4 minute wait after deployment as it sets up the database. Initially, it might be a tad slow since we're using the basic settings.
|
||||
|
||||
- [Getting Started with Local Development](https://doc.fastgpt.run/docs/development)
|
||||
- [Deploying FastGPT](https://doc.fastgpt.run/docs/installation)
|
||||
- [System Configuration File Explanation](https://doc.fastgpt.run/docs/installation/reference)
|
||||
- [Multi-model Configuration](https://doc.fastgpt.run/docs/installation/reference/models)
|
||||
- [V3 Upgrade V4 Initialization](https://doc.fastgpt.run/docs/installation/upgrading)
|
||||
- [Guide on System Configs](https://doc.fastgpt.run/docs/installation/reference)
|
||||
- [Configuring Multiple Models](https://doc.fastgpt.run/docs/installation/reference/models)
|
||||
- [Version Updates & Upgrades](https://doc.fastgpt.run/docs/installation/upgrading)
|
||||
|
||||
<!-- ## :point_right: RoadMap
|
||||
- [FastGpt RoadMap](https://kjqvjse66l.feishu.cn/docx/RVUxdqE2WolDYyxEKATcM0XXnte) -->
|
||||
- [FastGPT RoadMap](https://kjqvjse66l.feishu.cn/docx/RVUxdqE2WolDYyxEKATcM0XXnte) -->
|
||||
|
||||
<!-- ## 🏘️ Community
|
||||
|
||||
@@ -94,10 +113,10 @@ Project tech stack: NextJs + TS + ChakraUI + Mongo + Postgres (Vector plugin)
|
||||
|
||||
## 👀 Others
|
||||
|
||||
- [FastGpt FAQ](https://kjqvjse66l.feishu.cn/docx/HtrgdT0pkonP4kxGx8qcu6XDnGh)
|
||||
- [FastGPT FAQ](https://kjqvjse66l.feishu.cn/docx/HtrgdT0pkonP4kxGx8qcu6XDnGh)
|
||||
- [Docker Deployment Tutorial Video](https://www.bilibili.com/video/BV1jo4y147fT/)
|
||||
- [Official Account Integration Video Tutorial](https://www.bilibili.com/video/BV1xh4y1t7fy/)
|
||||
- [FastGpt Knowledge Base Demo](https://www.bilibili.com/video/BV1Wo4y1p7i1/)
|
||||
- [FastGPT Knowledge Base Demo](https://www.bilibili.com/video/BV1Wo4y1p7i1/)
|
||||
|
||||
## 💪 Related Projects
|
||||
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
# Install dependencies only when needed
|
||||
FROM node:current-alpine AS deps
|
||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||
RUN apk add --no-cache libc6-compat && npm install -g pnpm
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies based on the preferred package manager
|
||||
COPY package.json ./
|
||||
COPY pnpm-lock.yaml* ./
|
||||
RUN \
|
||||
[ -f pnpm-lock.yaml ] && pnpm fetch || \
|
||||
(echo "Lockfile not found." && exit 1)
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM node:current-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY pnpm-lock.yaml* ./
|
||||
COPY package.json ./
|
||||
COPY . .
|
||||
|
||||
# Next.js collects completely anonymous telemetry data about general usage.
|
||||
# Learn more here: https://nextjs.org/telemetry
|
||||
# Uncomment the following line in case you want to disable telemetry during the build.
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
RUN npm install -g pnpm
|
||||
RUN \
|
||||
[ -f pnpm-lock.yaml ] && (pnpm --offline install && pnpm run build) || \
|
||||
(echo "Lockfile not found." && exit 1)
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM node:current-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV production
|
||||
# Uncomment the following line in case you want to disable telemetry during runtime.
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
RUN sed -i 's/https/http/' /etc/apk/repositories
|
||||
RUN apk add curl \
|
||||
&& apk add ca-certificates \
|
||||
&& update-ca-certificates
|
||||
|
||||
# You only need to copy next.config.js if you are NOT using the default configuration
|
||||
# COPY --from=builder /app/next.config.js ./
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/package.json ./package.json
|
||||
# COPY --from=builder /app/.env* .
|
||||
|
||||
# Automatically leverage output traces to reduce image size
|
||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
|
||||
ENV PORT=3000
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
5
client/next-env.d.ts
vendored
@@ -1,5 +0,0 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
@@ -1,22 +0,0 @@
|
||||
### 常见问题
|
||||
|
||||
- [**Git 地址**,点击查看项目地址](https://github.com/labring/FastGPT)
|
||||
- [本地部署 FastGPT](https://doc.fastgpt.run/docs/installation)
|
||||
- [API 文档](https://kjqvjse66l.feishu.cn/docx/DmLedTWtUoNGX8xui9ocdUEjnNh?pre_pathname=%2Fdrive%2Fhome%2F)
|
||||
- **反馈问卷**: 如果你遇到任何使用问题或有期望的功能,可以[填写该问卷](https://www.wjx.cn/vm/rLIw1uD.aspx#)
|
||||
- **问题文档**: [先看文档,再提问](https://kjqvjse66l.feishu.cn/docx/HtrgdT0pkonP4kxGx8qcu6XDnGh)
|
||||
- [点击查看商业版文档](https://fael3z0zfze.feishu.cn/docx/F155dbirfo8vDDx2WgWc6extnwf)
|
||||
|
||||
**价格表**
|
||||
| 计费项 | 价格: 元/ 1K tokens(包含上下文)|
|
||||
| --- | --- |
|
||||
| 知识库 - 索引 | 0.002 |
|
||||
| FastAI4k - 对话 | 0.015 |
|
||||
| FastAI16k - 对话 | 0.03 |
|
||||
| FastAI-Plus - 对话 | 0.45 |
|
||||
| 文件拆分 | 0.03 |
|
||||
|
||||
**其他问题**
|
||||
| 交流群 | 小助手 |
|
||||
| ----------------------- | -------------------- |
|
||||
|  |  |
|
||||
@@ -1,7 +0,0 @@
|
||||
### Fast GPT V4.2.2
|
||||
|
||||
1. **新增 - 用户反馈和管理员标注预期答案,以不断提高模型回复准确率。** 该功能为测试版,未来交互可能会有变化,欢迎大家提出宝贵意见。
|
||||
2. 优化 - 知识库搜索提示词,更适配问答场景。
|
||||
3. 新增 - 好友邀请链接,[点击查看](/account?currentTab=promotion)
|
||||
4. 优化 - [使用文档](https://doc.fastgpt.run/docs/intro/)
|
||||
5. [点击查看高级编排介绍文档](https://doc.fastgpt.run/docs/workflow)
|
||||
|
Before Width: | Height: | Size: 11 KiB |
@@ -1,16 +0,0 @@
|
||||
import { GET, POST, DELETE } from './request';
|
||||
import { UserOpenApiKey } from '@/types/openapi';
|
||||
/**
|
||||
* crete a api key
|
||||
*/
|
||||
export const createAOpenApiKey = () => POST<string>('/openapi/postKey');
|
||||
|
||||
/**
|
||||
* get api keys
|
||||
*/
|
||||
export const getOpenApiKeys = () => GET<UserOpenApiKey[]>('/openapi/getKeys');
|
||||
|
||||
/**
|
||||
* delete api by id
|
||||
*/
|
||||
export const delOpenApiById = (id: string) => DELETE(`/openapi/delKey?id=${id}`);
|
||||
@@ -1,98 +0,0 @@
|
||||
import { GET, POST, PUT, DELETE } from '../request';
|
||||
import type { KbItemType, KbListItemType } from '@/types/plugin';
|
||||
import { RequestPaging } from '@/types/index';
|
||||
import { TrainingModeEnum } from '@/constants/plugin';
|
||||
import {
|
||||
Props as PushDataProps,
|
||||
Response as PushDateResponse
|
||||
} from '@/pages/api/openapi/kb/pushData';
|
||||
import {
|
||||
Props as SearchTestProps,
|
||||
Response as SearchTestResponse
|
||||
} from '@/pages/api/openapi/kb/searchTest';
|
||||
import { Response as KbDataItemType } from '@/pages/api/plugins/kb/data/getDataById';
|
||||
import { Props as UpdateDataProps } from '@/pages/api/openapi/kb/updateData';
|
||||
import type { KbUpdateParams, CreateKbParams } from '../request/kb';
|
||||
|
||||
/* knowledge base */
|
||||
export const getKbList = () => GET<KbListItemType[]>(`/plugins/kb/list`);
|
||||
|
||||
export const getKbById = (id: string) => GET<KbItemType>(`/plugins/kb/detail?id=${id}`);
|
||||
|
||||
export const postCreateKb = (data: CreateKbParams) => POST<string>(`/plugins/kb/create`, data);
|
||||
|
||||
export const putKbById = (data: KbUpdateParams) => PUT(`/plugins/kb/update`, data);
|
||||
|
||||
export const delKbById = (id: string) => DELETE(`/plugins/kb/delete?id=${id}`);
|
||||
|
||||
/* kb data */
|
||||
type GetKbDataListProps = RequestPaging & {
|
||||
kbId: string;
|
||||
searchText: string;
|
||||
};
|
||||
export const getKbDataList = (data: GetKbDataListProps) =>
|
||||
POST(`/plugins/kb/data/getDataList`, data);
|
||||
|
||||
/**
|
||||
* 获取导出数据(不分页)
|
||||
*/
|
||||
export const getExportDataList = (kbId: string) =>
|
||||
GET<[string, string, string][]>(
|
||||
`/plugins/kb/data/exportModelData`,
|
||||
{ kbId },
|
||||
{
|
||||
timeout: 600000
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 获取模型正在拆分数据的数量
|
||||
*/
|
||||
export const getTrainingData = (data: { kbId: string; init: boolean }) =>
|
||||
POST<{
|
||||
qaListLen: number;
|
||||
vectorListLen: number;
|
||||
}>(`/plugins/kb/data/getTrainingData`, data);
|
||||
|
||||
/* get length of system training queue */
|
||||
export const getTrainingQueueLen = () => GET<number>(`/plugins/kb/data/getQueueLen`);
|
||||
|
||||
export const getKbDataItemById = (dataId: string) =>
|
||||
GET<KbDataItemType>(`/plugins/kb/data/getDataById`, { dataId });
|
||||
|
||||
/**
|
||||
* 直接push数据
|
||||
*/
|
||||
export const postKbDataFromList = (data: PushDataProps) =>
|
||||
POST<PushDateResponse>(`/openapi/kb/pushData`, data);
|
||||
|
||||
/**
|
||||
* insert one data to dataset
|
||||
*/
|
||||
export const insertData2Kb = (data: {
|
||||
kbId: string;
|
||||
data: { a: string; q: string; source?: string };
|
||||
}) => POST<string>(`/plugins/kb/data/insertData`, data);
|
||||
|
||||
/**
|
||||
* 更新一条数据
|
||||
*/
|
||||
export const putKbDataById = (data: UpdateDataProps) => PUT('/openapi/kb/updateData', data);
|
||||
/**
|
||||
* 删除一条知识库数据
|
||||
*/
|
||||
export const delOneKbDataByDataId = (dataId: string) =>
|
||||
DELETE(`/openapi/kb/delDataById?dataId=${dataId}`);
|
||||
|
||||
/**
|
||||
* 拆分数据
|
||||
*/
|
||||
export const postSplitData = (data: {
|
||||
kbId: string;
|
||||
chunks: string[];
|
||||
prompt: string;
|
||||
mode: `${TrainingModeEnum}`;
|
||||
}) => POST(`/openapi/text/pushData`, data);
|
||||
|
||||
export const searchText = (data: SearchTestProps) =>
|
||||
POST<SearchTestResponse>(`/openapi/kb/searchTest`, data);
|
||||
12
client/src/api/request/kb.d.ts
vendored
@@ -1,12 +0,0 @@
|
||||
export type KbUpdateParams = {
|
||||
id: string;
|
||||
name: string;
|
||||
tags: string;
|
||||
avatar: string;
|
||||
};
|
||||
export type CreateKbParams = {
|
||||
name: string;
|
||||
tags: string[];
|
||||
avatar: string;
|
||||
vectorModel: string;
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
import { GET, POST } from './request';
|
||||
|
||||
export const textCensor = (data: { text: string }) =>
|
||||
POST<{ code?: number; message: string }>('/plugins/censor/text_baidu', data).then((res) => {
|
||||
if (res?.code === 5000) {
|
||||
return Promise.reject(res.message);
|
||||
}
|
||||
});
|
||||
@@ -1,143 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
TableContainer,
|
||||
IconButton
|
||||
} from '@chakra-ui/react';
|
||||
import { getOpenApiKeys, createAOpenApiKey, delOpenApiById } from '@/api/openapi';
|
||||
import { useQuery, useMutation } from '@tanstack/react-query';
|
||||
import { useLoading } from '@/hooks/useLoading';
|
||||
import dayjs from 'dayjs';
|
||||
import { AddIcon, DeleteIcon } from '@chakra-ui/icons';
|
||||
import { getErrText, useCopyData } from '@/utils/tools';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import MyIcon from '../Icon';
|
||||
import MyModal from '../MyModal';
|
||||
|
||||
const APIKeyModal = ({ onClose }: { onClose: () => void }) => {
|
||||
const { Loading } = useLoading();
|
||||
const { toast } = useToast();
|
||||
const {
|
||||
data: apiKeys = [],
|
||||
isLoading: isGetting,
|
||||
refetch
|
||||
} = useQuery(['getOpenApiKeys'], getOpenApiKeys);
|
||||
const [apiKey, setApiKey] = useState('');
|
||||
const { copyData } = useCopyData();
|
||||
|
||||
const { mutate: onclickCreateApiKey, isLoading: isCreating } = useMutation({
|
||||
mutationFn: () => createAOpenApiKey(),
|
||||
onSuccess(res) {
|
||||
setApiKey(res);
|
||||
refetch();
|
||||
},
|
||||
onError(err) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: getErrText(err)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const { mutate: onclickRemove, isLoading: isDeleting } = useMutation({
|
||||
mutationFn: async (id: string) => delOpenApiById(id),
|
||||
onSuccess() {
|
||||
refetch();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<MyModal isOpen onClose={onClose} w={'600px'}>
|
||||
<Box py={3} px={5}>
|
||||
<Box fontWeight={'bold'} fontSize={'2xl'}>
|
||||
API 秘钥管理
|
||||
</Box>
|
||||
<Box fontSize={'sm'} color={'myGray.600'}>
|
||||
如果你不想 API 秘钥被滥用,请勿将秘钥直接放置在前端使用~
|
||||
</Box>
|
||||
</Box>
|
||||
<ModalBody minH={'300px'} maxH={['70vh', '500px']} overflow={'overlay'}>
|
||||
<TableContainer mt={2} position={'relative'}>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Api Key</Th>
|
||||
<Th>创建时间</Th>
|
||||
<Th>最后一次使用时间</Th>
|
||||
<Th />
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody fontSize={'sm'}>
|
||||
{apiKeys.map(({ id, apiKey, createTime, lastUsedTime }) => (
|
||||
<Tr key={id}>
|
||||
<Td>{apiKey}</Td>
|
||||
<Td>{dayjs(createTime).format('YYYY/MM/DD HH:mm:ss')}</Td>
|
||||
<Td>
|
||||
{lastUsedTime
|
||||
? dayjs(lastUsedTime).format('YYYY/MM/DD HH:mm:ss')
|
||||
: '没有使用过'}
|
||||
</Td>
|
||||
<Td>
|
||||
<IconButton
|
||||
icon={<DeleteIcon />}
|
||||
size={'xs'}
|
||||
aria-label={'delete'}
|
||||
variant={'base'}
|
||||
colorScheme={'gray'}
|
||||
onClick={() => onclickRemove(id)}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
variant="base"
|
||||
leftIcon={<AddIcon color={'myGray.600'} fontSize={'sm'} />}
|
||||
onClick={() => onclickCreateApiKey()}
|
||||
>
|
||||
新建秘钥
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
|
||||
<Loading loading={isGetting || isCreating || isDeleting} fixed={false} />
|
||||
<MyModal isOpen={!!apiKey} w={'400px'} onClose={() => setApiKey('')}>
|
||||
<Box py={3} px={5}>
|
||||
<Box fontWeight={'bold'} fontSize={'2xl'}>
|
||||
新的 API 秘钥
|
||||
</Box>
|
||||
<Box fontSize={'sm'} color={'myGray.600'}>
|
||||
请保管好你的秘钥,秘钥不会再次展示~
|
||||
</Box>
|
||||
</Box>
|
||||
<ModalBody>
|
||||
<Flex bg={'myGray.100'} px={3} py={2} cursor={'pointer'} onClick={() => copyData(apiKey)}>
|
||||
<Box flex={1}>{apiKey}</Box>
|
||||
<MyIcon name={'copy'} w={'16px'}></MyIcon>
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant="base" onClick={() => setApiKey('')}>
|
||||
好的
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default APIKeyModal;
|
||||
@@ -1,71 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box, ModalBody, useTheme, ModalHeader, Flex } from '@chakra-ui/react';
|
||||
import type { ChatHistoryItemResType } from '@/types/chat';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import MyModal from '../MyModal';
|
||||
import MyTooltip from '../MyTooltip';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
|
||||
const ResponseModal = ({
|
||||
response,
|
||||
onClose
|
||||
}: {
|
||||
response: ChatHistoryItemResType[];
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
const formatResponse = useMemo(
|
||||
() =>
|
||||
response.map((item) => {
|
||||
const copy = { ...item };
|
||||
delete copy.completeMessages;
|
||||
delete copy.quoteList;
|
||||
return copy;
|
||||
}),
|
||||
[response]
|
||||
);
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen={true}
|
||||
onClose={onClose}
|
||||
h={['90vh', '80vh']}
|
||||
minW={['90vw', '600px']}
|
||||
title={
|
||||
<Flex alignItems={'center'}>
|
||||
{t('chat.Complete Response')}
|
||||
<MyTooltip
|
||||
label={
|
||||
'moduleName: 模型名\nprice: 价格,倍率:100000\nmodel?: 模型名\ntokens?: token 消耗\n\nanswer?: 回答内容\nquestion?: 问题\ntemperature?: 温度\nmaxToken?: 最大 tokens\n\nsimilarity?: 相似度\nlimit?: 单次搜索结果\n\ncqList?: 问题分类列表\ncqResult?: 分类结果\n\nextractDescription?: 内容提取描述\nextractResult?: 提取结果'
|
||||
}
|
||||
>
|
||||
<QuestionOutlineIcon ml={2} />
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
}
|
||||
isCentered
|
||||
>
|
||||
<ModalBody>
|
||||
{formatResponse.map((item, i) => (
|
||||
<Box
|
||||
key={i}
|
||||
p={2}
|
||||
pt={[0, 2]}
|
||||
borderRadius={'lg'}
|
||||
border={theme.borders.base}
|
||||
_notLast={{ mb: 2 }}
|
||||
position={'relative'}
|
||||
whiteSpace={'pre-wrap'}
|
||||
>
|
||||
{JSON.stringify(item, null, 2)}
|
||||
</Box>
|
||||
))}
|
||||
</ModalBody>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResponseModal;
|
||||
@@ -1,104 +0,0 @@
|
||||
import React from 'react';
|
||||
import type { IconProps } from '@chakra-ui/react';
|
||||
import { Icon } from '@chakra-ui/react';
|
||||
|
||||
const map = {
|
||||
appFill: require('./icons/fill/app.svg').default,
|
||||
appLight: require('./icons/light/app.svg').default,
|
||||
copy: require('./icons/copy.svg').default,
|
||||
chatSend: require('./icons/chatSend.svg').default,
|
||||
delete: require('./icons/delete.svg').default,
|
||||
stop: require('./icons/stop.svg').default,
|
||||
collectionLight: require('./icons/collectionLight.svg').default,
|
||||
collectionSolid: require('./icons/collectionSolid.svg').default,
|
||||
empty: require('./icons/empty.svg').default,
|
||||
back: require('./icons/back.svg').default,
|
||||
backFill: require('./icons/fill/back.svg').default,
|
||||
more: require('./icons/more.svg').default,
|
||||
tabbarChat: require('./icons/phoneTabbar/chat.svg').default,
|
||||
tabbarModel: require('./icons/phoneTabbar/app.svg').default,
|
||||
tabbarMore: require('./icons/phoneTabbar/more.svg').default,
|
||||
tabbarMe: require('./icons/phoneTabbar/me.svg').default,
|
||||
closeSolid: require('./icons/closeSolid.svg').default,
|
||||
wx: require('./icons/wx.svg').default,
|
||||
out: require('./icons/out.svg').default,
|
||||
git: require('./icons/git.svg').default,
|
||||
gitFill: require('./icons/fill/git.svg').default,
|
||||
menu: require('./icons/menu.svg').default,
|
||||
edit: require('./icons/edit.svg').default,
|
||||
inform: require('./icons/inform.svg').default,
|
||||
export: require('./icons/export.svg').default,
|
||||
text: require('./icons/text.svg').default,
|
||||
history: require('./icons/history.svg').default,
|
||||
kbTest: require('./icons/kbTest.svg').default,
|
||||
date: require('./icons/date.svg').default,
|
||||
apikey: require('./icons/apikey.svg').default,
|
||||
save: require('./icons/save.svg').default,
|
||||
minus: require('./icons/minus.svg').default,
|
||||
chat: require('./icons/light/chat.svg').default,
|
||||
chatFill: require('./icons/fill/chat.svg').default,
|
||||
clear: require('./icons/light/clear.svg').default,
|
||||
apiLight: require('./icons/light/appApi.svg').default,
|
||||
overviewLight: require('./icons/light/overview.svg').default,
|
||||
settingLight: require('./icons/light/setting.svg').default,
|
||||
shareLight: require('./icons/light/share.svg').default,
|
||||
dbLight: require('./icons/light/db.svg').default,
|
||||
dbFill: require('./icons/fill/db.svg').default,
|
||||
appStoreLight: require('./icons/light/appStore.svg').default,
|
||||
appStoreFill: require('./icons/fill/appStore.svg').default,
|
||||
meLight: require('./icons/light/me.svg').default,
|
||||
meFill: require('./icons/fill/me.svg').default,
|
||||
welcomeText: require('./icons/modules/welcomeText.svg').default,
|
||||
variable: require('./icons/modules/variable.svg').default,
|
||||
setTop: require('./icons/light/setTop.svg').default,
|
||||
fullScreenLight: require('./icons/light/fullScreen.svg').default,
|
||||
voice: require('./icons/voice.svg').default,
|
||||
html: require('./icons/file/html.svg').default,
|
||||
pdf: require('./icons/file/pdf.svg').default,
|
||||
markdown: require('./icons/file/markdown.svg').default,
|
||||
importLight: require('./icons/light/import.svg').default,
|
||||
manualImport: require('./icons/file/manualImport.svg').default,
|
||||
indexImport: require('./icons/file/indexImport.svg').default,
|
||||
csvImport: require('./icons/file/csv.svg').default,
|
||||
qaImport: require('./icons/file/qaImport.svg').default,
|
||||
uploadFile: require('./icons/file/uploadFile.svg').default,
|
||||
closeLight: require('./icons/light/close.svg').default,
|
||||
customTitle: require('./icons/light/customTitle.svg').default,
|
||||
billRecordLight: require('./icons/light/billRecord.svg').default,
|
||||
informLight: require('./icons/light/inform.svg').default,
|
||||
payRecordLight: require('./icons/light/payRecord.svg').default,
|
||||
loginoutLight: require('./icons/light/loginout.svg').default,
|
||||
chatModelTag: require('./icons/light/chatModelTag.svg').default,
|
||||
language_en: require('./icons/language/en.svg').default,
|
||||
language_zh: require('./icons/language/zh.svg').default,
|
||||
outlink_share: require('./icons/outlink/share.svg').default,
|
||||
outlink_iframe: require('./icons/outlink/iframe.svg').default,
|
||||
addCircle: require('./icons/circle/add.svg').default,
|
||||
playFill: require('./icons/fill/play.svg').default,
|
||||
courseLight: require('./icons/light/course.svg').default,
|
||||
promotionLight: require('./icons/light/promotion.svg').default,
|
||||
logsLight: require('./icons/light/logs.svg').default,
|
||||
badLight: require('./icons/light/bad.svg').default,
|
||||
markLight: require('./icons/light/mark.svg').default
|
||||
};
|
||||
|
||||
export type IconName = keyof typeof map;
|
||||
|
||||
const MyIcon = (
|
||||
{ name, w = 'auto', h = 'auto', ...props }: { name: IconName } & IconProps,
|
||||
ref: any
|
||||
) => {
|
||||
return map[name] ? (
|
||||
<Icon
|
||||
as={map[name]}
|
||||
w={w}
|
||||
h={h}
|
||||
boxSizing={'content-box'}
|
||||
verticalAlign={'top'}
|
||||
fill={'currentcolor'}
|
||||
{...props}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default React.forwardRef(MyIcon);
|
||||
@@ -1,22 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Spinner, Flex } from '@chakra-ui/react';
|
||||
|
||||
const Loading = ({ fixed = true }: { fixed?: boolean }) => {
|
||||
return (
|
||||
<Flex
|
||||
position={fixed ? 'fixed' : 'absolute'}
|
||||
zIndex={1000}
|
||||
backgroundColor={'rgba(255,255,255,0.5)'}
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
>
|
||||
<Spinner thickness="4px" speed="0.65s" emptyColor="myGray.100" color="myBlue.600" size="xl" />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loading;
|
||||
@@ -1,49 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import RemarkGfm from 'remark-gfm';
|
||||
import RemarkMath from 'remark-math';
|
||||
import RehypeKatex from 'rehype-katex';
|
||||
|
||||
import 'katex/dist/katex.min.css';
|
||||
import styles from '../index.module.scss';
|
||||
import { EventNameEnum } from '../constant';
|
||||
|
||||
const Guide = ({ text, onClick }: { text: string; onClick?: (e: any) => void }) => {
|
||||
const formatText = useMemo(() => text.replace(/\[(.*?)\]/g, '[$1]()'), [text]);
|
||||
|
||||
return (
|
||||
<ReactMarkdown
|
||||
className={`markdown ${styles.markdown}`}
|
||||
remarkPlugins={[RemarkGfm, RemarkMath]}
|
||||
rehypePlugins={[RehypeKatex]}
|
||||
components={{
|
||||
a({ children }: any) {
|
||||
return (
|
||||
<Box as={'li'} py={1} m={0}>
|
||||
<Box
|
||||
as={'span'}
|
||||
color={'blue.600'}
|
||||
textDecoration={'underline'}
|
||||
cursor={'pointer'}
|
||||
onClick={() => {
|
||||
if (!onClick) return;
|
||||
onClick({
|
||||
event: EventNameEnum.guideClick,
|
||||
data: String(children)
|
||||
});
|
||||
}}
|
||||
>
|
||||
{String(children)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{formatText}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Guide);
|
||||
@@ -1,3 +0,0 @@
|
||||
export enum EventNameEnum {
|
||||
guideClick = 'guideClick'
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import type { KbItemType } from '@/types/plugin';
|
||||
|
||||
export const defaultKbDetail: KbItemType = {
|
||||
_id: '',
|
||||
userId: '',
|
||||
avatar: '/icon/logo.svg',
|
||||
name: '',
|
||||
tags: '',
|
||||
vectorModel: {
|
||||
model: 'text-embedding-ada-002',
|
||||
name: 'Embedding-2',
|
||||
price: 0.2,
|
||||
defaultToken: 500,
|
||||
maxToken: 3000
|
||||
}
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
function Error() {
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
router.replace('/app/list');
|
||||
}, 2000);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<p>
|
||||
部分系统不兼容,导致页面崩溃。如果可以,请联系作者,反馈下具体操作和页面。大部分是 苹果 的
|
||||
safari 浏览器导致,可以尝试更换 chrome 浏览器。
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
export default Error;
|
||||
@@ -1,58 +0,0 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { connectToDatabase, Chat } from '@/service/mongo';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
await authUser({ req, authRoot: true });
|
||||
await connectToDatabase();
|
||||
|
||||
const { limit = 1000 } = req.body as { limit: number };
|
||||
let skip = 0;
|
||||
const total = await Chat.countDocuments({
|
||||
chatId: { $exists: false }
|
||||
});
|
||||
let promise = Promise.resolve();
|
||||
console.log(total);
|
||||
|
||||
for (let i = 0; i < total; i += limit) {
|
||||
const skipVal = skip;
|
||||
skip += limit;
|
||||
promise = promise
|
||||
.then(() => init(limit, skipVal))
|
||||
.then(() => {
|
||||
console.log(skipVal);
|
||||
});
|
||||
}
|
||||
|
||||
await promise;
|
||||
|
||||
jsonRes(res, {});
|
||||
} catch (error) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function init(limit: number, skip: number) {
|
||||
// 遍历 app
|
||||
const chats = await Chat.find(
|
||||
{
|
||||
chatId: { $exists: false }
|
||||
},
|
||||
'_id'
|
||||
).limit(limit);
|
||||
|
||||
await Promise.all(
|
||||
chats.map((chat) =>
|
||||
Chat.findByIdAndUpdate(chat._id, {
|
||||
chatId: String(chat._id),
|
||||
source: 'online'
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { connectToDatabase, Chat, ChatItem } from '@/service/mongo';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 24);
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
await authUser({ req, authRoot: true });
|
||||
await connectToDatabase();
|
||||
|
||||
const { limit = 100 } = req.body as { limit: number };
|
||||
let skip = 0;
|
||||
|
||||
const total = await Chat.countDocuments({
|
||||
content: { $exists: true, $not: { $size: 0 } },
|
||||
isInit: { $ne: true }
|
||||
});
|
||||
const totalChat = await Chat.aggregate([
|
||||
{
|
||||
$project: {
|
||||
contentLength: { $size: '$content' }
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
totalLength: { $sum: '$contentLength' }
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
console.log('chatLen:', total, totalChat);
|
||||
|
||||
let promise = Promise.resolve();
|
||||
|
||||
for (let i = 0; i < total; i += limit) {
|
||||
const skipVal = skip;
|
||||
skip += limit;
|
||||
promise = promise
|
||||
.then(() => init(limit))
|
||||
.then(() => {
|
||||
console.log(skipVal);
|
||||
});
|
||||
}
|
||||
|
||||
await promise;
|
||||
|
||||
jsonRes(res, {});
|
||||
} catch (error) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function init(limit: number) {
|
||||
// 遍历 app
|
||||
const chats = await Chat.find(
|
||||
{
|
||||
content: { $exists: true, $not: { $size: 0 } },
|
||||
isInit: { $ne: true }
|
||||
},
|
||||
'_id userId appId chatId content'
|
||||
)
|
||||
.sort({ updateTime: -1 })
|
||||
.limit(limit);
|
||||
|
||||
await Promise.all(
|
||||
chats.map(async (chat) => {
|
||||
const inserts = chat.content
|
||||
.map((item) => ({
|
||||
dataId: nanoid(),
|
||||
chatId: chat.chatId,
|
||||
userId: chat.userId,
|
||||
appId: chat.appId,
|
||||
obj: item.obj,
|
||||
value: item.value,
|
||||
responseData: item.responseData
|
||||
}))
|
||||
.filter((item) => item.chatId && item.userId && item.appId && item.obj && item.value);
|
||||
|
||||
try {
|
||||
await Promise.all(inserts.map((item) => ChatItem.create(item)));
|
||||
await Chat.findByIdAndUpdate(chat._id, {
|
||||
isInit: true
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
await ChatItem.deleteMany({ chatId: chat.chatId });
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -1,446 +0,0 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { connectToDatabase, App } from '@/service/mongo';
|
||||
import { FlowModuleTypeEnum, SpecialInputKeyEnum } from '@/constants/flow';
|
||||
import { TaskResponseKeyEnum } from '@/constants/chat';
|
||||
import { FlowInputItemType } from '@/types/flow';
|
||||
|
||||
const chatModelInput = ({
|
||||
model,
|
||||
temperature,
|
||||
maxToken,
|
||||
systemPrompt,
|
||||
limitPrompt,
|
||||
kbList
|
||||
}: {
|
||||
model: string;
|
||||
temperature: number;
|
||||
maxToken: number;
|
||||
systemPrompt: string;
|
||||
limitPrompt: string;
|
||||
kbList: { kbId: string }[];
|
||||
}): FlowInputItemType[] => [
|
||||
{
|
||||
key: 'model',
|
||||
value: model,
|
||||
type: 'custom',
|
||||
label: '对话模型',
|
||||
connected: true
|
||||
},
|
||||
{
|
||||
key: 'temperature',
|
||||
value: temperature,
|
||||
label: '温度',
|
||||
type: 'slider',
|
||||
connected: true
|
||||
},
|
||||
{
|
||||
key: 'maxToken',
|
||||
value: maxToken,
|
||||
type: 'custom',
|
||||
label: '回复上限',
|
||||
connected: true
|
||||
},
|
||||
{
|
||||
key: 'systemPrompt',
|
||||
value: systemPrompt,
|
||||
type: 'textarea',
|
||||
label: '系统提示词',
|
||||
connected: true
|
||||
},
|
||||
{
|
||||
key: 'limitPrompt',
|
||||
label: '限定词',
|
||||
type: 'textarea',
|
||||
value: limitPrompt,
|
||||
connected: true
|
||||
},
|
||||
{
|
||||
key: 'switch',
|
||||
type: 'target',
|
||||
label: '触发器',
|
||||
connected: kbList.length > 0
|
||||
},
|
||||
{
|
||||
key: 'quoteQA',
|
||||
type: 'target',
|
||||
label: '引用内容',
|
||||
connected: kbList.length > 0
|
||||
},
|
||||
{
|
||||
key: 'history',
|
||||
type: 'target',
|
||||
label: '聊天记录',
|
||||
connected: true
|
||||
},
|
||||
{
|
||||
key: 'userChatInput',
|
||||
type: 'target',
|
||||
label: '用户问题',
|
||||
connected: true
|
||||
}
|
||||
];
|
||||
const chatTemplate = ({
|
||||
model,
|
||||
temperature,
|
||||
maxToken,
|
||||
systemPrompt,
|
||||
limitPrompt
|
||||
}: {
|
||||
model: string;
|
||||
temperature: number;
|
||||
maxToken: number;
|
||||
systemPrompt: string;
|
||||
limitPrompt: string;
|
||||
}) => {
|
||||
return [
|
||||
{
|
||||
flowType: FlowModuleTypeEnum.questionInput,
|
||||
inputs: [
|
||||
{
|
||||
key: 'userChatInput',
|
||||
connected: true
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
key: 'userChatInput',
|
||||
targets: [
|
||||
{
|
||||
moduleId: 'chatModule',
|
||||
key: 'userChatInput'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
position: {
|
||||
x: 464.32198615344566,
|
||||
y: 1602.2698463081606
|
||||
},
|
||||
moduleId: 'userChatInput'
|
||||
},
|
||||
{
|
||||
flowType: FlowModuleTypeEnum.historyNode,
|
||||
inputs: [
|
||||
{
|
||||
key: 'maxContext',
|
||||
value: 10,
|
||||
connected: true
|
||||
},
|
||||
{
|
||||
key: 'history',
|
||||
connected: true
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
key: 'history',
|
||||
targets: [
|
||||
{
|
||||
moduleId: 'chatModule',
|
||||
key: 'history'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
position: {
|
||||
x: 452.5466249541586,
|
||||
y: 1276.3930310334215
|
||||
},
|
||||
moduleId: 'history'
|
||||
},
|
||||
{
|
||||
flowType: FlowModuleTypeEnum.chatNode,
|
||||
inputs: chatModelInput({
|
||||
model,
|
||||
temperature,
|
||||
maxToken,
|
||||
systemPrompt,
|
||||
limitPrompt,
|
||||
kbList: []
|
||||
}),
|
||||
outputs: [
|
||||
{
|
||||
key: TaskResponseKeyEnum.answerText,
|
||||
targets: []
|
||||
}
|
||||
],
|
||||
position: {
|
||||
x: 981.9682828103937,
|
||||
y: 890.014595014464
|
||||
},
|
||||
moduleId: 'chatModule'
|
||||
}
|
||||
];
|
||||
};
|
||||
const kbTemplate = ({
|
||||
model,
|
||||
temperature,
|
||||
maxToken,
|
||||
systemPrompt,
|
||||
limitPrompt,
|
||||
kbList = [],
|
||||
searchSimilarity,
|
||||
searchLimit,
|
||||
searchEmptyText
|
||||
}: {
|
||||
model: string;
|
||||
temperature: number;
|
||||
maxToken: number;
|
||||
systemPrompt: string;
|
||||
limitPrompt: string;
|
||||
kbList: { kbId: string }[];
|
||||
searchSimilarity: number;
|
||||
searchLimit: number;
|
||||
searchEmptyText: string;
|
||||
}) => {
|
||||
return [
|
||||
{
|
||||
flowType: FlowModuleTypeEnum.questionInput,
|
||||
inputs: [
|
||||
{
|
||||
key: 'userChatInput',
|
||||
connected: true
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
key: 'userChatInput',
|
||||
targets: [
|
||||
{
|
||||
moduleId: 'chatModule',
|
||||
key: 'userChatInput'
|
||||
},
|
||||
{
|
||||
moduleId: 'kbSearch',
|
||||
key: 'userChatInput'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
position: {
|
||||
x: 464.32198615344566,
|
||||
y: 1602.2698463081606
|
||||
},
|
||||
moduleId: 'userChatInput'
|
||||
},
|
||||
{
|
||||
flowType: FlowModuleTypeEnum.historyNode,
|
||||
inputs: [
|
||||
{
|
||||
key: 'maxContext',
|
||||
value: 10,
|
||||
connected: true
|
||||
},
|
||||
{
|
||||
key: 'history',
|
||||
connected: true
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
key: 'history',
|
||||
targets: [
|
||||
{
|
||||
moduleId: 'chatModule',
|
||||
key: 'history'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
position: {
|
||||
x: 452.5466249541586,
|
||||
y: 1276.3930310334215
|
||||
},
|
||||
moduleId: 'history'
|
||||
},
|
||||
{
|
||||
flowType: FlowModuleTypeEnum.kbSearchNode,
|
||||
inputs: [
|
||||
{
|
||||
key: 'kbList',
|
||||
value: kbList,
|
||||
connected: true
|
||||
},
|
||||
{
|
||||
key: 'similarity',
|
||||
value: searchSimilarity,
|
||||
connected: true
|
||||
},
|
||||
{
|
||||
key: 'limit',
|
||||
value: searchLimit,
|
||||
connected: true
|
||||
},
|
||||
{
|
||||
key: 'switch',
|
||||
connected: false
|
||||
},
|
||||
{
|
||||
key: 'userChatInput',
|
||||
connected: true
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
key: 'isEmpty',
|
||||
targets: searchEmptyText
|
||||
? [
|
||||
{
|
||||
moduleId: 'emptyText',
|
||||
key: 'switch'
|
||||
}
|
||||
]
|
||||
: [
|
||||
{
|
||||
moduleId: 'chatModule',
|
||||
key: 'switch'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'unEmpty',
|
||||
targets: [
|
||||
{
|
||||
moduleId: 'chatModule',
|
||||
key: 'switch'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'quoteQA',
|
||||
targets: [
|
||||
{
|
||||
moduleId: 'chatModule',
|
||||
key: 'quoteQA'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
position: {
|
||||
x: 956.0838440206068,
|
||||
y: 887.462827870246
|
||||
},
|
||||
moduleId: 'kbSearch'
|
||||
},
|
||||
...(searchEmptyText
|
||||
? [
|
||||
{
|
||||
flowType: FlowModuleTypeEnum.answerNode,
|
||||
inputs: [
|
||||
{
|
||||
key: 'switch',
|
||||
connected: true
|
||||
},
|
||||
{
|
||||
key: SpecialInputKeyEnum.answerText,
|
||||
value: searchEmptyText,
|
||||
connected: true
|
||||
}
|
||||
],
|
||||
outputs: [],
|
||||
position: {
|
||||
x: 1553.5815811529146,
|
||||
y: 637.8753731306779
|
||||
},
|
||||
moduleId: 'emptyText'
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
flowType: FlowModuleTypeEnum.chatNode,
|
||||
inputs: chatModelInput({ model, temperature, maxToken, systemPrompt, limitPrompt, kbList }),
|
||||
outputs: [
|
||||
{
|
||||
key: TaskResponseKeyEnum.answerText,
|
||||
targets: []
|
||||
}
|
||||
],
|
||||
position: {
|
||||
x: 1551.71405495818,
|
||||
y: 977.4911578918461
|
||||
},
|
||||
moduleId: 'chatModule'
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
await authUser({ req, authRoot: true });
|
||||
await connectToDatabase();
|
||||
|
||||
const { limit = 1000 } = req.body as { limit: number };
|
||||
let skip = 0;
|
||||
const total = await App.countDocuments();
|
||||
let promise = Promise.resolve();
|
||||
console.log(total);
|
||||
|
||||
for (let i = 0; i < total; i += limit) {
|
||||
const skipVal = skip;
|
||||
skip += limit;
|
||||
promise = promise
|
||||
.then(() => init(limit, skipVal))
|
||||
.then(() => {
|
||||
console.log(skipVal);
|
||||
});
|
||||
}
|
||||
|
||||
await promise;
|
||||
|
||||
jsonRes(res, {});
|
||||
} catch (error) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function init(limit: number, skip: number) {
|
||||
// 遍历 app
|
||||
const apps = await App.find(
|
||||
{
|
||||
chat: { $ne: null },
|
||||
modules: { $exists: false }
|
||||
// userId: '63f9a14228d2a688d8dc9e1b'
|
||||
},
|
||||
'_id chat'
|
||||
).limit(limit);
|
||||
|
||||
return Promise.all(
|
||||
apps.map(async (app) => {
|
||||
if (!app.chat) return app;
|
||||
const modules = (() => {
|
||||
if (app.chat.relatedKbs.length === 0) {
|
||||
return chatTemplate({
|
||||
model: app.chat.chatModel,
|
||||
temperature: app.chat.temperature,
|
||||
maxToken: app.chat.maxToken,
|
||||
systemPrompt: app.chat.systemPrompt,
|
||||
limitPrompt: app.chat.limitPrompt
|
||||
});
|
||||
} else {
|
||||
return kbTemplate({
|
||||
model: app.chat.chatModel,
|
||||
temperature: app.chat.temperature,
|
||||
maxToken: app.chat.maxToken,
|
||||
systemPrompt: app.chat.systemPrompt,
|
||||
limitPrompt: app.chat.limitPrompt,
|
||||
kbList: app.chat.relatedKbs.map((id) => ({ kbId: id })),
|
||||
searchEmptyText: app.chat.searchEmptyText,
|
||||
searchLimit: app.chat.searchLimit,
|
||||
searchSimilarity: app.chat.searchSimilarity
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
await App.findByIdAndUpdate(app.id, {
|
||||
modules
|
||||
});
|
||||
return modules;
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, OpenApi } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { UserOpenApiKey } from '@/types/openapi';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
const findResponse = await OpenApi.find({ userId }).sort({ _id: -1 });
|
||||
|
||||
// jus save four data
|
||||
const apiKeys = findResponse.map<UserOpenApiKey>(
|
||||
({ _id, apiKey, createTime, lastUsedTime }) => {
|
||||
return {
|
||||
id: _id,
|
||||
apiKey: `******${apiKey.substring(apiKey.length - 4)}`,
|
||||
createTime,
|
||||
lastUsedTime
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
jsonRes(res, {
|
||||
data: apiKeys
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, TrainingData, KB } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { authKb } from '@/service/utils/auth';
|
||||
import { withNextCors } from '@/service/utils/tools';
|
||||
import { PgTrainingTableName, TrainingModeEnum } from '@/constants/plugin';
|
||||
import { startQueue } from '@/service/utils/tools';
|
||||
import { PgClient } from '@/service/pg';
|
||||
import { modelToolMap } from '@/utils/plugin';
|
||||
|
||||
export type DateItemType = { a: string; q: string; source?: string };
|
||||
|
||||
export type Props = {
|
||||
kbId: string;
|
||||
data: DateItemType[];
|
||||
mode: `${TrainingModeEnum}`;
|
||||
prompt?: string;
|
||||
};
|
||||
|
||||
export type Response = {
|
||||
insertLen: number;
|
||||
};
|
||||
|
||||
const modeMaxToken = {
|
||||
[TrainingModeEnum.index]: 6000,
|
||||
[TrainingModeEnum.qa]: 12000
|
||||
};
|
||||
|
||||
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const { kbId, data, mode, prompt } = req.body as Props;
|
||||
|
||||
if (!kbId || !Array.isArray(data)) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
// 凭证校验
|
||||
const { userId } = await authUser({ req });
|
||||
|
||||
jsonRes<Response>(res, {
|
||||
data: await pushDataToKb({
|
||||
kbId,
|
||||
data,
|
||||
userId,
|
||||
mode,
|
||||
prompt
|
||||
})
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export async function pushDataToKb({
|
||||
userId,
|
||||
kbId,
|
||||
data,
|
||||
mode,
|
||||
prompt
|
||||
}: { userId: string } & Props): Promise<Response> {
|
||||
await authKb({
|
||||
userId,
|
||||
kbId
|
||||
});
|
||||
|
||||
// 过滤重复的 qa 内容
|
||||
const set = new Set();
|
||||
const filterData: DateItemType[] = [];
|
||||
|
||||
data.forEach((item) => {
|
||||
const text = item.q + item.a;
|
||||
|
||||
// count token
|
||||
const token = modelToolMap.countTokens({
|
||||
model: 'gpt-3.5-turbo',
|
||||
messages: [{ obj: 'System', value: item.q }]
|
||||
});
|
||||
|
||||
if (token > modeMaxToken[TrainingModeEnum.qa]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!set.has(text)) {
|
||||
filterData.push(item);
|
||||
set.add(text);
|
||||
}
|
||||
});
|
||||
|
||||
// 数据库去重
|
||||
const insertData = (
|
||||
await Promise.allSettled(
|
||||
filterData.map(async ({ q, a = '', source }) => {
|
||||
if (mode !== TrainingModeEnum.index) {
|
||||
return Promise.resolve({
|
||||
q,
|
||||
a,
|
||||
source
|
||||
});
|
||||
}
|
||||
|
||||
if (!q) {
|
||||
return Promise.reject('q为空');
|
||||
}
|
||||
|
||||
q = q.replace(/\\n/g, '\n').trim().replace(/'/g, '"');
|
||||
a = a.replace(/\\n/g, '\n').trim().replace(/'/g, '"');
|
||||
|
||||
// Exactly the same data, not push
|
||||
try {
|
||||
const { rows } = await PgClient.query(`
|
||||
SELECT COUNT(*) > 0 AS exists
|
||||
FROM ${PgTrainingTableName}
|
||||
WHERE md5(q)=md5('${q}') AND md5(a)=md5('${a}') AND user_id='${userId}' AND kb_id='${kbId}'
|
||||
`);
|
||||
const exists = rows[0]?.exists || false;
|
||||
|
||||
if (exists) {
|
||||
return Promise.reject('已经存在');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
error;
|
||||
}
|
||||
return Promise.resolve({
|
||||
q,
|
||||
a,
|
||||
source
|
||||
});
|
||||
})
|
||||
)
|
||||
)
|
||||
.filter((item) => item.status === 'fulfilled')
|
||||
.map<DateItemType>((item: any) => item.value);
|
||||
|
||||
const vectorModel = await (async () => {
|
||||
if (mode === TrainingModeEnum.index) {
|
||||
return (await KB.findById(kbId, 'vectorModel'))?.vectorModel || global.vectorModels[0].model;
|
||||
}
|
||||
return global.vectorModels[0].model;
|
||||
})();
|
||||
|
||||
// 插入记录
|
||||
await TrainingData.insertMany(
|
||||
insertData.map((item) => ({
|
||||
q: item.q,
|
||||
a: item.a,
|
||||
source: item.source,
|
||||
userId,
|
||||
kbId,
|
||||
mode,
|
||||
prompt,
|
||||
vectorModel
|
||||
}))
|
||||
);
|
||||
|
||||
insertData.length > 0 && startQueue();
|
||||
|
||||
return {
|
||||
insertLen: insertData.length
|
||||
};
|
||||
}
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: {
|
||||
sizeLimit: '20mb'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,67 +0,0 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import type { ChatItemType } from '@/types/chat';
|
||||
import { countOpenAIToken } from '@/utils/plugin/openai';
|
||||
import { OpenAiChatEnum } from '@/constants/model';
|
||||
|
||||
type ModelType = `${OpenAiChatEnum}`;
|
||||
|
||||
type Props = {
|
||||
messages: ChatItemType[];
|
||||
model: ModelType;
|
||||
maxLen: number;
|
||||
};
|
||||
type Response = ChatItemType[];
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
await authUser({ req });
|
||||
|
||||
const { messages, model, maxLen } = req.body as Props;
|
||||
|
||||
if (!Array.isArray(messages) || !model || !maxLen) {
|
||||
throw new Error('params is error');
|
||||
}
|
||||
|
||||
return jsonRes<Response>(res, {
|
||||
data: gpt_chatItemTokenSlice({
|
||||
messages,
|
||||
model,
|
||||
maxToken: maxLen
|
||||
})
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function gpt_chatItemTokenSlice({
|
||||
messages,
|
||||
model = 'gpt-3.5-turbo',
|
||||
maxToken
|
||||
}: {
|
||||
messages: ChatItemType[];
|
||||
model?: string;
|
||||
maxToken: number;
|
||||
}) {
|
||||
let result: ChatItemType[] = [];
|
||||
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
const msgs = [...result, messages[i]];
|
||||
|
||||
const tokens = countOpenAIToken({ messages: msgs, model });
|
||||
|
||||
if (tokens < maxToken) {
|
||||
result = msgs;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result.length === 0 && messages[0] ? [messages[0]] : result;
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import axios from 'axios';
|
||||
import { axiosConfig } from '@/service/ai/openai';
|
||||
|
||||
export type Props = {
|
||||
input: string;
|
||||
};
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
await authUser({ req });
|
||||
|
||||
const result = await sensitiveCheck(req.body);
|
||||
|
||||
jsonRes(res, {
|
||||
data: result,
|
||||
message: result
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function sensitiveCheck({ input }: Props) {
|
||||
const response = await axios({
|
||||
...axiosConfig(),
|
||||
method: 'POST',
|
||||
url: `/moderations`,
|
||||
data: {
|
||||
input
|
||||
}
|
||||
});
|
||||
|
||||
const data = (response.data.results?.[0]?.category_scores as Record<string, number>) || {};
|
||||
|
||||
const values = Object.values(data);
|
||||
|
||||
for (const val of values) {
|
||||
if (val > 0.2) {
|
||||
return Promise.reject('您的内容不合规');
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, User } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { PgClient } from '@/service/pg';
|
||||
import { PgTrainingTableName } from '@/constants/plugin';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
let { kbId } = req.query as {
|
||||
kbId: string;
|
||||
};
|
||||
|
||||
if (!kbId) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
// 凭证校验
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
const thirtyMinutesAgo = new Date(Date.now() - 30 * 60 * 1000);
|
||||
|
||||
// auth export times
|
||||
const authTimes = await User.findOne(
|
||||
{
|
||||
_id: userId,
|
||||
$or: [
|
||||
{ 'limit.exportKbTime': { $exists: false } },
|
||||
{ 'limit.exportKbTime': { $lte: thirtyMinutesAgo } }
|
||||
]
|
||||
},
|
||||
'_id limit'
|
||||
);
|
||||
|
||||
if (!authTimes) {
|
||||
throw new Error('上次导出未到半小时,每半小时仅可导出一次。');
|
||||
}
|
||||
|
||||
// 统计数据
|
||||
const count = await PgClient.count(PgTrainingTableName, {
|
||||
where: [['kb_id', kbId], 'AND', ['user_id', userId]]
|
||||
});
|
||||
// 从 pg 中获取所有数据
|
||||
const pgData = await PgClient.select<{ q: string; a: string; source: string }>(
|
||||
PgTrainingTableName,
|
||||
{
|
||||
where: [['kb_id', kbId], 'AND', ['user_id', userId]],
|
||||
fields: ['q', 'a', 'source'],
|
||||
order: [{ field: 'id', mode: 'DESC' }],
|
||||
limit: count
|
||||
}
|
||||
);
|
||||
|
||||
const data: [string, string, string][] = pgData.rows.map((item) => [
|
||||
item.q.replace(/\n/g, '\\n'),
|
||||
item.a.replace(/\n/g, '\\n'),
|
||||
item.source
|
||||
]);
|
||||
|
||||
// update export time
|
||||
await User.findByIdAndUpdate(userId, {
|
||||
'limit.exportKbTime': new Date()
|
||||
});
|
||||
|
||||
jsonRes(res, {
|
||||
data
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: {
|
||||
sizeLimit: '100mb'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,88 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, KB } from '@/service/mongo';
|
||||
import { authKb, authUser } from '@/service/utils/auth';
|
||||
import { withNextCors } from '@/service/utils/tools';
|
||||
import { PgTrainingTableName } from '@/constants/plugin';
|
||||
import { insertKbItem, PgClient } from '@/service/pg';
|
||||
import { modelToolMap } from '@/utils/plugin';
|
||||
import { getVectorModel } from '@/service/utils/data';
|
||||
import { getVector } from '@/pages/api/openapi/plugin/vector';
|
||||
|
||||
export type Props = {
|
||||
kbId: string;
|
||||
data: { a: string; q: string; source?: string };
|
||||
};
|
||||
|
||||
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
await connectToDatabase();
|
||||
|
||||
const { kbId, data = { q: '', a: '' } } = req.body as Props;
|
||||
|
||||
if (!kbId || !data?.q) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
// 凭证校验
|
||||
const { userId } = await authUser({ req });
|
||||
|
||||
// auth kb
|
||||
const kb = await authKb({ kbId, userId });
|
||||
|
||||
const q = data?.q?.replace(/\\n/g, '\n').trim().replace(/'/g, '"');
|
||||
const a = data?.a?.replace(/\\n/g, '\n').trim().replace(/'/g, '"');
|
||||
|
||||
// token check
|
||||
const token = modelToolMap.countTokens({
|
||||
model: 'gpt-3.5-turbo',
|
||||
messages: [{ obj: 'System', value: q }]
|
||||
});
|
||||
|
||||
if (token > getVectorModel(kb.vectorModel).maxToken) {
|
||||
throw new Error('Over Tokens');
|
||||
}
|
||||
|
||||
const { rows: existsRows } = await PgClient.query(`
|
||||
SELECT COUNT(*) > 0 AS exists
|
||||
FROM ${PgTrainingTableName}
|
||||
WHERE md5(q)=md5('${q}') AND md5(a)=md5('${a}') AND user_id='${userId}' AND kb_id='${kbId}'
|
||||
`);
|
||||
const exists = existsRows[0]?.exists || false;
|
||||
|
||||
if (exists) {
|
||||
throw new Error('已经存在完全一致的数据');
|
||||
}
|
||||
|
||||
const { vectors } = await getVector({
|
||||
model: kb.vectorModel,
|
||||
input: [q],
|
||||
userId
|
||||
});
|
||||
|
||||
const response = await insertKbItem({
|
||||
userId,
|
||||
kbId,
|
||||
data: [
|
||||
{
|
||||
q,
|
||||
a,
|
||||
source: data.source,
|
||||
vector: vectors[0]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
const id = response?.rows?.[0]?.id || '';
|
||||
|
||||
jsonRes(res, {
|
||||
data: id
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,56 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, KB, App, TrainingData } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import { PgClient } from '@/service/pg';
|
||||
import { Types } from 'mongoose';
|
||||
import { PgTrainingTableName } from '@/constants/plugin';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const { id } = req.query as {
|
||||
id: string;
|
||||
};
|
||||
|
||||
if (!id) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
// 凭证校验
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
// delete all pg data
|
||||
await PgClient.delete(PgTrainingTableName, {
|
||||
where: [['user_id', userId], 'AND', ['kb_id', id]]
|
||||
});
|
||||
|
||||
// delete training data
|
||||
await TrainingData.deleteMany({
|
||||
userId,
|
||||
kbId: id
|
||||
});
|
||||
|
||||
// delete related app
|
||||
await App.updateMany(
|
||||
{
|
||||
userId
|
||||
},
|
||||
{ $pull: { 'chat.relatedKbs': new Types.ObjectId(id) } }
|
||||
);
|
||||
|
||||
// delete kb data
|
||||
await KB.findOneAndDelete({
|
||||
_id: id,
|
||||
userId
|
||||
});
|
||||
|
||||
jsonRes(res);
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { User } from '@/service/models/user';
|
||||
import { generateToken, setCookie } from '@/service/utils/tools';
|
||||
import axios from 'axios';
|
||||
import { parseQueryString } from '@/utils/tools';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 8);
|
||||
|
||||
type GithubAccessTokenType = {
|
||||
access_token: string;
|
||||
expires_in: number;
|
||||
refresh_token: string;
|
||||
refresh_token_expires_in: number;
|
||||
token_type: 'bearer';
|
||||
scope: string;
|
||||
};
|
||||
type GithubUserType = {
|
||||
login: string;
|
||||
email: string;
|
||||
avatar_url: string;
|
||||
};
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const { code, inviterId } = req.query as { code: string; inviterId?: string };
|
||||
|
||||
const { data: gitAccessToken } = await axios.post<string>(
|
||||
`https://github.com/login/oauth/access_token?client_id=${global.feConfigs.gitLoginKey}&client_secret=${global.systemEnv.gitLoginSecret}&code=${code}`
|
||||
);
|
||||
const jsonGitAccessToken = parseQueryString(gitAccessToken) as GithubAccessTokenType;
|
||||
|
||||
const access_token = jsonGitAccessToken?.access_token;
|
||||
if (!access_token) {
|
||||
throw new Error('access_token is null');
|
||||
}
|
||||
|
||||
const { data } = await axios.get<GithubUserType>('https://api.github.com/user', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${access_token}`
|
||||
}
|
||||
});
|
||||
const { login, avatar_url } = data;
|
||||
const username = `git-${login}`;
|
||||
|
||||
try {
|
||||
jsonRes(res, {
|
||||
data: await loginByUsername({ username, res })
|
||||
});
|
||||
} catch (err: any) {
|
||||
if (err?.code === 500) {
|
||||
jsonRes(res, {
|
||||
data: await registerUser({ username, avatar: avatar_url, res, inviterId })
|
||||
});
|
||||
return;
|
||||
}
|
||||
throw new Error(err);
|
||||
}
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function loginByUsername({
|
||||
username,
|
||||
res
|
||||
}: {
|
||||
username: string;
|
||||
res: NextApiResponse;
|
||||
}) {
|
||||
const user = await User.findOne({ username });
|
||||
|
||||
if (!user) {
|
||||
return Promise.reject({
|
||||
code: 500
|
||||
});
|
||||
}
|
||||
|
||||
const token = generateToken(user._id);
|
||||
setCookie(res, token);
|
||||
return { user, token };
|
||||
}
|
||||
|
||||
export async function registerUser({
|
||||
username,
|
||||
avatar,
|
||||
inviterId,
|
||||
res
|
||||
}: {
|
||||
username: string;
|
||||
avatar?: string;
|
||||
inviterId?: string;
|
||||
res: NextApiResponse;
|
||||
}) {
|
||||
const response = await User.create({
|
||||
username,
|
||||
avatar,
|
||||
password: nanoid(),
|
||||
inviterId: inviterId ? inviterId : undefined
|
||||
});
|
||||
|
||||
// 根据 id 获取用户信息
|
||||
const user = await User.findById(response._id);
|
||||
|
||||
if (!user) {
|
||||
return Promise.reject('获取用户信息异常');
|
||||
}
|
||||
|
||||
const token = generateToken(user._id);
|
||||
setCookie(res, token);
|
||||
|
||||
return {
|
||||
user,
|
||||
token
|
||||
};
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Box, Divider, Flex, useTheme, Button, Skeleton, useDisclosure } from '@chakra-ui/react';
|
||||
import { useCopyData } from '@/utils/tools';
|
||||
import dynamic from 'next/dynamic';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
|
||||
const APIKeyModal = dynamic(() => import('@/components/APIKeyModal'), {
|
||||
ssr: false
|
||||
});
|
||||
|
||||
const API = ({ appId }: { appId: string }) => {
|
||||
const theme = useTheme();
|
||||
const { copyData } = useCopyData();
|
||||
const [baseUrl, setBaseUrl] = useState('https://fastgpt.run/api/openapi');
|
||||
const {
|
||||
isOpen: isOpenAPIModal,
|
||||
onOpen: onOpenAPIModal,
|
||||
onClose: onCloseAPIModal
|
||||
} = useDisclosure();
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
|
||||
const { isPc } = useGlobalStore();
|
||||
|
||||
useEffect(() => {
|
||||
setBaseUrl(`${location.origin}/api/openapi`);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} pt={[0, 5]} h={'100%'}>
|
||||
<Flex px={5} alignItems={'center'}>
|
||||
<Box flex={1}>
|
||||
AppId:
|
||||
<Box
|
||||
as={'span'}
|
||||
ml={2}
|
||||
fontWeight={'bold'}
|
||||
cursor={'pointer'}
|
||||
onClick={() => copyData(appId, '已复制 AppId')}
|
||||
>
|
||||
{appId}
|
||||
</Box>
|
||||
</Box>
|
||||
{isPc && (
|
||||
<>
|
||||
<Flex
|
||||
bg={'myWhite.600'}
|
||||
py={2}
|
||||
px={4}
|
||||
borderRadius={'md'}
|
||||
cursor={'pointer'}
|
||||
onClick={() => copyData(baseUrl, '已复制 API 地址')}
|
||||
>
|
||||
<Box border={theme.borders.md} px={2} borderRadius={'md'} fontSize={'sm'}>
|
||||
API服务器
|
||||
</Box>
|
||||
<Box ml={2} color={'myGray.900'} fontSize={['sm', 'md']}>
|
||||
{baseUrl}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Button
|
||||
ml={3}
|
||||
leftIcon={<MyIcon name={'apikey'} w={'16px'} color={''} />}
|
||||
variant={'base'}
|
||||
onClick={onOpenAPIModal}
|
||||
>
|
||||
API 秘钥
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
<Divider mt={3} />
|
||||
<Box flex={'1 0 0'} h={0}>
|
||||
<Skeleton h="100%" isLoaded={isLoaded} fadeDuration={2}>
|
||||
<iframe
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}}
|
||||
src="https://kjqvjse66l.feishu.cn/docx/DmLedTWtUoNGX8xui9ocdUEjnNh"
|
||||
frameBorder="0"
|
||||
onLoad={() => setIsLoaded(true)}
|
||||
onError={() => setIsLoaded(true)}
|
||||
/>
|
||||
</Skeleton>
|
||||
</Box>
|
||||
{isOpenAPIModal && <APIKeyModal onClose={onCloseAPIModal} />}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default API;
|
||||
@@ -1,59 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import { Box, Flex, Textarea } from '@chakra-ui/react';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import NodeCard from '../modules/NodeCard';
|
||||
import { FlowModuleItemType } from '@/types/flow';
|
||||
import Container from '../modules/Container';
|
||||
import { SystemInputEnum } from '@/constants/app';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import { welcomeTextTip } from '@/constants/flow/ModuleTemplate';
|
||||
|
||||
const NodeUserGuide = ({ data }: NodeProps<FlowModuleItemType>) => {
|
||||
const { inputs, moduleId, onChangeNode } = data;
|
||||
const welcomeText = useMemo(
|
||||
() => inputs.find((item) => item.key === SystemInputEnum.welcomeText),
|
||||
[inputs]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<NodeCard minW={'300px'} {...data}>
|
||||
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'}>
|
||||
<>
|
||||
<Flex mb={1} alignItems={'center'}>
|
||||
<MyIcon name={'welcomeText'} mr={2} w={'16px'} color={'#E74694'} />
|
||||
<Box>开场白</Box>
|
||||
<MyTooltip label={welcomeTextTip} forceShow>
|
||||
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
{welcomeText && (
|
||||
<Textarea
|
||||
className="nodrag"
|
||||
rows={6}
|
||||
resize={'both'}
|
||||
defaultValue={welcomeText.value}
|
||||
bg={'myWhite.500'}
|
||||
placeholder={welcomeTextTip}
|
||||
onChange={(e) => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
key: SystemInputEnum.welcomeText,
|
||||
type: 'inputs',
|
||||
value: {
|
||||
...welcomeText,
|
||||
value: e.target.value
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</Container>
|
||||
</NodeCard>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default React.memo(NodeUserGuide);
|
||||
@@ -1,632 +0,0 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
Controls,
|
||||
ReactFlowProvider,
|
||||
addEdge,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
XYPosition,
|
||||
Connection,
|
||||
useViewport
|
||||
} from 'reactflow';
|
||||
import { Box, Flex, IconButton, useTheme, useDisclosure } from '@chakra-ui/react';
|
||||
import { SmallCloseIcon } from '@chakra-ui/icons';
|
||||
import {
|
||||
edgeOptions,
|
||||
connectionLineStyle,
|
||||
FlowModuleTypeEnum,
|
||||
FlowInputItemTypeEnum,
|
||||
FlowValueTypeEnum
|
||||
} from '@/constants/flow';
|
||||
import { appModule2FlowNode, appModule2FlowEdge } from '@/utils/adapt';
|
||||
import {
|
||||
FlowModuleItemType,
|
||||
FlowModuleTemplateType,
|
||||
FlowOutputTargetItemType,
|
||||
type FlowModuleItemChangeProps
|
||||
} from '@/types/flow';
|
||||
import { AppModuleItemType } from '@/types/app';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import { useRequest } from '@/hooks/useRequest';
|
||||
import type { AppSchema } from '@/types/mongoSchema';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useCopyData } from '@/utils/tools';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import MyIcon from '@/components/Icon';
|
||||
import ButtonEdge from './components/modules/ButtonEdge';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import TemplateList from './components/TemplateList';
|
||||
import ChatTest, { type ChatTestComponentRef } from './components/ChatTest';
|
||||
|
||||
const ImportSettings = dynamic(() => import('./components/ImportSettings'), {
|
||||
ssr: false
|
||||
});
|
||||
const NodeChat = dynamic(() => import('./components/Nodes/NodeChat'), {
|
||||
ssr: false
|
||||
});
|
||||
const NodeKbSearch = dynamic(() => import('./components/Nodes/NodeKbSearch'), {
|
||||
ssr: false
|
||||
});
|
||||
const NodeHistory = dynamic(() => import('./components/Nodes/NodeHistory'), {
|
||||
ssr: false
|
||||
});
|
||||
const NodeTFSwitch = dynamic(() => import('./components/Nodes/NodeTFSwitch'), {
|
||||
ssr: false
|
||||
});
|
||||
const NodeAnswer = dynamic(() => import('./components/Nodes/NodeAnswer'), {
|
||||
ssr: false
|
||||
});
|
||||
const NodeQuestionInput = dynamic(() => import('./components/Nodes/NodeQuestionInput'), {
|
||||
ssr: false
|
||||
});
|
||||
const NodeCQNode = dynamic(() => import('./components/Nodes/NodeCQNode'), {
|
||||
ssr: false
|
||||
});
|
||||
const NodeVariable = dynamic(() => import('./components/Nodes/NodeVariable'), {
|
||||
ssr: false
|
||||
});
|
||||
const NodeUserGuide = dynamic(() => import('./components/Nodes/NodeUserGuide'), {
|
||||
ssr: false
|
||||
});
|
||||
const NodeExtract = dynamic(() => import('./components/Nodes/NodeExtract'), {
|
||||
ssr: false
|
||||
});
|
||||
const NodeHttp = dynamic(() => import('./components/Nodes/NodeHttp'), {
|
||||
ssr: false
|
||||
});
|
||||
|
||||
import 'reactflow/dist/style.css';
|
||||
import styles from './index.module.scss';
|
||||
import { AppTypeEnum } from '@/constants/app';
|
||||
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
|
||||
|
||||
const nodeTypes = {
|
||||
[FlowModuleTypeEnum.userGuide]: NodeUserGuide,
|
||||
[FlowModuleTypeEnum.variable]: NodeVariable,
|
||||
[FlowModuleTypeEnum.questionInput]: NodeQuestionInput,
|
||||
[FlowModuleTypeEnum.historyNode]: NodeHistory,
|
||||
[FlowModuleTypeEnum.chatNode]: NodeChat,
|
||||
[FlowModuleTypeEnum.kbSearchNode]: NodeKbSearch,
|
||||
[FlowModuleTypeEnum.tfSwitchNode]: NodeTFSwitch,
|
||||
[FlowModuleTypeEnum.answerNode]: NodeAnswer,
|
||||
[FlowModuleTypeEnum.classifyQuestion]: NodeCQNode,
|
||||
[FlowModuleTypeEnum.contentExtract]: NodeExtract,
|
||||
[FlowModuleTypeEnum.httpRequest]: NodeHttp
|
||||
// [FlowModuleTypeEnum.empty]: EmptyModule
|
||||
};
|
||||
const edgeTypes = {
|
||||
buttonedge: ButtonEdge
|
||||
};
|
||||
type Props = { app: AppSchema; onCloseSettings: () => void };
|
||||
|
||||
const AppEdit = ({ app, onCloseSettings }: Props) => {
|
||||
const theme = useTheme();
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
const { copyData } = useCopyData();
|
||||
|
||||
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
||||
const ChatTestRef = useRef<ChatTestComponentRef>(null);
|
||||
|
||||
const { updateAppDetail } = useUserStore();
|
||||
const { x, y, zoom } = useViewport();
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState<FlowModuleItemType>([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
const {
|
||||
isOpen: isOpenTemplate,
|
||||
onOpen: onOpenTemplate,
|
||||
onClose: onCloseTemplate
|
||||
} = useDisclosure();
|
||||
const { isOpen: isOpenImport, onOpen: onOpenImport, onClose: onCloseImport } = useDisclosure();
|
||||
|
||||
const [testModules, setTestModules] = useState<AppModuleItemType[]>();
|
||||
|
||||
const onFixView = useCallback(() => {
|
||||
const btn = document.querySelector('.react-flow__controls-fitview') as HTMLButtonElement;
|
||||
|
||||
setTimeout(() => {
|
||||
btn && btn.click();
|
||||
}, 100);
|
||||
}, []);
|
||||
|
||||
const onAddNode = useCallback(
|
||||
({ template, position }: { template: FlowModuleTemplateType; position: XYPosition }) => {
|
||||
if (!reactFlowWrapper.current) return;
|
||||
const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect();
|
||||
const mouseX = (position.x - reactFlowBounds.left - x) / zoom - 100;
|
||||
const mouseY = (position.y - reactFlowBounds.top - y) / zoom;
|
||||
|
||||
setNodes((state) =>
|
||||
state.concat(
|
||||
appModule2FlowNode({
|
||||
item: {
|
||||
...template,
|
||||
moduleId: nanoid(),
|
||||
position: { x: mouseX, y: mouseY }
|
||||
},
|
||||
onChangeNode,
|
||||
onDelNode,
|
||||
onDelEdge,
|
||||
onCopyNode,
|
||||
onCollectionNode
|
||||
})
|
||||
)
|
||||
);
|
||||
},
|
||||
[x, zoom, y]
|
||||
);
|
||||
const onDelNode = useCallback(
|
||||
(nodeId: string) => {
|
||||
setNodes((state) => state.filter((item) => item.id !== nodeId));
|
||||
setEdges((state) => state.filter((edge) => edge.source !== nodeId && edge.target !== nodeId));
|
||||
},
|
||||
[setEdges, setNodes]
|
||||
);
|
||||
const onDelEdge = useCallback(
|
||||
({
|
||||
moduleId,
|
||||
sourceHandle,
|
||||
targetHandle
|
||||
}: {
|
||||
moduleId: string;
|
||||
sourceHandle?: string;
|
||||
targetHandle?: string;
|
||||
}) => {
|
||||
if (!sourceHandle && !targetHandle) return;
|
||||
setEdges((state) =>
|
||||
state.filter((edge) => {
|
||||
if (edge.source === moduleId && edge.sourceHandle === sourceHandle) return false;
|
||||
if (edge.target === moduleId && edge.targetHandle === targetHandle) return false;
|
||||
|
||||
return true;
|
||||
})
|
||||
);
|
||||
},
|
||||
[setEdges]
|
||||
);
|
||||
const onCopyNode = useCallback(
|
||||
(nodeId: string) => {
|
||||
setNodes((nodes) => {
|
||||
const node = nodes.find((node) => node.id === nodeId);
|
||||
if (!node) return nodes;
|
||||
const template = {
|
||||
logo: node.data.logo,
|
||||
name: node.data.name,
|
||||
intro: node.data.intro,
|
||||
description: node.data.description,
|
||||
flowType: node.data.flowType,
|
||||
inputs: node.data.inputs,
|
||||
outputs: node.data.outputs,
|
||||
showStatus: node.data.showStatus
|
||||
};
|
||||
return nodes.concat(
|
||||
appModule2FlowNode({
|
||||
item: {
|
||||
...template,
|
||||
moduleId: nanoid(),
|
||||
position: { x: node.position.x + 200, y: node.position.y + 50 }
|
||||
},
|
||||
onChangeNode,
|
||||
onDelNode,
|
||||
onDelEdge,
|
||||
onCopyNode,
|
||||
onCollectionNode
|
||||
})
|
||||
);
|
||||
});
|
||||
},
|
||||
[setNodes]
|
||||
);
|
||||
const onCollectionNode = useCallback(
|
||||
(nodeId: string) => {
|
||||
console.log(nodes.find((node) => node.id === nodeId));
|
||||
},
|
||||
[nodes]
|
||||
);
|
||||
|
||||
const flow2AppModules = useCallback(() => {
|
||||
const modules: AppModuleItemType[] = nodes.map((item) => ({
|
||||
moduleId: item.data.moduleId,
|
||||
name: item.data.name,
|
||||
flowType: item.data.flowType,
|
||||
showStatus: item.data.showStatus,
|
||||
position: item.position,
|
||||
inputs: item.data.inputs.map((item) => ({
|
||||
...item,
|
||||
connected: item.type !== FlowInputItemTypeEnum.target
|
||||
})),
|
||||
outputs: item.data.outputs.map((item) => ({
|
||||
...item,
|
||||
targets: [] as FlowOutputTargetItemType[]
|
||||
}))
|
||||
}));
|
||||
|
||||
// update inputs and outputs
|
||||
modules.forEach((module) => {
|
||||
module.inputs.forEach((input) => {
|
||||
input.connected =
|
||||
input.connected ||
|
||||
!!edges.find(
|
||||
(edge) => edge.target === module.moduleId && edge.targetHandle === input.key
|
||||
);
|
||||
});
|
||||
module.outputs.forEach((output) => {
|
||||
output.targets = edges
|
||||
.filter(
|
||||
(edge) =>
|
||||
edge.source === module.moduleId &&
|
||||
edge.sourceHandle === output.key &&
|
||||
edge.targetHandle
|
||||
)
|
||||
.map((edge) => ({
|
||||
moduleId: edge.target,
|
||||
key: edge.targetHandle || ''
|
||||
}));
|
||||
});
|
||||
});
|
||||
return modules;
|
||||
}, [edges, nodes]);
|
||||
const onChangeNode = useCallback(
|
||||
({ moduleId, key, type = 'inputs', value }: FlowModuleItemChangeProps) => {
|
||||
setNodes((nodes) =>
|
||||
nodes.map((node) => {
|
||||
if (node.id !== moduleId) return node;
|
||||
if (type === 'inputs') {
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
inputs: node.data.inputs.map((item) => (item.key === key ? value : item))
|
||||
}
|
||||
};
|
||||
}
|
||||
if (type === 'addInput') {
|
||||
const input = node.data.inputs.find((input) => input.key === value.key);
|
||||
if (input) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: 'key 重复'
|
||||
});
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
inputs: node.data.inputs
|
||||
}
|
||||
};
|
||||
}
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
inputs: node.data.inputs.concat(value)
|
||||
}
|
||||
};
|
||||
}
|
||||
if (type === 'delInput') {
|
||||
onDelEdge({ moduleId, targetHandle: key });
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
inputs: node.data.inputs.filter((item) => item.key !== key)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// del output connect
|
||||
const delOutputs = node.data.outputs.filter(
|
||||
(item) => !value.find((output: FlowOutputTargetItemType) => output.key === item.key)
|
||||
);
|
||||
delOutputs.forEach((output) => {
|
||||
onDelEdge({ moduleId, sourceHandle: output.key });
|
||||
});
|
||||
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
outputs: value
|
||||
}
|
||||
};
|
||||
})
|
||||
);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const onDelConnect = useCallback((id: string) => {
|
||||
setEdges((state) => state.filter((item) => item.id !== id));
|
||||
}, []);
|
||||
const onConnect = useCallback(
|
||||
({ connect }: { connect: Connection }) => {
|
||||
const source = nodes.find((node) => node.id === connect.source)?.data;
|
||||
const sourceType = (() => {
|
||||
if (source?.flowType === FlowModuleTypeEnum.classifyQuestion) {
|
||||
return FlowValueTypeEnum.boolean;
|
||||
}
|
||||
return source?.outputs.find((output) => output.key === connect.sourceHandle)?.valueType;
|
||||
})();
|
||||
|
||||
const targetType = nodes
|
||||
.find((node) => node.id === connect.target)
|
||||
?.data?.inputs.find((input) => input.key === connect.targetHandle)?.valueType;
|
||||
|
||||
if (!sourceType || !targetType) {
|
||||
return toast({
|
||||
status: 'warning',
|
||||
title: t('app.Connection is invalid')
|
||||
});
|
||||
}
|
||||
if (
|
||||
sourceType !== FlowValueTypeEnum.any &&
|
||||
targetType !== FlowValueTypeEnum.any &&
|
||||
sourceType !== targetType
|
||||
) {
|
||||
return toast({
|
||||
status: 'warning',
|
||||
title: t('app.Connection type is different')
|
||||
});
|
||||
}
|
||||
|
||||
setEdges((state) =>
|
||||
addEdge(
|
||||
{
|
||||
...connect,
|
||||
type: 'buttonedge',
|
||||
animated: true,
|
||||
data: {
|
||||
onDelete: onDelConnect
|
||||
}
|
||||
},
|
||||
state
|
||||
)
|
||||
);
|
||||
},
|
||||
[nodes]
|
||||
);
|
||||
|
||||
const { mutate: onclickSave, isLoading } = useRequest({
|
||||
mutationFn: () => {
|
||||
return updateAppDetail(app._id, {
|
||||
modules: flow2AppModules(),
|
||||
type: AppTypeEnum.advanced
|
||||
});
|
||||
},
|
||||
successToast: '保存配置成功',
|
||||
errorToast: '保存配置异常',
|
||||
onSuccess() {
|
||||
ChatTestRef.current?.resetChatTest();
|
||||
}
|
||||
});
|
||||
|
||||
const initData = useCallback(
|
||||
(modules: AppModuleItemType[]) => {
|
||||
const edges = appModule2FlowEdge({
|
||||
modules,
|
||||
onDelete: onDelConnect
|
||||
});
|
||||
setEdges(edges);
|
||||
|
||||
setNodes(
|
||||
modules.map((item) =>
|
||||
appModule2FlowNode({
|
||||
item,
|
||||
onChangeNode,
|
||||
onDelNode,
|
||||
onDelEdge,
|
||||
onCopyNode,
|
||||
onCollectionNode
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
onFixView();
|
||||
},
|
||||
[
|
||||
onDelConnect,
|
||||
setEdges,
|
||||
setNodes,
|
||||
onFixView,
|
||||
onChangeNode,
|
||||
onDelNode,
|
||||
onDelEdge,
|
||||
onCopyNode,
|
||||
onCollectionNode
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
initData(JSON.parse(JSON.stringify(app.modules)));
|
||||
}, [app.modules]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* header */}
|
||||
<Flex
|
||||
py={3}
|
||||
px={[2, 5, 8]}
|
||||
borderBottom={theme.borders.base}
|
||||
alignItems={'center'}
|
||||
userSelect={'none'}
|
||||
>
|
||||
<MyTooltip label={'返回'} offset={[10, 10]}>
|
||||
<IconButton
|
||||
size={'sm'}
|
||||
icon={<MyIcon name={'back'} w={'14px'} />}
|
||||
borderRadius={'md'}
|
||||
borderColor={'myGray.300'}
|
||||
variant={'base'}
|
||||
aria-label={''}
|
||||
onClick={() => {
|
||||
onCloseSettings();
|
||||
onFixView();
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
<Box ml={[3, 6]} fontSize={['md', '2xl']} flex={1}>
|
||||
{app.name}
|
||||
</Box>
|
||||
|
||||
<MyTooltip label={t('app.Import Configs')}>
|
||||
<IconButton
|
||||
mr={[3, 6]}
|
||||
icon={<MyIcon name={'importLight'} w={['14px', '16px']} />}
|
||||
borderRadius={'lg'}
|
||||
variant={'base'}
|
||||
aria-label={'save'}
|
||||
onClick={onOpenImport}
|
||||
/>
|
||||
</MyTooltip>
|
||||
<MyTooltip label={t('app.Export Configs')}>
|
||||
<IconButton
|
||||
mr={[3, 6]}
|
||||
icon={<MyIcon name={'export'} w={['14px', '16px']} />}
|
||||
borderRadius={'lg'}
|
||||
variant={'base'}
|
||||
aria-label={'save'}
|
||||
onClick={() =>
|
||||
copyData(
|
||||
JSON.stringify(flow2AppModules(), null, 2),
|
||||
t('app.Export Config Successful')
|
||||
)
|
||||
}
|
||||
/>
|
||||
</MyTooltip>
|
||||
|
||||
{testModules ? (
|
||||
<IconButton
|
||||
mr={[3, 6]}
|
||||
icon={<SmallCloseIcon fontSize={'25px'} />}
|
||||
variant={'base'}
|
||||
color={'myGray.600'}
|
||||
borderRadius={'lg'}
|
||||
aria-label={''}
|
||||
onClick={() => setTestModules(undefined)}
|
||||
/>
|
||||
) : (
|
||||
<MyTooltip label={'测试对话'}>
|
||||
<IconButton
|
||||
mr={[3, 6]}
|
||||
icon={<MyIcon name={'chat'} w={['14px', '16px']} />}
|
||||
borderRadius={'lg'}
|
||||
aria-label={'save'}
|
||||
variant={'base'}
|
||||
onClick={() => {
|
||||
setTestModules(flow2AppModules());
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
)}
|
||||
|
||||
<MyTooltip label={'保存配置'}>
|
||||
<IconButton
|
||||
icon={<MyIcon name={'save'} w={['14px', '16px']} />}
|
||||
borderRadius={'lg'}
|
||||
isLoading={isLoading}
|
||||
aria-label={'save'}
|
||||
onClick={onclickSave}
|
||||
/>
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
<Box
|
||||
minH={'400px'}
|
||||
flex={'1 0 0'}
|
||||
w={'100%'}
|
||||
h={0}
|
||||
position={'relative'}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
{/* open module template */}
|
||||
<IconButton
|
||||
position={'absolute'}
|
||||
top={5}
|
||||
left={5}
|
||||
w={'38px'}
|
||||
h={'38px'}
|
||||
borderRadius={'50%'}
|
||||
icon={<SmallCloseIcon fontSize={'26px'} />}
|
||||
transform={isOpenTemplate ? '' : 'rotate(135deg)'}
|
||||
transition={'0.2s ease'}
|
||||
aria-label={''}
|
||||
zIndex={1}
|
||||
boxShadow={'2px 2px 6px #85b1ff'}
|
||||
onClick={() => {
|
||||
isOpenTemplate ? onCloseTemplate() : onOpenTemplate();
|
||||
}}
|
||||
/>
|
||||
|
||||
<ReactFlow
|
||||
ref={reactFlowWrapper}
|
||||
className={styles.panel}
|
||||
fitView
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
minZoom={0.4}
|
||||
maxZoom={1.5}
|
||||
defaultEdgeOptions={edgeOptions}
|
||||
connectionLineStyle={connectionLineStyle}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={(connect) => {
|
||||
connect.sourceHandle &&
|
||||
connect.targetHandle &&
|
||||
onConnect({
|
||||
connect
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Background />
|
||||
<Controls position={'bottom-right'} style={{ display: 'flex' }} showInteractive={false} />
|
||||
</ReactFlow>
|
||||
|
||||
<TemplateList
|
||||
isOpen={isOpenTemplate}
|
||||
nodes={nodes}
|
||||
onAddNode={onAddNode}
|
||||
onClose={onCloseTemplate}
|
||||
/>
|
||||
<ChatTest
|
||||
ref={ChatTestRef}
|
||||
modules={testModules}
|
||||
app={app}
|
||||
onClose={() => setTestModules(undefined)}
|
||||
/>
|
||||
</Box>
|
||||
{isOpenImport && (
|
||||
<ImportSettings
|
||||
onClose={onCloseImport}
|
||||
onSuccess={(data) => {
|
||||
setEdges([]);
|
||||
setNodes([]);
|
||||
setTimeout(() => {
|
||||
initData(data);
|
||||
}, 10);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Flow = (data: Props) => (
|
||||
<Box h={'100%'} position={'fixed'} zIndex={999} top={0} left={0} right={0} bottom={0}>
|
||||
<ReactFlowProvider>
|
||||
<Flex h={'100%'} flexDirection={'column'} bg={'#fff'}>
|
||||
{!!data.app._id && <AppEdit {...data} />}
|
||||
</Flex>
|
||||
</ReactFlowProvider>
|
||||
</Box>
|
||||
);
|
||||
|
||||
export default React.memo(Flow);
|
||||
@@ -1,242 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Flex,
|
||||
Box,
|
||||
Button,
|
||||
ModalBody,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
useTheme,
|
||||
Textarea
|
||||
} from '@chakra-ui/react';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import { KbListItemType } from '@/types/plugin';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import type { SelectedKbType } from '@/types/plugin';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import MySlider from '@/components/Slider';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import MyModal from '@/components/MyModal';
|
||||
import MyIcon from '@/components/Icon';
|
||||
|
||||
export type KbParamsType = {
|
||||
searchSimilarity: number;
|
||||
searchLimit: number;
|
||||
searchEmptyText: string;
|
||||
};
|
||||
|
||||
export const KBSelectModal = ({
|
||||
kbList,
|
||||
activeKbs = [],
|
||||
onChange,
|
||||
onClose
|
||||
}: {
|
||||
kbList: KbListItemType[];
|
||||
activeKbs: SelectedKbType;
|
||||
onChange: (e: SelectedKbType) => void;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const [selectedKbList, setSelectedKbList] = useState<SelectedKbType>(activeKbs);
|
||||
const { isPc } = useGlobalStore();
|
||||
const { toast } = useToast();
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen={true}
|
||||
isCentered={!isPc}
|
||||
maxW={['90vw', '800px']}
|
||||
w={'800px'}
|
||||
onClose={onClose}
|
||||
>
|
||||
<Flex flexDirection={'column'} h={['90vh', 'auto']}>
|
||||
<ModalHeader>
|
||||
<Box>关联的知识库({selectedKbList.length})</Box>
|
||||
<Box fontSize={'sm'} color={'myGray.500'} fontWeight={'normal'}>
|
||||
仅能选择同一个索引模型的知识库
|
||||
</Box>
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody
|
||||
flex={['1 0 0', '0 0 auto']}
|
||||
maxH={'80vh'}
|
||||
overflowY={'auto'}
|
||||
display={'grid'}
|
||||
gridTemplateColumns={['repeat(1,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)']}
|
||||
gridGap={3}
|
||||
userSelect={'none'}
|
||||
>
|
||||
{kbList.map((item) =>
|
||||
(() => {
|
||||
const selected = !!selectedKbList.find((kb) => kb.kbId === item._id);
|
||||
const active = !!activeKbs.find((kb) => kb.kbId === item._id);
|
||||
return (
|
||||
<Card
|
||||
key={item._id}
|
||||
p={3}
|
||||
border={theme.borders.base}
|
||||
boxShadow={'sm'}
|
||||
h={'80px'}
|
||||
cursor={'pointer'}
|
||||
order={active ? 0 : 1}
|
||||
_hover={{
|
||||
boxShadow: 'md'
|
||||
}}
|
||||
{...(selected
|
||||
? {
|
||||
bg: 'myBlue.300'
|
||||
}
|
||||
: {})}
|
||||
onClick={() => {
|
||||
if (selected) {
|
||||
setSelectedKbList((state) => state.filter((kb) => kb.kbId !== item._id));
|
||||
} else {
|
||||
const vectorModel = selectedKbList[0]?.vectorModel?.model;
|
||||
|
||||
if (vectorModel && vectorModel !== item.vectorModel.model) {
|
||||
return toast({
|
||||
status: 'warning',
|
||||
title: '仅能选择同一个索引模型的知识库'
|
||||
});
|
||||
}
|
||||
setSelectedKbList((state) => [
|
||||
...state,
|
||||
{ kbId: item._id, vectorModel: item.vectorModel }
|
||||
]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Flex alignItems={'center'} h={'38px'}>
|
||||
<Avatar src={item.avatar} w={['24px', '28px', '32px']}></Avatar>
|
||||
<Box ml={3} fontWeight={'bold'} fontSize={['md', 'lg', 'xl']}>
|
||||
{item.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex justifyContent={'flex-end'} alignItems={'center'} fontSize={'sm'}>
|
||||
<MyIcon mr={1} name="kbTest" w={'12px'} />
|
||||
<Box color={'myGray.500'}>{item.vectorModel.name}</Box>
|
||||
</Flex>
|
||||
</Card>
|
||||
);
|
||||
})()
|
||||
)}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onClose();
|
||||
onChange(selectedKbList);
|
||||
}}
|
||||
>
|
||||
完成
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Flex>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export const KbParamsModal = ({
|
||||
searchEmptyText,
|
||||
searchLimit,
|
||||
searchSimilarity,
|
||||
onClose,
|
||||
onChange
|
||||
}: KbParamsType & { onClose: () => void; onChange: (e: KbParamsType) => void }) => {
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
const { register, setValue, getValues, handleSubmit } = useForm<KbParamsType>({
|
||||
defaultValues: {
|
||||
searchEmptyText,
|
||||
searchLimit,
|
||||
searchSimilarity
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<MyModal isOpen={true} onClose={onClose} title={'搜索参数调整'} minW={['90vw', '600px']}>
|
||||
<Flex flexDirection={'column'}>
|
||||
<ModalBody>
|
||||
<Box display={['block', 'flex']} py={5} pt={[0, 5]}>
|
||||
<Box flex={'0 0 100px'} mb={[8, 0]}>
|
||||
相似度
|
||||
<MyTooltip
|
||||
label={'不同索引模型的相似度有区别,请通过搜索测试来选择合适的数值'}
|
||||
forceShow
|
||||
>
|
||||
<QuestionOutlineIcon ml={1} />
|
||||
</MyTooltip>
|
||||
</Box>
|
||||
<MySlider
|
||||
markList={[
|
||||
{ label: '0', value: 0 },
|
||||
{ label: '1', value: 1 }
|
||||
]}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
value={getValues('searchSimilarity')}
|
||||
onChange={(val) => {
|
||||
setValue('searchSimilarity', val);
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box display={['block', 'flex']} py={8}>
|
||||
<Box flex={'0 0 100px'} mb={[8, 0]}>
|
||||
单次搜索数量
|
||||
</Box>
|
||||
<Box flex={1}>
|
||||
<MySlider
|
||||
markList={[
|
||||
{ label: '1', value: 1 },
|
||||
{ label: '20', value: 20 }
|
||||
]}
|
||||
min={1}
|
||||
max={20}
|
||||
value={getValues('searchLimit')}
|
||||
onChange={(val) => {
|
||||
setValue('searchLimit', val);
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box display={['block', 'flex']} pt={3}>
|
||||
<Box flex={'0 0 100px'} mb={[2, 0]}>
|
||||
空搜索回复
|
||||
</Box>
|
||||
<Box flex={1}>
|
||||
<Textarea
|
||||
rows={5}
|
||||
maxLength={500}
|
||||
placeholder={
|
||||
'若填写该内容,没有搜索到对应内容时,将直接回复填写的内容。\n为了连贯上下文,FastGpt 会取部分上一个聊天的搜索记录作为补充,因此在连续对话时,该功能可能会失效。'
|
||||
}
|
||||
{...register('searchEmptyText')}
|
||||
></Textarea>
|
||||
</Box>
|
||||
</Box>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant={'base'} mr={3} onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onClose();
|
||||
handleSubmit(onChange)();
|
||||
}}
|
||||
>
|
||||
完成
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Flex>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default KBSelectModal;
|
||||
@@ -1,263 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Flex,
|
||||
Box,
|
||||
Button,
|
||||
TableContainer,
|
||||
Table,
|
||||
Thead,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
Tbody,
|
||||
useDisclosure,
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
FormControl,
|
||||
Input,
|
||||
useTheme
|
||||
} from '@chakra-ui/react';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import { useLoading } from '@/hooks/useLoading';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getShareChatList, delShareChatById, createShareChat } from '@/api/chat';
|
||||
import { formatTimeToChatTime, useCopyData } from '@/utils/tools';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { defaultShareChat } from '@/constants/model';
|
||||
import type { ShareChatEditType } from '@/types/app';
|
||||
import { useRequest } from '@/hooks/useRequest';
|
||||
import { formatPrice } from '@/utils/user';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import MyModal from '@/components/MyModal';
|
||||
import MyRadio from '@/components/Radio';
|
||||
|
||||
const Share = ({ appId }: { appId: string }) => {
|
||||
const { Loading, setIsLoading } = useLoading();
|
||||
const { copyData } = useCopyData();
|
||||
const {
|
||||
isOpen: isOpenCreateShareChat,
|
||||
onOpen: onOpenCreateShareChat,
|
||||
onClose: onCloseCreateShareChat
|
||||
} = useDisclosure();
|
||||
const {
|
||||
register: registerShareChat,
|
||||
getValues: getShareChatValues,
|
||||
setValue: setShareChatValues,
|
||||
handleSubmit: submitShareChat,
|
||||
reset: resetShareChat
|
||||
} = useForm({
|
||||
defaultValues: defaultShareChat
|
||||
});
|
||||
|
||||
const {
|
||||
isFetching,
|
||||
data: shareChatList = [],
|
||||
refetch: refetchShareChatList
|
||||
} = useQuery(['initShareChatList', appId], () => getShareChatList(appId));
|
||||
|
||||
const { mutate: onclickCreateShareChat, isLoading: creating } = useRequest({
|
||||
mutationFn: async (e: ShareChatEditType) =>
|
||||
createShareChat({
|
||||
...e,
|
||||
appId
|
||||
}),
|
||||
errorToast: '创建分享链接异常',
|
||||
onSuccess(id) {
|
||||
onCloseCreateShareChat();
|
||||
refetchShareChatList();
|
||||
const url = `${location.origin}/chat/share?shareId=${id}`;
|
||||
copyData(url, '创建成功。已复制分享地址,可直接分享使用');
|
||||
resetShareChat(defaultShareChat);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Box position={'relative'} pt={[3, 5, 8]} px={[5, 8]} minH={'50vh'}>
|
||||
<Flex justifyContent={'space-between'}>
|
||||
<Box fontWeight={'bold'}>
|
||||
免登录窗口
|
||||
<MyTooltip
|
||||
forceShow
|
||||
label="可以直接分享该模型给其他用户去进行对话,对方无需登录即可直接进行对话。注意,这个功能会消耗你账号的tokens。请保管好链接和密码。"
|
||||
>
|
||||
<QuestionOutlineIcon ml={1} />
|
||||
</MyTooltip>
|
||||
</Box>
|
||||
<Button
|
||||
variant={'base'}
|
||||
colorScheme={'myBlue'}
|
||||
size={['sm', 'md']}
|
||||
{...(shareChatList.length >= 10
|
||||
? {
|
||||
isDisabled: true,
|
||||
title: '最多创建10组'
|
||||
}
|
||||
: {})}
|
||||
onClick={onOpenCreateShareChat}
|
||||
>
|
||||
创建新链接
|
||||
</Button>
|
||||
</Flex>
|
||||
<TableContainer mt={3}>
|
||||
<Table variant={'simple'} w={'100%'} overflowX={'auto'}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>名称</Th>
|
||||
<Th>金额消耗</Th>
|
||||
<Th>最后使用时间</Th>
|
||||
<Th>操作</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{shareChatList.map((item) => (
|
||||
<Tr key={item._id}>
|
||||
<Td>{item.name}</Td>
|
||||
<Td>{formatPrice(item.total)}元</Td>
|
||||
<Td>{item.lastTime ? formatTimeToChatTime(item.lastTime) : '未使用'}</Td>
|
||||
<Td display={'flex'} alignItems={'center'}>
|
||||
<MyTooltip label={'嵌入网页'}>
|
||||
<MyIcon
|
||||
mr={4}
|
||||
name="apiLight"
|
||||
w={'14px'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'myBlue.600' }}
|
||||
onClick={() => {
|
||||
const url = `${location.origin}/chat/share?shareId=${item.shareId}`;
|
||||
const src = `${location.origin}/js/iframe.js`;
|
||||
const script = `<script src="${src}" id="fastgpt-iframe" data-src="${url}" data-color="#4e83fd"></script>`;
|
||||
copyData(script, '已复制嵌入 Script,可在应用 HTML 底部嵌入', 3000);
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
<MyTooltip label={'复制分享链接'}>
|
||||
<MyIcon
|
||||
mr={4}
|
||||
name="copy"
|
||||
w={'14px'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'myBlue.600' }}
|
||||
onClick={() => {
|
||||
const url = `${location.origin}/chat/share?shareId=${item.shareId}`;
|
||||
copyData(url, '已复制分享链接,可直接分享使用');
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
<MyTooltip label={'删除链接'}>
|
||||
<MyIcon
|
||||
name="delete"
|
||||
w={'14px'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'red' }}
|
||||
onClick={async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await delShareChatById(item._id);
|
||||
refetchShareChatList();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{shareChatList.length === 0 && !isFetching && (
|
||||
<Flex h={'100%'} flexDirection={'column'} alignItems={'center'} pt={'10vh'}>
|
||||
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
|
||||
<Box mt={2} color={'myGray.500'}>
|
||||
没有创建分享链接
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
{/* create shareChat modal */}
|
||||
<MyModal
|
||||
isOpen={isOpenCreateShareChat}
|
||||
onClose={onCloseCreateShareChat}
|
||||
title={'创建免登录窗口'}
|
||||
>
|
||||
<ModalBody>
|
||||
<FormControl>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box flex={'0 0 60px'} w={0}>
|
||||
名称:
|
||||
</Box>
|
||||
<Input
|
||||
placeholder="记录名字,仅用于展示"
|
||||
maxLength={20}
|
||||
{...registerShareChat('name', {
|
||||
required: '记录名称不能为空'
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button variant={'base'} mr={3} onClick={onCloseCreateShareChat}>
|
||||
取消
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
isLoading={creating}
|
||||
onClick={submitShareChat((data) => onclickCreateShareChat(data))}
|
||||
>
|
||||
确认
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
<Loading loading={isFetching} fixed={false} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
enum LinkTypeEnum {
|
||||
share = 'share',
|
||||
iframe = 'iframe'
|
||||
}
|
||||
|
||||
const OutLink = ({ appId }: { appId: string }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const [linkType, setLinkType] = useState<`${LinkTypeEnum}`>(LinkTypeEnum.share);
|
||||
|
||||
return (
|
||||
<Box pt={[1, 5]}>
|
||||
<Box fontWeight={'bold'} fontSize={['md', 'xl']} mb={2} px={[4, 8]}>
|
||||
外部使用途径
|
||||
</Box>
|
||||
<Box pb={[5, 7]} px={[4, 8]} borderBottom={theme.borders.base}>
|
||||
<MyRadio
|
||||
gridTemplateColumns={['repeat(1,1fr)', 'repeat(auto-fill, minmax(0, 360px))']}
|
||||
iconSize={'20px'}
|
||||
list={[
|
||||
{
|
||||
icon: 'outlink_share',
|
||||
title: '免登录窗口',
|
||||
desc: '分享链接给其他用户,无需登录即可直接进行使用',
|
||||
value: LinkTypeEnum.share
|
||||
}
|
||||
// {
|
||||
// icon: 'outlink_iframe',
|
||||
// title: '网页嵌入',
|
||||
// desc: '嵌入到已有网页中,右下角会生成对话按键',
|
||||
// value: LinkTypeEnum.iframe
|
||||
// }
|
||||
]}
|
||||
value={linkType}
|
||||
onChange={(e) => setLinkType(e as `${LinkTypeEnum}`)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{linkType === LinkTypeEnum.share && <Share appId={appId} />}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default OutLink;
|
||||
@@ -1,40 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
|
||||
import { serviceSideProps } from '@/utils/i18n';
|
||||
|
||||
import Navbar from './components/Navbar';
|
||||
import Hero from './components/Hero';
|
||||
import Ability from './components/Ability';
|
||||
import Choice from './components/Choice';
|
||||
import Footer from './components/Footer';
|
||||
|
||||
const Home = () => {
|
||||
return (
|
||||
<Box id="home" bg={'myWhite.600'} h={'100vh'} overflowY={'auto'} overflowX={'hidden'}>
|
||||
<Box position={'fixed'} zIndex={10} top={0} left={0} right={0}>
|
||||
<Navbar />
|
||||
</Box>
|
||||
<Box maxW={'1200px'} pt={'70px'} m={'auto'}>
|
||||
<Hero />
|
||||
<Ability />
|
||||
<Box my={[4, 6]}>
|
||||
<Choice />
|
||||
</Box>
|
||||
</Box>
|
||||
<Box bg={'white'}>
|
||||
<Footer />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export async function getServerSideProps(content: any) {
|
||||
return {
|
||||
props: {
|
||||
...(await serviceSideProps(content))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default Home;
|
||||
@@ -1,190 +0,0 @@
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { Box, Flex, IconButton, useTheme } from '@chakra-ui/react';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { KbItemType } from '@/types/plugin';
|
||||
import { getErrText } from '@/utils/tools';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { type ComponentRef } from './components/Info';
|
||||
import Tabs from '@/components/Tabs';
|
||||
import dynamic from 'next/dynamic';
|
||||
import DataCard from './components/DataCard';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import SideTabs from '@/components/SideTabs';
|
||||
import PageContainer from '@/components/PageContainer';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import Info from './components/Info';
|
||||
import { serviceSideProps } from '@/utils/i18n';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getTrainingQueueLen } from '@/api/plugins/kb';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
|
||||
const ImportData = dynamic(() => import('./components/Import'), {
|
||||
ssr: false
|
||||
});
|
||||
const Test = dynamic(() => import('./components/Test'), {
|
||||
ssr: false
|
||||
});
|
||||
|
||||
enum TabEnum {
|
||||
data = 'data',
|
||||
import = 'import',
|
||||
test = 'test',
|
||||
info = 'info'
|
||||
}
|
||||
|
||||
const Detail = ({ kbId, currentTab }: { kbId: string; currentTab: `${TabEnum}` }) => {
|
||||
const InfoRef = useRef<ComponentRef>(null);
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const { isPc } = useGlobalStore();
|
||||
const { kbDetail, getKbDetail } = useUserStore();
|
||||
|
||||
const tabList = useRef([
|
||||
{ label: '数据集', id: TabEnum.data, icon: 'overviewLight' },
|
||||
{ label: '导入数据', id: TabEnum.import, icon: 'importLight' },
|
||||
{ label: '搜索测试', id: TabEnum.test, icon: 'kbTest' },
|
||||
{ label: '配置', id: TabEnum.info, icon: 'settingLight' }
|
||||
]);
|
||||
|
||||
const setCurrentTab = useCallback(
|
||||
(tab: `${TabEnum}`) => {
|
||||
router.replace({
|
||||
query: {
|
||||
kbId,
|
||||
currentTab: tab
|
||||
}
|
||||
});
|
||||
},
|
||||
[kbId, router]
|
||||
);
|
||||
|
||||
const form = useForm<KbItemType>({
|
||||
defaultValues: kbDetail
|
||||
});
|
||||
|
||||
useQuery([kbId], () => getKbDetail(kbId), {
|
||||
onSuccess(res) {
|
||||
form.reset(res);
|
||||
InfoRef.current?.initInput(res.tags);
|
||||
},
|
||||
onError(err: any) {
|
||||
router.replace(`/kb/list`);
|
||||
toast({
|
||||
title: getErrText(err, '获取知识库异常'),
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const { data: trainingQueueLen = 0 } = useQuery(['getTrainingQueueLen'], getTrainingQueueLen, {
|
||||
refetchInterval: 5000
|
||||
});
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<Box display={['block', 'flex']} h={'100%'} pt={[4, 0]}>
|
||||
{isPc ? (
|
||||
<Flex
|
||||
flexDirection={'column'}
|
||||
p={4}
|
||||
h={'100%'}
|
||||
flex={'0 0 200px'}
|
||||
borderRight={theme.borders.base}
|
||||
>
|
||||
<Flex mb={4} alignItems={'center'}>
|
||||
<Avatar src={kbDetail.avatar} w={'34px'} borderRadius={'lg'} />
|
||||
<Box ml={2} fontWeight={'bold'}>
|
||||
{kbDetail.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
<SideTabs
|
||||
flex={1}
|
||||
mx={'auto'}
|
||||
mt={2}
|
||||
w={'100%'}
|
||||
list={tabList.current}
|
||||
activeId={currentTab}
|
||||
onChange={(e: any) => {
|
||||
setCurrentTab(e);
|
||||
}}
|
||||
/>
|
||||
<Box textAlign={'center'}>
|
||||
<Flex justifyContent={'center'} alignItems={'center'}>
|
||||
<MyIcon mr={1} name="overviewLight" w={'16px'} color={'green.500'} />
|
||||
<Box>{t('dataset.System Data Queue')}</Box>
|
||||
<MyTooltip label={t('dataset.Queue Desc')} placement={'top'}>
|
||||
<QuestionOutlineIcon ml={1} w={'16px'} />
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
<Box mt={1} fontWeight={'bold'}>
|
||||
{trainingQueueLen}
|
||||
</Box>
|
||||
</Box>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
py={2}
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
_hover={{ bg: 'myGray.100' }}
|
||||
onClick={() => router.replace('/kb/list')}
|
||||
>
|
||||
<IconButton
|
||||
mr={3}
|
||||
icon={<MyIcon name={'backFill'} w={'18px'} color={'myBlue.600'} />}
|
||||
bg={'white'}
|
||||
boxShadow={'1px 1px 9px rgba(0,0,0,0.15)'}
|
||||
h={'28px'}
|
||||
size={'sm'}
|
||||
borderRadius={'50%'}
|
||||
aria-label={''}
|
||||
/>
|
||||
全部知识库
|
||||
</Flex>
|
||||
</Flex>
|
||||
) : (
|
||||
<Box mb={3}>
|
||||
<Tabs
|
||||
m={'auto'}
|
||||
w={'260px'}
|
||||
size={isPc ? 'md' : 'sm'}
|
||||
list={tabList.current.map((item) => ({
|
||||
id: item.id,
|
||||
label: item.label
|
||||
}))}
|
||||
activeId={currentTab}
|
||||
onChange={(e: any) => setCurrentTab(e)}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{!!kbDetail._id && (
|
||||
<Box flex={'1 0 0'} h={'100%'} pb={[4, 0]}>
|
||||
{currentTab === TabEnum.data && <DataCard kbId={kbId} />}
|
||||
{currentTab === TabEnum.import && <ImportData kbId={kbId} />}
|
||||
{currentTab === TabEnum.test && <Test kbId={kbId} />}
|
||||
{currentTab === TabEnum.info && <Info ref={InfoRef} kbId={kbId} form={form} />}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export async function getServerSideProps(context: any) {
|
||||
const currentTab = context?.query?.currentTab || TabEnum.data;
|
||||
const kbId = context?.query?.kbId;
|
||||
|
||||
return {
|
||||
props: { currentTab, kbId, ...(await serviceSideProps(context)) }
|
||||
};
|
||||
}
|
||||
|
||||
export default React.memo(Detail);
|
||||
@@ -1,171 +0,0 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
Flex,
|
||||
Grid,
|
||||
useTheme,
|
||||
Button,
|
||||
IconButton,
|
||||
useDisclosure
|
||||
} from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import PageContainer from '@/components/PageContainer';
|
||||
import { useConfirm } from '@/hooks/useConfirm';
|
||||
import { AddIcon } from '@chakra-ui/icons';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { delKbById } from '@/api/plugins/kb';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import Tag from '@/components/Tag';
|
||||
import { serviceSideProps } from '@/utils/i18n';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const CreateModal = dynamic(() => import('./component/CreateModal'), { ssr: false });
|
||||
|
||||
const Kb = () => {
|
||||
const theme = useTheme();
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
const { openConfirm, ConfirmModal } = useConfirm({
|
||||
title: '删除提示',
|
||||
content: '确认删除该知识库?'
|
||||
});
|
||||
const { myKbList, loadKbList, setKbList } = useUserStore();
|
||||
|
||||
const {
|
||||
isOpen: isOpenCreateModal,
|
||||
onOpen: onOpenCreateModal,
|
||||
onClose: onCloseCreateModal
|
||||
} = useDisclosure();
|
||||
|
||||
const { refetch } = useQuery(['loadKbList'], () => loadKbList());
|
||||
|
||||
/* 点击删除 */
|
||||
const onclickDelKb = useCallback(
|
||||
async (id: string) => {
|
||||
try {
|
||||
delKbById(id);
|
||||
toast({
|
||||
title: '删除成功',
|
||||
status: 'success'
|
||||
});
|
||||
setKbList(myKbList.filter((item) => item._id !== id));
|
||||
} catch (err: any) {
|
||||
toast({
|
||||
title: err?.message || '删除失败',
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
},
|
||||
[toast, setKbList, myKbList]
|
||||
);
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<Flex pt={3} px={5} alignItems={'center'}>
|
||||
<Box flex={1} className="textlg" letterSpacing={1} fontSize={'24px'} fontWeight={'bold'}>
|
||||
我的知识库
|
||||
</Box>
|
||||
<Button leftIcon={<AddIcon />} variant={'base'} onClick={onOpenCreateModal}>
|
||||
新建
|
||||
</Button>
|
||||
</Flex>
|
||||
<Grid
|
||||
p={5}
|
||||
gridTemplateColumns={['1fr', 'repeat(3,1fr)', 'repeat(4,1fr)', 'repeat(5,1fr)']}
|
||||
gridGap={5}
|
||||
>
|
||||
{myKbList.map((kb) => (
|
||||
<Card
|
||||
display={'flex'}
|
||||
flexDirection={'column'}
|
||||
key={kb._id}
|
||||
py={4}
|
||||
px={5}
|
||||
cursor={'pointer'}
|
||||
h={'140px'}
|
||||
border={theme.borders.md}
|
||||
boxShadow={'none'}
|
||||
userSelect={'none'}
|
||||
position={'relative'}
|
||||
_hover={{
|
||||
boxShadow: '1px 1px 10px rgba(0,0,0,0.2)',
|
||||
borderColor: 'transparent',
|
||||
'& .delete': {
|
||||
display: 'block'
|
||||
}
|
||||
}}
|
||||
onClick={() =>
|
||||
router.push({
|
||||
pathname: '/kb/detail',
|
||||
query: {
|
||||
kbId: kb._id
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
<Flex alignItems={'center'} h={'38px'}>
|
||||
<Avatar src={kb.avatar} borderRadius={'lg'} w={'28px'} />
|
||||
<Box ml={3}>{kb.name}</Box>
|
||||
<IconButton
|
||||
className="delete"
|
||||
position={'absolute'}
|
||||
top={4}
|
||||
right={4}
|
||||
size={'sm'}
|
||||
icon={<MyIcon name={'delete'} w={'14px'} />}
|
||||
variant={'base'}
|
||||
borderRadius={'md'}
|
||||
aria-label={'delete'}
|
||||
display={['', 'none']}
|
||||
_hover={{
|
||||
bg: 'red.100'
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openConfirm(() => onclickDelKb(kb._id))();
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
<Box flex={'1 0 0'} overflow={'hidden'} pt={2}>
|
||||
<Flex>
|
||||
{kb.tags.map((tag, i) => (
|
||||
<Tag key={i} mr={2} mb={2}>
|
||||
{tag}
|
||||
</Tag>
|
||||
))}
|
||||
</Flex>
|
||||
</Box>
|
||||
<Flex justifyContent={'flex-end'} alignItems={'center'} fontSize={'sm'}>
|
||||
<MyIcon mr={1} name="kbTest" w={'12px'} />
|
||||
<Box color={'myGray.500'}>{kb.vectorModel.name}</Box>
|
||||
</Flex>
|
||||
</Card>
|
||||
))}
|
||||
</Grid>
|
||||
{myKbList.length === 0 && (
|
||||
<Flex mt={'35vh'} flexDirection={'column'} alignItems={'center'}>
|
||||
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
|
||||
<Box mt={2} color={'myGray.500'}>
|
||||
还没有知识库,快去创建一个吧!
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
<ConfirmModal />
|
||||
{isOpenCreateModal && <CreateModal onClose={onCloseCreateModal} />}
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export async function getServerSideProps(content: any) {
|
||||
return {
|
||||
props: {
|
||||
...(await serviceSideProps(content))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default Kb;
|
||||
@@ -1,28 +0,0 @@
|
||||
import { UserModelSchema } from '@/types/mongoSchema';
|
||||
import { Configuration, OpenAIApi } from 'openai';
|
||||
|
||||
export const openaiBaseUrl = process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1';
|
||||
export const baseUrl = process.env.ONEAPI_URL || openaiBaseUrl;
|
||||
|
||||
export const systemAIChatKey = process.env.CHAT_API_KEY || '';
|
||||
|
||||
export const getAIChatApi = (props?: UserModelSchema['openaiAccount']) => {
|
||||
return new OpenAIApi(
|
||||
new Configuration({
|
||||
basePath: props?.baseUrl || baseUrl,
|
||||
apiKey: props?.key || systemAIChatKey
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
/* openai axios config */
|
||||
export const axiosConfig = (props?: UserModelSchema['openaiAccount']) => {
|
||||
return {
|
||||
baseURL: props?.baseUrl || baseUrl, // 此处仅对非 npm 模块有效
|
||||
httpsAgent: global.httpsAgent,
|
||||
headers: {
|
||||
Authorization: `Bearer ${props?.key || systemAIChatKey}`,
|
||||
auth: process.env.OPENAI_BASE_URL_AUTH || ''
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -1,175 +0,0 @@
|
||||
import { connectToDatabase, Bill, User, OutLink } from '../mongo';
|
||||
import { BillSourceEnum } from '@/constants/user';
|
||||
import { getModel } from '../utils/data';
|
||||
import { ChatHistoryItemResType } from '@/types/chat';
|
||||
import { formatPrice } from '@/utils/user';
|
||||
import { addLog } from '../utils/tools';
|
||||
|
||||
export const pushTaskBill = async ({
|
||||
appName,
|
||||
appId,
|
||||
userId,
|
||||
source,
|
||||
shareId,
|
||||
response
|
||||
}: {
|
||||
appName: string;
|
||||
appId: string;
|
||||
userId: string;
|
||||
source: `${BillSourceEnum}`;
|
||||
shareId?: string;
|
||||
response: ChatHistoryItemResType[];
|
||||
}) => {
|
||||
const total = response.reduce((sum, item) => sum + item.price, 0);
|
||||
|
||||
await Promise.allSettled([
|
||||
Bill.create({
|
||||
userId,
|
||||
appName,
|
||||
appId,
|
||||
total,
|
||||
source,
|
||||
list: response.map((item) => ({
|
||||
moduleName: item.moduleName,
|
||||
amount: item.price || 0,
|
||||
model: item.model,
|
||||
tokenLen: item.tokens
|
||||
}))
|
||||
}),
|
||||
User.findByIdAndUpdate(userId, {
|
||||
$inc: { balance: -total }
|
||||
}),
|
||||
...(shareId
|
||||
? [
|
||||
updateShareChatBill({
|
||||
shareId,
|
||||
total
|
||||
})
|
||||
]
|
||||
: [])
|
||||
]);
|
||||
|
||||
addLog.info(`finish completions`, {
|
||||
source,
|
||||
userId,
|
||||
price: formatPrice(total)
|
||||
});
|
||||
};
|
||||
|
||||
export const updateShareChatBill = async ({
|
||||
shareId,
|
||||
total
|
||||
}: {
|
||||
shareId: string;
|
||||
total: number;
|
||||
}) => {
|
||||
try {
|
||||
await OutLink.findOneAndUpdate(
|
||||
{ shareId },
|
||||
{
|
||||
$inc: { total },
|
||||
lastTime: new Date()
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
addLog.error('update shareChat error', { err });
|
||||
}
|
||||
};
|
||||
|
||||
export const pushQABill = async ({
|
||||
userId,
|
||||
totalTokens,
|
||||
appName
|
||||
}: {
|
||||
userId: string;
|
||||
totalTokens: number;
|
||||
appName: string;
|
||||
}) => {
|
||||
addLog.info('splitData generate success', { totalTokens });
|
||||
|
||||
let billId;
|
||||
|
||||
try {
|
||||
await connectToDatabase();
|
||||
|
||||
// 获取模型单价格, 都是用 gpt35 拆分
|
||||
const unitPrice = global.qaModel.price || 3;
|
||||
// 计算价格
|
||||
const total = unitPrice * totalTokens;
|
||||
|
||||
// 插入 Bill 记录
|
||||
const res = await Bill.create({
|
||||
userId,
|
||||
appName,
|
||||
tokenLen: totalTokens,
|
||||
total
|
||||
});
|
||||
billId = res._id;
|
||||
|
||||
// 账号扣费
|
||||
await User.findByIdAndUpdate(userId, {
|
||||
$inc: { balance: -total }
|
||||
});
|
||||
} catch (err) {
|
||||
addLog.error('Create completions bill error', { err });
|
||||
billId && Bill.findByIdAndDelete(billId);
|
||||
}
|
||||
};
|
||||
|
||||
export const pushGenerateVectorBill = async ({
|
||||
userId,
|
||||
tokenLen,
|
||||
model
|
||||
}: {
|
||||
userId: string;
|
||||
tokenLen: number;
|
||||
model: string;
|
||||
}) => {
|
||||
let billId;
|
||||
|
||||
try {
|
||||
await connectToDatabase();
|
||||
|
||||
try {
|
||||
// 计算价格. 至少为1
|
||||
const vectorModel =
|
||||
global.vectorModels.find((item) => item.model === model) || global.vectorModels[0];
|
||||
const unitPrice = vectorModel.price || 0.2;
|
||||
let total = unitPrice * tokenLen;
|
||||
total = total > 1 ? total : 1;
|
||||
|
||||
// 插入 Bill 记录
|
||||
const res = await Bill.create({
|
||||
userId,
|
||||
model: vectorModel.model,
|
||||
appName: '索引生成',
|
||||
total,
|
||||
list: [
|
||||
{
|
||||
moduleName: '索引生成',
|
||||
amount: total,
|
||||
model: vectorModel.model,
|
||||
tokenLen
|
||||
}
|
||||
]
|
||||
});
|
||||
billId = res._id;
|
||||
|
||||
// 账号扣费
|
||||
await User.findByIdAndUpdate(userId, {
|
||||
$inc: { balance: -total }
|
||||
});
|
||||
} catch (err) {
|
||||
addLog.error('Create generateVector bill error', { err });
|
||||
billId && Bill.findByIdAndDelete(billId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const countModelPrice = ({ model, tokens }: { model: string; tokens: number }) => {
|
||||
const modelData = getModel(model);
|
||||
if (!modelData) return 0;
|
||||
return modelData.price * tokens;
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Schema, model, models, Model } from 'mongoose';
|
||||
import { OpenApiSchema } from '@/types/mongoSchema';
|
||||
|
||||
const OpenApiSchema = new Schema({
|
||||
userId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'user',
|
||||
required: true
|
||||
},
|
||||
apiKey: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
createTime: {
|
||||
type: Date,
|
||||
default: () => new Date()
|
||||
},
|
||||
lastUsedTime: {
|
||||
type: Date
|
||||
}
|
||||
});
|
||||
|
||||
export const OpenApi: Model<OpenApiSchema> = models['openapi'] || model('openapi', OpenApiSchema);
|
||||
@@ -1,107 +0,0 @@
|
||||
import { adaptChatItem_openAI } from '@/utils/plugin/openai';
|
||||
import { ChatContextFilter } from '@/service/utils/chat/index';
|
||||
import type { ChatHistoryItemResType, ChatItemType } from '@/types/chat';
|
||||
import { ChatModuleEnum, ChatRoleEnum, TaskResponseKeyEnum } from '@/constants/chat';
|
||||
import { getAIChatApi, axiosConfig } from '@/service/ai/openai';
|
||||
import type { ClassifyQuestionAgentItemType } from '@/types/app';
|
||||
import { countModelPrice } from '@/service/events/pushBill';
|
||||
import { UserModelSchema } from '@/types/mongoSchema';
|
||||
import { getModel } from '@/service/utils/data';
|
||||
import { SystemInputEnum } from '@/constants/app';
|
||||
import { SpecialInputKeyEnum } from '@/constants/flow';
|
||||
|
||||
export type CQProps = {
|
||||
systemPrompt?: string;
|
||||
history?: ChatItemType[];
|
||||
[SystemInputEnum.userChatInput]: string;
|
||||
userOpenaiAccount: UserModelSchema['openaiAccount'];
|
||||
[SpecialInputKeyEnum.agents]: ClassifyQuestionAgentItemType[];
|
||||
};
|
||||
export type CQResponse = {
|
||||
[TaskResponseKeyEnum.responseData]: ChatHistoryItemResType;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
const agentModel = 'gpt-3.5-turbo';
|
||||
const agentFunName = 'agent_user_question';
|
||||
const maxTokens = 3000;
|
||||
|
||||
/* request openai chat */
|
||||
export const dispatchClassifyQuestion = async (props: Record<string, any>): Promise<CQResponse> => {
|
||||
const { agents, systemPrompt, history = [], userChatInput, userOpenaiAccount } = props as CQProps;
|
||||
|
||||
if (!userChatInput) {
|
||||
return Promise.reject('Input is empty');
|
||||
}
|
||||
|
||||
const messages: ChatItemType[] = [
|
||||
...(systemPrompt
|
||||
? [
|
||||
{
|
||||
obj: ChatRoleEnum.System,
|
||||
value: systemPrompt
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...history,
|
||||
{
|
||||
obj: ChatRoleEnum.Human,
|
||||
value: userChatInput
|
||||
}
|
||||
];
|
||||
const filterMessages = ChatContextFilter({
|
||||
model: agentModel,
|
||||
prompts: messages,
|
||||
maxTokens
|
||||
});
|
||||
const adaptMessages = adaptChatItem_openAI({ messages: filterMessages, reserveId: false });
|
||||
|
||||
// function body
|
||||
const agentFunction = {
|
||||
name: agentFunName,
|
||||
description: '判断用户问题的类型属于哪方面,返回对应的枚举字段',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: {
|
||||
type: 'string',
|
||||
description: agents.map((item) => `${item.value},返回:'${item.key}'`).join(';'),
|
||||
enum: agents.map((item) => item.key)
|
||||
}
|
||||
},
|
||||
required: ['type']
|
||||
}
|
||||
};
|
||||
const chatAPI = getAIChatApi(userOpenaiAccount);
|
||||
|
||||
const response = await chatAPI.createChatCompletion(
|
||||
{
|
||||
model: agentModel,
|
||||
temperature: 0,
|
||||
messages: [...adaptMessages],
|
||||
function_call: { name: agentFunName },
|
||||
functions: [agentFunction]
|
||||
},
|
||||
{
|
||||
...axiosConfig(userOpenaiAccount)
|
||||
}
|
||||
);
|
||||
|
||||
const arg = JSON.parse(response.data.choices?.[0]?.message?.function_call?.arguments || '');
|
||||
|
||||
const tokens = response.data.usage?.total_tokens || 0;
|
||||
|
||||
const result = agents.find((item) => item.key === arg?.type) || agents[0];
|
||||
|
||||
return {
|
||||
[result.key]: 1,
|
||||
[TaskResponseKeyEnum.responseData]: {
|
||||
moduleName: ChatModuleEnum.CQ,
|
||||
price: userOpenaiAccount?.key ? 0 : countModelPrice({ model: agentModel, tokens }),
|
||||
model: getModel(agentModel)?.name || agentModel,
|
||||
tokens,
|
||||
cqList: agents,
|
||||
cqResult: result.value
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -1,131 +0,0 @@
|
||||
import { adaptChatItem_openAI } from '@/utils/plugin/openai';
|
||||
import { ChatContextFilter } from '@/service/utils/chat/index';
|
||||
import type { ChatHistoryItemResType, ChatItemType } from '@/types/chat';
|
||||
import { ChatModuleEnum, ChatRoleEnum, TaskResponseKeyEnum } from '@/constants/chat';
|
||||
import { getAIChatApi, axiosConfig } from '@/service/ai/openai';
|
||||
import type { ContextExtractAgentItemType } from '@/types/app';
|
||||
import { ContextExtractEnum } from '@/constants/flow/flowField';
|
||||
import { countModelPrice } from '@/service/events/pushBill';
|
||||
import { UserModelSchema } from '@/types/mongoSchema';
|
||||
import { getModel } from '@/service/utils/data';
|
||||
|
||||
export type Props = {
|
||||
userOpenaiAccount: UserModelSchema['openaiAccount'];
|
||||
history?: ChatItemType[];
|
||||
[ContextExtractEnum.content]: string;
|
||||
[ContextExtractEnum.extractKeys]: ContextExtractAgentItemType[];
|
||||
[ContextExtractEnum.description]: string;
|
||||
};
|
||||
export type Response = {
|
||||
[ContextExtractEnum.success]?: boolean;
|
||||
[ContextExtractEnum.failed]?: boolean;
|
||||
[ContextExtractEnum.fields]: string;
|
||||
[TaskResponseKeyEnum.responseData]: ChatHistoryItemResType;
|
||||
};
|
||||
|
||||
const agentModel = 'gpt-3.5-turbo';
|
||||
const agentFunName = 'agent_extract_data';
|
||||
const maxTokens = 4000;
|
||||
|
||||
export async function dispatchContentExtract({
|
||||
userOpenaiAccount,
|
||||
content,
|
||||
extractKeys,
|
||||
history = [],
|
||||
description
|
||||
}: Props): Promise<Response> {
|
||||
if (!content) {
|
||||
return Promise.reject('Input is empty');
|
||||
}
|
||||
const messages: ChatItemType[] = [
|
||||
...history,
|
||||
{
|
||||
obj: ChatRoleEnum.Human,
|
||||
value: content
|
||||
}
|
||||
];
|
||||
const filterMessages = ChatContextFilter({
|
||||
// @ts-ignore
|
||||
model: agentModel,
|
||||
prompts: messages,
|
||||
maxTokens
|
||||
});
|
||||
const adaptMessages = adaptChatItem_openAI({ messages: filterMessages, reserveId: false });
|
||||
|
||||
const properties: Record<
|
||||
string,
|
||||
{
|
||||
type: string;
|
||||
description: string;
|
||||
}
|
||||
> = {};
|
||||
extractKeys.forEach((item) => {
|
||||
properties[item.key] = {
|
||||
type: 'string',
|
||||
description: item.desc
|
||||
};
|
||||
});
|
||||
|
||||
// function body
|
||||
const agentFunction = {
|
||||
name: agentFunName,
|
||||
description: `${description}\n如果内容不存在,返回空字符串。`,
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties,
|
||||
required: extractKeys.filter((item) => item.required).map((item) => item.key)
|
||||
}
|
||||
};
|
||||
|
||||
const chatAPI = getAIChatApi(userOpenaiAccount);
|
||||
|
||||
const response = await chatAPI.createChatCompletion(
|
||||
{
|
||||
model: agentModel,
|
||||
temperature: 0,
|
||||
messages: [...adaptMessages],
|
||||
function_call: { name: agentFunName },
|
||||
functions: [agentFunction]
|
||||
},
|
||||
{
|
||||
...axiosConfig(userOpenaiAccount)
|
||||
}
|
||||
);
|
||||
|
||||
const arg: Record<string, any> = (() => {
|
||||
try {
|
||||
return JSON.parse(response.data.choices?.[0]?.message?.function_call?.arguments || '{}');
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
})();
|
||||
|
||||
// auth fields
|
||||
let success = !extractKeys.find((item) => !arg[item.key]);
|
||||
// auth empty value
|
||||
if (success) {
|
||||
for (const key in arg) {
|
||||
if (arg[key] === '') {
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const tokens = response.data.usage?.total_tokens || 0;
|
||||
|
||||
return {
|
||||
[ContextExtractEnum.success]: success ? true : undefined,
|
||||
[ContextExtractEnum.failed]: success ? undefined : true,
|
||||
[ContextExtractEnum.fields]: JSON.stringify(arg),
|
||||
...arg,
|
||||
[TaskResponseKeyEnum.responseData]: {
|
||||
moduleName: ChatModuleEnum.Extract,
|
||||
price: userOpenaiAccount?.key ? 0 : countModelPrice({ model: agentModel, tokens }),
|
||||
model: getModel(agentModel)?.name || agentModel,
|
||||
tokens,
|
||||
extractDescription: description,
|
||||
extractResult: arg
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,388 +0,0 @@
|
||||
import type { NextApiResponse } from 'next';
|
||||
import { sseResponse } from '@/service/utils/tools';
|
||||
import { OpenAiChatEnum } from '@/constants/model';
|
||||
import { adaptChatItem_openAI, countOpenAIToken } from '@/utils/plugin/openai';
|
||||
import { modelToolMap } from '@/utils/plugin';
|
||||
import { ChatContextFilter } from '@/service/utils/chat/index';
|
||||
import type { ChatItemType, QuoteItemType } from '@/types/chat';
|
||||
import type { ChatHistoryItemResType } from '@/types/chat';
|
||||
import { ChatModuleEnum, ChatRoleEnum, sseResponseEventEnum } from '@/constants/chat';
|
||||
import { SSEParseData, parseStreamChunk } from '@/utils/sse';
|
||||
import { textAdaptGptResponse } from '@/utils/adapt';
|
||||
import { getAIChatApi, axiosConfig } from '@/service/ai/openai';
|
||||
import { TaskResponseKeyEnum } from '@/constants/chat';
|
||||
import { getChatModel } from '@/service/utils/data';
|
||||
import { countModelPrice } from '@/service/events/pushBill';
|
||||
import { ChatModelItemType } from '@/types/model';
|
||||
import { UserModelSchema } from '@/types/mongoSchema';
|
||||
import { textCensor } from '@/api/service/plugins';
|
||||
import { ChatCompletionRequestMessageRoleEnum } from 'openai';
|
||||
import { AppModuleItemType } from '@/types/app';
|
||||
|
||||
export type ChatProps = {
|
||||
res: NextApiResponse;
|
||||
model: `${OpenAiChatEnum}`;
|
||||
temperature?: number;
|
||||
maxToken?: number;
|
||||
history?: ChatItemType[];
|
||||
userChatInput: string;
|
||||
stream?: boolean;
|
||||
detail?: boolean;
|
||||
quoteQA?: QuoteItemType[];
|
||||
systemPrompt?: string;
|
||||
limitPrompt?: string;
|
||||
userOpenaiAccount: UserModelSchema['openaiAccount'];
|
||||
outputs: AppModuleItemType['outputs'];
|
||||
};
|
||||
export type ChatResponse = {
|
||||
[TaskResponseKeyEnum.answerText]: string;
|
||||
[TaskResponseKeyEnum.responseData]: ChatHistoryItemResType;
|
||||
finish: boolean;
|
||||
};
|
||||
|
||||
/* request openai chat */
|
||||
export const dispatchChatCompletion = async (props: Record<string, any>): Promise<ChatResponse> => {
|
||||
let {
|
||||
res,
|
||||
model,
|
||||
temperature = 0,
|
||||
maxToken = 4000,
|
||||
stream = false,
|
||||
detail = false,
|
||||
history = [],
|
||||
quoteQA = [],
|
||||
userChatInput,
|
||||
systemPrompt = '',
|
||||
limitPrompt = '',
|
||||
userOpenaiAccount,
|
||||
outputs
|
||||
} = props as ChatProps;
|
||||
if (!userChatInput) {
|
||||
return Promise.reject('Question is empty');
|
||||
}
|
||||
|
||||
// temperature adapt
|
||||
const modelConstantsData = getChatModel(model);
|
||||
|
||||
if (!modelConstantsData) {
|
||||
return Promise.reject('The chat model is undefined, you need to select a chat model.');
|
||||
}
|
||||
|
||||
const { filterQuoteQA, quotePrompt } = filterQuote({
|
||||
quoteQA,
|
||||
model: modelConstantsData
|
||||
});
|
||||
|
||||
if (modelConstantsData.censor) {
|
||||
await textCensor({
|
||||
text: `${systemPrompt}
|
||||
${quotePrompt}
|
||||
${limitPrompt}
|
||||
${userChatInput}
|
||||
`
|
||||
});
|
||||
}
|
||||
|
||||
const { messages, filterMessages } = getChatMessages({
|
||||
model: modelConstantsData,
|
||||
history,
|
||||
quotePrompt,
|
||||
userChatInput,
|
||||
systemPrompt,
|
||||
limitPrompt
|
||||
});
|
||||
const { max_tokens } = getMaxTokens({
|
||||
model: modelConstantsData,
|
||||
maxToken,
|
||||
filterMessages
|
||||
});
|
||||
// console.log(messages);
|
||||
|
||||
// FastGpt temperature range: 1~10
|
||||
temperature = +(modelConstantsData.maxTemperature * (temperature / 10)).toFixed(2);
|
||||
temperature = Math.max(temperature, 0.01);
|
||||
const chatAPI = getAIChatApi(userOpenaiAccount);
|
||||
|
||||
const response = await chatAPI.createChatCompletion(
|
||||
{
|
||||
model,
|
||||
temperature,
|
||||
max_tokens,
|
||||
messages: [
|
||||
...(modelConstantsData.defaultSystem
|
||||
? [
|
||||
{
|
||||
role: ChatCompletionRequestMessageRoleEnum.System,
|
||||
content: modelConstantsData.defaultSystem
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...messages
|
||||
],
|
||||
// frequency_penalty: 0.5, // 越大,重复内容越少
|
||||
// presence_penalty: -0.5, // 越大,越容易出现新内容
|
||||
stream
|
||||
},
|
||||
{
|
||||
timeout: stream ? 60000 : 480000,
|
||||
responseType: stream ? 'stream' : 'json',
|
||||
...axiosConfig(userOpenaiAccount)
|
||||
}
|
||||
);
|
||||
|
||||
const { answerText, totalTokens, completeMessages } = await (async () => {
|
||||
if (stream) {
|
||||
// sse response
|
||||
const { answer } = await streamResponse({
|
||||
res,
|
||||
detail,
|
||||
response
|
||||
});
|
||||
// count tokens
|
||||
const completeMessages = filterMessages.concat({
|
||||
obj: ChatRoleEnum.AI,
|
||||
value: answer
|
||||
});
|
||||
|
||||
const totalTokens = countOpenAIToken({
|
||||
messages: completeMessages
|
||||
});
|
||||
|
||||
targetResponse({ res, detail, outputs });
|
||||
|
||||
return {
|
||||
answerText: answer,
|
||||
totalTokens,
|
||||
completeMessages
|
||||
};
|
||||
} else {
|
||||
const answer = stream ? '' : response.data.choices?.[0].message?.content || '';
|
||||
const totalTokens = stream ? 0 : response.data.usage?.total_tokens || 0;
|
||||
|
||||
const completeMessages = filterMessages.concat({
|
||||
obj: ChatRoleEnum.AI,
|
||||
value: answer
|
||||
});
|
||||
|
||||
return {
|
||||
answerText: answer,
|
||||
totalTokens,
|
||||
completeMessages
|
||||
};
|
||||
}
|
||||
})();
|
||||
|
||||
return {
|
||||
[TaskResponseKeyEnum.answerText]: answerText,
|
||||
[TaskResponseKeyEnum.responseData]: {
|
||||
moduleName: ChatModuleEnum.AIChat,
|
||||
price: userOpenaiAccount?.key ? 0 : countModelPrice({ model, tokens: totalTokens }),
|
||||
model: modelConstantsData.name,
|
||||
tokens: totalTokens,
|
||||
question: userChatInput,
|
||||
answer: answerText,
|
||||
maxToken: max_tokens,
|
||||
quoteList: filterQuoteQA,
|
||||
completeMessages
|
||||
},
|
||||
finish: true
|
||||
};
|
||||
};
|
||||
|
||||
function filterQuote({
|
||||
quoteQA = [],
|
||||
model
|
||||
}: {
|
||||
quoteQA: ChatProps['quoteQA'];
|
||||
model: ChatModelItemType;
|
||||
}) {
|
||||
const sliceResult = modelToolMap.tokenSlice({
|
||||
model: model.model,
|
||||
maxToken: model.quoteMaxToken,
|
||||
messages: quoteQA.map((item) => ({
|
||||
obj: ChatRoleEnum.System,
|
||||
value: item.a ? `${item.q}\n${item.a}` : item.q
|
||||
}))
|
||||
});
|
||||
|
||||
// slice filterSearch
|
||||
const filterQuoteQA = quoteQA.slice(0, sliceResult.length);
|
||||
|
||||
const quotePrompt =
|
||||
filterQuoteQA.length > 0
|
||||
? `"""${filterQuoteQA
|
||||
.map((item) =>
|
||||
item.a ? `{instruction:"${item.q}",output:"${item.a}"}` : `{instruction:"${item.q}"}`
|
||||
)
|
||||
.join('\n')}"""`
|
||||
: '';
|
||||
|
||||
return {
|
||||
filterQuoteQA,
|
||||
quotePrompt
|
||||
};
|
||||
}
|
||||
function getChatMessages({
|
||||
quotePrompt,
|
||||
history = [],
|
||||
systemPrompt,
|
||||
limitPrompt,
|
||||
userChatInput,
|
||||
model
|
||||
}: {
|
||||
quotePrompt: string;
|
||||
history: ChatProps['history'];
|
||||
systemPrompt: string;
|
||||
limitPrompt: string;
|
||||
userChatInput: string;
|
||||
model: ChatModelItemType;
|
||||
}) {
|
||||
const limitText = (() => {
|
||||
if (!quotePrompt) {
|
||||
return limitPrompt;
|
||||
}
|
||||
const defaultPrompt =
|
||||
'三引号是我提供给你的专属知识,它们拥有最高优先级。instruction 是相关介绍,output 是预期回答,使用引用内容来回答我下面的问题。';
|
||||
if (limitPrompt) {
|
||||
return `${defaultPrompt}${limitPrompt}`;
|
||||
}
|
||||
return `${defaultPrompt}\n回答内容限制:你仅回答三引号中提及的内容,下面我提出的问题与引用内容无关时,你可以直接回复: "你的问题没有在知识库中体现"`;
|
||||
})();
|
||||
|
||||
const messages: ChatItemType[] = [
|
||||
...(systemPrompt
|
||||
? [
|
||||
{
|
||||
obj: ChatRoleEnum.System,
|
||||
value: systemPrompt
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...(quotePrompt
|
||||
? [
|
||||
{
|
||||
obj: ChatRoleEnum.System,
|
||||
value: quotePrompt
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...history,
|
||||
...(limitText
|
||||
? [
|
||||
{
|
||||
obj: ChatRoleEnum.System,
|
||||
value: limitText
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
obj: ChatRoleEnum.Human,
|
||||
value: userChatInput
|
||||
}
|
||||
];
|
||||
|
||||
const filterMessages = ChatContextFilter({
|
||||
model: model.model,
|
||||
prompts: messages,
|
||||
maxTokens: Math.ceil(model.contextMaxToken - 300) // filter token. not response maxToken
|
||||
});
|
||||
|
||||
const adaptMessages = adaptChatItem_openAI({ messages: filterMessages, reserveId: false });
|
||||
|
||||
return {
|
||||
messages: adaptMessages,
|
||||
filterMessages
|
||||
};
|
||||
}
|
||||
function getMaxTokens({
|
||||
maxToken,
|
||||
model,
|
||||
filterMessages = []
|
||||
}: {
|
||||
maxToken: number;
|
||||
model: ChatModelItemType;
|
||||
filterMessages: ChatProps['history'];
|
||||
}) {
|
||||
const tokensLimit = model.contextMaxToken;
|
||||
/* count response max token */
|
||||
|
||||
const promptsToken = modelToolMap.countTokens({
|
||||
model: model.model,
|
||||
messages: filterMessages
|
||||
});
|
||||
maxToken = maxToken + promptsToken > tokensLimit ? tokensLimit - promptsToken : maxToken;
|
||||
|
||||
return {
|
||||
max_tokens: maxToken
|
||||
};
|
||||
}
|
||||
|
||||
function targetResponse({
|
||||
res,
|
||||
outputs,
|
||||
detail
|
||||
}: {
|
||||
res: NextApiResponse;
|
||||
outputs: AppModuleItemType['outputs'];
|
||||
detail: boolean;
|
||||
}) {
|
||||
const targets =
|
||||
outputs.find((output) => output.key === TaskResponseKeyEnum.answerText)?.targets || [];
|
||||
|
||||
if (targets.length === 0) return;
|
||||
sseResponse({
|
||||
res,
|
||||
event: detail ? sseResponseEventEnum.answer : undefined,
|
||||
data: textAdaptGptResponse({
|
||||
text: '\n'
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async function streamResponse({
|
||||
res,
|
||||
detail,
|
||||
response
|
||||
}: {
|
||||
res: NextApiResponse;
|
||||
detail: boolean;
|
||||
response: any;
|
||||
}) {
|
||||
let answer = '';
|
||||
let error: any = null;
|
||||
const parseData = new SSEParseData();
|
||||
|
||||
try {
|
||||
for await (const chunk of response.data as any) {
|
||||
if (res.closed) break;
|
||||
const parse = parseStreamChunk(chunk);
|
||||
parse.forEach((item) => {
|
||||
const { data } = parseData.parse(item);
|
||||
if (!data || data === '[DONE]') return;
|
||||
|
||||
const content: string = data?.choices?.[0]?.delta?.content || '';
|
||||
error = data.error;
|
||||
answer += content;
|
||||
|
||||
sseResponse({
|
||||
res,
|
||||
event: detail ? sseResponseEventEnum.answer : undefined,
|
||||
data: textAdaptGptResponse({
|
||||
text: content
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('pipe error', error);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
console.log(error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
return {
|
||||
answer
|
||||
};
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { SystemInputEnum } from '@/constants/app';
|
||||
|
||||
export type UserChatInputProps = {
|
||||
[SystemInputEnum.userChatInput]: string;
|
||||
};
|
||||
|
||||
export const dispatchChatInput = (props: Record<string, any>) => {
|
||||
const { userChatInput } = props as UserChatInputProps;
|
||||
return {
|
||||
userChatInput
|
||||
};
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
import axios from 'axios';
|
||||
|
||||
{
|
||||
/*Bing 搜索*/
|
||||
}
|
||||
const BingSearch = async (wait_val: string) => {
|
||||
const response = await axios.post('newbing中转服务器', {
|
||||
prompt: wait_val
|
||||
});
|
||||
const result = response.data.result;
|
||||
return result;
|
||||
};
|
||||
|
||||
{
|
||||
/*google 搜索*/
|
||||
}
|
||||
const GoogleSearch = async (wait_val: string) => {
|
||||
const response = await axios.get('https://www.googleapis.com/customsearch/v1', {
|
||||
params: {
|
||||
key: process.env.GOOGLE_KEY,
|
||||
q: wait_val,
|
||||
cx: process.env.searchEngineId,
|
||||
start: 1,
|
||||
num: 3,
|
||||
dateRestrict: 'm[1]' //搜索结果限定为一个月内
|
||||
}
|
||||
});
|
||||
const results = response.data.items;
|
||||
if (results !== null) {
|
||||
const result = results.map((item: { snippet: string }) => item.snippet).join('\n');
|
||||
return result;
|
||||
}
|
||||
};
|
||||
export { BingSearch, GoogleSearch };
|
||||
@@ -1,42 +0,0 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
import { type KbTestItemType } from '@/types/plugin';
|
||||
|
||||
type State = {
|
||||
kbTestList: KbTestItemType[];
|
||||
pushKbTestItem: (data: KbTestItemType) => void;
|
||||
delKbTestItemById: (id: string) => void;
|
||||
updateKbItemById: (data: KbTestItemType) => void;
|
||||
};
|
||||
|
||||
export const useKbStore = create<State>()(
|
||||
devtools(
|
||||
persist(
|
||||
immer((set, get) => ({
|
||||
kbTestList: [],
|
||||
pushKbTestItem(data) {
|
||||
set((state) => {
|
||||
state.kbTestList = [data, ...state.kbTestList].slice(0, 500);
|
||||
});
|
||||
},
|
||||
delKbTestItemById(id) {
|
||||
set((state) => {
|
||||
state.kbTestList = state.kbTestList.filter((item) => item.id !== id);
|
||||
});
|
||||
},
|
||||
updateKbItemById(data: KbTestItemType) {
|
||||
set((state) => {
|
||||
state.kbTestList = state.kbTestList.map((item) => (item.id === data.id ? data : item));
|
||||
});
|
||||
}
|
||||
})),
|
||||
{
|
||||
name: 'kbStore',
|
||||
partialize: (state) => ({
|
||||
kbTestList: state.kbTestList
|
||||
})
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
6
client/src/types/openapi.d.ts
vendored
@@ -1,6 +0,0 @@
|
||||
export interface UserOpenApiKey {
|
||||
id: string;
|
||||
apiKey: string;
|
||||
createTime: Date;
|
||||
lastUsedTime?: Date;
|
||||
}
|
||||
0
client/src/types/pg.d.ts
vendored
41
client/src/types/plugin.d.ts
vendored
@@ -1,41 +0,0 @@
|
||||
import { VectorModelItemType } from './model';
|
||||
import type { kbSchema } from './mongoSchema';
|
||||
|
||||
export type SelectedKbType = { kbId: string; vectorModel: VectorModelItemType }[];
|
||||
|
||||
export type KbListItemType = {
|
||||
_id: string;
|
||||
avatar: string;
|
||||
name: string;
|
||||
tags: string[];
|
||||
vectorModel: VectorModelItemType;
|
||||
};
|
||||
/* kb type */
|
||||
export interface KbItemType {
|
||||
_id: string;
|
||||
avatar: string;
|
||||
name: string;
|
||||
userId: string;
|
||||
vectorModel: VectorModelItemType;
|
||||
tags: string;
|
||||
}
|
||||
|
||||
export interface KbDataItemType {
|
||||
id: string;
|
||||
q: string; // 提问词
|
||||
a: string; // 原文
|
||||
source: string;
|
||||
}
|
||||
|
||||
export type KbTestItemType = {
|
||||
id: string;
|
||||
kbId: string;
|
||||
text: string;
|
||||
time: Date;
|
||||
results: (KbDataItemType & { score: number })[];
|
||||
};
|
||||
|
||||
export type FetchResultItem = {
|
||||
url: string;
|
||||
content: string;
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
export const LANG_KEY = 'NEXT_LOCALE_LANG';
|
||||
export enum LangEnum {
|
||||
'zh' = 'zh',
|
||||
'en' = 'en'
|
||||
}
|
||||
|
||||
export const setLangStore = (value: `${LangEnum}`) => {
|
||||
return Cookies.set(LANG_KEY, value, { expires: 7, sameSite: 'None', secure: true });
|
||||
};
|
||||
|
||||
export const getLangStore = () => {
|
||||
return (Cookies.get(LANG_KEY) as `${LangEnum}`) || LangEnum.zh;
|
||||
};
|
||||
|
||||
export const serviceSideProps = (content: any) => {
|
||||
return serverSideTranslations(
|
||||
content.req.cookies[LANG_KEY] || 'en',
|
||||
undefined,
|
||||
null,
|
||||
content.locales
|
||||
);
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
import { countOpenAIToken, openAiSliceTextByToken } from './openai';
|
||||
import { gpt_chatItemTokenSlice } from '@/pages/api/openapi/text/gptMessagesSlice';
|
||||
|
||||
export const modelToolMap = {
|
||||
countTokens: countOpenAIToken,
|
||||
sliceText: openAiSliceTextByToken,
|
||||
tokenSlice: gpt_chatItemTokenSlice
|
||||
};
|
||||
@@ -1,94 +0,0 @@
|
||||
import { encoding_for_model } from '@dqbd/tiktoken';
|
||||
import type { ChatItemType } from '@/types/chat';
|
||||
import { ChatRoleEnum } from '@/constants/chat';
|
||||
import { ChatCompletionRequestMessageRoleEnum } from 'openai';
|
||||
import axios from 'axios';
|
||||
import type { MessageItemType } from '@/pages/api/openapi/v1/chat/completions';
|
||||
|
||||
export const getOpenAiEncMap = () => {
|
||||
if (typeof window !== 'undefined' && window.OpenAiEncMap) {
|
||||
return window.OpenAiEncMap;
|
||||
}
|
||||
if (typeof global !== 'undefined' && global.OpenAiEncMap) {
|
||||
return global.OpenAiEncMap;
|
||||
}
|
||||
const enc = encoding_for_model('gpt-3.5-turbo', {
|
||||
'<|im_start|>': 100264,
|
||||
'<|im_end|>': 100265,
|
||||
'<|im_sep|>': 100266
|
||||
});
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.OpenAiEncMap = enc;
|
||||
}
|
||||
if (typeof global !== 'undefined') {
|
||||
global.OpenAiEncMap = enc;
|
||||
}
|
||||
|
||||
return enc;
|
||||
};
|
||||
|
||||
export const adaptChatItem_openAI = ({
|
||||
messages,
|
||||
reserveId
|
||||
}: {
|
||||
messages: ChatItemType[];
|
||||
reserveId: boolean;
|
||||
}): MessageItemType[] => {
|
||||
const map = {
|
||||
[ChatRoleEnum.AI]: ChatCompletionRequestMessageRoleEnum.Assistant,
|
||||
[ChatRoleEnum.Human]: ChatCompletionRequestMessageRoleEnum.User,
|
||||
[ChatRoleEnum.System]: ChatCompletionRequestMessageRoleEnum.System
|
||||
};
|
||||
return messages.map((item) => ({
|
||||
...(reserveId && { dataId: item.dataId }),
|
||||
role: map[item.obj] || ChatCompletionRequestMessageRoleEnum.System,
|
||||
content: item.value || ''
|
||||
}));
|
||||
};
|
||||
|
||||
export function countOpenAIToken({
|
||||
messages,
|
||||
model = 'gpt-3.5-turbo'
|
||||
}: {
|
||||
messages: ChatItemType[];
|
||||
model?: string;
|
||||
}) {
|
||||
const diffVal = model.startsWith('gpt-3.5-turbo') ? 3 : 2;
|
||||
|
||||
const adaptMessages = adaptChatItem_openAI({ messages, reserveId: true });
|
||||
const token = adaptMessages.reduce((sum, item) => {
|
||||
const text = `${item.role}\n${item.content}`;
|
||||
const enc = getOpenAiEncMap();
|
||||
const encodeText = enc.encode(text);
|
||||
const tokens = encodeText.length + diffVal;
|
||||
return sum + tokens;
|
||||
}, 0);
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
export const openAiSliceTextByToken = ({ text, length }: { text: string; length: number }) => {
|
||||
const enc = getOpenAiEncMap();
|
||||
const encodeText = enc.encode(text);
|
||||
const decoder = new TextDecoder();
|
||||
return decoder.decode(enc.decode(encodeText.slice(0, length)));
|
||||
};
|
||||
|
||||
export const authOpenAiKey = async (key: string) => {
|
||||
return axios
|
||||
.get('https://ccdbwscohpmu.cloud.sealos.io/openai/v1/dashboard/billing/subscription', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${key}`
|
||||
}
|
||||
})
|
||||
.then((res) => {
|
||||
if (!res.data.access_until) {
|
||||
return Promise.resolve('OpenAI Key 可能无效');
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
return Promise.reject(err?.response?.data?.error?.message || 'OpenAI Key 可能无效');
|
||||
});
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
import { PRICE_SCALE } from '@/constants/common';
|
||||
import { loginOut } from '@/api/user';
|
||||
|
||||
const tokenKey = 'token';
|
||||
export const clearToken = () => {
|
||||
try {
|
||||
loginOut();
|
||||
localStorage.removeItem(tokenKey);
|
||||
} catch (error) {
|
||||
error;
|
||||
}
|
||||
};
|
||||
|
||||
export const setToken = (token: string) => {
|
||||
localStorage.setItem(tokenKey, token);
|
||||
};
|
||||
export const getToken = () => {
|
||||
return localStorage.getItem(tokenKey) || '';
|
||||
};
|
||||
|
||||
/**
|
||||
* 把数据库读取到的price,转化成元
|
||||
*/
|
||||
export const formatPrice = (val = 0, multiple = 1) => {
|
||||
return Number(((val / PRICE_SCALE) * multiple).toFixed(10));
|
||||
};
|
||||
12
docSite/Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
||||
FROM hugomods/hugo:0.117.0 AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ADD ./docSite hugo
|
||||
RUN cd /app/hugo && hugo mod get -u github.com/colinwilson/lotusdocs && hugo -v --minify
|
||||
|
||||
FROM fholzer/nginx-brotli:latest
|
||||
|
||||
LABEL org.opencontainers.image.source https://github.com/labring/FastGPT
|
||||
|
||||
COPY --from=builder /app/hugo/public /usr/share/nginx/html
|
||||
@@ -3,7 +3,7 @@
|
||||
## 本地运行
|
||||
|
||||
1. 安装 go 语言环境。
|
||||
2. 安装 hugo。 [二进制下载](https://github.com/gohugoio/hugo/releases/tag/v0.117.0)
|
||||
2. 安装 hugo。 [二进制下载](https://github.com/gohugoio/hugo/releases/tag/v0.117.0),注意需要安装 extended 版本。
|
||||
3. cd docSite
|
||||
4. hugo serve
|
||||
5. 访问 http://localhost:1313
|
||||
|
||||
145
docSite/assets/docs/scss/custom/pages/_custom.scss
Normal file
@@ -0,0 +1,145 @@
|
||||
.docs-content .main-content img, .docs-content .main-content svg:not(.gitinfo svg):not(a svg) {
|
||||
max-width: 80% !important;
|
||||
height: auto;
|
||||
display: block !important;
|
||||
margin: 0 auto !important;
|
||||
border-radius: .25rem;
|
||||
}
|
||||
|
||||
div.code-toolbar {
|
||||
padding-top: 1.95rem !important;
|
||||
}
|
||||
|
||||
.docs-content .main-content pre code::before {
|
||||
background: #fc625d;
|
||||
border-radius: 50%;
|
||||
box-shadow: 20px 0 #fdbc40, 40px 0 #35cd4b;
|
||||
content: ' ';
|
||||
height: 12px;
|
||||
left: 12px;
|
||||
margin-top: -21px;
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
li p {
|
||||
margin-top: 1rem !important;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
footer {
|
||||
height: 118px !important;
|
||||
}
|
||||
|
||||
/*
|
||||
footer a:hover {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
*/
|
||||
|
||||
.medium-zoom-overlay,
|
||||
.medium-zoom-image--opened {
|
||||
z-index: 1999;
|
||||
}
|
||||
|
||||
/* 徽章样式 */
|
||||
.github-badge {
|
||||
display: inline-block;
|
||||
border-radius: 4px;
|
||||
text-shadow: none;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
line-height: 15px;
|
||||
margin-bottom: 5px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.github-badge .badge-subject {
|
||||
display: inline-block;
|
||||
background-color: #4D4D4D;
|
||||
padding: 4px 4px 4px 6px;
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
.github-badge .badge-value {
|
||||
display: inline-block;
|
||||
padding: 4px 6px 4px 4px;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
.github-badge .bg-brightgreen {
|
||||
background-color: #4DC820 !important;
|
||||
}
|
||||
.github-badge .bg-orange {
|
||||
background-color: #FFA500 !important;
|
||||
}
|
||||
.github-badge .bg-yellow {
|
||||
background-color: #D8B024 !important;
|
||||
}
|
||||
.github-badge .bg-blueviolet {
|
||||
background-color: #8833D7 !important;
|
||||
}
|
||||
.github-badge .bg-pink {
|
||||
background-color: #F26BAE !important;
|
||||
}
|
||||
.github-badge .bg-red {
|
||||
background-color: #e05d44 !important;
|
||||
}
|
||||
.github-badge .bg-blue {
|
||||
background-color: #007EC6 !important;
|
||||
}
|
||||
.github-badge .bg-lightgrey {
|
||||
background-color: #9F9F9F !important;
|
||||
}
|
||||
.github-badge .bg-grey, .github-badge .bg-gray {
|
||||
background-color: #555 !important;
|
||||
}
|
||||
.github-badge .bg-lightgrey, .github-badge .bg-lightgray {
|
||||
background-color: #9f9f9f !important;
|
||||
}
|
||||
|
||||
#fixed-box {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
#fixed-box {
|
||||
display: none
|
||||
}
|
||||
}
|
||||
|
||||
.feedback-btn-wrapper {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
margin: 2rem;
|
||||
}
|
||||
#feedback-btn {
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1.2rem 0.7rem;
|
||||
border-radius: 0.4rem;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
user-select: none;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
color: #fff;
|
||||
background-color: #4d698e;
|
||||
transition: filter 0.4s ease;
|
||||
}
|
||||
|
||||
#feedback-btn svg {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
#feedback-btn span {
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
77
docSite/assets/docs/scss/style.scss
Normal file
@@ -0,0 +1,77 @@
|
||||
/* Template Name: Lotus Docs
|
||||
Author: Colin Wilson
|
||||
E-mail: colin@aigis.uk
|
||||
Created: October 2022
|
||||
Version: 1.2.0
|
||||
File Description: Main CSS file for Lotus Docs
|
||||
*/
|
||||
|
||||
// Custom Font Variables
|
||||
$font-family-secondary: {{ .Site.Params.secondary_font | default "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', 'Ubuntu'" }};
|
||||
$font-family-sans-serif: {{ .Site.Params.sans_serif_font | default "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', 'Ubuntu'" }};
|
||||
$font-family-monospace: {{ .Site.Params.mono_font | default "SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace" }};
|
||||
|
||||
// Code Padding Variables
|
||||
$code-block-padding-top: {{ if eq .Site.Params.docs.prism true -}}0{{ else }}1.25rem 0 0 0{{ end }};
|
||||
|
||||
// Icon Fonts
|
||||
@import "custom/plugins/icons/google-material";
|
||||
|
||||
// Core files
|
||||
@import "../../scss/bootstrap/functions";
|
||||
@import "../../scss/bootstrap/variables";
|
||||
@import {{ printf "'%s%s'" "custom/colors/" (.Site.Params.docs.themeColor | default "blue") }}; // current theme color
|
||||
@import "../../scss/bootstrap/mixins";
|
||||
@import "../../scss/bootstrap/bootstrap";
|
||||
@import "variables";
|
||||
|
||||
{{ if and (.Site.Params.docsearch.appID) (.Site.Params.docsearch.apiKey) -}}
|
||||
@import "custom/plugins/docsearch/style";
|
||||
{{ end }}
|
||||
|
||||
// Structure
|
||||
@import "custom/structure/general";
|
||||
@import "custom/structure/content";
|
||||
@import "custom/structure/sidebar";
|
||||
@import "custom/structure/doc-nav";
|
||||
@import "custom/structure/toc";
|
||||
@import "custom/structure/footer";
|
||||
|
||||
// Components
|
||||
@import "custom/components/buttons";
|
||||
@import "custom/components/modal";
|
||||
@import "custom/components/breadcrumb";
|
||||
@import "custom/components/badge";
|
||||
@import "custom/components/backgrounds";
|
||||
@import "custom/components/alerts";
|
||||
@import "custom/components/card";
|
||||
@import "custom/components/forms";
|
||||
@import "custom/components/table";
|
||||
@import "custom/components/tabs";
|
||||
@import "custom/components/tooltip";
|
||||
|
||||
// Pages
|
||||
@import "custom/pages/features";
|
||||
@import "custom/pages/helper";
|
||||
|
||||
// Plugins
|
||||
|
||||
// Prism / Chroma
|
||||
{{- if eq .Site.Params.docs.prism true }}
|
||||
@import {{ printf "'%s%s'" "custom/plugins/prism/themes/" (.Site.Params.docs.prismTheme | default "lotusdocs") }}; // current prism theme
|
||||
@import "custom/plugins/prism/prism";
|
||||
{{- else }}
|
||||
@import "custom/plugins/chroma/default";
|
||||
{{- end -}}
|
||||
|
||||
// FlexSearch
|
||||
{{ if or (not (isset .Site.Params.flexsearch "enabled")) (eq .Site.Params.flexsearch.enabled true) -}}@import "custom/plugins/flexsearch/flexsearch";{{ end }}
|
||||
|
||||
// Feedback Widget
|
||||
{{ if .Site.Params.feedback.enabled | default false -}}@import "custom/plugins/feedback/feedback";{{ end}}
|
||||
|
||||
// Mermaid
|
||||
@import "custom/plugins/mermaid/mermaid";
|
||||
|
||||
// change
|
||||
@import "custom/pages/custom";
|
||||
@@ -1,4 +1,4 @@
|
||||
<svg width="32" height="32" viewBox="0 0 1041 1348" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg width="26" height="26" viewBox="0 0 1041 1348" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M340.837 0.33933L681.068 0.338989V0.455643C684.032 0.378397 686.999 0.339702 689.967 0.339702C735.961 0.3397 781.504 9.62899 823.997 27.6772C866.49 45.7254 905.099 72.1791 937.622 105.528C970.144 138.877 995.942 178.467 1013.54 222.04C1031.14 265.612 1040.2 312.312 1040.2 359.474L340.836 359.474L340.836 1347.84C296.157 1347.84 251.914 1338.55 210.636 1320.49C169.357 1302.43 131.85 1275.95 100.257 1242.58C68.6636 1209.21 43.6023 1169.59 26.5041 1125.99C11.3834 1087.43 2.75216 1046.42 0.957956 1004.81H0.605869L0.605897 368.098H0.70363C0.105752 341.831 2.23741 315.443 7.14306 289.411C20.2709 219.745 52.6748 155.754 100.257 105.528C147.839 55.3017 208.462 21.0975 274.461 7.24017C296.426 2.62833 318.657 0.339101 340.837 0.33933Z" fill="url(#paint0_linear_1172_228)"/>
|
||||
<path d="M633.639 904.645H513.029V576.37H635.422V576.377C678.161 576.607 720.454 585.093 759.951 601.37C799.997 617.874 836.384 642.064 867.033 672.559C897.683 703.054 921.996 739.257 938.583 779.101C955.171 818.944 963.709 861.648 963.709 904.775H633.639V904.645Z" fill="url(#paint1_linear_1172_228)"/>
|
||||
<defs>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
BIN
docSite/assets/imgs/demo-appointment1.png
Normal file
|
After Width: | Height: | Size: 485 KiB |
BIN
docSite/assets/imgs/demo-appointment2.png
Normal file
|
After Width: | Height: | Size: 147 KiB |
BIN
docSite/assets/imgs/demo-appointment3.png
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
docSite/assets/imgs/demo-appointment4.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
docSite/assets/imgs/demo-appointment5.png
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
docSite/assets/imgs/demo-appointment6.png
Normal file
|
After Width: | Height: | Size: 205 KiB |
BIN
docSite/assets/imgs/demo-appointment7.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
docSite/assets/imgs/demo-fix-evidence1.png
Normal file
|
After Width: | Height: | Size: 341 KiB |
BIN
docSite/assets/imgs/demo-fix-evidence2.png
Normal file
|
After Width: | Height: | Size: 470 KiB |
BIN
docSite/assets/imgs/demo_op_question1.png
Normal file
|
After Width: | Height: | Size: 294 KiB |
BIN
docSite/assets/imgs/demo_op_question2.png
Normal file
|
After Width: | Height: | Size: 175 KiB |
BIN
docSite/assets/imgs/demo_op_question3.png
Normal file
|
After Width: | Height: | Size: 172 KiB |
|
Before Width: | Height: | Size: 196 KiB After Width: | Height: | Size: 436 KiB |
BIN
docSite/assets/imgs/fastgpt-api2.png
Normal file
|
After Width: | Height: | Size: 335 KiB |
BIN
docSite/assets/imgs/fastgpt-price.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
docSite/assets/imgs/fastgptonsealos1.png
Normal file
|
After Width: | Height: | Size: 212 KiB |
BIN
docSite/assets/imgs/feishu-env.png
Normal file
|
After Width: | Height: | Size: 172 KiB |
BIN
docSite/assets/imgs/feishu-res.png
Normal file
|
After Width: | Height: | Size: 197 KiB |
BIN
docSite/assets/imgs/flow-kbsearch1.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
docSite/assets/imgs/flow-kbsearch2.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
docSite/assets/imgs/flow-kbsearch3.png
Normal file
|
After Width: | Height: | Size: 176 KiB |
BIN
docSite/assets/imgs/functional-arch.webp
Normal file
|
After Width: | Height: | Size: 190 KiB |
BIN
docSite/assets/imgs/getKbId.png
Normal file
|
After Width: | Height: | Size: 256 KiB |
BIN
docSite/assets/imgs/getfile_id.png
Normal file
|
After Width: | Height: | Size: 177 KiB |
BIN
docSite/assets/imgs/oneapi-demo1.png
Normal file
|
After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 215 KiB After Width: | Height: | Size: 206 KiB |