Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84daf85393 | ||
|
|
6c62d80a4c | ||
|
|
ff2043c0fb | ||
|
|
ee9afa310a | ||
|
|
2b93ae2d00 | ||
|
|
00c93a63cd | ||
|
|
61447c60ac | ||
|
|
df2fda6176 | ||
|
|
bc2504832f | ||
|
|
33ffd9d7dd | ||
|
|
80578a08c8 | ||
|
|
2463e11cb9 | ||
|
|
4cbe4ebdc3 | ||
|
|
bb36e637e0 | ||
|
|
6f9e929298 | ||
|
|
bf1592d2c6 | ||
|
|
c6259fca78 | ||
|
|
cf3eb3b7b5 | ||
|
|
7c52cec0ea | ||
|
|
7c159d8aba | ||
|
|
07f8e18c10 | ||
|
|
e4aeee7be3 | ||
|
|
8036ed6143 | ||
|
|
85e6a0f38d | ||
|
|
dab70378bb | ||
|
|
0a0febd2e6 | ||
|
|
391332c8dd | ||
|
|
89e7c1abca | ||
|
|
fc3c360985 | ||
|
|
006ba3b877 | ||
|
|
5a534aa630 | ||
|
|
98e3c0a41f | ||
|
|
99e47849f5 | ||
|
|
ca4cd8af9d | ||
|
|
36a0ea7e43 | ||
|
|
71dd7f3e6c | ||
|
|
6ac7119edf | ||
|
|
daf1148bb1 | ||
|
|
82b05b3d94 | ||
|
|
9ab5cef516 | ||
|
|
1ac3edccab | ||
|
|
d3959a918c | ||
|
|
623018f408 | ||
|
|
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
|
||||
17
README.md
@@ -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
|
||||
@@ -36,11 +37,17 @@ Fast GPT 允许你使用自己的 openai API KEY 来快速的调用 openai 接
|
||||
## 👀 其他
|
||||
|
||||
- [FastGpt 常见问题](https://kjqvjse66l.feishu.cn/docx/HtrgdT0pkonP4kxGx8qcu6XDnGh)
|
||||
- [docker 部署教程](https://www.bilibili.com/video/BV1jo4y147fT/)
|
||||
- [公众号接入](https://www.bilibili.com/video/BV1xh4y1t7fy/)
|
||||
- [FastGpt + Laf 最佳实践,将知识库装入公众号,点击去 Laf 公众号体验效果](https://b4jky7-fastgpt.oss.laf.run/lafercode.png)
|
||||
- [FastGpt V3.4 更新集合](https://www.bilibili.com/video/BV1Lo4y147Qh/?vd_source=92041a1a395f852f9d89158eaa3f61b4)
|
||||
- [FastGpt 知识库演示](https://www.bilibili.com/video/BV1Wo4y1p7i1/)
|
||||
|
||||
## Powered by
|
||||
|
||||
- [TuShan 5 分钟搭建后台管理系统](https://github.com/msgbyte/tushan)
|
||||
- [Laf 3 分钟快速接入三方应用](https://github.com/labring/laf)
|
||||
- [Sealos 快速部署集群应用](https://github.com/labring/sealos)
|
||||
|
||||
## 🌟 Star History
|
||||
|
||||
[](https://star-history.com/#c121914yu/FastGPT&Date)
|
||||
|
||||
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>
|
||||
42
admin/package.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"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",
|
||||
"nodemon": "^2.0.22",
|
||||
"react": "^18.2.0",
|
||||
"react-admin": "^4.11.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^12.3.1",
|
||||
"tushan": "^0.2.23"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
5659
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 |
30
admin/server.js
Normal file
@@ -0,0 +1,30 @@
|
||||
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);
|
||||
|
||||
app.get('/*', (req, res) => {
|
||||
res.sendFile(new URL('dist/index.html', import.meta.url).pathname);
|
||||
});
|
||||
|
||||
app.use((err, req, res, next) => {
|
||||
res.sendFile(new URL('dist/index.html', import.meta.url).pathname);
|
||||
});
|
||||
|
||||
|
||||
const PORT = process.env.PORT || 3001;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server is running on port ${PORT}`);
|
||||
});
|
||||
87
admin/service/route/app.js
Normal file
@@ -0,0 +1,87 @@
|
||||
import { 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;
|
||||
const name = req.query.name || '';
|
||||
const id = req.query.id || '';
|
||||
|
||||
const where = {
|
||||
...(name && { name: { $regex: name, $options: 'i' } }),
|
||||
...(id && { _id: id })
|
||||
};
|
||||
|
||||
const modelsRaw = await Model.find(where)
|
||||
.skip(start)
|
||||
.limit(end - start)
|
||||
.sort({ [sort]: order, 'share.isShare': -1, 'share.collection': -1 });
|
||||
|
||||
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,
|
||||
intro: model.intro,
|
||||
model: model.chat?.chatModel,
|
||||
relatedKbs: kbNames, // 将relatedKbs的id转换为相应的Kb名称
|
||||
systemPrompt: model.chat?.systemPrompt || '',
|
||||
temperature: model.chat?.temperature || 0,
|
||||
'share.topNum': model.share?.topNum || 0,
|
||||
'share.isShare': model.share?.isShare || false,
|
||||
'share.collection': model.share?.collection || 0
|
||||
};
|
||||
|
||||
models.push(orderedModel);
|
||||
}
|
||||
const totalCount = await Model.countDocuments(where);
|
||||
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 });
|
||||
}
|
||||
});
|
||||
|
||||
// 修改 app 信息
|
||||
app.put('/models/:id', auth(), async (req, res) => {
|
||||
try {
|
||||
const _id = req.params.id;
|
||||
|
||||
let {
|
||||
share: { isShare, topNum },
|
||||
intro
|
||||
} = req.body;
|
||||
|
||||
await Model.findByIdAndUpdate(_id, {
|
||||
$set: {
|
||||
intro: intro,
|
||||
'share.topNum': Number(topNum),
|
||||
'share.isShare': isShare === 'true' || isShare === true
|
||||
}
|
||||
});
|
||||
|
||||
res.json({});
|
||||
} catch (err) {
|
||||
console.log(`Error updating user: ${err}`);
|
||||
res.status(500).json({ error: 'Error updating user' });
|
||||
}
|
||||
});
|
||||
};
|
||||
58
admin/service/route/kb.js
Normal file
@@ -0,0 +1,58 @@
|
||||
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 name = req.query.name || '';
|
||||
|
||||
const where = {
|
||||
...(name
|
||||
? {
|
||||
name: { $regex: name, $options: 'i' }
|
||||
}
|
||||
: {}),
|
||||
...(tag
|
||||
? {
|
||||
tags: { $elemMatch: { $regex: tag, $options: 'i' } }
|
||||
}
|
||||
: {})
|
||||
};
|
||||
console.log(where);
|
||||
|
||||
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 });
|
||||
}
|
||||
});
|
||||
};
|
||||
133
admin/service/route/system.js
Normal file
@@ -0,0 +1,133 @@
|
||||
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) {
|
||||
return next(new Error("unAuthorization"))
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
};
|
||||
};
|
||||
182
admin/service/route/user.js
Normal file
@@ -0,0 +1,182 @@
|
||||
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) => {
|
||||
// 统计近 30 天注册用户数量
|
||||
app.get('/users/data', auth(), async (req, res) => {
|
||||
try {
|
||||
const day = 60;
|
||||
let startCount = await User.countDocuments({
|
||||
createTime: { $lt: new Date(Date.now() - day * 24 * 60 * 60 * 1000) }
|
||||
});
|
||||
const usersRaw = await User.aggregate([
|
||||
{ $match: { createTime: { $gte: new Date(Date.now() - day * 24 * 60 * 60 * 1000) } } },
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
year: { $year: '$createTime' },
|
||||
month: { $month: '$createTime' },
|
||||
day: { $dayOfMonth: '$createTime' }
|
||||
},
|
||||
count: { $sum: 1 }
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
date: { $dateFromParts: { year: '$_id.year', month: '$_id.month', day: '$_id.day' } },
|
||||
count: 1
|
||||
}
|
||||
},
|
||||
{ $sort: { date: 1 } }
|
||||
]);
|
||||
|
||||
const countResult = usersRaw.map((item) => {
|
||||
const increaseRate = `${((item.count / startCount) * 100).toFixed(2)}%`;
|
||||
startCount += item.count;
|
||||
return {
|
||||
date: item.date,
|
||||
count: startCount,
|
||||
increase: item.count,
|
||||
increaseRate
|
||||
};
|
||||
});
|
||||
|
||||
res.json(countResult);
|
||||
} catch (err) {
|
||||
console.log(`Error fetching users: ${err}`);
|
||||
res.status(500).json({ error: 'Error fetching users' });
|
||||
}
|
||||
});
|
||||
// 获取用户列表
|
||||
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 });
|
||||
}
|
||||
});
|
||||
};
|
||||
123
admin/service/schema.js
Normal file
@@ -0,0 +1,123 @@
|
||||
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,
|
||||
intro: String,
|
||||
chat: {
|
||||
relatedKbs: [mongoose.Schema.Types.ObjectId],
|
||||
systemPrompt: String,
|
||||
temperature: Number,
|
||||
chatModel: String
|
||||
},
|
||||
share: {
|
||||
topNum: Number,
|
||||
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);
|
||||
126
admin/src/App.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
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';
|
||||
import { IconUser, IconApps, IconBook, IconStamp } from 'tushan/icon';
|
||||
|
||||
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="用户信息"
|
||||
icon={<IconUser />}
|
||||
list={
|
||||
<ListTable
|
||||
filter={[
|
||||
createTextField('username', {
|
||||
label: 'username'
|
||||
})
|
||||
]}
|
||||
fields={userFields}
|
||||
action={{ detail: true, edit: true }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Resource
|
||||
name="models"
|
||||
icon={<IconApps />}
|
||||
label="应用"
|
||||
list={
|
||||
<ListTable
|
||||
filter={[
|
||||
createTextField('id', {
|
||||
label: 'id'
|
||||
}),
|
||||
createTextField('name', {
|
||||
label: 'name'
|
||||
})
|
||||
]}
|
||||
fields={ModelFields}
|
||||
action={{ detail: true, edit: true }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Resource
|
||||
name="pays"
|
||||
label="支付记录"
|
||||
icon={<IconStamp />}
|
||||
list={
|
||||
<ListTable
|
||||
filter={[
|
||||
createTextField('userId', {
|
||||
label: 'userId'
|
||||
})
|
||||
]}
|
||||
fields={payFields}
|
||||
action={{ detail: true }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Resource
|
||||
name="kbs"
|
||||
label="知识库"
|
||||
icon={<IconBook />}
|
||||
list={
|
||||
<ListTable
|
||||
filter={[
|
||||
createTextField('name', {
|
||||
label: 'name'
|
||||
}),
|
||||
createTextField('tag', {
|
||||
label: 'tag'
|
||||
})
|
||||
]}
|
||||
fields={kbFields}
|
||||
action={{ detail: true }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<Resource
|
||||
name="system"
|
||||
label="系统"
|
||||
list={
|
||||
<ListTable
|
||||
fields={SystemFields}
|
||||
action={{ detail: true, edit: true, create: true, delete: true }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Tushan>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
224
admin/src/Dashboard.tsx
Normal file
@@ -0,0 +1,224 @@
|
||||
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';
|
||||
import {
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
AreaChart,
|
||||
Area
|
||||
} from 'tushan/chart';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const authStorageKey = 'tushan:auth';
|
||||
|
||||
type UsersChartDataType = { count: number; date: string; increase: number; increaseRate: string };
|
||||
|
||||
export const Dashboard: React.FC = React.memo(() => {
|
||||
const [userCount, setUserCount] = useState(0); //用户数量
|
||||
const [kbCount, setkbCount] = useState(0);
|
||||
const [modelCount, setmodelCount] = useState(0);
|
||||
const [usersData, setUsersData] = useState<UsersChartDataType[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
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 fetchCounts = async () => {
|
||||
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');
|
||||
|
||||
if (userTotalCount) {
|
||||
setUserCount(Number(userTotalCount));
|
||||
}
|
||||
if (kbTotalCount) {
|
||||
setkbCount(Number(kbTotalCount));
|
||||
}
|
||||
if (modelTotalCount) {
|
||||
setmodelCount(Number(modelTotalCount));
|
||||
}
|
||||
};
|
||||
const fetchUserData = async () => {
|
||||
const userResponse: UsersChartDataType[] = await fetch(`${baseUrl}/users/data`, {
|
||||
headers
|
||||
}).then((res) => res.json());
|
||||
setUsersData(
|
||||
userResponse.map((item) => ({
|
||||
...item,
|
||||
date: dayjs(item.date).format('MM/DD')
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
||||
fetchCounts();
|
||||
fetchUserData();
|
||||
}, []);
|
||||
|
||||
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={'应用'} count={modelCount} />
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
|
||||
<Divider />
|
||||
<UserChart data={usersData} />
|
||||
</Card>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
Dashboard.displayName = 'Dashboard';
|
||||
|
||||
const DashboardItem = React.memo(
|
||||
(props: { title: string; href?: string; children: React.ReactNode }) => {
|
||||
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.memo((props: { icon: React.ReactElement; title: string; count: number }) => {
|
||||
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';
|
||||
|
||||
const CustomTooltip = ({ active, payload }: any) => {
|
||||
const data = payload?.[0]?.payload as UsersChartDataType;
|
||||
if (active && data) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background: 'white',
|
||||
padding: '5px 8px',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '2px 2px 5px rgba(0,0,0,0.2)'
|
||||
}}
|
||||
>
|
||||
<p className="label">
|
||||
count: <strong>{data.count}</strong>
|
||||
</p>
|
||||
<p className="label">
|
||||
increase: <strong>{data.increase}</strong>
|
||||
</p>
|
||||
<p className="label">
|
||||
increaseRate: <strong>{data.increaseRate}</strong>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const UserChart = ({ data }: { data: UsersChartDataType[] }) => {
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height={320}>
|
||||
<AreaChart
|
||||
width={730}
|
||||
height={250}
|
||||
data={data}
|
||||
margin={{ top: 10, right: 30, left: 0, bottom: 0 }}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="colorUv" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#8884d8" stopOpacity={0.8} />
|
||||
<stop offset="95%" stopColor="#8884d8" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
<linearGradient id="colorPv" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#82ca9d" stopOpacity={0.8} />
|
||||
<stop offset="95%" stopColor="#82ca9d" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<XAxis dataKey="date" />
|
||||
<YAxis />
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="count"
|
||||
stroke="#82ca9d"
|
||||
fillOpacity={1}
|
||||
fill="url(#colorPv)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
};
|
||||
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`
|
||||
});
|
||||
55
admin/src/fields.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { createTextField, createNumberField } from 'tushan';
|
||||
|
||||
export const userFields = [
|
||||
createTextField('id', { label: 'ID' }),
|
||||
createTextField('username', { label: '用户名', edit: { hidden: true } }),
|
||||
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: '所属用户', edit: { hidden: true } }),
|
||||
createTextField('name', { label: '知识库' }),
|
||||
createTextField('tags', { label: 'Tags' })
|
||||
];
|
||||
|
||||
export const ModelFields = [
|
||||
createTextField('id', { label: 'ID' }),
|
||||
createTextField('userId', { label: '所属用户', list: { hidden: true }, edit: { hidden: true } }),
|
||||
createTextField('name', { label: '名字' }),
|
||||
createTextField('model', { label: '模型', edit: { hidden: true } }),
|
||||
createTextField('share.collection', { label: '收藏数', list: { sort: true } }),
|
||||
createTextField('share.topNum', { label: '置顶等级', list: { sort: true } }),
|
||||
createTextField('share.isShare', { label: '是否分享(true,false)' }),
|
||||
createTextField('intro', { label: '介绍', list: { width: 400 } }),
|
||||
createTextField('relatedKbs', { label: '引用的知识库', list: { hidden: true } }),
|
||||
createTextField('temperature', { label: '温度' }),
|
||||
createTextField('systemPrompt', {
|
||||
label: '提示词',
|
||||
list: {
|
||||
width: 400,
|
||||
hidden: true
|
||||
}
|
||||
})
|
||||
];
|
||||
|
||||
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()],
|
||||
});
|
||||
32
client/.env.template
Normal file
@@ -0,0 +1,32 @@
|
||||
# 运行端口,如果不是 3000 口运行,需要改成其他的。注意:不是改了这个变量就会变成其他端口,而是因为改成其他端口,才用这个变量。
|
||||
DB_MAX_LINK=15 # database max link
|
||||
PORT=3000
|
||||
# 代理
|
||||
# AXIOS_PROXY_HOST=127.0.0.1
|
||||
# AXIOS_PROXY_PORT=7890
|
||||
# email
|
||||
MY_MAIL=xxxx@qq.com
|
||||
MAILE_CODE=xxxx
|
||||
# ali ems
|
||||
aliAccessKeyId=xxxx
|
||||
aliAccessKeySecret=xxxx
|
||||
aliSignName=xxxx
|
||||
aliTemplateCode=xxxx
|
||||
# token
|
||||
TOKEN_KEY=dfdasfdas
|
||||
# root key, 最高权限
|
||||
ROOT_KEY=fdafasd
|
||||
# openai
|
||||
# OPENAI_BASE_URL=http://ai.openai.com/v1
|
||||
# OPENAI_BASE_URL_AUTH=可选安全凭证,会放到 header.auth 里
|
||||
OPENAIKEY=sk-xxx
|
||||
OPENAI_TRAINING_KEY=sk-xxx
|
||||
GPT4KEY=sk-xxx
|
||||
# db
|
||||
MONGODB_URI=mongodb://username:password@0.0.0.0:27017/?authSource=admin
|
||||
MONGODB_NAME=fastgpt
|
||||
PG_HOST=0.0.0.0
|
||||
PG_PORT=8100
|
||||
PG_USER=root
|
||||
PG_PASSWORD=psw
|
||||
PG_DB_NAME=dbname
|
||||
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.
|
||||
85
client/package.json
Normal file
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"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.7.0",
|
||||
"@chakra-ui/system": "^2.5.8",
|
||||
"@dqbd/tiktoken": "^1.0.7",
|
||||
"@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",
|
||||
"date-fns": "^2.30.0",
|
||||
"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.3.0",
|
||||
"papaparse": "^5.4.1",
|
||||
"pg": "^8.10.0",
|
||||
"react": "18.2.0",
|
||||
"react-day-picker": "^8.7.1",
|
||||
"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-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"
|
||||
}
|
||||
}
|
||||
12404
client/pnpm-lock.yaml
generated
Normal file
18
client/public/docs/chatProblem.md
Normal file
@@ -0,0 +1,18 @@
|
||||
### 常见问题
|
||||
|
||||
**Git 地址**: [项目地址,完全开源,随便用。](https://github.com/c121914yu/FastGPT)
|
||||
**问题文档**: [先看文档,再提问](https://kjqvjse66l.feishu.cn/docx/HtrgdT0pkonP4kxGx8qcu6XDnGh)
|
||||
**价格表**
|
||||
如果使用了自己的 Api Key,网页上 openai 模型聊天不会计费。可以在账号页,看到详细账单。
|
||||
| 计费项 | 价格: 元/ 1K tokens(包含上下文)|
|
||||
| --- | --- |
|
||||
| 知识库 - 索引 | 0.001 |
|
||||
| chatgpt - 对话 | 0.022 |
|
||||
| chatgpt16K - 对话 | 0.025 |
|
||||
| gpt4 - 对话 | 0.5 |
|
||||
| 文件拆分 | 0.025 |
|
||||
|
||||
**其他问题**
|
||||
| 交流群 | 小助手 |
|
||||
| ----------------------- | -------------------- |
|
||||
|  |  |
|
||||
@@ -15,11 +15,12 @@ FastGpt 项目完全开源,可随意私有化部署,去除平台风险忧虑
|
||||
|
||||
### 价格表
|
||||
|
||||
如果使用了自己的 Api Key,不会计费。可以在账号页,看到详细账单。
|
||||
如果使用了自己的 Api Key,网页上 openai 模型聊天不会计费。可以在账号页,看到详细账单。
|
||||
| 计费项 | 价格: 元/ 1K tokens(包含上下文)|
|
||||
| --- | --- |
|
||||
| 知识库 - 索引 | 免费 |
|
||||
| chatgpt - 对话 | 0.025 |
|
||||
| 知识库 - 索引 | 0.001 |
|
||||
| chatgpt - 对话 | 0.022 |
|
||||
| chatgpt16K - 对话 | 0.025 |
|
||||
| gpt4 - 对话 | 0.5 |
|
||||
| 文件拆分 | 0.025 |
|
||||
|
||||
8
client/public/docs/versionIntro.md
Normal file
@@ -0,0 +1,8 @@
|
||||
### Fast GPT V3.8.8
|
||||
|
||||
1. 新增 - V2 版 OpenAPI,可以在任意第三方套壳 ChatGpt 项目中直接使用 FastGpt 的应用,注意!是直接,不需要改任何代码。具体参考[API 文档中《在第三方应用中使用 FastGpt》](https://kjqvjse66l.feishu.cn/docx/DmLedTWtUoNGX8xui9ocdUEjnNh)
|
||||
2. 新增 - 应用配置最大回复长度。
|
||||
3. 新增 - 更多的知识库配置项:相似度、最大搜索数量、自定义空搜索结果回复。
|
||||
4. 新增 - 知识库搜索测试,方便调试。
|
||||
5. 优化 - 知识库提示词位置,拥有更强的引导。
|
||||
6. 优化 - 应用编辑页面。
|
||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
BIN
client/public/icon/human.png
Normal file
|
After Width: | Height: | Size: 10 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 |
BIN
client/public/icon/logo.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
BIN
client/public/imgs/errImg.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
client/public/imgs/loading.gif
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
client/public/imgs/modelAvatar.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
@@ -38,6 +38,7 @@ export const updateHistoryQuote = (params: {
|
||||
chatId: string;
|
||||
historyId: string;
|
||||
quoteId: string;
|
||||
sourceText: string;
|
||||
}) => GET(`/chat/history/updateHistoryQuote`, params);
|
||||
|
||||
/**
|
||||
104
client/src/api/fetch.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { Props, ChatResponseType } from '@/pages/api/openapi/v1/chat/completions';
|
||||
import { sseResponseEventEnum } from '@/constants/chat';
|
||||
import { getErrText } from '@/utils/tools';
|
||||
|
||||
interface StreamFetchProps {
|
||||
data: Props;
|
||||
onMessage: (text: string) => void;
|
||||
abortSignal: AbortController;
|
||||
}
|
||||
export const streamFetch = ({ data, onMessage, abortSignal }: StreamFetchProps) =>
|
||||
new Promise<ChatResponseType & { responseText: string }>(async (resolve, reject) => {
|
||||
try {
|
||||
const response = await window.fetch('/api/openapi/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
signal: abortSignal.signal,
|
||||
body: JSON.stringify({
|
||||
...data,
|
||||
stream: true
|
||||
})
|
||||
});
|
||||
|
||||
if (!response?.body) {
|
||||
throw new Error('Request Error');
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
|
||||
// response data
|
||||
let responseText = '';
|
||||
let newChatId = '';
|
||||
let quoteLen = 0;
|
||||
|
||||
const read = async () => {
|
||||
try {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
if (response.status === 200) {
|
||||
return resolve({
|
||||
responseText,
|
||||
newChatId,
|
||||
quoteLen
|
||||
});
|
||||
} else {
|
||||
return reject('响应过程出现异常~');
|
||||
}
|
||||
}
|
||||
const chunk = decoder.decode(value);
|
||||
const chunkLines = chunk.split('\n\n').filter((item) => item);
|
||||
const chunkResponse = chunkLines.map((item) => {
|
||||
const splitEvent = item.split('\n');
|
||||
if (splitEvent.length === 2) {
|
||||
return {
|
||||
event: splitEvent[0].replace('event: ', ''),
|
||||
data: splitEvent[1].replace('data: ', '')
|
||||
};
|
||||
}
|
||||
return {
|
||||
event: '',
|
||||
data: splitEvent[0].replace('data: ', '')
|
||||
};
|
||||
});
|
||||
|
||||
chunkResponse.forEach((item) => {
|
||||
// parse json data
|
||||
const data = (() => {
|
||||
try {
|
||||
return JSON.parse(item.data);
|
||||
} catch (error) {
|
||||
return item.data;
|
||||
}
|
||||
})();
|
||||
|
||||
if (item.event === sseResponseEventEnum.answer && data !== '[DONE]') {
|
||||
const answer: string = data?.choices[0].delta.content || '';
|
||||
onMessage(answer);
|
||||
responseText += answer;
|
||||
} else if (item.event === sseResponseEventEnum.chatResponse) {
|
||||
const chatResponse = data as ChatResponseType;
|
||||
newChatId = chatResponse.newChatId;
|
||||
quoteLen = chatResponse.quoteLen || 0;
|
||||
}
|
||||
});
|
||||
read();
|
||||
} catch (err: any) {
|
||||
if (err?.message === 'The user aborted a request.') {
|
||||
return resolve({
|
||||
responseText,
|
||||
newChatId,
|
||||
quoteLen
|
||||
});
|
||||
}
|
||||
reject(getErrText(err, '请求异常'));
|
||||
}
|
||||
};
|
||||
read();
|
||||
} catch (err: any) {
|
||||
console.log(err, '====');
|
||||
reject(getErrText(err, '请求异常'));
|
||||
}
|
||||
});
|
||||
@@ -2,12 +2,22 @@ import { GET, POST, PUT, DELETE } from '../request';
|
||||
import type { KbItemType } from '@/types/plugin';
|
||||
import { RequestPaging } from '@/types/index';
|
||||
import { TrainingModeEnum } from '@/constants/plugin';
|
||||
import { type QuoteItemType } from '@/pages/api/openapi/kb/appKbSearch';
|
||||
import {
|
||||
Props as PushDataProps,
|
||||
Response as PushDateResponse
|
||||
} from '@/pages/api/openapi/kb/pushData';
|
||||
import {
|
||||
Props as SearchTestProps,
|
||||
Response as SearchTestResponse
|
||||
} from '@/pages/api/openapi/kb/searchTest';
|
||||
|
||||
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`);
|
||||
@@ -32,7 +42,7 @@ export const getKbDataList = (data: GetKbDataListProps) =>
|
||||
* 获取导出数据(不分页)
|
||||
*/
|
||||
export const getExportDataList = (kbId: string) =>
|
||||
GET<[string, string][]>(
|
||||
GET<[string, string, string][]>(
|
||||
`/plugins/kb/data/exportModelData`,
|
||||
{ kbId },
|
||||
{
|
||||
@@ -50,7 +60,7 @@ export const getTrainingData = (data: { kbId: string; init: boolean }) =>
|
||||
}>(`/plugins/kb/data/getTrainingData`, data);
|
||||
|
||||
export const getKbDataItemById = (dataId: string) =>
|
||||
GET(`/plugins/kb/data/getDataById`, { dataId });
|
||||
GET<QuoteItemType>(`/plugins/kb/data/getDataById`, { dataId });
|
||||
|
||||
/**
|
||||
* 直接push数据
|
||||
@@ -78,3 +88,6 @@ export const postSplitData = (data: {
|
||||
prompt: string;
|
||||
mode: `${TrainingModeEnum}`;
|
||||
}) => POST(`/openapi/text/pushData`, data);
|
||||
|
||||
export const searchText = (data: SearchTestProps) =>
|
||||
POST<SearchTestResponse>(`/openapi/kb/searchTest`, data);
|
||||
@@ -4,6 +4,7 @@ import type { ChatItemType } from '@/types/chat';
|
||||
export interface InitChatResponse {
|
||||
chatId: string;
|
||||
modelId: string;
|
||||
systemPrompt?: string;
|
||||
model: {
|
||||
name: string;
|
||||
avatar: string;
|
||||
@@ -5,3 +5,5 @@ import type { InitDateResponse } from '@/pages/api/system/getInitData';
|
||||
export const getInitData = () => GET<InitDateResponse>('/system/getInitData');
|
||||
|
||||
export const getSystemModelList = () => GET<ChatModelItemType[]>('/system/getModels');
|
||||
|
||||
export const uploadImg = (base64Img: string) => POST<string>('/system/uploadImage', { base64Img });
|
||||
@@ -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;
|
||||
@@ -66,7 +66,7 @@ export const loginOut = () => GET('/user/loginout');
|
||||
export const putUserInfo = (data: UserUpdateParams) => PUT('/user/update', data);
|
||||
|
||||
export const getUserBills = (data: RequestPaging) =>
|
||||
GET<PagingData<UserBillType>>(`/user/getBill?${Obj2Query(data)}`);
|
||||
POST<PagingData<UserBillType>>(`/user/getBill`, data);
|
||||
|
||||
export const getPayOrders = () => GET<PaySchema[]>(`/user/getPayOrders`);
|
||||
|
||||
@@ -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 });
|
||||
160
client/src/components/APIKeyModal/index.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
Flex,
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
TableContainer,
|
||||
IconButton
|
||||
} from '@chakra-ui/react';
|
||||
import { getOpenApiKeys, createAOpenApiKey, delOpenApiById } from '@/api/openapi';
|
||||
import { useQuery, useMutation } from '@tanstack/react-query';
|
||||
import { useLoading } from '@/hooks/useLoading';
|
||||
import dayjs from 'dayjs';
|
||||
import { AddIcon, DeleteIcon } from '@chakra-ui/icons';
|
||||
import { getErrText, useCopyData } from '@/utils/tools';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import MyIcon from '../Icon';
|
||||
|
||||
const APIKeyModal = ({ onClose }: { onClose: () => void }) => {
|
||||
const { Loading } = useLoading();
|
||||
const { toast } = useToast();
|
||||
const {
|
||||
data: apiKeys = [],
|
||||
isLoading: isGetting,
|
||||
refetch
|
||||
} = useQuery(['getOpenApiKeys'], getOpenApiKeys);
|
||||
const [apiKey, setApiKey] = useState('');
|
||||
const { copyData } = useCopyData();
|
||||
|
||||
const { mutate: onclickCreateApiKey, isLoading: isCreating } = useMutation({
|
||||
mutationFn: () => createAOpenApiKey(),
|
||||
onSuccess(res) {
|
||||
setApiKey(res);
|
||||
refetch();
|
||||
},
|
||||
onError(err) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: getErrText(err)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const { mutate: onclickRemove, isLoading: isDeleting } = useMutation({
|
||||
mutationFn: async (id: string) => delOpenApiById(id),
|
||||
onSuccess() {
|
||||
refetch();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal isOpen onClose={onClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent w={'600px'} maxW={'90vw'} position={'relative'}>
|
||||
<Box py={3} px={5}>
|
||||
<Box fontWeight={'bold'} fontSize={'2xl'}>
|
||||
API 秘钥管理
|
||||
</Box>
|
||||
<Box fontSize={'sm'} color={'myGray.600'}>
|
||||
如果你不想 API 秘钥被滥用,请勿将秘钥直接放置在前端使用~
|
||||
</Box>
|
||||
</Box>
|
||||
<ModalCloseButton />
|
||||
<ModalBody minH={'300px'} maxH={['70vh', '500px']} overflow={'overlay'}>
|
||||
<TableContainer mt={2} position={'relative'}>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Api Key</Th>
|
||||
<Th>创建时间</Th>
|
||||
<Th>最后一次使用时间</Th>
|
||||
<Th />
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody fontSize={'sm'}>
|
||||
{apiKeys.map(({ id, apiKey, createTime, lastUsedTime }) => (
|
||||
<Tr key={id}>
|
||||
<Td>{apiKey}</Td>
|
||||
<Td>{dayjs(createTime).format('YYYY/MM/DD HH:mm:ss')}</Td>
|
||||
<Td>
|
||||
{lastUsedTime
|
||||
? dayjs(lastUsedTime).format('YYYY/MM/DD HH:mm:ss')
|
||||
: '没有使用过'}
|
||||
</Td>
|
||||
<Td>
|
||||
<IconButton
|
||||
icon={<DeleteIcon />}
|
||||
size={'xs'}
|
||||
aria-label={'delete'}
|
||||
variant={'base'}
|
||||
colorScheme={'gray'}
|
||||
onClick={() => onclickRemove(id)}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
variant="base"
|
||||
leftIcon={<AddIcon color={'myGray.600'} fontSize={'sm'} />}
|
||||
onClick={() => onclickCreateApiKey()}
|
||||
>
|
||||
新建秘钥
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
|
||||
<Loading loading={isGetting || isCreating || isDeleting} fixed={false} />
|
||||
</ModalContent>
|
||||
<Modal isOpen={!!apiKey} onClose={() => setApiKey('')}>
|
||||
<ModalOverlay />
|
||||
<ModalContent w={'400px'} maxW={'90vw'}>
|
||||
<Box py={3} px={5}>
|
||||
<Box fontWeight={'bold'} fontSize={'2xl'}>
|
||||
新的 API 秘钥
|
||||
</Box>
|
||||
<Box fontSize={'sm'} color={'myGray.600'}>
|
||||
请保管好你的秘钥,秘钥不会再次展示~
|
||||
</Box>
|
||||
</Box>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<Flex
|
||||
bg={'myGray.100'}
|
||||
px={3}
|
||||
py={2}
|
||||
cursor={'pointer'}
|
||||
onClick={() => copyData(apiKey)}
|
||||
>
|
||||
<Box flex={1}>{apiKey}</Box>
|
||||
<MyIcon name={'copy'} w={'16px'}></MyIcon>
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant="base" onClick={() => setApiKey('')}>
|
||||
好的
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default APIKeyModal;
|
||||
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;
|
||||
4
client/src/components/DateRangePicker/index.module.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
.datePicker {
|
||||
--rdp-background-color: #d6e8ff;
|
||||
--rdp-accent-color: #0000ff;
|
||||
}
|
||||
121
client/src/components/DateRangePicker/index.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import React, { useState, useMemo, useRef } from 'react';
|
||||
import { Box, Card, Flex, useTheme, useOutsideClick, Button } from '@chakra-ui/react';
|
||||
import { addDays, format } from 'date-fns';
|
||||
import { type DateRange, DayPicker } from 'react-day-picker';
|
||||
import MyIcon from '../Icon';
|
||||
import 'react-day-picker/dist/style.css';
|
||||
import styles from './index.module.scss';
|
||||
import zhCN from 'date-fns/locale/zh-CN';
|
||||
|
||||
const DateRangePicker = ({
|
||||
onChange,
|
||||
onSuccess,
|
||||
position = 'bottom',
|
||||
defaultDate = {
|
||||
from: addDays(new Date(), -30),
|
||||
to: new Date()
|
||||
}
|
||||
}: {
|
||||
onChange?: (date: DateRange) => void;
|
||||
onSuccess?: (date: DateRange) => void;
|
||||
position?: 'bottom' | 'top';
|
||||
defaultDate?: DateRange;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const OutRangeRef = useRef(null);
|
||||
const [range, setRange] = useState<DateRange | undefined>(defaultDate);
|
||||
const [showSelected, setShowSelected] = useState(false);
|
||||
|
||||
const formatSelected = useMemo(() => {
|
||||
if (range?.from && range.to) {
|
||||
return `${format(range.from, 'y-MM-dd')} ~ ${format(range.to, 'y-MM-dd')}`;
|
||||
}
|
||||
return `${format(new Date(), 'y-MM-dd')} ~ ${format(new Date(), 'y-MM-dd')}`;
|
||||
}, [range]);
|
||||
|
||||
useOutsideClick({
|
||||
ref: OutRangeRef,
|
||||
handler: () => {
|
||||
setShowSelected(false);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Box position={'relative'} ref={OutRangeRef}>
|
||||
<Flex
|
||||
border={theme.borders.base}
|
||||
px={3}
|
||||
py={1}
|
||||
borderRadius={'sm'}
|
||||
cursor={'pointer'}
|
||||
bg={'myWhite.600'}
|
||||
fontSize={'sm'}
|
||||
onClick={() => setShowSelected(true)}
|
||||
>
|
||||
<Box>{formatSelected}</Box>
|
||||
<MyIcon ml={2} name={'date'} w={'16px'} color={'myGray.600'} />
|
||||
</Flex>
|
||||
{showSelected && (
|
||||
<Card
|
||||
position={'absolute'}
|
||||
zIndex={1}
|
||||
{...(position === 'top'
|
||||
? {
|
||||
bottom: '40px'
|
||||
}
|
||||
: {})}
|
||||
>
|
||||
<DayPicker
|
||||
locale={zhCN}
|
||||
id="test"
|
||||
mode="range"
|
||||
className={styles.datePicker}
|
||||
defaultMonth={defaultDate.to}
|
||||
selected={range}
|
||||
disabled={[
|
||||
{ from: new Date(2022, 3, 1), to: addDays(new Date(), -90) },
|
||||
{ from: addDays(new Date(), 1), to: new Date(2099, 1, 1) }
|
||||
]}
|
||||
onSelect={(date) => {
|
||||
if (date?.from === undefined) {
|
||||
date = {
|
||||
from: range?.from,
|
||||
to: range?.from
|
||||
};
|
||||
}
|
||||
if (date?.to === undefined) {
|
||||
date.to = date.from;
|
||||
}
|
||||
setRange(date);
|
||||
onChange && onChange(date);
|
||||
}}
|
||||
footer={
|
||||
<Flex justifyContent={'flex-end'}>
|
||||
<Button
|
||||
variant={'outline'}
|
||||
size={'sm'}
|
||||
mr={2}
|
||||
onClick={() => setShowSelected(false)}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
size={'sm'}
|
||||
onClick={() => {
|
||||
onSuccess && onSuccess(range || defaultDate);
|
||||
setShowSelected(false);
|
||||
}}
|
||||
>
|
||||
确认
|
||||
</Button>
|
||||
</Flex>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default DateRangePicker;
|
||||
export type DateRangeType = DateRange;
|
||||
1
client/src/components/Icon/icons/apikey.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="1686969412308" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3481" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><path d="M517.864056 487.834624c-56.774051-54.213739-58.850339-144.187937-4.6366-200.960964 54.212716-56.773028 144.187937-58.849316 200.960964-4.6366 56.775074 54.213739 58.850339 144.186913 4.6366 200.960964C664.613328 539.972075 574.639131 542.048363 517.864056 487.834624zM687.194626 452.994118c37.533848-39.308261 36.09508-101.596909-3.210112-139.128711-39.304168-37.531801-101.593839-36.094056-139.127687 3.211135-37.532825 39.307238-36.093033 101.593839 3.212158 139.125641C587.374176 493.736031 649.660778 492.302379 687.194626 452.994118zM479.104287 670.917406l-101.495602 106.289792c26.206872 25.024953 27.167756 66.540486 2.14178 92.749404-25.028023 26.209942-66.543555 27.16571-92.750427 2.140757l-58.361199 53.027727c0 0-68.750827 11.100826-100.379175-19.101033-31.630395-30.205952-37.865399-112.721271-37.865399-112.721271l246.37427-258.302951c-63.173808-117.608581-47.24707-267.162736 49.939389-368.939747 36.517705-38.242999 80.346933-65.156976 127.165238-81.040734l1.084705 46.269813c-35.443233 14.07967-68.566632 35.596729-96.618525 64.973804-80.271208 84.064604-96.099708 205.865671-49.433876 305.083393l23.075555 39.163975L146.090774 798.015106c0 0 0.593518 49.77873 17.242709 65.677838 14.888082 14.216793 61.832254 9.828856 61.832254 9.828856l60.407812-63.260789 31.631418 30.203906c8.741082 8.346085 22.570042 8.030907 30.91715-0.711198 8.347109-8.742105 8.026814-22.571065-0.713244-30.91715l-31.632441-30.207999 156.456355-163.846672 39.009456 22.481014c101.259218 42.039465 222.201731 20.61041 302.474986-63.453171 104.251366-109.178585 100.260471-282.211477-8.91709-386.464889-33.591049-32.075533-73.260537-53.829999-115.093295-65.49262l-1.030469-45.153386c53.197596 12.471033 103.945397 38.547944 146.323577 79.015611 126.645398 120.931257 131.277906 321.649698 10.344602 448.296119C748.158093 705.787588 599.500355 728.598106 479.104287 670.917406z" p-id="3482"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
|
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 |
1
client/src/components/Icon/icons/date.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="1686832863390" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4120" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><path d="M782.84 188.75h-43.15v-60.46c0-16.57-13.43-30-30-30s-30 13.43-30 30v60.46H371.88v-60.46c0-16.57-13.43-30-30-30s-30 13.43-30 30v60.46H250.5c-66.17 0-120 53.83-120 120v494.47c0 66.17 53.83 120 120 120h532.33c66.17 0 120-53.83 120-120V308.75c0.01-66.17-53.82-120-119.99-120z m-532.34 60h61.37v133.63c0 16.57 13.43 30 30 30s30-13.43 30-30V248.75h307.81v133.63c0 16.57 13.43 30 30 30s30-13.43 30-30V248.75h43.15c33.08 0 60 26.92 60 60V649.5H190.5V308.75c0-33.08 26.92-60 60-60z m532.34 614.47H250.5c-33.08 0-60-26.92-60-60V709.5h652.33v93.72c0.01 33.08-26.91 60-59.99 60z" p-id="4121"></path></svg>
|
||||
|
After Width: | Height: | Size: 924 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
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/history.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="1686557412109" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2150" xmlns:xlink="http://www.w3.org/1999/xlink" ><path d="M511.998 64C264.574 64 64 264.574 64 511.998S264.574 960 511.998 960 960 759.422 960 511.998 759.422 64 511.998 64z m353.851 597.438c-82.215 194.648-306.657 285.794-501.306 203.579S78.749 558.36 160.964 363.711 467.621 77.917 662.27 160.132c168.009 70.963 262.57 250.652 225.926 429.313a383.995 383.995 0 0 1-22.347 71.993z" p-id="2151"></path><path d="M543.311 498.639V256.121c0-17.657-14.314-31.97-31.97-31.97s-31.97 14.314-31.97 31.97v269.005l201.481 201.481c12.485 12.485 32.728 12.485 45.213 0s12.485-32.728 0-45.213L543.311 498.639z" p-id="2152"></path></svg>
|
||||
|
After Width: | Height: | Size: 875 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 |
1
client/src/components/Icon/icons/kbTest.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="1686561811905" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2855" xmlns:xlink="http://www.w3.org/1999/xlink" ><path d="M992 528c0 273.9-222.1 496-496 496S0 801.9 0 528 222.1 32 496 32c86.2 0 167.3 22 238 60.7 2.3 1.3 2.8 4.4 0.9 6.3l-37 37.3-4.2 4.3c-1.2 1.2-3.1 1.5-4.6 0.8-8.2-4.1-16.5-7.9-24.9-11.5C610.9 107.4 554.3 96 496 96s-114.9 11.4-168.1 33.9c-51.4 21.8-97.7 52.9-137.3 92.6-39.7 39.7-70.9 85.9-92.6 137.3C75.4 413.1 64 469.6 64 528c0 58.3 11.4 114.9 33.9 168.1 21.8 51.4 52.9 97.6 92.6 137.3 39.7 39.7 85.9 70.9 137.3 92.6 53.3 22.6 109.9 34 168.2 34s114.9-11.4 168.1-33.9c51.4-21.8 97.7-52.9 137.3-92.6 39.7-39.7 70.9-85.9 92.6-137.3 22.6-53.3 34-109.9 34-168.2 0-58.4-11.4-114.9-33.9-168.1-3.6-8.5-7.4-16.8-11.5-25-0.8-1.5-0.5-3.4 0.8-4.6l4.3-4.2 37.3-37c1.9-1.9 5-1.4 6.3 0.9C970 360.6 992 441.7 992 528z" p-id="2856"></path><path d="M781.4 397c-3.7-8-11.7-13.1-20.6-13.1H740c-6 0-11.8 2.4-16 6.6-7 7-8.6 17.6-4.1 26.4 2.6 5.1 5 10.3 7.3 15.7 13.2 31.2 19.9 64.3 19.9 98.5s-6.7 67.3-19.9 98.5c-12.7 30.1-31 57.2-54.2 80.4-23.3 23.3-50.3 41.5-80.4 54.2-31.3 13.1-64.4 19.8-98.6 19.8s-67.3-6.7-98.5-19.9c-30.1-12.7-57.2-31-80.4-54.2-23.3-23.3-41.5-50.3-54.2-80.4-13.2-31.2-19.9-64.3-19.9-98.5s6.7-67.3 19.9-98.5c12.7-30.1 31-57.2 54.2-80.4 23.3-23.3 50.3-41.5 80.4-54.2 31.2-13.2 64.3-19.9 98.5-19.9s67.3 6.7 98.5 19.9c4.9 2.1 9.8 4.3 14.6 6.7 8.8 4.4 19.4 2.6 26.3-4.4 4.3-4.3 6.7-10.1 6.7-16.2v-20.2c0-9-5.2-17.1-13.4-20.8-40.4-18.6-85.3-29-132.6-29-175.5 0-318 143.4-317 318.9C178 707.1 319.6 848 494 848c174.8 0 316.6-141.3 317-316.2 0.1-48.2-10.5-93.9-29.6-134.8z" p-id="2857"></path><path d="M634.5 488.5c-0.8-2.9-4.5-3.9-6.7-1.7l-34.7 34.7-1.8 1.8c-9 9-15.7 20.1-20.1 32.1-11.5 31.6-42.4 54-78.3 52.7-41.6-1.6-75.3-35.3-76.9-76.9-1.4-35.9 21-66.8 52.7-78.3 12-4.4 23-11.1 32.1-20.1l1.8-1.8 34.7-34.7c2.2-2.2 1.2-5.8-1.7-6.7-12.9-3.7-26.5-5.6-40.6-5.5-79.4 0.5-143 64.5-143 143.9 0 79.5 64.5 144 144 144 79.4 0 143.4-63.6 144-142.9 0.1-14.1-1.8-27.8-5.5-40.6z" p-id="2858"></path><path d="M1014.3 146H882c-2.2 0-4-1.8-4-4V9.8c0-2.4-2-4-4-4-1 0-2 0.4-2.8 1.2L766.8 112.4l-46.1 46.5-44 44.4c-3 3-4.6 7-4.6 11.3v85.5c0 4.3-1.7 8.3-4.7 11.3l-94.7 94.7-47.4 47.4-51.8 51.9c-12.5 12.5-12.5 32.8 0 45.3 6.3 6.3 14.4 9.4 22.6 9.4s16.4-3.1 22.6-9.4l51.8-51.9 123.2-123.2 19-19c3-3 7.1-4.7 11.3-4.7h85.5c4.2 0 8.3-1.7 11.3-4.6l44.3-43.9 46.5-46.1L1017 152.9c2.6-2.6 0.8-6.9-2.7-6.9zM864 214.3l-44 43.5-25.6 25.4c-3 3-7 4.6-11.3 4.6H744c-4.4 0-8-3.6-8-8v-39c0-4.2 1.7-8.3 4.6-11.3l25.5-25.7 43.5-43.9 1.6-1.6c1.6-1.7 4.5-0.7 4.8 1.6 4.8 25.8 23.5 41.6 48.6 47.7 2.1 0.5 2.9 3.2 1.3 4.7l-1.9 2z" p-id="2859"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
|
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 |