Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d04c298132 | ||
|
|
eceda01c19 | ||
|
|
ea1681e1eb | ||
|
|
f6c4b4c96d | ||
|
|
22cc9c85be | ||
|
|
43f8d6008f | ||
|
|
29c5554f9e | ||
|
|
9b18a46456 | ||
|
|
d5923bc64f | ||
|
|
f19c2d2ca1 | ||
|
|
84d91f3f76 | ||
|
|
7811f7482b | ||
|
|
9c8ca7dd25 | ||
|
|
1409916bd0 | ||
|
|
fc7edcb54f | ||
|
|
87d35042de | ||
|
|
77dc961a07 | ||
|
|
9a45fb64c2 | ||
|
|
881c36542c | ||
|
|
f88c6031f5 | ||
|
|
8a02b3b04a | ||
|
|
d460305871 | ||
|
|
144bed5a77 | ||
|
|
96fc917bad | ||
|
|
794a3698ad |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -36,4 +36,5 @@ yarn-error.log*
|
||||
next-env.d.ts
|
||||
/public/trainData/
|
||||
/.vscode/
|
||||
platform.json
|
||||
platform.json
|
||||
testApi/
|
||||
91
README.md
91
README.md
@@ -8,42 +8,71 @@ Fast GPT 允许你使用自己的 openai API KEY 来快速的调用 openai 接
|
||||
```
|
||||
AXIOS_PROXY_HOST=axios代理地址,目前 openai 接口都需要走代理,本机的话就填 127.0.0.1
|
||||
AXIOS_PROXY_PORT=代理端口
|
||||
MONGODB_URI=mongo数据库地址(例如:mongodb://username:password@ip:27017/?authSource=admin&readPreference=primary&appname=MongoDB%20Compass&directConnection=true&ssl=false)
|
||||
MONGODB_URI=mongo数据库地址
|
||||
MY_MAIL=发送验证码邮箱
|
||||
MAILE_CODE=邮箱秘钥(代理里设置的是QQ邮箱,不知道怎么找这个 code 的,可以百度搜"nodemailer发送邮件")
|
||||
TOKEN_KEY=随便填一个,用于生成和校验 token
|
||||
OPENAIKEY=openai的key
|
||||
REDIS_URL=redis的地址
|
||||
```
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
## 部署
|
||||
|
||||
### docker 模式
|
||||
请准备好 docker, mongo,代理, 和nginx。 镜像走本机的代理,所以用 network=host,port 改成代理的端口,clash 一般都是 7890。
|
||||
请准备好 docker, mongo,代理, 和 nginx。 镜像走本机的代理,所以用 network=host,port 改成代理的端口,clash 一般都是 7890。
|
||||
|
||||
#### docker 打包
|
||||
```bash
|
||||
docker build -t imageName:tag .
|
||||
docker push imageName:tag
|
||||
# 或者直接拉镜像,见下方
|
||||
```
|
||||
|
||||
#### 服务器拉取镜像和运行
|
||||
```yml
|
||||
# docker-compose
|
||||
version: "3.3"
|
||||
services:
|
||||
fast-gpt:
|
||||
image: c121914yu/fast-gpt:latest
|
||||
environment:
|
||||
AXIOS_PROXY_HOST: 127.0.0.1
|
||||
AXIOS_PROXY_PORT: 7890
|
||||
MY_MAIL:
|
||||
MAILE_CODE:
|
||||
TOKEN_KEY:
|
||||
MONGODB_URI:
|
||||
OPENAIKEY:
|
||||
REDIS_URL:
|
||||
network_mode: host
|
||||
restart: always
|
||||
container_name: fast-gpt
|
||||
```
|
||||
```bash
|
||||
# 服务器拉取部署, imageName 替换成镜像名
|
||||
docker pull imageName:tag
|
||||
docker stop fast-gpt || true
|
||||
docker rm fast-gpt || true
|
||||
docker run -d --network=host --name fast-gpt \
|
||||
-e AXIOS_PROXY_HOST=127.0.0.1 \
|
||||
-e AXIOS_PROXY_PORT=7890 \
|
||||
-e MY_MAIL=your email\
|
||||
-e MAILE_CODE=your email code \
|
||||
-e TOKEN_KEY=任意一个内容 \
|
||||
-e MONGODB_URI="mongodb://user:password@127.0.0.0:27017/?authSource=admin&readPreference=primary&appname=MongoDB%20Compass&ssl=false" \
|
||||
imageName:tag
|
||||
#!/bin/bash
|
||||
# 拉取最新镜像
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
|
||||
echo "Docker Compose 重新拉取镜像完成!"
|
||||
|
||||
# 删除本地旧镜像
|
||||
images=$(docker images --format "{{.ID}} {{.Repository}}" | grep fast-gpt)
|
||||
|
||||
# 将镜像 ID 和名称放入数组中
|
||||
IFS=$'\n' read -rd '' -a image_array <<<"$images"
|
||||
|
||||
# 遍历数组并删除所有旧的镜像
|
||||
for ((i=1; i<${#image_array[@]}; i++))
|
||||
do
|
||||
image=${image_array[$i]}
|
||||
image_id=${image%% *}
|
||||
docker rmi $image_id
|
||||
done
|
||||
```
|
||||
|
||||
#### 软件教程:docker 安装
|
||||
@@ -104,9 +133,33 @@ echo "Restart clash"
|
||||
|
||||
#### redis
|
||||
|
||||
安装
|
||||
```bash
|
||||
# 索引
|
||||
# FT.CREATE idx:model:data ON JSON PREFIX 1 model:data: SCHEMA $.modelId AS modelId TAG $.dataId AS dataId TAG $.vector AS vector VECTOR FLAT 6 DIM 1536 DISTANCE_METRIC COSINE TYPE FLOAT32
|
||||
# FT.CREATE idx:model:data:hash ON HASH PREFIX 1 model:data: SCHEMA modelId TAG dataId TAG vector VECTOR FLAT 6 DIM 1536 DISTANCE_METRIC COSINE TYPE FLOAT32
|
||||
FT.CREATE idx:model:data ON HASH PREFIX 1 model:data: SCHEMA modelId TAG userId TAG q TEXT text TEXT vector VECTOR FLAT 6 DIM 1536 DISTANCE_METRIC COSINE TYPE FLOAT32
|
||||
#!/bin/bash
|
||||
docker pull redis/redis-stack:6.2.6-v6
|
||||
docker stop fast-gpt-redis-stack
|
||||
docker rm fast-gpt-redis-stack
|
||||
|
||||
docker run -d --name fast-gpt-redis-stack \
|
||||
-v /redis/data:/data \
|
||||
-v /etc/localtime:/etc/localtime:ro \
|
||||
-v /redis.conf:/redis-stack.conf \
|
||||
-e REDIS_ARGS="--requirepass 1111111"\
|
||||
-p 8102:6379 \
|
||||
-p 8103:8001 \
|
||||
--restart unless-stopped \
|
||||
redis/redis-stack:6.2.6-v6
|
||||
```
|
||||
```bash
|
||||
# /redis.conf
|
||||
# 开启aop持久化
|
||||
appendonly yes
|
||||
#default: 持久化文件
|
||||
appendfilename "appendonly.aof"
|
||||
#default: 每秒同步一次
|
||||
appendfsync everysec
|
||||
```
|
||||
```bash
|
||||
# 添加索引
|
||||
FT.CREATE idx:model:data:hash ON HASH PREFIX 1 model:data: SCHEMA modelId TAG userId TAG status TAG q TEXT text TEXT vector VECTOR FLAT 6 DIM 1536 DISTANCE_METRIC COSINE TYPE FLOAT32
|
||||
```
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"sass": "^1.58.3",
|
||||
"sharp": "^0.31.3",
|
||||
"tunnel": "^0.0.6",
|
||||
"wxpay-v3": "^3.0.2",
|
||||
"zustand": "^4.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
867
pnpm-lock.yaml
generated
867
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,15 +0,0 @@
|
||||
{"prompt":"什么是百度plato?</s>","completion":" 百度PLATO是一种聊天机器人,发布于2021年10月,参数量为百亿,是GPT-3的1/17。</s>"}
|
||||
{"prompt":"与百度plato相比,chatgpt有哪些优点?</s>","completion":" 与百度PLATO相比,ChatGPT体现出耐心、体贴和妥当,被网友们普遍称赞。ChatGPT可以用于个性化聊天机器人,例如生成一个很相似的数字人,提供一位温柔健谈的朋友,指导语言学习等。</s>"}
|
||||
{"prompt":"replika是什么?</s>","completion":" Replika是一个产品,使用ChatGPT的技术,可以根据自己的需求生成一个虚拟女友、男友或单纯的朋友。目前还引起了不少有关性骚扰、家暴、隐私等问题的争议。</s>"}
|
||||
{"prompt":"聊天机器人可以用于哪些场景?</s>","completion":" 聊天机器人可以用于很多陪伴的场景,例如亲人去世、抑郁症或自闭症患者、语言学习、小朋友、养老院或独居在家的老人等。</s>"}
|
||||
{"prompt":"有人反对使用聊天机器人解决孤独困境,为什么?</s>","completion":" 有人反对使用聊天机器人解决孤独困境,认为孤独困境应该用人与人真正的交往解决,机器是冰冷的。</s>"}
|
||||
{"prompt":"为什么聊天机器人仍然需要解决当下的需求?</s>","completion":" 在社会结构性的困局还没有解开时,长夜漫漫的独身打工人、养老院里卧床的老人、父母在外留守在家的儿童,当下的需求仍需要解决。</s>"}
|
||||
{"prompt":"ai在未来会被戴上什么样的帽子?</s>","completion":" AI在未来各个场景下都会被戴上一个很虚无的帽子:“机器是冰冷的”、“没有灵魂”、“它并不是真正在思考”。</s>"}
|
||||
{"prompt":"怎么回答“ai是否真正思考”的问题?</s>","completion":" 引用乔姆斯基老爷子的那句名言:“思考是人类特性。AI有天会思考吗?那就像是在问潜水艇会不会游泳一样。”</s>"}
|
||||
{"prompt":"聊天机器人与人类完成任务的方式有何不同?</s>","completion":" 聊天机器人与人类完成任务的方式不同,但并不重要。它们能跟人一样完成任务,这是最重要的。</s>"}
|
||||
{"prompt":"聊天机器人的发展前景如何?</s>","completion":" 聊天机器人的发展前景广阔,可以应用于很多领域,例如客服、教育、医疗等。</s>"}
|
||||
{"prompt":"什么是百度plato?</s>","completion":" 百度PLATO是一种聊天机器人,发布于2021年10月,参数量为百亿,是GPT-3的1/17。</s>"}
|
||||
{"prompt":"与百度plato相比,chatgpt有哪些优点?</s>","completion":" 与百度PLATO相比,ChatGPT体现出耐心、体贴和妥当,被网友们普遍称赞。ChatGPT还可以用于很多陪伴的场景,如亲人去世、抑郁症或自闭症患者、语言学习、小朋友、养老院或独居在家的老人等。</s>"}
|
||||
{"prompt":"replika是什么产品?</s>","completion":" Replika是一个产品,用的是ChatGPT的技术。用户可以根据自己的需求生成一个虚拟女友、男友或单纯的朋友。目前还引起了不少有关性骚扰、家暴、隐私等问题的争议。</s>"}
|
||||
{"prompt":"聊天机器人能够解决哪些现实问题?</s>","completion":" 聊天机器人能够解决现代人面临的孤独困境,如长夜漫漫的独身打工人、养老院里卧床的老人、父母在外留守在家的儿童等。</s>"}
|
||||
{"prompt":"有人反对使用聊天机器人解决孤独困境,为什么?</s>","completion":" 有人反对使用聊天机器人解决孤独困境,认为孤独困境应该用人与人真正的交往解决,机器是冰冷的。</s>"}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,21 +0,0 @@
|
||||
{"prompt": "什么是高潜公司\n\n###\n\n", "completion": " 高潜公司是指高潜力的公司,其所处的赛道有发展潜力和前景。###"}
|
||||
{"prompt": "高潜公司的优势是什么\n\n###\n\n", "completion": " 高潜公司所处的整个行业和赛道有一定的增量空间,头部vc通过严格的筛选和加持为高潜公司背书,高潜公司的创始者一般都特别优秀。加入高潜公司可以获得很多机会来锻炼自己、强化自己的认知,快速成长、展现自己,并有可能成为公司扩张后的管理层###"}
|
||||
{"prompt": "高潜公司的风险是什么\n\n###\n\n", "completion": " 高潜公司的高风险问题也很明显。一旦公司失败,很多边缘人就会被淘汰掉。但宏观来讲,只要赛道是有潜力的,即便这家公司失败了,也可以跳槽到成功的公司去。###"}
|
||||
{"prompt": "高潜公司的例子有哪些\n\n###\n\n", "completion": " 很多高潜公司在做最近比较火的AIGC,包括去年的元宇宙,前两年比较火的新消费、企业服务和出海之类的。###"}
|
||||
{"prompt": "为什么现在加入大厂的前景不广阔?\n\n###\n\n", "completion": " 今年的大形势之下,互联网大厂纷纷开始裁员、取消普调、晋升比例严重缩水,HC也急剧减少。###"}
|
||||
{"prompt": "加入高潜公司可以获得什么样的机会?\n\n###\n\n", "completion": " 加入高潜公司可以获得很多机会来锻炼自己、强化自己的认知,快速成长、展现自己,并有可能成为公司扩张后的管理层。###"}
|
||||
{"prompt": "什么是高潜公司?\n\n###\n\n", "completion": " 高潜公司的定义是“高潜力的公司”,所处的行业和赛道有发展潜力和前景,头部vc为其背书,创始者一般都特别优秀。###"}
|
||||
{"prompt": "加入高潜公司有哪些优点\n\n###\n\n", "completion": " 加入高潜公司能够获得锻炼自己、强化认知的机会,快速成长、展现自己,在公司扩张后成为团队的管理层。同时,所处的行业还处于上升期,发展前景广阔。###"}
|
||||
{"prompt": "高潜公司存在哪些风险?\n\n###\n\n", "completion": " 高潜公司存在高风险问题,一旦公司失败,很多边缘人就会被淘汰掉。但宏观来讲,只要赛道是有潜力的,即便这家公司失败了,也可以跳槽到成功的公司去。###"}
|
||||
{"prompt": "高潜公司和互联网有什么相似之处?\n\n###\n\n", "completion": " 目前来说,高潜公司的风格大部分都比较像互联网大厂,有风格,但又没有那么内卷,因为大家每天都在开拓边疆,突破和探索市场的边界。同时,互联网产业的蓬勃发展也给其他行业,包括新能源汽车、新消费等,带来了用户增长和运营的经验###"}
|
||||
{"prompt": "互联网大厂的制度和分工如何?\n\n###\n\n", "completion": " 目前的互联网大厂的制度和分工非常明确,基本是按照一些标准SOP做事。同时,由于已经没有用户增量了,内卷和向上管理的问题也比较严重。###"}
|
||||
{"prompt": "高潜公司对于哪些毕业生是很好的选择\n\n###\n\n", "completion": " 对于追求长期创造价值而非想找份安稳的工作的毕业生来说,高潜公司是很好的选择。###"}
|
||||
{"prompt": "互联网大厂的晋升方式有哪些?\n\n###\n\n", "completion": " 互联网大厂的晋升方式有两种:老板特别喜欢你或者你的战功支撑你的竞争###"}
|
||||
{"prompt": "为什么目前更多优秀的人都会加入创业公司?\n\n###\n\n", "completion": " 目前社会的大趋势是更多优秀的人都会加入创业公司,因为真正在创造价值的其实永远是创业公司###"}
|
||||
{"prompt": "为什么选择高潜公司主要是因为我们迎来了什么三个繁荣?\n\n###\n\n", "completion": " 选择高潜公司主要是因为我们迎来了创新生态的三个繁荣:人才繁荣、资本繁荣和环境繁荣###"}
|
||||
{"prompt": "资本繁荣是如何推动创业生态的崛起的?\n\n###\n\n", "completion": " 资本繁荣,无论中国还是美国,创业生态的崛起都是伴随着移动互联网的发展。中国这一代VC的崛起,主要是通过投资移动互联网项目所积累的战绩扩大了资金池,通过这些成本低、增长快、回报率高的项目,才有了底气去推动更多行业发展,尝试着投资toB和硬科技这类成本高、增长慢、回报率低、风险大的项目。###"}
|
||||
{"prompt": "环境繁荣是指什么?\n\n###\n\n", "completion": " 环境繁荣是指互联网带动起经济发展后,各地政府也开始了对于创业进行培育,免费场地、巨额无偿补贴、各地送钱竞赛,大幅度降低了创业成本。###"}
|
||||
{"prompt": "高潜公司对于追求长期创造价值的毕业生来说为什么是好的选择?\n\n###\n\n", "completion": " 因为在高潜公司,毕业生可以通过参与创业公司的发展,获得更多的机会和成长空间,而不是被固定在某个职位上。此外,高潜公司通常会注重团队建设和文化建设,能够提供更好的工作环境和发展机会。###"}
|
||||
{"prompt": "如果想要寻找高潜公司,有哪些渠道可以利用?\n\n###\n\n", "completion": " 可以利用领英职场app和知潜微信小程序。领英职场app中入驻了很多高潜公司及高管,而知潜小程序现在也已经有100+高潜力创业公司入驻,都是经过筛选的、top VC投资过且赛道有前景的公司。此外,红杉和真格等VC机构自己的公众号也会经常披露投资的企业。###"}
|
||||
{"prompt": "为什么现在更多优秀的人才会加入创业公司?\n\n###\n\n", "completion": " 因为在当前社会的大趋势下,创业公司成为了创新生态的重要组成部分。创业公司在创造价值和推动行业发展方面具有更大的空间和潜力,而且随着移动互联网的发展,创业公司的崛起也得到了资本和政策的支持。因此,更多优秀的人才选择加入创业公司,以实现自己的创业梦想和获得更好的发展机会。###"}
|
||||
{"prompt": "高潜公司的优势在哪些方面?\n\n###\n\n", "completion": " 高潜公司在团队建设、文化建设和发展机会等方面具有优势。高潜公司通常会注重团队建设和文化建设,能够提供更好的工作环境和发展机会。同时,高潜公司的发展潜力和创新能力也很强,可以为毕业生提供更多的机会和成长空间。###"}
|
||||
@@ -1,21 +0,0 @@
|
||||
{"prompt":"什么是死锁?如何避免死锁?\n\n###\n\n","completion":" 死锁是指两个或多个进程在执行过程中,由于互相等待对方释放资源而造成的一种僵局。避免死锁的常用方法包括:破坏互斥条件、破坏请求和保持条件、破坏不剥夺条件、破坏循环等待条件。###"}
|
||||
{"prompt":"什么是虚拟内存?\n\n###\n\n","completion":" 虚拟内存是指一种计算机内存管理技术,它使得操作系统可以将进程所使用的虚拟地址空间映射到物理内存中,从而可以让多个进程共享物理内存。当物理内存不足时,操作系统会将一部分不常用的内存页面暂时存储到硬盘上,这样就可以释放出更多的物理内存供其他进程使用。###"}
|
||||
{"prompt":"什么是分页和分段?有什么区别?\n\n###\n\n","completion":" 分页和分段是虚拟内存管理中常用的两种技术。分页是将虚拟地址空间划分为固定大小的页面,而分段则是将虚拟地址空间划分为不同大小的段。区别在于,分页的页面大小是固定的,而分段的段大小是可变的;另外,分页是为了实现虚拟内存,而分段则是为了实现逻辑地址空间的管理。###"}
|
||||
{"prompt":"什么是lru算法?\n\n###\n\n","completion":" lru算法是一种页面置换算法,它的全称是least recently used,即最近最少使用算法。lru算法的基本思想是在物理内存中维护一个页面访问历史记录,当需要替换一页时,选择最近最少被访问的页面进行替换。###"}
|
||||
{"prompt":"什么是文件系统?\n\n###\n\n","completion":" 文件系统是一种用于管理计算机存储设备上文件和目录的组织方式。文件系统通常包括存储管理、目录管理、文件访问控制、文件系统维护等功能。常见的文件系统包括fat、ntfs、ext等。###"}
|
||||
{"prompt":"什么是并发和并行?\n\n###\n\n","completion":" 并发是指多个任务交替执行的情况,而并行是指多个任务同时执行的情况。并发的实现方式是通过时间分片技术,而并行的实现方式是通过多核处理器等硬件设备。###"}
|
||||
{"prompt":"什么是系统调用?\n\n###\n\n","completion":" 系统调用是操作系统中的一种机制,它允许用户空间程序请求操作系统内核提供一些服务,如读写文件、创建进程等。系统调用通常是通过软中断实现的###"}
|
||||
{"prompt":"什么是进程间通信?\n\n###\n\n","completion":" 进程间通信是指不同进程之间进行数据交换和通信的机制,常见的方式有管道、共享内存、消息队列、信号量等。###"}
|
||||
{"prompt":"什么是文件描述符?\n\n###\n\n","completion":" 文件描述符是一个非负整数,它是操作系统内核为了管理打开的文件而维护的一种抽象概念。应用程序使用文件描述符来访问文件或其他i\/o设备###"}
|
||||
{"prompt":"什么是中断? \n\n###\n\n","completion":" 中断是指计算机硬件或软件在执行程序时,暂停当前程序的执行,转而去执行另一个程序或处理器所需的其他任务的过程。中断可以是外部中断、内部中断或软中断。###"}
|
||||
{"prompt":"什么是页表?\n\n###\n\n","completion":" 页表是一种数据结构,它用于将虚拟地址转换为物理地址。在虚拟内存中,每个进程都有自己的页表,它描述了虚拟地址空间中每个页面对应的物理地址。 ###"}
|
||||
{"prompt":"什么是僵尸进程?\n\n###\n\n","completion":" 僵尸进程是指一个已经完成执行但其父进程还没有对其进行善后处理的进程,它会一直占用系统资源,直到其父进程将其处理为止。###"}
|
||||
{"prompt":"什么是守护进程? \n\n###\n\n","completion":" 守护进程是一种在后台运行的特殊进程,它通常用于提供某种服务或执行某种系统任务。守护进程通常不与任何用户界面交互,也不会终止,直到操作系统关机或重启。###"}
|
||||
{"prompt":"计算机操作系统是什么?\n\n###\n\n","completion":" 计算机操作系统是一种控制计算机硬件和协调应用程序运行的系统软件。###"}
|
||||
{"prompt":"什么是进程?\n\n###\n\n","completion":" 进程是一个正在执行中的程序实例,它包括了程序计数器、堆栈、数据区等信息。###"}
|
||||
{"prompt":"什么是线程?\n\n###\n\n","completion":" 线程是操作系统能够进行运算调度的最小单位,它是进程的一个执行流程,共享进程的地址空间。###"}
|
||||
{"prompt":"什么是虚拟内存?\n\n###\n\n","completion":" 虚拟内存是一种计算机系统内存管理技术,它使得应用程序认为它拥有连续的可用的内存,而实际上它通常是被分成多个物理内存碎片。###"}
|
||||
{"prompt":"什么是死锁?\n\n###\n\n","completion":" 死锁是指两个或多个进程在执行过程中,因争夺资源而造成的一种互相等待的现象,导致所有进程都无法继续执行。###"}
|
||||
{"prompt":"什么是缓存?\n\n###\n\n","completion":" 缓存是指在计算机中暂存数据的高速存储器,它可以提高计算机对数据的访问速度,避免频繁地访问较慢的主存储器。###"}
|
||||
{"prompt":"什么是文件系统?\n\n###\n\n","completion":" 文件系统是计算机中用来管理和组织文件的一种机制,它通过一系列的数据结构来描述文件和目录的组织方式,以及文件如何存储和访问。###"}
|
||||
{"prompt":"什么是调度算法?\n\n###\n\n","completion":" 调度算法是指操作系统中用来决定进程或线程在cpu上执行顺序的一种算法,它的目标是最大化系统吞吐量、最小化响应时间或最大化资源利用率等。###"}
|
||||
16
src/api/openapi.ts
Normal file
16
src/api/openapi.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { GET, POST, DELETE } from './request';
|
||||
import { UserOpenApiKey } from '@/types/openapi';
|
||||
/**
|
||||
* crete a api key
|
||||
*/
|
||||
export const createAOpenApiKey = () => POST<string>('/openapi/postKey');
|
||||
|
||||
/**
|
||||
* get api keys
|
||||
*/
|
||||
export const getOpenApiKeys = () => GET<UserOpenApiKey[]>('/openapi/getKeys');
|
||||
|
||||
/**
|
||||
* delete api by id
|
||||
*/
|
||||
export const delOpenApiById = (id: string) => DELETE(`/openapi/delKet?id=${id}`);
|
||||
1
src/components/Icon/icons/board.svg
Normal file
1
src/components/Icon/icons/board.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="1680878351566" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1173" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M896 771.413333h-768c-51.2 0-93.866667-42.666667-93.866667-93.866666V209.92c0-51.2 42.666667-93.866667 93.866667-93.866667h768c51.2 0 93.866667 42.666667 93.866667 93.866667v465.92c0 52.906667-42.666667 95.573333-93.866667 95.573333zM128 167.253333C104.106667 167.253333 85.333333 186.026667 85.333333 209.92v465.92c0 23.893333 18.773333 42.666667 42.666667 42.666667h768c23.893333 0 42.666667-18.773333 42.666667-42.666667V209.92c0-23.893333-18.773333-42.666667-42.666667-42.666667h-768z" p-id="1174"></path><path d="M512 907.946667c-13.653333 0-25.6-11.946667-25.6-25.6v-136.533334c0-13.653333 11.946667-25.6 25.6-25.6s25.6 11.946667 25.6 25.6v136.533334c0 13.653333-11.946667 25.6-25.6 25.6z" p-id="1175"></path><path d="M680.96 907.946667H343.04c-13.653333 0-25.6-11.946667-25.6-25.6s11.946667-25.6 25.6-25.6h337.92c13.653333 0 25.6 11.946667 25.6 25.6s-11.946667 25.6-25.6 25.6zM776.533333 648.533333h-529.066666c-13.653333 0-25.6-11.946667-25.6-25.6s11.946667-25.6 25.6-25.6h530.773333c13.653333 0 25.6 11.946667 25.6 25.6s-11.946667 25.6-27.306667 25.6z" p-id="1176"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
1
src/components/Icon/icons/develop.svg
Normal file
1
src/components/Icon/icons/develop.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="1680878410563" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2745" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M256 512l81.6 108.8a32 32 0 0 1-51.2 38.4l-96-128a31.968 31.968 0 0 1 0-38.4l96-128a32 32 0 0 1 51.2 38.4L256 512zM670.4 620.8a32 32 0 0 0 51.2 38.4l96-128a31.968 31.968 0 0 0 0-38.4l-96-128a32 32 0 0 0-51.2 38.4L752 512l-81.6 108.8zM503.232 646.944a32 32 0 1 1-62.464-13.888l64-288a32 32 0 1 1 62.464 13.888l-64 288z" p-id="2746"></path><path d="M160 144a32 32 0 0 0-32 32V864a32 32 0 0 0 32 32h688a32 32 0 0 0 32-32V176a32 32 0 0 0-32-32H160z m0-64h688a96 96 0 0 1 96 96V864a96 96 0 0 1-96 96H160a96 96 0 0 1-96-96V176a96 96 0 0 1 96-96z" p-id="2747"></path></svg>
|
||||
|
After Width: | Height: | Size: 897 B |
1
src/components/Icon/icons/user.svg
Normal file
1
src/components/Icon/icons/user.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="1680878383832" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1637" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M511.333 63.333c-247.424 0-448 200.576-448 448s200.576 448 448 448 448-200.576 448-448-200.576-448-448-448z m0 832c-51.868 0-102.15-10.144-149.451-30.15-36.011-15.231-69.123-35.67-98.812-60.897 12.177-31.985 42.226-63.875 84.223-88.903C396.189 686.243 456.222 669.53 512 669.53c55.631 0 115.416 16.658 164.026 45.703 41.762 24.953 71.689 56.812 83.863 88.804-29.764 25.342-62.976 45.865-99.106 61.146-47.299 20.006-97.582 30.15-149.45 30.15z m296.268-139.658c-20.493-35.937-54.353-68.855-98.747-95.381C649.75 624.979 579.839 605.53 512 605.53c-67.964 0-138.094 19.488-197.471 54.875-44.644 26.606-78.656 59.594-99.195 95.586-23.835-28.755-43.234-60.652-57.85-95.208-20.006-47.3-30.15-97.583-30.15-149.451s10.144-102.15 30.15-149.451c19.337-45.719 47.034-86.792 82.321-122.078 35.286-35.287 76.359-62.983 122.078-82.321 47.3-20.006 97.583-30.15 149.451-30.15 51.868 0 102.15 10.144 149.451 30.15 45.719 19.337 86.792 47.034 122.078 82.321 35.287 35.286 62.983 76.359 82.321 122.078 20.006 47.3 30.15 97.583 30.15 149.451s-10.144 102.15-30.15 149.451c-14.563 34.429-33.869 66.22-57.583 94.892z" p-id="1638"></path><path d="M512 220.223c-88.224 0-160 71.776-160 160s71.776 160 160 160c88.225 0 160-71.775 160-160s-71.775-160-160-160z m0 256c-52.935 0-96-43.065-96-96s43.065-96 96-96 96 43.065 96 96-43.065 96-96 96z" p-id="1639"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import type { IconProps } from '@chakra-ui/react';
|
||||
import { Icon } from '@chakra-ui/react';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const map = {
|
||||
model: require('./icons/model.svg').default,
|
||||
@@ -10,7 +9,11 @@ const map = {
|
||||
menu: require('./icons/menu.svg').default,
|
||||
pay: require('./icons/pay.svg').default,
|
||||
copy: require('./icons/copy.svg').default,
|
||||
chatSend: require('./icons/chatSend.svg').default
|
||||
chatSend: require('./icons/chatSend.svg').default,
|
||||
board: require('./icons/board.svg').default,
|
||||
develop: require('./icons/develop.svg').default,
|
||||
user: require('./icons/user.svg').default,
|
||||
chatting: require('./icons/chatting.svg').default
|
||||
};
|
||||
|
||||
export type IconName = keyof typeof map;
|
||||
|
||||
@@ -16,27 +16,27 @@ const unShowLayoutRoute: { [key: string]: boolean } = {
|
||||
const navbarList = [
|
||||
{
|
||||
label: '介绍',
|
||||
icon: 'icon-gongzuotai-01',
|
||||
icon: 'board',
|
||||
link: '/',
|
||||
activeLink: ['/']
|
||||
},
|
||||
{
|
||||
label: '模型',
|
||||
icon: 'icon-moxing',
|
||||
icon: 'model',
|
||||
link: '/model/list',
|
||||
activeLink: ['/model/list', '/model/detail']
|
||||
},
|
||||
// {
|
||||
// label: '数据',
|
||||
// icon: 'icon-datafull',
|
||||
// link: '/data/list',
|
||||
// activeLink: ['/data/list', '/data/detail']
|
||||
// },
|
||||
{
|
||||
label: '账号',
|
||||
icon: 'icon-yonghu-yuan',
|
||||
icon: 'user',
|
||||
link: '/number/setting',
|
||||
activeLink: ['/number/setting']
|
||||
},
|
||||
{
|
||||
label: '开发',
|
||||
icon: 'develop',
|
||||
link: '/openapi',
|
||||
activeLink: ['/openapi']
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -2,8 +2,7 @@ import React from 'react';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import Icon from '../Iconfont';
|
||||
|
||||
import MyIcon from '../Icon';
|
||||
export enum NavbarTypeEnum {
|
||||
normal = 'normal',
|
||||
small = 'small'
|
||||
@@ -66,20 +65,16 @@ const Navbar = ({
|
||||
backgroundColor: 'transparent'
|
||||
})}
|
||||
>
|
||||
<Icon
|
||||
name={item.icon}
|
||||
width={24}
|
||||
height={24}
|
||||
color={item.activeLink.includes(router.pathname) ? '#2B6CB0' : '#4A5568'}
|
||||
<MyIcon
|
||||
name={item.icon as any}
|
||||
width={'24px'}
|
||||
height={'24px'}
|
||||
fill={item.activeLink.includes(router.pathname) ? '#2B6CB0' : '#4A5568'}
|
||||
/>
|
||||
<Box mt={1}>{item.label}</Box>
|
||||
</Flex>
|
||||
))}
|
||||
</Box>
|
||||
{/* 通知 icon */}
|
||||
{/* <Flex className={styles.informIcon} mb={5} justifyContent={'center'}>
|
||||
<Icon name={'icon-tongzhi'} width={28} height={28} color={'#718096'}></Icon>
|
||||
</Flex> */}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import Icon from '../Iconfont';
|
||||
import MyIcon from '../Icon';
|
||||
import {
|
||||
Flex,
|
||||
Drawer,
|
||||
@@ -39,9 +39,8 @@ const NavbarPhone = ({
|
||||
px={7}
|
||||
>
|
||||
<Box onClick={onOpen}>
|
||||
<Icon name="icon-caidan" width={20} height={20}></Icon>
|
||||
<MyIcon name="menu" width={'20px'} height={'20px'} color={'blackAlpha.600'}></MyIcon>
|
||||
</Box>
|
||||
{/* <Icon name="icon-tongzhi" width={20} height={20}></Icon> */}
|
||||
</Flex>
|
||||
<Drawer isOpen={isOpen} placement="left" size={'xs'} onClose={onClose}>
|
||||
<DrawerOverlay />
|
||||
@@ -74,11 +73,11 @@ const NavbarPhone = ({
|
||||
backgroundColor: 'transparent'
|
||||
})}
|
||||
>
|
||||
<Icon
|
||||
name={item.icon}
|
||||
width={24}
|
||||
height={24}
|
||||
color={item.activeLink.includes(router.pathname) ? '#2B6CB0' : '#4A5568'}
|
||||
<MyIcon
|
||||
name={item.icon as any}
|
||||
width={'24px'}
|
||||
height={'24px'}
|
||||
fill={item.activeLink.includes(router.pathname) ? '#2B6CB0' : '#4A5568'}
|
||||
/>
|
||||
<Box ml={5}>{item.label}</Box>
|
||||
</Flex>
|
||||
|
||||
@@ -43,7 +43,7 @@ wx号: YNyiqi
|
||||
| --- | --- |
|
||||
| chatgpt - 对话 | 0.03 |
|
||||
| 知识库 - 对话 | 0.03 |
|
||||
| 知识库 - 索引 | 0.01 |
|
||||
| 知识库 - 索引 | 0.004 |
|
||||
| 文件拆分 | 0.03 |
|
||||
`;
|
||||
|
||||
@@ -60,10 +60,9 @@ export const chatProblem = `
|
||||
`;
|
||||
|
||||
export const versionIntro = `
|
||||
## Fast GPT V2.3
|
||||
* 数据集导出功能,可用于知识库分享。
|
||||
* 优化文件拆分功能,可自定义提示词。
|
||||
* 定制知识库:创建模型时可以选择【知识库】模型, 可以手动导入知识点或者直接导入一个文件自动学习。
|
||||
## Fast GPT V2.5
|
||||
* 内容压缩,替换中文标点符号和多余符号,减少一些上下文tokens。
|
||||
* 优化 QA 拆分记账。
|
||||
`;
|
||||
|
||||
export const shareHint = `
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import { useState, useCallback, useMemo, useEffect } from 'react';
|
||||
import type { PagingData } from '../types/index';
|
||||
import { IconButton, Flex, Box } from '@chakra-ui/react';
|
||||
import { IconButton, Flex, Box, Input } from '@chakra-ui/react';
|
||||
import { ArrowBackIcon, ArrowForwardIcon } from '@chakra-ui/icons';
|
||||
import { useQuery, useMutation } from '@tanstack/react-query';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useToast } from './useToast';
|
||||
|
||||
export const usePagination = <T = any,>({
|
||||
@@ -40,10 +40,10 @@ export const usePagination = <T = any,>({
|
||||
}
|
||||
}
|
||||
});
|
||||
useQuery(['init'], () => {
|
||||
|
||||
useEffect(() => {
|
||||
mutate(1);
|
||||
return null;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const Pagination = useCallback(() => {
|
||||
return (
|
||||
@@ -53,16 +53,40 @@ export const usePagination = <T = any,>({
|
||||
icon={<ArrowBackIcon />}
|
||||
aria-label={'left'}
|
||||
size={'sm'}
|
||||
w={'28px'}
|
||||
h={'28px'}
|
||||
onClick={() => mutate(pageNum - 1)}
|
||||
/>
|
||||
<Box mx={2}>
|
||||
{pageNum}/{maxPage}
|
||||
</Box>
|
||||
<Flex mx={2} alignItems={'center'}>
|
||||
<Input
|
||||
defaultValue={pageNum}
|
||||
w={'50px'}
|
||||
size={'xs'}
|
||||
type={'number'}
|
||||
min={1}
|
||||
max={maxPage}
|
||||
onBlur={(e) => {
|
||||
const val = +e.target.value;
|
||||
if (val === pageNum) return;
|
||||
if (val >= maxPage) {
|
||||
mutate(maxPage);
|
||||
} else if (val < 1) {
|
||||
mutate(1);
|
||||
} else {
|
||||
mutate(+e.target.value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Box mx={2}>/</Box>
|
||||
{maxPage}
|
||||
</Flex>
|
||||
<IconButton
|
||||
isDisabled={pageNum === maxPage}
|
||||
icon={<ArrowForwardIcon />}
|
||||
aria-label={'left'}
|
||||
size={'sm'}
|
||||
w={'28px'}
|
||||
h={'28px'}
|
||||
onClick={() => mutate(pageNum + 1)}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import type { PagingData } from '../types/index';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useToast } from './useToast';
|
||||
|
||||
export const usePaging = <T = any>({
|
||||
@@ -64,7 +63,9 @@ export const usePaging = <T = any>({
|
||||
getData(pageNum + 1);
|
||||
}, [getData, isLoadAll, pageNum, requesting]);
|
||||
|
||||
useQuery(['init'], () => getData(1, true));
|
||||
useEffect(() => {
|
||||
getData(1, true);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
pageNum,
|
||||
|
||||
@@ -51,7 +51,6 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||
/>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<Script src="/js/iconfont.js" strategy="afterInteractive"></Script>
|
||||
<Script src="/js/qrcode.min.js" strategy="afterInteractive"></Script>
|
||||
<Script src="/js/pdf.js" strategy="afterInteractive"></Script>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { createParser, ParsedEvent, ReconnectInterval } from 'eventsource-parser';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { getOpenAIApi, authChat } from '@/service/utils/chat';
|
||||
import { httpsAgent, openaiChatFilter } from '@/service/utils/tools';
|
||||
@@ -10,6 +9,7 @@ import type { ModelSchema } from '@/types/mongoSchema';
|
||||
import { PassThrough } from 'stream';
|
||||
import { modelList } from '@/constants/model';
|
||||
import { pushChatBill } from '@/service/events/pushBill';
|
||||
import { gpt35StreamResponse } from '@/service/utils/openai';
|
||||
|
||||
/* 发送提示词 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
@@ -102,51 +102,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
|
||||
console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);
|
||||
|
||||
// 创建响应流
|
||||
res.setHeader('Content-Type', 'text/event-stream;charset-utf-8');
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('X-Accel-Buffering', 'no');
|
||||
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
||||
step = 1;
|
||||
|
||||
let responseContent = '';
|
||||
stream.pipe(res);
|
||||
|
||||
const onParse = async (event: ParsedEvent | ReconnectInterval) => {
|
||||
if (event.type !== 'event') return;
|
||||
const data = event.data;
|
||||
if (data === '[DONE]') return;
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
const content: string = json?.choices?.[0].delta.content || '';
|
||||
if (!content || (responseContent === '' && content === '\n')) return;
|
||||
|
||||
responseContent += content;
|
||||
// console.log('content:', content)
|
||||
!stream.destroyed && stream.push(content.replace(/\n/g, '<br/>'));
|
||||
} catch (error) {
|
||||
error;
|
||||
}
|
||||
};
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
try {
|
||||
for await (const chunk of chatResponse.data as any) {
|
||||
if (stream.destroyed) {
|
||||
// 流被中断了,直接忽略后面的内容
|
||||
break;
|
||||
}
|
||||
const parser = createParser(onParse);
|
||||
parser.feed(decoder.decode(chunk));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('pipe error', error);
|
||||
}
|
||||
// close stream
|
||||
!stream.destroyed && stream.push(null);
|
||||
stream.destroy();
|
||||
|
||||
const { responseContent } = await gpt35StreamResponse({
|
||||
res,
|
||||
stream,
|
||||
chatResponse
|
||||
});
|
||||
const promptsContent = formatPrompts.map((item) => item.content).join('');
|
||||
|
||||
// 只有使用平台的 key 才计费
|
||||
pushChatBill({
|
||||
isPay: !userApiKey,
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { createParser, ParsedEvent, ReconnectInterval } from 'eventsource-parser';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { getOpenAIApi, authChat } from '@/service/utils/chat';
|
||||
import { httpsAgent } from '@/service/utils/tools';
|
||||
import { ChatItemType } from '@/types/chat';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import type { ModelSchema } from '@/types/mongoSchema';
|
||||
import { PassThrough } from 'stream';
|
||||
import { modelList } from '@/constants/model';
|
||||
import { pushChatBill } from '@/service/events/pushBill';
|
||||
|
||||
/* 发送提示词 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
let step = 0; // step=1时,表示开始了流响应
|
||||
const stream = new PassThrough();
|
||||
stream.on('error', () => {
|
||||
console.log('error: ', 'stream error');
|
||||
stream.destroy();
|
||||
});
|
||||
res.on('close', () => {
|
||||
stream.destroy();
|
||||
});
|
||||
res.on('error', () => {
|
||||
console.log('error: ', 'request error');
|
||||
stream.destroy();
|
||||
});
|
||||
|
||||
try {
|
||||
const { chatId, prompt } = req.body as {
|
||||
prompt: ChatItemType;
|
||||
chatId: string;
|
||||
};
|
||||
const { authorization } = req.headers;
|
||||
if (!chatId || !prompt) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
const { chat, userApiKey, systemKey, userId } = await authChat(chatId, authorization);
|
||||
|
||||
const model: ModelSchema = chat.modelId;
|
||||
|
||||
// 读取对话内容
|
||||
const prompts = [...chat.content, prompt];
|
||||
|
||||
// 上下文长度过滤
|
||||
const maxContext = model.security.contextMaxLen;
|
||||
const filterPrompts =
|
||||
prompts.length > maxContext ? prompts.slice(prompts.length - maxContext) : prompts;
|
||||
|
||||
// 格式化文本内容
|
||||
const formatPrompts: string[] = filterPrompts.map((item: ChatItemType) => item.value);
|
||||
// 如果有系统提示词,自动插入
|
||||
if (model.systemPrompt) {
|
||||
formatPrompts.unshift(`${model.systemPrompt}`);
|
||||
}
|
||||
|
||||
const promptText = formatPrompts.join('</s>');
|
||||
|
||||
// 计算温度
|
||||
const modelConstantsData = modelList.find((item) => item.model === model.service.modelName);
|
||||
if (!modelConstantsData) {
|
||||
throw new Error('模型异常,请用 chatgpt 模型');
|
||||
}
|
||||
const temperature = modelConstantsData.maxTemperature * (model.temperature / 10);
|
||||
|
||||
// 获取 chatAPI
|
||||
const chatAPI = getOpenAIApi(userApiKey || systemKey);
|
||||
let startTime = Date.now();
|
||||
// console.log({
|
||||
// model: model.service.chatModel,
|
||||
// temperature: temperature,
|
||||
// prompt: promptText,
|
||||
// stream: true,
|
||||
// max_tokens:
|
||||
// model.trainingTimes > 0 ? modelConstantsData.trainedMaxToken : modelConstantsData.maxToken,
|
||||
// presence_penalty: -0.5, // 越大,越容易出现新内容
|
||||
// frequency_penalty: 0.5, // 越大,重复内容越少
|
||||
// stop: [`###`]
|
||||
// });
|
||||
// 发出请求
|
||||
const chatResponse = await chatAPI.createCompletion(
|
||||
{
|
||||
model: model.service.chatModel,
|
||||
temperature: temperature,
|
||||
prompt: promptText,
|
||||
stream: true,
|
||||
max_tokens: modelConstantsData.maxToken,
|
||||
presence_penalty: -0.5, // 越大,越容易出现新内容
|
||||
frequency_penalty: 0.5, // 越大,重复内容越少
|
||||
stop: [`###`, '。!?.!.']
|
||||
},
|
||||
{
|
||||
timeout: 40000,
|
||||
responseType: 'stream',
|
||||
httpsAgent
|
||||
}
|
||||
);
|
||||
|
||||
console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);
|
||||
|
||||
// 创建响应流
|
||||
res.setHeader('Content-Type', 'text/event-stream;charset-utf-8');
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('X-Accel-Buffering', 'no');
|
||||
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
||||
step = 1;
|
||||
|
||||
let responseContent = '';
|
||||
stream.pipe(res);
|
||||
|
||||
const onParse = async (event: ParsedEvent | ReconnectInterval) => {
|
||||
if (event.type !== 'event') return;
|
||||
const data = event.data;
|
||||
if (data === '[DONE]') return;
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
const content: string = json?.choices?.[0].text || '';
|
||||
// console.log('content:', content);
|
||||
if (!content || (responseContent === '' && content === '\n')) return;
|
||||
|
||||
responseContent += content;
|
||||
!stream.destroyed && stream.push(content.replace(/\n/g, '<br/>'));
|
||||
} catch (error) {
|
||||
error;
|
||||
}
|
||||
};
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
try {
|
||||
for await (const chunk of chatResponse.data as any) {
|
||||
if (stream.destroyed) {
|
||||
// 流被中断了,直接忽略后面的内容
|
||||
break;
|
||||
}
|
||||
const parser = createParser(onParse);
|
||||
parser.feed(decoder.decode(chunk));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('pipe error', error);
|
||||
}
|
||||
// close stream
|
||||
!stream.destroyed && stream.push(null);
|
||||
stream.destroy();
|
||||
|
||||
// 只有使用平台的 key 才计费
|
||||
pushChatBill({
|
||||
isPay: !userApiKey,
|
||||
modelName: model.service.modelName,
|
||||
userId,
|
||||
chatId,
|
||||
text: promptText + responseContent
|
||||
});
|
||||
} catch (err: any) {
|
||||
// console.log(err?.response);
|
||||
if (step === 1) {
|
||||
// 直接结束流
|
||||
console.log('error,结束');
|
||||
stream.destroy();
|
||||
} else {
|
||||
res.status(500);
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { createParser, ParsedEvent, ReconnectInterval } from 'eventsource-parser';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { getOpenAIApi, authChat } from '@/service/utils/chat';
|
||||
import { httpsAgent, openaiChatFilter, systemPromptFilter } from '@/service/utils/tools';
|
||||
import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai';
|
||||
import { ChatItemType } from '@/types/chat';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import type { ModelSchema } from '@/types/mongoSchema';
|
||||
import { PassThrough } from 'stream';
|
||||
import { modelList } from '@/constants/model';
|
||||
import { pushChatBill } from '@/service/events/pushBill';
|
||||
import { connectRedis } from '@/service/redis';
|
||||
import { VecModelDataPrefix } from '@/constants/redis';
|
||||
import { vectorToBuffer } from '@/utils/tools';
|
||||
import { openaiCreateEmbedding } from '@/service/utils/openai';
|
||||
|
||||
/* 发送提示词 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
let step = 0; // step=1时,表示开始了流响应
|
||||
const stream = new PassThrough();
|
||||
stream.on('error', () => {
|
||||
console.log('error: ', 'stream error');
|
||||
stream.destroy();
|
||||
});
|
||||
res.on('close', () => {
|
||||
stream.destroy();
|
||||
});
|
||||
res.on('error', () => {
|
||||
console.log('error: ', 'request error');
|
||||
stream.destroy();
|
||||
});
|
||||
|
||||
try {
|
||||
const { chatId, prompt } = req.body as {
|
||||
prompt: ChatItemType;
|
||||
chatId: string;
|
||||
};
|
||||
|
||||
const { authorization } = req.headers;
|
||||
if (!chatId || !prompt) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
await connectToDatabase();
|
||||
const redis = await connectRedis();
|
||||
let startTime = Date.now();
|
||||
|
||||
const { chat, userApiKey, systemKey, userId } = await authChat(chatId, authorization);
|
||||
|
||||
const model: ModelSchema = chat.modelId;
|
||||
const modelConstantsData = modelList.find((item) => item.model === model.service.modelName);
|
||||
if (!modelConstantsData) {
|
||||
throw new Error('模型加载异常');
|
||||
}
|
||||
// 获取 chatAPI
|
||||
const chatAPI = getOpenAIApi(userApiKey || systemKey);
|
||||
|
||||
// 请求一次 chatgpt 拆解需求
|
||||
const promptResponse = await chatAPI.createChatCompletion(
|
||||
{
|
||||
model: model.service.chatModel,
|
||||
temperature: 0,
|
||||
// max_tokens: modelConstantsData.maxToken,
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: `服务端逻辑生成器。根据用户输入的需求,拆解成代码实现的步骤,并按格式返回: 1.\n2.\n3.\n ......
|
||||
|
||||
下面是一些例子:
|
||||
实现一个手机号注册账号的方法,包含两个函数
|
||||
* 发送手机验证码函数:
|
||||
1. 从 query 中获取 phone
|
||||
2. 校验手机号格式是否正确,不正确返回{error: "手机号格式错误"}
|
||||
3. 给 phone 发送一个短信验证码,验证码长度为6位字符串,内容为:你正在注册laf, 验证码为:code
|
||||
4. 数据库添加数据,表为"codes",内容为 {phone, code}
|
||||
* 注册函数
|
||||
1. 从 body 中获取 phone 和 code
|
||||
2. 校验手机号格式是否正确,不正确返回{error: "手机号格式错误"}
|
||||
2. 获取数据库数据,表为"codes",查找是否有符合 phone, code 等于body参数的记录,没有的话返回 {error:"验证码不正确"}
|
||||
4. 添加数据库数据,表为"users" ,内容为{phone, code, createTime}
|
||||
5. 删除数据库数据,删除 code 记录
|
||||
---------------
|
||||
更新博客记录。传入blogId,blogText,tags,还需要记录更新的时间
|
||||
1. 从 body 中获取 blogId,blogText 和 tags
|
||||
2. 校验 blogId 是否为空,为空则返回 {error: "博客ID不能为空"}
|
||||
3. 校验 blogText 是否为空,为空则返回 {error: "博客内容不能为空"}
|
||||
4. 校验 tags 是否为数组,不是则返回 {error: "标签必须为数组"}
|
||||
5. 获取当前时间,记录为 updateTime
|
||||
6. 更新数据库数据,表为"blogs",更新符合 blogId 的记录的内容为{blogText, tags, updateTime}
|
||||
7. 返回结果 {message: "更新博客记录成功"}`
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt.value
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
timeout: 40000,
|
||||
httpsAgent
|
||||
}
|
||||
);
|
||||
|
||||
const promptResolve = promptResponse.data.choices?.[0]?.message?.content || '';
|
||||
if (!promptResolve) {
|
||||
throw new Error('gpt 异常');
|
||||
}
|
||||
|
||||
prompt.value += `\n${promptResolve}`;
|
||||
console.log('prompt resolve success, time:', `${(Date.now() - startTime) / 1000}s`);
|
||||
|
||||
// 获取提示词的向量
|
||||
const { vector: promptVector } = await openaiCreateEmbedding({
|
||||
isPay: !userApiKey,
|
||||
apiKey: userApiKey || systemKey,
|
||||
userId,
|
||||
text: prompt.value
|
||||
});
|
||||
|
||||
// 读取对话内容
|
||||
const prompts = [...chat.content, prompt];
|
||||
|
||||
// 搜索系统提示词, 按相似度从 redis 中搜出相关的 q 和 text
|
||||
const redisData: any[] = await redis.sendCommand([
|
||||
'FT.SEARCH',
|
||||
`idx:${VecModelDataPrefix}:hash`,
|
||||
`@modelId:{${String(
|
||||
chat.modelId._id
|
||||
)}} @vector:[VECTOR_RANGE 0.25 $blob]=>{$YIELD_DISTANCE_AS: score}`,
|
||||
// `@modelId:{${String(chat.modelId._id)}}=>[KNN 10 @vector $blob AS score]`,
|
||||
'RETURN',
|
||||
'1',
|
||||
'text',
|
||||
'SORTBY',
|
||||
'score',
|
||||
'PARAMS',
|
||||
'2',
|
||||
'blob',
|
||||
vectorToBuffer(promptVector),
|
||||
'LIMIT',
|
||||
'0',
|
||||
'20',
|
||||
'DIALECT',
|
||||
'2'
|
||||
]);
|
||||
|
||||
// 格式化响应值,获取 qa
|
||||
const formatRedisPrompt = [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
|
||||
.map((i) => {
|
||||
if (!redisData[i]) return '';
|
||||
const text = (redisData[i][1] as string) || '';
|
||||
|
||||
if (!text) return '';
|
||||
|
||||
return text;
|
||||
})
|
||||
.filter((item) => item);
|
||||
|
||||
if (formatRedisPrompt.length === 0) {
|
||||
throw new Error('对不起,我没有找到你的问题');
|
||||
}
|
||||
|
||||
// textArr 筛选,最多 3000 tokens
|
||||
const systemPrompt = systemPromptFilter(formatRedisPrompt, 3400);
|
||||
|
||||
prompts.unshift({
|
||||
obj: 'SYSTEM',
|
||||
value: `${model.systemPrompt} 知识库内容是最新的,知识库内容为: "${systemPrompt}"`
|
||||
});
|
||||
|
||||
// 控制在 tokens 数量,防止超出
|
||||
const filterPrompts = openaiChatFilter(prompts, modelConstantsData.contextMaxToken);
|
||||
|
||||
// 格式化文本内容成 chatgpt 格式
|
||||
const map = {
|
||||
Human: ChatCompletionRequestMessageRoleEnum.User,
|
||||
AI: ChatCompletionRequestMessageRoleEnum.Assistant,
|
||||
SYSTEM: ChatCompletionRequestMessageRoleEnum.System
|
||||
};
|
||||
const formatPrompts: ChatCompletionRequestMessage[] = filterPrompts.map(
|
||||
(item: ChatItemType) => ({
|
||||
role: map[item.obj],
|
||||
content: item.value
|
||||
})
|
||||
);
|
||||
console.log(formatPrompts);
|
||||
// 计算温度
|
||||
const temperature = modelConstantsData.maxTemperature * (model.temperature / 10);
|
||||
|
||||
// 发出请求
|
||||
const chatResponse = await chatAPI.createChatCompletion(
|
||||
{
|
||||
model: model.service.chatModel,
|
||||
temperature: temperature,
|
||||
// max_tokens: modelConstantsData.maxToken,
|
||||
messages: formatPrompts,
|
||||
frequency_penalty: 0.5, // 越大,重复内容越少
|
||||
presence_penalty: -0.5, // 越大,越容易出现新内容
|
||||
stream: true
|
||||
},
|
||||
{
|
||||
timeout: 40000,
|
||||
responseType: 'stream',
|
||||
httpsAgent
|
||||
}
|
||||
);
|
||||
|
||||
console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);
|
||||
|
||||
// 创建响应流
|
||||
res.setHeader('Content-Type', 'text/event-stream;charset-utf-8');
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('X-Accel-Buffering', 'no');
|
||||
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
||||
step = 1;
|
||||
|
||||
let responseContent = '';
|
||||
stream.pipe(res);
|
||||
|
||||
const onParse = async (event: ParsedEvent | ReconnectInterval) => {
|
||||
if (event.type !== 'event') return;
|
||||
const data = event.data;
|
||||
if (data === '[DONE]') return;
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
const content: string = json?.choices?.[0].delta.content || '';
|
||||
if (!content || (responseContent === '' && content === '\n')) return;
|
||||
|
||||
responseContent += content;
|
||||
// console.log('content:', content)
|
||||
!stream.destroyed && stream.push(content.replace(/\n/g, '<br/>'));
|
||||
} catch (error) {
|
||||
error;
|
||||
}
|
||||
};
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
try {
|
||||
for await (const chunk of chatResponse.data as any) {
|
||||
if (stream.destroyed) {
|
||||
// 流被中断了,直接忽略后面的内容
|
||||
break;
|
||||
}
|
||||
const parser = createParser(onParse);
|
||||
parser.feed(decoder.decode(chunk));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('pipe error', error);
|
||||
}
|
||||
// close stream
|
||||
!stream.destroyed && stream.push(null);
|
||||
stream.destroy();
|
||||
|
||||
const promptsContent = formatPrompts.map((item) => item.content).join('');
|
||||
// 只有使用平台的 key 才计费
|
||||
pushChatBill({
|
||||
isPay: !userApiKey,
|
||||
modelName: model.service.modelName,
|
||||
userId,
|
||||
chatId,
|
||||
text: promptsContent + responseContent
|
||||
});
|
||||
} catch (err: any) {
|
||||
if (step === 1) {
|
||||
// 直接结束流
|
||||
console.log('error,结束');
|
||||
stream.destroy();
|
||||
} else {
|
||||
res.status(500);
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { createParser, ParsedEvent, ReconnectInterval } from 'eventsource-parser';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { getOpenAIApi, authChat } from '@/service/utils/chat';
|
||||
import { authChat } from '@/service/utils/chat';
|
||||
import { httpsAgent, openaiChatFilter, systemPromptFilter } from '@/service/utils/tools';
|
||||
import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai';
|
||||
import { ChatItemType } from '@/types/chat';
|
||||
@@ -13,7 +12,7 @@ import { pushChatBill } from '@/service/events/pushBill';
|
||||
import { connectRedis } from '@/service/redis';
|
||||
import { VecModelDataPrefix } from '@/constants/redis';
|
||||
import { vectorToBuffer } from '@/utils/tools';
|
||||
import { openaiCreateEmbedding } from '@/service/utils/openai';
|
||||
import { openaiCreateEmbedding, gpt35StreamResponse } from '@/service/utils/openai';
|
||||
|
||||
/* 发送提示词 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
@@ -71,8 +70,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
`idx:${VecModelDataPrefix}:hash`,
|
||||
`@modelId:{${String(
|
||||
chat.modelId._id
|
||||
)}} @vector:[VECTOR_RANGE 0.22 $blob]=>{$YIELD_DISTANCE_AS: score}`,
|
||||
// `@modelId:{${String(chat.modelId._id)}}=>[KNN 10 @vector $blob AS score]`,
|
||||
)}} @vector:[VECTOR_RANGE 0.24 $blob]=>{$YIELD_DISTANCE_AS: score}`,
|
||||
'RETURN',
|
||||
'1',
|
||||
'text',
|
||||
@@ -89,28 +87,25 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
'2'
|
||||
]);
|
||||
|
||||
const formatRedisPrompt: string[] = [];
|
||||
// 格式化响应值,获取 qa
|
||||
const formatRedisPrompt = [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
|
||||
.map((i) => {
|
||||
if (!redisData[i]) return '';
|
||||
const text = (redisData[i][1] as string) || '';
|
||||
|
||||
if (!text) return '';
|
||||
|
||||
return text;
|
||||
})
|
||||
.filter((item) => item);
|
||||
for (let i = 2; i < 42; i += 2) {
|
||||
const text = redisData[i]?.[1];
|
||||
if (text) {
|
||||
formatRedisPrompt.push(text);
|
||||
}
|
||||
}
|
||||
|
||||
if (formatRedisPrompt.length === 0) {
|
||||
throw new Error('对不起,我没有找到你的问题');
|
||||
}
|
||||
|
||||
// textArr 筛选,最多 3000 tokens
|
||||
const systemPrompt = systemPromptFilter(formatRedisPrompt, 3400);
|
||||
// textArr 筛选,最多 2800 tokens
|
||||
const systemPrompt = systemPromptFilter(formatRedisPrompt, 2800);
|
||||
|
||||
prompts.unshift({
|
||||
obj: 'SYSTEM',
|
||||
value: `${model.systemPrompt} 知识库内容是最新的,知识库内容为: "${systemPrompt}"`
|
||||
value: `${model.systemPrompt} 知识库内容是最新的,知识库内容为: "${systemPrompt}"`
|
||||
});
|
||||
|
||||
// 控制在 tokens 数量,防止超出
|
||||
@@ -152,49 +147,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
|
||||
console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);
|
||||
|
||||
// 创建响应流
|
||||
res.setHeader('Content-Type', 'text/event-stream;charset-utf-8');
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('X-Accel-Buffering', 'no');
|
||||
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
||||
step = 1;
|
||||
|
||||
let responseContent = '';
|
||||
stream.pipe(res);
|
||||
|
||||
const onParse = async (event: ParsedEvent | ReconnectInterval) => {
|
||||
if (event.type !== 'event') return;
|
||||
const data = event.data;
|
||||
if (data === '[DONE]') return;
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
const content: string = json?.choices?.[0].delta.content || '';
|
||||
if (!content || (responseContent === '' && content === '\n')) return;
|
||||
|
||||
responseContent += content;
|
||||
// console.log('content:', content)
|
||||
!stream.destroyed && stream.push(content.replace(/\n/g, '<br/>'));
|
||||
} catch (error) {
|
||||
error;
|
||||
}
|
||||
};
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
try {
|
||||
for await (const chunk of chatResponse.data as any) {
|
||||
if (stream.destroyed) {
|
||||
// 流被中断了,直接忽略后面的内容
|
||||
break;
|
||||
}
|
||||
const parser = createParser(onParse);
|
||||
parser.feed(decoder.decode(chunk));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('pipe error', error);
|
||||
}
|
||||
// close stream
|
||||
!stream.destroyed && stream.push(null);
|
||||
stream.destroy();
|
||||
const { responseContent } = await gpt35StreamResponse({
|
||||
res,
|
||||
stream,
|
||||
chatResponse
|
||||
});
|
||||
|
||||
const promptsContent = formatPrompts.map((item) => item.content).join('');
|
||||
// 只有使用平台的 key 才计费
|
||||
|
||||
@@ -32,7 +32,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
VecModelDataIdx,
|
||||
`@modelId:{${modelId}} @userId:{${userId}}`,
|
||||
{
|
||||
RETURN: ['q', 'text', 'rawVector'],
|
||||
RETURN: ['q', 'text'],
|
||||
LIMIT: {
|
||||
from: 0,
|
||||
size: 10000
|
||||
@@ -40,21 +40,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
}
|
||||
);
|
||||
|
||||
const data = searchRes.documents
|
||||
.filter((item) => {
|
||||
if (!item?.value?.rawVector) return false;
|
||||
try {
|
||||
JSON.parse(item.value.rawVector as string);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.map((item: any) => ({
|
||||
prompt: item.value.q,
|
||||
completion: item.value.text,
|
||||
vector: JSON.parse(item.value.rawVector)
|
||||
}));
|
||||
const data = searchRes.documents.map((item: any) => ({
|
||||
prompt: item.value.q,
|
||||
completion: item.value.text
|
||||
}));
|
||||
|
||||
jsonRes(res, {
|
||||
data: JSON.stringify(data)
|
||||
|
||||
@@ -53,9 +53,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
userId,
|
||||
'modelId',
|
||||
String(modelId),
|
||||
...(vector
|
||||
? ['vector', vectorToBuffer(formatVector(vector)), 'rawVector', JSON.stringify(vector)]
|
||||
: []),
|
||||
...(vector ? ['vector', vectorToBuffer(formatVector(vector))] : []),
|
||||
'q',
|
||||
item.prompt,
|
||||
'text',
|
||||
|
||||
@@ -76,3 +76,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: {
|
||||
sizeLimit: '10mb'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
250
src/pages/api/openapi/chat/lafGpt.ts
Normal file
250
src/pages/api/openapi/chat/lafGpt.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { connectToDatabase, Model } from '@/service/mongo';
|
||||
import { getOpenAIApi } from '@/service/utils/chat';
|
||||
import { authOpenApiKey } from '@/service/utils/tools';
|
||||
import { httpsAgent, openaiChatFilter, systemPromptFilter } from '@/service/utils/tools';
|
||||
import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai';
|
||||
import { ChatItemType } from '@/types/chat';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { PassThrough } from 'stream';
|
||||
import { ChatModelNameEnum, modelList, ChatModelNameMap } from '@/constants/model';
|
||||
import { pushChatBill } from '@/service/events/pushBill';
|
||||
import { connectRedis } from '@/service/redis';
|
||||
import { VecModelDataPrefix } from '@/constants/redis';
|
||||
import { vectorToBuffer } from '@/utils/tools';
|
||||
import { openaiCreateEmbedding, gpt35StreamResponse } from '@/service/utils/openai';
|
||||
|
||||
/* 发送提示词 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
let step = 0; // step=1时,表示开始了流响应
|
||||
const stream = new PassThrough();
|
||||
stream.on('error', () => {
|
||||
console.log('error: ', 'stream error');
|
||||
stream.destroy();
|
||||
});
|
||||
res.on('close', () => {
|
||||
stream.destroy();
|
||||
});
|
||||
res.on('error', () => {
|
||||
console.log('error: ', 'request error');
|
||||
stream.destroy();
|
||||
});
|
||||
|
||||
try {
|
||||
const {
|
||||
prompt,
|
||||
modelId,
|
||||
isStream = true
|
||||
} = req.body as {
|
||||
prompt: ChatItemType;
|
||||
modelId: string;
|
||||
isStream: boolean;
|
||||
};
|
||||
|
||||
if (!prompt || !modelId) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
await connectToDatabase();
|
||||
const redis = await connectRedis();
|
||||
let startTime = Date.now();
|
||||
|
||||
/* 凭证校验 */
|
||||
const { apiKey, userId } = await authOpenApiKey(req);
|
||||
|
||||
/* 查找数据库里的模型信息 */
|
||||
const model = await Model.findById(modelId);
|
||||
if (!model) {
|
||||
throw new Error('找不到模型');
|
||||
}
|
||||
|
||||
const modelConstantsData = modelList.find(
|
||||
(item) => item.model === ChatModelNameEnum.VECTOR_GPT
|
||||
);
|
||||
if (!modelConstantsData) {
|
||||
throw new Error('模型已下架');
|
||||
}
|
||||
console.log('laf gpt start');
|
||||
|
||||
// 获取 chatAPI
|
||||
const chatAPI = getOpenAIApi(apiKey);
|
||||
|
||||
// 请求一次 chatgpt 拆解需求
|
||||
const promptResponse = await chatAPI.createChatCompletion(
|
||||
{
|
||||
model: ChatModelNameMap[ChatModelNameEnum.GPT35],
|
||||
temperature: 0,
|
||||
frequency_penalty: 0.5, // 越大,重复内容越少
|
||||
presence_penalty: -0.5, // 越大,越容易出现新内容
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: `服务端逻辑生成器.根据用户输入的需求,拆解成代码实现的步骤,并按格式返回: 1.\n2.\n3.\n ......
|
||||
下面是一些例子:
|
||||
实现一个手机号发生注册验证码方法.
|
||||
1. 从 query 中获取 phone.
|
||||
2. 校验手机号格式是否正确,不正确返回{error: "手机号格式错误"}.
|
||||
3. 给 phone 发送一个短信验证码,验证码长度为6位字符串,内容为:你正在注册laf,验证码为:code.
|
||||
4. 数据库添加数据,表为"codes",内容为 {phone, code}.
|
||||
|
||||
实现根据手机号注册账号,需要验证手机验证码.
|
||||
1. 从 body 中获取 phone 和 code.
|
||||
2. 校验手机号格式是否正确,不正确返回{error: "手机号格式错误"}.
|
||||
2. 获取数据库数据,表为"codes",查找是否有符合 phone, code 等于body参数的记录,没有的话返回 {error:"验证码不正确"}.
|
||||
4. 添加数据库数据,表为"users" ,内容为{phone, code, createTime}.
|
||||
5. 删除数据库数据,删除 code 记录.
|
||||
|
||||
更新博客记录。传入blogId,blogText,tags,还需要记录更新的时间.
|
||||
1. 从 body 中获取 blogId,blogText 和 tags.
|
||||
2. 校验 blogId 是否为空,为空则返回 {error: "博客ID不能为空"}.
|
||||
3. 校验 blogText 是否为空,为空则返回 {error: "博客内容不能为空"}.
|
||||
4. 校验 tags 是否为数组,不是则返回 {error: "标签必须为数组"}.
|
||||
5. 获取当前时间,记录为 updateTime.
|
||||
6. 更新数据库数据,表为"blogs",更新符合 blogId 的记录的内容为{blogText, tags, updateTime}.
|
||||
7. 返回结果 {message: "更新博客记录成功"}.`
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt.value
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
timeout: 120000,
|
||||
httpsAgent
|
||||
}
|
||||
);
|
||||
|
||||
const promptResolve = promptResponse.data.choices?.[0]?.message?.content || '';
|
||||
if (!promptResolve) {
|
||||
throw new Error('gpt 异常');
|
||||
}
|
||||
|
||||
prompt.value += ` ${promptResolve}`;
|
||||
console.log('prompt resolve success, time:', `${(Date.now() - startTime) / 1000}s`);
|
||||
|
||||
// 获取提示词的向量
|
||||
const { vector: promptVector } = await openaiCreateEmbedding({
|
||||
isPay: true,
|
||||
apiKey: apiKey,
|
||||
userId,
|
||||
text: prompt.value
|
||||
});
|
||||
|
||||
// 读取对话内容
|
||||
const prompts = [prompt];
|
||||
|
||||
// 搜索系统提示词, 按相似度从 redis 中搜出相关的 q 和 text
|
||||
const redisData: any[] = await redis.sendCommand([
|
||||
'FT.SEARCH',
|
||||
`idx:${VecModelDataPrefix}:hash`,
|
||||
`@modelId:{${String(model._id)}}=>[KNN 20 @vector $blob AS score]`,
|
||||
'RETURN',
|
||||
'1',
|
||||
'text',
|
||||
'SORTBY',
|
||||
'score',
|
||||
'PARAMS',
|
||||
'2',
|
||||
'blob',
|
||||
vectorToBuffer(promptVector),
|
||||
'DIALECT',
|
||||
'2'
|
||||
]);
|
||||
|
||||
// 格式化响应值,获取 qa
|
||||
const formatRedisPrompt: string[] = [];
|
||||
for (let i = 2; i < 42; i += 2) {
|
||||
const text = redisData[i]?.[1];
|
||||
if (text) {
|
||||
formatRedisPrompt.push(text);
|
||||
}
|
||||
}
|
||||
|
||||
// textArr 筛选,最多 3200 tokens
|
||||
const systemPrompt = systemPromptFilter(formatRedisPrompt, 3200);
|
||||
|
||||
prompts.unshift({
|
||||
obj: 'SYSTEM',
|
||||
value: `${model.systemPrompt} 知识库内容是最新的,知识库内容为: "${systemPrompt}"`
|
||||
});
|
||||
|
||||
// 控制在 tokens 数量,防止超出
|
||||
const filterPrompts = openaiChatFilter(prompts, modelConstantsData.contextMaxToken);
|
||||
|
||||
// 格式化文本内容成 chatgpt 格式
|
||||
const map = {
|
||||
Human: ChatCompletionRequestMessageRoleEnum.User,
|
||||
AI: ChatCompletionRequestMessageRoleEnum.Assistant,
|
||||
SYSTEM: ChatCompletionRequestMessageRoleEnum.System
|
||||
};
|
||||
const formatPrompts: ChatCompletionRequestMessage[] = filterPrompts.map(
|
||||
(item: ChatItemType) => ({
|
||||
role: map[item.obj],
|
||||
content: item.value
|
||||
})
|
||||
);
|
||||
// console.log(formatPrompts);
|
||||
// 计算温度
|
||||
const temperature = modelConstantsData.maxTemperature * (model.temperature / 10);
|
||||
|
||||
// 发出请求
|
||||
const chatResponse = await chatAPI.createChatCompletion(
|
||||
{
|
||||
model: model.service.chatModel,
|
||||
temperature,
|
||||
messages: formatPrompts,
|
||||
frequency_penalty: 0.5, // 越大,重复内容越少
|
||||
presence_penalty: -0.5, // 越大,越容易出现新内容
|
||||
stream: isStream
|
||||
},
|
||||
{
|
||||
timeout: 120000,
|
||||
responseType: isStream ? 'stream' : 'json',
|
||||
httpsAgent
|
||||
}
|
||||
);
|
||||
|
||||
console.log('code response. time:', `${(Date.now() - startTime) / 1000}s`);
|
||||
|
||||
step = 1;
|
||||
let responseContent = '';
|
||||
|
||||
if (isStream) {
|
||||
const streamResponse = await gpt35StreamResponse({
|
||||
res,
|
||||
stream,
|
||||
chatResponse
|
||||
});
|
||||
responseContent = streamResponse.responseContent;
|
||||
} else {
|
||||
responseContent = chatResponse.data.choices?.[0]?.message?.content || '';
|
||||
jsonRes(res, {
|
||||
data: responseContent
|
||||
});
|
||||
}
|
||||
|
||||
console.log('laf gpt done. time:', `${(Date.now() - startTime) / 1000}s`);
|
||||
|
||||
const promptsContent = formatPrompts.map((item) => item.content).join('');
|
||||
|
||||
pushChatBill({
|
||||
isPay: true,
|
||||
modelName: model.service.modelName,
|
||||
userId,
|
||||
text: promptsContent + responseContent
|
||||
});
|
||||
} catch (err: any) {
|
||||
if (step === 1) {
|
||||
// 直接结束流
|
||||
console.log('error,结束');
|
||||
stream.destroy();
|
||||
} else {
|
||||
res.status(500);
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/pages/api/openapi/delKet.ts
Normal file
33
src/pages/api/openapi/delKet.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, OpenApi } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { id } = req.query as { id: string };
|
||||
const { authorization } = req.headers;
|
||||
|
||||
if (!authorization) {
|
||||
throw new Error('缺少登录凭证');
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
await OpenApi.findOneAndRemove({ _id: id, userId });
|
||||
|
||||
jsonRes(res);
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
43
src/pages/api/openapi/getKeys.ts
Normal file
43
src/pages/api/openapi/getKeys.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, OpenApi } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import { UserOpenApiKey } from '@/types/openapi';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { authorization } = req.headers;
|
||||
|
||||
if (!authorization) {
|
||||
throw new Error('缺少登录凭证');
|
||||
}
|
||||
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
const findResponse = await OpenApi.find({ userId }).sort({ _id: -1 });
|
||||
|
||||
// jus save four data
|
||||
const apiKeys = findResponse.map<UserOpenApiKey>(
|
||||
({ _id, apiKey, createTime, lastUsedTime }) => {
|
||||
return {
|
||||
id: _id,
|
||||
apiKey: `${apiKey.substring(0, 2)}******${apiKey.substring(apiKey.length - 2)}`,
|
||||
createTime,
|
||||
lastUsedTime
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
jsonRes(res, {
|
||||
data: apiKeys
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
43
src/pages/api/openapi/postKey.ts
Normal file
43
src/pages/api/openapi/postKey.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, OpenApi } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890');
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { authorization } = req.headers;
|
||||
|
||||
if (!authorization) {
|
||||
throw new Error('缺少登录凭证');
|
||||
}
|
||||
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
const count = await OpenApi.find({ userId }).countDocuments();
|
||||
|
||||
if (count >= 5) {
|
||||
throw new Error('最多 5 组API Key');
|
||||
}
|
||||
|
||||
const apiKey = `${userId}-${nanoid()}`;
|
||||
|
||||
await OpenApi.create({
|
||||
userId,
|
||||
apiKey
|
||||
});
|
||||
|
||||
jsonRes(res, {
|
||||
data: apiKey
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { AuthCode } from '@/service/models/authCode';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error('不是开发环境');
|
||||
}
|
||||
try {
|
||||
await connectToDatabase();
|
||||
|
||||
const authCode = await AuthCode.deleteMany({
|
||||
expiredTime: { $lt: Date.now() }
|
||||
});
|
||||
|
||||
jsonRes(res, {
|
||||
message: `删除了${authCode.deletedCount}条记录`
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, Chat } from '@/service/mongo';
|
||||
|
||||
/* 定时删除那些不活跃的内容 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error('不是开发环境');
|
||||
}
|
||||
try {
|
||||
await connectToDatabase();
|
||||
|
||||
const response = await Chat.deleteMany(
|
||||
{ $expr: { $lt: [{ $size: '$content' }, 5] } },
|
||||
// 使用 $pull 操作符删除数组中的元素
|
||||
{ $pull: { content: { $exists: true } } }
|
||||
);
|
||||
|
||||
jsonRes(res, {
|
||||
message: `删除了${response.deletedCount}条记录`
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, Bill } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import type { BillSchema } from '@/types/mongoSchema';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error('不是开发环境');
|
||||
}
|
||||
await connectToDatabase();
|
||||
|
||||
const bills = await Bill.find({
|
||||
tokenLen: { $exists: false }
|
||||
});
|
||||
await Promise.all(
|
||||
bills.map((bill) =>
|
||||
Bill.findByIdAndUpdate(bill._id, {
|
||||
tokenLen: bill.textLen
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
jsonRes(res, {
|
||||
data: {}
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, SplitData } from '@/service/mongo';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error('不是开发环境');
|
||||
}
|
||||
await connectToDatabase();
|
||||
|
||||
const data = await SplitData.aggregate([
|
||||
{ $match: { textList: { $exists: true, $ne: [] } } },
|
||||
{ $sample: { size: 1 } }
|
||||
]);
|
||||
|
||||
const dataItem: any = data[0];
|
||||
const textList: string[] = dataItem.textList.slice(-5);
|
||||
console.log(textList);
|
||||
console.log(dataItem.textList.slice(0, -5));
|
||||
await SplitData.findByIdAndUpdate(dataItem._id, {
|
||||
textList: dataItem.textList.slice(0, -5)
|
||||
});
|
||||
|
||||
jsonRes(res, {
|
||||
data: {}
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, Training, Model } from '@/service/mongo';
|
||||
import type { TrainingItemType } from '@/types/training';
|
||||
import { TrainingStatusEnum, ModelStatusEnum } from '@/constants/model';
|
||||
import { getUserApiOpenai } from '@/service/utils/openai';
|
||||
import { OpenAiTuneStatusEnum } from '@/service/constants/training';
|
||||
import { sendTrainSucceed } from '@/service/utils/sendEmail';
|
||||
import { httpsAgent } from '@/service/utils/tools';
|
||||
import { ModelPopulate } from '@/types/mongoSchema';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error('不是开发环境');
|
||||
}
|
||||
try {
|
||||
await connectToDatabase();
|
||||
|
||||
// 查询正在训练中的训练记录
|
||||
const trainingRecords: TrainingItemType[] = await Training.find({
|
||||
status: TrainingStatusEnum.pending
|
||||
});
|
||||
|
||||
const { openai } = await getUserApiOpenai('63f9a14228d2a688d8dc9e1b');
|
||||
|
||||
const response = await Promise.all(
|
||||
trainingRecords.map(async (item) => {
|
||||
const { data } = await openai.retrieveFineTune(item.tuneId, { httpsAgent });
|
||||
if (data.status === OpenAiTuneStatusEnum.succeeded) {
|
||||
// 删除训练文件
|
||||
openai.deleteFile(data.training_files[0].id, { httpsAgent });
|
||||
|
||||
const model = await Model.findById<ModelPopulate>(item.modelId).populate({
|
||||
path: 'userId',
|
||||
options: {
|
||||
strictPopulate: false
|
||||
}
|
||||
});
|
||||
|
||||
if (!model) {
|
||||
throw new Error('模型不存在');
|
||||
}
|
||||
|
||||
// 更新模型
|
||||
await Model.findByIdAndUpdate(item.modelId, {
|
||||
status: ModelStatusEnum.running,
|
||||
updateTime: new Date(),
|
||||
service: {
|
||||
...model.service,
|
||||
trainId: data.fine_tuned_model, // 训练完后,再次训练和对话使用的 model 是一样的
|
||||
chatModel: data.fine_tuned_model
|
||||
}
|
||||
});
|
||||
// 更新训练数据
|
||||
await Training.findByIdAndUpdate(item._id, {
|
||||
status: TrainingStatusEnum.succeed
|
||||
});
|
||||
|
||||
// 发送邮件通知
|
||||
await sendTrainSucceed(model.userId.email as string, model.name);
|
||||
return 'succeed';
|
||||
}
|
||||
return 'pending';
|
||||
})
|
||||
);
|
||||
|
||||
jsonRes(res, {
|
||||
data: `${response.length}个训练线程,${
|
||||
response.filter((item) => item === 'succeed').length
|
||||
}个完成`
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import axios from 'axios';
|
||||
import { connectToDatabase, User, Pay } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import { PaySchema } from '@/types/mongoSchema';
|
||||
import dayjs from 'dayjs';
|
||||
import { getPayResult } from '@/service/utils/wxpay';
|
||||
|
||||
/* 校验支付结果 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { authorization } = req.headers;
|
||||
@@ -25,18 +26,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
throw new Error('订单已结算');
|
||||
}
|
||||
|
||||
const { data } = await axios.get(
|
||||
`https://sif268.laf.dev/wechat-order-query?order_number=${payOrder.orderId}&api_key=${process.env.WXPAYCODE}`
|
||||
);
|
||||
const payRes = await getPayResult(payOrder.orderId);
|
||||
|
||||
// 校验下是否超过一天
|
||||
const orderTime = dayjs(payOrder.createTime);
|
||||
const diffInHours = dayjs().diff(orderTime, 'hours');
|
||||
|
||||
if (data.trade_state === 'SUCCESS') {
|
||||
if (payRes.trade_state === 'SUCCESS') {
|
||||
// 订单已支付
|
||||
try {
|
||||
// 更新订单状态
|
||||
// 更新订单状态. 如果没有合适的订单,说明订单重复了
|
||||
const updateRes = await Pay.updateOne(
|
||||
{
|
||||
_id: payId,
|
||||
@@ -61,7 +60,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
});
|
||||
console.log(error);
|
||||
}
|
||||
} else if (data.trade_state === 'CLOSED' || diffInHours > 24) {
|
||||
} else if (payRes.trade_state === 'CLOSED' || diffInHours > 24) {
|
||||
// 订单已关闭
|
||||
await Pay.findByIdAndUpdate(payId, {
|
||||
status: 'CLOSED'
|
||||
@@ -70,7 +69,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
data: '订单已过期'
|
||||
});
|
||||
} else {
|
||||
throw new Error(data.trade_state_desc);
|
||||
throw new Error(payRes?.trade_state_desc || '订单无效');
|
||||
}
|
||||
} catch (err) {
|
||||
// console.log(err);
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import axios from 'axios';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import { connectToDatabase, Pay } from '@/service/mongo';
|
||||
import { PRICE_SCALE } from '@/constants/common';
|
||||
import { nativePay } from '@/service/utils/wxpay';
|
||||
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 20);
|
||||
|
||||
/* 获取支付二维码 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { authorization } = req.headers;
|
||||
@@ -23,15 +23,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const id = nanoid();
|
||||
await connectToDatabase();
|
||||
|
||||
const response = await axios({
|
||||
url: 'https://sif268.laf.dev/wechat-pay',
|
||||
method: 'POST',
|
||||
data: {
|
||||
trade_order_number: id,
|
||||
amount: amount * 100,
|
||||
api_key: process.env.WXPAYCODE
|
||||
}
|
||||
});
|
||||
const code_url = await nativePay(amount * 100, id);
|
||||
|
||||
// 充值记录 + 1
|
||||
const payOrder = await Pay.create({
|
||||
@@ -43,11 +35,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
jsonRes(res, {
|
||||
data: {
|
||||
payId: payOrder._id,
|
||||
codeUrl: response.data?.code_url
|
||||
codeUrl: code_url
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
console.log(err, '==');
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
|
||||
18
src/pages/api/user/payRes.ts
Normal file
18
src/pages/api/user/payRes.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import axios from 'axios';
|
||||
import { connectToDatabase, User, Pay } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import { PaySchema } from '@/types/mongoSchema';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
res.send('');
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import { UserUpdateParams } from '@/types/user';
|
||||
/* 更新一些基本信息 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const { accounts } = req.body as UserUpdateParams;
|
||||
const { openaiKey } = req.body as UserUpdateParams;
|
||||
const { authorization } = req.headers;
|
||||
|
||||
if (!authorization) {
|
||||
@@ -26,8 +26,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
_id: userId
|
||||
},
|
||||
{
|
||||
// 限定字段
|
||||
...(accounts ? { accounts } : {})
|
||||
openaiKey
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -120,7 +120,6 @@ const Chat = ({ chatId }: { chatId: string }) => {
|
||||
const urlMap: Record<string, string> = {
|
||||
[ChatModelNameEnum.GPT35]: '/api/chat/chatGpt',
|
||||
[ChatModelNameEnum.VECTOR_GPT]: '/api/chat/vectorGpt',
|
||||
// [ChatModelNameEnum.VECTOR_GPT]: '/api/chat/lafGpt',
|
||||
[ChatModelNameEnum.GPT3]: '/api/chat/gpt3'
|
||||
};
|
||||
|
||||
@@ -520,11 +519,11 @@ const Chat = ({ chatId }: { chatId: string }) => {
|
||||
onClick={sendPrompt}
|
||||
>
|
||||
{isChatting ? (
|
||||
<Image
|
||||
<Icon
|
||||
style={{ transform: 'translateY(4px)' }}
|
||||
src={'/icon/chatting.svg'}
|
||||
fill
|
||||
alt={''}
|
||||
h={'30px'}
|
||||
w={'30px'}
|
||||
name={'chatting'}
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
|
||||
@@ -35,7 +35,7 @@ const InputDataModal = ({
|
||||
const [importing, setImporting] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
const { register, handleSubmit } = useForm<FormData>({
|
||||
const { register, handleSubmit, reset } = useForm<FormData>({
|
||||
defaultValues
|
||||
});
|
||||
|
||||
@@ -64,34 +64,39 @@ const InputDataModal = ({
|
||||
title: res === 0 ? '导入数据成功,需要一段时间训练' : '数据导入异常',
|
||||
status: res === 0 ? 'success' : 'warning'
|
||||
});
|
||||
onClose();
|
||||
reset({
|
||||
text: '',
|
||||
q: ''
|
||||
});
|
||||
onSuccess();
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
setImporting(false);
|
||||
},
|
||||
[modelId, onClose, onSuccess, toast]
|
||||
[modelId, onSuccess, reset, toast]
|
||||
);
|
||||
|
||||
const updateData = useCallback(
|
||||
async (e: FormData) => {
|
||||
if (!e.dataId) return;
|
||||
if (e.text === defaultValues.text && e.q === defaultValues.q) return;
|
||||
|
||||
await putModelDataById({
|
||||
dataId: e.dataId,
|
||||
text: e.text,
|
||||
q: e.q === defaultValues.q ? '' : e.q
|
||||
});
|
||||
if (e.text !== defaultValues.text || e.q !== defaultValues.q) {
|
||||
await putModelDataById({
|
||||
dataId: e.dataId,
|
||||
text: e.text,
|
||||
q: e.q === defaultValues.q ? '' : e.q
|
||||
});
|
||||
onSuccess();
|
||||
}
|
||||
|
||||
toast({
|
||||
title: '修改回答成功',
|
||||
status: 'success'
|
||||
});
|
||||
onClose();
|
||||
onSuccess();
|
||||
},
|
||||
[defaultValues.q, onClose, onSuccess, toast]
|
||||
[defaultValues, onClose, onSuccess, toast]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -148,8 +148,8 @@ const ModelDataCard = ({ model }: { model: ModelSchema }) => {
|
||||
>
|
||||
手动输入
|
||||
</MenuItem>
|
||||
<MenuItem onClick={onOpenSelectFileModal}>文件导入</MenuItem>
|
||||
<MenuItem onClick={onOpenSelectUrlModal}>网站地址导入</MenuItem>
|
||||
<MenuItem onClick={onOpenSelectFileModal}>文件QA拆分</MenuItem>
|
||||
<MenuItem onClick={onOpenSelectUrlModal}>网站内容QA拆分</MenuItem>
|
||||
<MenuItem onClick={onOpenSelectJsonModal}>JSON导入</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
} from '@chakra-ui/react';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useSelectFile } from '@/hooks/useSelectFile';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import { encode } from 'gpt-token-utils';
|
||||
import { useConfirm } from '@/hooks/useConfirm';
|
||||
import { readTxtContent, readPdfContent, readDocContent } from '@/utils/tools';
|
||||
@@ -22,8 +21,6 @@ import { useMutation } from '@tanstack/react-query';
|
||||
import { postModelDataSplitData } from '@/api/model';
|
||||
import { formatPrice } from '@/utils/user';
|
||||
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 12);
|
||||
|
||||
const fileExtension = '.txt,.doc,.docx,.pdf,.md';
|
||||
|
||||
const SelectFileModal = ({
|
||||
|
||||
@@ -37,7 +37,7 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await getModelById(modelId);
|
||||
console.log(res);
|
||||
// console.log(res);
|
||||
res.security.expiredTime /= 60 * 60 * 1000;
|
||||
setModel(res);
|
||||
formHooks.reset(res);
|
||||
|
||||
57
src/pages/number/components/BillTable.tsx
Normal file
57
src/pages/number/components/BillTable.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
import { Card, Box, Table, Thead, Tbody, Tr, Th, Td, TableContainer } from '@chakra-ui/react';
|
||||
import { BillTypeMap } from '@/constants/user';
|
||||
import { getUserBills } from '@/api/user';
|
||||
import type { UserBillType } from '@/types/user';
|
||||
import { usePagination } from '@/hooks/usePagination';
|
||||
import { useLoading } from '@/hooks/useLoading';
|
||||
|
||||
const BillTable = () => {
|
||||
const { Loading } = useLoading();
|
||||
|
||||
const {
|
||||
data: bills,
|
||||
isLoading,
|
||||
Pagination
|
||||
} = usePagination<UserBillType>({
|
||||
api: getUserBills
|
||||
});
|
||||
|
||||
return (
|
||||
<Card mt={6} py={4}>
|
||||
<Box fontSize={'xl'} fontWeight={'bold'} px={6} mb={1}>
|
||||
使用记录
|
||||
</Box>
|
||||
<TableContainer position={'relative'}>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>时间</Th>
|
||||
<Th>类型</Th>
|
||||
<Th>内容长度</Th>
|
||||
<Th>Tokens 长度</Th>
|
||||
<Th>消费</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody fontSize={'sm'}>
|
||||
{bills.map((item) => (
|
||||
<Tr key={item.id}>
|
||||
<Td>{item.time}</Td>
|
||||
<Td>{BillTypeMap[item.type]}</Td>
|
||||
<Td>{item.textLen}</Td>
|
||||
<Td>{item.tokenLen}</Td>
|
||||
<Td>{item.price}元</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
<Box mt={4} mr={4} textAlign={'end'}>
|
||||
<Pagination />
|
||||
</Box>
|
||||
<Loading loading={isLoading} fixed={false} />
|
||||
</TableContainer>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default BillTable;
|
||||
@@ -10,21 +10,12 @@ import {
|
||||
Button,
|
||||
Input,
|
||||
Box,
|
||||
Grid,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
TableContainer
|
||||
Grid
|
||||
} from '@chakra-ui/react';
|
||||
import { getPayCode, checkPayResult } from '@/api/user';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useRouter } from 'next/router';
|
||||
import { modelList } from '@/constants/model';
|
||||
import { formatPrice } from '../../../utils/user';
|
||||
|
||||
const PayModal = ({ onClose }: { onClose: () => void }) => {
|
||||
const router = useRouter();
|
||||
|
||||
112
src/pages/number/components/PayRecordTable.tsx
Normal file
112
src/pages/number/components/PayRecordTable.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Box,
|
||||
Flex,
|
||||
Button,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
TableContainer,
|
||||
useDisclosure
|
||||
} from '@chakra-ui/react';
|
||||
import { getPayOrders, checkPayResult } from '@/api/user';
|
||||
import { PaySchema } from '@/types/mongoSchema';
|
||||
import dayjs from 'dayjs';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { formatPrice } from '@/utils/user';
|
||||
import WxConcat from '@/components/WxConcat';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
|
||||
const PayRecordTable = () => {
|
||||
const { isOpen: isOpenWx, onOpen: onOpenWx, onClose: onCloseWx } = useDisclosure();
|
||||
const [payOrders, setPayOrders] = useState<PaySchema[]>([]);
|
||||
const { setLoading } = useGlobalStore();
|
||||
const { toast } = useToast();
|
||||
|
||||
const handleRefreshPayOrder = useCallback(
|
||||
async (payId: string) => {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const data = await checkPayResult(payId);
|
||||
toast({
|
||||
title: data,
|
||||
status: 'info'
|
||||
});
|
||||
const res = await getPayOrders();
|
||||
setPayOrders(res);
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: error?.message,
|
||||
status: 'warning'
|
||||
});
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
},
|
||||
[setLoading, toast]
|
||||
);
|
||||
|
||||
useQuery(['initPayOrder'], getPayOrders, {
|
||||
onSuccess(res) {
|
||||
setPayOrders(res);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card mt={6} py={4}>
|
||||
<Flex alignItems={'flex-end'} px={6} mb={1}>
|
||||
<Box fontSize={'xl'} fontWeight={'bold'}>
|
||||
充值记录
|
||||
</Box>
|
||||
<Button onClick={onOpenWx} size={'xs'} ml={4} variant={'outline'}>
|
||||
异常问题,wx联系
|
||||
</Button>
|
||||
</Flex>
|
||||
<TableContainer px={6}>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>订单号</Th>
|
||||
<Th>时间</Th>
|
||||
<Th>金额</Th>
|
||||
<Th>消费</Th>
|
||||
<Th></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody fontSize={'sm'}>
|
||||
{payOrders.map((item) => (
|
||||
<Tr key={item._id}>
|
||||
<Td>{item.orderId}</Td>
|
||||
<Td>
|
||||
{item.createTime ? dayjs(item.createTime).format('YYYY/MM/DD HH:mm:ss') : '-'}
|
||||
</Td>
|
||||
<Td>{formatPrice(item.price)}元</Td>
|
||||
<Td>{item.status}</Td>
|
||||
<Td>
|
||||
{item.status === 'NOTPAY' && (
|
||||
<Button onClick={() => handleRefreshPayOrder(item._id)} size={'sm'}>
|
||||
更新
|
||||
</Button>
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Card>
|
||||
{/* wx 联系 */}
|
||||
{isOpenWx && <WxConcat onClose={onCloseWx} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PayRecordTable;
|
||||
@@ -1,72 +1,32 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Box,
|
||||
Flex,
|
||||
Button,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
TableContainer,
|
||||
Select,
|
||||
Input,
|
||||
IconButton,
|
||||
useDisclosure
|
||||
} from '@chakra-ui/react';
|
||||
import { DeleteIcon } from '@chakra-ui/icons';
|
||||
import { useForm, useFieldArray } from 'react-hook-form';
|
||||
import { Card, Box, Flex, Button, Input } from '@chakra-ui/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { UserUpdateParams } from '@/types/user';
|
||||
import { putUserInfo, getUserBills, getPayOrders, checkPayResult } from '@/api/user';
|
||||
import { putUserInfo } from '@/api/user';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { UserType } from '@/types/user';
|
||||
import { usePaging } from '@/hooks/usePaging';
|
||||
import type { UserBillType } from '@/types/user';
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { PaySchema } from '@/types/mongoSchema';
|
||||
import dayjs from 'dayjs';
|
||||
import { formatPrice } from '@/utils/user';
|
||||
import WxConcat from '@/components/WxConcat';
|
||||
import ScrollData from '@/components/ScrollData';
|
||||
import { BillTypeMap } from '@/constants/user';
|
||||
|
||||
const PayRecordTable = dynamic(() => import('./components/PayRecordTable'));
|
||||
const BilTable = dynamic(() => import('./components/BillTable'));
|
||||
const PayModal = dynamic(() => import('./components/PayModal'));
|
||||
|
||||
const NumberSetting = () => {
|
||||
const { userInfo, updateUserInfo, initUserInfo } = useUserStore();
|
||||
const { setLoading } = useGlobalStore();
|
||||
const { register, handleSubmit, control } = useForm<UserUpdateParams>({
|
||||
const { register, handleSubmit } = useForm<UserUpdateParams>({
|
||||
defaultValues: userInfo as UserType
|
||||
});
|
||||
const [showPay, setShowPay] = useState(false);
|
||||
const { isOpen: isOpenWx, onOpen: onOpenWx, onClose: onCloseWx } = useDisclosure();
|
||||
const { toast } = useToast();
|
||||
const {
|
||||
fields: accounts,
|
||||
append: appendAccount,
|
||||
remove: removeAccount
|
||||
} = useFieldArray({
|
||||
control,
|
||||
name: 'accounts'
|
||||
});
|
||||
const {
|
||||
nextPage,
|
||||
isLoadAll,
|
||||
requesting,
|
||||
data: bills
|
||||
} = usePaging<UserBillType>({
|
||||
api: getUserBills,
|
||||
pageSize: 30
|
||||
});
|
||||
const [payOrders, setPayOrders] = useState<PaySchema[]>([]);
|
||||
|
||||
const onclickSave = useCallback(
|
||||
async (data: UserUpdateParams) => {
|
||||
if (data.openaiKey === userInfo?.openaiKey) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
await putUserInfo(data);
|
||||
@@ -78,54 +38,22 @@ const NumberSetting = () => {
|
||||
} catch (error) {}
|
||||
setLoading(false);
|
||||
},
|
||||
[setLoading, toast, updateUserInfo]
|
||||
[setLoading, toast, updateUserInfo, userInfo?.openaiKey]
|
||||
);
|
||||
|
||||
useQuery(['init'], initUserInfo);
|
||||
|
||||
useQuery(['initPayOrder'], getPayOrders, {
|
||||
onSuccess(res) {
|
||||
setPayOrders(res);
|
||||
}
|
||||
});
|
||||
|
||||
const handleRefreshPayOrder = useCallback(
|
||||
async (payId: string) => {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const data = await checkPayResult(payId);
|
||||
toast({
|
||||
title: data,
|
||||
status: 'info'
|
||||
});
|
||||
const res = await getPayOrders();
|
||||
setPayOrders(res);
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: error?.message,
|
||||
status: 'warning'
|
||||
});
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
},
|
||||
[setLoading, toast]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 核心信息 */}
|
||||
<Card px={6} py={4}>
|
||||
<Box fontSize={'xl'} fontWeight={'bold'}>
|
||||
账号信息
|
||||
</Box>
|
||||
<Box mt={6}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box flex={'0 0 60px'}>邮箱:</Box>
|
||||
<Box>{userInfo?.email}</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
<Flex mt={6} alignItems={'center'}>
|
||||
<Box flex={'0 0 60px'}>邮箱:</Box>
|
||||
<Box>{userInfo?.email}</Box>
|
||||
</Flex>
|
||||
<Box mt={6}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box flex={'0 0 60px'}>余额:</Box>
|
||||
@@ -140,157 +68,28 @@ const NumberSetting = () => {
|
||||
如果填写了自己的 openai 账号,将不会计费
|
||||
</Box>
|
||||
</Box>
|
||||
</Card>
|
||||
<Card mt={6} px={6} py={4}>
|
||||
<Flex mb={5} justifyContent={'space-between'}>
|
||||
<Box fontSize={'xl'} fontWeight={'bold'}>
|
||||
关联账号
|
||||
</Box>
|
||||
<Box>
|
||||
{accounts.length === 0 && (
|
||||
<Button
|
||||
mr={5}
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
appendAccount({
|
||||
type: 'openai',
|
||||
value: ''
|
||||
})
|
||||
}
|
||||
>
|
||||
新增
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={handleSubmit(onclickSave)}>保存</Button>
|
||||
</Box>
|
||||
<Flex mt={6} alignItems={'center'}>
|
||||
<Box flex={'0 0 85px'}>openaiKey:</Box>
|
||||
<Input
|
||||
{...register(`openaiKey`)}
|
||||
maxW={'300px'}
|
||||
placeholder={'openai账号。回车或失去焦点保存'}
|
||||
size={'sm'}
|
||||
onBlur={handleSubmit(onclickSave)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.keyCode === 13) {
|
||||
handleSubmit(onclickSave)();
|
||||
}
|
||||
}}
|
||||
></Input>
|
||||
</Flex>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>账号类型</Th>
|
||||
<Th>值</Th>
|
||||
<Th></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{accounts.map((item, i) => (
|
||||
<Tr key={item.id}>
|
||||
<Td minW={'200px'}>
|
||||
<Select
|
||||
{...register(`accounts.${i}.type`, {
|
||||
required: '类型不能为空'
|
||||
})}
|
||||
>
|
||||
<option value="openai">openai</option>
|
||||
</Select>
|
||||
</Td>
|
||||
<Td minW={'200px'} whiteSpace="pre-wrap" wordBreak={'break-all'}>
|
||||
<Input
|
||||
{...register(`accounts.${i}.value`, {
|
||||
required: '账号不能为空'
|
||||
})}
|
||||
></Input>
|
||||
</Td>
|
||||
<Td>
|
||||
<IconButton
|
||||
aria-label="删除账号"
|
||||
icon={<DeleteIcon />}
|
||||
colorScheme={'red'}
|
||||
onClick={() => {
|
||||
removeAccount(i);
|
||||
handleSubmit(onclickSave)();
|
||||
}}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Card>
|
||||
<Card mt={6} py={4}>
|
||||
<Flex alignItems={'flex-end'} px={6} mb={1}>
|
||||
<Box fontSize={'xl'} fontWeight={'bold'}>
|
||||
充值记录
|
||||
</Box>
|
||||
<Button onClick={onOpenWx} size={'xs'} ml={4} variant={'outline'}>
|
||||
异常问题,wx联系
|
||||
</Button>
|
||||
</Flex>
|
||||
<TableContainer maxH={'400px'} overflowY={'auto'} px={6}>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>订单号</Th>
|
||||
<Th>时间</Th>
|
||||
<Th>金额</Th>
|
||||
<Th>消费</Th>
|
||||
<Th></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody fontSize={'sm'}>
|
||||
{payOrders.map((item) => (
|
||||
<Tr key={item._id}>
|
||||
<Td>{item.orderId}</Td>
|
||||
<Td>
|
||||
{item.createTime ? dayjs(item.createTime).format('YYYY/MM/DD HH:mm:ss') : '-'}
|
||||
</Td>
|
||||
<Td>{formatPrice(item.price)}元</Td>
|
||||
<Td>{item.status}</Td>
|
||||
<Td>
|
||||
{item.status === 'NOTPAY' && (
|
||||
<Button onClick={() => handleRefreshPayOrder(item._id)} size={'sm'}>
|
||||
更新
|
||||
</Button>
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Card>
|
||||
<Card mt={6} py={4}>
|
||||
<Box fontSize={'xl'} fontWeight={'bold'} px={6} mb={1}>
|
||||
使用记录
|
||||
</Box>
|
||||
<ScrollData
|
||||
maxH={'400px'}
|
||||
px={6}
|
||||
isLoadAll={isLoadAll}
|
||||
requesting={requesting}
|
||||
nextPage={nextPage}
|
||||
>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>时间</Th>
|
||||
<Th>类型</Th>
|
||||
<Th>内容长度</Th>
|
||||
<Th>Tokens 长度</Th>
|
||||
<Th>消费</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody fontSize={'sm'}>
|
||||
{bills.map((item) => (
|
||||
<Tr key={item.id}>
|
||||
<Td>{item.time}</Td>
|
||||
<Td>{BillTypeMap[item.type]}</Td>
|
||||
<Td>{item.textLen}</Td>
|
||||
<Td>{item.tokenLen}</Td>
|
||||
<Td>{item.price}元</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</ScrollData>
|
||||
</Card>
|
||||
|
||||
{/* 充值记录 */}
|
||||
<PayRecordTable />
|
||||
{/* 账单表 */}
|
||||
<BilTable />
|
||||
{showPay && <PayModal onClose={() => setShowPay(false)} />}
|
||||
{/* wx 联系 */}
|
||||
{isOpenWx && <WxConcat onClose={onCloseWx} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
125
src/pages/openapi/index.tsx
Normal file
125
src/pages/openapi/index.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Box,
|
||||
Button,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
TableContainer,
|
||||
IconButton,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalCloseButton,
|
||||
ModalBody
|
||||
} 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 { DeleteIcon } from '@chakra-ui/icons';
|
||||
import { useCopyData } from '@/utils/tools';
|
||||
|
||||
const OpenApi = () => {
|
||||
const { Loading } = useLoading();
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
const { mutate: onclickRemove, isLoading: isDeleting } = useMutation({
|
||||
mutationFn: async (id: string) => delOpenApiById(id),
|
||||
onSuccess() {
|
||||
refetch();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card px={6} py={4} position={'relative'}>
|
||||
<Box fontSize={'xl'} fontWeight={'bold'}>
|
||||
Open Api Key
|
||||
</Box>
|
||||
<Box fontSize={'sm'} mt={2}>
|
||||
请注意保管你的 key,不要泄露!
|
||||
</Box>
|
||||
<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={'outline'}
|
||||
colorScheme={'gray'}
|
||||
onClick={() => onclickRemove(id)}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
<Button
|
||||
mt={5}
|
||||
isLoading={isCreating}
|
||||
isDisabled={apiKeys.length >= 5}
|
||||
title={apiKeys.length >= 5 ? '最多五组 Api Key' : ''}
|
||||
onClick={() => onclickCreateApiKey()}
|
||||
>
|
||||
添加新的 Api Key
|
||||
</Button>
|
||||
</TableContainer>
|
||||
<Loading loading={isGetting || isDeleting} fixed={false} />
|
||||
</Card>
|
||||
<Modal isOpen={!!apiKey} onClose={() => setApiKey('')}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Api Key</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody mb={5}>
|
||||
请保管好你的Api Key
|
||||
<Box userSelect={'all'} onClick={() => copyData(apiKey)}>
|
||||
{apiKey}
|
||||
</Box>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default OpenApi;
|
||||
@@ -105,7 +105,8 @@ export async function generateAbstract(next = false): Promise<any> {
|
||||
isPay: !userApiKey && splitContents.length > 0,
|
||||
userId: dataItem.userId,
|
||||
type: 'abstract',
|
||||
text: systemPrompt.content + dataItem.text + rawContent
|
||||
text: systemPrompt.content + dataItem.text + rawContent,
|
||||
tokenLen: 0
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.log('error: 生成摘要错误', dataItem?._id);
|
||||
|
||||
@@ -40,7 +40,8 @@ export async function generateQA(next = false): Promise<any> {
|
||||
const textList: string[] = dataItem.textList.slice(-5);
|
||||
|
||||
// 获取 openapi Key
|
||||
let userApiKey, systemKey;
|
||||
let userApiKey = '',
|
||||
systemKey = '';
|
||||
try {
|
||||
const key = await getOpenApiKey(dataItem.userId);
|
||||
userApiKey = key.userApiKey;
|
||||
@@ -93,10 +94,22 @@ export async function generateQA(next = false): Promise<any> {
|
||||
httpsAgent
|
||||
}
|
||||
)
|
||||
.then((res) => ({
|
||||
rawContent: res?.data.choices[0].message?.content || '', // chatgpt原本的回复
|
||||
result: splitText(res?.data.choices[0].message?.content || '') // 格式化后的QA对
|
||||
}))
|
||||
.then((res) => {
|
||||
const rawContent = res?.data.choices[0].message?.content || ''; // chatgpt 原本的回复
|
||||
const result = splitText(res?.data.choices[0].message?.content || ''); // 格式化后的QA对
|
||||
// 计费
|
||||
pushSplitDataBill({
|
||||
isPay: !userApiKey && result.length > 0,
|
||||
userId: dataItem.userId,
|
||||
type: 'QA',
|
||||
text: systemPrompt.content + text + rawContent,
|
||||
tokenLen: res.data.usage?.total_tokens || 0
|
||||
});
|
||||
return {
|
||||
rawContent,
|
||||
result
|
||||
};
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
@@ -141,17 +154,6 @@ export async function generateQA(next = false): Promise<any> {
|
||||
resultList.length
|
||||
);
|
||||
|
||||
// 计费
|
||||
pushSplitDataBill({
|
||||
isPay: !userApiKey && resultList.length > 0,
|
||||
userId: dataItem.userId,
|
||||
type: 'QA',
|
||||
text:
|
||||
systemPrompt.content +
|
||||
textList.join('') +
|
||||
successResponse.map((item) => item.rawContent).join('')
|
||||
});
|
||||
|
||||
generateQA(true);
|
||||
generateVector();
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -47,7 +47,8 @@ export async function generateVector(next = false): Promise<any> {
|
||||
} catch (error: any) {
|
||||
if (error?.code === 501) {
|
||||
await redis.del(dataItem.id);
|
||||
throw new Error(error?.message);
|
||||
generateVector(true);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error('获取 openai key 失败');
|
||||
@@ -67,8 +68,6 @@ export async function generateVector(next = false): Promise<any> {
|
||||
dataItem.id,
|
||||
'vector',
|
||||
vectorToBuffer(vector),
|
||||
'rawVector',
|
||||
JSON.stringify(vector),
|
||||
'status',
|
||||
ModelDataStatusEnum.ready
|
||||
]);
|
||||
@@ -102,6 +101,6 @@ export async function generateVector(next = false): Promise<any> {
|
||||
}
|
||||
setTimeout(() => {
|
||||
generateVector(true);
|
||||
}, 4000);
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { connectToDatabase, Bill, User } from '../mongo';
|
||||
import { modelList, ChatModelNameEnum } from '@/constants/model';
|
||||
import { encode } from 'gpt-token-utils';
|
||||
import { formatPrice } from '@/utils/user';
|
||||
import { BillTypeEnum } from '@/constants/user';
|
||||
import type { DataType } from '@/types/data';
|
||||
|
||||
@@ -15,16 +14,16 @@ export const pushChatBill = async ({
|
||||
isPay: boolean;
|
||||
modelName: string;
|
||||
userId: string;
|
||||
chatId: string;
|
||||
chatId?: string;
|
||||
text: string;
|
||||
}) => {
|
||||
let billId;
|
||||
|
||||
try {
|
||||
// 计算 token 数量
|
||||
const tokens = encode(text);
|
||||
const tokens = Math.floor(encode(text).length * 0.7);
|
||||
|
||||
console.log(`chat generate success. text len: ${text.length}. token len: ${tokens.length}`);
|
||||
console.log(`chat generate success. text len: ${text.length}. token len: ${tokens}`);
|
||||
|
||||
if (isPay) {
|
||||
await connectToDatabase();
|
||||
@@ -33,8 +32,7 @@ export const pushChatBill = async ({
|
||||
const modelItem = modelList.find((item) => item.model === modelName);
|
||||
// 计算价格
|
||||
const unitPrice = modelItem?.price || 5;
|
||||
const price = unitPrice * tokens.length;
|
||||
console.log(`unit price: ${unitPrice}, price: ${formatPrice(price)}元`);
|
||||
const price = unitPrice * tokens;
|
||||
|
||||
try {
|
||||
// 插入 Bill 记录
|
||||
@@ -44,7 +42,7 @@ export const pushChatBill = async ({
|
||||
modelName,
|
||||
chatId,
|
||||
textLen: text.length,
|
||||
tokenLen: tokens.length,
|
||||
tokenLen: tokens,
|
||||
price
|
||||
});
|
||||
billId = res._id;
|
||||
@@ -66,11 +64,13 @@ export const pushChatBill = async ({
|
||||
export const pushSplitDataBill = async ({
|
||||
isPay,
|
||||
userId,
|
||||
tokenLen,
|
||||
text,
|
||||
type
|
||||
}: {
|
||||
isPay: boolean;
|
||||
userId: string;
|
||||
tokenLen: number;
|
||||
text: string;
|
||||
type: DataType;
|
||||
}) => {
|
||||
@@ -79,12 +79,7 @@ export const pushSplitDataBill = async ({
|
||||
let billId;
|
||||
|
||||
try {
|
||||
// 计算 token 数量
|
||||
const tokens = encode(text);
|
||||
|
||||
console.log(
|
||||
`splitData generate success. text len: ${text.length}. token len: ${tokens.length}`
|
||||
);
|
||||
console.log(`splitData generate success. text len: ${text.length}. token len: ${tokenLen}`);
|
||||
|
||||
if (isPay) {
|
||||
try {
|
||||
@@ -92,9 +87,7 @@ export const pushSplitDataBill = async ({
|
||||
const modelItem = modelList.find((item) => item.model === ChatModelNameEnum.GPT35);
|
||||
const unitPrice = modelItem?.price || 3;
|
||||
// 计算价格
|
||||
const price = unitPrice * tokens.length;
|
||||
|
||||
console.log(`price: ${formatPrice(price)}元`);
|
||||
const price = unitPrice * tokenLen;
|
||||
|
||||
// 插入 Bill 记录
|
||||
const res = await Bill.create({
|
||||
@@ -102,7 +95,7 @@ export const pushSplitDataBill = async ({
|
||||
type,
|
||||
modelName: ChatModelNameEnum.GPT35,
|
||||
textLen: text.length,
|
||||
tokenLen: tokens.length,
|
||||
tokenLen,
|
||||
price
|
||||
});
|
||||
billId = res._id;
|
||||
@@ -124,29 +117,27 @@ export const pushSplitDataBill = async ({
|
||||
export const pushGenerateVectorBill = async ({
|
||||
isPay,
|
||||
userId,
|
||||
text
|
||||
text,
|
||||
tokenLen
|
||||
}: {
|
||||
isPay: boolean;
|
||||
userId: string;
|
||||
text: string;
|
||||
tokenLen: number;
|
||||
}) => {
|
||||
await connectToDatabase();
|
||||
|
||||
let billId;
|
||||
|
||||
try {
|
||||
// 计算 token 数量
|
||||
const tokens = encode(text);
|
||||
|
||||
console.log(`vector generate success. text len: ${text.length}. token len: ${tokens.length}`);
|
||||
console.log(`vector generate success. text len: ${text.length}. token len: ${tokenLen}`);
|
||||
|
||||
if (isPay) {
|
||||
try {
|
||||
const unitPrice = 1;
|
||||
// 计算价格
|
||||
const price = unitPrice * tokens.length;
|
||||
|
||||
console.log(`price: ${formatPrice(price)}元`);
|
||||
const unitPrice = 0.4;
|
||||
// 计算价格. 至少为1
|
||||
let price = unitPrice * tokenLen;
|
||||
price = price > 1 ? price : 1;
|
||||
|
||||
// 插入 Bill 记录
|
||||
const res = await Bill.create({
|
||||
@@ -154,7 +145,7 @@ export const pushGenerateVectorBill = async ({
|
||||
type: BillTypeEnum.vector,
|
||||
modelName: ChatModelNameEnum.VECTOR,
|
||||
textLen: text.length,
|
||||
tokenLen: tokens.length,
|
||||
tokenLen,
|
||||
price
|
||||
});
|
||||
billId = res._id;
|
||||
|
||||
23
src/service/models/openapi.ts
Normal file
23
src/service/models/openapi.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Schema, model, models, Model } from 'mongoose';
|
||||
import { OpenApiSchema } from '@/types/mongoSchema';
|
||||
|
||||
const OpenApiSchema = new Schema({
|
||||
userId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'user',
|
||||
required: true
|
||||
},
|
||||
apiKey: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
createTime: {
|
||||
type: Date,
|
||||
default: () => new Date()
|
||||
},
|
||||
lastUsedTime: {
|
||||
type: Date
|
||||
}
|
||||
});
|
||||
|
||||
export const OpenApi: Model<OpenApiSchema> = models['openapi'] || model('openapi', OpenApiSchema);
|
||||
@@ -19,6 +19,10 @@ const UserSchema = new Schema({
|
||||
type: Number,
|
||||
default: 0.5 * PRICE_SCALE
|
||||
},
|
||||
openaiKey: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
accounts: [
|
||||
{
|
||||
type: {
|
||||
|
||||
@@ -42,3 +42,4 @@ export * from './models/pay';
|
||||
export * from './models/data';
|
||||
export * from './models/dataItem';
|
||||
export * from './models/splitData';
|
||||
export * from './models/openapi';
|
||||
|
||||
@@ -30,13 +30,11 @@ export const jsonRes = <T = any>(
|
||||
} else if (openaiError[error?.response?.statusText]) {
|
||||
msg = openaiError[error.response.statusText];
|
||||
}
|
||||
console.log('error->');
|
||||
console.log('code:', error.code);
|
||||
console.log('msg:', msg);
|
||||
console.log(`error-> msg:${msg}`);
|
||||
// request 时候报错
|
||||
if (error?.response) {
|
||||
console.log('statusText:', error?.response?.statusText);
|
||||
console.log('type:', error?.response?.data?.error?.type);
|
||||
console.log('openai error:', error?.response?.data?.error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import axios from 'axios';
|
||||
import type { NextApiResponse } from 'next';
|
||||
import type { PassThrough } from 'stream';
|
||||
import { createParser, ParsedEvent, ReconnectInterval } from 'eventsource-parser';
|
||||
import { getOpenAIApi } from '@/service/utils/chat';
|
||||
import { httpsAgent } from './tools';
|
||||
import { User } from '../models/user';
|
||||
@@ -10,7 +12,7 @@ import { pushGenerateVectorBill } from '../events/pushBill';
|
||||
export const getUserApiOpenai = async (userId: string) => {
|
||||
const user = await User.findById(userId);
|
||||
|
||||
const userApiKey = user?.accounts?.find((item: any) => item.type === 'openai')?.value;
|
||||
const userApiKey = user?.openaiKey;
|
||||
|
||||
if (!userApiKey) {
|
||||
return Promise.reject('缺少ApiKey, 无法请求');
|
||||
@@ -33,7 +35,7 @@ export const getOpenApiKey = async (userId: string) => {
|
||||
});
|
||||
}
|
||||
|
||||
const userApiKey = user?.accounts?.find((item: any) => item.type === 'openai')?.value;
|
||||
const userApiKey = user?.openaiKey;
|
||||
|
||||
// 有自己的key
|
||||
if (userApiKey) {
|
||||
@@ -75,7 +77,7 @@ export const openaiCreateEmbedding = async ({
|
||||
const chatAPI = getOpenAIApi(apiKey);
|
||||
|
||||
// 把输入的内容转成向量
|
||||
const vector = await chatAPI
|
||||
const res = await chatAPI
|
||||
.createEmbedding(
|
||||
{
|
||||
model: ChatModelNameEnum.VECTOR,
|
||||
@@ -86,16 +88,83 @@ export const openaiCreateEmbedding = async ({
|
||||
httpsAgent
|
||||
}
|
||||
)
|
||||
.then((res) => res?.data?.data?.[0]?.embedding || []);
|
||||
.then((res) => ({
|
||||
tokenLen: res.data.usage.total_tokens || 0,
|
||||
vector: res?.data?.data?.[0]?.embedding || []
|
||||
}));
|
||||
|
||||
pushGenerateVectorBill({
|
||||
isPay,
|
||||
userId,
|
||||
text
|
||||
text,
|
||||
tokenLen: res.tokenLen
|
||||
});
|
||||
|
||||
return {
|
||||
vector,
|
||||
vector: res.vector,
|
||||
chatAPI
|
||||
};
|
||||
};
|
||||
|
||||
/* gpt35 响应 */
|
||||
export const gpt35StreamResponse = ({
|
||||
res,
|
||||
stream,
|
||||
chatResponse
|
||||
}: {
|
||||
res: NextApiResponse;
|
||||
stream: PassThrough;
|
||||
chatResponse: any;
|
||||
}) =>
|
||||
new Promise<{ responseContent: string }>(async (resolve, reject) => {
|
||||
try {
|
||||
// 创建响应流
|
||||
res.setHeader('Content-Type', 'text/event-stream;charset-utf-8');
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('X-Accel-Buffering', 'no');
|
||||
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
||||
|
||||
let responseContent = '';
|
||||
stream.pipe(res);
|
||||
|
||||
const onParse = async (event: ParsedEvent | ReconnectInterval) => {
|
||||
if (event.type !== 'event') return;
|
||||
const data = event.data;
|
||||
if (data === '[DONE]') return;
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
const content: string = json?.choices?.[0].delta.content || '';
|
||||
// console.log('content:', content);
|
||||
if (!content || (responseContent === '' && content === '\n')) return;
|
||||
|
||||
responseContent += content;
|
||||
!stream.destroyed && stream.push(content.replace(/\n/g, '<br/>'));
|
||||
} catch (error) {
|
||||
error;
|
||||
}
|
||||
};
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
try {
|
||||
for await (const chunk of chatResponse.data as any) {
|
||||
if (stream.destroyed) {
|
||||
// 流被中断了,直接忽略后面的内容
|
||||
break;
|
||||
}
|
||||
const parser = createParser(onParse);
|
||||
parser.feed(decoder.decode(chunk));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('pipe error', error);
|
||||
}
|
||||
// close stream
|
||||
!stream.destroyed && stream.push(null);
|
||||
stream.destroy();
|
||||
|
||||
resolve({
|
||||
responseContent
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import type { NextApiRequest } from 'next';
|
||||
import crypto from 'crypto';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import tunnel from 'tunnel';
|
||||
import { ChatItemType } from '@/types/chat';
|
||||
import { encode } from 'gpt-token-utils';
|
||||
import { OpenApi, User } from '../mongo';
|
||||
import { formatPrice } from '@/utils/user';
|
||||
|
||||
/* 密码加密 */
|
||||
export const hashPassword = (psw: string) => {
|
||||
@@ -41,6 +44,44 @@ export const authToken = (token?: string): Promise<string> => {
|
||||
});
|
||||
};
|
||||
|
||||
/* 校验 open api key */
|
||||
export const authOpenApiKey = async (req: NextApiRequest) => {
|
||||
const { apikey: apiKey } = req.headers;
|
||||
|
||||
if (!apiKey) {
|
||||
return Promise.reject('api key is empty');
|
||||
}
|
||||
|
||||
try {
|
||||
const openApi = await OpenApi.findOne({ apiKey });
|
||||
if (!openApi) {
|
||||
return Promise.reject('api key is error');
|
||||
}
|
||||
const userId = String(openApi.userId);
|
||||
|
||||
// 余额校验
|
||||
const user = await User.findById(userId);
|
||||
if (!user) {
|
||||
return Promise.reject('user is empty');
|
||||
}
|
||||
if (formatPrice(user.balance) <= 0) {
|
||||
return Promise.reject('Insufficient account balance');
|
||||
}
|
||||
|
||||
// 更新使用的时间
|
||||
await OpenApi.findByIdAndUpdate(openApi._id, {
|
||||
lastUsedTime: new Date()
|
||||
});
|
||||
|
||||
return {
|
||||
apiKey: process.env.OPENAIKEY as string,
|
||||
userId
|
||||
};
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
/* 代理 */
|
||||
export const httpsAgent =
|
||||
process.env.AXIOS_PROXY_HOST && process.env.AXIOS_PROXY_PORT
|
||||
@@ -54,21 +95,30 @@ export const httpsAgent =
|
||||
|
||||
/* tokens 截断 */
|
||||
export const openaiChatFilter = (prompts: ChatItemType[], maxTokens: number) => {
|
||||
const formatPrompts = prompts.map((item) => ({
|
||||
obj: item.obj,
|
||||
value: item.value
|
||||
.replace(/[\u3000\u3001\uff01-\uff5e\u3002]/g, ' ') // 中文标点改空格
|
||||
.replace(/\n+/g, '\n') // 连续空行
|
||||
.replace(/[^\S\r\n]+/g, ' ') // 连续空白内容
|
||||
.trim()
|
||||
}));
|
||||
|
||||
let res: ChatItemType[] = [];
|
||||
|
||||
let systemPrompt: ChatItemType | null = null;
|
||||
|
||||
// System 词保留
|
||||
if (prompts[0]?.obj === 'SYSTEM') {
|
||||
systemPrompt = prompts.shift() as ChatItemType;
|
||||
maxTokens -= encode(prompts[0].value).length;
|
||||
if (formatPrompts[0]?.obj === 'SYSTEM') {
|
||||
systemPrompt = formatPrompts.shift() as ChatItemType;
|
||||
maxTokens -= encode(formatPrompts[0].value).length;
|
||||
}
|
||||
|
||||
// 从后往前截取
|
||||
for (let i = prompts.length - 1; i >= 0; i--) {
|
||||
const tokens = encode(prompts[i].value).length;
|
||||
for (let i = formatPrompts.length - 1; i >= 0; i--) {
|
||||
const tokens = encode(formatPrompts[i].value).length;
|
||||
if (maxTokens >= tokens) {
|
||||
res.unshift(prompts[i]);
|
||||
res.unshift(formatPrompts[i]);
|
||||
maxTokens -= tokens;
|
||||
} else {
|
||||
break;
|
||||
@@ -93,5 +143,5 @@ export const systemPromptFilter = (prompts: string[], maxTokens: number) => {
|
||||
}
|
||||
}
|
||||
|
||||
return splitText;
|
||||
return splitText.slice(0, splitText.length - 1);
|
||||
};
|
||||
|
||||
31
src/service/utils/wxpay.ts
Normal file
31
src/service/utils/wxpay.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
// @ts-ignore
|
||||
import Payment from 'wxpay-v3';
|
||||
|
||||
export const getPayment = () => {
|
||||
return new Payment({
|
||||
appid: process.env.WX_APPID,
|
||||
mchid: process.env.WX_MCHID,
|
||||
private_key: process.env.WX_PRIVATE_KEY?.replace(/\\n/g, '\n'),
|
||||
serial_no: process.env.WX_SERIAL_NO,
|
||||
apiv3_private_key: process.env.WX_V3_CODE,
|
||||
notify_url: process.env.WX_NOTIFY_URL
|
||||
});
|
||||
};
|
||||
|
||||
export const nativePay = (amount: number, payId: string): Promise<string> =>
|
||||
getPayment()
|
||||
.native({
|
||||
description: 'Fast GPT 余额充值',
|
||||
out_trade_no: payId,
|
||||
amount: {
|
||||
total: amount
|
||||
}
|
||||
})
|
||||
.then((res: any) => JSON.parse(res.data).code_url);
|
||||
|
||||
export const getPayResult = (payId: string) =>
|
||||
getPayment()
|
||||
.getTransactionsByOutTradeNo({
|
||||
out_trade_no: payId
|
||||
})
|
||||
.then((res: any) => JSON.parse(res.data));
|
||||
12
src/types/mongoSchema.d.ts
vendored
12
src/types/mongoSchema.d.ts
vendored
@@ -9,7 +9,7 @@ export interface UserModelSchema {
|
||||
email: string;
|
||||
password: string;
|
||||
balance: number;
|
||||
accounts: { type: 'openai'; value: string }[];
|
||||
openaiKey: string;
|
||||
createTime: number;
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ export interface DataSchema {
|
||||
_id: string;
|
||||
userId: string;
|
||||
name: string;
|
||||
createTime: string;
|
||||
createTime: Date;
|
||||
type: DataType;
|
||||
}
|
||||
|
||||
@@ -148,3 +148,11 @@ export interface DataItemSchema {
|
||||
export interface DataItemPopulate extends DataItemSchema {
|
||||
userId: UserModelSchema;
|
||||
}
|
||||
|
||||
export interface OpenApiSchema {
|
||||
_id: string;
|
||||
userId: string;
|
||||
createTime: Date;
|
||||
lastUsedTime?: Date;
|
||||
apiKey: String;
|
||||
}
|
||||
|
||||
6
src/types/openapi.d.ts
vendored
Normal file
6
src/types/openapi.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface UserOpenApiKey {
|
||||
id: string;
|
||||
apiKey: string;
|
||||
createTime: Date;
|
||||
lastUsedTime?: Date;
|
||||
}
|
||||
10
src/types/user.d.ts
vendored
10
src/types/user.d.ts
vendored
@@ -6,19 +6,13 @@ export enum UserNumberEnum {
|
||||
export interface UserType {
|
||||
_id: string;
|
||||
email: string;
|
||||
accounts: {
|
||||
type: string;
|
||||
value: string;
|
||||
}[];
|
||||
openaiKey: string;
|
||||
balance: number;
|
||||
}
|
||||
|
||||
export interface UserUpdateParams {
|
||||
balance?: number;
|
||||
accounts?: {
|
||||
type: string;
|
||||
value: string;
|
||||
}[];
|
||||
openaiKey: string;
|
||||
}
|
||||
|
||||
export interface UserBillType {
|
||||
|
||||
Reference in New Issue
Block a user