Commit 21b3f854 by Finley Ge Committed by GitHub

refactor: merge standardConstants and standard in team plan (#6549)

* refactor: merge standardConstants and standard in team plan

* Update packages/service/support/wallet/sub/utils.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: remove type assertion

* chore: type

* test: test buildStandardPlan

* fix: type

* perf: code perf

* add test code

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: archer <545436317@qq.com>
parent 007ca097
...@@ -52,8 +52,8 @@ jobs: ...@@ -52,8 +52,8 @@ jobs:
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: registry.cn-hangzhou.aliyuncs.com registry: registry.cn-hangzhou.aliyuncs.com
username: ${{ secrets.ALI_HUB_USERNAME }} username: ${{ secrets.FASTGPT_ALI_IMAGE_USER }}
password: ${{ secrets.ALI_HUB_PASSWORD }} password: ${{ secrets.FASTGPT_ALI_IMAGE_PSW }}
- name: Build for ${{ matrix.archs.arch }} - name: Build for ${{ matrix.archs.arch }}
id: build id: build
...@@ -65,7 +65,7 @@ jobs: ...@@ -65,7 +65,7 @@ jobs:
labels: | labels: |
org.opencontainers.image.source=https://github.com/${{ github.repository }} org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.description=FastGPT Sandbox Server image org.opencontainers.image.description=FastGPT Sandbox Server image
outputs: type=image,"name=ghcr.io/${{ github.repository_owner }}/fastgpt-sandbox-server,${{ secrets.ALI_IMAGE_NAME }}/fastgpt-sandbox-server",push-by-digest=true,push=true outputs: type=image,"name=ghcr.io/${{ github.repository_owner }}/fastgpt-sandbox-server,${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-sandbox-server",push-by-digest=true,push=true
cache-from: type=local,src=/tmp/.buildx-cache cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache
...@@ -103,8 +103,8 @@ jobs: ...@@ -103,8 +103,8 @@ jobs:
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: registry.cn-hangzhou.aliyuncs.com registry: registry.cn-hangzhou.aliyuncs.com
username: ${{ secrets.ALI_HUB_USERNAME }} username: ${{ secrets.FASTGPT_ALI_IMAGE_USER }}
password: ${{ secrets.ALI_HUB_PASSWORD }} password: ${{ secrets.FASTGPT_ALI_IMAGE_PSW }}
- name: Download digests - name: Download digests
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
...@@ -122,8 +122,8 @@ jobs: ...@@ -122,8 +122,8 @@ jobs:
TAGS=( TAGS=(
"ghcr.io/${{ github.repository_owner }}/fastgpt-sandbox-server:${{ inputs.tag }}" "ghcr.io/${{ github.repository_owner }}/fastgpt-sandbox-server:${{ inputs.tag }}"
"ghcr.io/${{ github.repository_owner }}/fastgpt-sandbox-server:latest" "ghcr.io/${{ github.repository_owner }}/fastgpt-sandbox-server:latest"
"${{ secrets.ALI_IMAGE_NAME }}/fastgpt-sandbox-server:${{ inputs.tag }}" "${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-sandbox-server:${{ inputs.tag }}"
"${{ secrets.ALI_IMAGE_NAME }}/fastgpt-sandbox-server:latest" "${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-sandbox-server:latest"
) )
for TAG in "${TAGS[@]}"; do for TAG in "${TAGS[@]}"; do
docker buildx imagetools create -t $TAG \ docker buildx imagetools create -t $TAG \
......
...@@ -97,7 +97,7 @@ jobs: ...@@ -97,7 +97,7 @@ jobs:
with: with:
# list of Docker images to use as base name for tags # list of Docker images to use as base name for tags
images: | images: |
${{ secrets.ALI_IMAGE_NAME }}/fastgpt-docs ${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-docs
tags: | tags: |
${{ matrix.domain_config.suffix }}-${{ needs.generate-timestamp.outputs.datetime }} ${{ matrix.domain_config.suffix }}-${{ needs.generate-timestamp.outputs.datetime }}
flavor: latest=false flavor: latest=false
...@@ -106,8 +106,8 @@ jobs: ...@@ -106,8 +106,8 @@ jobs:
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: registry.cn-hangzhou.aliyuncs.com registry: registry.cn-hangzhou.aliyuncs.com
username: ${{ secrets.ALI_HUB_USERNAME }} username: ${{ secrets.FASTGPT_ALI_IMAGE_USER }}
password: ${{ secrets.ALI_HUB_PASSWORD }} password: ${{ secrets.FASTGPT_ALI_IMAGE_PSW }}
- name: Build and push Docker images (CN) - name: Build and push Docker images (CN)
if: matrix.domain_config.suffix == 'cn' if: matrix.domain_config.suffix == 'cn'
...@@ -166,8 +166,8 @@ jobs: ...@@ -166,8 +166,8 @@ jobs:
- name: Update deployment image - name: Update deployment image
run: | run: |
kubectl set image deployment/${{ matrix.domain_config.deployment }} ${{ matrix.domain_config.deployment }}=${{ secrets.ALI_IMAGE_NAME }}/fastgpt-docs:${{ matrix.domain_config.suffix }}-${{ needs.generate-timestamp.outputs.datetime }} kubectl set image deployment/${{ matrix.domain_config.deployment }} ${{ matrix.domain_config.deployment }}=${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-docs:${{ matrix.domain_config.suffix }}-${{ needs.generate-timestamp.outputs.datetime }}
- name: Annotate deployment - name: Annotate deployment
run: | run: |
kubectl annotate deployment/${{ matrix.domain_config.deployment }} originImageName="${{ secrets.ALI_IMAGE_NAME }}/fastgpt-docs:${{ matrix.domain_config.suffix }}-${{ needs.generate-timestamp.outputs.datetime }}" --overwrite kubectl annotate deployment/${{ matrix.domain_config.deployment }} originImageName="${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-docs:${{ matrix.domain_config.suffix }}-${{ needs.generate-timestamp.outputs.datetime }}" --overwrite
...@@ -32,7 +32,7 @@ jobs: ...@@ -32,7 +32,7 @@ jobs:
with: with:
# list of Docker images to use as base name for tags # list of Docker images to use as base name for tags
images: | images: |
${{ secrets.ALI_IMAGE_NAME }}/fastgpt-docs ${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-docs
tags: | tags: |
${{ steps.datetime.outputs.datetime }} ${{ steps.datetime.outputs.datetime }}
flavor: latest=false flavor: latest=false
...@@ -41,8 +41,8 @@ jobs: ...@@ -41,8 +41,8 @@ jobs:
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: registry.cn-hangzhou.aliyuncs.com registry: registry.cn-hangzhou.aliyuncs.com
username: ${{ secrets.ALI_HUB_USERNAME }} username: ${{ secrets.FASTGPT_ALI_IMAGE_USER }}
password: ${{ secrets.ALI_HUB_PASSWORD }} password: ${{ secrets.FASTGPT_ALI_IMAGE_PSW }}
- name: Build and push Docker images - name: Build and push Docker images
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
...@@ -75,11 +75,11 @@ jobs: ...@@ -75,11 +75,11 @@ jobs:
- name: Update deployment image - name: Update deployment image
run: | run: |
kubectl set image deployment/fastgpt-docs-preview fastgpt-docs-preview=${{ secrets.ALI_IMAGE_NAME }}/fastgpt-docs:${{ needs.build-images.outputs.tags }} kubectl set image deployment/fastgpt-docs-preview fastgpt-docs-preview=${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-docs:${{ needs.build-images.outputs.tags }}
- name: Annotate deployment - name: Annotate deployment
run: | run: |
kubectl annotate deployment/fastgpt-docs-preview originImageName="${{ secrets.ALI_IMAGE_NAME }}/fastgpt-docs:${{ needs.build-images.outputs.tags }}" --overwrite kubectl annotate deployment/fastgpt-docs-preview originImageName="${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-docs:${{ needs.build-images.outputs.tags }}" --overwrite
- name: '@finleyge/github-tools' - name: '@finleyge/github-tools'
uses: FinleyGe/github-tools@0.0.1 uses: FinleyGe/github-tools@0.0.1
......
...@@ -4,10 +4,10 @@ on: ...@@ -4,10 +4,10 @@ on:
workflow_dispatch: workflow_dispatch:
push: push:
paths: paths:
- "projects/app/**" - 'projects/app/**'
- "packages/**" - 'packages/**'
tags: tags:
- "v*" - 'v*'
jobs: jobs:
build-fastgpt-images: build-fastgpt-images:
...@@ -20,11 +20,11 @@ jobs: ...@@ -20,11 +20,11 @@ jobs:
matrix: matrix:
sub_routes: sub_routes:
- repo: fastgpt - repo: fastgpt
base_url: "" base_url: ''
- repo: fastgpt-sub-route - repo: fastgpt-sub-route
base_url: "/fastai" base_url: '/fastai'
- repo: fastgpt-sub-route-gchat - repo: fastgpt-sub-route-gchat
base_url: "/gchat" base_url: '/gchat'
archs: archs:
- arch: amd64 - arch: amd64
- arch: arm64 - arch: arm64
...@@ -61,8 +61,8 @@ jobs: ...@@ -61,8 +61,8 @@ jobs:
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: registry.cn-hangzhou.aliyuncs.com registry: registry.cn-hangzhou.aliyuncs.com
username: ${{ secrets.ALI_HUB_USERNAME }} username: ${{ secrets.FASTGPT_ALI_IMAGE_USER }}
password: ${{ secrets.ALI_HUB_PASSWORD }} password: ${{ secrets.FASTGPT_ALI_IMAGE_PSW }}
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
...@@ -81,7 +81,7 @@ jobs: ...@@ -81,7 +81,7 @@ jobs:
labels: | labels: |
org.opencontainers.image.source=https://github.com/${{ github.repository }} org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.description=${{ matrix.sub_routes.repo }} image org.opencontainers.image.description=${{ matrix.sub_routes.repo }} image
outputs: type=image,"name=ghcr.io/${{ github.repository_owner }}/${{ matrix.sub_routes.repo }},${{ secrets.ALI_IMAGE_NAME }}/${{ matrix.sub_routes.repo }},${{ secrets.DOCKER_IMAGE_NAME }}/${{ matrix.sub_routes.repo }}",push-by-digest=true,push=true outputs: type=image,"name=ghcr.io/${{ github.repository_owner }}/${{ matrix.sub_routes.repo }},${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/${{ matrix.sub_routes.repo }},${{ secrets.DOCKER_IMAGE_NAME }}/${{ matrix.sub_routes.repo }}",push-by-digest=true,push=true
cache-from: type=local,src=/tmp/.buildx-cache cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache
...@@ -124,8 +124,8 @@ jobs: ...@@ -124,8 +124,8 @@ jobs:
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: registry.cn-hangzhou.aliyuncs.com registry: registry.cn-hangzhou.aliyuncs.com
username: ${{ secrets.ALI_HUB_USERNAME }} username: ${{ secrets.FASTGPT_ALI_IMAGE_USER }}
password: ${{ secrets.ALI_HUB_PASSWORD }} password: ${{ secrets.FASTGPT_ALI_IMAGE_PSW }}
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
...@@ -147,15 +147,15 @@ jobs: ...@@ -147,15 +147,15 @@ jobs:
if [[ "${{ github.ref_name }}" == "main" ]]; then if [[ "${{ github.ref_name }}" == "main" ]]; then
echo "Git_Tag=ghcr.io/${{ github.repository_owner }}/${{ matrix.sub_routes.repo }}:latest" >> $GITHUB_ENV echo "Git_Tag=ghcr.io/${{ github.repository_owner }}/${{ matrix.sub_routes.repo }}:latest" >> $GITHUB_ENV
echo "Git_Latest=ghcr.io/${{ github.repository_owner }}/${{ matrix.sub_routes.repo }}:latest" >> $GITHUB_ENV echo "Git_Latest=ghcr.io/${{ github.repository_owner }}/${{ matrix.sub_routes.repo }}:latest" >> $GITHUB_ENV
echo "Ali_Tag=${{ secrets.ALI_IMAGE_NAME }}/${{ matrix.sub_routes.repo }}:latest" >> $GITHUB_ENV echo "Ali_Tag=${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/${{ matrix.sub_routes.repo }}:latest" >> $GITHUB_ENV
echo "Ali_Latest=${{ secrets.ALI_IMAGE_NAME }}/${{ matrix.sub_routes.repo }}:latest" >> $GITHUB_ENV echo "Ali_Latest=${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/${{ matrix.sub_routes.repo }}:latest" >> $GITHUB_ENV
echo "Docker_Hub_Tag=${{ secrets.DOCKER_IMAGE_NAME }}/${{ matrix.sub_routes.repo }}:latest" >> $GITHUB_ENV echo "Docker_Hub_Tag=${{ secrets.DOCKER_IMAGE_NAME }}/${{ matrix.sub_routes.repo }}:latest" >> $GITHUB_ENV
echo "Docker_Hub_Latest=${{ secrets.DOCKER_IMAGE_NAME }}/${{ matrix.sub_routes.repo }}:latest" >> $GITHUB_ENV echo "Docker_Hub_Latest=${{ secrets.DOCKER_IMAGE_NAME }}/${{ matrix.sub_routes.repo }}:latest" >> $GITHUB_ENV
else else
echo "Git_Tag=ghcr.io/${{ github.repository_owner }}/${{ matrix.sub_routes.repo }}:${{ github.ref_name }}" >> $GITHUB_ENV echo "Git_Tag=ghcr.io/${{ github.repository_owner }}/${{ matrix.sub_routes.repo }}:${{ github.ref_name }}" >> $GITHUB_ENV
echo "Git_Latest=ghcr.io/${{ github.repository_owner }}/${{ matrix.sub_routes.repo }}:latest" >> $GITHUB_ENV echo "Git_Latest=ghcr.io/${{ github.repository_owner }}/${{ matrix.sub_routes.repo }}:latest" >> $GITHUB_ENV
echo "Ali_Tag=${{ secrets.ALI_IMAGE_NAME }}/${{ matrix.sub_routes.repo }}:${{ github.ref_name }}" >> $GITHUB_ENV echo "Ali_Tag=${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/${{ matrix.sub_routes.repo }}:${{ github.ref_name }}" >> $GITHUB_ENV
echo "Ali_Latest=${{ secrets.ALI_IMAGE_NAME }}/${{ matrix.sub_routes.repo }}:latest" >> $GITHUB_ENV echo "Ali_Latest=${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/${{ matrix.sub_routes.repo }}:latest" >> $GITHUB_ENV
echo "Docker_Hub_Tag=${{ secrets.DOCKER_IMAGE_NAME }}/${{ matrix.sub_routes.repo }}:${{ github.ref_name }}" >> $GITHUB_ENV echo "Docker_Hub_Tag=${{ secrets.DOCKER_IMAGE_NAME }}/${{ matrix.sub_routes.repo }}:${{ github.ref_name }}" >> $GITHUB_ENV
echo "Docker_Hub_Latest=${{ secrets.DOCKER_IMAGE_NAME }}/${{ matrix.sub_routes.repo }}:latest" >> $GITHUB_ENV echo "Docker_Hub_Latest=${{ secrets.DOCKER_IMAGE_NAME }}/${{ matrix.sub_routes.repo }}:latest" >> $GITHUB_ENV
fi fi
......
...@@ -45,8 +45,8 @@ jobs: ...@@ -45,8 +45,8 @@ jobs:
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: registry.cn-hangzhou.aliyuncs.com registry: registry.cn-hangzhou.aliyuncs.com
username: ${{ secrets.ALI_HUB_USERNAME }} username: ${{ secrets.FASTGPT_ALI_IMAGE_USER }}
password: ${{ secrets.ALI_HUB_PASSWORD }} password: ${{ secrets.FASTGPT_ALI_IMAGE_PSW }}
- name: Set image config - name: Set image config
id: config id: config
...@@ -54,15 +54,15 @@ jobs: ...@@ -54,15 +54,15 @@ jobs:
if [[ "${{ matrix.image }}" == "fastgpt" ]]; then if [[ "${{ matrix.image }}" == "fastgpt" ]]; then
echo "DOCKERFILE=projects/app/Dockerfile" >> $GITHUB_OUTPUT echo "DOCKERFILE=projects/app/Dockerfile" >> $GITHUB_OUTPUT
echo "DESCRIPTION=fastgpt-pr image" >> $GITHUB_OUTPUT echo "DESCRIPTION=fastgpt-pr image" >> $GITHUB_OUTPUT
echo "DOCKER_REPO_TAGGED=${{ secrets.ALI_IMAGE_NAME }}/fastgpt-pr:fatsgpt_${{ github.event.pull_request.head.sha }}" >> $GITHUB_OUTPUT echo "DOCKER_REPO_TAGGED=${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-pr:fatsgpt_${{ github.event.pull_request.head.sha }}" >> $GITHUB_OUTPUT
elif [[ "${{ matrix.image }}" == "sandbox" ]]; then elif [[ "${{ matrix.image }}" == "sandbox" ]]; then
echo "DOCKERFILE=projects/sandbox/Dockerfile" >> $GITHUB_OUTPUT echo "DOCKERFILE=projects/sandbox/Dockerfile" >> $GITHUB_OUTPUT
echo "DESCRIPTION=fastgpt-sandbox-pr image" >> $GITHUB_OUTPUT echo "DESCRIPTION=fastgpt-sandbox-pr image" >> $GITHUB_OUTPUT
echo "DOCKER_REPO_TAGGED=${{ secrets.ALI_IMAGE_NAME }}/fastgpt-pr:fatsgpt_sandbox_${{ github.event.pull_request.head.sha }}" >> $GITHUB_OUTPUT echo "DOCKER_REPO_TAGGED=${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-pr:fatsgpt_sandbox_${{ github.event.pull_request.head.sha }}" >> $GITHUB_OUTPUT
elif [[ "${{ matrix.image }}" == "mcp_server" ]]; then elif [[ "${{ matrix.image }}" == "mcp_server" ]]; then
echo "DOCKERFILE=projects/mcp_server/Dockerfile" >> $GITHUB_OUTPUT echo "DOCKERFILE=projects/mcp_server/Dockerfile" >> $GITHUB_OUTPUT
echo "DESCRIPTION=fastgpt-mcp_server-pr image" >> $GITHUB_OUTPUT echo "DESCRIPTION=fastgpt-mcp_server-pr image" >> $GITHUB_OUTPUT
echo "DOCKER_REPO_TAGGED=${{ secrets.ALI_IMAGE_NAME }}/fastgpt-pr:fatsgpt_mcp_server_${{ github.event.pull_request.head.sha }}" >> $GITHUB_OUTPUT echo "DOCKER_REPO_TAGGED=${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-pr:fatsgpt_mcp_server_${{ github.event.pull_request.head.sha }}" >> $GITHUB_OUTPUT
fi fi
- name: Build ${{ matrix.image }} image for PR - name: Build ${{ matrix.image }} image for PR
......
...@@ -44,8 +44,8 @@ jobs: ...@@ -44,8 +44,8 @@ jobs:
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: registry.cn-hangzhou.aliyuncs.com registry: registry.cn-hangzhou.aliyuncs.com
username: ${{ secrets.ALI_HUB_USERNAME }} username: ${{ secrets.FASTGPT_ALI_IMAGE_USER }}
password: ${{ secrets.ALI_HUB_PASSWORD }} password: ${{ secrets.FASTGPT_ALI_IMAGE_PSW }}
- name: Build for ${{ matrix.arch }} - name: Build for ${{ matrix.arch }}
id: build id: build
...@@ -57,7 +57,7 @@ jobs: ...@@ -57,7 +57,7 @@ jobs:
labels: | labels: |
org.opencontainers.image.source=https://github.com/${{ github.repository }} org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.description=fastgpt-marketplace image org.opencontainers.image.description=fastgpt-marketplace image
outputs: type=image,"name=ghcr.io/${{ github.repository_owner }}/fastgpt-marketplace,${{ secrets.ALI_IMAGE_NAME }}/fastgpt-marketplace",push-by-digest=true,push=true outputs: type=image,"name=ghcr.io/${{ github.repository_owner }}/fastgpt-marketplace,${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-marketplace",push-by-digest=true,push=true
cache-from: type=local,src=/tmp/.buildx-cache cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache
...@@ -94,8 +94,8 @@ jobs: ...@@ -94,8 +94,8 @@ jobs:
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: registry.cn-hangzhou.aliyuncs.com registry: registry.cn-hangzhou.aliyuncs.com
username: ${{ secrets.ALI_HUB_USERNAME }} username: ${{ secrets.FASTGPT_ALI_IMAGE_USER }}
password: ${{ secrets.ALI_HUB_PASSWORD }} password: ${{ secrets.FASTGPT_ALI_IMAGE_PSW }}
- name: Download digests - name: Download digests
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
...@@ -118,7 +118,7 @@ jobs: ...@@ -118,7 +118,7 @@ jobs:
- name: Set image name and tag - name: Set image name and tag
run: | run: |
echo "Git_Tag=ghcr.io/${{ github.repository_owner }}/fastgpt-marketplace:${{ env.RANDOM_TAG }}" >> $GITHUB_ENV echo "Git_Tag=ghcr.io/${{ github.repository_owner }}/fastgpt-marketplace:${{ env.RANDOM_TAG }}" >> $GITHUB_ENV
echo "Ali_Tag=${{ secrets.ALI_IMAGE_NAME }}/fastgpt-marketplace:${{ env.RANDOM_TAG }}" >> $GITHUB_ENV echo "Ali_Tag=${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-marketplace:${{ env.RANDOM_TAG }}" >> $GITHUB_ENV
- name: Create manifest list and push - name: Create manifest list and push
working-directory: ${{ runner.temp }}/digests working-directory: ${{ runner.temp }}/digests
...@@ -138,7 +138,7 @@ jobs: ...@@ -138,7 +138,7 @@ jobs:
# Create manifest for Ali Cloud # Create manifest for Ali Cloud
echo "Creating manifest for Ali Cloud: ${Ali_Tag}" echo "Creating manifest for Ali Cloud: ${Ali_Tag}"
docker buildx imagetools create -t ${Ali_Tag} \ docker buildx imagetools create -t ${Ali_Tag} \
$(printf '${{ secrets.ALI_IMAGE_NAME }}/fastgpt-marketplace@sha256:%s ' *) $(printf '${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-marketplace@sha256:%s ' *)
echo "✅ Ali Cloud manifest created" echo "✅ Ali Cloud manifest created"
echo "" echo ""
......
...@@ -49,8 +49,8 @@ jobs: ...@@ -49,8 +49,8 @@ jobs:
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: registry.cn-hangzhou.aliyuncs.com registry: registry.cn-hangzhou.aliyuncs.com
username: ${{ secrets.ALI_HUB_USERNAME }} username: ${{ secrets.FASTGPT_ALI_IMAGE_USER }}
password: ${{ secrets.ALI_HUB_PASSWORD }} password: ${{ secrets.FASTGPT_ALI_IMAGE_PSW }}
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
...@@ -67,7 +67,7 @@ jobs: ...@@ -67,7 +67,7 @@ jobs:
labels: | labels: |
org.opencontainers.image.source=https://github.com/${{ github.repository }} org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.description=fastgpt-mcp_server image org.opencontainers.image.description=fastgpt-mcp_server image
outputs: type=image,"name=ghcr.io/${{ github.repository_owner }}/fastgpt-mcp_server,${{ secrets.ALI_IMAGE_NAME }}/fastgpt-mcp_server,${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-mcp_server",push-by-digest=true,push=true outputs: type=image,"name=ghcr.io/${{ github.repository_owner }}/fastgpt-mcp_server,${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-mcp_server,${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-mcp_server",push-by-digest=true,push=true
cache-from: type=local,src=/tmp/.buildx-cache cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache
...@@ -104,8 +104,8 @@ jobs: ...@@ -104,8 +104,8 @@ jobs:
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: registry.cn-hangzhou.aliyuncs.com registry: registry.cn-hangzhou.aliyuncs.com
username: ${{ secrets.ALI_HUB_USERNAME }} username: ${{ secrets.FASTGPT_ALI_IMAGE_USER }}
password: ${{ secrets.ALI_HUB_PASSWORD }} password: ${{ secrets.FASTGPT_ALI_IMAGE_PSW }}
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
...@@ -127,15 +127,15 @@ jobs: ...@@ -127,15 +127,15 @@ jobs:
if [[ "${{ github.ref_name }}" == "main" ]]; then if [[ "${{ github.ref_name }}" == "main" ]]; then
echo "Git_Tag=ghcr.io/${{ github.repository_owner }}/fastgpt-mcp_server:latest" >> $GITHUB_ENV echo "Git_Tag=ghcr.io/${{ github.repository_owner }}/fastgpt-mcp_server:latest" >> $GITHUB_ENV
echo "Git_Latest=ghcr.io/${{ github.repository_owner }}/fastgpt-mcp_server:latest" >> $GITHUB_ENV echo "Git_Latest=ghcr.io/${{ github.repository_owner }}/fastgpt-mcp_server:latest" >> $GITHUB_ENV
echo "Ali_Tag=${{ secrets.ALI_IMAGE_NAME }}/fastgpt-mcp_server:latest" >> $GITHUB_ENV echo "Ali_Tag=${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-mcp_server:latest" >> $GITHUB_ENV
echo "Ali_Latest=${{ secrets.ALI_IMAGE_NAME }}/fastgpt-mcp_server:latest" >> $GITHUB_ENV echo "Ali_Latest=${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-mcp_server:latest" >> $GITHUB_ENV
echo "Docker_Hub_Tag=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-mcp_server:latest" >> $GITHUB_ENV echo "Docker_Hub_Tag=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-mcp_server:latest" >> $GITHUB_ENV
echo "Docker_Hub_Latest=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-mcp_server:latest" >> $GITHUB_ENV echo "Docker_Hub_Latest=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-mcp_server:latest" >> $GITHUB_ENV
else else
echo "Git_Tag=ghcr.io/${{ github.repository_owner }}/fastgpt-mcp_server:${{ github.ref_name }}" >> $GITHUB_ENV echo "Git_Tag=ghcr.io/${{ github.repository_owner }}/fastgpt-mcp_server:${{ github.ref_name }}" >> $GITHUB_ENV
echo "Git_Latest=ghcr.io/${{ github.repository_owner }}/fastgpt-mcp_server:latest" >> $GITHUB_ENV echo "Git_Latest=ghcr.io/${{ github.repository_owner }}/fastgpt-mcp_server:latest" >> $GITHUB_ENV
echo "Ali_Tag=${{ secrets.ALI_IMAGE_NAME }}/fastgpt-mcp_server:${{ github.ref_name }}" >> $GITHUB_ENV echo "Ali_Tag=${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-mcp_server:${{ github.ref_name }}" >> $GITHUB_ENV
echo "Ali_Latest=${{ secrets.ALI_IMAGE_NAME }}/fastgpt-mcp_server:latest" >> $GITHUB_ENV echo "Ali_Latest=${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-mcp_server:latest" >> $GITHUB_ENV
echo "Docker_Hub_Tag=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-mcp_server:${{ github.ref_name }}" >> $GITHUB_ENV echo "Docker_Hub_Tag=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-mcp_server:${{ github.ref_name }}" >> $GITHUB_ENV
echo "Docker_Hub_Latest=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-mcp_server:latest" >> $GITHUB_ENV echo "Docker_Hub_Latest=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-mcp_server:latest" >> $GITHUB_ENV
fi fi
......
...@@ -49,8 +49,8 @@ jobs: ...@@ -49,8 +49,8 @@ jobs:
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: registry.cn-hangzhou.aliyuncs.com registry: registry.cn-hangzhou.aliyuncs.com
username: ${{ secrets.ALI_HUB_USERNAME }} username: ${{ secrets.FASTGPT_ALI_IMAGE_USER }}
password: ${{ secrets.ALI_HUB_PASSWORD }} password: ${{ secrets.FASTGPT_ALI_IMAGE_PSW }}
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
...@@ -67,7 +67,7 @@ jobs: ...@@ -67,7 +67,7 @@ jobs:
labels: | labels: |
org.opencontainers.image.source=https://github.com/${{ github.repository }} org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.description=fastgpt-sandbox image org.opencontainers.image.description=fastgpt-sandbox image
outputs: type=image,"name=ghcr.io/${{ github.repository_owner }}/fastgpt-sandbox,${{ secrets.ALI_IMAGE_NAME }}/fastgpt-sandbox,${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-sandbox",push-by-digest=true,push=true outputs: type=image,"name=ghcr.io/${{ github.repository_owner }}/fastgpt-sandbox,${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-sandbox,${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-sandbox",push-by-digest=true,push=true
cache-from: type=local,src=/tmp/.buildx-cache cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache
...@@ -104,8 +104,8 @@ jobs: ...@@ -104,8 +104,8 @@ jobs:
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: registry.cn-hangzhou.aliyuncs.com registry: registry.cn-hangzhou.aliyuncs.com
username: ${{ secrets.ALI_HUB_USERNAME }} username: ${{ secrets.FASTGPT_ALI_IMAGE_USER }}
password: ${{ secrets.ALI_HUB_PASSWORD }} password: ${{ secrets.FASTGPT_ALI_IMAGE_PSW }}
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
...@@ -127,15 +127,15 @@ jobs: ...@@ -127,15 +127,15 @@ jobs:
if [[ "${{ github.ref_name }}" == "main" ]]; then if [[ "${{ github.ref_name }}" == "main" ]]; then
echo "Git_Tag=ghcr.io/${{ github.repository_owner }}/fastgpt-sandbox:latest" >> $GITHUB_ENV echo "Git_Tag=ghcr.io/${{ github.repository_owner }}/fastgpt-sandbox:latest" >> $GITHUB_ENV
echo "Git_Latest=ghcr.io/${{ github.repository_owner }}/fastgpt-sandbox:latest" >> $GITHUB_ENV echo "Git_Latest=ghcr.io/${{ github.repository_owner }}/fastgpt-sandbox:latest" >> $GITHUB_ENV
echo "Ali_Tag=${{ secrets.ALI_IMAGE_NAME }}/fastgpt-sandbox:latest" >> $GITHUB_ENV echo "Ali_Tag=${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-sandbox:latest" >> $GITHUB_ENV
echo "Ali_Latest=${{ secrets.ALI_IMAGE_NAME }}/fastgpt-sandbox:latest" >> $GITHUB_ENV echo "Ali_Latest=${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-sandbox:latest" >> $GITHUB_ENV
echo "Docker_Hub_Tag=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-sandbox:latest" >> $GITHUB_ENV echo "Docker_Hub_Tag=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-sandbox:latest" >> $GITHUB_ENV
echo "Docker_Hub_Latest=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-sandbox:latest" >> $GITHUB_ENV echo "Docker_Hub_Latest=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-sandbox:latest" >> $GITHUB_ENV
else else
echo "Git_Tag=ghcr.io/${{ github.repository_owner }}/fastgpt-sandbox:${{ github.ref_name }}" >> $GITHUB_ENV echo "Git_Tag=ghcr.io/${{ github.repository_owner }}/fastgpt-sandbox:${{ github.ref_name }}" >> $GITHUB_ENV
echo "Git_Latest=ghcr.io/${{ github.repository_owner }}/fastgpt-sandbox:latest" >> $GITHUB_ENV echo "Git_Latest=ghcr.io/${{ github.repository_owner }}/fastgpt-sandbox:latest" >> $GITHUB_ENV
echo "Ali_Tag=${{ secrets.ALI_IMAGE_NAME }}/fastgpt-sandbox:${{ github.ref_name }}" >> $GITHUB_ENV echo "Ali_Tag=${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-sandbox:${{ github.ref_name }}" >> $GITHUB_ENV
echo "Ali_Latest=${{ secrets.ALI_IMAGE_NAME }}/fastgpt-sandbox:latest" >> $GITHUB_ENV echo "Ali_Latest=${{ secrets.FASTGPT_ALI_IMAGE_PREFIX }}/fastgpt-sandbox:latest" >> $GITHUB_ENV
echo "Docker_Hub_Tag=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-sandbox:${{ github.ref_name }}" >> $GITHUB_ENV echo "Docker_Hub_Tag=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-sandbox:${{ github.ref_name }}" >> $GITHUB_ENV
echo "Docker_Hub_Latest=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-sandbox:latest" >> $GITHUB_ENV echo "Docker_Hub_Latest=${{ secrets.DOCKER_IMAGE_NAME }}/fastgpt-sandbox:latest" >> $GITHUB_ENV
fi fi
......
...@@ -2,7 +2,9 @@ import z from 'zod'; ...@@ -2,7 +2,9 @@ import z from 'zod';
import { StandardSubLevelEnum, SubModeEnum, SubTypeEnum } from './constants'; import { StandardSubLevelEnum, SubModeEnum, SubTypeEnum } from './constants';
import { ObjectIdSchema } from '../../../common/type/mongo'; import { ObjectIdSchema } from '../../../common/type/mongo';
// Content of plan /**
* Static plan config, stored in global.subPlans
*/
export const TeamStandardSubPlanItemSchema = z.object({ export const TeamStandardSubPlanItemSchema = z.object({
name: z.string().optional(), name: z.string().optional(),
desc: z.string().optional(), desc: z.string().optional(),
...@@ -71,6 +73,10 @@ export const SubPlanSchema = z.object({ ...@@ -71,6 +73,10 @@ export const SubPlanSchema = z.object({
}); });
export type SubPlanType = z.infer<typeof SubPlanSchema>; export type SubPlanType = z.infer<typeof SubPlanSchema>;
/**
* TeamSub Schema in DB.
* Configs are optional
*/
export const TeamSubSchema = z.object({ export const TeamSubSchema = z.object({
_id: ObjectIdSchema, _id: ObjectIdSchema,
teamId: ObjectIdSchema, teamId: ObjectIdSchema,
...@@ -105,9 +111,30 @@ export const TeamSubSchema = z.object({ ...@@ -105,9 +111,30 @@ export const TeamSubSchema = z.object({
}); });
export type TeamSubSchemaType = z.infer<typeof TeamSubSchema>; export type TeamSubSchemaType = z.infer<typeof TeamSubSchema>;
/**
* Merged plan type: combines DB subscription record metadata with effective plan limits
*
* Omits:
* - maxApp/maxDataset from TeamSubSchema: 这些字段在 DB 中存储,但在合并后的类型中使用 maxAppAmount/maxDatasetAmount
* - pointPrice from TeamStandardSubPlanItemSchema: 避免与 price 字段冲突
*
* Field priority: TeamStandardSubPlanItemSchema fields override TeamSubSchema fields when both exist
*/ export const TeamPlanStandardSchema = z.object({
...TeamSubSchema.omit({
maxApp: true,
maxDataset: true
}).shape,
...TeamStandardSubPlanItemSchema.omit({
pointPrice: true,
price: true
}).shape,
price: z.number().optional()
});
export type TeamPlanStandardType = z.infer<typeof TeamPlanStandardSchema>;
export const TeamPlanStatusSchema = z.object({ export const TeamPlanStatusSchema = z.object({
[SubTypeEnum.standard]: TeamSubSchema.optional(), [SubTypeEnum.standard]: TeamPlanStandardSchema.optional(),
standardConstants: TeamStandardSubPlanItemSchema.optional(),
totalPoints: z.int(), totalPoints: z.int(),
usedPoints: z.int(), usedPoints: z.int(),
datasetMaxSize: z.int() datasetMaxSize: z.int()
......
...@@ -25,7 +25,7 @@ export const checkTeamAIPoints = async (teamId: string) => { ...@@ -25,7 +25,7 @@ export const checkTeamAIPoints = async (teamId: string) => {
}; };
export const checkTeamMemberLimit = async (teamId: string, newCount: number) => { export const checkTeamMemberLimit = async (teamId: string, newCount: number) => {
const [{ standardConstants }, memberCount] = await Promise.all([ const [{ standard }, memberCount] = await Promise.all([
getTeamStandPlan({ getTeamStandPlan({
teamId teamId
}), }),
...@@ -35,7 +35,7 @@ export const checkTeamMemberLimit = async (teamId: string, newCount: number) => ...@@ -35,7 +35,7 @@ export const checkTeamMemberLimit = async (teamId: string, newCount: number) =>
}) })
]); ]);
if (standardConstants && newCount + memberCount > standardConstants.maxTeamMember) { if (standard?.maxTeamMember && newCount + memberCount > standard.maxTeamMember) {
return Promise.reject(TeamErrEnum.teamOverSize); return Promise.reject(TeamErrEnum.teamOverSize);
} }
}; };
...@@ -50,7 +50,7 @@ export const checkTeamAppTypeLimit = async ({ ...@@ -50,7 +50,7 @@ export const checkTeamAppTypeLimit = async ({
amount?: number; amount?: number;
}) => { }) => {
if (appCheckType === 'app') { if (appCheckType === 'app') {
const [{ standardConstants }, appCount] = await Promise.all([ const [{ standard }, appCount] = await Promise.all([
getTeamStandPlan({ teamId }), getTeamStandPlan({ teamId }),
MongoApp.countDocuments({ MongoApp.countDocuments({
teamId, teamId,
...@@ -60,7 +60,7 @@ export const checkTeamAppTypeLimit = async ({ ...@@ -60,7 +60,7 @@ export const checkTeamAppTypeLimit = async ({
}) })
]); ]);
if (standardConstants && appCount + amount > standardConstants.maxAppAmount) { if (standard?.maxAppAmount && appCount + amount > standard.maxAppAmount) {
return Promise.reject(TeamErrEnum.appAmountNotEnough); return Promise.reject(TeamErrEnum.appAmountNotEnough);
} }
...@@ -107,10 +107,10 @@ export const checkDatasetIndexLimit = async ({ ...@@ -107,10 +107,10 @@ export const checkDatasetIndexLimit = async ({
teamId: string; teamId: string;
insertLen?: number; insertLen?: number;
}) => { }) => {
const [{ standardConstants, totalPoints, usedPoints, datasetMaxSize }, usedDatasetIndexSize] = const [{ standard, totalPoints, usedPoints, datasetMaxSize }, usedDatasetIndexSize] =
await Promise.all([getTeamPlanStatus({ teamId }), getVectorCountByTeamId(teamId)]); await Promise.all([getTeamPlanStatus({ teamId }), getVectorCountByTeamId(teamId)]);
if (!standardConstants) return; if (!standard) return;
if (usedDatasetIndexSize + insertLen >= datasetMaxSize) { if (usedDatasetIndexSize + insertLen >= datasetMaxSize) {
return Promise.reject(TeamErrEnum.datasetSizeNotEnough); return Promise.reject(TeamErrEnum.datasetSizeNotEnough);
...@@ -123,7 +123,7 @@ export const checkDatasetIndexLimit = async ({ ...@@ -123,7 +123,7 @@ export const checkDatasetIndexLimit = async ({
}; };
export const checkTeamDatasetLimit = async (teamId: string) => { export const checkTeamDatasetLimit = async (teamId: string) => {
const [{ standardConstants }, datasetCount] = await Promise.all([ const [{ standard }, datasetCount] = await Promise.all([
getTeamStandPlan({ teamId }), getTeamStandPlan({ teamId }),
MongoDataset.countDocuments({ MongoDataset.countDocuments({
teamId, teamId,
...@@ -132,7 +132,7 @@ export const checkTeamDatasetLimit = async (teamId: string) => { ...@@ -132,7 +132,7 @@ export const checkTeamDatasetLimit = async (teamId: string) => {
]); ]);
// User check // User check
if (standardConstants && datasetCount >= standardConstants.maxDatasetAmount) { if (standard?.maxDatasetAmount && datasetCount >= standard.maxDatasetAmount) {
return Promise.reject(TeamErrEnum.datasetAmountNotEnough); return Promise.reject(TeamErrEnum.datasetAmountNotEnough);
} }
...@@ -148,11 +148,11 @@ export const checkTeamDatasetLimit = async (teamId: string) => { ...@@ -148,11 +148,11 @@ export const checkTeamDatasetLimit = async (teamId: string) => {
}; };
export const checkTeamDatasetSyncPermission = async (teamId: string) => { export const checkTeamDatasetSyncPermission = async (teamId: string) => {
const { standardConstants } = await getTeamStandPlan({ const { standard } = await getTeamStandPlan({
teamId teamId
}); });
if (standardConstants && !standardConstants?.websiteSyncPerDataset) { if (standard && !standard?.websiteSyncPerDataset) {
return Promise.reject(TeamErrEnum.websiteSyncNotEnough); return Promise.reject(TeamErrEnum.websiteSyncNotEnough);
} }
}; };
...@@ -5,9 +5,11 @@ import { ...@@ -5,9 +5,11 @@ import {
standardSubLevelMap standardSubLevelMap
} from '@fastgpt/global/support/wallet/sub/constants'; } from '@fastgpt/global/support/wallet/sub/constants';
import { MongoTeamSub } from './schema'; import { MongoTeamSub } from './schema';
import { import type {
type TeamPlanStatusType, TeamStandardSubPlanItemType,
type TeamSubSchemaType TeamPlanStatusType,
TeamPlanStandardType,
TeamSubSchemaType
} from '@fastgpt/global/support/wallet/sub/type'; } from '@fastgpt/global/support/wallet/sub/type';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { type ClientSession } from '../../../common/mongo'; import { type ClientSession } from '../../../common/mongo';
...@@ -38,55 +40,33 @@ export const sortStandPlans = (plans: TeamSubSchemaType[]) => { ...@@ -38,55 +40,33 @@ export const sortStandPlans = (plans: TeamSubSchemaType[]) => {
standardSubLevelMap[b.currentSubLevel].weight - standardSubLevelMap[a.currentSubLevel].weight standardSubLevelMap[b.currentSubLevel].weight - standardSubLevelMap[a.currentSubLevel].weight
); );
}; };
export const getTeamStandPlan = async ({ teamId }: { teamId: string }) => { export const buildStandardPlan = (
const plans = await MongoTeamSub.find( standard: TeamSubSchemaType,
{ standardConstants: TeamStandardSubPlanItemType
teamId, ): TeamPlanStandardType => ({
type: SubTypeEnum.standard ...standard,
}, name: standardConstants.name,
undefined, desc: standardConstants.desc,
{ price: standardConstants.price,
...readFromSecondary priceDescription: standardConstants.priceDescription,
} customFormUrl: standardConstants.customFormUrl,
); customDescriptions: standardConstants.customDescriptions,
sortStandPlans(plans); wecom: standardConstants.wecom,
maxTeamMember: standard?.maxTeamMember ?? standardConstants.maxTeamMember,
const standardPlans = global.subPlans?.standard; maxAppAmount: standard?.maxApp ?? standardConstants.maxAppAmount,
const standard = plans[0]; maxDatasetAmount: standard?.maxDataset ?? standardConstants.maxDatasetAmount,
requestsPerMinute: standard?.requestsPerMinute ?? standardConstants.requestsPerMinute,
const standardConstants = chatHistoryStoreDuration:
standard.currentSubLevel && standardPlans standard?.chatHistoryStoreDuration ?? standardConstants.chatHistoryStoreDuration,
? standardPlans[ maxDatasetSize: standard?.maxDatasetSize ?? standardConstants.maxDatasetSize,
standard.currentSubLevel === StandardSubLevelEnum.custom websiteSyncPerDataset: standard?.websiteSyncPerDataset ?? standardConstants.websiteSyncPerDataset,
? StandardSubLevelEnum.advanced appRegistrationCount: standard?.appRegistrationCount ?? standardConstants.appRegistrationCount,
: standard.currentSubLevel auditLogStoreDuration: standard?.auditLogStoreDuration ?? standardConstants.auditLogStoreDuration,
] ticketResponseTime: standard?.ticketResponseTime ?? standardConstants.ticketResponseTime,
: undefined; customDomain: standard?.customDomain ?? standardConstants.customDomain,
maxUploadFileSize: standard?.maxUploadFileSize ?? standardConstants.maxUploadFileSize,
return { maxUploadFileCount: standard?.maxUploadFileCount ?? standardConstants.maxUploadFileCount
[SubTypeEnum.standard]: standard, });
standardConstants: standardConstants
? {
...standardConstants,
maxTeamMember: standard?.maxTeamMember ?? standardConstants.maxTeamMember,
maxAppAmount: standard?.maxApp ?? standardConstants.maxAppAmount,
maxDatasetAmount: standard?.maxDataset ?? standardConstants.maxDatasetAmount,
requestsPerMinute: standard?.requestsPerMinute ?? standardConstants.requestsPerMinute,
chatHistoryStoreDuration:
standard?.chatHistoryStoreDuration ?? standardConstants.chatHistoryStoreDuration,
maxDatasetSize: standard?.maxDatasetSize ?? standardConstants.maxDatasetSize,
websiteSyncPerDataset:
standard?.websiteSyncPerDataset ?? standardConstants.websiteSyncPerDataset,
appRegistrationCount:
standard?.appRegistrationCount ?? standardConstants.appRegistrationCount,
auditLogStoreDuration:
standard?.auditLogStoreDuration ?? standardConstants.auditLogStoreDuration,
ticketResponseTime: standard?.ticketResponseTime ?? standardConstants.ticketResponseTime,
customDomain: standard?.customDomain ?? standardConstants.customDomain
}
: undefined
};
};
export const initTeamFreePlan = async ({ export const initTeamFreePlan = async ({
teamId, teamId,
...@@ -176,11 +156,46 @@ export const initTeamFreePlan = async ({ ...@@ -176,11 +156,46 @@ export const initTeamFreePlan = async ({
); );
}; };
// 获取团队标准套餐
export const getTeamStandPlan = async ({ teamId }: { teamId: string }) => {
const plans = await MongoTeamSub.find(
{
teamId,
type: SubTypeEnum.standard
},
undefined,
{
...readFromSecondary
}
);
sortStandPlans(plans);
const standardPlans = global.subPlans?.standard;
const standard = plans[0];
const standardConstants =
standard.currentSubLevel && standardPlans
? standardPlans[
standard.currentSubLevel === StandardSubLevelEnum.custom
? StandardSubLevelEnum.advanced
: standard.currentSubLevel
]
: undefined;
return {
[SubTypeEnum.standard]: standardConstants
? buildStandardPlan(standard, standardConstants)
: undefined
};
};
// 获取团队所有套餐内容
export const getTeamPlanStatus = async ({ export const getTeamPlanStatus = async ({
teamId teamId
}: { }: {
teamId: string; teamId: string;
}): Promise<TeamPlanStatusType> => { }): Promise<TeamPlanStatusType> => {
/** 配置里的套餐 */
const standardPlans = global.subPlans?.standard; const standardPlans = global.subPlans?.standard;
/* Get all plans and datasetSize */ /* Get all plans and datasetSize */
...@@ -190,6 +205,7 @@ export const getTeamPlanStatus = async ({ ...@@ -190,6 +205,7 @@ export const getTeamPlanStatus = async ({
const teamStandardPlans = sortStandPlans( const teamStandardPlans = sortStandPlans(
plans.filter((plan) => plan.type === SubTypeEnum.standard) plans.filter((plan) => plan.type === SubTypeEnum.standard)
); );
/** 数据库里的,用户目前 active 的套餐 */
const standardPlan = teamStandardPlans[0]; const standardPlan = teamStandardPlans[0];
const extraDatasetSize = plans.filter((plan) => plan.type === SubTypeEnum.extraDatasetSize); const extraDatasetSize = plans.filter((plan) => plan.type === SubTypeEnum.extraDatasetSize);
...@@ -230,6 +246,7 @@ export const getTeamPlanStatus = async ({ ...@@ -230,6 +246,7 @@ export const getTeamPlanStatus = async ({
standardMaxDatasetSize + standardMaxDatasetSize +
extraDatasetSize.reduce((acc, cur) => acc + (cur.currentExtraDatasetSize || 0), 0); extraDatasetSize.reduce((acc, cur) => acc + (cur.currentExtraDatasetSize || 0), 0);
/** 静态的套餐配置,如果是 custom 则返回 advanced */
const standardConstants = const standardConstants =
standardPlan?.currentSubLevel && standardPlans standardPlan?.currentSubLevel && standardPlans
? standardPlans[ ? standardPlans[
...@@ -242,56 +259,8 @@ export const getTeamPlanStatus = async ({ ...@@ -242,56 +259,8 @@ export const getTeamPlanStatus = async ({
teamPoint.updateTeamPointsCache({ teamId, totalPoints, surplusPoints }); teamPoint.updateTeamPointsCache({ teamId, totalPoints, surplusPoints });
return { return {
standard: [SubTypeEnum.standard]: standardConstants
standardPlan.currentSubLevel === StandardSubLevelEnum.custom && standardConstants ? buildStandardPlan(standardPlan, standardConstants)
? {
...standardPlan,
maxTeamMember: standardPlan?.maxTeamMember ?? standardConstants.maxTeamMember,
maxApp: standardPlan?.maxApp ?? standardConstants.maxAppAmount,
maxDataset: standardPlan?.maxDataset ?? standardConstants.maxDatasetAmount,
requestsPerMinute:
standardPlan?.requestsPerMinute ?? standardConstants.requestsPerMinute,
chatHistoryStoreDuration:
standardPlan?.chatHistoryStoreDuration ?? standardConstants.chatHistoryStoreDuration,
maxDatasetSize: standardPlan?.maxDatasetSize ?? standardConstants.maxDatasetSize,
websiteSyncPerDataset:
standardPlan?.websiteSyncPerDataset || standardConstants.websiteSyncPerDataset,
appRegistrationCount:
standardPlan?.appRegistrationCount ?? standardConstants.appRegistrationCount,
auditLogStoreDuration:
standardPlan?.auditLogStoreDuration ?? standardConstants.auditLogStoreDuration,
ticketResponseTime:
standardPlan?.ticketResponseTime ?? standardConstants.ticketResponseTime,
customDomain: standardPlan?.customDomain ?? standardConstants.customDomain,
maxUploadFileSize:
standardPlan?.maxUploadFileSize ?? standardConstants.maxUploadFileSize,
maxUploadFileCount:
standardPlan?.maxUploadFileCount ?? standardConstants.maxUploadFileCount
}
: standardPlan,
standardConstants: standardConstants
? {
...standardConstants,
maxTeamMember: standardPlan?.maxTeamMember ?? standardConstants.maxTeamMember,
maxAppAmount: standardPlan?.maxApp ?? standardConstants.maxAppAmount,
maxDatasetAmount: standardPlan?.maxDataset ?? standardConstants.maxDatasetAmount,
requestsPerMinute: standardPlan?.requestsPerMinute ?? standardConstants.requestsPerMinute,
chatHistoryStoreDuration:
standardPlan?.chatHistoryStoreDuration ?? standardConstants.chatHistoryStoreDuration,
maxDatasetSize: standardPlan?.maxDatasetSize ?? standardConstants.maxDatasetSize,
websiteSyncPerDataset:
standardPlan?.websiteSyncPerDataset || standardConstants.websiteSyncPerDataset,
appRegistrationCount:
standardPlan?.appRegistrationCount ?? standardConstants.appRegistrationCount,
auditLogStoreDuration:
standardPlan?.auditLogStoreDuration ?? standardConstants.auditLogStoreDuration,
ticketResponseTime:
standardPlan?.ticketResponseTime ?? standardConstants.ticketResponseTime,
customDomain: standardPlan?.customDomain ?? standardConstants.customDomain,
maxUploadFileSize: standardPlan?.maxUploadFileSize ?? standardConstants.maxUploadFileSize,
maxUploadFileCount:
standardPlan?.maxUploadFileCount ?? standardConstants.maxUploadFileCount
}
: undefined, : undefined,
totalPoints, totalPoints,
...@@ -301,6 +270,7 @@ export const getTeamPlanStatus = async ({ ...@@ -301,6 +270,7 @@ export const getTeamPlanStatus = async ({
}; };
}; };
/* ===== Buffer controller ===== */
export const teamPoint = { export const teamPoint = {
getTeamPoints: async ({ teamId }: { teamId: string }) => { getTeamPoints: async ({ teamId }: { teamId: string }) => {
const surplusCacheKey = `${CacheKeyEnum.team_point_surplus}:${teamId}`; const surplusCacheKey = `${CacheKeyEnum.team_point_surplus}:${teamId}`;
...@@ -368,9 +338,7 @@ export const teamQPM = { ...@@ -368,9 +338,7 @@ export const teamQPM = {
// 2. Computed // 2. Computed
const teamPlanStatus = await getTeamPlanStatus({ teamId }); const teamPlanStatus = await getTeamPlanStatus({ teamId });
const limit = const limit = teamPlanStatus[SubTypeEnum.standard]?.requestsPerMinute;
teamPlanStatus[SubTypeEnum.standard]?.requestsPerMinute ??
teamPlanStatus.standardConstants?.requestsPerMinute;
if (!limit) { if (!limit) {
if (process.env.CHAT_MAX_QPM) return Number(process.env.CHAT_MAX_QPM); if (process.env.CHAT_MAX_QPM) return Number(process.env.CHAT_MAX_QPM);
......
...@@ -39,14 +39,12 @@ const FileSelector = ({ ...@@ -39,14 +39,12 @@ const FileSelector = ({
// 文件大小限制(B):团队套餐 || 系统配置 || 默认值 // 文件大小限制(B):团队套餐 || 系统配置 || 默认值
const displayMaxSize = formatFileSize( const displayMaxSize = formatFileSize(
(teamPlanStatus?.standardConstants?.maxUploadFileSize || feConfigs.uploadFileMaxSize) * (teamPlanStatus?.standard?.maxUploadFileSize || feConfigs.uploadFileMaxSize) * 1024 * 1024
1024 *
1024
); );
// 文件数量限制:组件传入的maxCount || 团队套餐 || 系统配置 // 文件数量限制:组件传入的maxCount || 团队套餐 || 系统配置
const formatMaxCount = Math.min( const formatMaxCount = Math.min(
maxCount, maxCount,
teamPlanStatus?.standardConstants?.maxUploadFileCount || feConfigs.uploadFileMaxAmount teamPlanStatus?.standard?.maxUploadFileCount || feConfigs.uploadFileMaxAmount
); );
const { File, onOpen } = useSelectFile({ const { File, onOpen } = useSelectFile({
......
...@@ -44,7 +44,7 @@ const FileSelect = ({ ...@@ -44,7 +44,7 @@ const FileSelect = ({
// 文件数量限制:团队套餐 || 系统配置 || 默认值(这里是指对话中,最多上传多少文件) // 文件数量限制:团队套餐 || 系统配置 || 默认值(这里是指对话中,最多上传多少文件)
const maxSelectFiles = Math.min( const maxSelectFiles = Math.min(
teamPlanStatus?.standardConstants?.maxUploadFileCount || feConfigs.uploadFileMaxAmount, teamPlanStatus?.standard?.maxUploadFileCount || feConfigs.uploadFileMaxAmount,
50 50
); );
......
...@@ -94,12 +94,12 @@ const FileSelector = ({ ...@@ -94,12 +94,12 @@ const FileSelector = ({
// 文件数量限制:组件参数 || 团队套餐 || 系统配置 || 默认值 // 文件数量限制:组件参数 || 团队套餐 || 系统配置 || 默认值
const maxSelectFiles = const maxSelectFiles =
maxFiles || maxFiles ||
teamPlanStatus?.standardConstants?.maxUploadFileCount || teamPlanStatus?.standard?.maxUploadFileCount ||
feConfigs?.uploadFileMaxAmount || feConfigs?.uploadFileMaxAmount ||
10; 10;
// 文件大小限制(MB):团队套餐 || 系统配置 || 默认值 // 文件大小限制(MB):团队套餐 || 系统配置 || 默认值
const maxSize = const maxSize =
(teamPlanStatus?.standardConstants?.maxUploadFileSize || feConfigs?.uploadFileMaxSize || 500) * (teamPlanStatus?.standard?.maxUploadFileSize || feConfigs?.uploadFileMaxSize || 500) *
1024 * 1024 *
1024; 1024;
const canSelectFileAmount = maxSelectFiles - value.length; const canSelectFileAmount = maxSelectFiles - value.length;
......
...@@ -57,12 +57,12 @@ export const useFileUpload = (props: UseFileUploadOptions) => { ...@@ -57,12 +57,12 @@ export const useFileUpload = (props: UseFileUploadOptions) => {
// 文件数量限制:配置的maxFiles || 团队套餐 || 系统配置 || 默认值 // 文件数量限制:配置的maxFiles || 团队套餐 || 系统配置 || 默认值
const maxSelectFiles = const maxSelectFiles =
fileSelectConfig?.maxFiles || fileSelectConfig?.maxFiles ||
teamPlanStatus?.standardConstants?.maxUploadFileCount || teamPlanStatus?.standard?.maxUploadFileCount ||
feConfigs?.uploadFileMaxAmount || feConfigs?.uploadFileMaxAmount ||
10; 10;
// 文件大小限制(MB):团队套餐 || 系统配置 || 默认值 // 文件大小限制(MB):团队套餐 || 系统配置 || 默认值
const maxSize = const maxSize =
(teamPlanStatus?.standardConstants?.maxUploadFileSize || feConfigs?.uploadFileMaxSize || 500) * (teamPlanStatus?.standard?.maxUploadFileSize || feConfigs?.uploadFileMaxSize || 500) *
1024 * 1024 *
1024; 1024;
const canSelectFileAmount = maxSelectFiles - fileList.length; const canSelectFileAmount = maxSelectFiles - fileList.length;
......
...@@ -12,7 +12,7 @@ import Markdown from '@/components/Markdown'; ...@@ -12,7 +12,7 @@ import Markdown from '@/components/Markdown';
import MyPopover from '@fastgpt/web/components/common/MyPopover'; import MyPopover from '@fastgpt/web/components/common/MyPopover';
import { useUserStore } from '@/web/support/user/useUserStore'; import { useUserStore } from '@/web/support/user/useUserStore';
import { formatFileSize } from '@fastgpt/global/common/file/tools'; import { formatFileSize } from '@fastgpt/global/common/file/tools';
import type { TeamSubSchemaType } from '@fastgpt/global/support/wallet/sub/type'; import type { TeamPlanStandardType } from '@fastgpt/global/support/wallet/sub/type';
const ModelPriceModal = dynamic(() => const ModelPriceModal = dynamic(() =>
import('@/components/core/ai/ModelTable').then((mod) => mod.ModelPriceModal) import('@/components/core/ai/ModelTable').then((mod) => mod.ModelPriceModal)
...@@ -25,7 +25,7 @@ const StandardPlanContentList = ({ ...@@ -25,7 +25,7 @@ const StandardPlanContentList = ({
}: { }: {
level: `${StandardSubLevelEnum}`; level: `${StandardSubLevelEnum}`;
mode: `${SubModeEnum}`; mode: `${SubModeEnum}`;
standplan?: TeamSubSchemaType; standplan?: TeamPlanStandardType;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
...@@ -58,8 +58,8 @@ const StandardPlanContentList = ({ ...@@ -58,8 +58,8 @@ const StandardPlanContentList = ({
: plan.totalPoints * (formatMode === SubModeEnum.month ? 1 : 12)), : plan.totalPoints * (formatMode === SubModeEnum.month ? 1 : 12)),
requestsPerMinute: standplan?.requestsPerMinute ?? plan.requestsPerMinute, requestsPerMinute: standplan?.requestsPerMinute ?? plan.requestsPerMinute,
maxTeamMember: standplan?.maxTeamMember ?? plan.maxTeamMember, maxTeamMember: standplan?.maxTeamMember ?? plan.maxTeamMember,
maxAppAmount: standplan?.maxApp ?? plan.maxAppAmount, maxAppAmount: standplan?.maxAppAmount ?? plan.maxAppAmount,
maxDatasetAmount: standplan?.maxDataset ?? plan.maxDatasetAmount, maxDatasetAmount: standplan?.maxDatasetAmount ?? plan.maxDatasetAmount,
maxDatasetSize: standplan?.maxDatasetSize ?? plan.maxDatasetSize, maxDatasetSize: standplan?.maxDatasetSize ?? plan.maxDatasetSize,
websiteSyncPerDataset: standplan?.websiteSyncPerDataset ?? plan.websiteSyncPerDataset, websiteSyncPerDataset: standplan?.websiteSyncPerDataset ?? plan.websiteSyncPerDataset,
chatHistoryStoreDuration: chatHistoryStoreDuration:
...@@ -84,8 +84,8 @@ const StandardPlanContentList = ({ ...@@ -84,8 +84,8 @@ const StandardPlanContentList = ({
standplan?.annualBonusPoints, standplan?.annualBonusPoints,
standplan?.requestsPerMinute, standplan?.requestsPerMinute,
standplan?.maxTeamMember, standplan?.maxTeamMember,
standplan?.maxApp, standplan?.maxAppAmount,
standplan?.maxDataset, standplan?.maxDatasetAmount,
standplan?.maxDatasetSize, standplan?.maxDatasetSize,
standplan?.websiteSyncPerDataset, standplan?.websiteSyncPerDataset,
standplan?.chatHistoryStoreDuration, standplan?.chatHistoryStoreDuration,
......
...@@ -124,7 +124,7 @@ const InputTypeConfig = ({ ...@@ -124,7 +124,7 @@ const InputTypeConfig = ({
const maxFiles = watch('maxFiles') ?? 5; const maxFiles = watch('maxFiles') ?? 5;
// 文件数量限制:团队套餐 || 系统配置 || 默认值 // 文件数量限制:团队套餐 || 系统配置 || 默认值
const maxSelectFiles = Math.min( const maxSelectFiles = Math.min(
teamPlanStatus?.standardConstants?.maxUploadFileCount || feConfigs.uploadFileMaxAmount, teamPlanStatus?.standard?.maxUploadFileCount || feConfigs.uploadFileMaxAmount,
50 50
); );
const canSelectFile = watch('canSelectFile') ?? true; const canSelectFile = watch('canSelectFile') ?? true;
......
...@@ -90,7 +90,7 @@ const TeamPlanStatusCard = () => { ...@@ -90,7 +90,7 @@ const TeamPlanStatusCard = () => {
setHiddenUntil(hideUntilTime); setHiddenUntil(hideUntilTime);
}, [setHiddenUntil, operationalAd]); }, [setHiddenUntil, operationalAd]);
if (!teamPlanStatus?.standardConstants) return null; if (!teamPlanStatus?.standard) return null;
return ( return (
<Box <Box
......
...@@ -35,10 +35,10 @@ const FileSelector = ({ ...@@ -35,10 +35,10 @@ const FileSelector = ({
// 文件数量限制:团队套餐 || 系统配置 || 默认值 // 文件数量限制:团队套餐 || 系统配置 || 默认值
const maxCount = const maxCount =
teamPlanStatus?.standardConstants?.maxUploadFileCount || feConfigs?.uploadFileMaxAmount || 1000; teamPlanStatus?.standard?.maxUploadFileCount || feConfigs?.uploadFileMaxAmount || 1000;
// 文件大小限制(bytes):优先级为 套餐限制 > 系统配置 > 默认值(500MB) // 文件大小限制(bytes):优先级为 套餐限制 > 系统配置 > 默认值(500MB)
const maxSize = const maxSize =
(teamPlanStatus?.standardConstants?.maxUploadFileSize ?? feConfigs?.uploadFileMaxSize ?? 500) * (teamPlanStatus?.standard?.maxUploadFileSize ?? feConfigs?.uploadFileMaxSize ?? 500) *
1024 * 1024 *
1024; 1024;
......
...@@ -6,7 +6,8 @@ import { StandardSubLevelEnum, SubModeEnum } from '@fastgpt/global/support/walle ...@@ -6,7 +6,8 @@ import { StandardSubLevelEnum, SubModeEnum } from '@fastgpt/global/support/walle
import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useSystemStore } from '@/web/common/system/useSystemStore';
import { standardSubLevelMap } from '@fastgpt/global/support/wallet/sub/constants'; import { standardSubLevelMap } from '@fastgpt/global/support/wallet/sub/constants';
import { useRequest } from '@fastgpt/web/hooks/useRequest'; import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { type TeamSubSchemaType } from '@fastgpt/global/support/wallet/sub/type'; import type { TeamPlanStandardType } from '@fastgpt/global/support/wallet/sub/type';
import QRCodePayModal, { type QRPayProps } from '@/components/support/wallet/QRCodePayModal'; import QRCodePayModal, { type QRPayProps } from '@/components/support/wallet/QRCodePayModal';
import { postCreatePayBill } from '@/web/support/wallet/bill/api'; import { postCreatePayBill } from '@/web/support/wallet/bill/api';
import { getDiscountCouponList } from '@/web/support/wallet/sub/discountCoupon/api'; import { getDiscountCouponList } from '@/web/support/wallet/sub/discountCoupon/api';
...@@ -35,7 +36,7 @@ const Standard = ({ ...@@ -35,7 +36,7 @@ const Standard = ({
standardPlan: myStandardPlan, standardPlan: myStandardPlan,
onPaySuccess onPaySuccess
}: { }: {
standardPlan?: TeamSubSchemaType; standardPlan?: TeamPlanStandardType;
onPaySuccess?: () => void; onPaySuccess?: () => void;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
...@@ -112,9 +113,9 @@ const Standard = ({ ...@@ -112,9 +113,9 @@ const Standard = ({
? value.wecom.price ? value.wecom.price
: value.price * (selectSubMode === SubModeEnum.month ? 1 : 10), : value.price * (selectSubMode === SubModeEnum.month ? 1 : 10),
level: level as `${StandardSubLevelEnum}`, level: level as `${StandardSubLevelEnum}`,
maxTeamMember: myStandardPlan?.maxTeamMember || value.maxTeamMember, maxTeamMember: myStandardPlan?.maxTeamMember ?? value.maxTeamMember,
maxAppAmount: myStandardPlan?.maxApp || value.maxAppAmount, maxAppAmount: myStandardPlan?.maxAppAmount ?? value.maxAppAmount,
maxDatasetAmount: myStandardPlan?.maxDataset || value.maxDatasetAmount, maxDatasetAmount: myStandardPlan?.maxDatasetAmount ?? value.maxDatasetAmount,
chatHistoryStoreDuration: value.chatHistoryStoreDuration, chatHistoryStoreDuration: value.chatHistoryStoreDuration,
maxDatasetSize: value.maxDatasetSize, maxDatasetSize: value.maxDatasetSize,
annualBonusPoints: selectSubMode === SubModeEnum.month ? 0 : value.annualBonusPoints, annualBonusPoints: selectSubMode === SubModeEnum.month ? 0 : value.annualBonusPoints,
...@@ -135,8 +136,8 @@ const Standard = ({ ...@@ -135,8 +136,8 @@ const Standard = ({
isWecomTeam, isWecomTeam,
selectSubMode, selectSubMode,
myStandardPlan?.maxTeamMember, myStandardPlan?.maxTeamMember,
myStandardPlan?.maxApp, myStandardPlan?.maxAppAmount,
myStandardPlan?.maxDataset myStandardPlan?.maxDatasetAmount
]); ]);
// Pay code // Pay code
......
...@@ -91,7 +91,7 @@ const CustomDomain = () => { ...@@ -91,7 +91,7 @@ const CustomDomain = () => {
<Box fontSize="20px" fontWeight="500"> <Box fontSize="20px" fontWeight="500">
{t('account:custom_domain')} {t('account:custom_domain')}
{customDomainList?.length ? ( {customDomainList?.length ? (
`: (${customDomainList.length}/${teamPlanStatus?.standardConstants?.customDomain})` `: (${customDomainList.length}/${teamPlanStatus?.standard?.customDomain})`
) : ( ) : (
<></> <></>
)} )}
......
...@@ -70,7 +70,7 @@ const ModelPriceModal = dynamic(() => ...@@ -70,7 +70,7 @@ const ModelPriceModal = dynamic(() =>
const Info = () => { const Info = () => {
const { isPc } = useSystem(); const { isPc } = useSystem();
const { teamPlanStatus, initUserInfo } = useUserStore(); const { teamPlanStatus, initUserInfo } = useUserStore();
const standardPlan = teamPlanStatus?.standardConstants; const standardPlan = teamPlanStatus?.standard;
const { isOpen: isOpenContact, onClose: onCloseContact, onOpen: onOpenContact } = useDisclosure(); const { isOpen: isOpenContact, onClose: onCloseContact, onOpen: onOpenContact } = useDisclosure();
useMount(() => { useMount(() => {
...@@ -125,7 +125,7 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => { ...@@ -125,7 +125,7 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
const { reset } = useForm<UserUpdateParams>({ const { reset } = useForm<UserUpdateParams>({
defaultValues: userInfo as UserType defaultValues: userInfo as UserType
}); });
const standardPlan = teamPlanStatus?.standardConstants; const standardPlan = teamPlanStatus?.standard;
const { isPc } = useSystem(); const { isPc } = useSystem();
const { toast } = useToast(); const { toast } = useToast();
...@@ -389,11 +389,10 @@ const PlanUsage = () => { ...@@ -389,11 +389,10 @@ const PlanUsage = () => {
const standardPlan = teamPlanStatus?.standard; const standardPlan = teamPlanStatus?.standard;
const isFreeTeam = useMemo(() => { const isFreeTeam = useMemo(() => {
if (!teamPlanStatus || !teamPlanStatus?.standardConstants) return false; if (!teamPlanStatus || !teamPlanStatus?.standard) return false;
const hasExtraDatasetSize = const hasExtraDatasetSize =
teamPlanStatus.datasetMaxSize > teamPlanStatus.standardConstants.maxDatasetSize; teamPlanStatus.datasetMaxSize > teamPlanStatus.standard.maxDatasetSize;
const hasExtraPoints = const hasExtraPoints = teamPlanStatus.totalPoints > teamPlanStatus.standard.totalPoints;
teamPlanStatus.totalPoints > teamPlanStatus.standardConstants.totalPoints;
if ( if (
teamPlanStatus?.standard?.currentSubLevel === StandardSubLevelEnum.free && teamPlanStatus?.standard?.currentSubLevel === StandardSubLevelEnum.free &&
!hasExtraDatasetSize && !hasExtraDatasetSize &&
...@@ -449,38 +448,32 @@ const PlanUsage = () => { ...@@ -449,38 +448,32 @@ const PlanUsage = () => {
{ {
label: t('account_info:member_amount'), label: t('account_info:member_amount'),
value: teamPlanStatus.usedMember, value: teamPlanStatus.usedMember,
max: teamPlanStatus?.standardConstants?.maxTeamMember ?? t('account_info:unlimited'), max: teamPlanStatus?.standard?.maxTeamMember ?? t('account_info:unlimited'),
rate: rate: (teamPlanStatus.usedMember / (teamPlanStatus?.standard?.maxTeamMember || 1)) * 100
(teamPlanStatus.usedMember / (teamPlanStatus?.standardConstants?.maxTeamMember || 1)) *
100
}, },
{ {
label: t('account_info:app_amount'), label: t('account_info:app_amount'),
value: teamPlanStatus.usedAppAmount, value: teamPlanStatus.usedAppAmount,
max: teamPlanStatus?.standardConstants?.maxAppAmount ?? t('account_info:unlimited'), max: teamPlanStatus?.standard?.maxAppAmount ?? t('account_info:unlimited'),
rate: rate: (teamPlanStatus.usedAppAmount / (teamPlanStatus?.standard?.maxAppAmount || 1)) * 100
(teamPlanStatus.usedAppAmount / (teamPlanStatus?.standardConstants?.maxAppAmount || 1)) *
100
}, },
{ {
label: t('account_info:dataset_amount'), label: t('account_info:dataset_amount'),
value: teamPlanStatus.usedDatasetSize, value: teamPlanStatus.usedDatasetSize,
max: teamPlanStatus?.standardConstants?.maxDatasetAmount ?? t('account_info:unlimited'), max: teamPlanStatus?.standard?.maxDatasetAmount ?? t('account_info:unlimited'),
rate: rate:
(teamPlanStatus.usedDatasetSize / (teamPlanStatus.usedDatasetSize / (teamPlanStatus?.standard?.maxDatasetAmount || 1)) * 100
(teamPlanStatus?.standardConstants?.maxDatasetAmount || 1)) *
100
} }
]; ];
if (teamPlanStatus?.standardConstants?.appRegistrationCount) { if (teamPlanStatus?.standard?.appRegistrationCount) {
data.push({ data.push({
label: t('account_info:app_registration_count'), label: t('account_info:app_registration_count'),
value: teamPlanStatus.usedRegistrationCount || 0, value: teamPlanStatus.usedRegistrationCount || 0,
max: teamPlanStatus.standardConstants.appRegistrationCount, max: teamPlanStatus.standard.appRegistrationCount,
rate: rate:
((teamPlanStatus.usedRegistrationCount || 0) / ((teamPlanStatus.usedRegistrationCount || 0) /
teamPlanStatus.standardConstants.appRegistrationCount) * teamPlanStatus.standard.appRegistrationCount) *
100 100
}); });
} }
......
...@@ -60,7 +60,7 @@ async function handler( ...@@ -60,7 +60,7 @@ async function handler(
// 获取团队套餐信息 // 获取团队套餐信息
const teamPlanStatus = await getTeamPlanStatus({ teamId: String(app.teamId) }); const teamPlanStatus = await getTeamPlanStatus({ teamId: String(app.teamId) });
const appRegistrationLimit = teamPlanStatus?.standardConstants?.appRegistrationCount; const appRegistrationLimit = teamPlanStatus?.standard?.appRegistrationCount;
// 检查是否有配额限制 // 检查是否有配额限制
if (appRegistrationLimit && appRegistrationLimit > 0) { if (appRegistrationLimit && appRegistrationLimit > 0) {
......
...@@ -28,8 +28,7 @@ async function handler( ...@@ -28,8 +28,7 @@ async function handler(
await authFrequencyLimit({ await authFrequencyLimit({
eventId: `${tmbId}-uploadfile`, eventId: `${tmbId}-uploadfile`,
maxAmount: maxAmount: planStatus.standard?.maxUploadFileCount || global.feConfigs.uploadFileMaxAmount,
planStatus.standardConstants?.maxUploadFileCount || global.feConfigs.uploadFileMaxAmount,
expiredTime: addSeconds(new Date(), 30) // 30s expiredTime: addSeconds(new Date(), 30) // 30s
}); });
...@@ -40,8 +39,7 @@ async function handler( ...@@ -40,8 +39,7 @@ async function handler(
{ rawKey: fileKey, filename }, { rawKey: fileKey, filename },
{ {
expiredHours: 1, expiredHours: 1,
maxFileSize: maxFileSize: planStatus.standard?.maxUploadFileSize || global.feConfigs.uploadFileMaxSize
planStatus.standardConstants?.maxUploadFileSize || global.feConfigs.uploadFileMaxSize
} }
); );
} }
......
...@@ -24,8 +24,7 @@ async function handler( ...@@ -24,8 +24,7 @@ async function handler(
const planStatus = await getTeamPlanStatus({ teamId }); const planStatus = await getTeamPlanStatus({ teamId });
await authFrequencyLimit({ await authFrequencyLimit({
eventId: `${uid}-uploadfile`, eventId: `${uid}-uploadfile`,
maxAmount: maxAmount: planStatus.standard?.maxUploadFileCount || global.feConfigs.uploadFileMaxAmount,
planStatus.standardConstants?.maxUploadFileCount || global.feConfigs.uploadFileMaxAmount,
expiredTime: addSeconds(new Date(), 30) // 30s expiredTime: addSeconds(new Date(), 30) // 30s
}); });
...@@ -34,8 +33,7 @@ async function handler( ...@@ -34,8 +33,7 @@ async function handler(
chatId, chatId,
filename, filename,
uId: uid, uId: uid,
maxFileSize: maxFileSize: planStatus.standard?.maxUploadFileSize || global.feConfigs.uploadFileMaxSize
planStatus.standardConstants?.maxUploadFileSize || global.feConfigs.uploadFileMaxSize
}); });
} }
......
...@@ -42,8 +42,7 @@ async function handler( ...@@ -42,8 +42,7 @@ async function handler(
const planStatus = await getTeamPlanStatus({ teamId }); const planStatus = await getTeamPlanStatus({ teamId });
await authFrequencyLimit({ await authFrequencyLimit({
eventId: `${tmbId}-uploadfile`, eventId: `${tmbId}-uploadfile`,
maxAmount: maxAmount: planStatus.standard?.maxUploadFileCount || global.feConfigs.uploadFileMaxAmount,
planStatus.standardConstants?.maxUploadFileCount || global.feConfigs.uploadFileMaxAmount,
expiredTime: addSeconds(new Date(), 30), // 30s expiredTime: addSeconds(new Date(), 30), // 30s
num: result.fileMetadata.length num: result.fileMetadata.length
}); });
......
...@@ -49,8 +49,7 @@ async function handler( ...@@ -49,8 +49,7 @@ async function handler(
const planStatus = await getTeamPlanStatus({ teamId }); const planStatus = await getTeamPlanStatus({ teamId });
await authFrequencyLimit({ await authFrequencyLimit({
eventId: `${tmbId}-uploadfile`, eventId: `${tmbId}-uploadfile`,
maxAmount: maxAmount: planStatus.standard?.maxUploadFileCount || global.feConfigs.uploadFileMaxAmount,
planStatus.standardConstants?.maxUploadFileCount || global.feConfigs.uploadFileMaxAmount,
expiredTime: addSeconds(new Date(), 30), // 30s expiredTime: addSeconds(new Date(), 30), // 30s
num: result.fileMetadata.length num: result.fileMetadata.length
}); });
......
...@@ -25,16 +25,14 @@ async function handler( ...@@ -25,16 +25,14 @@ async function handler(
const planStatus = await getTeamPlanStatus({ teamId }); const planStatus = await getTeamPlanStatus({ teamId });
await authFrequencyLimit({ await authFrequencyLimit({
eventId: `${userId}-uploadfile`, eventId: `${userId}-uploadfile`,
maxAmount: maxAmount: planStatus.standard?.maxUploadFileCount || global.feConfigs.uploadFileMaxAmount,
planStatus.standardConstants?.maxUploadFileCount || global.feConfigs.uploadFileMaxAmount,
expiredTime: addSeconds(new Date(), 30) // 30s expiredTime: addSeconds(new Date(), 30) // 30s
}); });
return await getS3DatasetSource().createUploadDatasetFileURL({ return await getS3DatasetSource().createUploadDatasetFileURL({
datasetId, datasetId,
filename, filename,
maxFileSize: maxFileSize: planStatus.standard?.maxUploadFileSize || global.feConfigs.uploadFileMaxSize
planStatus.standardConstants?.maxUploadFileSize || global.feConfigs.uploadFileMaxSize
}); });
} }
......
import { describe, it, expect, beforeEach, vi } from 'vitest';
import {
checkTeamAIPoints,
checkTeamMemberLimit,
checkTeamAppTypeLimit,
checkDatasetIndexLimit,
checkTeamDatasetLimit,
checkTeamDatasetSyncPermission
} from '@fastgpt/service/support/permission/teamLimit';
import * as walletUtils from '@fastgpt/service/support/wallet/sub/utils';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { MongoDataset } from '@fastgpt/service/core/dataset/schema';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import * as vectorController from '@fastgpt/service/common/vectorDB/controller';
import { TeamErrEnum } from '@fastgpt/global/common/error/code/team';
import { SystemErrEnum } from '@fastgpt/global/common/error/code/system';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { TeamMemberStatusEnum } from '@fastgpt/global/support/user/team/constant';
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { StandardSubLevelEnum } from '@fastgpt/global/support/wallet/sub/constants';
// Valid ObjectId for testing
const mockTeamId = '507f1f77bcf86cd799439011';
describe('checkTeamAIPoints', () => {
beforeEach(() => {
vi.clearAllMocks();
delete (global as any).subPlans;
});
it('当 global.subPlans.standard 不存在时直接返回', async () => {
const result = await checkTeamAIPoints(mockTeamId);
expect(result).toBeUndefined();
});
it('当积分充足时返回积分信息', async () => {
(global as any).subPlans = {
standard: {
[StandardSubLevelEnum.basic]: {
totalPoints: 2000
}
}
};
vi.spyOn(walletUtils.teamPoint, 'getTeamPoints').mockResolvedValue({
totalPoints: 2000,
surplusPoints: 1500,
usedPoints: 500
});
const result = await checkTeamAIPoints(mockTeamId);
expect(result).toEqual({
totalPoints: 2000,
usedPoints: 500
});
});
it('当积分不足时抛出错误', async () => {
(global as any).subPlans = {
standard: {
[StandardSubLevelEnum.basic]: {
totalPoints: 2000
}
}
};
vi.spyOn(walletUtils.teamPoint, 'getTeamPoints').mockResolvedValue({
totalPoints: 2000,
surplusPoints: 0,
usedPoints: 2000
});
await expect(checkTeamAIPoints(mockTeamId)).rejects.toBe(TeamErrEnum.aiPointsNotEnough);
});
it('当已用积分等于总积分时抛出错误', async () => {
(global as any).subPlans = {
standard: {
[StandardSubLevelEnum.basic]: {
totalPoints: 1000
}
}
};
vi.spyOn(walletUtils.teamPoint, 'getTeamPoints').mockResolvedValue({
totalPoints: 1000,
surplusPoints: 0,
usedPoints: 1000
});
await expect(checkTeamAIPoints(mockTeamId)).rejects.toBe(TeamErrEnum.aiPointsNotEnough);
});
it('当已用积分超过总积分时抛出错误', async () => {
(global as any).subPlans = {
standard: {
[StandardSubLevelEnum.basic]: {
totalPoints: 1000
}
}
};
vi.spyOn(walletUtils.teamPoint, 'getTeamPoints').mockResolvedValue({
totalPoints: 1000,
surplusPoints: -100,
usedPoints: 1100
});
await expect(checkTeamAIPoints(mockTeamId)).rejects.toBe(TeamErrEnum.aiPointsNotEnough);
});
});
describe('checkTeamMemberLimit', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('当团队成员数量未超限时正常通过', async () => {
vi.spyOn(MongoTeamMember, 'countDocuments').mockResolvedValue(5);
const mockStandard = {
standard: {
maxTeamMember: 10
}
};
vi.spyOn(walletUtils, 'getTeamStandPlan').mockResolvedValue(mockStandard as any);
await expect(checkTeamMemberLimit(mockTeamId, 3)).resolves.toBeUndefined();
});
it('当新增成员后超过限制时抛出错误', async () => {
vi.spyOn(MongoTeamMember, 'countDocuments').mockResolvedValue(8);
const mockStandard = {
standard: {
maxTeamMember: 10
}
};
vi.spyOn(walletUtils, 'getTeamStandPlan').mockResolvedValue(mockStandard as any);
await expect(checkTeamMemberLimit(mockTeamId, 3)).rejects.toBe(TeamErrEnum.teamOverSize);
});
it('当新增成员后刚好达到限制时正常通过', async () => {
vi.spyOn(MongoTeamMember, 'countDocuments').mockResolvedValue(8);
const mockStandard = {
standard: {
maxTeamMember: 10
}
};
vi.spyOn(walletUtils, 'getTeamStandPlan').mockResolvedValue(mockStandard as any);
await expect(checkTeamMemberLimit(mockTeamId, 2)).resolves.toBeUndefined();
});
it('当 maxTeamMember 未设置时不限制', async () => {
vi.spyOn(MongoTeamMember, 'countDocuments').mockResolvedValue(100);
const mockStandard = {
standard: {}
};
vi.spyOn(walletUtils, 'getTeamStandPlan').mockResolvedValue(mockStandard as any);
await expect(checkTeamMemberLimit(mockTeamId, 50)).resolves.toBeUndefined();
});
it('查询时排除已离开的成员', async () => {
const countSpy = vi.spyOn(MongoTeamMember, 'countDocuments').mockResolvedValue(5);
const mockStandard = {
standard: {
maxTeamMember: 10
}
};
vi.spyOn(walletUtils, 'getTeamStandPlan').mockResolvedValue(mockStandard as any);
await checkTeamMemberLimit(mockTeamId, 2);
expect(countSpy).toHaveBeenCalledWith({
teamId: mockTeamId,
status: { $ne: TeamMemberStatusEnum.leave }
});
});
});
describe('checkTeamAppTypeLimit', () => {
beforeEach(() => {
vi.clearAllMocks();
delete (global as any).licenseData;
});
describe('app 类型检查', () => {
it('当应用数量未超限时正常通过', async () => {
vi.spyOn(MongoApp, 'countDocuments').mockResolvedValue(30);
const mockStandard = {
standard: {
maxAppAmount: 50
}
};
vi.spyOn(walletUtils, 'getTeamStandPlan').mockResolvedValue(mockStandard as any);
await expect(
checkTeamAppTypeLimit({
teamId: mockTeamId,
appCheckType: 'app',
amount: 10
})
).resolves.toBeUndefined();
});
it('当应用数量超限时抛出错误', async () => {
vi.spyOn(MongoApp, 'countDocuments').mockResolvedValue(45);
const mockStandard = {
standard: {
maxAppAmount: 50
}
};
vi.spyOn(walletUtils, 'getTeamStandPlan').mockResolvedValue(mockStandard as any);
await expect(
checkTeamAppTypeLimit({
teamId: mockTeamId,
appCheckType: 'app',
amount: 10
})
).rejects.toBe(TeamErrEnum.appAmountNotEnough);
});
it('当 maxAppAmount 未设置时不限制', async () => {
vi.spyOn(MongoApp, 'countDocuments').mockResolvedValue(100);
const mockStandard = {
standard: {}
};
vi.spyOn(walletUtils, 'getTeamStandPlan').mockResolvedValue(mockStandard as any);
await expect(
checkTeamAppTypeLimit({
teamId: mockTeamId,
appCheckType: 'app',
amount: 50
})
).resolves.toBeUndefined();
});
it('默认 amount 为 1', async () => {
vi.spyOn(MongoApp, 'countDocuments').mockResolvedValue(49);
const mockStandard = {
standard: {
maxAppAmount: 50
}
};
vi.spyOn(walletUtils, 'getTeamStandPlan').mockResolvedValue(mockStandard as any);
await expect(
checkTeamAppTypeLimit({
teamId: mockTeamId,
appCheckType: 'app'
})
).resolves.toBeUndefined();
});
it('查询时只统计 chatAgent/simple/workflow 类型', async () => {
const countSpy = vi.spyOn(MongoApp, 'countDocuments').mockResolvedValue(30);
const mockStandard = {
standard: {
maxAppAmount: 50
}
};
vi.spyOn(walletUtils, 'getTeamStandPlan').mockResolvedValue(mockStandard as any);
await checkTeamAppTypeLimit({
teamId: mockTeamId,
appCheckType: 'app'
});
expect(countSpy).toHaveBeenCalledWith({
teamId: mockTeamId,
type: {
$in: [AppTypeEnum.chatAgent, AppTypeEnum.simple, AppTypeEnum.workflow]
}
});
});
it('当系统许可证限制存在且超限时抛出系统错误', async () => {
vi.spyOn(MongoApp, 'countDocuments')
.mockResolvedValueOnce(30) // 团队应用数
.mockResolvedValueOnce(150); // 系统总应用数
const mockStandard = {
standard: {
maxAppAmount: 50
}
};
vi.spyOn(walletUtils, 'getTeamStandPlan').mockResolvedValue(mockStandard as any);
(global as any).licenseData = {
maxApps: 100
};
await expect(
checkTeamAppTypeLimit({
teamId: mockTeamId,
appCheckType: 'app'
})
).rejects.toBe(SystemErrEnum.licenseAppAmountLimit);
});
it('当系统许可证限制存在但未超限时正常通过', async () => {
vi.spyOn(MongoApp, 'countDocuments').mockResolvedValueOnce(30).mockResolvedValueOnce(80);
const mockStandard = {
standard: {
maxAppAmount: 50
}
};
vi.spyOn(walletUtils, 'getTeamStandPlan').mockResolvedValue(mockStandard as any);
(global as any).licenseData = {
maxApps: 100
};
await expect(
checkTeamAppTypeLimit({
teamId: mockTeamId,
appCheckType: 'app'
})
).resolves.toBeUndefined();
});
});
describe('tool 类型检查', () => {
it('当工具数量未超限时正常通过', async () => {
vi.spyOn(MongoApp, 'countDocuments').mockResolvedValue(500);
await expect(
checkTeamAppTypeLimit({
teamId: mockTeamId,
appCheckType: 'tool',
amount: 100
})
).resolves.toBeUndefined();
});
it('当工具数量超过 1000 时抛出错误', async () => {
vi.spyOn(MongoApp, 'countDocuments').mockResolvedValue(950);
await expect(
checkTeamAppTypeLimit({
teamId: mockTeamId,
appCheckType: 'tool',
amount: 100
})
).rejects.toBe(TeamErrEnum.pluginAmountNotEnough);
});
it('当工具数量刚好达到 1000 时正常通过', async () => {
vi.spyOn(MongoApp, 'countDocuments').mockResolvedValue(999);
await expect(
checkTeamAppTypeLimit({
teamId: mockTeamId,
appCheckType: 'tool',
amount: 1
})
).resolves.toBeUndefined();
});
});
describe('folder 类型检查', () => {
it('当文件夹数量未超限时正常通过', async () => {
vi.spyOn(MongoApp, 'countDocuments').mockResolvedValue(500);
await expect(
checkTeamAppTypeLimit({
teamId: mockTeamId,
appCheckType: 'folder',
amount: 100
})
).resolves.toBeUndefined();
});
it('当文件夹数量超过 1000 时抛出错误', async () => {
vi.spyOn(MongoApp, 'countDocuments').mockResolvedValue(950);
await expect(
checkTeamAppTypeLimit({
teamId: mockTeamId,
appCheckType: 'folder',
amount: 100
})
).rejects.toBe(TeamErrEnum.appFolderAmountNotEnough);
});
it('当文件夹数量刚好达到 1000 时正常通过', async () => {
vi.spyOn(MongoApp, 'countDocuments').mockResolvedValue(999);
await expect(
checkTeamAppTypeLimit({
teamId: mockTeamId,
appCheckType: 'folder',
amount: 1
})
).resolves.toBeUndefined();
});
});
});
describe('checkDatasetIndexLimit', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('当 standard 不存在时直接返回', async () => {
const mockPlanStatus = {
standard: undefined,
totalPoints: 1000,
usedPoints: 500,
datasetMaxSize: 10000
};
vi.spyOn(walletUtils, 'getTeamPlanStatus').mockResolvedValue(mockPlanStatus as any);
vi.spyOn(vectorController, 'getVectorCountByTeamId').mockResolvedValue(5000);
await expect(
checkDatasetIndexLimit({
teamId: mockTeamId,
insertLen: 100
})
).resolves.toBeUndefined();
});
it('当数据集大小未超限时正常通过', async () => {
const mockPlanStatus = {
standard: {
maxDatasetSize: 10000
},
totalPoints: 2000,
usedPoints: 500,
datasetMaxSize: 10000
};
vi.spyOn(walletUtils, 'getTeamPlanStatus').mockResolvedValue(mockPlanStatus as any);
vi.spyOn(vectorController, 'getVectorCountByTeamId').mockResolvedValue(5000);
await expect(
checkDatasetIndexLimit({
teamId: mockTeamId,
insertLen: 1000
})
).resolves.toBeUndefined();
});
it('当数据集大小超限时抛出错误', async () => {
const mockPlanStatus = {
standard: {
maxDatasetSize: 10000
},
totalPoints: 2000,
usedPoints: 500,
datasetMaxSize: 10000
};
vi.spyOn(walletUtils, 'getTeamPlanStatus').mockResolvedValue(mockPlanStatus as any);
vi.spyOn(vectorController, 'getVectorCountByTeamId').mockResolvedValue(9500);
await expect(
checkDatasetIndexLimit({
teamId: mockTeamId,
insertLen: 1000
})
).rejects.toBe(TeamErrEnum.datasetSizeNotEnough);
});
it('当数据集大小刚好达到限制时抛出错误', async () => {
const mockPlanStatus = {
standard: {
maxDatasetSize: 10000
},
totalPoints: 2000,
usedPoints: 500,
datasetMaxSize: 10000
};
vi.spyOn(walletUtils, 'getTeamPlanStatus').mockResolvedValue(mockPlanStatus as any);
vi.spyOn(vectorController, 'getVectorCountByTeamId').mockResolvedValue(9000);
await expect(
checkDatasetIndexLimit({
teamId: mockTeamId,
insertLen: 1000
})
).rejects.toBe(TeamErrEnum.datasetSizeNotEnough);
});
it('当积分不足时抛出错误', async () => {
const mockPlanStatus = {
standard: {
maxDatasetSize: 10000
},
totalPoints: 2000,
usedPoints: 2000,
datasetMaxSize: 10000
};
vi.spyOn(walletUtils, 'getTeamPlanStatus').mockResolvedValue(mockPlanStatus as any);
vi.spyOn(vectorController, 'getVectorCountByTeamId').mockResolvedValue(5000);
await expect(
checkDatasetIndexLimit({
teamId: mockTeamId,
insertLen: 100
})
).rejects.toBe(TeamErrEnum.aiPointsNotEnough);
});
it('当积分超支时抛出错误', async () => {
const mockPlanStatus = {
standard: {
maxDatasetSize: 10000
},
totalPoints: 2000,
usedPoints: 2500,
datasetMaxSize: 10000
};
vi.spyOn(walletUtils, 'getTeamPlanStatus').mockResolvedValue(mockPlanStatus as any);
vi.spyOn(vectorController, 'getVectorCountByTeamId').mockResolvedValue(5000);
await expect(
checkDatasetIndexLimit({
teamId: mockTeamId,
insertLen: 100
})
).rejects.toBe(TeamErrEnum.aiPointsNotEnough);
});
it('默认 insertLen 为 0', async () => {
const mockPlanStatus = {
standard: {
maxDatasetSize: 10000
},
totalPoints: 2000,
usedPoints: 500,
datasetMaxSize: 10000
};
vi.spyOn(walletUtils, 'getTeamPlanStatus').mockResolvedValue(mockPlanStatus as any);
vi.spyOn(vectorController, 'getVectorCountByTeamId').mockResolvedValue(5000);
await expect(
checkDatasetIndexLimit({
teamId: mockTeamId
})
).resolves.toBeUndefined();
});
it('数据集大小检查优先于积分检查', async () => {
const mockPlanStatus = {
standard: {
maxDatasetSize: 10000
},
totalPoints: 2000,
usedPoints: 2000,
datasetMaxSize: 10000
};
vi.spyOn(walletUtils, 'getTeamPlanStatus').mockResolvedValue(mockPlanStatus as any);
vi.spyOn(vectorController, 'getVectorCountByTeamId').mockResolvedValue(9500);
await expect(
checkDatasetIndexLimit({
teamId: mockTeamId,
insertLen: 1000
})
).rejects.toBe(TeamErrEnum.datasetSizeNotEnough);
});
});
describe('checkTeamDatasetLimit', () => {
beforeEach(() => {
vi.clearAllMocks();
delete (global as any).licenseData;
});
it('当数据集数量未超限时正常通过', async () => {
vi.spyOn(MongoDataset, 'countDocuments').mockResolvedValue(10);
const mockStandard = {
standard: {
maxDatasetAmount: 20
}
};
vi.spyOn(walletUtils, 'getTeamStandPlan').mockResolvedValue(mockStandard as any);
await expect(checkTeamDatasetLimit(mockTeamId)).resolves.toBeUndefined();
});
it('当数据集数量超限时抛出错误', async () => {
vi.spyOn(MongoDataset, 'countDocuments').mockResolvedValue(20);
const mockStandard = {
standard: {
maxDatasetAmount: 20
}
};
vi.spyOn(walletUtils, 'getTeamStandPlan').mockResolvedValue(mockStandard as any);
await expect(checkTeamDatasetLimit(mockTeamId)).rejects.toBe(
TeamErrEnum.datasetAmountNotEnough
);
});
it('当数据集数量刚好达到限制时抛出错误', async () => {
vi.spyOn(MongoDataset, 'countDocuments').mockResolvedValue(20);
const mockStandard = {
standard: {
maxDatasetAmount: 20
}
};
vi.spyOn(walletUtils, 'getTeamStandPlan').mockResolvedValue(mockStandard as any);
await expect(checkTeamDatasetLimit(mockTeamId)).rejects.toBe(
TeamErrEnum.datasetAmountNotEnough
);
});
it('当 maxDatasetAmount 未设置时不限制', async () => {
vi.spyOn(MongoDataset, 'countDocuments').mockResolvedValue(100);
const mockStandard = {
standard: {}
};
vi.spyOn(walletUtils, 'getTeamStandPlan').mockResolvedValue(mockStandard as any);
await expect(checkTeamDatasetLimit(mockTeamId)).resolves.toBeUndefined();
});
it('查询时排除文件夹类型', async () => {
const countSpy = vi.spyOn(MongoDataset, 'countDocuments').mockResolvedValue(10);
const mockStandard = {
standard: {
maxDatasetAmount: 20
}
};
vi.spyOn(walletUtils, 'getTeamStandPlan').mockResolvedValue(mockStandard as any);
await checkTeamDatasetLimit(mockTeamId);
expect(countSpy).toHaveBeenCalledWith({
teamId: mockTeamId,
type: { $ne: DatasetTypeEnum.folder }
});
});
it('当系统许可证限制存在且超限时抛出系统错误', async () => {
vi.spyOn(MongoDataset, 'countDocuments')
.mockResolvedValueOnce(10) // 团队数据集数
.mockResolvedValueOnce(150); // 系统总数据集数
const mockStandard = {
standard: {
maxDatasetAmount: 20
}
};
vi.spyOn(walletUtils, 'getTeamStandPlan').mockResolvedValue(mockStandard as any);
(global as any).licenseData = {
maxDatasets: 100
};
await expect(checkTeamDatasetLimit(mockTeamId)).rejects.toBe(
SystemErrEnum.licenseDatasetAmountLimit
);
});
it('当系统许可证限制存在但未超限时正常通过', async () => {
vi.spyOn(MongoDataset, 'countDocuments').mockResolvedValueOnce(10).mockResolvedValueOnce(80);
const mockStandard = {
standard: {
maxDatasetAmount: 20
}
};
vi.spyOn(walletUtils, 'getTeamStandPlan').mockResolvedValue(mockStandard as any);
(global as any).licenseData = {
maxDatasets: 100
};
await expect(checkTeamDatasetLimit(mockTeamId)).resolves.toBeUndefined();
});
it('系统许可证限制检查也排除文件夹类型', async () => {
const countSpy = vi
.spyOn(MongoDataset, 'countDocuments')
.mockResolvedValueOnce(10)
.mockResolvedValueOnce(80);
const mockStandard = {
standard: {
maxDatasetAmount: 20
}
};
vi.spyOn(walletUtils, 'getTeamStandPlan').mockResolvedValue(mockStandard as any);
(global as any).licenseData = {
maxDatasets: 100
};
await checkTeamDatasetLimit(mockTeamId);
expect(countSpy).toHaveBeenNthCalledWith(2, {
type: { $ne: DatasetTypeEnum.folder }
});
});
});
describe('checkTeamDatasetSyncPermission', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('当套餐支持网站同步时正常通过', async () => {
const mockStandard = {
standard: {
websiteSyncPerDataset: 100
}
};
vi.spyOn(walletUtils, 'getTeamStandPlan').mockResolvedValue(mockStandard as any);
await expect(checkTeamDatasetSyncPermission(mockTeamId)).resolves.toBeUndefined();
});
it('当套餐不支持网站同步时抛出错误', async () => {
const mockStandard = {
standard: {
websiteSyncPerDataset: undefined
}
};
vi.spyOn(walletUtils, 'getTeamStandPlan').mockResolvedValue(mockStandard as any);
await expect(checkTeamDatasetSyncPermission(mockTeamId)).rejects.toBe(
TeamErrEnum.websiteSyncNotEnough
);
});
it('当 websiteSyncPerDataset 为 0 时抛出错误', async () => {
const mockStandard = {
standard: {
websiteSyncPerDataset: 0
}
};
vi.spyOn(walletUtils, 'getTeamStandPlan').mockResolvedValue(mockStandard as any);
await expect(checkTeamDatasetSyncPermission(mockTeamId)).rejects.toBe(
TeamErrEnum.websiteSyncNotEnough
);
});
it('当 standard 不存在时不抛出错误', async () => {
const mockStandard = {
standard: undefined
};
vi.spyOn(walletUtils, 'getTeamStandPlan').mockResolvedValue(mockStandard as any);
await expect(checkTeamDatasetSyncPermission(mockTeamId)).resolves.toBeUndefined();
});
it('当 websiteSyncPerDataset 为正数时正常通过', async () => {
const mockStandard = {
standard: {
websiteSyncPerDataset: 50
}
};
vi.spyOn(walletUtils, 'getTeamStandPlan').mockResolvedValue(mockStandard as any);
await expect(checkTeamDatasetSyncPermission(mockTeamId)).resolves.toBeUndefined();
});
});
import { describe, it, expect, beforeEach, vi } from 'vitest';
import {
SubTypeEnum,
SubModeEnum,
StandardSubLevelEnum
} from '@fastgpt/global/support/wallet/sub/constants';
import type {
TeamSubSchemaType,
TeamStandardSubPlanItemType,
StandSubPlanLevelMapType
} from '@fastgpt/global/support/wallet/sub/type';
import {
buildStandardPlan,
getStandardPlansConfig,
getStandardPlanConfig,
sortStandPlans,
initTeamFreePlan,
getTeamStandPlan,
getTeamPlanStatus,
teamPoint,
teamQPM,
clearTeamPlanCache
} from '@fastgpt/service/support/wallet/sub/utils';
import { MongoTeamSub } from '@fastgpt/service/support/wallet/sub/schema';
// Valid ObjectId for testing
const mockTeamId = '507f1f77bcf86cd799439011';
const mockPlanId = '507f1f77bcf86cd799439012';
const baseStandard: TeamSubSchemaType = {
_id: mockPlanId,
teamId: mockTeamId,
type: SubTypeEnum.standard,
startTime: new Date('2024-01-01'),
expiredTime: new Date('2025-01-01'),
currentMode: SubModeEnum.month,
nextMode: SubModeEnum.month,
currentSubLevel: StandardSubLevelEnum.basic,
nextSubLevel: StandardSubLevelEnum.basic,
totalPoints: 1000,
surplusPoints: 500,
currentExtraDatasetSize: 0
};
const baseConstants: TeamStandardSubPlanItemType = {
price: 99,
name: 'Basic Plan',
desc: 'Basic description',
totalPoints: 2000,
maxTeamMember: 10,
maxAppAmount: 50,
maxDatasetAmount: 20,
maxDatasetSize: 100,
chatHistoryStoreDuration: 30
};
describe('getStandardPlansConfig', () => {
beforeEach(() => {
// 清理 global.subPlans
delete (global as any).subPlans;
});
it('返回 global.subPlans.standard 配置', () => {
const mockStandardPlans: StandSubPlanLevelMapType = {
[StandardSubLevelEnum.free]: {
price: 0,
totalPoints: 100,
maxTeamMember: 1,
maxAppAmount: 5,
maxDatasetAmount: 2,
chatHistoryStoreDuration: 7,
maxDatasetSize: 10
},
[StandardSubLevelEnum.basic]: {
price: 99,
totalPoints: 2000,
maxTeamMember: 10,
maxAppAmount: 50,
maxDatasetAmount: 20,
chatHistoryStoreDuration: 30,
maxDatasetSize: 100
},
[StandardSubLevelEnum.advanced]: {
price: 299,
totalPoints: 10000,
maxTeamMember: 50,
maxAppAmount: 200,
maxDatasetAmount: 100,
chatHistoryStoreDuration: 90,
maxDatasetSize: 500
},
[StandardSubLevelEnum.custom]: {
price: 999,
totalPoints: 50000,
maxTeamMember: 200,
maxAppAmount: 1000,
maxDatasetAmount: 500,
chatHistoryStoreDuration: 365,
maxDatasetSize: 2000
},
[StandardSubLevelEnum.experience]: {
price: 0,
totalPoints: 500,
maxTeamMember: 3,
maxAppAmount: 10,
maxDatasetAmount: 5,
chatHistoryStoreDuration: 14,
maxDatasetSize: 30
},
[StandardSubLevelEnum.team]: {
price: 199,
totalPoints: 5000,
maxTeamMember: 20,
maxAppAmount: 100,
maxDatasetAmount: 50,
chatHistoryStoreDuration: 60,
maxDatasetSize: 300
},
[StandardSubLevelEnum.enterprise]: {
price: 599,
totalPoints: 30000,
maxTeamMember: 100,
maxAppAmount: 500,
maxDatasetAmount: 300,
chatHistoryStoreDuration: 180,
maxDatasetSize: 1000
}
};
(global as any).subPlans = {
standard: mockStandardPlans
};
const result = getStandardPlansConfig();
expect(result).toBe(mockStandardPlans);
expect(result).toHaveProperty(StandardSubLevelEnum.free);
expect(result).toHaveProperty(StandardSubLevelEnum.basic);
expect(result).toHaveProperty(StandardSubLevelEnum.advanced);
});
it('global.subPlans 不存在时返回 undefined', () => {
const result = getStandardPlansConfig();
expect(result).toBeUndefined();
});
it('global.subPlans 存在但 standard 不存在时返回 undefined', () => {
(global as any).subPlans = {};
const result = getStandardPlansConfig();
expect(result).toBeUndefined();
});
});
describe('getStandardPlanConfig', () => {
beforeEach(() => {
delete (global as any).subPlans;
});
it('返回指定 level 的套餐配置', () => {
const mockBasicPlan: TeamStandardSubPlanItemType = {
price: 99,
totalPoints: 2000,
maxTeamMember: 10,
maxAppAmount: 50,
maxDatasetAmount: 20,
chatHistoryStoreDuration: 30,
maxDatasetSize: 100
};
(global as any).subPlans = {
standard: {
[StandardSubLevelEnum.basic]: mockBasicPlan
}
};
const result = getStandardPlanConfig(StandardSubLevelEnum.basic);
expect(result).toBe(mockBasicPlan);
expect(result?.price).toBe(99);
expect(result?.totalPoints).toBe(2000);
});
it('返回 free 级别配置', () => {
const mockFreePlan: TeamStandardSubPlanItemType = {
price: 0,
totalPoints: 100,
maxTeamMember: 1,
maxAppAmount: 5,
maxDatasetAmount: 2,
chatHistoryStoreDuration: 7,
maxDatasetSize: 10
};
(global as any).subPlans = {
standard: {
[StandardSubLevelEnum.free]: mockFreePlan
}
};
const result = getStandardPlanConfig(StandardSubLevelEnum.free);
expect(result).toBe(mockFreePlan);
});
it('返回 advanced 级别配置', () => {
const mockAdvancedPlan: TeamStandardSubPlanItemType = {
price: 299,
totalPoints: 10000,
maxTeamMember: 50,
maxAppAmount: 200,
maxDatasetAmount: 100,
chatHistoryStoreDuration: 90,
maxDatasetSize: 500
};
(global as any).subPlans = {
standard: {
[StandardSubLevelEnum.advanced]: mockAdvancedPlan
}
};
const result = getStandardPlanConfig(StandardSubLevelEnum.advanced);
expect(result).toBe(mockAdvancedPlan);
});
it('返回 custom 级别配置', () => {
const mockCustomPlan: TeamStandardSubPlanItemType = {
price: 999,
totalPoints: 50000,
maxTeamMember: 200,
maxAppAmount: 1000,
maxDatasetAmount: 500,
chatHistoryStoreDuration: 365,
maxDatasetSize: 2000
};
(global as any).subPlans = {
standard: {
[StandardSubLevelEnum.custom]: mockCustomPlan
}
};
const result = getStandardPlanConfig(StandardSubLevelEnum.custom);
expect(result).toBe(mockCustomPlan);
});
it('global.subPlans 不存在时返回 undefined', () => {
const result = getStandardPlanConfig(StandardSubLevelEnum.basic);
expect(result).toBeUndefined();
});
it('global.subPlans.standard 不存在时返回 undefined', () => {
(global as any).subPlans = {};
const result = getStandardPlanConfig(StandardSubLevelEnum.basic);
expect(result).toBeUndefined();
});
it('指定的 level 不存在时返回 undefined', () => {
(global as any).subPlans = {
standard: {
[StandardSubLevelEnum.free]: {
price: 0,
totalPoints: 100,
maxTeamMember: 1,
maxAppAmount: 5,
maxDatasetAmount: 2,
chatHistoryStoreDuration: 7,
maxDatasetSize: 10
}
}
};
const result = getStandardPlanConfig(StandardSubLevelEnum.basic);
expect(result).toBeUndefined();
});
});
describe('sortStandPlans', () => {
const createMockPlan = (
level: StandardSubLevelEnum,
id: string = '507f1f77bcf86cd799439011'
): TeamSubSchemaType => ({
_id: id,
teamId: '507f1f77bcf86cd799439012',
type: SubTypeEnum.standard,
startTime: new Date('2024-01-01'),
expiredTime: new Date('2025-01-01'),
currentMode: SubModeEnum.month,
nextMode: SubModeEnum.month,
currentSubLevel: level,
nextSubLevel: level,
totalPoints: 1000,
surplusPoints: 500,
currentExtraDatasetSize: 0
});
it('按 weight 降序排序:custom(7) > enterprise(6) > advanced(5)', () => {
const plans = [
createMockPlan(StandardSubLevelEnum.advanced, 'id1'),
createMockPlan(StandardSubLevelEnum.custom, 'id2'),
createMockPlan(StandardSubLevelEnum.enterprise, 'id3')
];
const sorted = sortStandPlans(plans);
expect(sorted[0].currentSubLevel).toBe(StandardSubLevelEnum.custom); // weight: 7
expect(sorted[1].currentSubLevel).toBe(StandardSubLevelEnum.enterprise); // weight: 6
expect(sorted[2].currentSubLevel).toBe(StandardSubLevelEnum.advanced); // weight: 5
});
it('按 weight 降序排序:basic(4) > team(3) > experience(2) > free(1)', () => {
const plans = [
createMockPlan(StandardSubLevelEnum.free, 'id1'),
createMockPlan(StandardSubLevelEnum.experience, 'id2'),
createMockPlan(StandardSubLevelEnum.team, 'id3'),
createMockPlan(StandardSubLevelEnum.basic, 'id4')
];
const sorted = sortStandPlans(plans);
expect(sorted[0].currentSubLevel).toBe(StandardSubLevelEnum.basic); // weight: 4
expect(sorted[1].currentSubLevel).toBe(StandardSubLevelEnum.team); // weight: 3
expect(sorted[2].currentSubLevel).toBe(StandardSubLevelEnum.experience); // weight: 2
expect(sorted[3].currentSubLevel).toBe(StandardSubLevelEnum.free); // weight: 1
});
it('完整排序:custom > enterprise > advanced > basic > team > experience > free', () => {
const plans = [
createMockPlan(StandardSubLevelEnum.free, 'id1'),
createMockPlan(StandardSubLevelEnum.basic, 'id2'),
createMockPlan(StandardSubLevelEnum.advanced, 'id3'),
createMockPlan(StandardSubLevelEnum.custom, 'id4'),
createMockPlan(StandardSubLevelEnum.experience, 'id5'),
createMockPlan(StandardSubLevelEnum.team, 'id6'),
createMockPlan(StandardSubLevelEnum.enterprise, 'id7')
];
const sorted = sortStandPlans(plans);
expect(sorted[0].currentSubLevel).toBe(StandardSubLevelEnum.custom); // 7
expect(sorted[1].currentSubLevel).toBe(StandardSubLevelEnum.enterprise); // 6
expect(sorted[2].currentSubLevel).toBe(StandardSubLevelEnum.advanced); // 5
expect(sorted[3].currentSubLevel).toBe(StandardSubLevelEnum.basic); // 4
expect(sorted[4].currentSubLevel).toBe(StandardSubLevelEnum.team); // 3
expect(sorted[5].currentSubLevel).toBe(StandardSubLevelEnum.experience); // 2
expect(sorted[6].currentSubLevel).toBe(StandardSubLevelEnum.free); // 1
});
it('单个元素数组保持不变', () => {
const plans = [createMockPlan(StandardSubLevelEnum.basic)];
const sorted = sortStandPlans(plans);
expect(sorted).toHaveLength(1);
expect(sorted[0].currentSubLevel).toBe(StandardSubLevelEnum.basic);
});
it('空数组返回空数组', () => {
const plans: TeamSubSchemaType[] = [];
const sorted = sortStandPlans(plans);
expect(sorted).toHaveLength(0);
});
it('相同 level 的多个套餐保持相对顺序稳定', () => {
const plans = [
createMockPlan(StandardSubLevelEnum.basic, 'id1'),
createMockPlan(StandardSubLevelEnum.basic, 'id2'),
createMockPlan(StandardSubLevelEnum.basic, 'id3')
];
const sorted = sortStandPlans(plans);
expect(sorted).toHaveLength(3);
expect(sorted[0]._id).toBe('id1');
expect(sorted[1]._id).toBe('id2');
expect(sorted[2]._id).toBe('id3');
});
it('修改原数组(sort 是 in-place 操作)', () => {
const plans = [
createMockPlan(StandardSubLevelEnum.free, 'id1'),
createMockPlan(StandardSubLevelEnum.advanced, 'id2')
];
const sorted = sortStandPlans(plans);
expect(sorted).toBe(plans); // 返回的是同一个数组引用
expect(plans[0].currentSubLevel).toBe(StandardSubLevelEnum.advanced);
expect(plans[1].currentSubLevel).toBe(StandardSubLevelEnum.free);
});
it('混合不同 level 的套餐正确排序', () => {
const plans = [
createMockPlan(StandardSubLevelEnum.basic, 'id1'),
createMockPlan(StandardSubLevelEnum.free, 'id2'),
createMockPlan(StandardSubLevelEnum.custom, 'id3'),
createMockPlan(StandardSubLevelEnum.advanced, 'id4')
];
const sorted = sortStandPlans(plans);
expect(sorted[0]._id).toBe('id3'); // custom
expect(sorted[1]._id).toBe('id4'); // advanced
expect(sorted[2]._id).toBe('id1'); // basic
expect(sorted[3]._id).toBe('id2'); // free
});
});
describe('buildStandardPlan', () => {
describe('展示字段始终取自 standardConstants', () => {
it('name/desc/price 取自 standardConstants', () => {
const result = buildStandardPlan(baseStandard, baseConstants);
expect(result.name).toBe('Basic Plan');
expect(result.desc).toBe('Basic description');
expect(result.price).toBe(99);
});
it('priceDescription/customFormUrl/customDescriptions 取自 standardConstants', () => {
const constants: TeamStandardSubPlanItemType = {
...baseConstants,
priceDescription: '联系销售',
customFormUrl: 'https://example.com/form',
customDescriptions: ['特性A', '特性B']
};
const result = buildStandardPlan(baseStandard, constants);
expect(result.priceDescription).toBe('联系销售');
expect(result.customFormUrl).toBe('https://example.com/form');
expect(result.customDescriptions).toEqual(['特性A', '特性B']);
});
it('wecom 取自 standardConstants', () => {
const constants: TeamStandardSubPlanItemType = {
...baseConstants,
wecom: { price: 9.9, points: 500 }
};
const result = buildStandardPlan(baseStandard, constants);
expect(result.wecom).toEqual({ price: 9.9, points: 500 });
});
it('standardConstants 展示字段为 undefined 时结果也为 undefined', () => {
const result = buildStandardPlan(baseStandard, baseConstants);
expect(result.priceDescription).toBeUndefined();
expect(result.customFormUrl).toBeUndefined();
expect(result.customDescriptions).toBeUndefined();
expect(result.wecom).toBeUndefined();
});
});
describe('DB 元数据从 standard spread', () => {
it('携带 _id/teamId/type/时间/模式等字段', () => {
const result = buildStandardPlan(baseStandard, baseConstants);
expect(result._id).toBe(mockPlanId);
expect(result.teamId).toBe(mockTeamId);
expect(result.type).toBe(SubTypeEnum.standard);
expect(result.startTime).toEqual(new Date('2024-01-01'));
expect(result.expiredTime).toEqual(new Date('2025-01-01'));
expect(result.currentMode).toBe(SubModeEnum.month);
expect(result.nextMode).toBe(SubModeEnum.month);
expect(result.currentSubLevel).toBe(StandardSubLevelEnum.basic);
expect(result.nextSubLevel).toBe(StandardSubLevelEnum.basic);
});
it('totalPoints/surplusPoints/currentExtraDatasetSize 来自 standard', () => {
const result = buildStandardPlan(baseStandard, baseConstants);
// standard.totalPoints=1000,standardConstants.totalPoints=2000,应取 standard
expect(result.totalPoints).toBe(1000);
expect(result.surplusPoints).toBe(500);
expect(result.currentExtraDatasetSize).toBe(0);
});
it('annualBonusPoints 来自 standard', () => {
const standard: TeamSubSchemaType = { ...baseStandard, annualBonusPoints: 200 };
const result = buildStandardPlan(standard, baseConstants);
expect(result.annualBonusPoints).toBe(200);
});
});
describe('限制字段:DB override 优先', () => {
it('standard.maxTeamMember 有值时取 standard 的值', () => {
const standard: TeamSubSchemaType = { ...baseStandard, maxTeamMember: 5 };
const result = buildStandardPlan(standard, baseConstants);
expect(result.maxTeamMember).toBe(5);
});
it('standard.maxTeamMember 为 undefined 时回退到 standardConstants', () => {
const result = buildStandardPlan(baseStandard, baseConstants);
expect(result.maxTeamMember).toBe(10);
});
it('standard.requestsPerMinute 有值时取 standard 的值', () => {
const standard: TeamSubSchemaType = { ...baseStandard, requestsPerMinute: 60 };
const constants: TeamStandardSubPlanItemType = { ...baseConstants, requestsPerMinute: 30 };
const result = buildStandardPlan(standard, constants);
expect(result.requestsPerMinute).toBe(60);
});
it('standard.requestsPerMinute 为 undefined 时回退到 standardConstants', () => {
const constants: TeamStandardSubPlanItemType = { ...baseConstants, requestsPerMinute: 30 };
const result = buildStandardPlan(baseStandard, constants);
expect(result.requestsPerMinute).toBe(30);
});
it('standard.chatHistoryStoreDuration 有值时取 standard 的值', () => {
const standard: TeamSubSchemaType = { ...baseStandard, chatHistoryStoreDuration: 90 };
const result = buildStandardPlan(standard, baseConstants);
expect(result.chatHistoryStoreDuration).toBe(90);
});
it('standard.chatHistoryStoreDuration 为 undefined 时回退到 standardConstants', () => {
const result = buildStandardPlan(baseStandard, baseConstants);
expect(result.chatHistoryStoreDuration).toBe(30);
});
it('standard.maxDatasetSize 有值时取 standard 的值', () => {
const standard: TeamSubSchemaType = { ...baseStandard, maxDatasetSize: 200 };
const result = buildStandardPlan(standard, baseConstants);
expect(result.maxDatasetSize).toBe(200);
});
it('standard.maxDatasetSize 为 undefined 时回退到 standardConstants', () => {
const result = buildStandardPlan(baseStandard, baseConstants);
expect(result.maxDatasetSize).toBe(100);
});
it('所有可选限制字段 DB override 行为一致', () => {
const standard: TeamSubSchemaType = {
...baseStandard,
websiteSyncPerDataset: 100,
appRegistrationCount: 5,
auditLogStoreDuration: 180,
ticketResponseTime: 4,
customDomain: 3,
maxUploadFileSize: 512,
maxUploadFileCount: 20
};
const constants: TeamStandardSubPlanItemType = {
...baseConstants,
websiteSyncPerDataset: 50,
appRegistrationCount: 2,
auditLogStoreDuration: 90,
ticketResponseTime: 8,
customDomain: 1,
maxUploadFileSize: 256,
maxUploadFileCount: 10
};
const result = buildStandardPlan(standard, constants);
expect(result.websiteSyncPerDataset).toBe(100);
expect(result.appRegistrationCount).toBe(5);
expect(result.auditLogStoreDuration).toBe(180);
expect(result.ticketResponseTime).toBe(4);
expect(result.customDomain).toBe(3);
expect(result.maxUploadFileSize).toBe(512);
expect(result.maxUploadFileCount).toBe(20);
});
it('可选限制字段全部 undefined 时回退到 standardConstants', () => {
const constants: TeamStandardSubPlanItemType = {
...baseConstants,
websiteSyncPerDataset: 50,
appRegistrationCount: 2,
auditLogStoreDuration: 90,
ticketResponseTime: 8,
customDomain: 1,
maxUploadFileSize: 256,
maxUploadFileCount: 10
};
const result = buildStandardPlan(baseStandard, constants);
expect(result.websiteSyncPerDataset).toBe(50);
expect(result.appRegistrationCount).toBe(2);
expect(result.auditLogStoreDuration).toBe(90);
expect(result.ticketResponseTime).toBe(8);
expect(result.customDomain).toBe(1);
expect(result.maxUploadFileSize).toBe(256);
expect(result.maxUploadFileCount).toBe(10);
});
});
describe('字段名映射:maxApp → maxAppAmount,maxDataset → maxDatasetAmount', () => {
it('standard.maxApp 映射到结果的 maxAppAmount', () => {
const standard: TeamSubSchemaType = { ...baseStandard, maxApp: 100 };
const result = buildStandardPlan(standard, baseConstants);
expect(result.maxAppAmount).toBe(100);
});
it('standard.maxApp 为 undefined 时 maxAppAmount 回退到 standardConstants.maxAppAmount', () => {
const result = buildStandardPlan(baseStandard, baseConstants);
expect(result.maxAppAmount).toBe(50);
});
it('standard.maxDataset 映射到结果的 maxDatasetAmount', () => {
const standard: TeamSubSchemaType = { ...baseStandard, maxDataset: 30 };
const result = buildStandardPlan(standard, baseConstants);
expect(result.maxDatasetAmount).toBe(30);
});
it('standard.maxDataset 为 undefined 时 maxDatasetAmount 回退到 standardConstants.maxDatasetAmount', () => {
const result = buildStandardPlan(baseStandard, baseConstants);
expect(result.maxDatasetAmount).toBe(20);
});
it('standard.maxApp 和 standard.maxDataset 同时有值', () => {
const standard: TeamSubSchemaType = { ...baseStandard, maxApp: 88, maxDataset: 44 };
const result = buildStandardPlan(standard, baseConstants);
expect(result.maxAppAmount).toBe(88);
expect(result.maxDatasetAmount).toBe(44);
});
});
});
describe('initTeamFreePlan', () => {
beforeEach(() => {
vi.clearAllMocks();
delete (global as any).subPlans;
});
it('创建新的免费套餐(不存在时)', async () => {
const teamId = mockTeamId;
vi.spyOn(MongoTeamSub, 'findOne').mockResolvedValue(null);
const mockCreatedPlan = {
_id: mockPlanId,
teamId,
type: SubTypeEnum.standard,
currentSubLevel: StandardSubLevelEnum.free
};
vi.spyOn(MongoTeamSub, 'create').mockResolvedValue([mockCreatedPlan] as any);
(global as any).subPlans = {
standard: {
[StandardSubLevelEnum.free]: {
totalPoints: 100
}
}
};
const result = await initTeamFreePlan({ teamId });
expect(MongoTeamSub.findOne).toHaveBeenCalled();
expect(MongoTeamSub.create).toHaveBeenCalled();
expect(result).toEqual([mockCreatedPlan]);
});
it('重置已存在的免费套餐', async () => {
const teamId = mockTeamId;
const mockExistingPlan = {
_id: mockPlanId,
teamId,
type: SubTypeEnum.standard,
currentSubLevel: StandardSubLevelEnum.free,
nextSubLevel: StandardSubLevelEnum.free,
currentMode: SubModeEnum.month,
nextMode: SubModeEnum.month,
startTime: new Date('2024-01-01'),
expiredTime: new Date('2024-12-01'),
totalPoints: 100,
surplusPoints: -50,
currentExtraDatasetSize: 0,
save: vi.fn().mockResolvedValue(true)
};
vi.spyOn(MongoTeamSub, 'findOne').mockResolvedValue(mockExistingPlan as any);
(global as any).subPlans = {
standard: {
[StandardSubLevelEnum.free]: {
totalPoints: 100
}
}
};
await initTeamFreePlan({ teamId });
expect(mockExistingPlan.surplusPoints).toBe(50); // -50 + 100
expect(mockExistingPlan.save).toHaveBeenCalled();
});
});
describe('getTeamStandPlan', () => {
beforeEach(() => {
vi.clearAllMocks();
delete (global as any).subPlans;
});
it('返回团队标准套餐', async () => {
const teamId = mockTeamId;
const mockPlan = {
_id: mockPlanId,
teamId,
type: SubTypeEnum.standard,
currentSubLevel: StandardSubLevelEnum.basic,
totalPoints: 2000,
surplusPoints: 1500,
startTime: new Date('2024-01-01'),
expiredTime: new Date('2025-01-01'),
currentMode: SubModeEnum.month,
nextMode: SubModeEnum.month,
nextSubLevel: StandardSubLevelEnum.basic,
currentExtraDatasetSize: 0
};
// getTeamStandPlan 不使用 .lean(),直接返回数组
vi.spyOn(MongoTeamSub, 'find').mockResolvedValue([mockPlan] as any);
(global as any).subPlans = {
standard: {
[StandardSubLevelEnum.basic]: {
name: 'Basic Plan',
price: 99,
totalPoints: 2000,
maxTeamMember: 10,
maxAppAmount: 50,
maxDatasetAmount: 20,
maxDatasetSize: 100,
chatHistoryStoreDuration: 30
}
}
};
const result = await getTeamStandPlan({ teamId });
expect(MongoTeamSub.find).toHaveBeenCalled();
expect(result[SubTypeEnum.standard]).toBeDefined();
expect(result[SubTypeEnum.standard]?.name).toBe('Basic Plan');
});
});
describe('getTeamPlanStatus', () => {
beforeEach(() => {
vi.clearAllMocks();
delete (global as any).subPlans;
});
it('返回团队套餐状态(包含标准套餐)', async () => {
const teamId = mockTeamId;
const mockStandardPlan = {
_id: mockPlanId,
teamId,
type: SubTypeEnum.standard,
currentSubLevel: StandardSubLevelEnum.basic,
totalPoints: 2000,
surplusPoints: 1500,
startTime: new Date('2024-01-01'),
expiredTime: new Date('2025-01-01'),
currentMode: SubModeEnum.month,
nextMode: SubModeEnum.month,
nextSubLevel: StandardSubLevelEnum.basic,
currentExtraDatasetSize: 0,
maxDatasetSize: 100
};
// getTeamPlanStatus 使用 .lean(),需要 mock 返回带 lean 方法的对象
const mockQuery = {
lean: vi.fn().mockResolvedValue([mockStandardPlan])
};
vi.spyOn(MongoTeamSub, 'find').mockReturnValue(mockQuery as any);
(global as any).subPlans = {
standard: {
[StandardSubLevelEnum.basic]: {
name: 'Basic Plan',
price: 99,
totalPoints: 2000,
maxTeamMember: 10,
maxAppAmount: 50,
maxDatasetAmount: 20,
maxDatasetSize: 100,
chatHistoryStoreDuration: 30
}
}
};
const result = await getTeamPlanStatus({ teamId });
expect(result.totalPoints).toBe(2000);
expect(result.usedPoints).toBe(500); // 2000 - 1500
expect(result.datasetMaxSize).toBe(100);
expect(result[SubTypeEnum.standard]).toBeDefined();
});
it('包含额外积分套餐', async () => {
const teamId = mockTeamId;
const mockStandardPlan = {
_id: mockPlanId,
teamId,
type: SubTypeEnum.standard,
currentSubLevel: StandardSubLevelEnum.basic,
totalPoints: 2000,
surplusPoints: 1500,
startTime: new Date('2024-01-01'),
expiredTime: new Date('2025-01-01'),
currentMode: SubModeEnum.month,
nextMode: SubModeEnum.month,
nextSubLevel: StandardSubLevelEnum.basic,
currentExtraDatasetSize: 0
};
const mockExtraPointsPlan = {
_id: '507f1f77bcf86cd799439013',
teamId,
type: SubTypeEnum.extraPoints,
totalPoints: 5000,
surplusPoints: 3000,
startTime: new Date('2024-01-01'),
expiredTime: new Date('2025-01-01')
};
// getTeamPlanStatus 使用 .lean()
const mockQuery = {
lean: vi.fn().mockResolvedValue([mockStandardPlan, mockExtraPointsPlan])
};
vi.spyOn(MongoTeamSub, 'find').mockReturnValue(mockQuery as any);
(global as any).subPlans = {
standard: {
[StandardSubLevelEnum.basic]: {
name: 'Basic Plan',
price: 99,
totalPoints: 2000,
maxTeamMember: 10,
maxAppAmount: 50,
maxDatasetAmount: 20,
maxDatasetSize: 100,
chatHistoryStoreDuration: 30
}
}
};
const result = await getTeamPlanStatus({ teamId });
expect(result.totalPoints).toBe(7000); // 2000 + 5000
expect(result.usedPoints).toBe(2500); // 7000 - (1500 + 3000)
});
it('包含额外数据集大小套餐', async () => {
const teamId = mockTeamId;
const mockStandardPlan = {
_id: mockPlanId,
teamId,
type: SubTypeEnum.standard,
currentSubLevel: StandardSubLevelEnum.basic,
totalPoints: 2000,
surplusPoints: 1500,
startTime: new Date('2024-01-01'),
expiredTime: new Date('2025-01-01'),
currentMode: SubModeEnum.month,
nextMode: SubModeEnum.month,
nextSubLevel: StandardSubLevelEnum.basic,
currentExtraDatasetSize: 0,
maxDatasetSize: 100
};
const mockExtraDatasetPlan = {
_id: '507f1f77bcf86cd799439013',
teamId,
type: SubTypeEnum.extraDatasetSize,
currentExtraDatasetSize: 200,
totalPoints: 0,
surplusPoints: 0,
startTime: new Date('2024-01-01'),
expiredTime: new Date('2025-01-01')
};
// getTeamPlanStatus 使用 .lean()
const mockQuery = {
lean: vi.fn().mockResolvedValue([mockStandardPlan, mockExtraDatasetPlan])
};
vi.spyOn(MongoTeamSub, 'find').mockReturnValue(mockQuery as any);
(global as any).subPlans = {
standard: {
[StandardSubLevelEnum.basic]: {
name: 'Basic Plan',
price: 99,
totalPoints: 2000,
maxTeamMember: 10,
maxAppAmount: 50,
maxDatasetAmount: 20,
maxDatasetSize: 100,
chatHistoryStoreDuration: 30
}
}
};
const result = await getTeamPlanStatus({ teamId });
expect(result.datasetMaxSize).toBe(300); // 100 + 200
});
});
describe('teamPoint', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('getTeamPoints', () => {
it('从缓存获取积分信息', async () => {
const teamId = mockTeamId;
// 全局 mock 会自动处理 Redis 调用
const result = await teamPoint.getTeamPoints({ teamId });
expect(result).toHaveProperty('totalPoints');
expect(result).toHaveProperty('surplusPoints');
expect(result).toHaveProperty('usedPoints');
});
});
describe('incrTeamPointsCache', () => {
it('增加团队积分缓存', async () => {
const teamId = mockTeamId;
const value = 100;
// 全局 mock 会自动处理 Redis 调用
await expect(teamPoint.incrTeamPointsCache({ teamId, value })).resolves.not.toThrow();
});
});
describe('updateTeamPointsCache', () => {
it('更新团队积分缓存', async () => {
const teamId = mockTeamId;
const totalPoints = 2000;
const surplusPoints = 1500;
// 全局 mock 会自动处理 Redis 调用
await expect(
teamPoint.updateTeamPointsCache({ teamId, totalPoints, surplusPoints })
).resolves.not.toThrow();
});
});
describe('clearTeamPointsCache', () => {
it('清除团队积分缓存', async () => {
const teamId = mockTeamId;
// 全局 mock 会自动处理 Redis 调用
await expect(teamPoint.clearTeamPointsCache(teamId)).resolves.not.toThrow();
});
});
});
describe('teamQPM', () => {
beforeEach(() => {
vi.clearAllMocks();
delete (global as any).subPlans;
});
describe('getTeamQPMLimit', () => {
it('获取团队 QPM 限制', async () => {
const teamId = mockTeamId;
// 全局 mock 会自动处理 Redis 和数据库调用
const result = await teamQPM.getTeamQPMLimit(teamId);
expect(typeof result === 'number' || result === null).toBe(true);
});
});
describe('setCachedTeamQPMLimit', () => {
it('设置团队 QPM 限制缓存', async () => {
const teamId = mockTeamId;
const limit = 60;
// 全局 mock 会自动处理 Redis 调用
await expect(teamQPM.setCachedTeamQPMLimit(teamId, limit)).resolves.not.toThrow();
});
});
describe('clearTeamQPMLimitCache', () => {
it('清除团队 QPM 限制缓存', async () => {
const teamId = mockTeamId;
// 全局 mock 会自动处理 Redis 调用
await expect(teamQPM.clearTeamQPMLimitCache(teamId)).resolves.not.toThrow();
});
});
});
describe('clearTeamPlanCache', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('清除团队套餐相关的所有缓存', async () => {
const teamId = mockTeamId;
await clearTeamPlanCache(teamId);
// 函数应该被调用(具体实现会调用 teamPoint 和 teamQPM 的清除方法)
expect(teamId).toBeDefined();
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment