Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b6da76ac1 | ||
|
|
3c9f12bb94 | ||
|
|
d0c3d60751 | ||
|
|
d057d20c17 | ||
|
|
9a831b5b8b | ||
|
|
7957f96d2c | ||
|
|
fb1392565d | ||
|
|
b8a75921ed | ||
|
|
7dd8e7bea1 | ||
|
|
7f9899f7f3 | ||
|
|
eef6d7518e | ||
|
|
6fd49b0955 | ||
|
|
e19ac56fe5 | ||
|
|
2378615887 | ||
|
|
ba9d9c3d5f | ||
|
|
d9450bd7ee | ||
|
|
69bb1f3fa5 | ||
|
|
7a38a81e12 | ||
|
|
55d0ed9de6 | ||
|
|
941549ff04 | ||
|
|
1f170e1cd2 | ||
|
|
b0d0a76a8e | ||
|
|
16018a7e0b | ||
|
|
707be3362c | ||
|
|
942aeeac2e | ||
|
|
1111f07fa7 | ||
|
|
2f1506bf07 | ||
|
|
48fbc74168 | ||
|
|
92b592dd98 | ||
|
|
8cafebe26c | ||
|
|
d69554575d |
@@ -1,33 +0,0 @@
|
||||
# proxy
|
||||
# AXIOS_PROXY_HOST=127.0.0.1
|
||||
# AXIOS_PROXY_PORT=7890
|
||||
# email
|
||||
MY_MAIL=xxx@qq.com
|
||||
MAILE_CODE=xxx
|
||||
# ali ems
|
||||
aliAccessKeyId=xxx
|
||||
aliAccessKeySecret=xxx
|
||||
aliSignName=xxx
|
||||
aliTemplateCode=SMS_xxx
|
||||
# token
|
||||
TOKEN_KEY=xxx
|
||||
# root key, 最高权限
|
||||
ROOT_KEY=xxx
|
||||
# 是否进行安全校验(1: 开启,0: 关闭)
|
||||
SENSITIVE_CHECK=1
|
||||
# openai
|
||||
# OPENAI_BASE_URL=https://api.openai.com/v1
|
||||
# OPENAI_BASE_URL_AUTH=可选的安全凭证(不需要的时候,记得去掉)
|
||||
OPENAIKEY=sk-xxx # 对话用的key
|
||||
OPENAI_TRAINING_KEY=sk-xxx # 训练用的key
|
||||
GPT4KEY=sk-xxx
|
||||
# claude
|
||||
CLAUDE_BASE_URL=calude模型请求地址
|
||||
CLAUDE_KEY=CLAUDE_KEY
|
||||
# db
|
||||
MONGODB_URI=mongodb://username:password@0.0.0.0:27017/test?authSource=admin
|
||||
PG_HOST=0.0.0.0
|
||||
PG_PORT=8100
|
||||
PG_USER=xxx
|
||||
PG_PASSWORD=xxx
|
||||
PG_DB_NAME=xxx
|
||||
77
.github/workflows/image.yml
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
name: Build images and copy image to docker
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
jobs:
|
||||
build-images:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
sudo apt update && sudo apt install -y nodejs npm
|
||||
- name: Set up QEMU (optional)
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
driver-opts: network=host
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GH_PAT }}
|
||||
- name: Set DOCKER_REPO_TAGGED based on branch or tag
|
||||
run: |
|
||||
if [[ "${{ github.ref_name }}" == "main" ]]; then
|
||||
echo "DOCKER_REPO_TAGGED=ghcr.io/${{ github.repository_owner }}/fastgpt:latest" >> $GITHUB_ENV
|
||||
else
|
||||
echo "DOCKER_REPO_TAGGED=ghcr.io/${{ github.repository_owner }}/fastgpt:${{ github.ref_name }}" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Build and publish image for main branch or tag push event
|
||||
env:
|
||||
DOCKER_REPO_TAGGED: ${{ env.DOCKER_REPO_TAGGED }}
|
||||
run: |
|
||||
cd client && \
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--label "org.opencontainers.image.source= https://github.com/ ${{ github.repository_owner }}/FastGPT" \
|
||||
--label "org.opencontainers.image.description=fastgpt image" \
|
||||
--label "org.opencontainers.image.licenses=MIT" \
|
||||
--push \
|
||||
-t ${DOCKER_REPO_TAGGED} \
|
||||
-f Dockerfile \
|
||||
.
|
||||
push-to-docker-hub:
|
||||
needs: build-images
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_NAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
- name: Set DOCKER_REPO_TAGGED based on branch or tag
|
||||
run: |
|
||||
if [[ "${{ github.ref_name }}" == "main" ]]; then
|
||||
echo "IMAGE_TAG=latest" >> $GITHUB_ENV
|
||||
else
|
||||
echo "IMAGE_TAG=${{ github.ref_name }}" >> $GITHUB_ENV
|
||||
fi
|
||||
- name: Pull image from GitHub Container Registry
|
||||
run: docker pull ghcr.io/${{ github.repository_owner }}/fastgpt:${{env.IMAGE_TAG}}
|
||||
- name: Tag image with Docker Hub repository name and version tag
|
||||
run: docker tag ghcr.io/${{ github.repository_owner }}/fastgpt:${{env.IMAGE_TAG}} ${{ secrets.DOCKER_IMAGE_NAME }}:${{env.IMAGE_TAG}}
|
||||
- name: Push image to Docker Hub
|
||||
run: docker push ${{ secrets.DOCKER_IMAGE_NAME }}:${{env.IMAGE_TAG}}
|
||||
50
.github/workflows/release.yml
vendored
@@ -1,50 +0,0 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
sudo apt update && sudo apt install -y nodejs npm
|
||||
- # Add support for more platforms with QEMU (optional)
|
||||
# https://github.com/docker/setup-qemu-action
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
driver-opts: network=host
|
||||
|
||||
- name: Login to gitbub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GH_PAT }}
|
||||
- name: build and publish image
|
||||
env:
|
||||
# fork friendly ^^
|
||||
DOCKER_REPO: ghcr.io/${{ github.repository_owner }}/fastgpt
|
||||
run: |
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--label "org.opencontainers.image.source=https://github.com/${{ github.repository_owner }}/FastGPT" \
|
||||
--label "org.opencontainers.image.description=fastgpt image" \
|
||||
--label "org.opencontainers.image.licenses=MIT" \
|
||||
--push \
|
||||
-t ${DOCKER_REPO}:latest \
|
||||
-f Dockerfile \
|
||||
.
|
||||
22
.gitignore
vendored
@@ -1,19 +1,10 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
node_modules/
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
.next/
|
||||
out/
|
||||
# production
|
||||
/build
|
||||
build/
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
@@ -34,6 +25,7 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
/.vscode/
|
||||
platform.json
|
||||
testApi/
|
||||
testApi/
|
||||
local/
|
||||
dist/
|
||||
6
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"editor.formatOnSave": true, //每次保存自动格式化
|
||||
"editor.mouseWheelZoom": true,
|
||||
"typescript.tsdk": "./client/node_modules/typescript/lib",
|
||||
"prettier.prettierPath": "./node_modules/prettier"
|
||||
}
|
||||
47
Makefile
@@ -1,47 +0,0 @@
|
||||
SERVICE_NAME=fastgpt
|
||||
# Image URL to use all building/pushing image targets
|
||||
IMG ?= $(SERVICE_NAME):latest
|
||||
|
||||
.PHONY: all
|
||||
all: build
|
||||
|
||||
##@ General
|
||||
|
||||
# The help target prints out all targets with their descriptions organized
|
||||
# beneath their categories. The categories are represented by '##@' and the
|
||||
# target descriptions by '##'. The awk commands is responsible for reading the
|
||||
# entire set of makefiles included in this invocation, looking for lines of the
|
||||
# file as xyz: ## something, and then pretty-format the target and help. Then,
|
||||
# if there's a line with ##@ something, that gets pretty-printed as a category.
|
||||
# More info on the usage of ANSI control characters for terminal formatting:
|
||||
# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
|
||||
# More info on the awk command:
|
||||
# http://linuxcommand.org/lc3_adv_awk.php
|
||||
|
||||
.PHONY: help
|
||||
help: ## Display this help.
|
||||
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
|
||||
|
||||
##@ Build
|
||||
|
||||
.PHONY: build
|
||||
build: ## Build desktop-frontend binary.
|
||||
pnpm run build
|
||||
|
||||
.PHONY: run
|
||||
run: ## Run a dev service from host.
|
||||
pnpm run start
|
||||
|
||||
.PHONY: docker-build
|
||||
docker-build: ## Build docker image with the desktop-frontend.
|
||||
docker build -t registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:latest . --network host --build-arg HTTP_PROXY=http://127.0.0.1:7890 --build-arg HTTPS_PROXY=http://127.0.0.1:7890
|
||||
|
||||
##@ Deployment
|
||||
|
||||
.PHONY: docker-run
|
||||
docker-run: ## Push docker image.
|
||||
docker run -d -p 8008:3000 --name fastgpt -v /web_project/yjl/fastgpt/logs:/app/.next/logs registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:latest
|
||||
|
||||
#TODO: add support of docker push
|
||||
|
||||
#TODO: add support of sealos apply
|
||||
@@ -1,11 +1,11 @@
|
||||
# Fast GPT
|
||||
|
||||
Fast GPT 允许你使用自己的 openai API KEY 来快速的调用 openai 接口,目前集成了 gpt35 和 embedding. 可构建自己的知识库。
|
||||
Fast GPT 允许你使用自己的 openai API KEY 来快速的调用 openai 接口,目前集成了 Gpt35, Gpt4 和 embedding. 可构建自己的知识库。
|
||||
|
||||
## 🛸 在线体验
|
||||
|
||||
🎉 [fastgpt.run](https://fastgpt.run/) (国内版)
|
||||
🎉 [ai.fastgpt.run](https://ai.fastgpt.run/) (海外版)
|
||||
🎉 [fastgpt.run](https://fastgpt.run/)
|
||||
🎉 [ai.fastgpt.run](https://ai.fastgpt.run/)
|
||||
|
||||

|
||||
|
||||
@@ -21,7 +21,8 @@ Fast GPT 允许你使用自己的 openai API KEY 来快速的调用 openai 接
|
||||
|
||||
## 🚀 私有化部署
|
||||
|
||||
- [docker-compose 部署教程](docs/deploy/docker.md)
|
||||
- [Sealos 部署](https://sealos.io/docs/examples/ai-applications/install-fastgpt-on-desktop) 无需服务器,代理和域名。
|
||||
- [docker-compose 部署](docs/deploy/docker.md)
|
||||
- [由社区贡献的宝塔部署和本地运行教程](https://space.bilibili.com/431177525/channel/collectiondetail?sid=1370663)
|
||||
|
||||
## :point_right: RoadMap
|
||||
|
||||
11
admin/.dockerignore
Normal file
@@ -0,0 +1,11 @@
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
node_modules
|
||||
npm-debug.log
|
||||
README.md
|
||||
.git
|
||||
|
||||
.yalc/
|
||||
yalc.lock
|
||||
testApi/
|
||||
node_modules
|
||||
8
admin/.env.template
Normal file
@@ -0,0 +1,8 @@
|
||||
MONGODB_URI=mongodb://username:psw@0.0.0.0:27017/?authSource=admin
|
||||
MONGODB_NAME=fastgpt
|
||||
ADMIN_USER=username
|
||||
ADMIN_PASS=password
|
||||
ADMIN_SECRET=any
|
||||
PARENT_URL=http://localhost:3000 # FastGpt服务的地址
|
||||
PARENT_ROOT_KEY=rootkey # FastGpt 的rootkey
|
||||
VITE_PUBLIC_SERVER_URL=http://localhost:3001 # 和server.js一致
|
||||
1
admin/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
node_modules/
|
||||
48
admin/Dockerfile
Normal file
@@ -0,0 +1,48 @@
|
||||
# Install dependencies only when needed
|
||||
FROM node:current-alpine AS builder
|
||||
RUN npm config set registry https://registry.npmmirror.com/
|
||||
RUN apk add --no-cache libc6-compat && npm install -g pnpm
|
||||
RUN pnpm config set registry https://registry.npmmirror.com/
|
||||
|
||||
WORKDIR /app
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
ENV VITE_PUBLIC_SERVER_URL ''
|
||||
# Install dependencies based on the preferred package manager
|
||||
COPY . .
|
||||
RUN \
|
||||
[ -f pnpm-lock.yaml ] && pnpm install || \
|
||||
(echo "Lockfile not found." && exit 1)
|
||||
|
||||
RUN pnpm build
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM node:current-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV production
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
RUN sed -i 's/https/http/' /etc/apk/repositories
|
||||
RUN apk add curl \
|
||||
&& apk add ca-certificates \
|
||||
&& update-ca-certificates
|
||||
|
||||
COPY package.json pnpm-lock.yaml* ./
|
||||
COPY --from=builder /app/server.js ./server.js
|
||||
COPY --from=builder /app/service ./service
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
||||
RUN npm config set registry https://registry.npmmirror.com/
|
||||
RUN npm install -g pnpm
|
||||
RUN pnpm config set registry https://registry.npmmirror.com/
|
||||
RUN pnpm install --prod
|
||||
RUN npm remove -g pnpm
|
||||
|
||||
ENV PORT=3001
|
||||
|
||||
EXPOSE 3001
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
201
admin/LICENSE
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
41
admin/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# FastGpt Admin
|
||||
|
||||
## 项目原理
|
||||
|
||||
使用 tushan 项目做前端,然后构造了一个与 mongodb 做沟通的 API 做后端,可以做到创建、修改和删除用户
|
||||
|
||||
## 开发
|
||||
|
||||
1. 复制 .env.template 文件,添加环境变量
|
||||
2. pnpm i
|
||||
3. pnpm dev
|
||||
|
||||
## 部署
|
||||
|
||||
1. 本地打包
|
||||
|
||||
`docker build -t registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-admin:latest . --network host --build-arg HTTP_PROXY=http://127.0.0.1:7890 --build-arg HTTPS_PROXY=http://127.0.0.1:7890`
|
||||
|
||||
2. 直接拉镜像: `registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-admin:latest`
|
||||
3. 部署时候填写环境变量: 数据库同 FastGpt 一致
|
||||
|
||||
```
|
||||
MONGODB_URI=mongodb://username:psw@0.0.0.0:27017/?authSource=admin
|
||||
MONGODB_NAME=fastgpt
|
||||
ADMIN_USER=username
|
||||
ADMIN_PASS=password
|
||||
ADMIN_SECRET=any
|
||||
VITE_PUBLIC_SERVER_URL=http://localhost:3001 # 和server.js一致
|
||||
```
|
||||
|
||||
## sealos 部署
|
||||
|
||||
1. 进入 sealos 官网: https://cloud.sealos.io/
|
||||
2. 打开 App Launchpad(应用管理) 工具
|
||||
3. 新建应用
|
||||
1. 镜像名: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-admin:latest
|
||||
2. 容器端口: 3001
|
||||
3. 环境变量: 参考上面
|
||||
4. 打开外网访问开关
|
||||
4. 点击部署。 完成后大约等待 1 分钟,
|
||||
5. 点击 sealos 提供的外网访问地址,可以直接访问。
|
||||
13
admin/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Tushan</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
41
admin/package.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "kbgpt-deafult",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"author": "anonymous",
|
||||
"scripts": {
|
||||
"dev": "concurrently \"vite\" \"npm run start:api\"",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"start:api": "nodemon server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@arco-design/web-react": "^2.49.1",
|
||||
"concurrently": "^8.1.0",
|
||||
"cors": "^2.8.5",
|
||||
"crypto": "^1.0.1",
|
||||
"dayjs": "^1.11.8",
|
||||
"dotenv": "^16.1.4",
|
||||
"express": "^4.18.2",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"mongoose": "^7.2.2",
|
||||
"react": "^18.2.0",
|
||||
"react-admin": "^4.11.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^12.3.1",
|
||||
"tushan": "^0.2.22"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jsonexport": "^3.0.2",
|
||||
"@types/lodash-es": "^4.17.7",
|
||||
"@types/node": "^20.2.5",
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@types/react-helmet": "^6.1.6",
|
||||
"@types/styled-components": "^5.1.26",
|
||||
"@vitejs/plugin-react": "^3.1.0",
|
||||
"typescript": "^4.9.2",
|
||||
"vite": "^4.2.1"
|
||||
}
|
||||
}
|
||||
5472
admin/pnpm-lock.yaml
generated
Normal file
10
admin/public/logo.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M28 88L49.5 57L118.5 29.5L248 51L323.5 122.5L360.5 324L301 421.5L164.5 412.5L118.5 324L127.5 225.5L143.5 184.5L151.5 130.5L127.5 95L82.5 80L49.5 95L28 88Z" fill="#DFDFDF"/>
|
||||
<path d="M144.734 22.04C139.186 22.0047 133.638 22.1568 128.1 22.496C84.33 25.196 40.5 49 24.238 67.492C7.97598 85.984 4 91.601 4 91.601C4 91.601 34.922 98.392 57 97.5C79.078 96.608 111.355 88.82 127.692 104.564C144.032 120.309 151.428 146.017 135.232 175.709C116.062 210.852 102.516 271.862 115.086 332.235C127.656 392.609 168.054 451.995 254.814 478.007C288.29 488.043 333.639 494.757 376.459 485.673C420.966 476.885 472.309 450.915 483.351 422.563C474.101 431.448 463.911 437.703 453.149 442.353C471.455 421.433 484.884 392.621 489.939 354.179L492.469 334.939L476.147 345.435C465.644 352.19 455.562 358.838 446.054 363.831C448.692 357.959 451.092 350.611 453.784 341.054C442.687 356.244 430.054 366.409 415.186 372.526C405.952 372.023 396.833 367.659 385.976 356.429C374.618 344.682 367.856 324.334 363.513 298.763C359.169 273.191 357.053 242.836 352.845 211.886C344.425 149.984 326.933 84.013 263.105 50.851C226.15 31.651 184.013 22.274 144.733 22.038L144.734 22.04ZM144.611 40.05C181.073 40.305 220.721 49.115 254.808 66.824C311.201 96.124 326.802 153.964 335.011 214.312C339.115 244.487 341.197 274.866 345.769 301.777C347.085 309.53 348.604 317.019 350.462 324.162C335.014 324.202 323.208 315.855 308.758 299.445C316.143 329.855 320.748 335.979 334.463 354.995C306.243 346.76 273.823 320.255 253.513 290.932C250.239 330.979 273.736 362.506 286.788 374.862C261.612 360.666 226.075 333.326 202.165 286.207C201.149 327.633 214.095 373.939 238.615 402.672C204.1 391.136 173.645 303.2 153.195 275.039C140.155 308.256 150.247 364.124 169.267 405.161C149.639 382.323 138.38 355.786 132.712 328.565C121.188 273.223 134.462 214.718 151.037 184.327C170.587 148.485 161.952 112.577 140.187 91.601C118.419 70.625 66 81 53.633 83.286C41.266 85.572 31 83.286 31 83.286C31 83.286 41.3371 75.1684 48 70C74.6656 49.3155 88.786 42.954 129.211 40.461C134.263 40.149 139.406 40.011 144.614 40.047L144.611 40.05Z" fill="url(#paint0_linear_1104_3)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1104_3" x1="384.5" y1="480" x2="256" y2="256" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF6011"/>
|
||||
<stop offset="1" stop-color="#FF9411"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
21
admin/server.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import { useUserRoute } from './service/route/user.js';
|
||||
import { useAppRoute } from './service/route/app.js';
|
||||
import { useKbRoute } from './service/route/kb.js';
|
||||
import { useSystemRoute } from './service/route/system.js';
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(express.static('dist'));
|
||||
|
||||
useUserRoute(app);
|
||||
useAppRoute(app);
|
||||
useKbRoute(app);
|
||||
useSystemRoute(app);
|
||||
|
||||
const PORT = process.env.PORT || 3001;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server is running on port ${PORT}`);
|
||||
});
|
||||
57
admin/service/route/app.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import { User, Model, Kb } from '../schema.js';
|
||||
import { auth } from './system.js';
|
||||
|
||||
export const useAppRoute = (app) => {
|
||||
// 获取AI助手列表
|
||||
app.get('/models', auth(), async (req, res) => {
|
||||
try {
|
||||
const start = parseInt(req.query._start) || 0;
|
||||
const end = parseInt(req.query._end) || 20;
|
||||
const order = req.query._order === 'DESC' ? -1 : 1;
|
||||
const sort = req.query._sort || '_id';
|
||||
const userId = req.query.userId || '';
|
||||
const name = req.query.name || '';
|
||||
const where = {
|
||||
...(userId ? { userId: userId } : {}),
|
||||
name
|
||||
};
|
||||
|
||||
const modelsRaw = await Model.find()
|
||||
.skip(start)
|
||||
.limit(end - start)
|
||||
.sort({ [sort]: order });
|
||||
|
||||
const models = [];
|
||||
|
||||
for (const modelRaw of modelsRaw) {
|
||||
const model = modelRaw.toObject();
|
||||
|
||||
// 获取与模型关联的知识库名称
|
||||
const kbNames = [];
|
||||
for (const kbId of model.chat.relatedKbs) {
|
||||
const kb = await Kb.findById(kbId);
|
||||
kbNames.push(kb.name);
|
||||
}
|
||||
|
||||
const orderedModel = {
|
||||
id: model._id.toString(),
|
||||
userId: model.userId,
|
||||
name: model.name,
|
||||
relatedKbs: kbNames, // 将relatedKbs的id转换为相应的Kb名称
|
||||
searchMode: model.chat?.searchMode,
|
||||
systemPrompt: model.chat?.systemPrompt || '',
|
||||
temperature: model.chat?.temperature
|
||||
};
|
||||
|
||||
models.push(orderedModel);
|
||||
}
|
||||
const totalCount = await Model.countDocuments();
|
||||
res.header('Access-Control-Expose-Headers', 'X-Total-Count');
|
||||
res.header('X-Total-Count', totalCount);
|
||||
res.json(models);
|
||||
} catch (err) {
|
||||
console.log(`Error fetching models: ${err}`);
|
||||
res.status(500).json({ error: 'Error fetching models', details: err.message });
|
||||
}
|
||||
});
|
||||
};
|
||||
44
admin/service/route/kb.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Kb } from '../schema.js';
|
||||
import { auth } from './system.js';
|
||||
|
||||
export const useKbRoute = (app) => {
|
||||
// 获取用户知识库列表
|
||||
app.get('/kbs', auth(), async (req, res) => {
|
||||
try {
|
||||
const start = parseInt(req.query._start) || 0;
|
||||
const end = parseInt(req.query._end) || 20;
|
||||
const order = req.query._order === 'DESC' ? -1 : 1;
|
||||
const sort = req.query._sort || '_id';
|
||||
const tag = req.query.tag || '';
|
||||
const where = { tags: { $elemMatch: { $regex: tag, $options: 'i' } } };
|
||||
|
||||
const kbsRaw = await Kb.find(where)
|
||||
.skip(start)
|
||||
.limit(end - start)
|
||||
.sort({ [sort]: order });
|
||||
|
||||
const kbs = [];
|
||||
|
||||
for (const kbRaw of kbsRaw) {
|
||||
const kb = kbRaw.toObject();
|
||||
|
||||
const orderedKb = {
|
||||
id: kb._id.toString(),
|
||||
userId: kb.userId,
|
||||
name: kb.name,
|
||||
tags: kb.tags,
|
||||
avatar: kb.avatar
|
||||
};
|
||||
|
||||
kbs.push(orderedKb);
|
||||
}
|
||||
const totalCount = await Kb.countDocuments(where);
|
||||
res.header('Access-Control-Expose-Headers', 'X-Total-Count');
|
||||
res.header('X-Total-Count', totalCount);
|
||||
res.json(kbs);
|
||||
} catch (err) {
|
||||
console.log(`Error fetching kbs: ${err}`);
|
||||
res.status(500).json({ error: 'Error fetching kbs', details: err.message });
|
||||
}
|
||||
});
|
||||
};
|
||||
134
admin/service/route/system.js
Normal file
@@ -0,0 +1,134 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { System } from '../schema.js';
|
||||
|
||||
const adminAuth = {
|
||||
username: process.env.ADMIN_USER,
|
||||
password: process.env.ADMIN_PASS
|
||||
};
|
||||
const authSecret = process.env.ADMIN_SECRET;
|
||||
|
||||
const postParent = () => {
|
||||
fetch(`${process.env.PARENT_URL}/api/system/updateEnv`, {
|
||||
headers: {
|
||||
rootkey: process.env.PARENT_ROOT_KEY
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useSystemRoute = (app) => {
|
||||
app.post('/api/login', (req, res) => {
|
||||
if (!adminAuth.username || !adminAuth.password) {
|
||||
res.status(401).end('Server not set env: ADMIN_USER, ADMIN_PASS');
|
||||
return;
|
||||
}
|
||||
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (username === adminAuth.username && password === adminAuth.password) {
|
||||
// 用户名和密码都正确,返回token
|
||||
const token = jwt.sign(
|
||||
{
|
||||
username,
|
||||
platform: 'admin'
|
||||
},
|
||||
authSecret,
|
||||
{
|
||||
expiresIn: '2h'
|
||||
}
|
||||
);
|
||||
|
||||
res.json({
|
||||
username,
|
||||
token: token,
|
||||
expiredAt: new Date().valueOf() + 2 * 60 * 60 * 1000
|
||||
});
|
||||
} else {
|
||||
res.status(401).end('username or password incorrect');
|
||||
}
|
||||
});
|
||||
app.get('/system', auth(), async (req, res) => {
|
||||
try {
|
||||
const data = await System.find();
|
||||
const totalCount = await System.countDocuments();
|
||||
|
||||
res.header('Access-Control-Expose-Headers', 'X-Total-Count');
|
||||
res.header('X-Total-Count', totalCount);
|
||||
res.json(
|
||||
data.map((item) => {
|
||||
const obj = item.toObject();
|
||||
return {
|
||||
...obj,
|
||||
id: obj._id
|
||||
};
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
res.status(500).json({ error: 'Error creating system env' });
|
||||
}
|
||||
});
|
||||
app.post('/system', auth(), async (req, res) => {
|
||||
try {
|
||||
await System.create({
|
||||
...req.body,
|
||||
sensitiveCheck: req.body.sensitiveCheck === 'true'
|
||||
});
|
||||
postParent();
|
||||
res.json({});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Error creating system env' });
|
||||
}
|
||||
});
|
||||
app.put('/system/:id', auth(), async (req, res) => {
|
||||
try {
|
||||
const _id = req.params.id;
|
||||
await System.findByIdAndUpdate(_id, {
|
||||
...req.body,
|
||||
sensitiveCheck: req.body.sensitiveCheck === 'true'
|
||||
});
|
||||
postParent();
|
||||
res.json({});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Error updating system env' });
|
||||
}
|
||||
});
|
||||
app.delete('/system/:id', auth(), async (req, res) => {
|
||||
try {
|
||||
const _id = req.params.id;
|
||||
await System.findByIdAndDelete(_id);
|
||||
|
||||
res.json({});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Error updating system env' });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const auth = () => {
|
||||
return (req, res, next) => {
|
||||
try {
|
||||
const authorization = req.headers.authorization;
|
||||
if (!authorization) {
|
||||
res.status(401).end('not found authorization in headers');
|
||||
return;
|
||||
}
|
||||
|
||||
const token = authorization.slice('Bearer '.length);
|
||||
|
||||
const payload = jwt.verify(token, authSecret);
|
||||
if (typeof payload === 'string') {
|
||||
res.status(401).end('payload type error');
|
||||
return;
|
||||
}
|
||||
if (payload.platform !== 'admin') {
|
||||
res.status(401).end('Payload invalid');
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (err) {
|
||||
res.status(401).end(String(err));
|
||||
}
|
||||
};
|
||||
};
|
||||
136
admin/service/route/user.js
Normal file
@@ -0,0 +1,136 @@
|
||||
import { User, Pay } from '../schema.js';
|
||||
import dayjs from 'dayjs';
|
||||
import { auth } from './system.js';
|
||||
import crypto from 'crypto';
|
||||
|
||||
// 加密
|
||||
const hashPassword = (psw) => {
|
||||
return crypto.createHash('sha256').update(psw).digest('hex');
|
||||
};
|
||||
|
||||
export const useUserRoute = (app) => {
|
||||
// 获取用户列表
|
||||
app.get('/users', auth(), async (req, res) => {
|
||||
try {
|
||||
const start = parseInt(req.query._start) || 0;
|
||||
const end = parseInt(req.query._end) || 20;
|
||||
const order = req.query._order === 'DESC' ? -1 : 1;
|
||||
const sort = req.query._sort || 'createTime';
|
||||
const username = req.query.username || '';
|
||||
const where = {
|
||||
username: { $regex: username, $options: 'i' }
|
||||
};
|
||||
|
||||
const usersRaw = await User.find(where)
|
||||
.skip(start)
|
||||
.limit(end - start)
|
||||
.sort({ [sort]: order });
|
||||
|
||||
const users = usersRaw.map((user) => {
|
||||
const obj = user.toObject();
|
||||
return {
|
||||
...obj,
|
||||
id: obj._id,
|
||||
createTime: dayjs(obj.createTime).format('YYYY/MM/DD HH:mm'),
|
||||
password: ''
|
||||
};
|
||||
});
|
||||
|
||||
const totalCount = await User.countDocuments(where);
|
||||
|
||||
res.header('Access-Control-Expose-Headers', 'X-Total-Count');
|
||||
res.header('X-Total-Count', totalCount);
|
||||
res.json(users);
|
||||
} catch (err) {
|
||||
console.log(`Error fetching users: ${err}`);
|
||||
res.status(500).json({ error: 'Error fetching users' });
|
||||
}
|
||||
});
|
||||
|
||||
// 创建用户
|
||||
app.post('/users', auth(), async (req, res) => {
|
||||
try {
|
||||
const { username, password, balance } = req.body;
|
||||
if (!username || !password || !balance) {
|
||||
return res.status(400).json({ error: 'Invalid user information' });
|
||||
}
|
||||
const existingUser = await User.findOne({ username });
|
||||
if (existingUser) {
|
||||
return res.status(400).json({ error: 'Username already exists' });
|
||||
}
|
||||
|
||||
const result = await User.create({
|
||||
username,
|
||||
password,
|
||||
balance
|
||||
});
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
console.log(`Error creating user: ${err}`);
|
||||
res.status(500).json({ error: 'Error creating user' });
|
||||
}
|
||||
});
|
||||
|
||||
// 修改用户信息
|
||||
app.put('/users/:id', auth(), async (req, res) => {
|
||||
try {
|
||||
const _id = req.params.id;
|
||||
|
||||
let { password, balance = 0 } = req.body;
|
||||
|
||||
const result = await User.findByIdAndUpdate(_id, {
|
||||
...(password && { password: hashPassword(hashPassword(password)) }),
|
||||
...(balance && { balance })
|
||||
});
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
console.log(`Error updating user: ${err}`);
|
||||
res.status(500).json({ error: 'Error updating user' });
|
||||
}
|
||||
});
|
||||
|
||||
// 新增: 获取 pays 列表
|
||||
app.get('/pays', auth(), async (req, res) => {
|
||||
try {
|
||||
const start = parseInt(req.query._start) || 0;
|
||||
const end = parseInt(req.query._end) || 20;
|
||||
const order = req.query._order === 'DESC' ? -1 : 1;
|
||||
const sort = req.query._sort || '_id';
|
||||
const userId = req.query.userId || '';
|
||||
const where = userId ? { userId: userId } : {};
|
||||
|
||||
const paysRaw = await Pay.find({
|
||||
...where
|
||||
})
|
||||
.skip(start)
|
||||
.limit(end - start)
|
||||
.sort({ [sort]: order });
|
||||
|
||||
const pays = [];
|
||||
|
||||
for (const payRaw of paysRaw) {
|
||||
const pay = payRaw.toObject();
|
||||
|
||||
const orderedPay = {
|
||||
id: pay._id.toString(),
|
||||
userId: pay.userId,
|
||||
price: pay.price,
|
||||
orderId: pay.orderId,
|
||||
status: pay.status,
|
||||
createTime: dayjs(pay.createTime).format('YYYY/MM/DD HH:mm')
|
||||
};
|
||||
|
||||
pays.push(orderedPay);
|
||||
}
|
||||
const totalCount = await Pay.countDocuments({
|
||||
...where
|
||||
});
|
||||
res.header('Access-Control-Expose-Headers', 'X-Total-Count');
|
||||
res.header('X-Total-Count', totalCount);
|
||||
res.json(pays);
|
||||
} catch (err) {
|
||||
console.log(`Error fetching pays: ${err}`);
|
||||
res.status(500).json({ error: 'Error fetching pays', details: err.message });
|
||||
}
|
||||
});
|
||||
};
|
||||
122
admin/service/schema.js
Normal file
@@ -0,0 +1,122 @@
|
||||
import mongoose from 'mongoose';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config({ path: '.env.local' });
|
||||
|
||||
const mongoUrl = process.env.MONGODB_URI;
|
||||
const mongoDBName = process.env.MONGODB_NAME;
|
||||
|
||||
if (!mongoUrl || !mongoDBName) {
|
||||
throw new Error('db error');
|
||||
}
|
||||
|
||||
mongoose
|
||||
.connect(mongoUrl, {
|
||||
dbName: mongoDBName,
|
||||
bufferCommands: true,
|
||||
maxPoolSize: 5,
|
||||
minPoolSize: 1,
|
||||
maxConnecting: 5
|
||||
})
|
||||
.then(() => console.log('Connected to MongoDB successfully!'))
|
||||
.catch((err) => console.log(`Error connecting to MongoDB: ${err}`));
|
||||
|
||||
const userSchema = new mongoose.Schema({
|
||||
_id: mongoose.Schema.Types.ObjectId,
|
||||
username: String,
|
||||
password: String,
|
||||
balance: Number,
|
||||
promotion: {
|
||||
rate: Number
|
||||
},
|
||||
openaiKey: String,
|
||||
avatar: String,
|
||||
createTime: Date
|
||||
});
|
||||
|
||||
// 新增: 定义 pays 模型
|
||||
const paySchema = new mongoose.Schema({
|
||||
_id: mongoose.Schema.Types.ObjectId,
|
||||
userId: mongoose.Schema.Types.ObjectId,
|
||||
price: Number,
|
||||
orderId: String,
|
||||
status: String,
|
||||
createTime: Date,
|
||||
__v: Number
|
||||
});
|
||||
|
||||
// 新增: 定义 kb 模型
|
||||
const kbSchema = new mongoose.Schema({
|
||||
_id: mongoose.Schema.Types.ObjectId,
|
||||
userId: mongoose.Schema.Types.ObjectId,
|
||||
avatar: String,
|
||||
name: String,
|
||||
tags: [String],
|
||||
updateTime: Date,
|
||||
__v: Number
|
||||
});
|
||||
|
||||
const modelSchema = new mongoose.Schema({
|
||||
userId: mongoose.Schema.Types.ObjectId,
|
||||
name: String,
|
||||
avatar: String,
|
||||
status: String,
|
||||
chat: {
|
||||
relatedKbs: [mongoose.Schema.Types.ObjectId],
|
||||
searchMode: String,
|
||||
systemPrompt: String,
|
||||
temperature: Number,
|
||||
chatModel: String
|
||||
},
|
||||
share: {
|
||||
isShare: Boolean,
|
||||
isShareDetail: Boolean,
|
||||
intro: String,
|
||||
collection: Number
|
||||
},
|
||||
security: {
|
||||
domain: [String],
|
||||
contextMaxLen: Number,
|
||||
contentMaxLen: Number,
|
||||
expiredTime: Number,
|
||||
maxLoadAmount: Number
|
||||
},
|
||||
updateTime: Date
|
||||
});
|
||||
|
||||
const SystemSchema = new mongoose.Schema({
|
||||
openAIKeys: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
openAITrainingKeys: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
gpt4Key: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
vectorMaxProcess: {
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
qaMaxProcess: {
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
pgIvfflatProbe: {
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
sensitiveCheck: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
export const Model = mongoose.models['model'] || mongoose.model('model', modelSchema);
|
||||
export const Kb = mongoose.models['kb'] || mongoose.model('kb', kbSchema);
|
||||
export const User = mongoose.models['user'] || mongoose.model('user', userSchema);
|
||||
export const Pay = mongoose.models['pay'] || mongoose.model('pay', paySchema);
|
||||
export const System = mongoose.models['system'] || mongoose.model('system', SystemSchema);
|
||||
105
admin/src/App.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import {
|
||||
createTextField,
|
||||
jsonServerProvider,
|
||||
ListTable,
|
||||
Resource,
|
||||
Tushan,
|
||||
fetchJSON
|
||||
} from 'tushan';
|
||||
import { authProvider } from './auth';
|
||||
import { userFields, payFields, kbFields, ModelFields, SystemFields } from './fields';
|
||||
import { Dashboard } from './Dashboard';
|
||||
|
||||
const authStorageKey = 'tushan:auth';
|
||||
|
||||
const httpClient: typeof fetchJSON = (url, options = {}) => {
|
||||
try {
|
||||
if (!options.headers) {
|
||||
options.headers = new Headers({ Accept: 'application/json' });
|
||||
}
|
||||
const { token } = JSON.parse(window.localStorage.getItem(authStorageKey) ?? '{}');
|
||||
(options.headers as Headers).set('Authorization', `Bearer ${token}`);
|
||||
|
||||
return fetchJSON(url, options);
|
||||
} catch (err) {
|
||||
return Promise.reject();
|
||||
}
|
||||
};
|
||||
|
||||
const dataProvider = jsonServerProvider(import.meta.env.VITE_PUBLIC_SERVER_URL, httpClient);
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Tushan
|
||||
basename="/"
|
||||
header={'FastGpt-Admin'}
|
||||
dataProvider={dataProvider}
|
||||
authProvider={authProvider}
|
||||
dashboard={<Dashboard />}
|
||||
>
|
||||
<Resource
|
||||
name="users"
|
||||
label="用户信息"
|
||||
list={
|
||||
<ListTable
|
||||
filter={[
|
||||
createTextField('username', {
|
||||
label: 'username'
|
||||
})
|
||||
]}
|
||||
fields={userFields}
|
||||
action={{ detail: true, edit: true }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<Resource
|
||||
name="pays"
|
||||
label="支付记录"
|
||||
list={
|
||||
<ListTable
|
||||
filter={[
|
||||
createTextField('userId', {
|
||||
label: 'userId'
|
||||
})
|
||||
]}
|
||||
fields={payFields}
|
||||
action={{ detail: true }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Resource
|
||||
name="kbs"
|
||||
label="知识库"
|
||||
list={
|
||||
<ListTable
|
||||
filter={[
|
||||
createTextField('tag', {
|
||||
label: 'tag'
|
||||
})
|
||||
]}
|
||||
fields={kbFields}
|
||||
action={{ detail: true }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Resource
|
||||
name="models"
|
||||
label="应用"
|
||||
list={<ListTable fields={ModelFields} action={{ detail: true }} />}
|
||||
/>
|
||||
<Resource
|
||||
name="system"
|
||||
label="系统"
|
||||
list={
|
||||
<ListTable
|
||||
fields={SystemFields}
|
||||
action={{ detail: true, edit: true, create: true, delete: true }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Tushan>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
143
admin/src/Dashboard.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import { Card, Link, Space, Grid, Divider, Typography } from '@arco-design/web-react';
|
||||
import { IconApps, IconUser, IconUserGroup } from 'tushan/icon';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const authStorageKey = 'tushan:auth';
|
||||
|
||||
export const Dashboard: React.FC = React.memo(() => {
|
||||
const [userCount, setUserCount] = useState(0); //用户数量
|
||||
const [kbCount, setkbCount] = useState(0);
|
||||
const [modelCount, setmodelCount] = useState(0);
|
||||
useEffect(() => {
|
||||
const fetchCounts = async () => {
|
||||
const baseUrl = import.meta.env.VITE_PUBLIC_SERVER_URL;
|
||||
const { token } = JSON.parse(window.localStorage.getItem(authStorageKey) ?? '{}');
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
};
|
||||
const userResponse = await fetch(`${baseUrl}/users?_end=1`, {
|
||||
headers
|
||||
});
|
||||
const kbResponse = await fetch(`${baseUrl}/kbs?_end=1`, {
|
||||
headers
|
||||
});
|
||||
const modelResponse = await fetch(`${baseUrl}/models?_end=1`, {
|
||||
headers
|
||||
});
|
||||
|
||||
const userTotalCount = userResponse.headers.get('X-Total-Count');
|
||||
const kbTotalCount = kbResponse.headers.get('X-Total-Count');
|
||||
const modelTotalCount = modelResponse.headers.get('X-Total-Count');
|
||||
console.log(userTotalCount);
|
||||
|
||||
if (userTotalCount) {
|
||||
setUserCount(Number(userTotalCount));
|
||||
}
|
||||
if (kbTotalCount) {
|
||||
setkbCount(Number(kbTotalCount));
|
||||
}
|
||||
if (modelTotalCount) {
|
||||
setmodelCount(Number(modelTotalCount));
|
||||
}
|
||||
};
|
||||
|
||||
fetchCounts();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Card bordered={false}>
|
||||
<Typography.Title heading={5}>FastGpt Admin</Typography.Title>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Grid.Row justify="center">
|
||||
<Grid.Col flex={1} style={{ paddingLeft: '1rem' }}>
|
||||
{/* 把 userCount 传递给 DataItem 组件 */}
|
||||
<DataItem icon={<IconUser />} title={'用户'} count={userCount} />
|
||||
</Grid.Col>
|
||||
|
||||
<Divider type="vertical" style={{ height: 40 }} />
|
||||
|
||||
<Grid.Col flex={1} style={{ paddingLeft: '1rem' }}>
|
||||
<DataItem icon={<IconUserGroup />} title={'知识库'} count={kbCount} />
|
||||
</Grid.Col>
|
||||
|
||||
<Divider type="vertical" style={{ height: 40 }} />
|
||||
|
||||
<Grid.Col flex={1} style={{ paddingLeft: '1rem' }}>
|
||||
<DataItem icon={<IconApps />} title={'AI模型'} count={modelCount} />
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
|
||||
<Divider />
|
||||
</Card>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
Dashboard.displayName = 'Dashboard';
|
||||
|
||||
const DashboardItem: React.FC<
|
||||
React.PropsWithChildren<{
|
||||
title: string;
|
||||
href?: string;
|
||||
}>
|
||||
> = React.memo((props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={props.title}
|
||||
extra={
|
||||
props.href && (
|
||||
<Link target="_blank" href={props.href}>
|
||||
{t('tushan.dashboard.more')}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
bordered={false}
|
||||
style={{ overflow: 'hidden' }}
|
||||
>
|
||||
{props.children}
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
DashboardItem.displayName = 'DashboardItem';
|
||||
|
||||
const DataItem: React.FC<{
|
||||
icon: React.ReactElement;
|
||||
title: string;
|
||||
count: number;
|
||||
}> = React.memo((props) => {
|
||||
return (
|
||||
<Space>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 20,
|
||||
padding: '0.5rem',
|
||||
borderRadius: '9999px',
|
||||
border: '1px solid #ccc',
|
||||
width: 24,
|
||||
height: 24,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
{props.icon}
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontWeight: 700 }}>{props.title}</div>
|
||||
<div>{props.count}</div>
|
||||
</div>
|
||||
</Space>
|
||||
);
|
||||
});
|
||||
DataItem.displayName = 'DataItem';
|
||||
5
admin/src/auth.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { createAuthProvider, type AuthProvider } from 'tushan';
|
||||
|
||||
export const authProvider: AuthProvider = createAuthProvider({
|
||||
loginUrl: `${import.meta.env.VITE_PUBLIC_SERVER_URL}/api/login`
|
||||
});
|
||||
50
admin/src/fields.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { createTextField, createNumberField } from 'tushan';
|
||||
|
||||
export const userFields = [
|
||||
createTextField('id', { label: 'ID' }),
|
||||
createTextField('username', { label: '用户名' }),
|
||||
createNumberField('balance', { label: '余额', list: { sort: true } }),
|
||||
createTextField('createTime', { label: 'Create Time', list: { sort: true } }),
|
||||
createTextField('password', { label: '密码', list: { hidden: true } })
|
||||
];
|
||||
|
||||
export const payFields = [
|
||||
createTextField('id', { label: 'ID' }),
|
||||
createTextField('userId', { label: '用户Id' }),
|
||||
createNumberField('price', { label: '支付金额' }),
|
||||
createTextField('orderId', { label: 'orderId' }),
|
||||
createTextField('status', { label: '状态' }),
|
||||
createTextField('createTime', { label: 'Create Time', list: { sort: true } })
|
||||
];
|
||||
|
||||
export const kbFields = [
|
||||
createTextField('id', { label: 'ID' }),
|
||||
createTextField('userId', { label: '所属用户' }),
|
||||
createTextField('name', { label: '知识库' }),
|
||||
createTextField('tags', { label: 'Tags' })
|
||||
];
|
||||
|
||||
export const ModelFields = [
|
||||
createTextField('id', { label: 'ID' }),
|
||||
createTextField('userId', { label: '所属用户' }),
|
||||
createTextField('name', { label: '名字' }),
|
||||
createTextField('relatedKbs', { label: '引用的知识库' }),
|
||||
createTextField('searchMode', { label: '搜索模式' }),
|
||||
createTextField('systemPrompt', {
|
||||
label: '提示词',
|
||||
list: {
|
||||
width: 400
|
||||
}
|
||||
}),
|
||||
createTextField('temperature', { label: '温度' })
|
||||
];
|
||||
|
||||
export const SystemFields = [
|
||||
createTextField('openAIKeys', { label: 'openAIKeys,逗号隔开' }),
|
||||
createTextField('openAITrainingKeys', { label: 'openAITrainingKeys' }),
|
||||
createTextField('gpt4Key', { label: 'gpt4Key' }),
|
||||
createTextField('vectorMaxProcess', { label: '向量最大进程' }),
|
||||
createTextField('qaMaxProcess', { label: 'qa最大进程' }),
|
||||
createTextField('pgIvfflatProbe', { label: 'pg 探针数量' }),
|
||||
createTextField('sensitiveCheck', { label: '敏感词校验(true,false)' })
|
||||
];
|
||||
5
admin/src/main.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<App />);
|
||||
1
admin/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
21
admin/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
9
admin/tsconfig.node.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
7
admin/vite.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
});
|
||||
31
client/.gitignore
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
# dependencies
|
||||
node_modules/
|
||||
# next.js
|
||||
.next/
|
||||
out/
|
||||
# production
|
||||
build/
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
platform.json
|
||||
testApi/
|
||||
local/
|
||||
.husky/
|
||||
@@ -6,6 +6,7 @@ WORKDIR /app
|
||||
|
||||
# Install dependencies based on the preferred package manager
|
||||
COPY package.json pnpm-lock.yaml* ./
|
||||
RUN pnpm config set registry https://registry.npmmirror.com/
|
||||
RUN \
|
||||
[ -f pnpm-lock.yaml ] && pnpm install || \
|
||||
(echo "Lockfile not found." && exit 1)
|
||||
5
client/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
84
client/package.json
Normal file
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"name": "fastgpt",
|
||||
"version": "3.7",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@alicloud/dysmsapi20170525": "^2.0.23",
|
||||
"@alicloud/openapi-client": "^0.4.5",
|
||||
"@alicloud/tea-util": "^1.4.5",
|
||||
"@chakra-ui/icons": "^2.0.17",
|
||||
"@chakra-ui/react": "^2.5.1",
|
||||
"@chakra-ui/system": "^2.5.5",
|
||||
"@dqbd/tiktoken": "^1.0.6",
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@emotion/styled": "^11.10.6",
|
||||
"@next/font": "13.1.6",
|
||||
"@tanstack/react-query": "^4.24.10",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"axios": "^1.3.3",
|
||||
"cookie": "^0.5.0",
|
||||
"crypto": "^1.0.1",
|
||||
"dayjs": "^1.11.7",
|
||||
"eventsource-parser": "^0.1.0",
|
||||
"formidable": "^2.1.1",
|
||||
"framer-motion": "^9.0.6",
|
||||
"graphemer": "^1.4.0",
|
||||
"hyperdown": "^2.4.29",
|
||||
"immer": "^9.0.19",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mammoth": "^1.5.1",
|
||||
"mermaid": "^8.13.5",
|
||||
"mongoose": "^6.10.0",
|
||||
"nanoid": "^4.0.1",
|
||||
"next": "13.1.6",
|
||||
"nextjs-cors": "^2.1.2",
|
||||
"nodemailer": "^6.9.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"openai": "^3.2.1",
|
||||
"papaparse": "^5.4.1",
|
||||
"pg": "^8.10.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.43.1",
|
||||
"react-markdown": "^8.0.5",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"rehype-katex": "^6.0.2",
|
||||
"remark-breaks": "^3.0.3",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-math": "^5.1.1",
|
||||
"request-ip": "^3.3.0",
|
||||
"sass": "^1.58.3",
|
||||
"tunnel": "^0.0.6",
|
||||
"wxpay-v3": "^3.0.2",
|
||||
"zustand": "^4.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/webpack": "^6.5.1",
|
||||
"@types/cookie": "^0.5.1",
|
||||
"@types/formidable": "^2.0.5",
|
||||
"@types/jsonwebtoken": "^9.0.1",
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/node": "18.14.0",
|
||||
"@types/nodemailer": "^6.4.7",
|
||||
"@types/papaparse": "^5.3.7",
|
||||
"@types/pg": "^8.6.6",
|
||||
"@types/react": "18.0.28",
|
||||
"@types/react-dom": "18.0.11",
|
||||
"@types/react-syntax-highlighter": "^15.5.6",
|
||||
"@types/request-ip": "^0.0.37",
|
||||
"@types/tunnel": "^0.0.3",
|
||||
"eslint": "8.34.0",
|
||||
"eslint-config-next": "13.1.6",
|
||||
"typescript": "4.9.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
12469
client/pnpm-lock.yaml
generated
Normal file
@@ -11,10 +11,10 @@
|
||||
移动端:点击对话头像,可以选择复制或删除该条内容。
|
||||
|
||||
**价格表**
|
||||
如果使用了自己的 Api Key,不会计费。可以在账号页,看到详细账单。
|
||||
如果使用了自己的 Api Key,网页上 openai 模型聊天不会计费。可以在账号页,看到详细账单。
|
||||
| 计费项 | 价格: 元/ 1K tokens(包含上下文)|
|
||||
| --- | --- |
|
||||
| 知识库 - 索引 | 免费 |
|
||||
| 知识库 - 索引 | 0.001 |
|
||||
| chatgpt - 对话 | 0.025 |
|
||||
| gpt4 - 对话 | 0.5 |
|
||||
| 文件拆分 | 0.025 |
|
||||
@@ -15,10 +15,10 @@ FastGpt 项目完全开源,可随意私有化部署,去除平台风险忧虑
|
||||
|
||||
### 价格表
|
||||
|
||||
如果使用了自己的 Api Key,不会计费。可以在账号页,看到详细账单。
|
||||
如果使用了自己的 Api Key,网页上 openai 模型聊天不会计费。可以在账号页,看到详细账单。
|
||||
| 计费项 | 价格: 元/ 1K tokens(包含上下文)|
|
||||
| --- | --- |
|
||||
| 知识库 - 索引 | 免费 |
|
||||
| 知识库 - 索引 | 0.001 |
|
||||
| chatgpt - 对话 | 0.025 |
|
||||
| gpt4 - 对话 | 0.5 |
|
||||
| 文件拆分 | 0.025 |
|
||||
4
client/public/docs/versionIntro.md
Normal file
@@ -0,0 +1,4 @@
|
||||
### Fast GPT V3.8.3
|
||||
|
||||
1. 新增 - mermaid 导图兼容,可以在应用市场 'mermaid 导图' 进行体验。
|
||||
2. 优化 - 部分 UI 和账号页。
|
||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 201 KiB After Width: | Height: | Size: 201 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
BIN
client/public/imgs/loading.gif
Normal file
|
After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
67
client/src/api/fetch.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { GUIDE_PROMPT_HEADER, NEW_CHATID_HEADER, QUOTE_LEN_HEADER } from '@/constants/chat';
|
||||
|
||||
interface StreamFetchProps {
|
||||
url: string;
|
||||
data: any;
|
||||
onMessage: (text: string) => void;
|
||||
abortSignal: AbortController;
|
||||
}
|
||||
export const streamFetch = ({ url, data, onMessage, abortSignal }: StreamFetchProps) =>
|
||||
new Promise<{
|
||||
responseText: string;
|
||||
newChatId: string;
|
||||
systemPrompt: string;
|
||||
quoteLen: number;
|
||||
}>(async (resolve, reject) => {
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
signal: abortSignal.signal
|
||||
});
|
||||
const reader = res.body?.getReader();
|
||||
if (!reader) return;
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
const newChatId = decodeURIComponent(res.headers.get(NEW_CHATID_HEADER) || '');
|
||||
const systemPrompt = decodeURIComponent(res.headers.get(GUIDE_PROMPT_HEADER) || '').trim();
|
||||
const quoteLen = res.headers.get(QUOTE_LEN_HEADER)
|
||||
? Number(res.headers.get(QUOTE_LEN_HEADER))
|
||||
: 0;
|
||||
|
||||
let responseText = '';
|
||||
|
||||
const read = async () => {
|
||||
try {
|
||||
const { done, value } = await reader?.read();
|
||||
if (done) {
|
||||
if (res.status === 200) {
|
||||
resolve({ responseText, newChatId, quoteLen, systemPrompt });
|
||||
} else {
|
||||
const parseError = JSON.parse(responseText);
|
||||
reject(parseError?.message || '请求异常');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
const text = decoder.decode(value);
|
||||
responseText += text;
|
||||
onMessage(text);
|
||||
read();
|
||||
} catch (err: any) {
|
||||
if (err?.message === 'The user aborted a request.') {
|
||||
return resolve({ responseText, newChatId, quoteLen, systemPrompt });
|
||||
}
|
||||
reject(typeof err === 'string' ? err : err?.message || '请求异常');
|
||||
}
|
||||
};
|
||||
read();
|
||||
} catch (err: any) {
|
||||
console.log(err, '====');
|
||||
reject(typeof err === 'string' ? err : err?.message || '请求异常');
|
||||
}
|
||||
});
|
||||
@@ -7,7 +7,12 @@ import {
|
||||
Response as PushDateResponse
|
||||
} from '@/pages/api/openapi/kb/pushData';
|
||||
|
||||
export type KbUpdateParams = { id: string; name: string; tags: string; avatar: string };
|
||||
export type KbUpdateParams = {
|
||||
id: string;
|
||||
name: string;
|
||||
tags: string;
|
||||
avatar: string;
|
||||
};
|
||||
|
||||
/* knowledge base */
|
||||
export const getKbList = () => GET<KbItemType[]>(`/plugins/kb/list`);
|
||||
@@ -4,7 +4,7 @@ import { ResLogin, PromotionRecordType } from './response/user';
|
||||
import { UserAuthTypeEnum } from '@/constants/common';
|
||||
import { UserBillType, UserType, UserUpdateParams } from '@/types/user';
|
||||
import type { PagingData, RequestPaging } from '@/types';
|
||||
import { PaySchema } from '@/types/mongoSchema';
|
||||
import { informSchema, PaySchema } from '@/types/mongoSchema';
|
||||
|
||||
export const sendAuthCode = (data: {
|
||||
username: string;
|
||||
@@ -81,3 +81,9 @@ export const checkPayResult = (payId: string) => GET<number>(`/user/checkPayResu
|
||||
/* promotion records */
|
||||
export const getPromotionRecords = (data: RequestPaging) =>
|
||||
GET<PromotionRecordType>(`/user/promotion/getPromotions?${Obj2Query(data)}`);
|
||||
|
||||
export const getInforms = (data: RequestPaging) =>
|
||||
POST<PagingData<informSchema>>(`/user/inform/list`, data);
|
||||
|
||||
export const getUnreadCount = () => GET<number>(`/user/inform/countUnread`);
|
||||
export const readInform = (id: string) => GET(`/user/inform/read`, { id });
|
||||
42
client/src/components/Badge/index.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
|
||||
const Badge = ({
|
||||
children,
|
||||
isDot = false,
|
||||
max = 99,
|
||||
count = 0
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
isDot?: boolean;
|
||||
max?: number;
|
||||
count?: number;
|
||||
}) => {
|
||||
return (
|
||||
<Box position={'relative'}>
|
||||
{children}
|
||||
{count > 0 && (
|
||||
<Box position={'absolute'} right={0} top={0} transform={'translate(70%,-50%)'}>
|
||||
{isDot ? (
|
||||
<Box w={'5px'} h={'5px'} bg={'myRead.600'} borderRadius={'20px'}></Box>
|
||||
) : (
|
||||
<Box
|
||||
color={'white'}
|
||||
bg={'myRead.600'}
|
||||
lineHeight={0.9}
|
||||
borderRadius={'100px'}
|
||||
px={'4px'}
|
||||
py={'2px'}
|
||||
fontSize={'12px'}
|
||||
border={'1px solid white'}
|
||||
>
|
||||
{count > max ? `${max}+` : count}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Badge;
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 866 B After Width: | Height: | Size: 866 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 878 B After Width: | Height: | Size: 878 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 976 B After Width: | Height: | Size: 976 B |
|
Before Width: | Height: | Size: 810 B After Width: | Height: | Size: 810 B |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
1
client/src/components/Icon/icons/export.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1686468581713" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2951" xmlns:xlink="http://www.w3.org/1999/xlink" ><path d="M512 640.64a42.666667 42.666667 0 0 0 42.666667-42.666667v-341.333333h130.986666a21.333333 21.333333 0 0 0 14.250667-5.461333l2.688-2.901334a21.333333 21.333333 0 0 0-4.010667-29.909333l-165.717333-126.464a32 32 0 0 0-38.912 0.042667L329.472 218.453333a21.333333 21.333333 0 0 0 12.970667 38.229334H469.333333v341.333333a42.666667 42.666667 0 0 0 42.666667 42.666667z m229.674667-298.368a42.666667 42.666667 0 0 0 4.992 85.034667H853.333333v426.666666H170.666667v-426.666666h106.666666a42.666667 42.666667 0 0 0 0-85.333334H170.666667a85.333333 85.333333 0 0 0-85.333334 85.333334v426.666666a85.333333 85.333333 0 0 0 85.333334 85.333334h682.666666a85.333333 85.333333 0 0 0 85.333334-85.333334v-426.666666a85.333333 85.333333 0 0 0-85.333334-85.333334h-106.666666z" fill="#000000" p-id="2952"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1013 B After Width: | Height: | Size: 1013 B |
1
client/src/components/Icon/icons/inform.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1686042262954" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3245" xmlns:xlink="http://www.w3.org/1999/xlink" ><path d="M510.1 928h5.5c52.6-0.7 96.7-38.4 103.3-88.5H406.8c6.6 50.1 50.7 87.9 103.3 88.5zM771.7 598.5V410.9c0.6-105.3-70.9-197-172.2-220.8v-4.5c0.8-31.7-15.5-61.4-42.5-77.6-27.1-16.1-60.6-16.1-87.7 0s-43.3 45.8-42.5 77.6v4.5C325.2 213.7 253.4 305.5 254 410.9v187.6c-51.9 41.3-83.2 103.5-85.9 170.2h689.5c-2.6-66.7-34-128.9-85.9-170.2z" p-id="3246"></path></svg>
|
||||
|
After Width: | Height: | Size: 663 B |
|
Before Width: | Height: | Size: 694 B After Width: | Height: | Size: 694 B |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 823 B After Width: | Height: | Size: 823 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
@@ -8,7 +8,6 @@ const map = {
|
||||
chatSend: require('./icons/chatSend.svg').default,
|
||||
develop: require('./icons/develop.svg').default,
|
||||
user: require('./icons/user.svg').default,
|
||||
promotion: require('./icons/promotion.svg').default,
|
||||
delete: require('./icons/delete.svg').default,
|
||||
withdraw: require('./icons/withdraw.svg').default,
|
||||
stop: require('./icons/stop.svg').default,
|
||||
@@ -29,7 +28,9 @@ const map = {
|
||||
kb: require('./icons/kb.svg').default,
|
||||
appStore: require('./icons/appStore.svg').default,
|
||||
menu: require('./icons/menu.svg').default,
|
||||
edit: require('./icons/edit.svg').default
|
||||
edit: require('./icons/edit.svg').default,
|
||||
inform: require('./icons/inform.svg').default,
|
||||
export: require('./icons/export.svg').default
|
||||
};
|
||||
|
||||
export type IconName = keyof typeof map;
|
||||