mirror of
				https://github.com/yangjian102621/geekai.git
				synced 2025-11-01 06:43:47 +08:00 
			
		
		
		
	Compare commits
	
		
			142 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 9cbe36d4c6 | ||
|  | b25bb2cc53 | ||
|  | 79ded6018b | ||
|  | 59f316b341 | ||
|  | f307b8ba7a | ||
|  | 5034a20345 | ||
|  | 26944f9e39 | ||
|  | e64946c3b6 | ||
|  | e0a62d9b35 | ||
|  | 39dbffd8d0 | ||
|  | 952d6183ed | ||
|  | 3365a6008d | ||
|  | 2e13ddf405 | ||
|  | 1d3acc8ed3 | ||
|  | fa341bab30 | ||
|  | 036a6e3e41 | ||
|  | f4c6ca4554 | ||
|  | 327929243c | ||
|  | f4349c7a8c | ||
|  | 4b46d847f0 | ||
|  | c3f016eae8 | ||
|  | ebd3ef842f | ||
|  | 18c033d57f | ||
|  | b676f80110 | ||
|  | f7fbaa534d | ||
|  | ea93a22e14 | ||
|  | 9f7e6778c5 | ||
|  | 6c31a2bfa6 | ||
|  | f943669e18 | ||
|  | 3b26735998 | ||
|  | 79d25769ee | ||
|  | 1dd6800987 | ||
|  | 5e673a9ee0 | ||
|  | 92eb67a2af | ||
|  | b1bed59be2 | ||
|  | 0ac732a3a3 | ||
|  | bf3f68fa19 | ||
|  | 46a551df16 | ||
|  | 20a12462b1 | ||
|  | a49fb1940e | ||
|  | 32774d23c7 | ||
|  | 7ecd7eeba1 | ||
|  | 0cc9cf8b45 | ||
|  | d06f94bddd | ||
|  | b5955f08c9 | ||
|  | c120569894 | ||
|  | aa376f1737 | ||
|  | 0f8a0f89e3 | ||
|  | 68dc261b44 | ||
|  | 4cf3af0c7b | ||
|  | b99b6735d9 | ||
|  | 52189b7880 | ||
|  | 3dbeb1ccb6 | ||
|  | 5a0f272fa8 | ||
|  | 6561b99f8f | ||
|  | 329e3eee21 | ||
|  | 07049c9afb | ||
|  | 36c5dd7eaa | ||
|  | b84039b506 | ||
|  | fab43097dc | ||
|  | c8998ba294 | ||
|  | 40b2466adc | ||
|  | 35fedbe817 | ||
|  | 827acdd3f9 | ||
|  | 6c76086916 | ||
|  | 373370fde5 | ||
|  | 2165ba3406 | ||
|  | b0e02b43fc | ||
|  | 2107c13b3d | ||
|  | 5f41aecc8d | ||
|  | 6840a13370 | ||
|  | 8f1e28c0ab | ||
|  | 7903eed284 | ||
|  | 0d49ea0d41 | ||
|  | 2ee4db5e48 | ||
|  | 48c4789505 | ||
|  | 4e65a5b1a1 | ||
|  | b09d23f97f | ||
|  | 3529649ba9 | ||
|  | fdd659f393 | ||
|  | 9eb8da2789 | ||
|  | ffb1ef0470 | ||
|  | 862c6aea43 | ||
|  | 54fe4b7588 | ||
|  | c6062ee70e | ||
|  | bed184dc1f | ||
|  | 29094ba3b3 | ||
|  | a18188876c | ||
|  | 4faee3e48e | ||
|  | 1a6afcd266 | ||
|  | f567831d92 | ||
|  | cf36ca4285 | ||
|  | 0e4ae01498 | ||
|  | 7b90f8cb13 | ||
|  | c33215529a | ||
|  | c5be114db2 | ||
|  | cab955c292 | ||
|  | ca8c8e6490 | ||
|  | 253951b4b3 | ||
|  | 4d6444ebf3 | ||
|  | 94d8d8a9d4 | ||
|  | e02badf7bb | ||
|  | dd88622c64 | ||
|  | c4d7126c4d | ||
|  | 86bc063941 | ||
|  | dce85eb519 | ||
|  | 4ab879d697 | ||
|  | 681e52df50 | ||
|  | fb554c0315 | ||
|  | accf8eeb77 | ||
|  | 3e41edd3b5 | ||
|  | 9126cfff20 | ||
|  | 9806d5ff4c | ||
|  | d1d13a72e4 | ||
|  | 00c520d066 | ||
|  | 797ff66474 | ||
|  | 9d51a478b9 | ||
|  | 1d4179df75 | ||
|  | 917b6012e8 | ||
|  | da14632794 | ||
|  | a868a8a8b7 | ||
|  | 5037df744f | ||
|  | da88a501ad | ||
|  | b9885e8de4 | ||
|  | 22efe81080 | ||
|  | 2926717aef | ||
|  | a49d54d66c | ||
|  | ce0267e25b | ||
|  | 9088d22a66 | ||
|  | 1ff32d5d0a | ||
|  | adf6916598 | ||
|  | 31c14bf748 | ||
|  | 5395385d1e | ||
|  | 0035da548b | ||
|  | 9bceaade05 | ||
|  | 3194becdad | ||
|  | 6174b17c24 | ||
|  | 53fa4a20e9 | ||
|  | 43c1de51f5 | ||
|  | 7eb8c5ec35 | ||
|  | 296bf63196 | ||
|  | 6c65a21692 | 
							
								
								
									
										108
									
								
								CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | ||||
| # 更新日志 | ||||
| ## v3.1.3 | ||||
| 1. 页面重构:重后 Home 页面,拆分成聊天,MJ绘画,SD 绘画,应用广场等多个功能菜单。 | ||||
| 2. 功能新增:新增 MidJourney 专业绘画页面,开放更高级的 MJ 绘画姿势。 | ||||
| 3. 功能优化:采用队列的方式控制绘画任务并发,简化任务回调通知逻辑,给任务回调加锁。 | ||||
| 4. 功能优化:精简用户表字段,删除用户名和昵称,只保留手机号。 | ||||
| 5. 功能优化:优化文件上传服务工厂实现,只创建激活的 Uploader 服务,节省资源。 | ||||
| 6. Bug修复:修复 JWT token 有效期计算错误的 Bug。 | ||||
|  | ||||
| ## v3.1.2 | ||||
| 1. 功能新增:新增七牛云 OSS 实现,目前已支持三种文件上传服务:Local, Minio, QiNiu OSS。 | ||||
| 2. 功能新增:新增桌面版,使用 electron 套壳网页版。 | ||||
| 3. Bug修复:自动去除众筹核销时候转账单号中的空格,防止复制的时候多复制了空格。 | ||||
| 4. 功能优化:ChatPlus.vue 页面支持通过 chat_id path variable 来定位到指定的聊天。 | ||||
| 5. 功能优化:取消导出聊天页面的授权验证 | ||||
| 6. 功能优化:所有路由跳转都使用绝对路径 | ||||
|  | ||||
| ## v3.1.1 | ||||
| 紧急修复版本,采用弹窗的方式显示验证码,解决验证码在低分辨率下被掩盖的Bug | ||||
|  | ||||
| ## v3.1.0(大版本更新) | ||||
| 1. 功能重构:将聊天模型独立拆分,以便支持多平台模型,目前已经内置支持 OPenAI,Azure 以及 ChatGLM,用户可以在这两个平台的模型中随意切换,体验不同的模型聊天。 | ||||
| 2. 功能重构:重写系统 API 授权机制,使用 JWT 替换传统的 session 会话授权,使得 API 授权变得更加灵活。 | ||||
| 3. 功能重构:重构文件夹上传服务,支持多种文件上传存储handler,目前已经实现本地存储和 minio oss 存储。 | ||||
| 4. 功能优化:更新头像自动删除旧的图片资源。 | ||||
| 5. 功能优化:将应用日志在终端输出的同时存盘,方便 docker 部署查看日志。 | ||||
| 6. 功能新增:允许用户配置自己的 OPenAI,Azure 以及 ChatGLM API KEY。 | ||||
| 7. 功能优化:优化移动版的行为验证码样式,修复低分辨率显示器验证码被遮挡的 Bug | ||||
| 8. 升级 gin, element-plus,redis 组件到最新版本。  | ||||
| 9. Bug修复:修复若干已知的的 Bug | ||||
|  | ||||
| ## v3.0.7 | ||||
|  | ||||
| 1. 聊天主界面:新增聊天引导页面,介绍产品功能 | ||||
| 2. 功能重构:拆分项目,将函数插件以及微信机器人,MidJourney 机器人等功能拆分新项目独立部署。 | ||||
| 3. 功能新增:新增 MidJourney AI 绘画支持,当识别到用户的绘画需求时,自动调用 MidJourney 绘画函数进行绘画。 | ||||
| 4. 功能新增:支持导出聊天记录为 PDF 文件。 | ||||
| 5. 功能优化:在后台 dashboard 页面新增统计今日众筹收入。 | ||||
| 6. 功能优化:支持用户设置默认的 GPT 模型 | ||||
| 7. Bug修复:修复若干已知的的 Bug | ||||
|  | ||||
| ## v3.0.6 | ||||
|  | ||||
| 1. 管理后台:新增用户名和手机号码搜索功能 | ||||
| 2. 管理后台:新增重置用户密码功能 | ||||
| 3. 管理后台:支持关闭注册功能,新增添加用户功能,适用于内部使用场景 | ||||
| 4. 管理后台:新增仪表盘页面,统计当天的新增用户,新增会话数据,以及 Token 消耗 | ||||
| 5. Bug修复:修复注册页面验证码不显示 Bug | ||||
| 6. Bug修复:优化上下文 Token 计算算法,修复聊天上下文超出限制时循环发送消息的 Bug | ||||
| 7. 功能修正:允许用户使用手机号码登录 | ||||
| 8. 功能优化:更新系统配置后同步更新服务端内存变量数据 | ||||
| 9. 功能优化:优化打包脚本,减少容器镜像大小 | ||||
|  | ||||
| ## v3.0.5 | ||||
|  | ||||
| 重磅功能更新!!! 新增函数插件支持,可以轻松地接入你的第三方插件服务,ChatGPT 自动帮您调用对应的函数完成任务。 | ||||
|  | ||||
| 1. 新增函数功能支持,全球早报,今日头条和微博热搜等插件服务,您也可以接入自己的第三方服务。 | ||||
| 2. 集成微信机器人模块,可以通过微信个人收款码来完成充值,无需接入微信支付功能也可以完成收款功能。 | ||||
| 3. 用户注册添加短信验证码功能,引入交互安全认证服务,有效防刷短信。 | ||||
| 4. 支持配置聊天上下文深度,精确统计每轮对话所消耗的总 TOKEN 数量。 | ||||
| 5. 修复已知的 Bug。 | ||||
|  | ||||
| ## v3.0.4 | ||||
|  | ||||
| 1. 调整项目目录结构,移除其他语言 API 目录 | ||||
| 2. 修复 nodejs apple M1 跨平台打包,运行报错 exec format error | ||||
| 3. 增加用户 token 消耗统计功能 | ||||
|  | ||||
| ## v3.0.3 | ||||
|  | ||||
| 1. 优化启动参数接收处理,支持环境变量传参 | ||||
| 2. 修复 PC 端聊天界面出现滚动条的 Bug | ||||
| 3. 修正前端 user_init_call 字段错误和用户注册初始化头像路径问题 | ||||
| 4. 更改 docker 构建镜像的基础镜像,改用作者的阿里云镜像,这样打包更快一些。 | ||||
|  | ||||
| ## v3.0.2 | ||||
|  | ||||
| 1. Feat:新增移动端的聊天和用户设置功能 | ||||
| 2. Fix: 修复 markdown 换行符解析的 Bug | ||||
| 3. Feat: 新增头像上传功能 | ||||
| 4. Docs: 增加容器部署支持,支持 docker-compose 一键部署 | ||||
| 5. Fix: 增加全局错误处理 handler,修复业务处理异常导致服务退出的 Bug | ||||
|  | ||||
| ## v3.0.1 | ||||
|  | ||||
| 1. 紧急修复前端 Home 组件路由被后台管理 Home 组件路由覆盖的 Bug。 | ||||
| 2. 增加 docker-compose 部署脚本 | ||||
|  | ||||
| ## v3.0.0 | ||||
|  | ||||
| 全新的重构版本!!! | ||||
| 新版的系统前后端都进行大改动的重构,后端还是用的 Gin Web 框架,但是作者整合了 fx 自动注入框架,整个后端应用结构非常简洁,特别适合二次开发。 | ||||
| 另外,数据存储用 MySQL 替换了 leveldb, 因为要对 C 端,后期会涉及到很多业务数据查询统计,leveldb 已经完全不够用了。 | ||||
| 前后台技术架构还是基于 `Vue3 + Element-Plus` ,但是页面风格已经全部变了,几乎所有页面样式代码都重写了,希望会你是希望的风格! | ||||
|  | ||||
| 此次重构改版主要是为了后面功能的扩展准备了。 | ||||
|  | ||||
| 新版本已经实现的功能如下: | ||||
|  | ||||
| 1. 引入用户体系,新增用户注册和登录功能。 | ||||
| 2. 聊天页面改版,实现了跟 ChatGPT 官方版本一致的聊天体验。 | ||||
| 3. 创建会话的时候可以选择聊天角色和模型。 | ||||
| 4. 新增聊天设置功能,用户可以导入自己的 API KEY | ||||
| 5. 保存聊天记录,支持聊天上下文。 | ||||
| 6. 重构后台管理模块,更友好,扩展性更好的后台管理系统。 | ||||
| 7. 引入 ip2region 组件,记录用户的登录IP和地址。 | ||||
| 8. 支持会话搜索过滤。 | ||||
							
								
								
									
										238
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										238
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,39 +1,50 @@ | ||||
| # ChatGPT-Plus | ||||
|  | ||||
| **ChatGPT-PLUS** 是基于 OpenAI API 实现的 ChatGPT 聊天系统。主要有如下特性: | ||||
| **ChatGPT-PLUS** 基于 AI 大语言模型 API 实现的 AI 助手全套开源解决方案,自带运营管理后台,开箱即用。集成了 OpenAI, Azure, ChatGLM,讯飞星火,文心一言等多个平台的大语言模型。主要有如下特性: | ||||
|  | ||||
| * 完整的开源系统,前端应用和后台管理系统皆可开箱即用。 | ||||
| * 聊天体验跟 ChatGPT 官方版本完全一致。 | ||||
| * 内置了各种预训练好的角色,比如小红书写手,英语翻译大师,苏格拉底,孔子,乔布斯,周报助手等。轻松满足你的各种聊天和应用需求。 | ||||
|  | ||||
| **本项目基于 MIT 协议,免费开放全部源代码,可以作为个人学习使用或者商用。如需商用建议联系作者登记,仅做统计使用, | ||||
| 优秀项目我们将在项目首页为您展示。** | ||||
| * 支持 MidJourney AI 绘画集成,开箱即用。 | ||||
| * 支持使用个人微信二维码作为充值收费的支付渠道,无需企业支付通道。(可定制开发其他支付通道支持) | ||||
| * 集成插件 API 功能,可结合 GPT 开发各种强大的插件,已内置实现了微博热搜,今日头条,今日早报和 AI 绘画函数插件。 | ||||
|  | ||||
| ## 功能截图 | ||||
|  | ||||
| ### 1.PC 端聊天界面 | ||||
| ### PC 端聊天界面 | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ### 2. 新版聊天界面 | ||||
| ### 新版聊天界面 | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ### 3. 用户设置 | ||||
| ### MidJourney 专业绘画界面(v3.1.3) | ||||
|  | ||||
|  | ||||
|  | ||||
| ### 自动调用函数插件 | ||||
|  | ||||
| ### 4. 登录页面 | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ### 5. 管理后台 | ||||
| ### 用户设置 | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ### 登录页面 | ||||
|  | ||||
| ### 6. 移动端 Web 页面 | ||||
|  | ||||
|  | ||||
| ### 管理后台 | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ### 移动端 Web 页面 | ||||
|  | ||||
|  | ||||
|  | ||||
| @@ -41,8 +52,13 @@ | ||||
|  | ||||
| ### 7. 体验地址 | ||||
|  | ||||
| > 体验地址:[https://www.chat-plus.net/chat](https://www.chat-plus.net/chat) <br/> | ||||
| > 涉及到数据隐私问题,没有提供共享账号,大家自己快速注册一个账号就可以免费体验 | ||||
| > 免费体验地址:[https://ai.r9it.com/chat](https://ai.r9it.com/chat) <br/> | ||||
| > **注意:请合法使用,禁止输出任何敏感、不友好或违规的内容!!!** | ||||
|  | ||||
| ## 使用须知 | ||||
|  | ||||
| 1. 本项目基于 MIT 协议,免费开放全部源代码,可以作为个人学习使用或者商用。 | ||||
| 2. 如需商用必须保留版权信息,请自觉遵守。确保合法合规使用,在运营过程中产生的一切任何后果自负,与作者无关。 | ||||
|  | ||||
| ## 项目介绍 | ||||
|  | ||||
| @@ -76,6 +92,7 @@ ChatGPT 的服务。 | ||||
| 6. 重构后台管理模块,更友好,扩展性更好的后台管理系统。 | ||||
| 7. 引入 ip2region 组件,记录用户的登录IP和地址。 | ||||
| 8. 支持会话搜索过滤。 | ||||
| 9. 支持微信支付充值 | ||||
|  | ||||
| ## 项目地址 | ||||
|  | ||||
| @@ -84,14 +101,20 @@ ChatGPT 的服务。 | ||||
|  | ||||
| ## TODOLIST | ||||
|  | ||||
| * [ ] 整合 Midjourney AI 绘画 API | ||||
| * [ ] 开发移动端聊天页面 | ||||
| * [ ] 接入微信支付功能 | ||||
| * [ ] 接入语音和 TTS API,支持语音聊天 | ||||
| * [x] 整合 Midjourney AI 绘画 API | ||||
| * [x] 开发移动端聊天页面 | ||||
| * [x] 接入微信支付功能 | ||||
| * [x] 支持 ChatGPT 函数功能,通过函数实现插件 | ||||
| * [ ] 支持基于知识库的 AI 问答 | ||||
| * [ ] 开发桌面版应用 | ||||
| * [ ] 开发手机 App 客户端 | ||||
|  | ||||
| ## Docker 快速部署 | ||||
|  | ||||
| > | ||||
| 鉴于最新不少网友反馈在部署的时候遇到一些问题,大部分问题都是相同的,所以我这边做了一个视频教程 [五分钟部署自己的 ChatGPT 服务](https://www.bilibili.com/video/BV1H14y1B7Qw/)。 | ||||
| > 习惯看视频教程的朋友可以去看视频教程,视频的语速比较慢,建议 2 倍速观看。 | ||||
|  | ||||
| V3.0.0 版本以后已经支持使用容器部署了,跳过所有的繁琐的环境准备,一条命令就可以轻松部署上线。 | ||||
|  | ||||
| ### 1. 导入数据库 | ||||
| @@ -103,7 +126,7 @@ cd docker/mysql | ||||
| # 创建 mysql 容器 | ||||
| docker-compose up -d | ||||
| # 导入数据库 | ||||
| docker exec -i chatgpt-plus-mysql sh -c 'exec mysql -uroot -p12345678' < ../../database/chatgpt_plus.sql | ||||
| docker exec -i chatgpt-plus-mysql sh -c 'exec mysql -uroot -p12345678' < ../../database/chatgpt_plus-v3.1.3.sql | ||||
| ``` | ||||
|  | ||||
| 如果你本地已经安装了 MySQL 服务,那么你只需手动导入数据库即可。 | ||||
| @@ -121,24 +144,64 @@ source database/chatgpt_plus.sql | ||||
|  | ||||
| ```toml | ||||
| Listen = "0.0.0.0:5678" | ||||
| ProxyURL = ["YOUR_PROXY_URL"] # 替换成你本地代理,如:http://127.0.0.1:7777 | ||||
| #ProxyURL = "" 如果你的服务器本身就在墙外,那么你直接留空就好了 | ||||
| ProxyURL = "" # 如 http://127.0.0.1:7777 | ||||
| MysqlDns = "root:12345678@tcp(172.22.11.200:3307)/chatgpt_plus?charset=utf8&parseTime=True&loc=Local" | ||||
| StaticDir = "./static" # 静态资源的目录 | ||||
| StaticUrl = "/static" # 静态资源访问 URL | ||||
| AesEncryptKey = "" | ||||
|  | ||||
| [Session] | ||||
|   SecretKey = "azyehq3ivunjhbntz78isj00i4hz2mt9xtddysfucxakadq4qbfrt0b7q3lnvg80" | ||||
|   Name = "CHAT_SESSION_ID" | ||||
|   Path = "/" | ||||
|   Domain = "" | ||||
|   SecretKey = "azyehq3ivunjhbntz78isj00i4hz2mt9xtddysfucxakadq4qbfrt0b7q3lnvg80" # 注意:这个是 JWT Token 授权密钥,生产环境请务必更换 | ||||
|   MaxAge = 86400 | ||||
|   Secure = false | ||||
|   HttpOnly = false | ||||
|   SameSite = 2 | ||||
|  | ||||
| [Manager] | ||||
|   Username = "admin" | ||||
|   Password = "admin123" # 如果是生产环境的话,这里管理员的密码记得修改 | ||||
|    | ||||
| [Redis] # redis 配置信息 | ||||
|   Host = "localhost"  | ||||
|   Port = 6379 | ||||
|   Password = "" | ||||
|   DB = 0 | ||||
|    | ||||
| [ApiConfig] # 微博热搜,今日头条等函数服务 API 配置,此为第三方插件服务,如需使用请联系作者开通 | ||||
|   ApiURL = "" | ||||
|   AppId = "" | ||||
|   Token = "" | ||||
|  | ||||
| [SmsConfig] # 阿里云短信服务配置 | ||||
|   AccessKey = "" | ||||
|   AccessSecret = "" | ||||
|   Product = "Dysmsapi" | ||||
|   Domain = "dysmsapi.aliyuncs.com" | ||||
|  | ||||
| [ExtConfig] # MidJourney和微信机器人服务 API 配置,开通此功能需要配合 chatpgt-plus-exts 项目部署 | ||||
|   ApiURL = "" # 插件扩展 API 地址 | ||||
|   Token = "" # 这个 token 随便填,只要确保跟 chatgpt-plus-exts 项目的 token 一样就行  | ||||
|    | ||||
| [OSS] # OSS 配置,用于存储 MJ 绘画图片 | ||||
|    Active = "local" # 默认使用本地文件存储引擎 | ||||
|    [OSS.Local] | ||||
|      BasePath = "./static/upload" # 本地文件上传根路径 | ||||
|      BaseURL = "http://localhost:5678/static/upload" # 本地上传文件根 URL 如果是线上,则直接设置为 /static/upload 即可 | ||||
|    [OSS.Minio] | ||||
|      Endpoint = "" # 如 172.22.11.200:9000 | ||||
|      AccessKey = "" # 自己去 Minio 控制台去创建一个 Access Key | ||||
|      AccessSecret = "" | ||||
|      Bucket = "chatgpt-plus" # 替换为你自己创建的 Bucket,注意要给 Bucket 设置公开的读权限,否则会出现图片无法显示。 | ||||
|      UseSSL = false | ||||
|      Domain = "" # 地址必须是能够通过公网访问的,否则会出现图片无法显示。 | ||||
|    [OSS.QiNiu] # 七牛云 OSS 配置 | ||||
|        Zone = "z2" # 区域,z0:华东,z1: 华北,na0:北美,as0:新加坡 | ||||
|        AccessKey = "" | ||||
|        AccessSecret = "" | ||||
|        Bucket = "" | ||||
|        Domain = "" # OSS Bucket 所绑定的域名,如 https://img.r9it.com | ||||
| ``` | ||||
|  | ||||
| > 如果要启用微信收款服务和 MidJourney | ||||
| > 绘画功能,请先部署扩展服务项目 [chatgpt-plus-exts](https://github.com/yangjian102621/chatgpt-plus-exts)。 | ||||
|  | ||||
| 修改 nginx 配置文档 `docker/conf/nginx/conf.d/chatgpt-plus.conf`,把后端转发的地址改成当前主机的内网 IP 地址。 | ||||
|  | ||||
| ```shell | ||||
| @@ -154,6 +217,11 @@ location /api/ { | ||||
|        proxy_set_header Upgrade $http_upgrade; | ||||
|        proxy_set_header Connection $connection_upgrade; | ||||
|        proxy_pass http://172.28.173.76:6789; # 这里改成后端服务的内网 IP 地址 | ||||
|         | ||||
| # 静态资源转发 | ||||
| location /static/ { | ||||
|    proxy_pass http://172.22.11.47:5678; # 这里改成后端服务的内网 IP 地址 | ||||
| } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| @@ -172,104 +240,9 @@ docker-compose up -d | ||||
| > 输入你前面配置文档中设置的管理员用户名和密码登录。 | ||||
| > 然后进入 `API KEY 管理` 菜单,添加一个 OpenAI 的 API KEY 才可以正常开启 AI 对话。 | ||||
|  | ||||
| ## 手动安装部署 | ||||
|  | ||||
| 由于本项目采用的是前后端分离的开发方式,所以部署也需要前后端分开部署。我这里以 linux 系统为例,演示一下部署过程: | ||||
|  | ||||
| ### 1. 导入数据库 | ||||
|  | ||||
| 请参考容器部署的[导入数据](#1-导入数据库)。 | ||||
|  | ||||
| ### 2. 修改配置文档 | ||||
|  | ||||
| 先拷贝项目中的 `api/go/config.sample.toml` 配置文档,修改代理地址和管理员密码: | ||||
|  | ||||
| 如何修改请参考[修改配置文档](#2-修改配置文档) | ||||
|  | ||||
| ### 3. 运行后端程序 | ||||
|  | ||||
| 你可以自己编译或者直接下载我打包好的后端程序运行。 | ||||
|  | ||||
| ```shell | ||||
| # 1. 下载程序,你也可以自己编译 | ||||
| wget https://github.com/yangjian102621/chatgpt-plus/releases/download/v3.0.0/chatgpt-v3-amd64-linux | ||||
| # 2. 添加执行权限 | ||||
| chmod +x chatgpt-v3-amd64-linux | ||||
| # 3. 运行程序,如果配置文档不在当前目录,注意指定配置文档 | ||||
| ./chatgpt-v3-amd64-linux | ||||
| ``` | ||||
|  | ||||
| ### 4. 前端部署 | ||||
|  | ||||
| 前端是 Vue 项目编译好静态资源文件,同样你也可以直接下载我编译好的文件解压。 | ||||
|  | ||||
| ```shell | ||||
| # 1. 下载程序 | ||||
| wget https://github.com/yangjian102621/chatgpt-plus/releases/download/v3.0.0/dist.tar.gz | ||||
| # 2. 解压 | ||||
| tar -xf dist.tar.gz | ||||
| ``` | ||||
|  | ||||
| ### 5. 配置 Nginx 服务 | ||||
|  | ||||
| 前端程序需要搭载 Web 服务器才可以运行,这里我们选择 Nginx,先安装: | ||||
|  | ||||
| ```shell | ||||
| sudo apt install nginx -y | ||||
| ``` | ||||
|  | ||||
| 建立 Nginx 配置文件: | ||||
|  | ||||
| ```conf | ||||
| server { | ||||
|     listen  443 ssl; | ||||
|     server_name  www.chatgpt.com; #替换成你自己的域名 | ||||
|  | ||||
|     ssl_certificate     xxx.pem;  # 替换成自己的 SSL 证书 | ||||
|     ssl_certificate_key  xxx.key; | ||||
|     ssl_session_timeout  5m; | ||||
|     ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; | ||||
|     ssl_protocols TLSv1 TLSv1.1 TLSv1.2; | ||||
|     ssl_prefer_server_ciphers on; | ||||
|      | ||||
|     # 日志地址 | ||||
|     access_log  /var/log/chatgpt/access.log; | ||||
|     error_log /var/log/chatgpt/error.log; | ||||
|      | ||||
|     index index.html; | ||||
|     root /var/www/chatgpt/dist; # 这里改成前端静态页面的地址 | ||||
|  | ||||
|     location / { | ||||
|         try_files $uri $uri/ /index.html; | ||||
|          | ||||
|          # 后端 API 的转发 | ||||
|         location /api/ { | ||||
|                 proxy_http_version 1.1; | ||||
|                 proxy_connect_timeout 300s; | ||||
|                 proxy_read_timeout 300s; | ||||
|                 proxy_send_timeout 12s; | ||||
|                 proxy_set_header Host $host; | ||||
|                 proxy_set_header X-Real-IP $remote_addr; | ||||
|                 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | ||||
|                 proxy_set_header Upgrade $http_upgrade; | ||||
|                 proxy_set_header Connection $connection_upgrade; | ||||
|                 proxy_pass http://172.28.173.76:6789; # 这里改成后端服务的内网 IP 地址 | ||||
|         } | ||||
|  | ||||
|         # 静态资源转发 | ||||
|         location /static/ { | ||||
|             proxy_pass http://172.28.173.76:6789; # 这里改成后端服务的内网 IP 地址 | ||||
|         } | ||||
|     } | ||||
|      | ||||
| } | ||||
| ``` | ||||
|  | ||||
| 配置好之后重启 Nginx,然后 [] | ||||
|  | ||||
|  | ||||
|  | ||||
| 最后登录前端聊天页面 [http://www.chatgpt.com/admin](http://www.chatgpt.com/admin) | ||||
| 最后登录前端聊天页面 [http://localhost:8080/chat](http://localhost:8080/chat) | ||||
| 你可以注册新用户,也可以使用系统默认有个账号:`geekmaster/12345678` 登录聊天。 | ||||
|  | ||||
| 祝你使用愉快!!! | ||||
| @@ -285,7 +258,7 @@ server { | ||||
| 3. 运行后端程序: | ||||
|  | ||||
|     ```shell | ||||
|     cd api/go  | ||||
|     cd api  | ||||
|     # 1. 先下载依赖 | ||||
|     go mod tidy | ||||
|     # 2. 运行程序 | ||||
| @@ -338,11 +311,11 @@ npm run build | ||||
| 你可以根据个人需求将项目打包成 windows/linux/darwin 平台项目。 | ||||
|  | ||||
| ```shell | ||||
| cd api/go | ||||
| cd api | ||||
| # for all platforms | ||||
| make all | ||||
| make clean all | ||||
| # for linux only | ||||
| make linux | ||||
| make clean linux | ||||
| ``` | ||||
|  | ||||
| 打包后的可执行文件在 `bin` 目录下。 | ||||
| @@ -350,7 +323,6 @@ make linux | ||||
| ## 参与贡献 | ||||
|  | ||||
| 个人的力量始终有限,任何形式的贡献都是欢迎的,包括但不限于贡献代码,优化文档,提交 issue 和 PR 等。 | ||||
| **尤其是新版本的开发计划比较大,包括各种语言的后端 API 实现,本人精力有限,希望借助社区的力量来完成这些 API 的开发。** | ||||
|  | ||||
| 如果有兴趣的话,也可以加微信进入微信讨论群(**添加好友时请注明来自Github!!!**)。 | ||||
|  | ||||
|   | ||||
							
								
								
									
										3
									
								
								api/go/.gitignore → api/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								api/go/.gitignore → api/.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -16,4 +16,5 @@ tmp | ||||
| bin | ||||
| data | ||||
| config.toml | ||||
| static/upload | ||||
| static/upload  | ||||
| storage.json | ||||
| @@ -1,5 +1,5 @@ | ||||
| SHELL=/usr/bin/env bash | ||||
| NAME := chatgpt-v3 | ||||
| NAME := chatgpt-plus | ||||
| all: window linux darwin | ||||
| 
 | ||||
| 
 | ||||
| @@ -12,7 +12,7 @@ linux: | ||||
| .PHONY: linux | ||||
| 
 | ||||
| darwin: | ||||
| 	CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -o bin/$(NAME)-amd64-darwin main.go | ||||
| 	CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o bin/$(NAME)-amd64-darwin main.go | ||||
| .PHONY: darwin | ||||
| 
 | ||||
| clean: | ||||
							
								
								
									
										54
									
								
								api/config.sample.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								api/config.sample.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | ||||
| Listen = "0.0.0.0:5678" | ||||
| ProxyURL = "" # 如 http://127.0.0.1:7777 | ||||
| MysqlDns = "root:12345678@tcp(172.22.11.200:3307)/chatgpt_plus?charset=utf8&parseTime=True&loc=Local" | ||||
| StaticDir = "./static" # 静态资源的目录 | ||||
| StaticUrl = "/static" # 静态资源访问 URL | ||||
| AesEncryptKey = "" | ||||
|  | ||||
| [Session] | ||||
|   SecretKey = "azyehq3ivunjhbntz78isj00i4hz2mt9xtddysfucxakadq4qbfrt0b7q3lnvg80" # 注意:这个是 JWT Token 授权密钥,生产环境请务必更换 | ||||
|   MaxAge = 86400 | ||||
|  | ||||
| [Manager] | ||||
|   Username = "admin" | ||||
|   Password = "admin123" # 如果是生产环境的话,这里管理员的密码记得修改 | ||||
|  | ||||
| [Redis] # redis 配置信息 | ||||
|   Host = "localhost" | ||||
|   Port = 6379 | ||||
|   Password = "" | ||||
|   DB = 0 | ||||
|  | ||||
| [ApiConfig] # 微博热搜,今日头条等函数服务 API 配置,此为第三方插件服务,如需使用请联系作者开通 | ||||
|   ApiURL = "" | ||||
|   AppId = "" | ||||
|   Token = "" | ||||
|  | ||||
| [SmsConfig] # 阿里云短信服务配置 | ||||
|   AccessKey = "" | ||||
|   AccessSecret = "" | ||||
|   Product = "Dysmsapi" | ||||
|   Domain = "dysmsapi.aliyuncs.com" | ||||
|  | ||||
| [ExtConfig] # MidJourney和微信机器人服务 API 配置,开通此功能需要配合 chatpgt-plus-exts 项目部署 | ||||
|   ApiURL = "" # 插件扩展 API 地址 | ||||
|   Token = "" # 这个 token 随便填,只要确保跟 chatgpt-plus-exts 项目的 token 一样就行 | ||||
|  | ||||
| [OSS] # OSS 配置,用于存储 MJ 绘画图片 | ||||
|    Active = "local" # 默认使用本地文件存储引擎 | ||||
|    [OSS.Local] | ||||
|      BasePath = "./static/upload" # 本地文件上传根路径 | ||||
|      BaseURL = "http://localhost:5678/static/upload" # 本地上传文件根 URL 如果是线上,则直接设置为 /static/upload 即可 | ||||
|    [OSS.Minio] | ||||
|      Endpoint = "" # 如 172.22.11.200:9000 | ||||
|      AccessKey = "" # 自己去 Minio 控制台去创建一个 Access Key | ||||
|      AccessSecret = "" | ||||
|      Bucket = "chatgpt-plus" # 替换为你自己创建的 Bucket,注意要给 Bucket 设置公开的读权限,否则会出现图片无法显示。 | ||||
|      UseSSL = false | ||||
|      Domain = "" # 地址必须是能够通过公网访问的,否则会出现图片无法显示。 | ||||
|    [OSS.QiNiu] # 七牛云 OSS 配置 | ||||
|        Zone = "z2" # 区域,z0:华东,z1: 华北,na0:北美,as0:新加坡 | ||||
|        AccessKey = "" | ||||
|        AccessSecret = "" | ||||
|        Bucket = "" | ||||
|        Domain = "" # OSS Bucket 所绑定的域名,如 https://img.r9it.com | ||||
							
								
								
									
										209
									
								
								api/core/app_server.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										209
									
								
								api/core/app_server.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,209 @@ | ||||
| package core | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/service/function" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/utils" | ||||
| 	"chatplus/utils/resp" | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/go-redis/redis/v8" | ||||
| 	"github.com/golang-jwt/jwt/v5" | ||||
| 	"gorm.io/gorm" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"runtime/debug" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| type AppServer struct { | ||||
| 	Debug        bool | ||||
| 	Config       *types.AppConfig | ||||
| 	Engine       *gin.Engine | ||||
| 	ChatContexts *types.LMap[string, []interface{}] // 聊天上下文 Map [chatId] => []Message | ||||
|  | ||||
| 	ChatConfig *types.ChatConfig   // chat config cache | ||||
| 	SysConfig  *types.SystemConfig // system config cache | ||||
|  | ||||
| 	// 保存 Websocket 会话 UserId, 每个 UserId 只能连接一次 | ||||
| 	// 防止第三方直接连接 socket 调用 OpenAI API | ||||
| 	ChatSession   *types.LMap[string, *types.ChatSession] //map[sessionId]UserId | ||||
| 	ChatClients   *types.LMap[string, *types.WsClient]    // map[sessionId]Websocket 连接集合 | ||||
| 	ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function | ||||
| 	Functions     map[string]function.Function | ||||
| 	MjTaskClients *types.LMap[string, *types.WsClient] | ||||
| } | ||||
|  | ||||
| func NewServer(appConfig *types.AppConfig, functions map[string]function.Function) *AppServer { | ||||
| 	gin.SetMode(gin.ReleaseMode) | ||||
| 	gin.DefaultWriter = io.Discard | ||||
| 	return &AppServer{ | ||||
| 		Debug:         false, | ||||
| 		Config:        appConfig, | ||||
| 		Engine:        gin.Default(), | ||||
| 		ChatContexts:  types.NewLMap[string, []interface{}](), | ||||
| 		ChatSession:   types.NewLMap[string, *types.ChatSession](), | ||||
| 		ChatClients:   types.NewLMap[string, *types.WsClient](), | ||||
| 		ReqCancelFunc: types.NewLMap[string, context.CancelFunc](), | ||||
| 		MjTaskClients: types.NewLMap[string, *types.WsClient](), | ||||
| 		Functions:     functions, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (s *AppServer) Init(debug bool, client *redis.Client) { | ||||
| 	if debug { // 调试模式允许跨域请求 API | ||||
| 		s.Debug = debug | ||||
| 		logger.Info("Enabled debug mode") | ||||
| 	} | ||||
| 	s.Engine.Use(corsMiddleware()) | ||||
| 	s.Engine.Use(authorizeMiddleware(s, client)) | ||||
| 	s.Engine.Use(errorHandler) | ||||
| 	// 添加静态资源访问 | ||||
| 	s.Engine.Static("/static", s.Config.StaticDir) | ||||
| } | ||||
|  | ||||
| func (s *AppServer) Run(db *gorm.DB) error { | ||||
| 	// load chat config from database | ||||
| 	var chatConfig model.Config | ||||
| 	res := db.Where("marker", "chat").First(&chatConfig) | ||||
| 	if res.Error != nil { | ||||
| 		return res.Error | ||||
| 	} | ||||
| 	err := utils.JsonDecode(chatConfig.Config, &s.ChatConfig) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	// load system configs | ||||
| 	var sysConfig model.Config | ||||
| 	res = db.Where("marker", "system").First(&sysConfig) | ||||
| 	if res.Error != nil { | ||||
| 		return res.Error | ||||
| 	} | ||||
| 	err = utils.JsonDecode(sysConfig.Config, &s.SysConfig) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	logger.Infof("http://%s", s.Config.Listen) | ||||
| 	return s.Engine.Run(s.Config.Listen) | ||||
| } | ||||
|  | ||||
| // 全局异常处理 | ||||
| func errorHandler(c *gin.Context) { | ||||
| 	defer func() { | ||||
| 		if r := recover(); r != nil { | ||||
| 			logger.Errorf("Handler Panic: %v", r) | ||||
| 			debug.PrintStack() | ||||
| 			c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: types.ErrorMsg}) | ||||
| 			c.Abort() | ||||
| 		} | ||||
| 	}() | ||||
| 	//加载完 defer recover,继续后续接口调用 | ||||
| 	c.Next() | ||||
| } | ||||
|  | ||||
| // 跨域中间件设置 | ||||
| func corsMiddleware() gin.HandlerFunc { | ||||
| 	return func(c *gin.Context) { | ||||
| 		method := c.Request.Method | ||||
| 		origin := c.Request.Header.Get("Origin") | ||||
| 		if origin != "" { | ||||
| 			// 设置允许的请求源 | ||||
| 			c.Header("Access-Control-Allow-Origin", origin) | ||||
| 			c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE") | ||||
| 			//允许跨域设置可以返回其他子段,可以自定义字段 | ||||
| 			c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, Content-Type, Chat-Token, Admin-Authorization") | ||||
| 			// 允许浏览器(客户端)可以解析的头部 (重要) | ||||
| 			c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers") | ||||
| 			//设置缓存时间 | ||||
| 			c.Header("Access-Control-Max-Age", "172800") | ||||
| 			//允许客户端传递校验信息比如 cookie (重要) | ||||
| 			c.Header("Access-Control-Allow-Credentials", "true") | ||||
| 		} | ||||
|  | ||||
| 		if method == http.MethodOptions { | ||||
| 			c.JSON(http.StatusOK, "ok!") | ||||
| 		} | ||||
|  | ||||
| 		defer func() { | ||||
| 			if err := recover(); err != nil { | ||||
| 				logger.Info("Panic info is: %v", err) | ||||
| 			} | ||||
| 		}() | ||||
|  | ||||
| 		c.Next() | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // 用户授权验证 | ||||
| func authorizeMiddleware(s *AppServer, client *redis.Client) gin.HandlerFunc { | ||||
| 	return func(c *gin.Context) { | ||||
| 		if c.Request.URL.Path == "/api/user/login" || | ||||
| 			c.Request.URL.Path == "/api/admin/login" || | ||||
| 			c.Request.URL.Path == "/api/user/register" || | ||||
| 			c.Request.URL.Path == "/api/reward/notify" || | ||||
| 			c.Request.URL.Path == "/api/mj/notify" || | ||||
| 			c.Request.URL.Path == "/api/chat/history" || | ||||
| 			c.Request.URL.Path == "/api/chat/detail" || | ||||
| 			c.Request.URL.Path == "/api/mj/proxy" || | ||||
| 			strings.HasPrefix(c.Request.URL.Path, "/api/sms/") || | ||||
| 			strings.HasPrefix(c.Request.URL.Path, "/api/captcha/") || | ||||
| 			strings.HasPrefix(c.Request.URL.Path, "/static/") || | ||||
| 			c.Request.URL.Path == "/api/admin/config/get" { | ||||
| 			c.Next() | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		var tokenString string | ||||
| 		if strings.Contains(c.Request.URL.Path, "/api/admin/") { // 后台管理 API | ||||
| 			tokenString = c.GetHeader(types.AdminAuthHeader) | ||||
| 		} else if c.Request.URL.Path == "/api/chat/new" || c.Request.URL.Path == "/api/mj/client" { | ||||
| 			tokenString = c.Query("token") | ||||
| 		} else { | ||||
| 			tokenString = c.GetHeader(types.UserAuthHeader) | ||||
| 		} | ||||
| 		if tokenString == "" { | ||||
| 			resp.ERROR(c, "You should put Authorization in request headers") | ||||
| 			c.Abort() | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { | ||||
| 			if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { | ||||
| 				return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) | ||||
| 			} | ||||
|  | ||||
| 			return []byte(s.Config.Session.SecretKey), nil | ||||
| 		}) | ||||
|  | ||||
| 		if err != nil { | ||||
| 			resp.NotAuth(c, fmt.Sprintf("Error with parse auth token: %v", err)) | ||||
| 			c.Abort() | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		claims, ok := token.Claims.(jwt.MapClaims) | ||||
| 		if !ok || !token.Valid { | ||||
| 			resp.NotAuth(c, "Token is invalid") | ||||
| 			c.Abort() | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		expr := utils.IntValue(utils.InterfaceToString(claims["expired"]), 0) | ||||
| 		if expr > 0 && int64(expr) < time.Now().Unix() { | ||||
| 			resp.NotAuth(c, "Token is expired") | ||||
| 			c.Abort() | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		key := fmt.Sprintf("users/%v", claims["user_id"]) | ||||
| 		if _, err := client.Get(context.Background(), key).Result(); err != nil { | ||||
| 			resp.NotAuth(c, "Token is not found in redis") | ||||
| 			c.Abort() | ||||
| 			return | ||||
| 		} | ||||
| 		c.Set(types.LoginUserID, claims["user_id"]) | ||||
| 	} | ||||
| } | ||||
| @@ -5,7 +5,6 @@ import ( | ||||
| 	"chatplus/core/types" | ||||
| 	logger2 "chatplus/logger" | ||||
| 	"chatplus/utils" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 
 | ||||
| 	"github.com/BurntSushi/toml" | ||||
| @@ -15,23 +14,25 @@ var logger = logger2.GetLogger() | ||||
| 
 | ||||
| func NewDefaultConfig() *types.AppConfig { | ||||
| 	return &types.AppConfig{ | ||||
| 		Listen:    "0.0.0.0:5678", | ||||
| 		ProxyURL:  "", | ||||
| 		Manager:   types.Manager{Username: "admin", Password: "admin123"}, | ||||
| 		StaticDir: "./static", | ||||
| 		StaticUrl: "http://localhost/5678/static", | ||||
| 		Redis:     types.RedisConfig{Host: "localhost", Port: 6379, Password: ""}, | ||||
| 
 | ||||
| 		Listen:        "0.0.0.0:5678", | ||||
| 		ProxyURL:      "", | ||||
| 		Manager:       types.Manager{Username: "admin", Password: "admin123"}, | ||||
| 		StaticDir:     "./static", | ||||
| 		StaticUrl:     "http://localhost/5678/static", | ||||
| 		Redis:         types.RedisConfig{Host: "localhost", Port: 6379, Password: ""}, | ||||
| 		AesEncryptKey: utils.RandString(24), | ||||
| 		Session: types.Session{ | ||||
| 			Driver:    types.SessionDriverCookie, | ||||
| 			SecretKey: utils.RandString(64), | ||||
| 			Name:      "CHAT_PLUS_SESSION", | ||||
| 			Domain:    "", | ||||
| 			Path:      "/", | ||||
| 			MaxAge:    86400, | ||||
| 			Secure:    true, | ||||
| 			HttpOnly:  false, | ||||
| 			SameSite:  http.SameSiteLaxMode, | ||||
| 		}, | ||||
| 		ApiConfig: types.ChatPlusApiConfig{}, | ||||
| 		ExtConfig: types.ChatPlusExtConfig{Token: utils.RandString(32)}, | ||||
| 		OSS: types.OSSConfig{ | ||||
| 			Active: "local", | ||||
| 			Local: types.LocalStorageConfig{ | ||||
| 				BaseURL:  "http://localhost/5678/static/upload", | ||||
| 				BasePath: "./static/upload", | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										70
									
								
								api/core/types/chat.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								api/core/types/chat.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | ||||
| package types | ||||
|  | ||||
| // ApiRequest API 请求实体 | ||||
| type ApiRequest struct { | ||||
| 	Model       string        `json:"model"` | ||||
| 	Temperature float32       `json:"temperature"` | ||||
| 	MaxTokens   int           `json:"max_tokens"` | ||||
| 	Stream      bool          `json:"stream"` | ||||
| 	Messages    []interface{} `json:"messages,omitempty"` | ||||
| 	Prompt      []interface{} `json:"prompt,omitempty"` // 兼容 ChatGLM | ||||
| 	Functions   []Function    `json:"functions,omitempty"` | ||||
| } | ||||
|  | ||||
| type Message struct { | ||||
| 	Role    string `json:"role"` | ||||
| 	Content string `json:"content"` | ||||
| } | ||||
|  | ||||
| type ApiResponse struct { | ||||
| 	Choices []ChoiceItem `json:"choices"` | ||||
| } | ||||
|  | ||||
| // ChoiceItem API 响应实体 | ||||
| type ChoiceItem struct { | ||||
| 	Delta        Delta  `json:"delta"` | ||||
| 	FinishReason string `json:"finish_reason"` | ||||
| } | ||||
|  | ||||
| type Delta struct { | ||||
| 	Role         string       `json:"role"` | ||||
| 	Name         string       `json:"name"` | ||||
| 	Content      interface{}  `json:"content"` | ||||
| 	FunctionCall FunctionCall `json:"function_call,omitempty"` | ||||
| } | ||||
|  | ||||
| // ChatSession 聊天会话对象 | ||||
| type ChatSession struct { | ||||
| 	SessionId string    `json:"session_id"` | ||||
| 	ClientIP  string    `json:"client_ip"` // 客户端 IP | ||||
| 	Username  string    `json:"username"`  // 当前登录的 username | ||||
| 	UserId    uint      `json:"user_id"`   // 当前登录的 user ID | ||||
| 	ChatId    string    `json:"chat_id"`   // 客户端聊天会话 ID, 多会话模式专用字段 | ||||
| 	Model     ChatModel `json:"model"`     // GPT 模型 | ||||
| } | ||||
|  | ||||
| type ChatModel struct { | ||||
| 	Id       uint     `json:"id"` | ||||
| 	Platform Platform `json:"platform"` | ||||
| 	Value    string   `json:"value"` | ||||
| } | ||||
|  | ||||
| type ApiError struct { | ||||
| 	Error struct { | ||||
| 		Message string | ||||
| 		Type    string | ||||
| 		Param   interface{} | ||||
| 		Code    string | ||||
| 	} | ||||
| } | ||||
|  | ||||
| const PromptMsg = "prompt" // prompt message | ||||
| const ReplyMsg = "reply"   // reply message | ||||
| const MjMsg = "mj" | ||||
|  | ||||
| var ModelToTokens = map[string]int{ | ||||
| 	"gpt-3.5-turbo":     4096, | ||||
| 	"gpt-3.5-turbo-16k": 16384, | ||||
| 	"gpt-4":             8192, | ||||
| 	"gpt-4-32k":         32768, | ||||
| } | ||||
| @@ -6,18 +6,14 @@ import ( | ||||
| 	"sync" | ||||
| ) | ||||
| 
 | ||||
| var ErrConClosed = errors.New("connection closed") | ||||
| 
 | ||||
| type Client interface { | ||||
| 	Close() | ||||
| } | ||||
| var ErrConClosed = errors.New("connection Closed") | ||||
| 
 | ||||
| // WsClient websocket client | ||||
| type WsClient struct { | ||||
| 	Conn   *websocket.Conn | ||||
| 	lock   sync.Mutex | ||||
| 	mt     int | ||||
| 	closed bool | ||||
| 	Closed bool | ||||
| } | ||||
| 
 | ||||
| func NewWsClient(conn *websocket.Conn) *WsClient { | ||||
| @@ -25,7 +21,7 @@ func NewWsClient(conn *websocket.Conn) *WsClient { | ||||
| 		Conn:   conn, | ||||
| 		lock:   sync.Mutex{}, | ||||
| 		mt:     2, // fixed bug for 'Invalid UTF-8 in text frame' | ||||
| 		closed: false, | ||||
| 		Closed: false, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| @@ -33,7 +29,7 @@ func (wc *WsClient) Send(message []byte) error { | ||||
| 	wc.lock.Lock() | ||||
| 	defer wc.lock.Unlock() | ||||
| 
 | ||||
| 	if wc.closed { | ||||
| 	if wc.Closed { | ||||
| 		return ErrConClosed | ||||
| 	} | ||||
| 
 | ||||
| @@ -41,7 +37,7 @@ func (wc *WsClient) Send(message []byte) error { | ||||
| } | ||||
| 
 | ||||
| func (wc *WsClient) Receive() (int, []byte, error) { | ||||
| 	if wc.closed { | ||||
| 	if wc.Closed { | ||||
| 		return 0, nil, ErrConClosed | ||||
| 	} | ||||
| 
 | ||||
| @@ -52,10 +48,10 @@ func (wc *WsClient) Close() { | ||||
| 	wc.lock.Lock() | ||||
| 	defer wc.lock.Unlock() | ||||
| 
 | ||||
| 	if wc.closed { | ||||
| 	if wc.Closed { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	_ = wc.Conn.Close() | ||||
| 	wc.closed = true | ||||
| 	wc.Closed = true | ||||
| } | ||||
							
								
								
									
										127
									
								
								api/core/types/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								api/core/types/config.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,127 @@ | ||||
| package types | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| ) | ||||
|  | ||||
| type AppConfig struct { | ||||
| 	Path          string `toml:"-"` | ||||
| 	Listen        string | ||||
| 	Session       Session | ||||
| 	ProxyURL      string | ||||
| 	MysqlDns      string            // mysql 连接地址 | ||||
| 	Manager       Manager           // 后台管理员账户信息 | ||||
| 	StaticDir     string            // 静态资源目录 | ||||
| 	StaticUrl     string            // 静态资源 URL | ||||
| 	Redis         RedisConfig       // redis 连接信息 | ||||
| 	ApiConfig     ChatPlusApiConfig // ChatPlus API authorization configs | ||||
| 	AesEncryptKey string | ||||
| 	SmsConfig     AliYunSmsConfig   // AliYun send message service config | ||||
| 	ExtConfig     ChatPlusExtConfig // ChatPlus extensions callback api config | ||||
|  | ||||
| 	OSS OSSConfig // OSS config | ||||
| } | ||||
|  | ||||
| type ChatPlusApiConfig struct { | ||||
| 	ApiURL string | ||||
| 	AppId  string | ||||
| 	Token  string | ||||
| } | ||||
|  | ||||
| type ChatPlusExtConfig struct { | ||||
| 	ApiURL string | ||||
| 	Token  string | ||||
| } | ||||
|  | ||||
| type AliYunSmsConfig struct { | ||||
| 	AccessKey    string | ||||
| 	AccessSecret string | ||||
| 	Product      string | ||||
| 	Domain       string | ||||
| } | ||||
|  | ||||
| type OSSConfig struct { | ||||
| 	Active string | ||||
| 	Local  LocalStorageConfig | ||||
| 	Minio  MinioConfig | ||||
| 	QiNiu  QiNiuConfig | ||||
| } | ||||
| type MinioConfig struct { | ||||
| 	Endpoint     string | ||||
| 	AccessKey    string | ||||
| 	AccessSecret string | ||||
| 	Bucket       string | ||||
| 	UseSSL       bool | ||||
| 	Domain       string | ||||
| } | ||||
|  | ||||
| type QiNiuConfig struct { | ||||
| 	Zone         string | ||||
| 	AccessKey    string | ||||
| 	AccessSecret string | ||||
| 	Bucket       string | ||||
| 	Domain       string | ||||
| } | ||||
|  | ||||
| type LocalStorageConfig struct { | ||||
| 	BasePath string | ||||
| 	BaseURL  string | ||||
| } | ||||
|  | ||||
| type RedisConfig struct { | ||||
| 	Host     string | ||||
| 	Port     int | ||||
| 	Password string | ||||
| 	DB       int | ||||
| } | ||||
|  | ||||
| func (c RedisConfig) Url() string { | ||||
| 	return fmt.Sprintf("%s:%d", c.Host, c.Port) | ||||
| } | ||||
|  | ||||
| // Manager 管理员 | ||||
| type Manager struct { | ||||
| 	Username string `json:"username"` | ||||
| 	Password string `json:"password"` | ||||
| } | ||||
|  | ||||
| // ChatConfig 系统默认的聊天配置 | ||||
| type ChatConfig struct { | ||||
| 	OpenAI  ModelAPIConfig `json:"open_ai"` | ||||
| 	Azure   ModelAPIConfig `json:"azure"` | ||||
| 	ChatGML ModelAPIConfig `json:"chat_gml"` | ||||
|  | ||||
| 	EnableContext bool `json:"enable_context"` // 是否开启聊天上下文 | ||||
| 	EnableHistory bool `json:"enable_history"` // 是否允许保存聊天记录 | ||||
| 	ContextDeep   int  `json:"context_deep"`   // 上下文深度 | ||||
| } | ||||
|  | ||||
| type Platform string | ||||
|  | ||||
| const OpenAI = Platform("OpenAI") | ||||
| const Azure = Platform("Azure") | ||||
| const ChatGLM = Platform("ChatGLM") | ||||
|  | ||||
| // UserChatConfig 用户的聊天配置 | ||||
| type UserChatConfig struct { | ||||
| 	ApiKeys map[Platform]string `json:"api_keys"` | ||||
| } | ||||
|  | ||||
| type ModelAPIConfig struct { | ||||
| 	ApiURL      string  `json:"api_url,omitempty"` | ||||
| 	Temperature float32 `json:"temperature"` | ||||
| 	MaxTokens   int     `json:"max_tokens"` | ||||
| 	ApiKey      string  `json:"api_key"` | ||||
| } | ||||
|  | ||||
| type SystemConfig struct { | ||||
| 	Title             string   `json:"title"` | ||||
| 	AdminTitle        string   `json:"admin_title"` | ||||
| 	Models            []string `json:"models"` | ||||
| 	UserInitCalls     int      `json:"user_init_calls"` // 新用户注册默认总送多少次调用 | ||||
| 	InitImgCalls      int      `json:"init_img_calls"` | ||||
| 	VipMonthCalls     int      `json:"vip_month_calls"` // 会员每个赠送的调用次数 | ||||
| 	EnabledRegister   bool     `json:"enabled_register"` | ||||
| 	EnabledMsgService bool     `json:"enabled_msg_service"` | ||||
| 	EnabledDraw       bool     `json:"enabled_draw"` // 启动 AI 绘画功能 | ||||
| } | ||||
							
								
								
									
										92
									
								
								api/core/types/function.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								api/core/types/function.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | ||||
| package types | ||||
|  | ||||
| type FunctionCall struct { | ||||
| 	Name      string `json:"name"` | ||||
| 	Arguments string `json:"arguments"` | ||||
| } | ||||
|  | ||||
| type Function struct { | ||||
| 	Name        string     `json:"name"` | ||||
| 	Description string     `json:"description"` | ||||
| 	Parameters  Parameters `json:"parameters"` | ||||
| } | ||||
|  | ||||
| type Parameters struct { | ||||
| 	Type       string              `json:"type"` | ||||
| 	Required   []string            `json:"required"` | ||||
| 	Properties map[string]Property `json:"properties"` | ||||
| } | ||||
|  | ||||
| type Property struct { | ||||
| 	Type        string `json:"type"` | ||||
| 	Description string `json:"description"` | ||||
| } | ||||
|  | ||||
| const ( | ||||
| 	FuncZaoBao     = "zao_bao"     // 每日早报 | ||||
| 	FuncHeadLine   = "headline"    // 今日头条 | ||||
| 	FuncWeibo      = "weibo_hot"   // 微博热搜 | ||||
| 	FuncMidJourney = "mid_journey" // MJ 绘画 | ||||
| ) | ||||
|  | ||||
| var InnerFunctions = []Function{ | ||||
| 	{ | ||||
| 		Name:        FuncZaoBao, | ||||
| 		Description: "每日早报,获取当天全球的热门新闻事件列表", | ||||
| 		Parameters: Parameters{ | ||||
|  | ||||
| 			Type: "object", | ||||
| 			Properties: map[string]Property{ | ||||
| 				"text": { | ||||
| 					Type:        "string", | ||||
| 					Description: "", | ||||
| 				}, | ||||
| 			}, | ||||
| 			Required: []string{}, | ||||
| 		}, | ||||
| 	}, | ||||
| 	{ | ||||
| 		Name:        FuncWeibo, | ||||
| 		Description: "新浪微博热搜榜,微博当日热搜榜单", | ||||
| 		Parameters: Parameters{ | ||||
| 			Type: "object", | ||||
| 			Properties: map[string]Property{ | ||||
| 				"text": { | ||||
| 					Type:        "string", | ||||
| 					Description: "", | ||||
| 				}, | ||||
| 			}, | ||||
| 			Required: []string{}, | ||||
| 		}, | ||||
| 	}, | ||||
|  | ||||
| 	{ | ||||
| 		Name:        FuncHeadLine, | ||||
| 		Description: "今日头条,给用户推荐当天的头条新闻,周榜热文", | ||||
| 		Parameters: Parameters{ | ||||
| 			Type: "object", | ||||
| 			Properties: map[string]Property{ | ||||
| 				"text": { | ||||
| 					Type:        "string", | ||||
| 					Description: "", | ||||
| 				}, | ||||
| 			}, | ||||
| 			Required: []string{}, | ||||
| 		}, | ||||
| 	}, | ||||
|  | ||||
| 	{ | ||||
| 		Name:        FuncMidJourney, | ||||
| 		Description: "AI 绘画工具,使用 MJ MidJourney API 进行 AI 绘画", | ||||
| 		Parameters: Parameters{ | ||||
| 			Type: "object", | ||||
| 			Properties: map[string]Property{ | ||||
| 				"prompt": { | ||||
| 					Type:        "string", | ||||
| 					Description: "提示词,如果该参数中有中文的话,则需要翻译成英文。提示词中的参数作为提示的一部分,不要删除", | ||||
| 				}, | ||||
| 			}, | ||||
| 			Required: []string{}, | ||||
| 		}, | ||||
| 	}, | ||||
| } | ||||
| @@ -9,7 +9,7 @@ type MKey interface { | ||||
| 	string | int | ||||
| } | ||||
| type MValue interface { | ||||
| 	*WsClient | ChatSession | []Message | context.CancelFunc | ||||
| 	*WsClient | *ChatSession | context.CancelFunc | []interface{} | ||||
| } | ||||
| type LMap[K MKey, T MValue] struct { | ||||
| 	lock sync.RWMutex | ||||
							
								
								
									
										14
									
								
								api/core/types/session.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								api/core/types/session.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| package types | ||||
|  | ||||
| const LoginUserID = "LOGIN_USER_ID" | ||||
| const LoginUserCache = "LOGIN_USER_CACHE" | ||||
|  | ||||
| const UserAuthHeader = "Authorization" | ||||
| const AdminAuthHeader = "Admin-Authorization" | ||||
| const ChatTokenHeader = "Chat-Token" | ||||
|  | ||||
| // Session configs struct | ||||
| type Session struct { | ||||
| 	SecretKey string | ||||
| 	MaxAge    int | ||||
| } | ||||
| @@ -12,8 +12,8 @@ type BizVo struct { | ||||
| 
 | ||||
| // WsMessage Websocket message | ||||
| type WsMessage struct { | ||||
| 	Type    WsMsgType `json:"type"` // 消息类别,start, end | ||||
| 	Content string    `json:"content"` | ||||
| 	Type    WsMsgType   `json:"type"` // 消息类别,start, end, img | ||||
| 	Content interface{} `json:"content"` | ||||
| } | ||||
| type WsMsgType string | ||||
| 
 | ||||
| @@ -21,6 +21,7 @@ const ( | ||||
| 	WsStart  = WsMsgType("start") | ||||
| 	WsMiddle = WsMsgType("middle") | ||||
| 	WsEnd    = WsMsgType("end") | ||||
| 	WsMjImg  = WsMsgType("mj") | ||||
| ) | ||||
| 
 | ||||
| type BizCode int | ||||
| @@ -33,4 +34,5 @@ const ( | ||||
| 	OkMsg       = "Success" | ||||
| 	ErrorMsg    = "系统开小差了" | ||||
| 	InvalidArgs = "非法参数或参数解析失败" | ||||
| 	NoData      = "No Data" | ||||
| ) | ||||
							
								
								
									
										90
									
								
								api/go.mod
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								api/go.mod
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | ||||
| module chatplus | ||||
|  | ||||
| go 1.19 | ||||
|  | ||||
| require ( | ||||
| 	github.com/BurntSushi/toml v1.1.0 | ||||
| 	github.com/aliyun/alibaba-cloud-sdk-go v1.62.405 | ||||
| 	github.com/gin-gonic/gin v1.9.1 | ||||
| 	github.com/go-redis/redis/v8 v8.11.5 | ||||
| 	github.com/golang-jwt/jwt/v5 v5.0.0 | ||||
| 	github.com/gorilla/websocket v1.5.0 | ||||
| 	github.com/imroc/req/v3 v3.37.2 | ||||
| 	github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0 | ||||
| 	github.com/minio/minio-go/v7 v7.0.62 | ||||
| 	github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480 | ||||
| 	github.com/qiniu/go-sdk/v7 v7.17.1 | ||||
| 	github.com/syndtr/goleveldb v1.0.0 | ||||
| 	go.uber.org/zap v1.23.0 | ||||
| 	gopkg.in/natefinch/lumberjack.v2 v2.2.1 | ||||
| 	gorm.io/driver/mysql v1.4.7 | ||||
| ) | ||||
|  | ||||
| require ( | ||||
| 	github.com/andybalholm/brotli v1.0.4 // indirect | ||||
| 	github.com/bytedance/sonic v1.9.1 // indirect | ||||
| 	github.com/cespare/xxhash/v2 v2.1.2 // indirect | ||||
| 	github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect | ||||
| 	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect | ||||
| 	github.com/dlclark/regexp2 v1.8.1 // indirect | ||||
| 	github.com/dustin/go-humanize v1.0.1 // indirect | ||||
| 	github.com/gabriel-vasile/mimetype v1.4.2 // indirect | ||||
| 	github.com/gaukas/godicttls v0.0.3 // indirect | ||||
| 	github.com/go-sql-driver/mysql v1.7.0 // indirect | ||||
| 	github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect | ||||
| 	github.com/goccy/go-json v0.10.2 // indirect | ||||
| 	github.com/golang/mock v1.6.0 // indirect | ||||
| 	github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 // indirect | ||||
| 	github.com/google/uuid v1.3.0 // indirect | ||||
| 	github.com/hashicorp/errwrap v1.1.0 // indirect | ||||
| 	github.com/hashicorp/go-multierror v1.1.1 // indirect | ||||
| 	github.com/jinzhu/inflection v1.0.0 // indirect | ||||
| 	github.com/jinzhu/now v1.1.5 // indirect | ||||
| 	github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af // indirect | ||||
| 	github.com/klauspost/compress v1.16.7 // indirect | ||||
| 	github.com/klauspost/cpuid/v2 v2.2.5 // indirect | ||||
| 	github.com/minio/md5-simd v1.1.2 // indirect | ||||
| 	github.com/minio/sha256-simd v1.0.1 // indirect | ||||
| 	github.com/onsi/ginkgo/v2 v2.10.0 // indirect | ||||
| 	github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect | ||||
| 	github.com/pelletier/go-toml/v2 v2.0.8 // indirect | ||||
| 	github.com/quic-go/qpack v0.4.0 // indirect | ||||
| 	github.com/quic-go/qtls-go1-19 v0.3.2 // indirect | ||||
| 	github.com/quic-go/qtls-go1-20 v0.2.2 // indirect | ||||
| 	github.com/quic-go/quic-go v0.35.1 // indirect | ||||
| 	github.com/refraction-networking/utls v1.3.2 // indirect | ||||
| 	github.com/rs/xid v1.5.0 // indirect | ||||
| 	github.com/sirupsen/logrus v1.9.3 // indirect | ||||
| 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect | ||||
| 	go.uber.org/dig v1.16.1 // indirect | ||||
| 	golang.org/x/arch v0.3.0 // indirect | ||||
| 	golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect | ||||
| 	golang.org/x/mod v0.11.0 // indirect | ||||
| 	golang.org/x/net v0.14.0 // indirect | ||||
| 	golang.org/x/sync v0.3.0 // indirect | ||||
| 	golang.org/x/text v0.12.0 // indirect | ||||
| 	golang.org/x/tools v0.10.0 // indirect | ||||
| 	google.golang.org/protobuf v1.30.0 // indirect | ||||
| 	gopkg.in/ini.v1 v1.67.0 // indirect | ||||
| 	gopkg.in/yaml.v3 v3.0.1 // indirect | ||||
| ) | ||||
|  | ||||
| require ( | ||||
| 	github.com/gin-contrib/sse v0.1.0 // indirect | ||||
| 	github.com/go-playground/locales v0.14.1 // indirect | ||||
| 	github.com/go-playground/universal-translator v0.18.1 // indirect | ||||
| 	github.com/go-playground/validator/v10 v10.14.0 // indirect | ||||
| 	github.com/golang/snappy v0.0.1 // indirect | ||||
| 	github.com/json-iterator/go v1.1.12 // indirect | ||||
| 	github.com/leodido/go-urn v1.2.4 // indirect | ||||
| 	github.com/mattn/go-isatty v0.0.19 // indirect | ||||
| 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect | ||||
| 	github.com/modern-go/reflect2 v1.0.2 // indirect | ||||
| 	github.com/ugorji/go/codec v1.2.11 // indirect | ||||
| 	go.uber.org/atomic v1.9.0 // indirect | ||||
| 	go.uber.org/fx v1.19.3 | ||||
| 	go.uber.org/multierr v1.6.0 // indirect | ||||
| 	golang.org/x/crypto v0.12.0 | ||||
| 	golang.org/x/sys v0.11.0 // indirect | ||||
| 	gorm.io/gorm v1.25.1 | ||||
| ) | ||||
							
								
								
									
										299
									
								
								api/go.sum
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										299
									
								
								api/go.sum
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,299 @@ | ||||
| github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= | ||||
| github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= | ||||
| github.com/aliyun/alibaba-cloud-sdk-go v1.62.405 h1:cKNFQmeCQFN0WNfjScKoVrGi7vXxTVbkCvCqSrOf+P4= | ||||
| github.com/aliyun/alibaba-cloud-sdk-go v1.62.405/go.mod h1:Api2AkmMgGaSUAhmk76oaFObkoeCPc/bKAqcyplPODs= | ||||
| github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= | ||||
| github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= | ||||
| github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= | ||||
| github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= | ||||
| github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= | ||||
| github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= | ||||
| github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= | ||||
| github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= | ||||
| github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= | ||||
| github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= | ||||
| github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= | ||||
| github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= | ||||
| github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||||
| github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= | ||||
| github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= | ||||
| github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0= | ||||
| github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= | ||||
| github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= | ||||
| github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= | ||||
| github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= | ||||
| github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= | ||||
| github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= | ||||
| github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= | ||||
| github.com/gaukas/godicttls v0.0.3 h1:YNDIf0d9adcxOijiLrEzpfZGAkNwLRzPaG6OjU7EITk= | ||||
| github.com/gaukas/godicttls v0.0.3/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI= | ||||
| github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= | ||||
| github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= | ||||
| github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= | ||||
| github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= | ||||
| github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= | ||||
| github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= | ||||
| github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= | ||||
| github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= | ||||
| github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= | ||||
| github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= | ||||
| github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= | ||||
| github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= | ||||
| github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= | ||||
| github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= | ||||
| github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= | ||||
| github.com/go-playground/validator/v10 v10.8.0/go.mod h1:9JhgTzTaE31GZDpH/HSvHiRJrJ3iKAgqqH0Bl/Ocjdk= | ||||
| github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= | ||||
| github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= | ||||
| github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= | ||||
| github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= | ||||
| github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= | ||||
| github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= | ||||
| github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= | ||||
| github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= | ||||
| github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= | ||||
| github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= | ||||
| github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= | ||||
| github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= | ||||
| github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= | ||||
| github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= | ||||
| github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= | ||||
| github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | ||||
| github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= | ||||
| github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= | ||||
| github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= | ||||
| github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= | ||||
| github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= | ||||
| github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||
| github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= | ||||
| github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | ||||
| github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 h1:hR7/MlvK23p6+lIw9SN1TigNLn9ZnF3W4SYRKq2gAHs= | ||||
| github.com/google/pprof v0.0.0-20230602150820-91b7bce49751/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA= | ||||
| github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= | ||||
| github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||
| github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= | ||||
| github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= | ||||
| github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= | ||||
| github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= | ||||
| github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= | ||||
| github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= | ||||
| github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= | ||||
| github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= | ||||
| github.com/imroc/req/v3 v3.37.2 h1:vEemuA0cq9zJ6lhe+mSRhsZm951bT0CdiSH47+KTn6I= | ||||
| github.com/imroc/req/v3 v3.37.2/go.mod h1:DECzjVIrj6jcUr5n6e+z0ygmCO93rx4Jy0RjOEe1YCI= | ||||
| github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= | ||||
| github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= | ||||
| github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= | ||||
| github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= | ||||
| github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= | ||||
| github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= | ||||
| github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= | ||||
| github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= | ||||
| github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= | ||||
| github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= | ||||
| github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= | ||||
| github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= | ||||
| github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= | ||||
| github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= | ||||
| github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= | ||||
| github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= | ||||
| github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= | ||||
| github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= | ||||
| github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= | ||||
| github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= | ||||
| github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= | ||||
| github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= | ||||
| github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= | ||||
| github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= | ||||
| github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= | ||||
| github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= | ||||
| github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= | ||||
| github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0 h1:LgmjED/yQILqmUED4GaXjrINWe7YJh4HM6z2EvEINPs= | ||||
| github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0/go.mod h1:C5LA5UO2ZXJrLaPLYtE1wUJMiyd/nwWaCO5cw/2pSHs= | ||||
| github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= | ||||
| github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= | ||||
| github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= | ||||
| github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= | ||||
| github.com/minio/minio-go/v7 v7.0.62 h1:qNYsFZHEzl+NfH8UxW4jpmlKav1qUAgfY30YNRneVhc= | ||||
| github.com/minio/minio-go/v7 v7.0.62/go.mod h1:Q6X7Qjb7WMhvG65qKf4gUgA5XaiSox74kR1uAEjxRS4= | ||||
| github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= | ||||
| github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= | ||||
| github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | ||||
| github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= | ||||
| github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | ||||
| github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= | ||||
| github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= | ||||
| github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= | ||||
| github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= | ||||
| github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= | ||||
| github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= | ||||
| github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= | ||||
| github.com/onsi/ginkgo/v2 v2.10.0 h1:sfUl4qgLdvkChZrWCYndY2EAu9BRIw1YphNAzy1VNWs= | ||||
| github.com/onsi/ginkgo/v2 v2.10.0/go.mod h1:UDQOh5wbQUlMnkLfVaIUMtQ1Vus92oM+P2JX1aulgcE= | ||||
| github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= | ||||
| github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU= | ||||
| github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A= | ||||
| github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU= | ||||
| github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= | ||||
| github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= | ||||
| github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= | ||||
| github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= | ||||
| github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||||
| github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480 h1:IFhPCcB0/HtnEN+ZoUGDT55YgFCymbFJ15kXqs3nv5w= | ||||
| github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480/go.mod h1:BijIqAP84FMYC4XbdJgjyMpiSjusU8x0Y0W9K2t0QtU= | ||||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||
| github.com/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdkk= | ||||
| github.com/qiniu/go-sdk/v7 v7.17.1 h1:UoQv7fBKtzAiD1qZPIvTy62Se48YLKxcCYP9nAwWMa0= | ||||
| github.com/qiniu/go-sdk/v7 v7.17.1/go.mod h1:nqoYCNo53ZlGA521RvRethvxUDvXKt4gtYXOwye868w= | ||||
| github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs= | ||||
| github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= | ||||
| github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= | ||||
| github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U= | ||||
| github.com/quic-go/qtls-go1-19 v0.3.2/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI= | ||||
| github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8GEa3E= | ||||
| github.com/quic-go/qtls-go1-20 v0.2.2/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM= | ||||
| github.com/quic-go/quic-go v0.35.1 h1:b0kzj6b/cQAf05cT0CkQubHM31wiA+xH3IBkxP62poo= | ||||
| github.com/quic-go/quic-go v0.35.1/go.mod h1:+4CVgVppm0FNjpG3UcX8Joi/frKOH7/ciD5yGcwOO1g= | ||||
| github.com/refraction-networking/utls v1.3.2 h1:o+AkWB57mkcoW36ET7uJ002CpBWHu0KPxi6vzxvPnv8= | ||||
| github.com/refraction-networking/utls v1.3.2/go.mod h1:fmoaOww2bxzzEpIKOebIsnBvjQpqP7L2vcm/9KUfm/E= | ||||
| github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= | ||||
| github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= | ||||
| github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= | ||||
| github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= | ||||
| github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= | ||||
| github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= | ||||
| github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= | ||||
| github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||
| github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= | ||||
| github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= | ||||
| github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | ||||
| github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||
| github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||
| github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||
| github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= | ||||
| github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | ||||
| github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | ||||
| github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= | ||||
| github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= | ||||
| github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= | ||||
| github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= | ||||
| github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= | ||||
| github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= | ||||
| github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= | ||||
| github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= | ||||
| github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= | ||||
| github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= | ||||
| github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= | ||||
| github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= | ||||
| github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= | ||||
| github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= | ||||
| go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= | ||||
| go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= | ||||
| go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= | ||||
| go.uber.org/dig v1.16.1 h1:+alNIBsl0qfY0j6epRubp/9obgtrObRAc5aD+6jbWY8= | ||||
| go.uber.org/dig v1.16.1/go.mod h1:557JTAUZT5bUK0SvCwikmLPPtdQhfvLYtO5tJgQSbnk= | ||||
| go.uber.org/fx v1.19.3 h1:YqMRE4+2IepTYCMOvXqQpRa+QAVdiSTnsHU4XNWBceA= | ||||
| go.uber.org/fx v1.19.3/go.mod h1:w2HrQg26ql9fLK7hlBiZ6JsRUKV+Lj/atT1KCjT8YhM= | ||||
| go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= | ||||
| go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= | ||||
| go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= | ||||
| go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= | ||||
| go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= | ||||
| golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= | ||||
| golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= | ||||
| golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= | ||||
| golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||
| golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||
| golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= | ||||
| golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= | ||||
| golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= | ||||
| golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= | ||||
| golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= | ||||
| golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= | ||||
| golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= | ||||
| golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | ||||
| golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= | ||||
| golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= | ||||
| golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= | ||||
| golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||
| golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | ||||
| golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||
| golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= | ||||
| golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= | ||||
| golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= | ||||
| golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= | ||||
| golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= | ||||
| golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= | ||||
| golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= | ||||
| golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= | ||||
| golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= | ||||
| golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | ||||
| golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= | ||||
| golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= | ||||
| golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||
| golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= | ||||
| golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||
| golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||
| golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= | ||||
| golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= | ||||
| golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= | ||||
| golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= | ||||
| golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||
| golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | ||||
| golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= | ||||
| golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= | ||||
| golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg= | ||||
| golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM= | ||||
| golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= | ||||
| google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= | ||||
| google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= | ||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
| gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
| gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= | ||||
| gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= | ||||
| gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= | ||||
| gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= | ||||
| gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= | ||||
| gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= | ||||
| gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= | ||||
| gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= | ||||
| gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= | ||||
| gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= | ||||
| gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= | ||||
| gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
| gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
| gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
| gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||||
| gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
| gorm.io/driver/mysql v1.4.7 h1:rY46lkCspzGHn7+IYsNpSfEv9tA+SU4SkkB+GFX125Y= | ||||
| gorm.io/driver/mysql v1.4.7/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8oc= | ||||
| gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= | ||||
| gorm.io/gorm v1.25.1 h1:nsSALe5Pr+cM3V1qwwQ7rOkw+6UeLrX5O4v3llhHa64= | ||||
| gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= | ||||
| rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= | ||||
| @@ -1,17 +0,0 @@ | ||||
| Listen = "0.0.0.0:5678" | ||||
| ProxyURL = "YOUR_PROXY_URL" | ||||
| MysqlDns = "mysql_user:mysql_pass@tcp(localhost:3306)/chatgpt_plus?charset=utf8&parseTime=True&loc=Local" | ||||
|  | ||||
| [Session] | ||||
|   SecretKey = "azyehq3ivunjhbntz78isj00i4hz2mt9xtddysfucxakadq4qbfrt0b7q3lnvg80" | ||||
|   Name = "CHAT_SESSION_ID" | ||||
|   Path = "/" | ||||
|   Domain = "" | ||||
|   MaxAge = 86400 | ||||
|   Secure = false | ||||
|   HttpOnly = false | ||||
|   SameSite = 2 | ||||
|  | ||||
| [Manager] | ||||
|   Username = "admin" | ||||
|   Password = "admin123" | ||||
| @@ -1,197 +0,0 @@ | ||||
| package core | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/utils" | ||||
| 	"chatplus/utils/resp" | ||||
| 	"context" | ||||
| 	"github.com/gin-contrib/sessions" | ||||
| 	"github.com/gin-contrib/sessions/cookie" | ||||
| 	"github.com/gin-contrib/sessions/memstore" | ||||
| 	"github.com/gin-contrib/sessions/redis" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"runtime/debug" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| type AppServer struct { | ||||
| 	Debug        bool | ||||
| 	AppConfig    *types.AppConfig | ||||
| 	Engine       *gin.Engine | ||||
| 	ChatContexts *types.LMap[string, []types.Message] // 聊天上下文 Map [chatId] => []Message | ||||
| 	ChatConfig   *types.ChatConfig                    // 聊天配置 | ||||
|  | ||||
| 	// 保存 Websocket 会话 UserId, 每个 UserId 只能连接一次 | ||||
| 	// 防止第三方直接连接 socket 调用 OpenAI API | ||||
| 	ChatSession   *types.LMap[string, types.ChatSession]  //map[sessionId]UserId | ||||
| 	ChatClients   *types.LMap[string, *types.WsClient]    // Websocket 连接集合 | ||||
| 	ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function | ||||
| } | ||||
|  | ||||
| func NewServer(appConfig *types.AppConfig) *AppServer { | ||||
| 	gin.SetMode(gin.ReleaseMode) | ||||
| 	gin.DefaultWriter = io.Discard | ||||
| 	return &AppServer{ | ||||
| 		Debug:         false, | ||||
| 		AppConfig:     appConfig, | ||||
| 		Engine:        gin.Default(), | ||||
| 		ChatContexts:  types.NewLMap[string, []types.Message](), | ||||
| 		ChatSession:   types.NewLMap[string, types.ChatSession](), | ||||
| 		ChatClients:   types.NewLMap[string, *types.WsClient](), | ||||
| 		ReqCancelFunc: types.NewLMap[string, context.CancelFunc](), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (s *AppServer) Init(debug bool) { | ||||
| 	if debug { // 调试模式允许跨域请求 API | ||||
| 		s.Debug = debug | ||||
| 		logger.Info("Enabled debug mode") | ||||
| 		s.Engine.Use(corsMiddleware()) | ||||
| 	} | ||||
|  | ||||
| 	s.Engine.Use(sessionMiddleware(s.AppConfig)) | ||||
| 	s.Engine.Use(authorizeMiddleware(s)) | ||||
| 	s.Engine.Use(errorHandler) | ||||
| 	// 添加静态资源访问 | ||||
| 	s.Engine.Static("/static", s.AppConfig.StaticDir) | ||||
| } | ||||
|  | ||||
| func (s *AppServer) Run(db *gorm.DB) error { | ||||
| 	// load chat config from database | ||||
| 	var config model.Config | ||||
| 	res := db.Where("marker", "chat").First(&config) | ||||
| 	if res.Error != nil { | ||||
| 		return res.Error | ||||
| 	} | ||||
| 	err := utils.JsonDecode(config.Config, &s.ChatConfig) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	logger.Infof("http://%s", s.AppConfig.Listen) | ||||
| 	return s.Engine.Run(s.AppConfig.Listen) | ||||
| } | ||||
|  | ||||
| // 全局异常处理 | ||||
| func errorHandler(c *gin.Context) { | ||||
| 	defer func() { | ||||
| 		if r := recover(); r != nil { | ||||
| 			logger.Error("Handler Panic: %v\n", r) | ||||
| 			debug.PrintStack() | ||||
| 			c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: types.ErrorMsg}) | ||||
| 			c.Abort() | ||||
| 		} | ||||
| 	}() | ||||
| 	//加载完 defer recover,继续后续接口调用 | ||||
| 	c.Next() | ||||
| } | ||||
|  | ||||
| // 会话处理 | ||||
| func sessionMiddleware(config *types.AppConfig) gin.HandlerFunc { | ||||
| 	// encrypt the cookie | ||||
| 	var store sessions.Store | ||||
| 	var err error | ||||
| 	switch config.Session.Driver { | ||||
| 	case types.SessionDriverMem: | ||||
| 		store = memstore.NewStore([]byte(config.Session.SecretKey)) | ||||
| 		break | ||||
| 	case types.SessionDriverRedis: | ||||
| 		store, err = redis.NewStore(10, "tcp", config.Redis.Url(), config.Redis.Password, []byte(config.Session.SecretKey)) | ||||
| 		if err != nil { | ||||
| 			logger.Fatal(err) | ||||
| 		} | ||||
| 		break | ||||
| 	case types.SessionDriverCookie: | ||||
| 		store = cookie.NewStore([]byte(config.Session.SecretKey)) | ||||
| 		break | ||||
| 	default: | ||||
| 		store = cookie.NewStore([]byte(config.Session.SecretKey)) | ||||
| 	} | ||||
|  | ||||
| 	logger.Info("Session driver: ", config.Session.Driver) | ||||
|  | ||||
| 	store.Options(sessions.Options{ | ||||
| 		Path:     config.Session.Path, | ||||
| 		Domain:   config.Session.Domain, | ||||
| 		MaxAge:   config.Session.MaxAge, | ||||
| 		Secure:   config.Session.Secure, | ||||
| 		HttpOnly: config.Session.HttpOnly, | ||||
| 		SameSite: config.Session.SameSite, | ||||
| 	}) | ||||
| 	return sessions.Sessions(config.Session.Name, store) | ||||
| } | ||||
|  | ||||
| // 跨域中间件设置 | ||||
| func corsMiddleware() gin.HandlerFunc { | ||||
| 	return func(c *gin.Context) { | ||||
| 		method := c.Request.Method | ||||
| 		origin := c.Request.Header.Get("Origin") | ||||
| 		if origin != "" { | ||||
| 			// 设置允许的请求源 | ||||
| 			c.Header("Access-Control-Allow-Origin", origin) | ||||
| 			c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE") | ||||
| 			//允许跨域设置可以返回其他子段,可以自定义字段 | ||||
| 			c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, Content-Type, ChatGPT-TOKEN, ADMIN-SESSION-TOKEN") | ||||
| 			// 允许浏览器(客户端)可以解析的头部 (重要) | ||||
| 			c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers") | ||||
| 			//设置缓存时间 | ||||
| 			c.Header("Access-Control-Max-Age", "172800") | ||||
| 			//允许客户端传递校验信息比如 cookie (重要) | ||||
| 			c.Header("Access-Control-Allow-Credentials", "true") | ||||
| 		} | ||||
|  | ||||
| 		if method == http.MethodOptions { | ||||
| 			c.JSON(http.StatusOK, "ok!") | ||||
| 		} | ||||
|  | ||||
| 		defer func() { | ||||
| 			if err := recover(); err != nil { | ||||
| 				logger.Info("Panic info is: %v", err) | ||||
| 			} | ||||
| 		}() | ||||
|  | ||||
| 		c.Next() | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // 用户授权验证 | ||||
| func authorizeMiddleware(s *AppServer) gin.HandlerFunc { | ||||
| 	return func(c *gin.Context) { | ||||
| 		if c.Request.URL.Path == "/api/user/login" || | ||||
| 			c.Request.URL.Path == "/api/admin/login" || | ||||
| 			c.Request.URL.Path == "/api/user/register" || | ||||
| 			strings.HasPrefix(c.Request.URL.Path, "/static/") || | ||||
| 			c.Request.URL.Path == "/api/admin/config/get" { | ||||
| 			c.Next() | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		// WebSocket 连接请求验证 | ||||
| 		if c.Request.URL.Path == "/api/chat" { | ||||
| 			sessionId := c.Query("sessionId") | ||||
| 			session := s.ChatSession.Get(sessionId) | ||||
| 			if session.ClientIP == c.ClientIP() { | ||||
| 				c.Next() | ||||
| 			} else { | ||||
| 				c.Abort() | ||||
| 			} | ||||
| 			return | ||||
| 		} | ||||
| 		session := sessions.Default(c) | ||||
| 		var value interface{} | ||||
| 		if strings.Contains(c.Request.URL.Path, "/api/admin/") { | ||||
| 			value = session.Get(types.SessionAdmin) | ||||
| 		} else { | ||||
| 			value = session.Get(types.SessionUser) | ||||
| 		} | ||||
| 		if value != nil { | ||||
| 			c.Next() | ||||
| 		} else { | ||||
| 			resp.NotAuth(c) | ||||
| 			c.Abort() | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -1,47 +0,0 @@ | ||||
| package types | ||||
|  | ||||
| // ApiRequest API 请求实体 | ||||
| type ApiRequest struct { | ||||
| 	Model       string    `json:"model"` | ||||
| 	Temperature float32   `json:"temperature"` | ||||
| 	MaxTokens   int       `json:"max_tokens"` | ||||
| 	Stream      bool      `json:"stream"` | ||||
| 	Messages    []Message `json:"messages"` | ||||
| } | ||||
|  | ||||
| type Message struct { | ||||
| 	Role    string `json:"role"` | ||||
| 	Content string `json:"content"` | ||||
| } | ||||
|  | ||||
| type ApiResponse struct { | ||||
| 	Choices []ChoiceItem `json:"choices"` | ||||
| } | ||||
|  | ||||
| // ChoiceItem API 响应实体 | ||||
| type ChoiceItem struct { | ||||
| 	Delta        Message `json:"delta"` | ||||
| 	FinishReason string  `json:"finish_reason"` | ||||
| } | ||||
|  | ||||
| // ChatSession 聊天会话对象 | ||||
| type ChatSession struct { | ||||
| 	SessionId string `json:"session_id"` | ||||
| 	ClientIP  string `json:"client_ip"` // 客户端 IP | ||||
| 	Username  string `json:"username"`  // 当前登录的 username | ||||
| 	UserId    uint   `json:"user_id"`   // 当前登录的 user ID | ||||
| 	ChatId    string `json:"chat_id"`   // 客户端聊天会话 ID, 多会话模式专用字段 | ||||
| 	Model     string `json:"model"`     // GPT 模型 | ||||
| } | ||||
|  | ||||
| type ApiError struct { | ||||
| 	Error struct { | ||||
| 		Message string | ||||
| 		Type    string | ||||
| 		Param   interface{} | ||||
| 		Code    string | ||||
| 	} | ||||
| } | ||||
|  | ||||
| const PromptMsg = "prompt" // prompt message | ||||
| const ReplyMsg = "reply"   // reply message | ||||
| @@ -1,75 +0,0 @@ | ||||
| package types | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| ) | ||||
|  | ||||
| type AppConfig struct { | ||||
| 	Path      string `toml:"-"` | ||||
| 	Listen    string | ||||
| 	Session   Session | ||||
| 	ProxyURL  string | ||||
| 	MysqlDns  string      // mysql 连接地址 | ||||
| 	Manager   Manager     // 后台管理员账户信息 | ||||
| 	StaticDir string      // 静态资源目录 | ||||
| 	StaticUrl string      // 静态资源 URL | ||||
| 	Redis     RedisConfig // redis 连接信息 | ||||
| } | ||||
|  | ||||
| type RedisConfig struct { | ||||
| 	Host     string | ||||
| 	Port     int | ||||
| 	Password string | ||||
| } | ||||
|  | ||||
| func (c RedisConfig) Url() string { | ||||
| 	return fmt.Sprintf("%s:%d", c.Host, c.Port) | ||||
| } | ||||
|  | ||||
| // Manager 管理员 | ||||
| type Manager struct { | ||||
| 	Username string `json:"username"` | ||||
| 	Password string `json:"password"` | ||||
| } | ||||
|  | ||||
| type SessionDriver string | ||||
|  | ||||
| const ( | ||||
| 	SessionDriverMem    = SessionDriver("mem") | ||||
| 	SessionDriverRedis  = SessionDriver("redis") | ||||
| 	SessionDriverCookie = SessionDriver("cookie") | ||||
| ) | ||||
|  | ||||
| // Session configs struct | ||||
| type Session struct { | ||||
| 	Driver    SessionDriver // session 存储驱动 mem|cookie|redis | ||||
| 	SecretKey string        // session encryption key | ||||
| 	Name      string | ||||
| 	Path      string | ||||
| 	Domain    string | ||||
| 	MaxAge    int | ||||
| 	Secure    bool | ||||
| 	HttpOnly  bool | ||||
| 	SameSite  http.SameSite | ||||
| } | ||||
|  | ||||
| // ChatConfig 系统默认的聊天配置 | ||||
| type ChatConfig struct { | ||||
| 	ApiURL        string  `json:"api_url,omitempty"` | ||||
| 	Model         string  `json:"model"` // 默认模型 | ||||
| 	Temperature   float32 `json:"temperature"` | ||||
| 	MaxTokens     int     `json:"max_tokens"` | ||||
| 	EnableContext bool    `json:"enable_context"` // 是否开启聊天上下文 | ||||
| 	EnableHistory bool    `json:"enable_history"` // 是否允许保存聊天记录 | ||||
| 	ApiKey        string  `json:"api_key"`        // OpenAI  API key | ||||
| } | ||||
|  | ||||
| type SystemConfig struct { | ||||
| 	Title         string   `json:"title"` | ||||
| 	AdminTitle    string   `json:"admin_title"` | ||||
| 	Models        []string `json:"models"` | ||||
| 	UserInitCalls int      `json:"user_init_calls"` // 新用户注册默认总送多少次调用 | ||||
| } | ||||
|  | ||||
| const UserInitCalls = 1000 | ||||
| @@ -1,6 +0,0 @@ | ||||
| package types | ||||
|  | ||||
| const SessionName = "ChatGPT-TOKEN" | ||||
| const SessionUser = "SESSION_USER"        // 存储用户信息的 session key | ||||
| const SessionAdmin = "SESSION_ADMIN"      //存储管理员信息的 session key | ||||
| const LoginUserCache = "LOGIN_USER_CACHE" // 已登录用户缓存 | ||||
| @@ -1,60 +0,0 @@ | ||||
| module chatplus | ||||
|  | ||||
| go 1.19 | ||||
|  | ||||
| require ( | ||||
| 	github.com/BurntSushi/toml v1.1.0 | ||||
| 	github.com/gin-contrib/sessions v0.0.5 | ||||
| 	github.com/gin-gonic/gin v1.9.0 | ||||
| 	github.com/gorilla/websocket v1.5.0 | ||||
| 	github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0 | ||||
| 	github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480 | ||||
| 	github.com/syndtr/goleveldb v1.0.0 | ||||
| 	go.uber.org/zap v1.23.0 | ||||
| 	gorm.io/driver/mysql v1.4.7 | ||||
| ) | ||||
|  | ||||
| require ( | ||||
| 	github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect | ||||
| 	github.com/bytedance/sonic v1.8.0 // indirect | ||||
| 	github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect | ||||
| 	github.com/dlclark/regexp2 v1.8.1 // indirect | ||||
| 	github.com/go-sql-driver/mysql v1.7.0 // indirect | ||||
| 	github.com/goccy/go-json v0.10.0 // indirect | ||||
| 	github.com/gomodule/redigo v2.0.0+incompatible // indirect | ||||
| 	github.com/jinzhu/inflection v1.0.0 // indirect | ||||
| 	github.com/jinzhu/now v1.1.5 // indirect | ||||
| 	github.com/klauspost/cpuid/v2 v2.0.9 // indirect | ||||
| 	github.com/pelletier/go-toml/v2 v2.0.6 // indirect | ||||
| 	github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b // indirect | ||||
| 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect | ||||
| 	go.uber.org/dig v1.16.1 // indirect | ||||
| 	golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect | ||||
| 	golang.org/x/net v0.7.0 // indirect | ||||
| 	golang.org/x/text v0.7.0 // indirect | ||||
| 	google.golang.org/protobuf v1.28.1 // indirect | ||||
| 	gopkg.in/yaml.v3 v3.0.1 // indirect | ||||
| ) | ||||
|  | ||||
| require ( | ||||
| 	github.com/gin-contrib/sse v0.1.0 // indirect | ||||
| 	github.com/go-playground/locales v0.14.1 // indirect | ||||
| 	github.com/go-playground/universal-translator v0.18.1 // indirect | ||||
| 	github.com/go-playground/validator/v10 v10.11.2 // indirect | ||||
| 	github.com/golang/snappy v0.0.1 // indirect | ||||
| 	github.com/gorilla/context v1.1.1 // indirect | ||||
| 	github.com/gorilla/securecookie v1.1.1 // indirect | ||||
| 	github.com/gorilla/sessions v1.2.1 // indirect | ||||
| 	github.com/json-iterator/go v1.1.12 // indirect | ||||
| 	github.com/leodido/go-urn v1.2.1 // indirect | ||||
| 	github.com/mattn/go-isatty v0.0.17 // indirect | ||||
| 	github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect | ||||
| 	github.com/modern-go/reflect2 v1.0.2 // indirect | ||||
| 	github.com/ugorji/go/codec v1.2.9 // indirect | ||||
| 	go.uber.org/atomic v1.7.0 // indirect | ||||
| 	go.uber.org/fx v1.19.3 | ||||
| 	go.uber.org/multierr v1.6.0 // indirect | ||||
| 	golang.org/x/crypto v0.6.0 | ||||
| 	golang.org/x/sys v0.5.0 // indirect | ||||
| 	gorm.io/gorm v1.25.1 | ||||
| ) | ||||
							
								
								
									
										155
									
								
								api/go/go.sum
									
									
									
									
									
								
							
							
						
						
									
										155
									
								
								api/go/go.sum
									
									
									
									
									
								
							| @@ -1,155 +0,0 @@ | ||||
| github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= | ||||
| github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= | ||||
| github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= | ||||
| github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff h1:RmdPFa+slIr4SCBg4st/l/vZWVe9QJKMXGO60Bxbe04= | ||||
| github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw= | ||||
| github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= | ||||
| github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA= | ||||
| github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= | ||||
| github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= | ||||
| github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= | ||||
| github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= | ||||
| github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||||
| github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0= | ||||
| github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= | ||||
| github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= | ||||
| github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE= | ||||
| github.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY= | ||||
| github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= | ||||
| github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= | ||||
| github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8= | ||||
| github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k= | ||||
| github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= | ||||
| github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= | ||||
| github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= | ||||
| github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= | ||||
| github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= | ||||
| github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU= | ||||
| github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s= | ||||
| github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= | ||||
| github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= | ||||
| github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA= | ||||
| github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= | ||||
| github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | ||||
| github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= | ||||
| github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= | ||||
| github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= | ||||
| github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= | ||||
| github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= | ||||
| github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= | ||||
| github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= | ||||
| github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||
| github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | ||||
| github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= | ||||
| github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= | ||||
| github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= | ||||
| github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= | ||||
| github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= | ||||
| github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= | ||||
| github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= | ||||
| github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= | ||||
| github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= | ||||
| github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= | ||||
| github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= | ||||
| github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= | ||||
| github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= | ||||
| github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= | ||||
| github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= | ||||
| github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= | ||||
| github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= | ||||
| github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= | ||||
| github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= | ||||
| github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= | ||||
| github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= | ||||
| github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= | ||||
| github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= | ||||
| github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= | ||||
| github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0 h1:LgmjED/yQILqmUED4GaXjrINWe7YJh4HM6z2EvEINPs= | ||||
| github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0/go.mod h1:C5LA5UO2ZXJrLaPLYtE1wUJMiyd/nwWaCO5cw/2pSHs= | ||||
| github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= | ||||
| github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= | ||||
| github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= | ||||
| github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | ||||
| github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= | ||||
| github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= | ||||
| github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= | ||||
| github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= | ||||
| github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= | ||||
| github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= | ||||
| github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= | ||||
| github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= | ||||
| github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= | ||||
| github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= | ||||
| github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480 h1:IFhPCcB0/HtnEN+ZoUGDT55YgFCymbFJ15kXqs3nv5w= | ||||
| github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480/go.mod h1:BijIqAP84FMYC4XbdJgjyMpiSjusU8x0Y0W9K2t0QtU= | ||||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||
| github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b h1:aUNXCGgukb4gtY99imuIeoh8Vr0GSwAlYxPAhqZrpFc= | ||||
| github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg= | ||||
| github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= | ||||
| github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||
| github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= | ||||
| github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= | ||||
| github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | ||||
| github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||
| github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||
| github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||
| github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= | ||||
| github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | ||||
| github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= | ||||
| github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= | ||||
| github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= | ||||
| github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= | ||||
| github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= | ||||
| github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU= | ||||
| github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= | ||||
| go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= | ||||
| go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= | ||||
| go.uber.org/dig v1.16.1 h1:+alNIBsl0qfY0j6epRubp/9obgtrObRAc5aD+6jbWY8= | ||||
| go.uber.org/dig v1.16.1/go.mod h1:557JTAUZT5bUK0SvCwikmLPPtdQhfvLYtO5tJgQSbnk= | ||||
| go.uber.org/fx v1.19.3 h1:YqMRE4+2IepTYCMOvXqQpRa+QAVdiSTnsHU4XNWBceA= | ||||
| go.uber.org/fx v1.19.3/go.mod h1:w2HrQg26ql9fLK7hlBiZ6JsRUKV+Lj/atT1KCjT8YhM= | ||||
| go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= | ||||
| go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= | ||||
| go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= | ||||
| go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= | ||||
| go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= | ||||
| golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= | ||||
| golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= | ||||
| golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= | ||||
| golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= | ||||
| golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||
| golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= | ||||
| golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= | ||||
| golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= | ||||
| golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||
| golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= | ||||
| golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= | ||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= | ||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= | ||||
| google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= | ||||
| google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= | ||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
| gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= | ||||
| gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= | ||||
| gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= | ||||
| gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= | ||||
| gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= | ||||
| gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
| gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= | ||||
| gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
| gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||||
| gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
| gorm.io/driver/mysql v1.4.7 h1:rY46lkCspzGHn7+IYsNpSfEv9tA+SU4SkkB+GFX125Y= | ||||
| gorm.io/driver/mysql v1.4.7/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8oc= | ||||
| gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= | ||||
| gorm.io/gorm v1.25.1 h1:nsSALe5Pr+cM3V1qwwQ7rOkw+6UeLrX5O4v3llhHa64= | ||||
| gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= | ||||
| rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= | ||||
| @@ -1,466 +0,0 @@ | ||||
| package handler | ||||
|  | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"bytes" | ||||
| 	"chatplus/core" | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/store/vo" | ||||
| 	"chatplus/utils" | ||||
| 	"chatplus/utils/resp" | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 	"unicode/utf8" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/gorilla/websocket" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| const ErrorMsg = "抱歉,AI 助手开小差了,请稍后再试。" | ||||
|  | ||||
| type ChatHandler struct { | ||||
| 	BaseHandler | ||||
| 	db *gorm.DB | ||||
| } | ||||
|  | ||||
| func NewChatHandler(app *core.AppServer, db *gorm.DB) *ChatHandler { | ||||
| 	handler := ChatHandler{db: db} | ||||
| 	handler.App = app | ||||
| 	return &handler | ||||
| } | ||||
|  | ||||
| // ChatHandle 处理聊天 WebSocket 请求 | ||||
| func (h *ChatHandler) ChatHandle(c *gin.Context) { | ||||
| 	ws, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}).Upgrade(c.Writer, c.Request, nil) | ||||
| 	if err != nil { | ||||
| 		logger.Error(err) | ||||
| 		return | ||||
| 	} | ||||
| 	sessionId := c.Query("session_id") | ||||
| 	roleId := h.GetInt(c, "role_id", 0) | ||||
| 	chatId := c.Query("chat_id") | ||||
| 	chatModel := c.Query("model") | ||||
|  | ||||
| 	session := h.App.ChatSession.Get(sessionId) | ||||
| 	if session.SessionId == "" { | ||||
| 		user, err := utils.GetLoginUser(c, h.db) | ||||
| 		if err != nil { | ||||
| 			logger.Info("用户未登录") | ||||
| 			c.Abort() | ||||
| 			return | ||||
| 		} | ||||
| 		session = types.ChatSession{ | ||||
| 			SessionId: sessionId, | ||||
| 			ClientIP:  c.ClientIP(), | ||||
| 			Username:  user.Username, | ||||
| 			UserId:    user.Id, | ||||
| 		} | ||||
| 		h.App.ChatSession.Put(sessionId, session) | ||||
| 	} | ||||
|  | ||||
| 	// use old chat data override the chat model and role ID | ||||
| 	var chat model.ChatItem | ||||
| 	res := h.db.Where("chat_id=?", chatId).First(&chat) | ||||
| 	if res.Error == nil { | ||||
| 		chatModel = chat.Model | ||||
| 		roleId = int(chat.RoleId) | ||||
| 	} | ||||
|  | ||||
| 	session.ChatId = chatId | ||||
| 	session.Model = chatModel | ||||
| 	logger.Infof("New websocket connected, IP: %s, Username: %s", c.Request.RemoteAddr, session.Username) | ||||
| 	client := types.NewWsClient(ws) | ||||
| 	var chatRole model.ChatRole | ||||
| 	res = h.db.First(&chatRole, roleId) | ||||
| 	if res.Error != nil || !chatRole.Enable { | ||||
| 		replyMessage(client, "当前聊天角色不存在或者未启用!!!") | ||||
| 		c.Abort() | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// 保存会话连接 | ||||
| 	h.App.ChatClients.Put(sessionId, client) | ||||
| 	go func() { | ||||
| 		for { | ||||
| 			_, message, err := client.Receive() | ||||
| 			if err != nil { | ||||
| 				logger.Error(err) | ||||
| 				client.Close() | ||||
| 				h.App.ChatClients.Delete(sessionId) | ||||
| 				h.App.ReqCancelFunc.Delete(sessionId) | ||||
| 				return | ||||
| 			} | ||||
| 			logger.Info("Receive a message: ", string(message)) | ||||
| 			//replyMessage(client, "这是一条测试消息!") | ||||
| 			ctx, cancel := context.WithCancel(context.Background()) | ||||
| 			h.App.ReqCancelFunc.Put(sessionId, cancel) | ||||
| 			// 回复消息 | ||||
| 			err = h.sendMessage(ctx, session, chatRole, string(message), client) | ||||
| 			if err != nil { | ||||
| 				logger.Error(err) | ||||
| 			} else { | ||||
| 				replyChunkMessage(client, types.WsMessage{Type: types.WsEnd}) | ||||
| 				logger.Info("回答完毕: " + string(message)) | ||||
| 			} | ||||
|  | ||||
| 		} | ||||
| 	}() | ||||
| } | ||||
|  | ||||
| // 将消息发送给 ChatGPT 并获取结果,通过 WebSocket 推送到客户端 | ||||
| func (h *ChatHandler) sendMessage(ctx context.Context, session types.ChatSession, role model.ChatRole, prompt string, ws types.Client) error { | ||||
| 	promptCreatedAt := time.Now() // 记录提问时间 | ||||
|  | ||||
| 	var user model.User | ||||
| 	res := h.db.Model(&model.User{}).First(&user, session.UserId) | ||||
| 	if res.Error != nil { | ||||
| 		replyMessage(ws, "非法用户,请联系管理员!") | ||||
| 		return res.Error | ||||
| 	} | ||||
| 	var userVo vo.User | ||||
| 	err := utils.CopyObject(user, &userVo) | ||||
| 	userVo.Id = user.Id | ||||
| 	if err != nil { | ||||
| 		return errors.New("User 对象转换失败," + err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	if userVo.Status == false { | ||||
| 		replyMessage(ws, "您的账号已经被禁用,如果疑问,请联系管理员!") | ||||
| 		replyMessage(ws, "") | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	if userVo.Calls <= 0 { | ||||
| 		replyMessage(ws, "您的对话次数已经用尽,请联系管理员充值!") | ||||
| 		replyMessage(ws, "") | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	if userVo.ExpiredTime > 0 && userVo.ExpiredTime <= time.Now().Unix() { | ||||
| 		replyMessage(ws, "您的账号已经过期,请联系管理员!") | ||||
| 		replyMessage(ws, "") | ||||
| 		return nil | ||||
| 	} | ||||
| 	var req = types.ApiRequest{ | ||||
| 		Model:       session.Model, | ||||
| 		Temperature: userVo.ChatConfig.Temperature, | ||||
| 		MaxTokens:   userVo.ChatConfig.MaxTokens, | ||||
| 		Stream:      true, | ||||
| 	} | ||||
|  | ||||
| 	// 加载聊天上下文 | ||||
| 	var chatCtx []types.Message | ||||
| 	if userVo.ChatConfig.EnableContext { | ||||
| 		if h.App.ChatContexts.Has(session.ChatId) { | ||||
| 			chatCtx = h.App.ChatContexts.Get(session.ChatId) | ||||
| 		} else { | ||||
| 			// 加载角色信息 | ||||
| 			var messages []types.Message | ||||
| 			err := utils.JsonDecode(role.Context, &messages) | ||||
| 			if err == nil { | ||||
| 				chatCtx = messages | ||||
| 			} | ||||
| 			// TODO: 这里默认加载最近 4 条聊天记录作为上下文,后期应该做成可配置的 | ||||
| 			var historyMessages []model.HistoryMessage | ||||
| 			res := h.db.Where("chat_id = ?", session.ChatId).Limit(4).Order("created_at desc").Find(&historyMessages) | ||||
| 			if res.Error == nil { | ||||
| 				for _, msg := range historyMessages { | ||||
| 					ms := types.Message{Role: "user", Content: msg.Content} | ||||
| 					if msg.Type == types.ReplyMsg { | ||||
| 						ms.Role = "assistant" | ||||
| 					} | ||||
| 					chatCtx = append(chatCtx, ms) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if h.App.Debug { // 调试打印聊天上下文 | ||||
| 			logger.Info("聊天上下文:", chatCtx) | ||||
| 		} | ||||
| 	} | ||||
| 	req.Messages = append(chatCtx, types.Message{ | ||||
| 		Role:    "user", | ||||
| 		Content: prompt, | ||||
| 	}) | ||||
| 	var apiKey string | ||||
| 	response, err := h.doRequest(ctx, userVo, &apiKey, req) | ||||
| 	if err != nil { | ||||
| 		if strings.Contains(err.Error(), "context canceled") { | ||||
| 			logger.Info("用户取消了请求:", prompt) | ||||
| 			return nil | ||||
| 		} else { | ||||
| 			logger.Error(err) | ||||
| 		} | ||||
|  | ||||
| 		replyMessage(ws, ErrorMsg) | ||||
| 		replyMessage(ws, "") | ||||
| 		return err | ||||
| 	} else { | ||||
| 		defer response.Body.Close() | ||||
| 	} | ||||
|  | ||||
| 	contentType := response.Header.Get("Content-Type") | ||||
| 	if strings.Contains(contentType, "text/event-stream") { | ||||
| 		replyCreatedAt := time.Now() | ||||
| 		// 循环读取 Chunk 消息 | ||||
| 		var message = types.Message{} | ||||
| 		var contents = make([]string, 0) | ||||
| 		var responseBody = types.ApiResponse{} | ||||
| 		reader := bufio.NewReader(response.Body) | ||||
| 		for { | ||||
| 			line, err := reader.ReadString('\n') | ||||
| 			if err != nil { | ||||
| 				if strings.Contains(err.Error(), "context canceled") { | ||||
| 					logger.Info("用户取消了请求:", prompt) | ||||
| 				} else { | ||||
| 					logger.Error(err) | ||||
| 				} | ||||
| 				break | ||||
| 			} | ||||
| 			if !strings.Contains(line, "data:") { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			err = json.Unmarshal([]byte(line[6:]), &responseBody) | ||||
| 			if err != nil || len(responseBody.Choices) == 0 { // 数据解析出错 | ||||
| 				logger.Error(err, line) | ||||
| 				replyMessage(ws, ErrorMsg) | ||||
| 				replyMessage(ws, "") | ||||
| 				break | ||||
| 			} | ||||
|  | ||||
| 			// 初始化 role | ||||
| 			if responseBody.Choices[0].Delta.Role != "" && message.Role == "" { | ||||
| 				message.Role = responseBody.Choices[0].Delta.Role | ||||
| 				replyChunkMessage(ws, types.WsMessage{Type: types.WsStart}) | ||||
| 				continue | ||||
| 			} else if responseBody.Choices[0].FinishReason != "" { | ||||
| 				break // 输出完成或者输出中断了 | ||||
| 			} else { | ||||
| 				content := responseBody.Choices[0].Delta.Content | ||||
| 				contents = append(contents, content) | ||||
| 				replyChunkMessage(ws, types.WsMessage{ | ||||
| 					Type:    types.WsMiddle, | ||||
| 					Content: responseBody.Choices[0].Delta.Content, | ||||
| 				}) | ||||
| 			} | ||||
| 		} // end for | ||||
|  | ||||
| 		// 消息发送成功 | ||||
| 		if len(contents) > 0 { | ||||
| 			// 更新用户的对话次数 | ||||
| 			res := h.db.Model(&user).UpdateColumn("calls", gorm.Expr("calls - ?", 1)) | ||||
| 			if res.Error != nil { | ||||
| 				return res.Error | ||||
| 			} | ||||
|  | ||||
| 			if message.Role == "" { | ||||
| 				message.Role = "assistant" | ||||
| 			} | ||||
| 			message.Content = strings.Join(contents, "") | ||||
| 			useMsg := types.Message{Role: "user", Content: prompt} | ||||
|  | ||||
| 			// 更新上下文消息 | ||||
| 			if userVo.ChatConfig.EnableContext { | ||||
| 				chatCtx = append(chatCtx, useMsg)  // 提问消息 | ||||
| 				chatCtx = append(chatCtx, message) // 回复消息 | ||||
| 				h.App.ChatContexts.Put(session.ChatId, chatCtx) | ||||
| 			} | ||||
|  | ||||
| 			// 追加聊天记录 | ||||
| 			if userVo.ChatConfig.EnableHistory { | ||||
| 				// for prompt | ||||
| 				token, err := utils.CalcTokens(prompt, req.Model) | ||||
| 				if err != nil { | ||||
| 					logger.Error(err) | ||||
| 				} | ||||
| 				historyUserMsg := model.HistoryMessage{ | ||||
| 					UserId:  userVo.Id, | ||||
| 					ChatId:  session.ChatId, | ||||
| 					RoleId:  role.Id, | ||||
| 					Type:    types.PromptMsg, | ||||
| 					Icon:    user.Avatar, | ||||
| 					Content: prompt, | ||||
| 					Tokens:  token, | ||||
| 				} | ||||
| 				historyUserMsg.CreatedAt = promptCreatedAt | ||||
| 				historyUserMsg.UpdatedAt = promptCreatedAt | ||||
| 				res := h.db.Save(&historyUserMsg) | ||||
| 				if res.Error != nil { | ||||
| 					logger.Error("failed to save prompt history message: ", res.Error) | ||||
| 				} | ||||
|  | ||||
| 				// for reply | ||||
| 				token, err = utils.CalcTokens(message.Content, req.Model) | ||||
| 				if err != nil { | ||||
| 					logger.Error(err) | ||||
| 				} | ||||
| 				historyReplyMsg := model.HistoryMessage{ | ||||
| 					UserId:  userVo.Id, | ||||
| 					ChatId:  session.ChatId, | ||||
| 					RoleId:  role.Id, | ||||
| 					Type:    types.ReplyMsg, | ||||
| 					Icon:    role.Icon, | ||||
| 					Content: message.Content, | ||||
| 					Tokens:  token, | ||||
| 				} | ||||
| 				historyReplyMsg.CreatedAt = replyCreatedAt | ||||
| 				historyReplyMsg.UpdatedAt = replyCreatedAt | ||||
| 				res = h.db.Create(&historyReplyMsg) | ||||
| 				if res.Error != nil { | ||||
| 					logger.Error("failed to save reply history message: ", res.Error) | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			// 保存当前会话 | ||||
| 			var chatItem model.ChatItem | ||||
| 			res = h.db.Where("chat_id = ?", session.ChatId).First(&chatItem) | ||||
| 			if res.Error != nil { | ||||
| 				chatItem.ChatId = session.ChatId | ||||
| 				chatItem.UserId = session.UserId | ||||
| 				chatItem.RoleId = role.Id | ||||
| 				chatItem.Model = session.Model | ||||
| 				if utf8.RuneCountInString(prompt) > 30 { | ||||
| 					chatItem.Title = string([]rune(prompt)[:30]) + "..." | ||||
| 				} else { | ||||
| 					chatItem.Title = prompt | ||||
| 				} | ||||
| 				h.db.Create(&chatItem) | ||||
| 			} | ||||
| 		} | ||||
| 	} else { | ||||
| 		body, err := io.ReadAll(response.Body) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("error with reading response: %v", err) | ||||
| 		} | ||||
| 		var res types.ApiError | ||||
| 		err = json.Unmarshal(body, &res) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("error with decode response: %v", err) | ||||
| 		} | ||||
|  | ||||
| 		// OpenAI API 调用异常处理 | ||||
| 		// TODO: 是否考虑重发消息? | ||||
| 		if strings.Contains(res.Error.Message, "This key is associated with a deactivated account") { | ||||
| 			replyMessage(ws, "请求 OpenAI API 失败:API KEY 所关联的账户被禁用。") | ||||
| 			// 移除当前 API key | ||||
| 			h.db.Where("value = ?", apiKey).Delete(&model.ApiKey{}) | ||||
| 		} else if strings.Contains(res.Error.Message, "You exceeded your current quota") { | ||||
| 			replyMessage(ws, "请求 OpenAI API 失败:API KEY 触发并发限制,请稍后再试。") | ||||
| 		} else if strings.Contains(res.Error.Message, "This model's maximum context length") { | ||||
| 			replyMessage(ws, "当前会话上下文长度超出限制,已为您删减会话上下文!") | ||||
| 			// 只保留最近的三条记录 | ||||
| 			chatContext := h.App.ChatContexts.Get(session.ChatId) | ||||
| 			if len(chatContext) > 3 { | ||||
| 				chatContext = chatContext[len(chatContext)-3:] | ||||
| 			} | ||||
| 			h.App.ChatContexts.Put(session.ChatId, chatContext) | ||||
| 			return h.sendMessage(ctx, session, role, prompt, ws) | ||||
| 		} else { | ||||
| 			replyMessage(ws, "请求 OpenAI API 失败:"+res.Error.Message) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // 发送请求到 OpenAI 服务器 | ||||
| // useOwnApiKey: 是否使用了用户自己的 API KEY | ||||
| func (h *ChatHandler) doRequest(ctx context.Context, user vo.User, apiKey *string, req types.ApiRequest) (*http.Response, error) { | ||||
| 	var client *http.Client | ||||
| 	requestBody, err := json.Marshal(req) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	// 创建 HttpClient 请求对象 | ||||
| 	request, err := http.NewRequest(http.MethodPost, h.App.ChatConfig.ApiURL, bytes.NewBuffer(requestBody)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	request = request.WithContext(ctx) | ||||
| 	request.Header.Add("Content-Type", "application/json") | ||||
|  | ||||
| 	proxyURL := h.App.AppConfig.ProxyURL | ||||
| 	if proxyURL == "" { | ||||
| 		client = &http.Client{} | ||||
| 	} else { // 使用代理 | ||||
| 		uri := url.URL{} | ||||
| 		proxy, _ := uri.Parse(proxyURL) | ||||
| 		client = &http.Client{ | ||||
| 			Transport: &http.Transport{ | ||||
| 				Proxy: http.ProxyURL(proxy), | ||||
| 			}, | ||||
| 		} | ||||
| 	} | ||||
| 	// 查询当前用户是否导入了自己的 API KEY | ||||
| 	if user.ChatConfig.ApiKey != "" { | ||||
| 		logger.Info("使用用户自己的 API KEY: ", user.ChatConfig.ApiKey) | ||||
| 		*apiKey = user.ChatConfig.ApiKey | ||||
| 	} else { // 获取系统的 API KEY | ||||
| 		var key model.ApiKey | ||||
| 		res := h.db.Where("user_id = ?", 0).Order("last_used_at ASC").First(&key) | ||||
| 		if res.Error != nil { | ||||
| 			return nil, errors.New("no available key, please import key") | ||||
| 		} | ||||
| 		*apiKey = key.Value | ||||
| 		// 更新 API KEY 的最后使用时间 | ||||
| 		h.db.Model(&key).UpdateColumn("last_used_at", time.Now().Unix()) | ||||
| 	} | ||||
|  | ||||
| 	logger.Infof("Sending OpenAI request, KEY: %s, PROXY: %s, Model: %s", *apiKey, proxyURL, req.Model) | ||||
| 	request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiKey)) | ||||
| 	return client.Do(request) | ||||
| } | ||||
|  | ||||
| // 回复客户片段端消息 | ||||
| func replyChunkMessage(client types.Client, message types.WsMessage) { | ||||
| 	msg, err := json.Marshal(message) | ||||
| 	if err != nil { | ||||
| 		logger.Errorf("Error for decoding json data: %v", err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	err = client.(*types.WsClient).Send(msg) | ||||
| 	if err != nil { | ||||
| 		logger.Errorf("Error for reply message: %v", err.Error()) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // 回复客户端一条完整的消息 | ||||
| func replyMessage(ws types.Client, message string) { | ||||
| 	replyChunkMessage(ws, types.WsMessage{Type: types.WsStart}) | ||||
| 	replyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: message}) | ||||
| 	replyChunkMessage(ws, types.WsMessage{Type: types.WsEnd}) | ||||
| } | ||||
|  | ||||
| // Tokens 统计 token 数量 | ||||
| func (h *ChatHandler) Tokens(c *gin.Context) { | ||||
| 	text := c.Query("text") | ||||
| 	md := c.Query("model") | ||||
| 	tokens, err := utils.CalcTokens(text, md) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, tokens) | ||||
| } | ||||
|  | ||||
| // StopGenerate 停止生成 | ||||
| func (h *ChatHandler) StopGenerate(c *gin.Context) { | ||||
| 	sessionId := c.Query("session_id") | ||||
| 	if h.App.ReqCancelFunc.Has(sessionId) { | ||||
| 		h.App.ReqCancelFunc.Get(sessionId)() | ||||
| 		h.App.ReqCancelFunc.Delete(sessionId) | ||||
| 	} | ||||
| 	resp.SUCCESS(c, types.OkMsg) | ||||
| } | ||||
| @@ -1,157 +0,0 @@ | ||||
| package handler | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/store/vo" | ||||
| 	"chatplus/utils" | ||||
| 	"chatplus/utils/resp" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| ) | ||||
|  | ||||
| // List 获取会话列表 | ||||
| func (h *ChatHandler) List(c *gin.Context) { | ||||
| 	userId := h.GetInt(c, "user_id", 0) | ||||
| 	if userId == 0 { | ||||
| 		resp.ERROR(c, "The parameter 'user_id' is needed.") | ||||
| 		return | ||||
| 	} | ||||
| 	var items = make([]vo.ChatItem, 0) | ||||
| 	var chats []model.ChatItem | ||||
| 	res := h.db.Where("user_id = ?", userId).Order("id DESC").Find(&chats) | ||||
| 	if res.Error == nil { | ||||
| 		var roleIds = make([]uint, 0) | ||||
| 		for _, chat := range chats { | ||||
| 			roleIds = append(roleIds, chat.RoleId) | ||||
| 		} | ||||
| 		var roles []model.ChatRole | ||||
| 		res = h.db.Find(&roles, roleIds) | ||||
| 		if res.Error == nil { | ||||
| 			roleMap := make(map[uint]model.ChatRole) | ||||
| 			for _, role := range roles { | ||||
| 				roleMap[role.Id] = role | ||||
| 			} | ||||
|  | ||||
| 			for _, chat := range chats { | ||||
| 				var item vo.ChatItem | ||||
| 				err := utils.CopyObject(chat, &item) | ||||
| 				if err == nil { | ||||
| 					item.Id = chat.Id | ||||
| 					item.Icon = roleMap[chat.RoleId].Icon | ||||
| 					items = append(items, item) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 	} | ||||
| 	resp.SUCCESS(c, items) | ||||
| } | ||||
|  | ||||
| // Update 更新会话标题 | ||||
| func (h *ChatHandler) Update(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Id    uint   `json:"id"` | ||||
| 		Title string `json:"title"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 	var m = model.ChatItem{} | ||||
| 	m.Id = data.Id | ||||
| 	res := h.db.Model(&m).UpdateColumn("title", data.Title) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "Failed to update database") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, types.OkMsg) | ||||
| } | ||||
|  | ||||
| // Remove 删除会话 | ||||
| func (h *ChatHandler) Remove(c *gin.Context) { | ||||
| 	chatId := h.GetTrim(c, "chat_id") | ||||
| 	if chatId == "" { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 	user, err := utils.GetLoginUser(c, h.db) | ||||
| 	if err != nil { | ||||
| 		resp.NotAuth(c) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	res := h.db.Where("user_id = ? AND chat_id = ?", user.Id, chatId).Delete(&model.ChatItem{}) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "Failed to update database") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// 清空会话上下文 | ||||
| 	h.App.ChatContexts.Delete(chatId) | ||||
| 	resp.SUCCESS(c, types.OkMsg) | ||||
| } | ||||
|  | ||||
| // History 获取聊天历史记录 | ||||
| func (h *ChatHandler) History(c *gin.Context) { | ||||
| 	chatId := c.Query("chat_id") // 会话 ID | ||||
| 	user, err := utils.GetLoginUser(c, h.db) | ||||
| 	if err != nil { | ||||
| 		resp.NotAuth(c) | ||||
| 		return | ||||
| 	} | ||||
| 	var items []model.HistoryMessage | ||||
| 	var messages = make([]vo.HistoryMessage, 0) | ||||
| 	res := h.db.Where("chat_id = ? AND user_id = ?", chatId, user.Id).Find(&items) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "No history message") | ||||
| 		return | ||||
| 	} else { | ||||
| 		for _, item := range items { | ||||
| 			var v vo.HistoryMessage | ||||
| 			err := utils.CopyObject(item, &v) | ||||
| 			v.CreatedAt = item.CreatedAt.Unix() | ||||
| 			v.UpdatedAt = item.UpdatedAt.Unix() | ||||
| 			if err == nil { | ||||
| 				messages = append(messages, v) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, messages) | ||||
| } | ||||
|  | ||||
| // Clear 清空所有聊天记录 | ||||
| func (h *ChatHandler) Clear(c *gin.Context) { | ||||
| 	// 获取当前登录用户所有的聊天会话 | ||||
| 	user, err := utils.GetLoginUser(c, h.db) | ||||
| 	if err != nil { | ||||
| 		resp.NotAuth(c) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var chats []model.ChatItem | ||||
| 	res := h.db.Where("user_id = ?", user.Id).Find(&chats) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "No chats found") | ||||
| 		return | ||||
| 	} | ||||
| 	// 清空聊天记录 | ||||
| 	for _, chat := range chats { | ||||
| 		err := h.db.Where("chat_id = ? AND user_id = ?", chat.ChatId, user.Id).Delete(&model.HistoryMessage{}) | ||||
| 		if err != nil { | ||||
| 			logger.Warnf("Failed to delele chat history for ChatID: %s", chat.ChatId) | ||||
| 		} | ||||
| 		// 清空会话上下文 | ||||
| 		h.App.ChatContexts.Delete(chat.ChatId) | ||||
| 	} | ||||
| 	// 删除所有的会话记录 | ||||
| 	res = h.db.Where("user_id = ?", user.Id).Delete(&model.ChatItem{}) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "Failed to remove chat from database.") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, types.OkMsg) | ||||
| } | ||||
| @@ -1,67 +0,0 @@ | ||||
| package handler | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core" | ||||
| 	"chatplus/utils/resp" | ||||
| 	"fmt" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| type UploadHandler struct { | ||||
| 	BaseHandler | ||||
| 	db *gorm.DB | ||||
| } | ||||
|  | ||||
| func NewUploadHandler(app *core.AppServer, db *gorm.DB) *UploadHandler { | ||||
| 	handler := &UploadHandler{db: db} | ||||
| 	handler.App = app | ||||
| 	return handler | ||||
| } | ||||
|  | ||||
| func (h *UploadHandler) Upload(c *gin.Context) { | ||||
| 	file, err := c.FormFile("file") | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, fmt.Sprintf("文件上传失败: %s", err.Error())) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	filePath, err := h.genFilePath(file.Filename) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, fmt.Sprintf("文件上传失败: %s", err.Error())) | ||||
| 		return | ||||
| 	} | ||||
| 	// 将文件保存到指定路径 | ||||
| 	err = c.SaveUploadedFile(file, filePath) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, fmt.Sprintf("文件保存失败: %s", err.Error())) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, h.genFileUrl(filePath)) | ||||
| } | ||||
|  | ||||
| // 生成上传文件路径 | ||||
| func (h *UploadHandler) genFilePath(filename string) (string, error) { | ||||
| 	now := time.Now() | ||||
| 	dir := fmt.Sprintf("%s/upload/%d/%d", h.App.AppConfig.StaticDir, now.Year(), now.Month()) | ||||
| 	_, err := os.Stat(dir) | ||||
| 	if err != nil { | ||||
| 		err = os.MkdirAll(dir, 0755) | ||||
| 		if err != nil { | ||||
| 			return "", fmt.Errorf("创建上传目录失败:%s", err) | ||||
| 		} | ||||
| 	} | ||||
| 	fileExt := filepath.Ext(filename) | ||||
| 	return fmt.Sprintf("%s/%d%s", dir, now.UnixMilli(), fileExt), nil | ||||
| } | ||||
|  | ||||
| // 生成上传文件 URL | ||||
| func (h *UploadHandler) genFileUrl(filePath string) string { | ||||
| 	now := time.Now() | ||||
| 	filename := filepath.Base(filePath) | ||||
| 	return fmt.Sprintf("%s/upload/%d/%d/%s", h.App.AppConfig.StaticUrl, now.Year(), now.Month(), filename) | ||||
| } | ||||
| @@ -1,26 +0,0 @@ | ||||
| package logger | ||||
|  | ||||
| import ( | ||||
| 	"go.uber.org/zap" | ||||
| ) | ||||
|  | ||||
| var logger *zap.SugaredLogger | ||||
|  | ||||
| func GetLogger() *zap.SugaredLogger { | ||||
| 	if logger != nil { | ||||
| 		return logger | ||||
| 	} | ||||
|  | ||||
| 	logLevel := zap.NewAtomicLevel() | ||||
| 	logLevel.SetLevel(zap.InfoLevel) | ||||
| 	log, _ := zap.Config{ | ||||
| 		Level:            logLevel, | ||||
| 		Development:      false, | ||||
| 		Encoding:         "console", | ||||
| 		EncoderConfig:    zap.NewDevelopmentEncoderConfig(), | ||||
| 		OutputPaths:      []string{"stderr"}, | ||||
| 		ErrorOutputPaths: []string{"stderr"}, | ||||
| 	}.Build() | ||||
| 	logger = log.Sugar() | ||||
| 	return logger | ||||
| } | ||||
| @@ -1,16 +0,0 @@ | ||||
| package model | ||||
|  | ||||
| type HistoryMessage struct { | ||||
| 	BaseModel | ||||
| 	ChatId  string // 会话 ID | ||||
| 	UserId  uint   // 用户 ID | ||||
| 	RoleId  uint   // 角色 ID | ||||
| 	Type    string | ||||
| 	Icon    string | ||||
| 	Tokens  int | ||||
| 	Content string | ||||
| } | ||||
|  | ||||
| func (HistoryMessage) TableName() string { | ||||
| 	return "chatgpt_chat_history" | ||||
| } | ||||
| @@ -1,10 +0,0 @@ | ||||
| package model | ||||
|  | ||||
| type ChatItem struct { | ||||
| 	BaseModel | ||||
| 	ChatId string `gorm:"column:chat_id;unique"` // 会话 ID | ||||
| 	UserId uint   // 用户 ID | ||||
| 	RoleId uint   // 角色 ID | ||||
| 	Model  string // 会话模型 | ||||
| 	Title  string // 会话标题 | ||||
| } | ||||
| @@ -1,16 +0,0 @@ | ||||
| package vo | ||||
|  | ||||
| type HistoryMessage struct { | ||||
| 	BaseVo | ||||
| 	ChatId  string `json:"chat_id"` | ||||
| 	UserId  uint   `json:"user_id"` | ||||
| 	RoleId  uint   `json:"role_id"` | ||||
| 	Type    string `json:"type"` | ||||
| 	Icon    string `json:"icon"` | ||||
| 	Tokens  int    `json:"tokens"` | ||||
| 	Content string `json:"content"` | ||||
| } | ||||
|  | ||||
| func (HistoryMessage) TableName() string { | ||||
| 	return "chatgpt_chat_history" | ||||
| } | ||||
| @@ -1,11 +0,0 @@ | ||||
| package vo | ||||
|  | ||||
| type ChatItem struct { | ||||
| 	BaseVo | ||||
| 	UserId uint   `json:"user_id"` | ||||
| 	Icon   string `json:"icon"` | ||||
| 	RoleId uint   `json:"role_id"` | ||||
| 	ChatId string `json:"chat_id"` | ||||
| 	Model  string `json:"model"` | ||||
| 	Title  string `json:"title"` | ||||
| } | ||||
| @@ -1,19 +0,0 @@ | ||||
| package vo | ||||
|  | ||||
| import "chatplus/core/types" | ||||
|  | ||||
| type User struct { | ||||
| 	BaseVo | ||||
| 	Username    string           `json:"username"` | ||||
| 	Nickname    string           `json:"nickname"` | ||||
| 	Avatar      string           `json:"avatar"` | ||||
| 	Salt        string           `json:"salt"`          // 密码盐 | ||||
| 	Tokens      int64            `json:"tokens"`        // 剩余tokens | ||||
| 	Calls       int              `json:"calls"`         // 剩余对话次数 | ||||
| 	ChatConfig  types.ChatConfig `json:"chat_config"`   // 聊天配置 | ||||
| 	ChatRoles   []string         `json:"chat_roles"`    // 聊天角色集合 | ||||
| 	ExpiredTime int64            `json:"expired_time"`  // 账户到期时间 | ||||
| 	Status      bool             `json:"status"`        // 当前状态 | ||||
| 	LastLoginAt int64            `json:"last_login_at"` // 最后登录时间 | ||||
| 	LastLoginIp string           `json:"last_login_ip"` // 最后登录 IP | ||||
| } | ||||
| @@ -1,145 +0,0 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/store/vo" | ||||
| 	"chatplus/utils" | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"github.com/lionsoul2014/ip2region/binding/golang/xdb" | ||||
| 	"github.com/pkoukk/tiktoken-go" | ||||
| 	"io" | ||||
| 	"log" | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| func main() { | ||||
| 	lMap := types.NewLMap[string, types.ChatSession]() | ||||
| 	lMap.Put("name", types.ChatSession{SessionId: utils.RandString(32)}) | ||||
|  | ||||
| 	item := lMap.Get("abc") | ||||
| 	fmt.Println(item) | ||||
| } | ||||
|  | ||||
| // Http client 取消操作 | ||||
| func testHttpClient(ctx context.Context) { | ||||
|  | ||||
| 	req, err := http.NewRequest("GET", "http://localhost:2345", nil) | ||||
| 	if err != nil { | ||||
| 		fmt.Println(err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	req = req.WithContext(ctx) | ||||
|  | ||||
| 	client := &http.Client{} | ||||
|  | ||||
| 	resp, err := client.Do(req) | ||||
| 	if err != nil { | ||||
| 		fmt.Println(err) | ||||
| 		return | ||||
| 	} | ||||
| 	defer func(Body io.ReadCloser) { | ||||
| 		err := Body.Close() | ||||
| 		if err != nil { | ||||
|  | ||||
| 		} | ||||
| 	}(resp.Body) | ||||
| 	_, err = io.ReadAll(resp.Body) | ||||
| 	for { | ||||
| 		time.Sleep(time.Second) | ||||
| 		fmt.Println(time.Now()) | ||||
| 		select { | ||||
| 		case <-ctx.Done(): | ||||
| 			fmt.Println("取消退出") | ||||
| 			return | ||||
| 		default: | ||||
| 			continue | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| } | ||||
|  | ||||
| func testDate() { | ||||
| 	fmt.Println(time.Unix(1683336167, 0).Format("2006-01-02 15:04:05")) | ||||
| } | ||||
|  | ||||
| func testIp2Region() { | ||||
| 	dbPath := "res/ip2region.xdb" | ||||
| 	// 1、从 dbPath 加载整个 xdb 到内存 | ||||
| 	cBuff, err := xdb.LoadContentFromFile(dbPath) | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("failed to load content from `%s`: %s\n", dbPath, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// 2、用全局的 cBuff 创建完全基于内存的查询对象。 | ||||
| 	searcher, err := xdb.NewWithBuffer(cBuff) | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("failed to create searcher with content: %s\n", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	str, err := searcher.SearchByStr("103.88.46.85") | ||||
| 	fmt.Println(str) | ||||
| 	if err != nil { | ||||
| 		log.Fatal(err) | ||||
| 	} | ||||
| 	arr := strings.Split(str, "|") | ||||
| 	fmt.Println(arr[2], arr[3], arr[4]) | ||||
|  | ||||
| } | ||||
|  | ||||
| func testJson() { | ||||
|  | ||||
| 	var role = model.ChatRole{ | ||||
| 		Key:      "programmer", | ||||
| 		Name:     "程序员", | ||||
| 		Context:  "[{\"role\":\"user\",\"content\":\"现在开始你扮演一位程序员,你是一名优秀的程序员,具有很强的逻辑思维能力,总能高效的解决问题。你热爱编程,熟悉多种编程语言,尤其精通 Go 语言,注重代码质量,有创新意识,持续学习,良好的沟通协作。\"},{\"role\"\n:\"assistant\",\"content\":\"好的,现在我将扮演一位程序员,非常感谢您对我的评价。作为一名优秀的程序员,我非常热爱编程,并且注重代码质量。我熟悉多种编程语言,尤其是 Go 语言,可以使用它来高效地解决各种问题。\"}]", | ||||
| 		HelloMsg: "Talk is cheap, i will show code!", | ||||
| 		Icon:     "images/avatar/programmer.jpg", | ||||
| 		Enable:   true, | ||||
| 		Sort:     1, | ||||
| 	} | ||||
| 	role.Id = 1 | ||||
| 	var v vo.ChatRole | ||||
|  | ||||
| 	err := utils.CopyObject(role, &v) | ||||
| 	if err != nil { | ||||
| 		log.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	fmt.Printf("%+v\n", v.Id) | ||||
|  | ||||
| 	//var v2 = model.ChatRoles{} | ||||
| 	//err = utils.CopyObject(v, &v2) | ||||
| 	//if err != nil { | ||||
| 	//	log.Fatal(err) | ||||
| 	//} | ||||
| 	// | ||||
| 	//fmt.Printf("%+v\n", v2.Id) | ||||
|  | ||||
| } | ||||
|  | ||||
| func calTokens() { | ||||
| 	text := "须知少年凌云志,曾许人间第一流" | ||||
| 	encoding := "cl100k_base" | ||||
|  | ||||
| 	tke, err := tiktoken.GetEncoding(encoding) | ||||
| 	if err != nil { | ||||
| 		err = fmt.Errorf("getEncoding: %v", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// encode | ||||
| 	token := tke.Encode(text, nil, nil) | ||||
|  | ||||
| 	//tokens | ||||
| 	fmt.Println(token) | ||||
| 	// num_tokens | ||||
| 	fmt.Println(len(token)) | ||||
|  | ||||
| } | ||||
| @@ -1,45 +0,0 @@ | ||||
| package utils | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/store/model" | ||||
| 	"errors" | ||||
|  | ||||
| 	"github.com/gin-contrib/sessions" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| func SetLoginUser(c *gin.Context, user model.User) error { | ||||
| 	session := sessions.Default(c) | ||||
| 	session.Set(types.SessionUser, user.Id) | ||||
| 	// TODO: 后期用户数量增加,考虑将用户数据存储到 leveldb,避免每次查询数据库 | ||||
| 	return session.Save() | ||||
| } | ||||
|  | ||||
| func SetLoginAdmin(c *gin.Context, admin types.Manager) error { | ||||
| 	session := sessions.Default(c) | ||||
| 	session.Set(types.SessionAdmin, admin.Username) | ||||
| 	return session.Save() | ||||
| } | ||||
|  | ||||
| func GetLoginUser(c *gin.Context, db *gorm.DB) (model.User, error) { | ||||
| 	value, exists := c.Get(types.LoginUserCache) | ||||
| 	if exists { | ||||
| 		return value.(model.User), nil | ||||
| 	} | ||||
|  | ||||
| 	session := sessions.Default(c) | ||||
| 	userId := session.Get(types.SessionUser) | ||||
| 	if userId == nil { | ||||
| 		return model.User{}, errors.New("user not login") | ||||
| 	} | ||||
|  | ||||
| 	var user model.User | ||||
| 	res := db.First(&user, userId) | ||||
| 	// 更新缓存 | ||||
| 	if res.Error == nil { | ||||
| 		c.Set(types.LoginUserCache, user) | ||||
| 	} | ||||
| 	return user, res.Error | ||||
| } | ||||
| @@ -8,9 +8,11 @@ import ( | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/utils" | ||||
| 	"chatplus/utils/resp" | ||||
| 	"context" | ||||
| 	"github.com/go-redis/redis/v8" | ||||
| 	"github.com/golang-jwt/jwt/v5" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/gin-contrib/sessions" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| @@ -20,11 +22,12 @@ var logger = logger2.GetLogger() | ||||
| 
 | ||||
| type ManagerHandler struct { | ||||
| 	handler.BaseHandler | ||||
| 	db *gorm.DB | ||||
| 	db    *gorm.DB | ||||
| 	redis *redis.Client | ||||
| } | ||||
| 
 | ||||
| func NewAdminHandler(app *core.AppServer, db *gorm.DB) *ManagerHandler { | ||||
| 	h := ManagerHandler{db: db} | ||||
| func NewAdminHandler(app *core.AppServer, db *gorm.DB, client *redis.Client) *ManagerHandler { | ||||
| 	h := ManagerHandler{db: db, redis: client} | ||||
| 	h.App = app | ||||
| 	return &h | ||||
| } | ||||
| @@ -36,15 +39,25 @@ func (h *ManagerHandler) Login(c *gin.Context) { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 	manager := h.App.AppConfig.Manager | ||||
| 	manager := h.App.Config.Manager | ||||
| 	if data.Username == manager.Username && data.Password == manager.Password { | ||||
| 		err := utils.SetLoginAdmin(c, manager) | ||||
| 		// 创建 token | ||||
| 		token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ | ||||
| 			"user_id": manager.Username, | ||||
| 			"expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)).Unix(), | ||||
| 		}) | ||||
| 		tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey)) | ||||
| 		if err != nil { | ||||
| 			resp.ERROR(c, "Save session failed") | ||||
| 			resp.ERROR(c, "Failed to generate token, "+err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 		manager.Password = "" // 清空密码] | ||||
| 		resp.SUCCESS(c, manager) | ||||
| 		// 保存到 redis | ||||
| 		key := "users/" + manager.Username | ||||
| 		if _, err := h.redis.Set(context.Background(), key, tokenString, 0).Result(); err != nil { | ||||
| 			resp.ERROR(c, "error with save token: "+err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 		resp.SUCCESS(c, tokenString) | ||||
| 	} else { | ||||
| 		resp.ERROR(c, "用户名或者密码错误") | ||||
| 	} | ||||
| @@ -52,11 +65,9 @@ func (h *ManagerHandler) Login(c *gin.Context) { | ||||
| 
 | ||||
| // Logout 注销 | ||||
| func (h *ManagerHandler) Logout(c *gin.Context) { | ||||
| 	session := sessions.Default(c) | ||||
| 	session.Delete(types.SessionAdmin) | ||||
| 	err := session.Save() | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, "Save session failed") | ||||
| 	key := h.GetUserKey(c) | ||||
| 	if _, err := h.redis.Del(c, key).Result(); err != nil { | ||||
| 		logger.Error("error with delete session: ", err) | ||||
| 	} else { | ||||
| 		resp.SUCCESS(c) | ||||
| 	} | ||||
| @@ -64,9 +75,8 @@ func (h *ManagerHandler) Logout(c *gin.Context) { | ||||
| 
 | ||||
| // Session 会话检测 | ||||
| func (h *ManagerHandler) Session(c *gin.Context) { | ||||
| 	session := sessions.Default(c) | ||||
| 	admin := session.Get(types.SessionAdmin) | ||||
| 	if admin == nil { | ||||
| 	token := c.GetHeader(types.AdminAuthHeader) | ||||
| 	if token == "" { | ||||
| 		resp.NotAuth(c) | ||||
| 	} else { | ||||
| 		resp.SUCCESS(c) | ||||
| @@ -113,8 +123,11 @@ func (h *ManagerHandler) Migrate(c *gin.Context) { | ||||
| 		var message []model.HistoryMessage | ||||
| 		h.db.Find(&message) | ||||
| 		for _, r := range message { | ||||
| 			r.Icon = "/" + r.Icon | ||||
| 			h.db.Updates(&r) | ||||
| 			if !strings.HasPrefix(r.Icon, "/") { | ||||
| 				r.Icon = "/" + r.Icon | ||||
| 				h.db.Updates(&r) | ||||
| 			} | ||||
| 
 | ||||
| 		} | ||||
| 		break | ||||
| 
 | ||||
| @@ -8,8 +8,6 @@ import ( | ||||
| 	"chatplus/store/vo" | ||||
| 	"chatplus/utils" | ||||
| 	"chatplus/utils/resp" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
| @@ -27,22 +25,21 @@ func NewApiKeyHandler(app *core.AppServer, db *gorm.DB) *ApiKeyHandler { | ||||
| 
 | ||||
| func (h *ApiKeyHandler) Save(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Id         uint   `json:"id"` | ||||
| 		UserId     uint   `json:"user_id"` | ||||
| 		Value      string `json:"value"` | ||||
| 		LastUsedAt string `json:"last_used_at"` | ||||
| 		CreatedAt  int64  `json:"created_at"` | ||||
| 		Id       uint   `json:"id"` | ||||
| 		Platform string `json:"platform"` | ||||
| 		Value    string `json:"value"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	apiKey := model.ApiKey{Value: data.Value, UserId: data.UserId, LastUsedAt: utils.Str2stamp(data.LastUsedAt)} | ||||
| 	apiKey.Id = data.Id | ||||
| 	if apiKey.Id > 0 { | ||||
| 		apiKey.CreatedAt = time.Unix(data.CreatedAt, 0) | ||||
| 	apiKey := model.ApiKey{} | ||||
| 	if data.Id > 0 { | ||||
| 		h.db.Find(&apiKey) | ||||
| 	} | ||||
| 	apiKey.Platform = data.Platform | ||||
| 	apiKey.Value = data.Value | ||||
| 	res := h.db.Save(&apiKey) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "更新数据库失败!") | ||||
| @@ -61,14 +58,9 @@ func (h *ApiKeyHandler) Save(c *gin.Context) { | ||||
| } | ||||
| 
 | ||||
| func (h *ApiKeyHandler) List(c *gin.Context) { | ||||
| 	userId := h.GetInt(c, "user_id", -1) | ||||
| 	query := h.db.Session(&gorm.Session{}) | ||||
| 	if userId >= 0 { | ||||
| 		query = query.Where("user_id", userId) | ||||
| 	} | ||||
| 	var items []model.ApiKey | ||||
| 	var keys = make([]vo.ApiKey, 0) | ||||
| 	res := query.Find(&items) | ||||
| 	res := h.db.Find(&items) | ||||
| 	if res.Error == nil { | ||||
| 		for _, item := range items { | ||||
| 			var key vo.ApiKey | ||||
							
								
								
									
										143
									
								
								api/handler/admin/chat_model_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								api/handler/admin/chat_model_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,143 @@ | ||||
| package admin | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core" | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/handler" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/store/vo" | ||||
| 	"chatplus/utils" | ||||
| 	"chatplus/utils/resp" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| type ChatModelHandler struct { | ||||
| 	handler.BaseHandler | ||||
| 	db *gorm.DB | ||||
| } | ||||
|  | ||||
| func NewChatModelHandler(app *core.AppServer, db *gorm.DB) *ChatModelHandler { | ||||
| 	h := ChatModelHandler{db: db} | ||||
| 	h.App = app | ||||
| 	return &h | ||||
| } | ||||
|  | ||||
| func (h *ChatModelHandler) Save(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Id        uint   `json:"id"` | ||||
| 		Name      string `json:"name"` | ||||
| 		Value     string `json:"value"` | ||||
| 		Enabled   bool   `json:"enabled"` | ||||
| 		SortNum   int    `json:"sort_num"` | ||||
| 		Platform  string `json:"platform"` | ||||
| 		CreatedAt int64  `json:"created_at"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	item := model.ChatModel{Platform: data.Platform, Name: data.Name, Value: data.Value, Enabled: data.Enabled} | ||||
| 	item.Id = data.Id | ||||
| 	if item.Id > 0 { | ||||
| 		item.CreatedAt = time.Unix(data.CreatedAt, 0) | ||||
| 	} | ||||
| 	res := h.db.Save(&item) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "更新数据库失败!") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var itemVo vo.ChatModel | ||||
| 	err := utils.CopyObject(item, &itemVo) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, "数据拷贝失败!") | ||||
| 		return | ||||
| 	} | ||||
| 	itemVo.Id = item.Id | ||||
| 	itemVo.CreatedAt = item.CreatedAt.Unix() | ||||
| 	resp.SUCCESS(c, itemVo) | ||||
| } | ||||
|  | ||||
| // List 模型列表 | ||||
| func (h *ChatModelHandler) List(c *gin.Context) { | ||||
| 	session := h.db.Session(&gorm.Session{}) | ||||
| 	enable := h.GetBool(c, "enable") | ||||
| 	if enable { | ||||
| 		session = session.Where("enabled", enable) | ||||
| 	} | ||||
| 	var items []model.ChatModel | ||||
| 	var cms = make([]vo.ChatModel, 0) | ||||
| 	res := session.Order("sort_num ASC").Find(&items) | ||||
| 	if res.Error == nil { | ||||
| 		for _, item := range items { | ||||
| 			var cm vo.ChatModel | ||||
| 			err := utils.CopyObject(item, &cm) | ||||
| 			if err == nil { | ||||
| 				cm.Id = item.Id | ||||
| 				cm.CreatedAt = item.CreatedAt.Unix() | ||||
| 				cm.UpdatedAt = item.UpdatedAt.Unix() | ||||
| 				cms = append(cms, cm) | ||||
| 			} else { | ||||
| 				logger.Error(err) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	resp.SUCCESS(c, cms) | ||||
| } | ||||
|  | ||||
| func (h *ChatModelHandler) Enable(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Id      uint `json:"id"` | ||||
| 		Enabled bool `json:"enabled"` | ||||
| 	} | ||||
|  | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	res := h.db.Model(&model.ChatModel{}).Where("id = ?", data.Id).Update("enabled", data.Enabled) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "更新数据库失败!") | ||||
| 		return | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| func (h *ChatModelHandler) Sort(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Ids   []uint `json:"ids"` | ||||
| 		Sorts []int  `json:"sorts"` | ||||
| 	} | ||||
|  | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	for index, id := range data.Ids { | ||||
| 		res := h.db.Model(&model.ChatModel{}).Where("id = ?", id).Update("sort_num", data.Sorts[index]) | ||||
| 		if res.Error != nil { | ||||
| 			resp.ERROR(c, "更新数据库失败!") | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| func (h *ChatModelHandler) Remove(c *gin.Context) { | ||||
| 	id := h.GetInt(c, "id", 0) | ||||
|  | ||||
| 	if id > 0 { | ||||
| 		res := h.db.Where("id = ?", id).Delete(&model.ChatModel{}) | ||||
| 		if res.Error != nil { | ||||
| 			resp.ERROR(c, "更新数据库失败!") | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
| @@ -55,7 +55,7 @@ func (h *ChatRoleHandler) Save(c *gin.Context) { | ||||
| func (h *ChatRoleHandler) List(c *gin.Context) { | ||||
| 	var items []model.ChatRole | ||||
| 	var roles = make([]vo.ChatRole, 0) | ||||
| 	res := h.db.Order("sort ASC").Find(&items) | ||||
| 	res := h.db.Order("sort_num ASC").Find(&items) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "No data found") | ||||
| 		return | ||||
| @@ -75,24 +75,24 @@ func (h *ChatRoleHandler) List(c *gin.Context) { | ||||
| 	resp.SUCCESS(c, roles) | ||||
| } | ||||
| 
 | ||||
| // SetSort 更新角色排序 | ||||
| func (h *ChatRoleHandler) SetSort(c *gin.Context) { | ||||
| // Sort 更新角色排序 | ||||
| func (h *ChatRoleHandler) Sort(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Id   uint `json:"id"` | ||||
| 		Sort int  `json:"sort"` | ||||
| 		Ids   []uint `json:"ids"` | ||||
| 		Sorts []int  `json:"sorts"` | ||||
| 	} | ||||
| 
 | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 	if data.Id <= 0 { | ||||
| 		resp.HACKER(c) | ||||
| 		return | ||||
| 	} | ||||
| 	res := h.db.Model(&model.ChatRole{}).Where("id = ?", data.Id).Update("sort", data.Sort) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "更新数据库失败!") | ||||
| 		return | ||||
| 
 | ||||
| 	for index, id := range data.Ids { | ||||
| 		res := h.db.Model(&model.ChatRole{}).Where("id = ?", id).Update("sort_num", data.Sorts[index]) | ||||
| 		if res.Error != nil { | ||||
| 			resp.ERROR(c, "更新数据库失败!") | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	resp.SUCCESS(c) | ||||
| @@ -48,6 +48,21 @@ func (h *ConfigHandler) Update(c *gin.Context) { | ||||
| 			resp.ERROR(c, res.Error.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		// update config cache for AppServer | ||||
| 		var cfg model.Config | ||||
| 		h.db.Where("marker", data.Key).First(&cfg) | ||||
| 		var err error | ||||
| 		if data.Key == "system" { | ||||
| 			err = utils.JsonDecode(cfg.Config, &h.App.SysConfig) | ||||
| 		} else if data.Key == "chat" { | ||||
| 			err = utils.JsonDecode(cfg.Config, &h.App.ChatConfig) | ||||
| 		} | ||||
| 		if err != nil { | ||||
| 			resp.ERROR(c, "Failed to update config cache: "+err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 		logger.Infof("Update AppServer's config successfully: %v", config.Config) | ||||
| 	} | ||||
| 
 | ||||
| 	resp.SUCCESS(c, config) | ||||
							
								
								
									
										63
									
								
								api/handler/admin/dashboard_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								api/handler/admin/dashboard_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | ||||
| package admin | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core" | ||||
| 	"chatplus/handler" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/utils/resp" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| type DashboardHandler struct { | ||||
| 	handler.BaseHandler | ||||
| 	db *gorm.DB | ||||
| } | ||||
|  | ||||
| func NewDashboardHandler(app *core.AppServer, db *gorm.DB) *DashboardHandler { | ||||
| 	h := DashboardHandler{db: db} | ||||
| 	h.App = app | ||||
| 	return &h | ||||
| } | ||||
|  | ||||
| type statsVo struct { | ||||
| 	Users   int64   `json:"users"` | ||||
| 	Chats   int64   `json:"chats"` | ||||
| 	Tokens  int64   `json:"tokens"` | ||||
| 	Rewards float64 `json:"rewards"` | ||||
| } | ||||
|  | ||||
| func (h *DashboardHandler) Stats(c *gin.Context) { | ||||
| 	stats := statsVo{} | ||||
| 	// new users statistic | ||||
| 	var userCount int64 | ||||
| 	now := time.Now() | ||||
| 	zeroTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) | ||||
| 	res := h.db.Model(&model.User{}).Where("created_at > ?", zeroTime).Count(&userCount) | ||||
| 	if res.Error == nil { | ||||
| 		stats.Users = userCount | ||||
| 	} | ||||
|  | ||||
| 	// new chats statistic | ||||
| 	var chatCount int64 | ||||
| 	res = h.db.Model(&model.ChatItem{}).Where("created_at > ?", zeroTime).Count(&chatCount) | ||||
| 	if res.Error == nil { | ||||
| 		stats.Chats = chatCount | ||||
| 	} | ||||
|  | ||||
| 	// tokens took stats | ||||
| 	var tokenCount int64 | ||||
| 	res = h.db.Model(&model.HistoryMessage{}).Select("sum(tokens) as total").Where("created_at > ?", zeroTime).Scan(&tokenCount) | ||||
| 	if res.Error == nil { | ||||
| 		stats.Tokens = tokenCount | ||||
| 	} | ||||
|  | ||||
| 	// reward revenue | ||||
| 	var amount float64 | ||||
| 	res = h.db.Model(&model.Reward{}).Select("sum(amount) as total").Where("created_at > ?", zeroTime).Scan(&amount) | ||||
| 	if res.Error == nil { | ||||
| 		stats.Rewards = amount | ||||
| 	} | ||||
| 	resp.SUCCESS(c, stats) | ||||
| } | ||||
							
								
								
									
										57
									
								
								api/handler/admin/reward_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								api/handler/admin/reward_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| package admin | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core" | ||||
| 	"chatplus/handler" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/store/vo" | ||||
| 	"chatplus/utils" | ||||
| 	"chatplus/utils/resp" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| type RewardHandler struct { | ||||
| 	handler.BaseHandler | ||||
| 	db *gorm.DB | ||||
| } | ||||
|  | ||||
| func NewRewardHandler(app *core.AppServer, db *gorm.DB) *RewardHandler { | ||||
| 	h := RewardHandler{db: db} | ||||
| 	h.App = app | ||||
| 	return &h | ||||
| } | ||||
|  | ||||
| func (h *RewardHandler) List(c *gin.Context) { | ||||
| 	var items []model.Reward | ||||
| 	res := h.db.Order("id DESC").Find(&items) | ||||
| 	var rewards = make([]vo.Reward, 0) | ||||
| 	if res.Error == nil { | ||||
| 		userIds := make([]uint, 0) | ||||
| 		for _, v := range items { | ||||
| 			userIds = append(userIds, v.UserId) | ||||
| 		} | ||||
| 		var users []model.User | ||||
| 		h.db.Where("id IN ?", userIds).Find(&users) | ||||
| 		var userMap = make(map[uint]model.User) | ||||
| 		for _, u := range users { | ||||
| 			userMap[u.Id] = u | ||||
| 		} | ||||
|  | ||||
| 		for _, v := range items { | ||||
| 			var r vo.Reward | ||||
| 			err := utils.CopyObject(v, &r) | ||||
| 			if err != nil { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			r.Id = v.Id | ||||
| 			r.Username = userMap[v.UserId].Mobile | ||||
| 			r.CreatedAt = v.CreatedAt.Unix() | ||||
| 			r.UpdatedAt = v.UpdatedAt.Unix() | ||||
| 			rewards = append(rewards, r) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, rewards) | ||||
| } | ||||
| @@ -8,7 +8,6 @@ import ( | ||||
| 	"chatplus/store/vo" | ||||
| 	"chatplus/utils" | ||||
| 	"chatplus/utils/resp" | ||||
| 
 | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
| @@ -28,12 +27,20 @@ func NewUserHandler(app *core.AppServer, db *gorm.DB) *UserHandler { | ||||
| func (h *UserHandler) List(c *gin.Context) { | ||||
| 	page := h.GetInt(c, "page", 1) | ||||
| 	pageSize := h.GetInt(c, "page_size", 20) | ||||
| 	mobile := h.GetTrim(c, "mobile") | ||||
| 
 | ||||
| 	offset := (page - 1) * pageSize | ||||
| 	var items []model.User | ||||
| 	var users = make([]vo.User, 0) | ||||
| 	var total int64 | ||||
| 	h.db.Model(&model.User{}).Count(&total) | ||||
| 	res := h.db.Offset(offset).Limit(pageSize).Find(&items) | ||||
| 
 | ||||
| 	session := h.db.Session(&gorm.Session{}) | ||||
| 	if mobile != "" { | ||||
| 		session = session.Where("mobile LIKE ?", "%"+mobile+"%") | ||||
| 	} | ||||
| 
 | ||||
| 	session.Model(&model.User{}).Count(&total) | ||||
| 	res := session.Offset(offset).Limit(pageSize).Find(&items) | ||||
| 	if res.Error == nil { | ||||
| 		for _, item := range items { | ||||
| 			var user vo.User | ||||
| @@ -52,11 +59,13 @@ func (h *UserHandler) List(c *gin.Context) { | ||||
| 	resp.SUCCESS(c, pageVo) | ||||
| } | ||||
| 
 | ||||
| func (h *UserHandler) Update(c *gin.Context) { | ||||
| func (h *UserHandler) Save(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Id          uint     `json:"id"` | ||||
| 		Nickname    string   `json:"nickname"` | ||||
| 		Password    string   `json:"password"` | ||||
| 		Mobile      string   `json:"mobile"` | ||||
| 		Calls       int      `json:"calls"` | ||||
| 		ImgCalls    int      `json:"img_calls"` | ||||
| 		ChatRoles   []string `json:"chat_roles"` | ||||
| 		ExpiredTime string   `json:"expired_time"` | ||||
| 		Status      bool     `json:"status"` | ||||
| @@ -66,21 +75,79 @@ func (h *UserHandler) Update(c *gin.Context) { | ||||
| 		return | ||||
| 	} | ||||
| 	var user = model.User{} | ||||
| 	user.Id = data.Id | ||||
| 	// 此处需要用 map 更新,用结构体无法更新 0 值 | ||||
| 	res := h.db.Model(&user).Updates(map[string]interface{}{ | ||||
| 		"nickname":        data.Nickname, | ||||
| 		"calls":           data.Calls, | ||||
| 		"status":          data.Status, | ||||
| 		"chat_roles_json": utils.JsonEncode(data.ChatRoles), | ||||
| 		"expired_time":    utils.Str2stamp(data.ExpiredTime), | ||||
| 	}) | ||||
| 	var res *gorm.DB | ||||
| 	var userVo vo.User | ||||
| 	if data.Id > 0 { // 更新 | ||||
| 		user.Id = data.Id | ||||
| 		// 此处需要用 map 更新,用结构体无法更新 0 值 | ||||
| 		res = h.db.Model(&user).Updates(map[string]interface{}{ | ||||
| 			"mobile":          data.Mobile, | ||||
| 			"calls":           data.Calls, | ||||
| 			"img_calls":       data.ImgCalls, | ||||
| 			"status":          data.Status, | ||||
| 			"chat_roles_json": utils.JsonEncode(data.ChatRoles), | ||||
| 			"expired_time":    utils.Str2stamp(data.ExpiredTime), | ||||
| 		}) | ||||
| 	} else { | ||||
| 		salt := utils.RandString(8) | ||||
| 		u := model.User{ | ||||
| 			Mobile:      data.Mobile, | ||||
| 			Password:    utils.GenPassword(data.Password, salt), | ||||
| 			Avatar:      "/images/avatar/user.png", | ||||
| 			Salt:        salt, | ||||
| 			Status:      true, | ||||
| 			ChatRoles:   utils.JsonEncode(data.ChatRoles), | ||||
| 			ExpiredTime: utils.Str2stamp(data.ExpiredTime), | ||||
| 			ChatConfig: utils.JsonEncode(types.UserChatConfig{ | ||||
| 				ApiKeys: map[types.Platform]string{ | ||||
| 					types.OpenAI:  "", | ||||
| 					types.Azure:   "", | ||||
| 					types.ChatGLM: "", | ||||
| 				}, | ||||
| 			}), | ||||
| 			Calls: h.App.SysConfig.UserInitCalls, | ||||
| 		} | ||||
| 		res = h.db.Create(&u) | ||||
| 		_ = utils.CopyObject(u, &userVo) | ||||
| 		userVo.Id = u.Id | ||||
| 		userVo.CreatedAt = u.CreatedAt.Unix() | ||||
| 		userVo.UpdatedAt = u.UpdatedAt.Unix() | ||||
| 	} | ||||
| 
 | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "更新数据库失败") | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	resp.SUCCESS(c) | ||||
| 	resp.SUCCESS(c, userVo) | ||||
| } | ||||
| 
 | ||||
| // ResetPass 重置密码 | ||||
| func (h *UserHandler) ResetPass(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Id       uint | ||||
| 		Password string | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	var user model.User | ||||
| 	res := h.db.First(&user, data.Id) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "No user found") | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	password := utils.GenPassword(data.Password, user.Salt) | ||||
| 	user.Password = password | ||||
| 	res = h.db.Updates(&user) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c) | ||||
| 	} else { | ||||
| 		resp.SUCCESS(c) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (h *UserHandler) Remove(c *gin.Context) { | ||||
| @@ -125,7 +192,7 @@ func (h *UserHandler) LoginLog(c *gin.Context) { | ||||
| 	h.db.Model(&model.UserLoginLog{}).Count(&total) | ||||
| 	offset := (page - 1) * pageSize | ||||
| 	var items []model.UserLoginLog | ||||
| 	res := h.db.Offset(offset).Limit(pageSize).Find(&items) | ||||
| 	res := h.db.Offset(offset).Limit(pageSize).Order("id DESC").Find(&items) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "获取数据失败") | ||||
| 		return | ||||
							
								
								
									
										288
									
								
								api/handler/azure_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										288
									
								
								api/handler/azure_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,288 @@ | ||||
| package handler | ||||
|  | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/store/vo" | ||||
| 	"chatplus/utils" | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"gorm.io/gorm" | ||||
| 	"io" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 	"unicode/utf8" | ||||
| ) | ||||
|  | ||||
| // 将消息发送给 Azure API 并获取结果,通过 WebSocket 推送到客户端 | ||||
| func (h *ChatHandler) sendAzureMessage( | ||||
| 	chatCtx []interface{}, | ||||
| 	req types.ApiRequest, | ||||
| 	userVo vo.User, | ||||
| 	ctx context.Context, | ||||
| 	session *types.ChatSession, | ||||
| 	role model.ChatRole, | ||||
| 	prompt string, | ||||
| 	ws *types.WsClient) error { | ||||
| 	promptCreatedAt := time.Now() // 记录提问时间 | ||||
| 	start := time.Now() | ||||
| 	var apiKey = userVo.ChatConfig.ApiKeys[session.Model.Platform] | ||||
| 	response, err := h.doRequest(ctx, req, session.Model.Platform, &apiKey) | ||||
| 	logger.Info("HTTP请求完成,耗时:", time.Now().Sub(start)) | ||||
| 	if err != nil { | ||||
| 		if strings.Contains(err.Error(), "context canceled") { | ||||
| 			logger.Info("用户取消了请求:", prompt) | ||||
| 			return nil | ||||
| 		} else if strings.Contains(err.Error(), "no available key") { | ||||
| 			utils.ReplyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!") | ||||
| 			return nil | ||||
| 		} else { | ||||
| 			logger.Error(err) | ||||
| 		} | ||||
|  | ||||
| 		utils.ReplyMessage(ws, ErrorMsg) | ||||
| 		utils.ReplyMessage(ws, "") | ||||
| 		return err | ||||
| 	} else { | ||||
| 		defer response.Body.Close() | ||||
| 	} | ||||
|  | ||||
| 	contentType := response.Header.Get("Content-Type") | ||||
| 	if strings.Contains(contentType, "text/event-stream") { | ||||
| 		replyCreatedAt := time.Now() // 记录回复时间 | ||||
| 		// 循环读取 Chunk 消息 | ||||
| 		var message = types.Message{} | ||||
| 		var contents = make([]string, 0) | ||||
| 		var functionCall = false | ||||
| 		var functionName string | ||||
| 		var arguments = make([]string, 0) | ||||
| 		scanner := bufio.NewScanner(response.Body) | ||||
| 		for scanner.Scan() { | ||||
| 			line := scanner.Text() | ||||
| 			if !strings.Contains(line, "data:") || len(line) < 30 { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			var responseBody = types.ApiResponse{} | ||||
| 			err = json.Unmarshal([]byte(line[6:]), &responseBody) | ||||
| 			if err != nil || len(responseBody.Choices) == 0 { // 数据解析出错 | ||||
| 				logger.Error(err, line) | ||||
| 				utils.ReplyMessage(ws, ErrorMsg) | ||||
| 				utils.ReplyMessage(ws, "") | ||||
| 				break | ||||
| 			} | ||||
|  | ||||
| 			fun := responseBody.Choices[0].Delta.FunctionCall | ||||
| 			if functionCall && fun.Name == "" { | ||||
| 				arguments = append(arguments, fun.Arguments) | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			if !utils.IsEmptyValue(fun) { | ||||
| 				functionName = fun.Name | ||||
| 				f := h.App.Functions[functionName] | ||||
| 				if f != nil { | ||||
| 					functionCall = true | ||||
| 					utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart}) | ||||
| 					utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: fmt.Sprintf("正在调用函数 `%s` 作答 ...\n\n", f.Name())}) | ||||
| 					continue | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if responseBody.Choices[0].FinishReason == "function_call" { // 函数调用完毕 | ||||
| 				break | ||||
| 			} | ||||
|  | ||||
| 			// 初始化 role | ||||
| 			if responseBody.Choices[0].Delta.Role != "" && message.Role == "" { | ||||
| 				message.Role = responseBody.Choices[0].Delta.Role | ||||
| 				utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart}) | ||||
| 				continue | ||||
| 			} else if responseBody.Choices[0].FinishReason != "" { | ||||
| 				break // 输出完成或者输出中断了 | ||||
| 			} else { | ||||
| 				content := responseBody.Choices[0].Delta.Content | ||||
| 				contents = append(contents, utils.InterfaceToString(content)) | ||||
| 				utils.ReplyChunkMessage(ws, types.WsMessage{ | ||||
| 					Type:    types.WsMiddle, | ||||
| 					Content: utils.InterfaceToString(responseBody.Choices[0].Delta.Content), | ||||
| 				}) | ||||
| 			} | ||||
| 		} // end for | ||||
|  | ||||
| 		if err := scanner.Err(); err != nil { | ||||
| 			if strings.Contains(err.Error(), "context canceled") { | ||||
| 				logger.Info("用户取消了请求:", prompt) | ||||
| 			} else { | ||||
| 				logger.Error("信息读取出错:", err) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if functionCall { // 调用函数完成任务 | ||||
| 			var params map[string]interface{} | ||||
| 			_ = utils.JsonDecode(strings.Join(arguments, ""), ¶ms) | ||||
| 			logger.Debugf("函数名称: %s, 函数参数:%s", functionName, params) | ||||
|  | ||||
| 			// for creating image, check if the user's img_calls > 0 | ||||
| 			if functionName == types.FuncMidJourney && userVo.ImgCalls <= 0 { | ||||
| 				utils.ReplyMessage(ws, "**当前用户剩余绘图次数已用尽,请扫描下面二维码联系管理员!**") | ||||
| 				utils.ReplyMessage(ws, "") | ||||
| 			} else { | ||||
| 				f := h.App.Functions[functionName] | ||||
| 				if functionName == types.FuncMidJourney { | ||||
| 					params["user_id"] = userVo.Id | ||||
| 					params["role_id"] = role.Id | ||||
| 					params["chat_id"] = session.ChatId | ||||
| 					params["icon"] = "/images/avatar/mid_journey.png" | ||||
| 					params["session_id"] = session.SessionId | ||||
| 				} | ||||
| 				data, err := f.Invoke(params) | ||||
| 				if err != nil { | ||||
| 					msg := "调用函数出错:" + err.Error() | ||||
| 					utils.ReplyChunkMessage(ws, types.WsMessage{ | ||||
| 						Type:    types.WsMiddle, | ||||
| 						Content: msg, | ||||
| 					}) | ||||
| 					contents = append(contents, msg) | ||||
| 				} else { | ||||
| 					content := data | ||||
| 					if functionName == types.FuncMidJourney { | ||||
| 						content = fmt.Sprintf("绘画提示词:%s 已推送任务到 MidJourney 机器人,请耐心等待任务执行...", data) | ||||
| 						h.App.MjTaskClients.Put(session.SessionId, ws) | ||||
| 						// update user's img_calls | ||||
| 						h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1)) | ||||
| 					} | ||||
|  | ||||
| 					utils.ReplyChunkMessage(ws, types.WsMessage{ | ||||
| 						Type:    types.WsMiddle, | ||||
| 						Content: content, | ||||
| 					}) | ||||
| 					contents = append(contents, content) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// 消息发送成功 | ||||
| 		if len(contents) > 0 { | ||||
| 			// 更新用户的对话次数 | ||||
| 			if userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" { | ||||
| 				h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("calls", gorm.Expr("calls - ?", 1)) | ||||
| 			} | ||||
|  | ||||
| 			if message.Role == "" { | ||||
| 				message.Role = "assistant" | ||||
| 			} | ||||
| 			message.Content = strings.Join(contents, "") | ||||
| 			useMsg := types.Message{Role: "user", Content: prompt} | ||||
|  | ||||
| 			// 更新上下文消息,如果是调用函数则不需要更新上下文 | ||||
| 			if h.App.ChatConfig.EnableContext && functionCall == false { | ||||
| 				chatCtx = append(chatCtx, useMsg)  // 提问消息 | ||||
| 				chatCtx = append(chatCtx, message) // 回复消息 | ||||
| 				h.App.ChatContexts.Put(session.ChatId, chatCtx) | ||||
| 			} | ||||
|  | ||||
| 			// 追加聊天记录 | ||||
| 			if h.App.ChatConfig.EnableHistory { | ||||
| 				useContext := true | ||||
| 				if functionCall { | ||||
| 					useContext = false | ||||
| 				} | ||||
|  | ||||
| 				// for prompt | ||||
| 				promptToken, err := utils.CalcTokens(prompt, req.Model) | ||||
| 				if err != nil { | ||||
| 					logger.Error(err) | ||||
| 				} | ||||
| 				historyUserMsg := model.HistoryMessage{ | ||||
| 					UserId:     userVo.Id, | ||||
| 					ChatId:     session.ChatId, | ||||
| 					RoleId:     role.Id, | ||||
| 					Type:       types.PromptMsg, | ||||
| 					Icon:       userVo.Avatar, | ||||
| 					Content:    prompt, | ||||
| 					Tokens:     promptToken, | ||||
| 					UseContext: useContext, | ||||
| 				} | ||||
| 				historyUserMsg.CreatedAt = promptCreatedAt | ||||
| 				historyUserMsg.UpdatedAt = promptCreatedAt | ||||
| 				res := h.db.Save(&historyUserMsg) | ||||
| 				if res.Error != nil { | ||||
| 					logger.Error("failed to save prompt history message: ", res.Error) | ||||
| 				} | ||||
|  | ||||
| 				// 计算本次对话消耗的总 token 数量 | ||||
| 				var totalTokens = 0 | ||||
| 				if functionCall { // prompt + 函数名 + 参数 token | ||||
| 					tokens, _ := utils.CalcTokens(functionName, req.Model) | ||||
| 					totalTokens += tokens | ||||
| 					tokens, _ = utils.CalcTokens(utils.InterfaceToString(arguments), req.Model) | ||||
| 					totalTokens += tokens | ||||
| 				} else { | ||||
| 					totalTokens, _ = utils.CalcTokens(message.Content, req.Model) | ||||
| 				} | ||||
| 				totalTokens += getTotalTokens(req) | ||||
|  | ||||
| 				historyReplyMsg := model.HistoryMessage{ | ||||
| 					UserId:     userVo.Id, | ||||
| 					ChatId:     session.ChatId, | ||||
| 					RoleId:     role.Id, | ||||
| 					Type:       types.ReplyMsg, | ||||
| 					Icon:       role.Icon, | ||||
| 					Content:    message.Content, | ||||
| 					Tokens:     totalTokens, | ||||
| 					UseContext: useContext, | ||||
| 				} | ||||
| 				historyReplyMsg.CreatedAt = replyCreatedAt | ||||
| 				historyReplyMsg.UpdatedAt = replyCreatedAt | ||||
| 				res = h.db.Create(&historyReplyMsg) | ||||
| 				if res.Error != nil { | ||||
| 					logger.Error("failed to save reply history message: ", res.Error) | ||||
| 				} | ||||
|  | ||||
| 				// 更新用户信息 | ||||
| 				h.db.Model(&model.User{}).Where("id = ?", userVo.Id). | ||||
| 					UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", totalTokens)) | ||||
| 			} | ||||
|  | ||||
| 			// 保存当前会话 | ||||
| 			var chatItem model.ChatItem | ||||
| 			res := h.db.Where("chat_id = ?", session.ChatId).First(&chatItem) | ||||
| 			if res.Error != nil { | ||||
| 				chatItem.ChatId = session.ChatId | ||||
| 				chatItem.UserId = session.UserId | ||||
| 				chatItem.RoleId = role.Id | ||||
| 				chatItem.ModelId = session.Model.Id | ||||
| 				if utf8.RuneCountInString(prompt) > 30 { | ||||
| 					chatItem.Title = string([]rune(prompt)[:30]) + "..." | ||||
| 				} else { | ||||
| 					chatItem.Title = prompt | ||||
| 				} | ||||
| 				h.db.Create(&chatItem) | ||||
| 			} | ||||
| 		} | ||||
| 	} else { | ||||
| 		body, err := io.ReadAll(response.Body) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("error with reading response: %v", err) | ||||
| 		} | ||||
| 		var res types.ApiError | ||||
| 		err = json.Unmarshal(body, &res) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("error with decode response: %v", err) | ||||
| 		} | ||||
|  | ||||
| 		if strings.Contains(res.Error.Message, "maximum context length") { | ||||
| 			logger.Error(res.Error.Message) | ||||
| 			utils.ReplyMessage(ws, "当前会话上下文长度超出限制,已为您清空会话上下文!") | ||||
| 			h.App.ChatContexts.Delete(session.ChatId) | ||||
| 			return h.sendMessage(ctx, session, role, prompt, ws) | ||||
| 		} else { | ||||
| 			utils.ReplyMessage(ws, "请求 Azure API 失败:"+res.Error.Message) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
| @@ -2,8 +2,10 @@ package handler | ||||
| 
 | ||||
| import ( | ||||
| 	"chatplus/core" | ||||
| 	"chatplus/core/types" | ||||
| 	logger2 "chatplus/logger" | ||||
| 	"strconv" | ||||
| 	"chatplus/utils" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/gin-gonic/gin" | ||||
| @@ -20,47 +22,30 @@ func (h *BaseHandler) GetTrim(c *gin.Context, key string) string { | ||||
| } | ||||
| 
 | ||||
| func (h *BaseHandler) PostInt(c *gin.Context, key string, defaultValue int) int { | ||||
| 	return intValue(c.PostForm(key), defaultValue) | ||||
| 	return utils.IntValue(c.PostForm(key), defaultValue) | ||||
| } | ||||
| 
 | ||||
| func (h *BaseHandler) GetInt(c *gin.Context, key string, defaultValue int) int { | ||||
| 	return intValue(c.Query(key), defaultValue) | ||||
| } | ||||
| 
 | ||||
| func intValue(str string, defaultValue int) int { | ||||
| 	value, err := strconv.Atoi(str) | ||||
| 	if err != nil { | ||||
| 		return defaultValue | ||||
| 	} | ||||
| 	return value | ||||
| 	return utils.IntValue(c.Query(key), defaultValue) | ||||
| } | ||||
| 
 | ||||
| func (h *BaseHandler) GetFloat(c *gin.Context, key string) float64 { | ||||
| 	return floatValue(c.Query(key)) | ||||
| 	return utils.FloatValue(c.Query(key)) | ||||
| } | ||||
| func (h *BaseHandler) PostFloat(c *gin.Context, key string) float64 { | ||||
| 	return floatValue(c.PostForm(key)) | ||||
| } | ||||
| 
 | ||||
| func floatValue(str string) float64 { | ||||
| 	value, err := strconv.ParseFloat(str, 64) | ||||
| 	if err != nil { | ||||
| 		return 0 | ||||
| 	} | ||||
| 	return value | ||||
| 	return utils.FloatValue(c.PostForm(key)) | ||||
| } | ||||
| 
 | ||||
| func (h *BaseHandler) GetBool(c *gin.Context, key string) bool { | ||||
| 	return boolValue(c.Query(key)) | ||||
| 	return utils.BoolValue(c.Query(key)) | ||||
| } | ||||
| func (h *BaseHandler) PostBool(c *gin.Context, key string) bool { | ||||
| 	return boolValue(c.PostForm(key)) | ||||
| 	return utils.BoolValue(c.PostForm(key)) | ||||
| } | ||||
| 
 | ||||
| func boolValue(str string) bool { | ||||
| 	value, err := strconv.ParseBool(str) | ||||
| 	if err != nil { | ||||
| 		return false | ||||
| func (h *BaseHandler) GetUserKey(c *gin.Context) string { | ||||
| 	userId, ok := c.Get(types.LoginUserID) | ||||
| 	if !ok { | ||||
| 		return "" | ||||
| 	} | ||||
| 	return value | ||||
| 	return fmt.Sprintf("users/%v", userId) | ||||
| } | ||||
							
								
								
									
										47
									
								
								api/handler/captcha_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								api/handler/captcha_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| package handler | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/service" | ||||
| 	"chatplus/utils/resp" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| ) | ||||
|  | ||||
| // 今日头条函数实现 | ||||
|  | ||||
| type CaptchaHandler struct { | ||||
| 	service *service.CaptchaService | ||||
| } | ||||
|  | ||||
| func NewCaptchaHandler(s *service.CaptchaService) *CaptchaHandler { | ||||
| 	return &CaptchaHandler{service: s} | ||||
| } | ||||
|  | ||||
| func (h *CaptchaHandler) Get(c *gin.Context) { | ||||
| 	data, err := h.service.Get() | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, data) | ||||
| } | ||||
|  | ||||
| // Check verify the captcha data | ||||
| func (h *CaptchaHandler) Check(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Key  string `json:"key"` | ||||
| 		Dots string `json:"dots"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if h.service.Check(data) { | ||||
| 		resp.SUCCESS(c) | ||||
| 	} else { | ||||
| 		resp.ERROR(c) | ||||
| 	} | ||||
|  | ||||
| } | ||||
							
								
								
									
										416
									
								
								api/handler/chat_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										416
									
								
								api/handler/chat_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,416 @@ | ||||
| package handler | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"chatplus/core" | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/store" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/store/vo" | ||||
| 	"chatplus/utils" | ||||
| 	"chatplus/utils/resp" | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/go-redis/redis/v8" | ||||
| 	"github.com/gorilla/websocket" | ||||
| 	"gorm.io/gorm" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| const ErrorMsg = "抱歉,AI 助手开小差了,请稍后再试。" | ||||
|  | ||||
| type ChatHandler struct { | ||||
| 	BaseHandler | ||||
| 	db      *gorm.DB | ||||
| 	leveldb *store.LevelDB | ||||
| 	redis   *redis.Client | ||||
| } | ||||
|  | ||||
| func NewChatHandler(app *core.AppServer, db *gorm.DB, levelDB *store.LevelDB, redis *redis.Client) *ChatHandler { | ||||
| 	handler := ChatHandler{db: db, leveldb: levelDB, redis: redis} | ||||
| 	handler.App = app | ||||
| 	return &handler | ||||
| } | ||||
|  | ||||
| var chatConfig types.ChatConfig | ||||
|  | ||||
| // ChatHandle 处理聊天 WebSocket 请求 | ||||
| func (h *ChatHandler) ChatHandle(c *gin.Context) { | ||||
| 	ws, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}).Upgrade(c.Writer, c.Request, nil) | ||||
| 	if err != nil { | ||||
| 		logger.Error(err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	sessionId := c.Query("session_id") | ||||
| 	roleId := h.GetInt(c, "role_id", 0) | ||||
| 	chatId := c.Query("chat_id") | ||||
| 	modelId := h.GetInt(c, "model_id", 0) | ||||
|  | ||||
| 	client := types.NewWsClient(ws) | ||||
| 	// get model info | ||||
| 	var chatModel model.ChatModel | ||||
| 	res := h.db.First(&chatModel, modelId) | ||||
| 	if res.Error != nil || chatModel.Enabled == false { | ||||
| 		utils.ReplyMessage(client, "当前AI模型暂未启用,连接已关闭!!!") | ||||
| 		c.Abort() | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	session := h.App.ChatSession.Get(sessionId) | ||||
| 	if session == nil { | ||||
| 		user, err := utils.GetLoginUser(c, h.db) | ||||
| 		if err != nil { | ||||
| 			logger.Info("用户未登录") | ||||
| 			c.Abort() | ||||
| 			return | ||||
| 		} | ||||
| 		session = &types.ChatSession{ | ||||
| 			SessionId: sessionId, | ||||
| 			ClientIP:  c.ClientIP(), | ||||
| 			Username:  user.Mobile, | ||||
| 			UserId:    user.Id, | ||||
| 		} | ||||
| 		h.App.ChatSession.Put(sessionId, session) | ||||
| 	} | ||||
|  | ||||
| 	// use old chat data override the chat model and role ID | ||||
| 	var chat model.ChatItem | ||||
| 	res = h.db.Where("chat_id=?", chatId).First(&chat) | ||||
| 	if res.Error == nil { | ||||
| 		chatModel.Id = chat.ModelId | ||||
| 		roleId = int(chat.RoleId) | ||||
| 	} | ||||
|  | ||||
| 	session.ChatId = chatId | ||||
| 	session.Model = types.ChatModel{ | ||||
| 		Id:       chatModel.Id, | ||||
| 		Value:    chatModel.Value, | ||||
| 		Platform: types.Platform(chatModel.Platform)} | ||||
| 	logger.Infof("New websocket connected, IP: %s, Username: %s", c.ClientIP(), session.Username) | ||||
| 	var chatRole model.ChatRole | ||||
| 	res = h.db.First(&chatRole, roleId) | ||||
| 	if res.Error != nil || !chatRole.Enable { | ||||
| 		utils.ReplyMessage(client, "当前聊天角色不存在或者未启用,连接已关闭!!!") | ||||
| 		c.Abort() | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// 初始化聊天配置 | ||||
| 	var config model.Config | ||||
| 	h.db.Where("marker", "chat").First(&config) | ||||
| 	err = utils.JsonDecode(config.Config, &chatConfig) | ||||
| 	if err != nil { | ||||
| 		utils.ReplyMessage(client, "加载系统配置失败,连接已关闭!!!") | ||||
| 		c.Abort() | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// 保存会话连接 | ||||
| 	h.App.ChatClients.Put(sessionId, client) | ||||
| 	go func() { | ||||
| 		for { | ||||
| 			_, msg, err := client.Receive() | ||||
| 			if err != nil { | ||||
| 				logger.Error(err) | ||||
| 				client.Close() | ||||
| 				h.App.ChatClients.Delete(sessionId) | ||||
| 				h.App.ReqCancelFunc.Delete(sessionId) | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			message := string(msg) | ||||
| 			logger.Info("Receive a message: ", message) | ||||
| 			//utils.ReplyMessage(client, "这是一条测试消息!") | ||||
| 			ctx, cancel := context.WithCancel(context.Background()) | ||||
| 			h.App.ReqCancelFunc.Put(sessionId, cancel) | ||||
| 			// 回复消息 | ||||
| 			err = h.sendMessage(ctx, session, chatRole, message, client) | ||||
| 			if err != nil { | ||||
| 				logger.Error(err) | ||||
| 				utils.ReplyChunkMessage(client, types.WsMessage{Type: types.WsEnd}) | ||||
| 			} else { | ||||
| 				utils.ReplyChunkMessage(client, types.WsMessage{Type: types.WsEnd}) | ||||
| 				logger.Info("回答完毕: " + string(message)) | ||||
| 			} | ||||
|  | ||||
| 		} | ||||
| 	}() | ||||
| } | ||||
|  | ||||
| func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSession, role model.ChatRole, prompt string, ws *types.WsClient) error { | ||||
| 	defer func() { | ||||
| 		if r := recover(); r != nil { | ||||
| 			logger.Error("Recover message from error: ", r) | ||||
| 		} | ||||
| 	}() | ||||
|  | ||||
| 	var user model.User | ||||
| 	res := h.db.Model(&model.User{}).First(&user, session.UserId) | ||||
| 	if res.Error != nil { | ||||
| 		utils.ReplyMessage(ws, "非法用户,请联系管理员!") | ||||
| 		return res.Error | ||||
| 	} | ||||
| 	var userVo vo.User | ||||
| 	err := utils.CopyObject(user, &userVo) | ||||
| 	userVo.Id = user.Id | ||||
| 	if err != nil { | ||||
| 		return errors.New("User 对象转换失败," + err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	if userVo.Status == false { | ||||
| 		utils.ReplyMessage(ws, "您的账号已经被禁用,如果疑问,请联系管理员!") | ||||
| 		utils.ReplyMessage(ws, "") | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	if userVo.Calls <= 0 && userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" { | ||||
| 		utils.ReplyMessage(ws, "您的对话次数已经用尽,请联系管理员或者点击左下角菜单加入众筹获得100次对话!") | ||||
| 		utils.ReplyMessage(ws, "") | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	if userVo.ExpiredTime > 0 && userVo.ExpiredTime <= time.Now().Unix() { | ||||
| 		utils.ReplyMessage(ws, "您的账号已经过期,请联系管理员!") | ||||
| 		utils.ReplyMessage(ws, "") | ||||
| 		return nil | ||||
| 	} | ||||
| 	var req = types.ApiRequest{ | ||||
| 		Model:  session.Model.Value, | ||||
| 		Stream: true, | ||||
| 	} | ||||
| 	switch session.Model.Platform { | ||||
| 	case types.Azure: | ||||
| 		req.Temperature = h.App.ChatConfig.Azure.Temperature | ||||
| 		req.MaxTokens = h.App.ChatConfig.Azure.MaxTokens | ||||
| 		break | ||||
| 	case types.ChatGLM: | ||||
| 		req.Temperature = h.App.ChatConfig.ChatGML.Temperature | ||||
| 		req.MaxTokens = h.App.ChatConfig.ChatGML.MaxTokens | ||||
| 		break | ||||
| 	default: | ||||
| 		req.Temperature = h.App.ChatConfig.OpenAI.Temperature | ||||
| 		req.MaxTokens = h.App.ChatConfig.OpenAI.MaxTokens | ||||
| 		var functions = make([]types.Function, 0) | ||||
| 		for _, f := range types.InnerFunctions { | ||||
| 			if !h.App.SysConfig.EnabledDraw && f.Name == types.FuncMidJourney { | ||||
| 				continue | ||||
| 			} | ||||
| 			functions = append(functions, f) | ||||
| 		} | ||||
| 		req.Functions = functions | ||||
| 	} | ||||
|  | ||||
| 	// 加载聊天上下文 | ||||
| 	var chatCtx []interface{} | ||||
| 	if h.App.ChatConfig.EnableContext { | ||||
| 		if h.App.ChatContexts.Has(session.ChatId) { | ||||
| 			chatCtx = h.App.ChatContexts.Get(session.ChatId) | ||||
| 		} else { | ||||
| 			// calculate the tokens of current request, to prevent to exceeding the max tokens num | ||||
| 			tokens := req.MaxTokens | ||||
| 			for _, f := range types.InnerFunctions { | ||||
| 				tks, _ := utils.CalcTokens(utils.JsonEncode(f), req.Model) | ||||
| 				tokens += tks | ||||
| 			} | ||||
|  | ||||
| 			// loading the role context | ||||
| 			var messages []types.Message | ||||
| 			err := utils.JsonDecode(role.Context, &messages) | ||||
| 			if err == nil { | ||||
| 				for _, v := range messages { | ||||
| 					tks, _ := utils.CalcTokens(v.Content, req.Model) | ||||
| 					if tokens+tks >= types.ModelToTokens[req.Model] { | ||||
| 						break | ||||
| 					} | ||||
| 					tokens += tks | ||||
| 					chatCtx = append(chatCtx, v) | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			// loading recent chat history as chat context | ||||
| 			if chatConfig.ContextDeep > 0 { | ||||
| 				var historyMessages []model.HistoryMessage | ||||
| 				res := h.db.Where("chat_id = ? and use_context = 1", session.ChatId).Limit(chatConfig.ContextDeep).Order("created_at desc").Find(&historyMessages) | ||||
| 				if res.Error == nil { | ||||
| 					for _, msg := range historyMessages { | ||||
| 						if tokens+msg.Tokens >= types.ModelToTokens[session.Model.Value] { | ||||
| 							break | ||||
| 						} | ||||
| 						tokens += msg.Tokens | ||||
| 						ms := types.Message{Role: "user", Content: msg.Content} | ||||
| 						if msg.Type == types.ReplyMsg { | ||||
| 							ms.Role = "assistant" | ||||
| 						} | ||||
| 						chatCtx = append(chatCtx, ms) | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		logger.Debugf("聊天上下文:%+v", chatCtx) | ||||
| 	} | ||||
| 	reqMgs := make([]interface{}, 0) | ||||
| 	for _, m := range chatCtx { | ||||
| 		reqMgs = append(reqMgs, m) | ||||
| 	} | ||||
|  | ||||
| 	req.Messages = append(reqMgs, map[string]interface{}{ | ||||
| 		"role":    "user", | ||||
| 		"content": prompt, | ||||
| 	}) | ||||
|  | ||||
| 	switch session.Model.Platform { | ||||
| 	case types.Azure: | ||||
| 		return h.sendAzureMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws) | ||||
| 	case types.OpenAI: | ||||
| 		return h.sendOpenAiMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws) | ||||
| 	case types.ChatGLM: | ||||
| 		return h.sendChatGLMMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws) | ||||
| 	} | ||||
| 	utils.ReplyChunkMessage(ws, types.WsMessage{ | ||||
| 		Type:    types.WsMiddle, | ||||
| 		Content: fmt.Sprintf("Not supported platform: %s", session.Model.Platform), | ||||
| 	}) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // Tokens 统计 token 数量 | ||||
| func (h *ChatHandler) Tokens(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Text  string `json:"text"` | ||||
| 		Model string `json:"model"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// 如果没有传入 text 字段,则说明是获取当前 reply 总的 token 消耗(带上下文) | ||||
| 	if data.Text == "" { | ||||
| 		var item model.HistoryMessage | ||||
| 		userId, _ := c.Get(types.LoginUserID) | ||||
| 		res := h.db.Where("user_id = ?", userId).Last(&item) | ||||
| 		if res.Error != nil { | ||||
| 			resp.ERROR(c, res.Error.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 		resp.SUCCESS(c, item.Tokens) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	tokens, err := utils.CalcTokens(data.Text, data.Model) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, tokens) | ||||
| } | ||||
|  | ||||
| func getTotalTokens(req types.ApiRequest) int { | ||||
| 	encode := utils.JsonEncode(req.Messages) | ||||
| 	var items []map[string]interface{} | ||||
| 	err := utils.JsonDecode(encode, &items) | ||||
| 	if err != nil { | ||||
| 		return 0 | ||||
| 	} | ||||
| 	tokens := 0 | ||||
| 	for _, item := range items { | ||||
| 		content, ok := item["content"] | ||||
| 		if ok && !utils.IsEmptyValue(content) { | ||||
| 			t, err := utils.CalcTokens(utils.InterfaceToString(content), req.Model) | ||||
| 			if err == nil { | ||||
| 				tokens += t | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return tokens | ||||
| } | ||||
|  | ||||
| // StopGenerate 停止生成 | ||||
| func (h *ChatHandler) StopGenerate(c *gin.Context) { | ||||
| 	sessionId := c.Query("session_id") | ||||
| 	if h.App.ReqCancelFunc.Has(sessionId) { | ||||
| 		h.App.ReqCancelFunc.Get(sessionId)() | ||||
| 		h.App.ReqCancelFunc.Delete(sessionId) | ||||
| 	} | ||||
| 	resp.SUCCESS(c, types.OkMsg) | ||||
| } | ||||
|  | ||||
| // 发送请求到 OpenAI 服务器 | ||||
| // useOwnApiKey: 是否使用了用户自己的 API KEY | ||||
| func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platform types.Platform, apiKey *string) (*http.Response, error) { | ||||
|  | ||||
| 	var apiURL string | ||||
| 	switch platform { | ||||
| 	case types.Azure: | ||||
| 		md := strings.Replace(req.Model, ".", "", 1) | ||||
| 		apiURL = strings.Replace(h.App.ChatConfig.Azure.ApiURL, "{model}", md, 1) | ||||
| 		break | ||||
| 	case types.ChatGLM: | ||||
| 		apiURL = strings.Replace(h.App.ChatConfig.ChatGML.ApiURL, "{model}", req.Model, 1) | ||||
| 		req.Prompt = req.Messages | ||||
| 		req.Messages = nil | ||||
| 		break | ||||
| 	default: | ||||
| 		apiURL = h.App.ChatConfig.OpenAI.ApiURL | ||||
| 	} | ||||
| 	// 创建 HttpClient 请求对象 | ||||
| 	var client *http.Client | ||||
| 	requestBody, err := json.Marshal(req) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	request, err := http.NewRequest(http.MethodPost, apiURL, bytes.NewBuffer(requestBody)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	request = request.WithContext(ctx) | ||||
| 	request.Header.Set("Content-Type", "application/json") | ||||
| 	proxyURL := h.App.Config.ProxyURL | ||||
| 	if proxyURL != "" && platform == types.OpenAI { // 使用代理 | ||||
| 		proxy, _ := url.Parse(proxyURL) | ||||
| 		client = &http.Client{ | ||||
| 			Transport: &http.Transport{ | ||||
| 				Proxy: http.ProxyURL(proxy), | ||||
| 			}, | ||||
| 		} | ||||
| 	} else { | ||||
| 		client = http.DefaultClient | ||||
| 	} | ||||
| 	if *apiKey == "" { | ||||
| 		var key model.ApiKey | ||||
| 		res := h.db.Where("platform = ?", platform).Order("last_used_at ASC").First(&key) | ||||
| 		if res.Error != nil { | ||||
| 			return nil, errors.New("no available key, please import key") | ||||
| 		} | ||||
| 		// 更新 API KEY 的最后使用时间 | ||||
| 		h.db.Model(&key).UpdateColumn("last_used_at", time.Now().Unix()) | ||||
| 		*apiKey = key.Value | ||||
| 	} | ||||
|  | ||||
| 	logger.Infof("Sending %s request, KEY: %s, PROXY: %s, Model: %s", platform, *apiKey, proxyURL, req.Model) | ||||
| 	switch platform { | ||||
| 	case types.Azure: | ||||
| 		request.Header.Set("api-key", *apiKey) | ||||
| 		break | ||||
| 	case types.ChatGLM: | ||||
| 		token, err := h.getChatGLMToken(*apiKey) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		logger.Info(token) | ||||
| 		request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) | ||||
| 		break | ||||
| 	default: | ||||
| 		request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiKey)) | ||||
| 	} | ||||
| 	return client.Do(request) | ||||
| } | ||||
							
								
								
									
										103
									
								
								api/handler/chat_history_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								api/handler/chat_history_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,103 @@ | ||||
| package handler | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/store/vo" | ||||
| 	"chatplus/utils" | ||||
| 	"chatplus/utils/resp" | ||||
| 	"gorm.io/gorm" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| ) | ||||
|  | ||||
| // Update 更新会话标题 | ||||
| func (h *ChatHandler) Update(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Id    uint   `json:"id"` | ||||
| 		Title string `json:"title"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 	var m = model.ChatItem{} | ||||
| 	m.Id = data.Id | ||||
| 	res := h.db.Model(&m).UpdateColumn("title", data.Title) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "Failed to update database") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, types.OkMsg) | ||||
| } | ||||
|  | ||||
| // History 获取聊天历史记录 | ||||
| func (h *ChatHandler) History(c *gin.Context) { | ||||
| 	chatId := c.Query("chat_id") // 会话 ID | ||||
| 	var items []model.HistoryMessage | ||||
| 	var messages = make([]vo.HistoryMessage, 0) | ||||
| 	res := h.db.Where("chat_id = ?", chatId).Find(&items) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "No history message") | ||||
| 		return | ||||
| 	} else { | ||||
| 		for _, item := range items { | ||||
| 			var v vo.HistoryMessage | ||||
| 			err := utils.CopyObject(item, &v) | ||||
| 			v.CreatedAt = item.CreatedAt.Unix() | ||||
| 			v.UpdatedAt = item.UpdatedAt.Unix() | ||||
| 			if err == nil { | ||||
| 				messages = append(messages, v) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, messages) | ||||
| } | ||||
|  | ||||
| // Clear 清空所有聊天记录 | ||||
| func (h *ChatHandler) Clear(c *gin.Context) { | ||||
| 	// 获取当前登录用户所有的聊天会话 | ||||
| 	user, err := utils.GetLoginUser(c, h.db) | ||||
| 	if err != nil { | ||||
| 		resp.NotAuth(c) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var chats []model.ChatItem | ||||
| 	res := h.db.Where("user_id = ?", user.Id).Find(&chats) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "No chats found") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var chatIds = make([]string, 0) | ||||
| 	for _, chat := range chats { | ||||
| 		chatIds = append(chatIds, chat.ChatId) | ||||
| 		// 清空会话上下文 | ||||
| 		h.App.ChatContexts.Delete(chat.ChatId) | ||||
| 	} | ||||
| 	err = h.db.Transaction(func(tx *gorm.DB) error { | ||||
| 		res := h.db.Where("user_id =?", user.Id).Delete(&model.ChatItem{}) | ||||
| 		if res.Error != nil { | ||||
| 			return res.Error | ||||
| 		} | ||||
|  | ||||
| 		res = h.db.Where("user_id = ? AND chat_id IN ?", user.Id, chatIds).Delete(&model.HistoryMessage{}) | ||||
| 		if res.Error != nil { | ||||
| 			return res.Error | ||||
| 		} | ||||
|  | ||||
| 		// TODO: 是否要删除 MidJourney 绘画记录和图片文件? | ||||
| 		return nil | ||||
| 	}) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		logger.Errorf("Error with delete chats: %+v", err) | ||||
| 		resp.ERROR(c, "Failed to remove chat from database.") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, types.OkMsg) | ||||
| } | ||||
							
								
								
									
										105
									
								
								api/handler/chat_item_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								api/handler/chat_item_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,105 @@ | ||||
| package handler | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/store/vo" | ||||
| 	"chatplus/utils" | ||||
| 	"chatplus/utils/resp" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| ) | ||||
|  | ||||
| // List 获取会话列表 | ||||
| func (h *ChatHandler) List(c *gin.Context) { | ||||
| 	userId := h.GetInt(c, "user_id", 0) | ||||
| 	if userId == 0 { | ||||
| 		resp.ERROR(c, "The parameter 'user_id' is needed.") | ||||
| 		return | ||||
| 	} | ||||
| 	var items = make([]vo.ChatItem, 0) | ||||
| 	var chats []model.ChatItem | ||||
| 	res := h.db.Where("user_id = ?", userId).Order("id DESC").Find(&chats) | ||||
| 	if res.Error == nil { | ||||
| 		var roleIds = make([]uint, 0) | ||||
| 		for _, chat := range chats { | ||||
| 			roleIds = append(roleIds, chat.RoleId) | ||||
| 		} | ||||
| 		var roles []model.ChatRole | ||||
| 		res = h.db.Find(&roles, roleIds) | ||||
| 		if res.Error == nil { | ||||
| 			roleMap := make(map[uint]model.ChatRole) | ||||
| 			for _, role := range roles { | ||||
| 				roleMap[role.Id] = role | ||||
| 			} | ||||
|  | ||||
| 			for _, chat := range chats { | ||||
| 				var item vo.ChatItem | ||||
| 				err := utils.CopyObject(chat, &item) | ||||
| 				if err == nil { | ||||
| 					item.Id = chat.Id | ||||
| 					item.Icon = roleMap[chat.RoleId].Icon | ||||
| 					items = append(items, item) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 	} | ||||
| 	resp.SUCCESS(c, items) | ||||
| } | ||||
|  | ||||
| // Remove 删除会话 | ||||
| func (h *ChatHandler) Remove(c *gin.Context) { | ||||
| 	chatId := h.GetTrim(c, "chat_id") | ||||
| 	if chatId == "" { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 	user, err := utils.GetLoginUser(c, h.db) | ||||
| 	if err != nil { | ||||
| 		resp.NotAuth(c) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	res := h.db.Where("user_id = ? AND chat_id = ?", user.Id, chatId).Delete(&model.ChatItem{}) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "Failed to update database") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// 删除当前会话的聊天记录 | ||||
| 	res = h.db.Where("user_id = ? AND chat_id =?", user.Id, chatId).Delete(&model.ChatItem{}) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "Failed to remove chat from database.") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// TODO: 是否要删除 MidJourney 绘画记录和图片文件? | ||||
|  | ||||
| 	// 清空会话上下文 | ||||
| 	h.App.ChatContexts.Delete(chatId) | ||||
| 	resp.SUCCESS(c, types.OkMsg) | ||||
| } | ||||
|  | ||||
| func (h *ChatHandler) Detail(c *gin.Context) { | ||||
| 	chatId := h.GetTrim(c, "chat_id") | ||||
| 	if utils.IsEmptyValue(chatId) { | ||||
| 		resp.ERROR(c, "Invalid chatId") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var chatItem model.ChatItem | ||||
| 	res := h.db.Where("chat_id = ?", chatId).First(&chatItem) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "No chat found") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var chatItemVo vo.ChatItem | ||||
| 	err := utils.CopyObject(chatItem, &chatItemVo) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, chatItemVo) | ||||
| } | ||||
							
								
								
									
										44
									
								
								api/handler/chat_model_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								api/handler/chat_model_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| package handler | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/store/vo" | ||||
| 	"chatplus/utils" | ||||
| 	"chatplus/utils/resp" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| type ChatModelHandler struct { | ||||
| 	BaseHandler | ||||
| 	db *gorm.DB | ||||
| } | ||||
|  | ||||
| func NewChatModelHandler(app *core.AppServer, db *gorm.DB) *ChatModelHandler { | ||||
| 	h := ChatModelHandler{db: db} | ||||
| 	h.App = app | ||||
| 	return &h | ||||
| } | ||||
|  | ||||
| // List 模型列表 | ||||
| func (h *ChatModelHandler) List(c *gin.Context) { | ||||
| 	var items []model.ChatModel | ||||
| 	var cms = make([]vo.ChatModel, 0) | ||||
| 	res := h.db.Where("enabled = ?", true).Order("sort_num ASC").Find(&items) | ||||
| 	if res.Error == nil { | ||||
| 		for _, item := range items { | ||||
| 			var cm vo.ChatModel | ||||
| 			err := utils.CopyObject(item, &cm) | ||||
| 			if err == nil { | ||||
| 				cm.Id = item.Id | ||||
| 				cm.CreatedAt = item.CreatedAt.Unix() | ||||
| 				cm.UpdatedAt = item.UpdatedAt.Unix() | ||||
| 				cms = append(cms, cm) | ||||
| 			} else { | ||||
| 				logger.Error(err) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	resp.SUCCESS(c, cms) | ||||
| } | ||||
| @@ -25,7 +25,7 @@ func NewChatRoleHandler(app *core.AppServer, db *gorm.DB) *ChatRoleHandler { | ||||
| // List get user list | ||||
| func (h *ChatRoleHandler) List(c *gin.Context) { | ||||
| 	var roles []model.ChatRole | ||||
| 	res := h.db.Where("enable", true).Order("sort ASC").Find(&roles) | ||||
| 	res := h.db.Where("enable", true).Order("sort_num ASC").Find(&roles) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "No roles found,"+res.Error.Error()) | ||||
| 		return | ||||
							
								
								
									
										235
									
								
								api/handler/chatglm_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										235
									
								
								api/handler/chatglm_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,235 @@ | ||||
| package handler | ||||
|  | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/store/vo" | ||||
| 	"chatplus/utils" | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"github.com/golang-jwt/jwt/v5" | ||||
| 	"gorm.io/gorm" | ||||
| 	"io" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 	"unicode/utf8" | ||||
| ) | ||||
|  | ||||
| // 将消息发送给 ChatGLM API 并获取结果,通过 WebSocket 推送到客户端 | ||||
| func (h *ChatHandler) sendChatGLMMessage( | ||||
| 	chatCtx []interface{}, | ||||
| 	req types.ApiRequest, | ||||
| 	userVo vo.User, | ||||
| 	ctx context.Context, | ||||
| 	session *types.ChatSession, | ||||
| 	role model.ChatRole, | ||||
| 	prompt string, | ||||
| 	ws *types.WsClient) error { | ||||
| 	promptCreatedAt := time.Now() // 记录提问时间 | ||||
| 	start := time.Now() | ||||
| 	var apiKey = userVo.ChatConfig.ApiKeys[session.Model.Platform] | ||||
| 	response, err := h.doRequest(ctx, req, session.Model.Platform, &apiKey) | ||||
| 	logger.Info("HTTP请求完成,耗时:", time.Now().Sub(start)) | ||||
| 	if err != nil { | ||||
| 		if strings.Contains(err.Error(), "context canceled") { | ||||
| 			logger.Info("用户取消了请求:", prompt) | ||||
| 			return nil | ||||
| 		} else if strings.Contains(err.Error(), "no available key") { | ||||
| 			utils.ReplyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!") | ||||
| 			return nil | ||||
| 		} else { | ||||
| 			logger.Error(err) | ||||
| 		} | ||||
|  | ||||
| 		utils.ReplyMessage(ws, ErrorMsg) | ||||
| 		utils.ReplyMessage(ws, "") | ||||
| 		return err | ||||
| 	} else { | ||||
| 		defer response.Body.Close() | ||||
| 	} | ||||
|  | ||||
| 	contentType := response.Header.Get("Content-Type") | ||||
| 	if strings.Contains(contentType, "text/event-stream") { | ||||
| 		replyCreatedAt := time.Now() // 记录回复时间 | ||||
| 		// 循环读取 Chunk 消息 | ||||
| 		var message = types.Message{} | ||||
| 		var contents = make([]string, 0) | ||||
| 		var event, content string | ||||
| 		scanner := bufio.NewScanner(response.Body) | ||||
| 		for scanner.Scan() { | ||||
| 			line := scanner.Text() | ||||
| 			if len(line) < 5 || strings.HasPrefix(line, "id:") { | ||||
| 				continue | ||||
| 			} | ||||
| 			if strings.HasPrefix(line, "event:") { | ||||
| 				event = line[6:] | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			if strings.HasPrefix(line, "data:") { | ||||
| 				content = line[5:] | ||||
| 			} | ||||
| 			switch event { | ||||
| 			case "add": | ||||
| 				if len(contents) == 0 { | ||||
| 					utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart}) | ||||
| 				} | ||||
| 				utils.ReplyChunkMessage(ws, types.WsMessage{ | ||||
| 					Type:    types.WsMiddle, | ||||
| 					Content: utils.InterfaceToString(content), | ||||
| 				}) | ||||
| 				contents = append(contents, content) | ||||
| 			case "finish": | ||||
| 				break | ||||
| 			case "error": | ||||
| 				utils.ReplyMessage(ws, fmt.Sprintf("**调用 ChatGLM API 出错:%s**", content)) | ||||
| 				break | ||||
| 			case "interrupted": | ||||
| 				utils.ReplyMessage(ws, "**调用 ChatGLM API 出错,当前输出被中断!**") | ||||
| 			} | ||||
|  | ||||
| 		} // end for | ||||
|  | ||||
| 		if err := scanner.Err(); err != nil { | ||||
| 			if strings.Contains(err.Error(), "context canceled") { | ||||
| 				logger.Info("用户取消了请求:", prompt) | ||||
| 			} else { | ||||
| 				logger.Error("信息读取出错:", err) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// 消息发送成功 | ||||
| 		if len(contents) > 0 { | ||||
| 			// 更新用户的对话次数 | ||||
| 			if userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" { | ||||
| 				h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("calls", gorm.Expr("calls - ?", 1)) | ||||
| 			} | ||||
|  | ||||
| 			if message.Role == "" { | ||||
| 				message.Role = "assistant" | ||||
| 			} | ||||
| 			message.Content = strings.Join(contents, "") | ||||
| 			useMsg := types.Message{Role: "user", Content: prompt} | ||||
|  | ||||
| 			// 更新上下文消息,如果是调用函数则不需要更新上下文 | ||||
| 			if h.App.ChatConfig.EnableContext { | ||||
| 				chatCtx = append(chatCtx, useMsg)  // 提问消息 | ||||
| 				chatCtx = append(chatCtx, message) // 回复消息 | ||||
| 				h.App.ChatContexts.Put(session.ChatId, chatCtx) | ||||
| 			} | ||||
|  | ||||
| 			// 追加聊天记录 | ||||
| 			if h.App.ChatConfig.EnableHistory { | ||||
| 				// for prompt | ||||
| 				promptToken, err := utils.CalcTokens(prompt, req.Model) | ||||
| 				if err != nil { | ||||
| 					logger.Error(err) | ||||
| 				} | ||||
| 				historyUserMsg := model.HistoryMessage{ | ||||
| 					UserId:     userVo.Id, | ||||
| 					ChatId:     session.ChatId, | ||||
| 					RoleId:     role.Id, | ||||
| 					Type:       types.PromptMsg, | ||||
| 					Icon:       userVo.Avatar, | ||||
| 					Content:    prompt, | ||||
| 					Tokens:     promptToken, | ||||
| 					UseContext: true, | ||||
| 				} | ||||
| 				historyUserMsg.CreatedAt = promptCreatedAt | ||||
| 				historyUserMsg.UpdatedAt = promptCreatedAt | ||||
| 				res := h.db.Save(&historyUserMsg) | ||||
| 				if res.Error != nil { | ||||
| 					logger.Error("failed to save prompt history message: ", res.Error) | ||||
| 				} | ||||
|  | ||||
| 				// for reply | ||||
| 				// 计算本次对话消耗的总 token 数量 | ||||
| 				replyToken, _ := utils.CalcTokens(message.Content, req.Model) | ||||
| 				totalTokens := replyToken + getTotalTokens(req) | ||||
| 				historyReplyMsg := model.HistoryMessage{ | ||||
| 					UserId:     userVo.Id, | ||||
| 					ChatId:     session.ChatId, | ||||
| 					RoleId:     role.Id, | ||||
| 					Type:       types.ReplyMsg, | ||||
| 					Icon:       role.Icon, | ||||
| 					Content:    message.Content, | ||||
| 					Tokens:     totalTokens, | ||||
| 					UseContext: true, | ||||
| 				} | ||||
| 				historyReplyMsg.CreatedAt = replyCreatedAt | ||||
| 				historyReplyMsg.UpdatedAt = replyCreatedAt | ||||
| 				res = h.db.Create(&historyReplyMsg) | ||||
| 				if res.Error != nil { | ||||
| 					logger.Error("failed to save reply history message: ", res.Error) | ||||
| 				} | ||||
| 				// 更新用户信息 | ||||
| 				h.db.Model(&model.User{}).Where("id = ?", userVo.Id). | ||||
| 					UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", totalTokens)) | ||||
| 			} | ||||
|  | ||||
| 			// 保存当前会话 | ||||
| 			var chatItem model.ChatItem | ||||
| 			res := h.db.Where("chat_id = ?", session.ChatId).First(&chatItem) | ||||
| 			if res.Error != nil { | ||||
| 				chatItem.ChatId = session.ChatId | ||||
| 				chatItem.UserId = session.UserId | ||||
| 				chatItem.RoleId = role.Id | ||||
| 				chatItem.ModelId = session.Model.Id | ||||
| 				if utf8.RuneCountInString(prompt) > 30 { | ||||
| 					chatItem.Title = string([]rune(prompt)[:30]) + "..." | ||||
| 				} else { | ||||
| 					chatItem.Title = prompt | ||||
| 				} | ||||
| 				h.db.Create(&chatItem) | ||||
| 			} | ||||
| 		} | ||||
| 	} else { | ||||
| 		body, err := io.ReadAll(response.Body) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("error with reading response: %v", err) | ||||
| 		} | ||||
|  | ||||
| 		var res struct { | ||||
| 			Code    int    `json:"code"` | ||||
| 			Success bool   `json:"success"` | ||||
| 			Msg     string `json:"msg"` | ||||
| 		} | ||||
| 		err = json.Unmarshal(body, &res) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("error with decode response: %v", err) | ||||
| 		} | ||||
| 		if !res.Success { | ||||
| 			utils.ReplyMessage(ws, "请求 ChatGLM 失败:"+res.Msg) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (h *ChatHandler) getChatGLMToken(apiKey string) (string, error) { | ||||
| 	ctx := context.Background() | ||||
| 	tokenString, err := h.redis.Get(ctx, apiKey).Result() | ||||
| 	if err == nil { | ||||
| 		return tokenString, nil | ||||
| 	} | ||||
|  | ||||
| 	expr := time.Hour * 2 | ||||
| 	key := strings.Split(apiKey, ".") | ||||
| 	if len(key) != 2 { | ||||
| 		return "", fmt.Errorf("invalid api key: %s", apiKey) | ||||
| 	} | ||||
| 	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ | ||||
| 		"api_key":   key[0], | ||||
| 		"timestamp": time.Now().Unix(), | ||||
| 		"exp":       time.Now().Add(expr).Add(time.Second * 10).Unix(), | ||||
| 	}) | ||||
| 	token.Header["alg"] = "HS256" | ||||
| 	token.Header["sign_type"] = "SIGN" | ||||
| 	delete(token.Header, "typ") | ||||
| 	// Sign and get the complete encoded token as a string using the secret | ||||
| 	tokenString, err = token.SignedString([]byte(key[1])) | ||||
| 	h.redis.Set(ctx, apiKey, tokenString, expr) | ||||
| 	return tokenString, err | ||||
| } | ||||
							
								
								
									
										560
									
								
								api/handler/mj_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										560
									
								
								api/handler/mj_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,560 @@ | ||||
| package handler | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core" | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/service" | ||||
| 	"chatplus/service/oss" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/store/vo" | ||||
| 	"chatplus/utils" | ||||
| 	"chatplus/utils/resp" | ||||
| 	"encoding/base64" | ||||
| 	"fmt" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/go-redis/redis/v8" | ||||
| 	"github.com/gorilla/websocket" | ||||
| 	"gorm.io/gorm" | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| type TaskStatus string | ||||
|  | ||||
| const ( | ||||
| 	Stopped  = TaskStatus("Stopped") | ||||
| 	Finished = TaskStatus("Finished") | ||||
| ) | ||||
|  | ||||
| type Image struct { | ||||
| 	URL      string `json:"url"` | ||||
| 	ProxyURL string `json:"proxy_url"` | ||||
| 	Filename string `json:"filename"` | ||||
| 	Width    int    `json:"width"` | ||||
| 	Height   int    `json:"height"` | ||||
| 	Size     int    `json:"size"` | ||||
| 	Hash     string `json:"hash"` | ||||
| } | ||||
|  | ||||
| type MidJourneyHandler struct { | ||||
| 	BaseHandler | ||||
| 	redis           *redis.Client | ||||
| 	db              *gorm.DB | ||||
| 	mjService       *service.MjService | ||||
| 	uploaderManager *oss.UploaderManager | ||||
| 	lock            sync.Mutex | ||||
| 	clients         *types.LMap[string, *types.WsClient] | ||||
| } | ||||
|  | ||||
| func NewMidJourneyHandler( | ||||
| 	app *core.AppServer, | ||||
| 	client *redis.Client, | ||||
| 	db *gorm.DB, | ||||
| 	manager *oss.UploaderManager, | ||||
| 	mjService *service.MjService) *MidJourneyHandler { | ||||
| 	h := MidJourneyHandler{ | ||||
| 		redis:           client, | ||||
| 		db:              db, | ||||
| 		uploaderManager: manager, | ||||
| 		lock:            sync.Mutex{}, | ||||
| 		mjService:       mjService, | ||||
| 		clients:         types.NewLMap[string, *types.WsClient](), | ||||
| 	} | ||||
| 	h.App = app | ||||
| 	return &h | ||||
| } | ||||
|  | ||||
| type notifyData struct { | ||||
| 	MessageId   string     `json:"message_id"` | ||||
| 	ReferenceId string     `json:"reference_id"` | ||||
| 	Image       Image      `json:"image"` | ||||
| 	Content     string     `json:"content"` | ||||
| 	Prompt      string     `json:"prompt"` | ||||
| 	Status      TaskStatus `json:"status"` | ||||
| 	Progress    int        `json:"progress"` | ||||
| } | ||||
|  | ||||
| // Client WebSocket 客户端,用于通知任务状态变更 | ||||
| func (h *MidJourneyHandler) Client(c *gin.Context) { | ||||
| 	ws, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}).Upgrade(c.Writer, c.Request, nil) | ||||
| 	if err != nil { | ||||
| 		logger.Error(err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	sessionId := c.Query("session_id") | ||||
| 	client := types.NewWsClient(ws) | ||||
| 	// 删除旧的连接 | ||||
| 	h.clients.Delete(sessionId) | ||||
| 	h.clients.Put(sessionId, client) | ||||
| 	logger.Infof("New websocket connected, IP: %s", c.ClientIP()) | ||||
| } | ||||
|  | ||||
| func (h *MidJourneyHandler) Notify(c *gin.Context) { | ||||
| 	token := c.GetHeader("Authorization") | ||||
| 	if token != h.App.Config.ExtConfig.Token { | ||||
| 		resp.NotAuth(c) | ||||
| 		return | ||||
| 	} | ||||
| 	var data notifyData | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil || data.Prompt == "" { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 	logger.Debugf("收到 MidJourney 回调请求:%+v", data) | ||||
|  | ||||
| 	h.lock.Lock() | ||||
| 	defer h.lock.Unlock() | ||||
|  | ||||
| 	err, finished := h.notifyHandler(c, data) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// 解除任务锁定 | ||||
| 	if finished && (data.Status == Finished || data.Status == Stopped) { | ||||
| 		h.redis.Del(c, service.MjRunningJobKey) | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
|  | ||||
| } | ||||
|  | ||||
| func (h *MidJourneyHandler) notifyHandler(c *gin.Context, data notifyData) (error, bool) { | ||||
| 	taskString, err := h.redis.Get(c, service.MjRunningJobKey).Result() | ||||
| 	if err != nil { // 过期任务,丢弃 | ||||
| 		logger.Warn("任务已过期:", err) | ||||
| 		return nil, true | ||||
| 	} | ||||
|  | ||||
| 	var task service.MjTask | ||||
| 	err = utils.JsonDecode(taskString, &task) | ||||
| 	if err != nil { // 非标准任务,丢弃 | ||||
| 		logger.Warn("任务解析失败:", err) | ||||
| 		return nil, false | ||||
| 	} | ||||
|  | ||||
| 	var job model.MidJourneyJob | ||||
| 	res := h.db.Where("message_id = ?", data.MessageId).First(&job) | ||||
| 	if res.Error == nil && data.Status == Finished { | ||||
| 		logger.Warn("重复消息:", data.MessageId) | ||||
| 		return nil, false | ||||
| 	} | ||||
|  | ||||
| 	if task.Src == service.TaskSrcImg { // 绘画任务 | ||||
| 		var job model.MidJourneyJob | ||||
| 		res := h.db.Where("id = ?", task.Id).First(&job) | ||||
| 		if res.Error != nil { | ||||
| 			logger.Warn("非法任务:", res.Error) | ||||
| 			return nil, false | ||||
| 		} | ||||
| 		job.MessageId = data.MessageId | ||||
| 		job.ReferenceId = data.ReferenceId | ||||
| 		job.Progress = data.Progress | ||||
| 		job.Prompt = data.Prompt | ||||
| 		job.Hash = data.Image.Hash | ||||
|  | ||||
| 		// 任务完成,将最终的图片下载下来 | ||||
| 		if data.Progress == 100 { | ||||
| 			imgURL, err := h.uploaderManager.GetUploadHandler().PutImg(data.Image.URL) | ||||
| 			if err != nil { | ||||
| 				logger.Error("error with download img: ", err.Error()) | ||||
| 				return err, false | ||||
| 			} | ||||
| 			job.ImgURL = imgURL | ||||
| 		} else { | ||||
| 			// 临时图片直接保存,访问的时候使用代理进行转发 | ||||
| 			job.ImgURL = data.Image.URL | ||||
| 		} | ||||
| 		res = h.db.Updates(&job) | ||||
| 		if res.Error != nil { | ||||
| 			logger.Error("error with update job: ", res.Error) | ||||
| 			return res.Error, false | ||||
| 		} | ||||
|  | ||||
| 		var jobVo vo.MidJourneyJob | ||||
| 		err := utils.CopyObject(job, &jobVo) | ||||
| 		if err == nil { | ||||
| 			if data.Progress < 100 { | ||||
| 				image, err := utils.DownloadImage(jobVo.ImgURL, h.App.Config.ProxyURL) | ||||
| 				if err == nil { | ||||
| 					jobVo.ImgURL = "data:image/png;base64," + base64.StdEncoding.EncodeToString(image) | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			// 推送任务到前端 | ||||
| 			client := h.clients.Get(task.SessionId) | ||||
| 			if client != nil { | ||||
| 				utils.ReplyChunkMessage(client, jobVo) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 	} else if task.Src == service.TaskSrcChat { // 聊天任务 | ||||
| 		wsClient := h.App.MjTaskClients.Get(task.SessionId) | ||||
| 		if data.Status == Finished { | ||||
| 			if wsClient != nil && data.ReferenceId != "" { | ||||
| 				content := fmt.Sprintf("**%s** 任务执行成功,正在从 MidJourney 服务器下载图片,请稍后...", data.Prompt) | ||||
| 				utils.ReplyMessage(wsClient, content) | ||||
| 			} | ||||
| 			// download image | ||||
| 			imgURL, err := h.uploaderManager.GetUploadHandler().PutImg(data.Image.URL) | ||||
| 			if err != nil { | ||||
| 				logger.Error("error with download image: ", err) | ||||
| 				if wsClient != nil && data.ReferenceId != "" { | ||||
| 					content := fmt.Sprintf("**%s** 图片下载失败:%s", data.Prompt, err.Error()) | ||||
| 					utils.ReplyMessage(wsClient, content) | ||||
| 				} | ||||
| 				return err, false | ||||
| 			} | ||||
|  | ||||
| 			tx := h.db.Begin() | ||||
| 			data.Image.URL = imgURL | ||||
| 			message := model.HistoryMessage{ | ||||
| 				UserId:     uint(task.UserId), | ||||
| 				ChatId:     task.ChatId, | ||||
| 				RoleId:     uint(task.RoleId), | ||||
| 				Type:       types.MjMsg, | ||||
| 				Icon:       task.Icon, | ||||
| 				Content:    utils.JsonEncode(data), | ||||
| 				Tokens:     0, | ||||
| 				UseContext: false, | ||||
| 			} | ||||
| 			res = tx.Create(&message) | ||||
| 			if res.Error != nil { | ||||
| 				return res.Error, false | ||||
| 			} | ||||
|  | ||||
| 			// save the job | ||||
| 			job.UserId = task.UserId | ||||
| 			job.Type = task.Type.String() | ||||
| 			job.MessageId = data.MessageId | ||||
| 			job.ReferenceId = data.ReferenceId | ||||
| 			job.Prompt = data.Prompt | ||||
| 			job.ImgURL = imgURL | ||||
| 			job.Progress = data.Progress | ||||
| 			job.Hash = data.Image.Hash | ||||
| 			job.CreatedAt = time.Now() | ||||
| 			res = tx.Create(&job) | ||||
| 			if res.Error != nil { | ||||
| 				tx.Rollback() | ||||
| 				return res.Error, false | ||||
| 			} | ||||
| 			tx.Commit() | ||||
| 		} | ||||
|  | ||||
| 		if wsClient == nil { // 客户端断线,则丢弃 | ||||
| 			logger.Errorf("Client is offline: %+v", data) | ||||
| 			return nil, true | ||||
| 		} | ||||
|  | ||||
| 		if data.Status == Finished { | ||||
| 			utils.ReplyChunkMessage(wsClient, types.WsMessage{Type: types.WsMjImg, Content: data}) | ||||
| 			utils.ReplyChunkMessage(wsClient, types.WsMessage{Type: types.WsEnd}) | ||||
| 			// 本次绘画完毕,移除客户端 | ||||
| 			h.App.MjTaskClients.Delete(task.SessionId) | ||||
| 		} else { | ||||
| 			// 使用代理临时转发图片 | ||||
| 			if data.Image.URL != "" { | ||||
| 				image, err := utils.DownloadImage(data.Image.URL, h.App.Config.ProxyURL) | ||||
| 				if err == nil { | ||||
| 					data.Image.URL = "data:image/png;base64," + base64.StdEncoding.EncodeToString(image) | ||||
| 				} | ||||
| 			} | ||||
| 			utils.ReplyChunkMessage(wsClient, types.WsMessage{Type: types.WsMjImg, Content: data}) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// 更新用户剩余绘图次数 | ||||
| 	// TODO: 放大图片是否需要消耗绘图次数? | ||||
| 	if data.Status == Finished { | ||||
| 		h.db.Model(&model.User{}).Where("id = ?", task.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1)) | ||||
| 	} | ||||
|  | ||||
| 	return nil, true | ||||
| } | ||||
|  | ||||
| func (h *MidJourneyHandler) checkLimits(c *gin.Context) bool { | ||||
| 	user, err := utils.GetLoginUser(c, h.db) | ||||
| 	if err != nil { | ||||
| 		resp.NotAuth(c) | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	if user.ImgCalls <= 0 { | ||||
| 		resp.ERROR(c, "您的绘图次数不足,请联系管理员充值!") | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	return true | ||||
|  | ||||
| } | ||||
|  | ||||
| // Image 创建一个绘画任务 | ||||
| func (h *MidJourneyHandler) Image(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		SessionId string  `json:"session_id"` | ||||
| 		Prompt    string  `json:"prompt"` | ||||
| 		Rate      string  `json:"rate"` | ||||
| 		Model     string  `json:"model"` | ||||
| 		Chaos     int     `json:"chaos"` | ||||
| 		Raw       bool    `json:"raw"` | ||||
| 		Seed      int64   `json:"seed"` | ||||
| 		Stylize   int     `json:"stylize"` | ||||
| 		Img       string  `json:"img"` | ||||
| 		Weight    float32 `json:"weight"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 	if !h.checkLimits(c) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var prompt = data.Prompt | ||||
| 	if data.Rate != "" && !strings.Contains(prompt, "--ar") { | ||||
| 		prompt += " --ar " + data.Rate | ||||
| 	} | ||||
| 	if data.Seed > 0 && !strings.Contains(prompt, "--seed") { | ||||
| 		prompt += fmt.Sprintf(" --seed %d", data.Seed) | ||||
| 	} | ||||
| 	if data.Stylize > 0 && !strings.Contains(prompt, "--s") && !strings.Contains(prompt, "--stylize") { | ||||
| 		prompt += fmt.Sprintf(" --s %d", data.Stylize) | ||||
| 	} | ||||
| 	if data.Chaos > 0 && !strings.Contains(prompt, "--c") && !strings.Contains(prompt, "--chaos") { | ||||
| 		prompt += fmt.Sprintf(" --c %d", data.Chaos) | ||||
| 	} | ||||
| 	if data.Img != "" { | ||||
| 		prompt = fmt.Sprintf("%s %s", data.Img, prompt) | ||||
| 		if data.Weight > 0 { | ||||
| 			prompt += fmt.Sprintf(" --iw %f", data.Weight) | ||||
| 		} | ||||
| 	} | ||||
| 	if data.Raw { | ||||
| 		prompt += " --style raw" | ||||
| 	} | ||||
| 	if data.Model != "" && !strings.Contains(prompt, "--v") && !strings.Contains(prompt, "--niji") { | ||||
| 		prompt += data.Model | ||||
| 	} | ||||
|  | ||||
| 	idValue, _ := c.Get(types.LoginUserID) | ||||
| 	userId := utils.IntValue(utils.InterfaceToString(idValue), 0) | ||||
| 	job := model.MidJourneyJob{ | ||||
| 		Type:      service.Image.String(), | ||||
| 		UserId:    userId, | ||||
| 		Progress:  0, | ||||
| 		Prompt:    prompt, | ||||
| 		CreatedAt: time.Now(), | ||||
| 	} | ||||
| 	if res := h.db.Create(&job); res.Error != nil { | ||||
| 		resp.ERROR(c, "添加任务失败:"+res.Error.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	h.mjService.PushTask(service.MjTask{ | ||||
| 		Id:        int(job.Id), | ||||
| 		SessionId: data.SessionId, | ||||
| 		Src:       service.TaskSrcImg, | ||||
| 		Type:      service.Image, | ||||
| 		Prompt:    prompt, | ||||
| 		UserId:    userId, | ||||
| 	}) | ||||
|  | ||||
| 	var jobVo vo.MidJourneyJob | ||||
| 	err := utils.CopyObject(job, &jobVo) | ||||
| 	if err == nil { | ||||
| 		// 推送任务到前端 | ||||
| 		client := h.clients.Get(data.SessionId) | ||||
| 		if client != nil { | ||||
| 			utils.ReplyChunkMessage(client, jobVo) | ||||
| 		} | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| type reqVo struct { | ||||
| 	Src         string `json:"src"` | ||||
| 	Index       int32  `json:"index"` | ||||
| 	MessageId   string `json:"message_id"` | ||||
| 	MessageHash string `json:"message_hash"` | ||||
| 	SessionId   string `json:"session_id"` | ||||
| 	Prompt      string `json:"prompt"` | ||||
| 	ChatId      string `json:"chat_id"` | ||||
| 	RoleId      int    `json:"role_id"` | ||||
| 	Icon        string `json:"icon"` | ||||
| } | ||||
|  | ||||
| // Upscale send upscale command to MidJourney Bot | ||||
| func (h *MidJourneyHandler) Upscale(c *gin.Context) { | ||||
| 	var data reqVo | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil || data.SessionId == "" { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if !h.checkLimits(c) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	idValue, _ := c.Get(types.LoginUserID) | ||||
| 	jobId := 0 | ||||
| 	userId := utils.IntValue(utils.InterfaceToString(idValue), 0) | ||||
| 	src := service.TaskSrc(data.Src) | ||||
| 	if src == service.TaskSrcImg { | ||||
| 		job := model.MidJourneyJob{ | ||||
| 			Type:      service.Upscale.String(), | ||||
| 			UserId:    userId, | ||||
| 			Hash:      data.MessageHash, | ||||
| 			Progress:  0, | ||||
| 			Prompt:    data.Prompt, | ||||
| 			CreatedAt: time.Now(), | ||||
| 		} | ||||
| 		if res := h.db.Create(&job); res.Error == nil { | ||||
| 			jobId = int(job.Id) | ||||
| 		} else { | ||||
| 			resp.ERROR(c, "添加任务失败:"+res.Error.Error()) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		var jobVo vo.MidJourneyJob | ||||
| 		err := utils.CopyObject(job, &jobVo) | ||||
| 		if err == nil { | ||||
| 			// 推送任务到前端 | ||||
| 			client := h.clients.Get(data.SessionId) | ||||
| 			if client != nil { | ||||
| 				utils.ReplyChunkMessage(client, jobVo) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	h.mjService.PushTask(service.MjTask{ | ||||
| 		Id:          jobId, | ||||
| 		SessionId:   data.SessionId, | ||||
| 		Src:         src, | ||||
| 		Type:        service.Upscale, | ||||
| 		Prompt:      data.Prompt, | ||||
| 		UserId:      userId, | ||||
| 		RoleId:      data.RoleId, | ||||
| 		Icon:        data.Icon, | ||||
| 		ChatId:      data.ChatId, | ||||
| 		Index:       data.Index, | ||||
| 		MessageId:   data.MessageId, | ||||
| 		MessageHash: data.MessageHash, | ||||
| 	}) | ||||
|  | ||||
| 	wsClient := h.App.ChatClients.Get(data.SessionId) | ||||
| 	if wsClient != nil { | ||||
| 		content := fmt.Sprintf("**%s** 已推送 upscale 任务到 MidJourney 机器人,请耐心等待任务执行...", data.Prompt) | ||||
| 		utils.ReplyMessage(wsClient, content) | ||||
| 		if h.App.MjTaskClients.Get(data.SessionId) == nil { | ||||
| 			h.App.MjTaskClients.Put(data.SessionId, wsClient) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| // Variation send variation command to MidJourney Bot | ||||
| func (h *MidJourneyHandler) Variation(c *gin.Context) { | ||||
| 	var data reqVo | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil || data.SessionId == "" { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if !h.checkLimits(c) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	idValue, _ := c.Get(types.LoginUserID) | ||||
| 	jobId := 0 | ||||
| 	userId := utils.IntValue(utils.InterfaceToString(idValue), 0) | ||||
| 	src := service.TaskSrc(data.Src) | ||||
| 	if src == service.TaskSrcImg { | ||||
| 		job := model.MidJourneyJob{ | ||||
| 			Type:      service.Variation.String(), | ||||
| 			UserId:    userId, | ||||
| 			ImgURL:    "", | ||||
| 			Hash:      data.MessageHash, | ||||
| 			Progress:  0, | ||||
| 			Prompt:    data.Prompt, | ||||
| 			CreatedAt: time.Now(), | ||||
| 		} | ||||
| 		if res := h.db.Create(&job); res.Error == nil { | ||||
| 			jobId = int(job.Id) | ||||
| 		} else { | ||||
| 			resp.ERROR(c, "添加任务失败:"+res.Error.Error()) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		var jobVo vo.MidJourneyJob | ||||
| 		err := utils.CopyObject(job, &jobVo) | ||||
| 		if err == nil { | ||||
| 			// 推送任务到前端 | ||||
| 			client := h.clients.Get(data.SessionId) | ||||
| 			if client != nil { | ||||
| 				utils.ReplyChunkMessage(client, jobVo) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	h.mjService.PushTask(service.MjTask{ | ||||
| 		Id:          jobId, | ||||
| 		SessionId:   data.SessionId, | ||||
| 		Src:         src, | ||||
| 		Type:        service.Variation, | ||||
| 		Prompt:      data.Prompt, | ||||
| 		UserId:      userId, | ||||
| 		RoleId:      data.RoleId, | ||||
| 		Icon:        data.Icon, | ||||
| 		ChatId:      data.ChatId, | ||||
| 		Index:       data.Index, | ||||
| 		MessageId:   data.MessageId, | ||||
| 		MessageHash: data.MessageHash, | ||||
| 	}) | ||||
|  | ||||
| 	// 从聊天窗口发送的请求,记录客户端信息 | ||||
| 	wsClient := h.App.ChatClients.Get(data.SessionId) | ||||
| 	if wsClient != nil { | ||||
| 		content := fmt.Sprintf("**%s** 已推送 variation 任务到 MidJourney 机器人,请耐心等待任务执行...", data.Prompt) | ||||
| 		utils.ReplyMessage(wsClient, content) | ||||
| 		if h.App.MjTaskClients.Get(data.SessionId) == nil { | ||||
| 			h.App.MjTaskClients.Put(data.SessionId, wsClient) | ||||
| 		} | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| // JobList 获取 MJ 任务列表 | ||||
| func (h *MidJourneyHandler) JobList(c *gin.Context) { | ||||
| 	status := h.GetInt(c, "status", 0) | ||||
| 	var items []model.MidJourneyJob | ||||
| 	var res *gorm.DB | ||||
| 	userId, _ := c.Get(types.LoginUserID) | ||||
| 	if status == 1 { | ||||
| 		res = h.db.Where("user_id = ? AND progress = 100", userId).Order("id DESC").Find(&items) | ||||
| 	} else { | ||||
| 		res = h.db.Where("user_id = ? AND progress < 100", userId).Order("id ASC").Find(&items) | ||||
| 	} | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, types.NoData) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var jobs = make([]vo.MidJourneyJob, 0) | ||||
| 	for _, item := range items { | ||||
| 		var job vo.MidJourneyJob | ||||
| 		err := utils.CopyObject(item, &job) | ||||
| 		if err != nil { | ||||
| 			continue | ||||
| 		} | ||||
| 		if item.Progress < 100 && item.ImgURL != "" { // 正在运行中任务使用代理访问图片 | ||||
| 			image, err := utils.DownloadImage(item.ImgURL, h.App.Config.ProxyURL) | ||||
| 			if err == nil { | ||||
| 				job.ImgURL = "data:image/png;base64," + base64.StdEncoding.EncodeToString(image) | ||||
| 			} | ||||
| 		} | ||||
| 		jobs = append(jobs, job) | ||||
| 	} | ||||
| 	resp.SUCCESS(c, jobs) | ||||
| } | ||||
							
								
								
									
										295
									
								
								api/handler/openai_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										295
									
								
								api/handler/openai_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,295 @@ | ||||
| package handler | ||||
|  | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/store/vo" | ||||
| 	"chatplus/utils" | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"gorm.io/gorm" | ||||
| 	"io" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 	"unicode/utf8" | ||||
| ) | ||||
|  | ||||
| // 将消息发送给 OpenAI API 并获取结果,通过 WebSocket 推送到客户端 | ||||
| func (h *ChatHandler) sendOpenAiMessage( | ||||
| 	chatCtx []interface{}, | ||||
| 	req types.ApiRequest, | ||||
| 	userVo vo.User, | ||||
| 	ctx context.Context, | ||||
| 	session *types.ChatSession, | ||||
| 	role model.ChatRole, | ||||
| 	prompt string, | ||||
| 	ws *types.WsClient) error { | ||||
| 	promptCreatedAt := time.Now() // 记录提问时间 | ||||
| 	start := time.Now() | ||||
| 	var apiKey = userVo.ChatConfig.ApiKeys[session.Model.Platform] | ||||
| 	response, err := h.doRequest(ctx, req, session.Model.Platform, &apiKey) | ||||
| 	logger.Info("HTTP请求完成,耗时:", time.Now().Sub(start)) | ||||
| 	if err != nil { | ||||
| 		if strings.Contains(err.Error(), "context canceled") { | ||||
| 			logger.Info("用户取消了请求:", prompt) | ||||
| 			return nil | ||||
| 		} else if strings.Contains(err.Error(), "no available key") { | ||||
| 			utils.ReplyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!") | ||||
| 			return nil | ||||
| 		} else { | ||||
| 			logger.Error(err) | ||||
| 		} | ||||
|  | ||||
| 		utils.ReplyMessage(ws, ErrorMsg) | ||||
| 		utils.ReplyMessage(ws, "") | ||||
| 		return err | ||||
| 	} else { | ||||
| 		defer response.Body.Close() | ||||
| 	} | ||||
|  | ||||
| 	contentType := response.Header.Get("Content-Type") | ||||
| 	if strings.Contains(contentType, "text/event-stream") { | ||||
| 		replyCreatedAt := time.Now() // 记录回复时间 | ||||
| 		// 循环读取 Chunk 消息 | ||||
| 		var message = types.Message{} | ||||
| 		var contents = make([]string, 0) | ||||
| 		var functionCall = false | ||||
| 		var functionName string | ||||
| 		var arguments = make([]string, 0) | ||||
| 		scanner := bufio.NewScanner(response.Body) | ||||
| 		for scanner.Scan() { | ||||
| 			line := scanner.Text() | ||||
| 			if !strings.Contains(line, "data:") || len(line) < 30 { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			var responseBody = types.ApiResponse{} | ||||
| 			err = json.Unmarshal([]byte(line[6:]), &responseBody) | ||||
| 			if err != nil || len(responseBody.Choices) == 0 { // 数据解析出错 | ||||
| 				logger.Error(err, line) | ||||
| 				utils.ReplyMessage(ws, ErrorMsg) | ||||
| 				utils.ReplyMessage(ws, "") | ||||
| 				break | ||||
| 			} | ||||
|  | ||||
| 			fun := responseBody.Choices[0].Delta.FunctionCall | ||||
| 			if functionCall && fun.Name == "" { | ||||
| 				arguments = append(arguments, fun.Arguments) | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			if !utils.IsEmptyValue(fun) { | ||||
| 				functionName = fun.Name | ||||
| 				f := h.App.Functions[functionName] | ||||
| 				if f != nil { | ||||
| 					functionCall = true | ||||
| 					utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart}) | ||||
| 					utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: fmt.Sprintf("正在调用函数 `%s` 作答 ...\n\n", f.Name())}) | ||||
| 				} | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			if responseBody.Choices[0].FinishReason == "function_call" { // 函数调用完毕 | ||||
| 				break | ||||
| 			} | ||||
|  | ||||
| 			// 初始化 role | ||||
| 			if responseBody.Choices[0].Delta.Role != "" && message.Role == "" { | ||||
| 				message.Role = responseBody.Choices[0].Delta.Role | ||||
| 				utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart}) | ||||
| 				continue | ||||
| 			} else if responseBody.Choices[0].FinishReason != "" { | ||||
| 				break // 输出完成或者输出中断了 | ||||
| 			} else { | ||||
| 				content := responseBody.Choices[0].Delta.Content | ||||
| 				contents = append(contents, utils.InterfaceToString(content)) | ||||
| 				utils.ReplyChunkMessage(ws, types.WsMessage{ | ||||
| 					Type:    types.WsMiddle, | ||||
| 					Content: utils.InterfaceToString(responseBody.Choices[0].Delta.Content), | ||||
| 				}) | ||||
| 			} | ||||
| 		} // end for | ||||
|  | ||||
| 		if err := scanner.Err(); err != nil { | ||||
| 			if strings.Contains(err.Error(), "context canceled") { | ||||
| 				logger.Info("用户取消了请求:", prompt) | ||||
| 			} else { | ||||
| 				logger.Error("信息读取出错:", err) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if functionCall { // 调用函数完成任务 | ||||
| 			var params map[string]interface{} | ||||
| 			_ = utils.JsonDecode(strings.Join(arguments, ""), ¶ms) | ||||
| 			logger.Debugf("函数名称: %s, 函数参数:%s", functionName, params) | ||||
|  | ||||
| 			// for creating image, check if the user's img_calls > 0 | ||||
| 			if functionName == types.FuncMidJourney && userVo.ImgCalls <= 0 { | ||||
| 				utils.ReplyMessage(ws, "**当前用户剩余绘图次数已用尽,请扫描下面二维码联系管理员!**") | ||||
| 				utils.ReplyMessage(ws, "") | ||||
| 			} else { | ||||
| 				f := h.App.Functions[functionName] | ||||
| 				if functionName == types.FuncMidJourney { | ||||
| 					params["user_id"] = userVo.Id | ||||
| 					params["role_id"] = role.Id | ||||
| 					params["chat_id"] = session.ChatId | ||||
| 					params["icon"] = "/images/avatar/mid_journey.png" | ||||
| 					params["session_id"] = session.SessionId | ||||
| 				} | ||||
| 				data, err := f.Invoke(params) | ||||
| 				if err != nil { | ||||
| 					msg := "调用函数出错:" + err.Error() | ||||
| 					utils.ReplyChunkMessage(ws, types.WsMessage{ | ||||
| 						Type:    types.WsMiddle, | ||||
| 						Content: msg, | ||||
| 					}) | ||||
| 					contents = append(contents, msg) | ||||
| 				} else { | ||||
| 					content := data | ||||
| 					if functionName == types.FuncMidJourney { | ||||
| 						content = fmt.Sprintf("绘画提示词:%s 已推送任务到 MidJourney 机器人,请耐心等待任务执行...", data) | ||||
| 						h.App.MjTaskClients.Put(session.SessionId, ws) | ||||
| 						// update user's img_calls | ||||
| 						h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1)) | ||||
| 					} | ||||
|  | ||||
| 					utils.ReplyChunkMessage(ws, types.WsMessage{ | ||||
| 						Type:    types.WsMiddle, | ||||
| 						Content: content, | ||||
| 					}) | ||||
| 					contents = append(contents, content) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// 消息发送成功 | ||||
| 		if len(contents) > 0 { | ||||
| 			// 更新用户的对话次数 | ||||
| 			if userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" { | ||||
| 				h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("calls", gorm.Expr("calls - ?", 1)) | ||||
| 			} | ||||
|  | ||||
| 			if message.Role == "" { | ||||
| 				message.Role = "assistant" | ||||
| 			} | ||||
| 			message.Content = strings.Join(contents, "") | ||||
| 			useMsg := types.Message{Role: "user", Content: prompt} | ||||
|  | ||||
| 			// 更新上下文消息,如果是调用函数则不需要更新上下文 | ||||
| 			if h.App.ChatConfig.EnableContext && functionCall == false { | ||||
| 				chatCtx = append(chatCtx, useMsg)  // 提问消息 | ||||
| 				chatCtx = append(chatCtx, message) // 回复消息 | ||||
| 				h.App.ChatContexts.Put(session.ChatId, chatCtx) | ||||
| 			} | ||||
|  | ||||
| 			// 追加聊天记录 | ||||
| 			if h.App.ChatConfig.EnableHistory { | ||||
| 				useContext := true | ||||
| 				if functionCall { | ||||
| 					useContext = false | ||||
| 				} | ||||
|  | ||||
| 				// for prompt | ||||
| 				promptToken, err := utils.CalcTokens(prompt, req.Model) | ||||
| 				if err != nil { | ||||
| 					logger.Error(err) | ||||
| 				} | ||||
| 				historyUserMsg := model.HistoryMessage{ | ||||
| 					UserId:     userVo.Id, | ||||
| 					ChatId:     session.ChatId, | ||||
| 					RoleId:     role.Id, | ||||
| 					Type:       types.PromptMsg, | ||||
| 					Icon:       userVo.Avatar, | ||||
| 					Content:    prompt, | ||||
| 					Tokens:     promptToken, | ||||
| 					UseContext: useContext, | ||||
| 				} | ||||
| 				historyUserMsg.CreatedAt = promptCreatedAt | ||||
| 				historyUserMsg.UpdatedAt = promptCreatedAt | ||||
| 				res := h.db.Save(&historyUserMsg) | ||||
| 				if res.Error != nil { | ||||
| 					logger.Error("failed to save prompt history message: ", res.Error) | ||||
| 				} | ||||
|  | ||||
| 				// 计算本次对话消耗的总 token 数量 | ||||
| 				var totalTokens = 0 | ||||
| 				if functionCall { // prompt + 函数名 + 参数 token | ||||
| 					tokens, _ := utils.CalcTokens(functionName, req.Model) | ||||
| 					totalTokens += tokens | ||||
| 					tokens, _ = utils.CalcTokens(utils.InterfaceToString(arguments), req.Model) | ||||
| 					totalTokens += tokens | ||||
| 				} else { | ||||
| 					totalTokens, _ = utils.CalcTokens(message.Content, req.Model) | ||||
| 				} | ||||
| 				totalTokens += getTotalTokens(req) | ||||
|  | ||||
| 				historyReplyMsg := model.HistoryMessage{ | ||||
| 					UserId:     userVo.Id, | ||||
| 					ChatId:     session.ChatId, | ||||
| 					RoleId:     role.Id, | ||||
| 					Type:       types.ReplyMsg, | ||||
| 					Icon:       role.Icon, | ||||
| 					Content:    message.Content, | ||||
| 					Tokens:     totalTokens, | ||||
| 					UseContext: useContext, | ||||
| 				} | ||||
| 				historyReplyMsg.CreatedAt = replyCreatedAt | ||||
| 				historyReplyMsg.UpdatedAt = replyCreatedAt | ||||
| 				res = h.db.Create(&historyReplyMsg) | ||||
| 				if res.Error != nil { | ||||
| 					logger.Error("failed to save reply history message: ", res.Error) | ||||
| 				} | ||||
|  | ||||
| 				// 更新用户信息 | ||||
| 				h.db.Model(&model.User{}).Where("id = ?", userVo.Id). | ||||
| 					UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", totalTokens)) | ||||
| 			} | ||||
|  | ||||
| 			// 保存当前会话 | ||||
| 			var chatItem model.ChatItem | ||||
| 			res := h.db.Where("chat_id = ?", session.ChatId).First(&chatItem) | ||||
| 			if res.Error != nil { | ||||
| 				chatItem.ChatId = session.ChatId | ||||
| 				chatItem.UserId = session.UserId | ||||
| 				chatItem.RoleId = role.Id | ||||
| 				chatItem.ModelId = session.Model.Id | ||||
| 				if utf8.RuneCountInString(prompt) > 30 { | ||||
| 					chatItem.Title = string([]rune(prompt)[:30]) + "..." | ||||
| 				} else { | ||||
| 					chatItem.Title = prompt | ||||
| 				} | ||||
| 				h.db.Create(&chatItem) | ||||
| 			} | ||||
| 		} | ||||
| 	} else { | ||||
| 		body, err := io.ReadAll(response.Body) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("error with reading response: %v", err) | ||||
| 		} | ||||
| 		var res types.ApiError | ||||
| 		err = json.Unmarshal(body, &res) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("error with decode response: %v", err) | ||||
| 		} | ||||
|  | ||||
| 		// OpenAI API 调用异常处理 | ||||
| 		if strings.Contains(res.Error.Message, "This key is associated with a deactivated account") { | ||||
| 			utils.ReplyMessage(ws, "请求 OpenAI API 失败:API KEY 所关联的账户被禁用。") | ||||
| 			// 移除当前 API key | ||||
| 			h.db.Where("value = ?", apiKey).Delete(&model.ApiKey{}) | ||||
| 		} else if strings.Contains(res.Error.Message, "You exceeded your current quota") { | ||||
| 			utils.ReplyMessage(ws, "请求 OpenAI API 失败:API KEY 触发并发限制,请稍后再试。") | ||||
| 		} else if strings.Contains(res.Error.Message, "This model's maximum context length") { | ||||
| 			logger.Error(res.Error.Message) | ||||
| 			utils.ReplyMessage(ws, "当前会话上下文长度超出限制,已为您清空会话上下文!") | ||||
| 			h.App.ChatContexts.Delete(session.ChatId) | ||||
| 			return h.sendMessage(ctx, session, role, prompt, ws) | ||||
| 		} else { | ||||
| 			utils.ReplyMessage(ws, "请求 OpenAI API 失败:"+res.Error.Message) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										121
									
								
								api/handler/reward_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								api/handler/reward_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,121 @@ | ||||
| package handler | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core" | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/utils" | ||||
| 	"chatplus/utils/resp" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| type RewardHandler struct { | ||||
| 	BaseHandler | ||||
| 	db *gorm.DB | ||||
| } | ||||
|  | ||||
| func NewRewardHandler(server *core.AppServer, db *gorm.DB) *RewardHandler { | ||||
| 	h := RewardHandler{db: db} | ||||
| 	h.App = server | ||||
| 	return &h | ||||
| } | ||||
|  | ||||
| func (h *RewardHandler) Notify(c *gin.Context) { | ||||
| 	token := c.GetHeader("Authorization") | ||||
| 	if token != h.App.Config.ExtConfig.Token { | ||||
| 		resp.NotAuth(c) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var data struct { | ||||
| 		TransId string  `json:"trans_id"` // 微信转账交易 ID | ||||
| 		Amount  float64 `json:"amount"`   // 微信转账交易金额 | ||||
| 		Remark  string  `json:"remark"`   // 转账备注 | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if data.Amount <= 0 { | ||||
| 		resp.ERROR(c, "Amount should not be 0") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	logger.Infof("收到众筹收款信息: %+v", data) | ||||
| 	var item model.Reward | ||||
| 	res := h.db.Where("tx_id = ?", data.TransId).First(&item) | ||||
| 	if res.Error == nil { | ||||
| 		resp.ERROR(c, "当前交易 ID 己经存在!") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	res = h.db.Create(&model.Reward{ | ||||
| 		TxId:   data.TransId, | ||||
| 		Amount: data.Amount, | ||||
| 		Remark: data.Remark, | ||||
| 		Status: false, | ||||
| 	}) | ||||
| 	if res.Error != nil { | ||||
| 		logger.Errorf("交易保存失败: %v", res.Error) | ||||
| 		resp.ERROR(c, "交易保存失败") | ||||
| 		return | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| // Verify 打赏码核销 | ||||
| func (h *RewardHandler) Verify(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		TxId string `json:"tx_id"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// 移除转账单号中间的空格,防止有人复制的时候多复制了空格 | ||||
| 	data.TxId = strings.ReplaceAll(data.TxId, " ", "") | ||||
|  | ||||
| 	var item model.Reward | ||||
| 	res := h.db.Where("tx_id = ?", data.TxId).First(&item) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "无效的众筹交易流水号!") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if item.Status { | ||||
| 		resp.ERROR(c, "当前众筹交易流水号已经被核销,请不要重复核销!") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	user, err := utils.GetLoginUser(c, h.db) | ||||
| 	if err != nil { | ||||
| 		resp.HACKER(c) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	tx := h.db.Begin() | ||||
| 	calls := (item.Amount + 0.1) * 10 | ||||
| 	res = h.db.Model(&user).UpdateColumn("calls", gorm.Expr("calls + ?", calls)) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "更新数据库失败!") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// 更新核销状态 | ||||
| 	item.Status = true | ||||
| 	item.UserId = user.Id | ||||
| 	res = h.db.Updates(&item) | ||||
| 	if res.Error != nil { | ||||
| 		tx.Rollback() | ||||
| 		resp.ERROR(c, "更新数据库失败!") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	tx.Commit() | ||||
| 	resp.SUCCESS(c) | ||||
|  | ||||
| } | ||||
							
								
								
									
										70
									
								
								api/handler/sms_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								api/handler/sms_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | ||||
| package handler | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core" | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/service" | ||||
| 	"chatplus/store" | ||||
| 	"chatplus/utils" | ||||
| 	"chatplus/utils/resp" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| ) | ||||
|  | ||||
| const CodeStorePrefix = "/verify/codes/" | ||||
|  | ||||
| type SmsHandler struct { | ||||
| 	BaseHandler | ||||
| 	leveldb *store.LevelDB | ||||
| 	sms     *service.AliYunSmsService | ||||
| 	captcha *service.CaptchaService | ||||
| } | ||||
|  | ||||
| func NewSmsHandler(app *core.AppServer, db *store.LevelDB, sms *service.AliYunSmsService, captcha *service.CaptchaService) *SmsHandler { | ||||
| 	handler := &SmsHandler{leveldb: db, sms: sms, captcha: captcha} | ||||
| 	handler.App = app | ||||
| 	return handler | ||||
| } | ||||
|  | ||||
| // SendCode 发送验证码短信 | ||||
| func (h *SmsHandler) SendCode(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Mobile string `json:"mobile"` | ||||
| 		Key    string `json:"key"` | ||||
| 		Dots   string `json:"dots"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if !h.captcha.Check(data) { | ||||
| 		resp.ERROR(c, "验证码错误,请先完人机验证") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	code := utils.RandomNumber(6) | ||||
| 	err := h.sms.SendVerifyCode(data.Mobile, code) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// 存储验证码,等待后面注册验证 | ||||
| 	err = h.leveldb.Put(CodeStorePrefix+data.Mobile, code) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, "验证码保存失败") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| type statusVo struct { | ||||
| 	EnabledMsgService bool `json:"enabled_msg_service"` | ||||
| 	EnabledRegister   bool `json:"enabled_register"` | ||||
| } | ||||
|  | ||||
| // Status check if the message service is enabled | ||||
| func (h *SmsHandler) Status(c *gin.Context) { | ||||
| 	resp.SUCCESS(c, statusVo{EnabledMsgService: h.App.SysConfig.EnabledMsgService, EnabledRegister: h.App.SysConfig.EnabledRegister}) | ||||
| } | ||||
							
								
								
									
										31
									
								
								api/handler/upload_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								api/handler/upload_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| package handler | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core" | ||||
| 	"chatplus/service/oss" | ||||
| 	"chatplus/utils/resp" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| type UploadHandler struct { | ||||
| 	BaseHandler | ||||
| 	db              *gorm.DB | ||||
| 	uploaderManager *oss.UploaderManager | ||||
| } | ||||
|  | ||||
| func NewUploadHandler(app *core.AppServer, db *gorm.DB, manager *oss.UploaderManager) *UploadHandler { | ||||
| 	handler := &UploadHandler{db: db, uploaderManager: manager} | ||||
| 	handler.App = app | ||||
| 	return handler | ||||
| } | ||||
|  | ||||
| func (h *UploadHandler) Upload(c *gin.Context) { | ||||
| 	fileURL, err := h.uploaderManager.GetUploadHandler().PutFile(c, "file") | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	resp.SUCCESS(c, fileURL) | ||||
| } | ||||
| @@ -3,15 +3,17 @@ package handler | ||||
| import ( | ||||
| 	"chatplus/core" | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/store" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/store/vo" | ||||
| 	"chatplus/utils" | ||||
| 	"chatplus/utils/resp" | ||||
| 	"fmt" | ||||
| 	"github.com/go-redis/redis/v8" | ||||
| 	"github.com/golang-jwt/jwt/v5" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/gin-contrib/sessions" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/lionsoul2014/ip2region/binding/golang/xdb" | ||||
| 	"gorm.io/gorm" | ||||
| @@ -21,10 +23,17 @@ type UserHandler struct { | ||||
| 	BaseHandler | ||||
| 	db       *gorm.DB | ||||
| 	searcher *xdb.Searcher | ||||
| 	leveldb  *store.LevelDB | ||||
| 	redis    *redis.Client | ||||
| } | ||||
| 
 | ||||
| func NewUserHandler(app *core.AppServer, db *gorm.DB, searcher *xdb.Searcher) *UserHandler { | ||||
| 	handler := &UserHandler{db: db, searcher: searcher} | ||||
| func NewUserHandler( | ||||
| 	app *core.AppServer, | ||||
| 	db *gorm.DB, | ||||
| 	searcher *xdb.Searcher, | ||||
| 	levelDB *store.LevelDB, | ||||
| 	client *redis.Client) *UserHandler { | ||||
| 	handler := &UserHandler{db: db, searcher: searcher, leveldb: levelDB, redis: client} | ||||
| 	handler.App = app | ||||
| 	return handler | ||||
| } | ||||
| @@ -33,18 +42,18 @@ func NewUserHandler(app *core.AppServer, db *gorm.DB, searcher *xdb.Searcher) *U | ||||
| func (h *UserHandler) Register(c *gin.Context) { | ||||
| 	// parameters process | ||||
| 	var data struct { | ||||
| 		Username string `json:"username"` | ||||
| 		Mobile   string `json:"mobile"` | ||||
| 		Password string `json:"password"` | ||||
| 		Code     int    `json:"code"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 	data.Username = strings.TrimSpace(data.Username) | ||||
| 	data.Password = strings.TrimSpace(data.Password) | ||||
| 
 | ||||
| 	if len(data.Username) < 5 { | ||||
| 		resp.ERROR(c, "用户名长度不能少于5个字符") | ||||
| 	if len(data.Mobile) < 10 { | ||||
| 		resp.ERROR(c, "请输入合法的手机号") | ||||
| 		return | ||||
| 	} | ||||
| 	if len(data.Password) < 8 { | ||||
| @@ -52,13 +61,25 @@ func (h *UserHandler) Register(c *gin.Context) { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// 检查验证码 | ||||
| 	key := CodeStorePrefix + data.Mobile | ||||
| 	if h.App.SysConfig.EnabledMsgService { | ||||
| 		var code int | ||||
| 		err := h.leveldb.Get(key, &code) | ||||
| 		if err != nil || code != data.Code { | ||||
| 			resp.ERROR(c, "短信验证码错误") | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// check if the username is exists | ||||
| 	var item model.User | ||||
| 	tx := h.db.Where("username = ?", data.Username).First(&item) | ||||
| 	if tx.RowsAffected > 0 { | ||||
| 		resp.ERROR(c, "用户名已存在") | ||||
| 	res := h.db.Where("mobile = ?", data.Mobile).First(&item) | ||||
| 	if res.RowsAffected > 0 { | ||||
| 		resp.ERROR(c, "该手机号码已经被注册,请更换其他手机号") | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// 默认订阅所有角色 | ||||
| 	var chatRoles []model.ChatRole | ||||
| 	h.db.Find(&chatRoles) | ||||
| @@ -69,54 +90,47 @@ func (h *UserHandler) Register(c *gin.Context) { | ||||
| 
 | ||||
| 	salt := utils.RandString(8) | ||||
| 	user := model.User{ | ||||
| 		Username:  data.Username, | ||||
| 		Password:  utils.GenPassword(data.Password, salt), | ||||
| 		Nickname:  fmt.Sprintf("极客学长@%d", utils.RandomNumber(5)), | ||||
| 		Avatar:    "images/avatar/user.png", | ||||
| 		Avatar:    "/images/avatar/user.png", | ||||
| 		Salt:      salt, | ||||
| 		Status:    true, | ||||
| 		Mobile:    data.Mobile, | ||||
| 		ChatRoles: utils.JsonEncode(roleKeys), | ||||
| 		ChatConfig: utils.JsonEncode(types.ChatConfig{ | ||||
| 			Temperature:   h.App.ChatConfig.Temperature, | ||||
| 			MaxTokens:     h.App.ChatConfig.MaxTokens, | ||||
| 			EnableContext: h.App.ChatConfig.EnableContext, | ||||
| 			EnableHistory: true, | ||||
| 			Model:         h.App.ChatConfig.Model, | ||||
| 			ApiKey:        "", | ||||
| 		ChatConfig: utils.JsonEncode(types.UserChatConfig{ | ||||
| 			ApiKeys: map[types.Platform]string{ | ||||
| 				types.OpenAI:  "", | ||||
| 				types.Azure:   "", | ||||
| 				types.ChatGLM: "", | ||||
| 			}, | ||||
| 		}), | ||||
| 		Calls:    h.App.SysConfig.UserInitCalls, | ||||
| 		ImgCalls: h.App.SysConfig.InitImgCalls, | ||||
| 	} | ||||
| 	// 初始化调用次数 | ||||
| 	var cfg model.Config | ||||
| 	h.db.Where("marker = ?", "system").First(&cfg) | ||||
| 	var config types.SystemConfig | ||||
| 	err := utils.JsonDecode(cfg.Config, &config) | ||||
| 	if err != nil || config.UserInitCalls <= 0 { | ||||
| 		user.Calls = types.UserInitCalls | ||||
| 	} else { | ||||
| 		user.Calls = config.UserInitCalls | ||||
| 	} | ||||
| 	res := h.db.Create(&user) | ||||
| 	res = h.db.Create(&user) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "保存数据失败") | ||||
| 		logger.Error(res.Error) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if h.App.SysConfig.EnabledMsgService { | ||||
| 		_ = h.leveldb.Delete(key) // 注册成功,删除短信验证码 | ||||
| 	} | ||||
| 	resp.SUCCESS(c, user) | ||||
| } | ||||
| 
 | ||||
| // Login 用户登录 | ||||
| func (h *UserHandler) Login(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Username string | ||||
| 		Password string | ||||
| 		Mobile   string `json:"username"` | ||||
| 		Password string `json:"password"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 	var user model.User | ||||
| 	res := h.db.Where("username = ?", data.Username).First(&user) | ||||
| 	res := h.db.Where("mobile = ?", data.Mobile).First(&user) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "用户名不存在") | ||||
| 		return | ||||
| @@ -128,83 +142,48 @@ func (h *UserHandler) Login(c *gin.Context) { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if user.Status == false { | ||||
| 		resp.ERROR(c, "该用户已被禁止登录,请联系管理员") | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// 更新最后登录时间和IP | ||||
| 	user.LastLoginIp = c.ClientIP() | ||||
| 	user.LastLoginAt = time.Now().Unix() | ||||
| 	h.db.Model(&user).Updates(user) | ||||
| 
 | ||||
| 	sessionId := utils.RandString(42) | ||||
| 	err := utils.SetLoginUser(c, user) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, "保存会话失败") | ||||
| 		logger.Error("Error for save session: ", err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// 记录登录信息在服务端 | ||||
| 	h.App.ChatSession.Put(sessionId, types.ChatSession{ClientIP: c.ClientIP(), UserId: user.Id, Username: data.Username, SessionId: sessionId}) | ||||
| 
 | ||||
| 	// 加载用户订阅的聊天角色 | ||||
| 	var roleKeys []string | ||||
| 	err = utils.JsonDecode(user.ChatRoles, &roleKeys) | ||||
| 	var chatRoles interface{} | ||||
| 	if err == nil { | ||||
| 		var roles []model.ChatRole | ||||
| 		res = h.db.Where("marker IN ?", roleKeys).Find(&roles) | ||||
| 		if res.Error == err { | ||||
| 			type Item struct { | ||||
| 				Name string | ||||
| 				Key  string | ||||
| 				Icon string | ||||
| 			} | ||||
| 			items := make([]Item, 0) | ||||
| 			for _, r := range roles { | ||||
| 				items = append(items, Item{Name: r.Name, Key: r.Key, Icon: r.Icon}) | ||||
| 			} | ||||
| 			chatRoles = items | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	h.db.Create(&model.UserLoginLog{ | ||||
| 		UserId:       user.Id, | ||||
| 		Username:     user.Username, | ||||
| 		Username:     user.Mobile, | ||||
| 		LoginIp:      c.ClientIP(), | ||||
| 		LoginAddress: utils.Ip2Region(h.searcher, c.ClientIP()), | ||||
| 	}) | ||||
| 	var chatConfig types.ChatConfig | ||||
| 	err = utils.JsonDecode(user.ChatConfig, &chatConfig) | ||||
| 
 | ||||
| 	// 创建 token | ||||
| 	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ | ||||
| 		"user_id": user.Id, | ||||
| 		"expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)).Unix(), | ||||
| 	}) | ||||
| 	tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey)) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		resp.ERROR(c, "Failed to generate token, "+err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	resp.SUCCESS(c, gin.H{ | ||||
| 		"session_id":     sessionId, | ||||
| 		"id":             user.Id, | ||||
| 		"nickname":       user.Nickname, | ||||
| 		"avatar":         user.Avatar, | ||||
| 		"username":       user.Username, | ||||
| 		"tokens":         user.Tokens, | ||||
| 		"calls":          user.Calls, | ||||
| 		"expiredTime":    user.ExpiredTime, | ||||
| 		"chatRoles":      chatRoles, | ||||
| 		"api_key":        chatConfig.ApiKey, | ||||
| 		"model":          chatConfig.Model, | ||||
| 		"temperature":    chatConfig.Temperature, | ||||
| 		"max_tokens":     chatConfig.MaxTokens, | ||||
| 		"enable_context": chatConfig.EnableContext, | ||||
| 		"enable_history": chatConfig.EnableHistory, | ||||
| 	}) | ||||
| 	// 保存到 redis | ||||
| 	key := fmt.Sprintf("users/%d", user.Id) | ||||
| 	if _, err := h.redis.Set(c, key, tokenString, 0).Result(); err != nil { | ||||
| 		resp.ERROR(c, "error with save token: "+err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	resp.SUCCESS(c, tokenString) | ||||
| } | ||||
| 
 | ||||
| // Logout 注 销 | ||||
| func (h *UserHandler) Logout(c *gin.Context) { | ||||
| 	sessionId := c.GetHeader(types.SessionName) | ||||
| 	session := sessions.Default(c) | ||||
| 	session.Delete(types.SessionUser) | ||||
| 	err := session.Save() | ||||
| 	if err != nil { | ||||
| 		logger.Error("Error for save session: ", err) | ||||
| 	sessionId := c.GetHeader(types.ChatTokenHeader) | ||||
| 	key := h.GetUserKey(c) | ||||
| 	if _, err := h.redis.Del(c, key).Result(); err != nil { | ||||
| 		logger.Error("error with delete session: ", err) | ||||
| 	} | ||||
| 	// 删除 websocket 会话列表 | ||||
| 	h.App.ChatSession.Delete(sessionId) | ||||
| @@ -234,13 +213,13 @@ func (h *UserHandler) Session(c *gin.Context) { | ||||
| } | ||||
| 
 | ||||
| type userProfile struct { | ||||
| 	Id         uint             `json:"id"` | ||||
| 	Username   string           `json:"username"` | ||||
| 	Nickname   string           `json:"nickname"` | ||||
| 	Avatar     string           `json:"avatar"` | ||||
| 	ChatConfig types.ChatConfig `json:"chat_config"` | ||||
| 	Calls      int              `json:"calls"` | ||||
| 	Tokens     int              `json:"tokens"` | ||||
| 	Id          uint                 `json:"id"` | ||||
| 	Mobile      string               `json:"mobile"` | ||||
| 	Avatar      string               `json:"avatar"` | ||||
| 	ChatConfig  types.UserChatConfig `json:"chat_config"` | ||||
| 	Calls       int                  `json:"calls"` | ||||
| 	ImgCalls    int                  `json:"img_calls"` | ||||
| 	TotalTokens int64                `json:"total_tokens"` | ||||
| } | ||||
| 
 | ||||
| func (h *UserHandler) Profile(c *gin.Context) { | ||||
| @@ -276,29 +255,14 @@ func (h *UserHandler) ProfileUpdate(c *gin.Context) { | ||||
| 		return | ||||
| 	} | ||||
| 	h.db.First(&user, user.Id) | ||||
| 	user.Nickname = data.Nickname | ||||
| 	user.Avatar = data.Avatar | ||||
| 
 | ||||
| 	var chatConfig types.ChatConfig | ||||
| 	err = utils.JsonDecode(user.ChatConfig, &chatConfig) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, "用户配置解析失败") | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	chatConfig.EnableHistory = data.ChatConfig.EnableHistory | ||||
| 	chatConfig.EnableContext = data.ChatConfig.EnableContext | ||||
| 	chatConfig.Model = data.ChatConfig.Model | ||||
| 	chatConfig.MaxTokens = data.ChatConfig.MaxTokens | ||||
| 	chatConfig.ApiKey = data.ChatConfig.ApiKey | ||||
| 	chatConfig.Temperature = data.ChatConfig.Temperature | ||||
| 
 | ||||
| 	user.ChatConfig = utils.JsonEncode(chatConfig) | ||||
| 	user.ChatConfig = utils.JsonEncode(data.ChatConfig) | ||||
| 	res := h.db.Updates(&user) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "更新用户信息失败") | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
| 
 | ||||
| @@ -341,3 +305,47 @@ func (h *UserHandler) Password(c *gin.Context) { | ||||
| 
 | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
| 
 | ||||
| // BindMobile 绑定手机号 | ||||
| func (h *UserHandler) BindMobile(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Mobile string `json:"mobile"` | ||||
| 		Code   int    `json:"code"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// 检查手机号是否被其他账号绑定 | ||||
| 	var item model.User | ||||
| 	res := h.db.Where("mobile = ?", data.Mobile).First(&item) | ||||
| 	if res.Error == nil { | ||||
| 		resp.ERROR(c, "该手机号已经被其他账号绑定") | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// 检查验证码 | ||||
| 	key := CodeStorePrefix + data.Mobile | ||||
| 	var code int | ||||
| 	err := h.leveldb.Get(key, &code) | ||||
| 	if err != nil || code != data.Code { | ||||
| 		resp.ERROR(c, "短信验证码错误") | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	user, err := utils.GetLoginUser(c, h.db) | ||||
| 	if err != nil { | ||||
| 		resp.NotAuth(c) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	res = h.db.Model(&user).UpdateColumn("mobile", data.Mobile) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "更新数据库失败") | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	_ = h.leveldb.Delete(key) // 删除短信验证码 | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
| @@ -1,5 +0,0 @@ | ||||
| # chatgpt-plus-java | ||||
|  | ||||
| chatgpt-plus 后端 API Java 语言实现,待开发。 | ||||
|  | ||||
|  | ||||
							
								
								
									
										74
									
								
								api/logger/logger.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								api/logger/logger.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | ||||
| package logger | ||||
|  | ||||
| import ( | ||||
| 	"go.uber.org/zap" | ||||
| 	"go.uber.org/zap/zapcore" | ||||
| 	"gopkg.in/natefinch/lumberjack.v2" | ||||
| 	"os" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| var logger *zap.Logger | ||||
| var sugarLogger *zap.SugaredLogger | ||||
|  | ||||
| func GetLogger() *zap.SugaredLogger { | ||||
| 	if sugarLogger != nil { | ||||
| 		return sugarLogger | ||||
| 	} | ||||
|  | ||||
| 	logLevel := zap.NewAtomicLevelAt(getLogLevel(os.Getenv("LOG_LEVEL"))) | ||||
| 	encoder := getEncoder() | ||||
| 	writerSyncer := getLogWriter() | ||||
| 	fileCore := zapcore.NewCore(encoder, writerSyncer, logLevel) | ||||
| 	consoleOutput := zapcore.Lock(os.Stdout) | ||||
| 	consoleCore := zapcore.NewCore( | ||||
| 		encoder, | ||||
| 		consoleOutput, | ||||
| 		logLevel, | ||||
| 	) | ||||
| 	core := zapcore.NewTee(fileCore, consoleCore) | ||||
| 	logger = zap.New(core, zap.AddCaller()) | ||||
| 	sugarLogger = logger.Sugar() | ||||
| 	return sugarLogger | ||||
| } | ||||
|  | ||||
| // core 三个参数之  编码 | ||||
| func getEncoder() zapcore.Encoder { | ||||
| 	encoderConfig := zapcore.EncoderConfig{ | ||||
| 		TimeKey:        "time", | ||||
| 		LevelKey:       "level", | ||||
| 		NameKey:        "logger", | ||||
| 		CallerKey:      "caller", | ||||
| 		MessageKey:     "msg", | ||||
| 		StacktraceKey:  "stacktrace", | ||||
| 		EncodeTime:     zapcore.ISO8601TimeEncoder, | ||||
| 		EncodeDuration: zapcore.SecondsDurationEncoder, | ||||
| 		EncodeCaller:   zapcore.ShortCallerEncoder, | ||||
| 		EncodeLevel:    zapcore.CapitalLevelEncoder, | ||||
| 	} | ||||
| 	return zapcore.NewConsoleEncoder(encoderConfig) | ||||
| } | ||||
|  | ||||
| func getLogWriter() zapcore.WriteSyncer { | ||||
| 	lumberJackLogger := &lumberjack.Logger{ | ||||
| 		Filename:   "logs/app.log", | ||||
| 		MaxSize:    10, | ||||
| 		MaxBackups: 5, | ||||
| 		MaxAge:     30, | ||||
| 		Compress:   false, | ||||
| 	} | ||||
| 	return zapcore.AddSync(lumberJackLogger) | ||||
| } | ||||
|  | ||||
| func getLogLevel(level string) zapcore.Level { | ||||
| 	switch strings.ToUpper(level) { | ||||
| 	case "DEBUG": | ||||
| 		return zapcore.DebugLevel | ||||
| 	case "WARN": | ||||
| 		return zapcore.WarnLevel | ||||
| 	case "ERROR": | ||||
| 		return zapcore.ErrorLevel | ||||
| 	default: | ||||
| 		return zapcore.InfoLevel | ||||
| 	} | ||||
| } | ||||
| @@ -6,9 +6,13 @@ import ( | ||||
| 	"chatplus/handler" | ||||
| 	"chatplus/handler/admin" | ||||
| 	logger2 "chatplus/logger" | ||||
| 	"chatplus/service" | ||||
| 	"chatplus/service/function" | ||||
| 	"chatplus/service/oss" | ||||
| 	"chatplus/store" | ||||
| 	"context" | ||||
| 	"embed" | ||||
| 	"github.com/go-redis/redis/v8" | ||||
| 	"io" | ||||
| 	"log" | ||||
| 	"os" | ||||
| @@ -45,7 +49,16 @@ func (l *AppLifecycle) OnStop(context.Context) error { | ||||
| 
 | ||||
| func main() { | ||||
| 	configFile := os.Getenv("CONFIG_FILE") | ||||
| 	debug, _ := strconv.ParseBool(os.Getenv("DEBUG")) | ||||
| 	if configFile == "" { | ||||
| 		configFile = "config.toml" | ||||
| 	} | ||||
| 	var debug bool | ||||
| 	debugEnv := os.Getenv("DEBUG") | ||||
| 	if debugEnv == "" { | ||||
| 		debug = true | ||||
| 	} else { | ||||
| 		debug, _ = strconv.ParseBool(os.Getenv("DEBUG")) | ||||
| 	} | ||||
| 	logger.Info("Loading config file: ", configFile) | ||||
| 	defer func() { | ||||
| 		if err := recover(); err != nil { | ||||
| @@ -61,19 +74,23 @@ func main() { | ||||
| 				log.Fatal(err) | ||||
| 			} | ||||
| 			config.Path = configFile | ||||
| 			if debug { | ||||
| 				_ = core.SaveConfig(config) | ||||
| 			} | ||||
| 			return config | ||||
| 		}), | ||||
| 		// 创建应用服务 | ||||
| 		fx.Provide(core.NewServer), | ||||
| 		// 初始化 | ||||
| 		fx.Invoke(func(s *core.AppServer) { | ||||
| 			s.Init(debug) | ||||
| 		fx.Invoke(func(s *core.AppServer, client *redis.Client) { | ||||
| 			s.Init(debug, client) | ||||
| 		}), | ||||
| 
 | ||||
| 		// 初始化数据库 | ||||
| 		fx.Provide(store.NewGormConfig), | ||||
| 		fx.Provide(store.NewMysql), | ||||
| 		fx.Provide(store.NewLevelDB), | ||||
| 		fx.Provide(store.NewRedisClient), | ||||
| 
 | ||||
| 		// 创建 Ip2Region 查询对象 | ||||
| 		fx.Provide(func() (*xdb.Searcher, error) { | ||||
| @@ -89,17 +106,41 @@ func main() { | ||||
| 			return xdb.NewWithBuffer(cBuff) | ||||
| 		}), | ||||
| 
 | ||||
| 		// 创建函数 | ||||
| 		fx.Provide(function.NewFunctions), | ||||
| 
 | ||||
| 		// 创建控制器 | ||||
| 		fx.Provide(handler.NewChatRoleHandler), | ||||
| 		fx.Provide(handler.NewUserHandler), | ||||
| 		fx.Provide(handler.NewChatHandler), | ||||
| 		fx.Provide(handler.NewUploadHandler), | ||||
| 		fx.Provide(handler.NewSmsHandler), | ||||
| 		fx.Provide(handler.NewRewardHandler), | ||||
| 		fx.Provide(handler.NewCaptchaHandler), | ||||
| 		fx.Provide(handler.NewMidJourneyHandler), | ||||
| 		fx.Provide(handler.NewChatModelHandler), | ||||
| 
 | ||||
| 		fx.Provide(admin.NewConfigHandler), | ||||
| 		fx.Provide(admin.NewAdminHandler), | ||||
| 		fx.Provide(admin.NewApiKeyHandler), | ||||
| 		fx.Provide(admin.NewUserHandler), | ||||
| 		fx.Provide(admin.NewChatRoleHandler), | ||||
| 		fx.Provide(admin.NewRewardHandler), | ||||
| 		fx.Provide(admin.NewDashboardHandler), | ||||
| 		fx.Provide(admin.NewChatModelHandler), | ||||
| 
 | ||||
| 		// 创建服务 | ||||
| 		fx.Provide(service.NewAliYunSmsService), | ||||
| 		fx.Provide(func(config *types.AppConfig) *service.CaptchaService { | ||||
| 			return service.NewCaptchaService(config.ApiConfig) | ||||
| 		}), | ||||
| 		fx.Provide(oss.NewUploaderManager), | ||||
| 		fx.Provide(service.NewMjService), | ||||
| 		fx.Invoke(func(mjService *service.MjService) { | ||||
| 			go func() { | ||||
| 				mjService.Run() | ||||
| 			}() | ||||
| 		}), | ||||
| 
 | ||||
| 		// 注册路由 | ||||
| 		fx.Invoke(func(s *core.AppServer, h *handler.ChatRoleHandler) { | ||||
| @@ -115,21 +156,47 @@ func main() { | ||||
| 			group.GET("profile", h.Profile) | ||||
| 			group.POST("profile/update", h.ProfileUpdate) | ||||
| 			group.POST("password", h.Password) | ||||
| 			group.POST("bind/mobile", h.BindMobile) | ||||
| 		}), | ||||
| 		fx.Invoke(func(s *core.AppServer, h *handler.ChatHandler) { | ||||
| 			group := s.Engine.Group("/api/chat/") | ||||
| 			group.Any("new", h.ChatHandle) | ||||
| 			group.GET("list", h.List) | ||||
| 			group.GET("detail", h.Detail) | ||||
| 			group.POST("update", h.Update) | ||||
| 			group.GET("remove", h.Remove) | ||||
| 			group.GET("history", h.History) | ||||
| 			group.GET("clear", h.Clear) | ||||
| 			group.GET("tokens", h.Tokens) | ||||
| 			group.POST("tokens", h.Tokens) | ||||
| 			group.GET("stop", h.StopGenerate) | ||||
| 		}), | ||||
| 		fx.Invoke(func(s *core.AppServer, h *handler.UploadHandler) { | ||||
| 			s.Engine.POST("/api/upload", h.Upload) | ||||
| 		}), | ||||
| 		fx.Invoke(func(s *core.AppServer, h *handler.SmsHandler) { | ||||
| 			group := s.Engine.Group("/api/sms/") | ||||
| 			group.GET("status", h.Status) | ||||
| 			group.POST("code", h.SendCode) | ||||
| 		}), | ||||
| 		fx.Invoke(func(s *core.AppServer, h *handler.CaptchaHandler) { | ||||
| 			group := s.Engine.Group("/api/captcha/") | ||||
| 			group.GET("get", h.Get) | ||||
| 			group.POST("check", h.Check) | ||||
| 		}), | ||||
| 		fx.Invoke(func(s *core.AppServer, h *handler.RewardHandler) { | ||||
| 			group := s.Engine.Group("/api/reward/") | ||||
| 			group.POST("notify", h.Notify) | ||||
| 			group.POST("verify", h.Verify) | ||||
| 		}), | ||||
| 		fx.Invoke(func(s *core.AppServer, h *handler.MidJourneyHandler) { | ||||
| 			group := s.Engine.Group("/api/mj/") | ||||
| 			group.POST("notify", h.Notify) | ||||
| 			group.POST("image", h.Image) | ||||
| 			group.POST("upscale", h.Upscale) | ||||
| 			group.POST("variation", h.Variation) | ||||
| 			group.GET("jobs", h.JobList) | ||||
| 			group.Any("client", h.Client) | ||||
| 		}), | ||||
| 
 | ||||
| 		// 管理后台控制器 | ||||
| 		fx.Invoke(func(s *core.AppServer, h *admin.ConfigHandler) { | ||||
| @@ -153,15 +220,36 @@ func main() { | ||||
| 		fx.Invoke(func(s *core.AppServer, h *admin.UserHandler) { | ||||
| 			group := s.Engine.Group("/api/admin/user/") | ||||
| 			group.GET("list", h.List) | ||||
| 			group.POST("update", h.Update) | ||||
| 			group.POST("save", h.Save) | ||||
| 			group.GET("remove", h.Remove) | ||||
| 			group.GET("loginLog", h.LoginLog) | ||||
| 			group.POST("resetPass", h.ResetPass) | ||||
| 		}), | ||||
| 		fx.Invoke(func(s *core.AppServer, h *admin.ChatRoleHandler) { | ||||
| 			group := s.Engine.Group("/api/admin/role/") | ||||
| 			group.GET("list", h.List) | ||||
| 			group.POST("save", h.Save) | ||||
| 			group.POST("sort", h.SetSort) | ||||
| 			group.POST("sort", h.Sort) | ||||
| 			group.GET("remove", h.Remove) | ||||
| 		}), | ||||
| 		fx.Invoke(func(s *core.AppServer, h *admin.RewardHandler) { | ||||
| 			group := s.Engine.Group("/api/admin/reward/") | ||||
| 			group.GET("list", h.List) | ||||
| 		}), | ||||
| 		fx.Invoke(func(s *core.AppServer, h *admin.DashboardHandler) { | ||||
| 			group := s.Engine.Group("/api/admin/dashboard/") | ||||
| 			group.GET("stats", h.Stats) | ||||
| 		}), | ||||
| 		fx.Invoke(func(s *core.AppServer, h *handler.ChatModelHandler) { | ||||
| 			group := s.Engine.Group("/api/model/") | ||||
| 			group.GET("list", h.List) | ||||
| 		}), | ||||
| 		fx.Invoke(func(s *core.AppServer, h *admin.ChatModelHandler) { | ||||
| 			group := s.Engine.Group("/api/admin/model/") | ||||
| 			group.POST("save", h.Save) | ||||
| 			group.GET("list", h.List) | ||||
| 			group.POST("enable", h.Enable) | ||||
| 			group.POST("sort", h.Sort) | ||||
| 			group.GET("remove", h.Remove) | ||||
| 		}), | ||||
| 
 | ||||
| @@ -1,5 +0,0 @@ | ||||
| # chatgpt-plus-php | ||||
|  | ||||
| chatgpt-plus 后端 API PHP 语言实现,待开发。 | ||||
|  | ||||
|  | ||||
| @@ -1,5 +0,0 @@ | ||||
| # chatgpt-plus-python | ||||
|  | ||||
| chatgpt-plus 后端 API Python 语言实现,待开发。 | ||||
|  | ||||
|  | ||||
							
								
								
									
										54
									
								
								api/service/aliyun_sms_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								api/service/aliyun_sms_service.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | ||||
| package service | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/store" | ||||
| 	"fmt" | ||||
| 	"github.com/aliyun/alibaba-cloud-sdk-go/services/dysmsapi" | ||||
| ) | ||||
|  | ||||
| type AliYunSmsService struct { | ||||
| 	config *types.AppConfig | ||||
| 	db     *store.LevelDB | ||||
| 	client *dysmsapi.Client | ||||
| } | ||||
|  | ||||
| func NewAliYunSmsService(config *types.AppConfig, db *store.LevelDB) (*AliYunSmsService, error) { | ||||
| 	// 创建阿里云短信客户端 | ||||
| 	client, err := dysmsapi.NewClientWithAccessKey( | ||||
| 		"cn-hangzhou", | ||||
| 		config.SmsConfig.AccessKey, | ||||
| 		config.SmsConfig.AccessSecret) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to create client: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	return &AliYunSmsService{ | ||||
| 		config: config, | ||||
| 		db:     db, | ||||
| 		client: client, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| func (s *AliYunSmsService) SendVerifyCode(mobile string, code int) error { | ||||
| 	// 创建短信请求并设置参数 | ||||
| 	request := dysmsapi.CreateSendSmsRequest() | ||||
| 	request.Scheme = "https" | ||||
| 	request.Domain = s.config.SmsConfig.Domain | ||||
| 	request.PhoneNumbers = mobile | ||||
| 	request.SignName = "飞行的蜗牛" | ||||
| 	request.TemplateCode = "SMS_281460317" | ||||
| 	request.TemplateParam = fmt.Sprintf("{\"code\":\"%d\"}", code) // 短信模板中的参数 | ||||
|  | ||||
| 	// 发送短信 | ||||
| 	response, err := s.client.SendSms(request) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to send SMS:%v", err) | ||||
| 	} | ||||
|  | ||||
| 	if response.Code != "OK" { | ||||
| 		return fmt.Errorf("failed to send SMS:%v", response.Message) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										62
									
								
								api/service/captcha_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								api/service/captcha_service.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| package service | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core/types" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"github.com/imroc/req/v3" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| type CaptchaService struct { | ||||
| 	config types.ChatPlusApiConfig | ||||
| 	client *req.Client | ||||
| } | ||||
|  | ||||
| func NewCaptchaService(config types.ChatPlusApiConfig) *CaptchaService { | ||||
| 	return &CaptchaService{ | ||||
| 		config: config, | ||||
| 		client: req.C().SetTimeout(10 * time.Second), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (s *CaptchaService) Get() (interface{}, error) { | ||||
| 	if s.config.Token == "" { | ||||
| 		return nil, errors.New("无效的 API Token") | ||||
| 	} | ||||
|  | ||||
| 	url := fmt.Sprintf("%s/api/captcha/get", s.config.ApiURL) | ||||
| 	var res types.BizVo | ||||
| 	r, err := s.client.R(). | ||||
| 		SetHeader("AppId", s.config.AppId). | ||||
| 		SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.Token)). | ||||
| 		SetSuccessResult(&res).Get(url) | ||||
| 	if err != nil || r.IsErrorState() { | ||||
| 		return nil, fmt.Errorf("请求 API 失败:%v", err) | ||||
| 	} | ||||
|  | ||||
| 	if res.Code != types.Success { | ||||
| 		return nil, fmt.Errorf("请求 API 失败:%s", res.Message) | ||||
| 	} | ||||
|  | ||||
| 	return res.Data, nil | ||||
| } | ||||
|  | ||||
| func (s *CaptchaService) Check(data interface{}) bool { | ||||
| 	url := fmt.Sprintf("%s/api/captcha/check", s.config.ApiURL) | ||||
| 	var res types.BizVo | ||||
| 	r, err := s.client.R(). | ||||
| 		SetHeader("AppId", s.config.AppId). | ||||
| 		SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.Token)). | ||||
| 		SetBodyJsonMarshal(data). | ||||
| 		SetSuccessResult(&res).Post(url) | ||||
| 	if err != nil || r.IsErrorState() { | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	if res.Code != types.Success { | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	return true | ||||
| } | ||||
							
								
								
									
										41
									
								
								api/service/function/func_mj.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								api/service/function/func_mj.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| package function | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/service" | ||||
| 	"chatplus/utils" | ||||
| ) | ||||
|  | ||||
| // AI 绘画函数 | ||||
|  | ||||
| type FuncMidJourney struct { | ||||
| 	name    string | ||||
| 	service *service.MjService | ||||
| } | ||||
|  | ||||
| func NewMidJourneyFunc(mjService *service.MjService) FuncMidJourney { | ||||
| 	return FuncMidJourney{ | ||||
| 		name:    "MidJourney AI 绘画", | ||||
| 		service: mjService} | ||||
| } | ||||
|  | ||||
| func (f FuncMidJourney) Invoke(params map[string]interface{}) (string, error) { | ||||
| 	logger.Infof("MJ 绘画参数:%+v", params) | ||||
| 	prompt := utils.InterfaceToString(params["prompt"]) | ||||
| 	f.service.PushTask(service.MjTask{ | ||||
| 		SessionId: utils.InterfaceToString(params["session_id"]), | ||||
| 		Src:       service.TaskSrcChat, | ||||
| 		Type:      service.Image, | ||||
| 		Prompt:    prompt, | ||||
| 		UserId:    utils.IntValue(utils.InterfaceToString(params["user_id"]), 0), | ||||
| 		RoleId:    utils.IntValue(utils.InterfaceToString(params["role_id"]), 0), | ||||
| 		Icon:      utils.InterfaceToString(params["icon"]), | ||||
| 		ChatId:    utils.InterfaceToString(params["chat_id"]), | ||||
| 	}) | ||||
| 	return prompt, nil | ||||
| } | ||||
|  | ||||
| func (f FuncMidJourney) Name() string { | ||||
| 	return f.name | ||||
| } | ||||
|  | ||||
| var _ Function = &FuncMidJourney{} | ||||
							
								
								
									
										39
									
								
								api/service/function/function.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								api/service/function/function.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| package function | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core/types" | ||||
| 	logger2 "chatplus/logger" | ||||
| 	"chatplus/service" | ||||
| ) | ||||
|  | ||||
| type Function interface { | ||||
| 	Invoke(map[string]interface{}) (string, error) | ||||
| 	Name() string | ||||
| } | ||||
|  | ||||
| var logger = logger2.GetLogger() | ||||
|  | ||||
| type resVo struct { | ||||
| 	Code    types.BizCode `json:"code"` | ||||
| 	Message string        `json:"message"` | ||||
| 	Data    struct { | ||||
| 		Title     string     `json:"title"` | ||||
| 		UpdatedAt string     `json:"updated_at"` | ||||
| 		Items     []dataItem `json:"items"` | ||||
| 	} `json:"data"` | ||||
| } | ||||
|  | ||||
| type dataItem struct { | ||||
| 	Title  string `json:"title"` | ||||
| 	Url    string `json:"url"` | ||||
| 	Remark string `json:"remark"` | ||||
| } | ||||
|  | ||||
| func NewFunctions(config *types.AppConfig, mjService *service.MjService) map[string]Function { | ||||
| 	return map[string]Function{ | ||||
| 		types.FuncZaoBao:     NewZaoBao(config.ApiConfig), | ||||
| 		types.FuncWeibo:      NewWeiboHot(config.ApiConfig), | ||||
| 		types.FuncHeadLine:   NewHeadLines(config.ApiConfig), | ||||
| 		types.FuncMidJourney: NewMidJourneyFunc(mjService), | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										58
									
								
								api/service/function/tou_tiao.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								api/service/function/tou_tiao.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| package function | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core/types" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"github.com/imroc/req/v3" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| // 今日头条函数实现 | ||||
|  | ||||
| type FuncHeadlines struct { | ||||
| 	name   string | ||||
| 	config types.ChatPlusApiConfig | ||||
| 	client *req.Client | ||||
| } | ||||
|  | ||||
| func NewHeadLines(config types.ChatPlusApiConfig) FuncHeadlines { | ||||
| 	return FuncHeadlines{ | ||||
| 		name:   "今日头条", | ||||
| 		config: config, | ||||
| 		client: req.C().SetTimeout(10 * time.Second)} | ||||
| } | ||||
|  | ||||
| func (f FuncHeadlines) Invoke(map[string]interface{}) (string, error) { | ||||
| 	if f.config.Token == "" { | ||||
| 		return "", errors.New("无效的 API Token") | ||||
| 	} | ||||
|  | ||||
| 	url := fmt.Sprintf("%s/api/headline/fetch", f.config.ApiURL) | ||||
| 	var res resVo | ||||
| 	r, err := f.client.R(). | ||||
| 		SetHeader("AppId", f.config.AppId). | ||||
| 		SetHeader("Authorization", fmt.Sprintf("Bearer %s", f.config.Token)). | ||||
| 		SetSuccessResult(&res).Get(url) | ||||
| 	if err != nil || r.IsErrorState() { | ||||
| 		return "", fmt.Errorf("%v%v", err, r.Err) | ||||
| 	} | ||||
|  | ||||
| 	if res.Code != types.Success { | ||||
| 		return "", errors.New(res.Message) | ||||
| 	} | ||||
|  | ||||
| 	builder := make([]string, 0) | ||||
| 	builder = append(builder, fmt.Sprintf("**%s**,最新更新:%s", res.Data.Title, res.Data.UpdatedAt)) | ||||
| 	for i, v := range res.Data.Items { | ||||
| 		builder = append(builder, fmt.Sprintf("%d、 [%s](%s) [%s]", i+1, v.Title, v.Url, v.Remark)) | ||||
| 	} | ||||
| 	return strings.Join(builder, "\n\n"), nil | ||||
| } | ||||
|  | ||||
| func (f FuncHeadlines) Name() string { | ||||
| 	return f.name | ||||
| } | ||||
|  | ||||
| var _ Function = &FuncHeadlines{} | ||||
							
								
								
									
										58
									
								
								api/service/function/weibo_hot.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								api/service/function/weibo_hot.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| package function | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core/types" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"github.com/imroc/req/v3" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| // 微博热搜函数实现 | ||||
|  | ||||
| type FuncWeiboHot struct { | ||||
| 	name   string | ||||
| 	config types.ChatPlusApiConfig | ||||
| 	client *req.Client | ||||
| } | ||||
|  | ||||
| func NewWeiboHot(config types.ChatPlusApiConfig) FuncWeiboHot { | ||||
| 	return FuncWeiboHot{ | ||||
| 		name:   "微博热搜", | ||||
| 		config: config, | ||||
| 		client: req.C().SetTimeout(10 * time.Second)} | ||||
| } | ||||
|  | ||||
| func (f FuncWeiboHot) Invoke(map[string]interface{}) (string, error) { | ||||
| 	if f.config.Token == "" { | ||||
| 		return "", errors.New("无效的 API Token") | ||||
| 	} | ||||
|  | ||||
| 	url := fmt.Sprintf("%s/api/weibo/fetch", f.config.ApiURL) | ||||
| 	var res resVo | ||||
| 	r, err := f.client.R(). | ||||
| 		SetHeader("AppId", f.config.AppId). | ||||
| 		SetHeader("Authorization", fmt.Sprintf("Bearer %s", f.config.Token)). | ||||
| 		SetSuccessResult(&res).Get(url) | ||||
| 	if err != nil || r.IsErrorState() { | ||||
| 		return "", fmt.Errorf("%v%v", err, r.Err) | ||||
| 	} | ||||
|  | ||||
| 	if res.Code != types.Success { | ||||
| 		return "", errors.New(res.Message) | ||||
| 	} | ||||
|  | ||||
| 	builder := make([]string, 0) | ||||
| 	builder = append(builder, fmt.Sprintf("**%s**,最新更新:%s", res.Data.Title, res.Data.UpdatedAt)) | ||||
| 	for i, v := range res.Data.Items { | ||||
| 		builder = append(builder, fmt.Sprintf("%d、 [%s](%s) [热度:%s]", i+1, v.Title, v.Url, v.Remark)) | ||||
| 	} | ||||
| 	return strings.Join(builder, "\n\n"), nil | ||||
| } | ||||
|  | ||||
| func (f FuncWeiboHot) Name() string { | ||||
| 	return f.name | ||||
| } | ||||
|  | ||||
| var _ Function = &FuncWeiboHot{} | ||||
							
								
								
									
										59
									
								
								api/service/function/zao_bao.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								api/service/function/zao_bao.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| package function | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core/types" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"github.com/imroc/req/v3" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| // 每日早报函数实现 | ||||
|  | ||||
| type FuncZaoBao struct { | ||||
| 	name   string | ||||
| 	config types.ChatPlusApiConfig | ||||
| 	client *req.Client | ||||
| } | ||||
|  | ||||
| func NewZaoBao(config types.ChatPlusApiConfig) FuncZaoBao { | ||||
| 	return FuncZaoBao{ | ||||
| 		name:   "每日早报", | ||||
| 		config: config, | ||||
| 		client: req.C().SetTimeout(10 * time.Second)} | ||||
| } | ||||
|  | ||||
| func (f FuncZaoBao) Invoke(map[string]interface{}) (string, error) { | ||||
| 	if f.config.Token == "" { | ||||
| 		return "", errors.New("无效的 API Token") | ||||
| 	} | ||||
|  | ||||
| 	url := fmt.Sprintf("%s/api/zaobao/fetch", f.config.ApiURL) | ||||
| 	var res resVo | ||||
| 	r, err := f.client.R(). | ||||
| 		SetHeader("AppId", f.config.AppId). | ||||
| 		SetHeader("Authorization", fmt.Sprintf("Bearer %s", f.config.Token)). | ||||
| 		SetSuccessResult(&res).Get(url) | ||||
| 	if err != nil || r.IsErrorState() { | ||||
| 		return "", fmt.Errorf("%v%v", err, r.Err) | ||||
| 	} | ||||
|  | ||||
| 	if res.Code != types.Success { | ||||
| 		return "", errors.New(res.Message) | ||||
| 	} | ||||
|  | ||||
| 	builder := make([]string, 0) | ||||
| 	builder = append(builder, fmt.Sprintf("**%s 早报:**", res.Data.UpdatedAt)) | ||||
| 	for _, v := range res.Data.Items { | ||||
| 		builder = append(builder, v.Title) | ||||
| 	} | ||||
| 	builder = append(builder, fmt.Sprintf("%s", res.Data.Title)) | ||||
| 	return strings.Join(builder, "\n\n"), nil | ||||
| } | ||||
|  | ||||
| func (f FuncZaoBao) Name() string { | ||||
| 	return f.name | ||||
| } | ||||
|  | ||||
| var _ Function = &FuncZaoBao{} | ||||
							
								
								
									
										203
									
								
								api/service/mj_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										203
									
								
								api/service/mj_service.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,203 @@ | ||||
| package service | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core/types" | ||||
| 	logger2 "chatplus/logger" | ||||
| 	"chatplus/store" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/utils" | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"github.com/go-redis/redis/v8" | ||||
| 	"github.com/imroc/req/v3" | ||||
| 	"gorm.io/gorm" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| var logger = logger2.GetLogger() | ||||
|  | ||||
| // MJ 绘画服务 | ||||
|  | ||||
| const MjRunningJobKey = "MidJourney_Running_Job" | ||||
|  | ||||
| type TaskType string | ||||
|  | ||||
| func (t TaskType) String() string { | ||||
| 	return string(t) | ||||
| } | ||||
|  | ||||
| const ( | ||||
| 	Image     = TaskType("image") | ||||
| 	Upscale   = TaskType("upscale") | ||||
| 	Variation = TaskType("variation") | ||||
| ) | ||||
|  | ||||
| type TaskSrc string | ||||
|  | ||||
| const ( | ||||
| 	TaskSrcChat = TaskSrc("chat") | ||||
| 	TaskSrcImg  = TaskSrc("img") | ||||
| ) | ||||
|  | ||||
| type MjTask struct { | ||||
| 	Id          int      `json:"id"` | ||||
| 	SessionId   string   `json:"session_id"` | ||||
| 	Src         TaskSrc  `json:"src"` | ||||
| 	Type        TaskType `json:"type"` | ||||
| 	UserId      int      `json:"user_id"` | ||||
| 	Prompt      string   `json:"prompt,omitempty"` | ||||
| 	ChatId      string   `json:"chat_id,omitempty"` | ||||
| 	RoleId      int      `json:"role_id,omitempty"` | ||||
| 	Icon        string   `json:"icon,omitempty"` | ||||
| 	Index       int32    `json:"index,omitempty"` | ||||
| 	MessageId   string   `json:"message_id,omitempty"` | ||||
| 	MessageHash string   `json:"message_hash,omitempty"` | ||||
| 	RetryCount  int      `json:"retry_count"` | ||||
| } | ||||
|  | ||||
| type MjService struct { | ||||
| 	config    types.ChatPlusExtConfig | ||||
| 	client    *req.Client | ||||
| 	taskQueue *store.RedisQueue | ||||
| 	redis     *redis.Client | ||||
| 	db        *gorm.DB | ||||
| } | ||||
|  | ||||
| func NewMjService(appConfig *types.AppConfig, client *redis.Client, db *gorm.DB) *MjService { | ||||
| 	return &MjService{ | ||||
| 		config:    appConfig.ExtConfig, | ||||
| 		redis:     client, | ||||
| 		db:        db, | ||||
| 		taskQueue: store.NewRedisQueue("midjourney_task_queue", client), | ||||
| 		client:    req.C().SetTimeout(30 * time.Second)} | ||||
| } | ||||
|  | ||||
| func (s *MjService) Run() { | ||||
| 	logger.Info("Starting MidJourney job consumer.") | ||||
| 	ctx := context.Background() | ||||
| 	for { | ||||
| 		_, err := s.redis.Get(ctx, MjRunningJobKey).Result() | ||||
| 		if err == nil { | ||||
| 			time.Sleep(time.Second * 3) | ||||
| 			continue | ||||
| 		} | ||||
| 		var task MjTask | ||||
| 		err = s.taskQueue.LPop(&task) | ||||
| 		if err != nil { | ||||
| 			logger.Errorf("taking task with error: %v", err) | ||||
| 			continue | ||||
| 		} | ||||
| 		logger.Infof("Consuming Task: %+v", task) | ||||
| 		switch task.Type { | ||||
| 		case Image: | ||||
| 			err = s.image(task.Prompt) | ||||
| 			break | ||||
| 		case Upscale: | ||||
| 			err = s.upscale(MjUpscaleReq{ | ||||
| 				Index:       task.Index, | ||||
| 				MessageId:   task.MessageId, | ||||
| 				MessageHash: task.MessageHash, | ||||
| 			}) | ||||
| 			break | ||||
| 		case Variation: | ||||
| 			err = s.variation(MjVariationReq{ | ||||
| 				Index:       task.Index, | ||||
| 				MessageId:   task.MessageId, | ||||
| 				MessageHash: task.MessageHash, | ||||
| 			}) | ||||
| 		} | ||||
| 		if err != nil { | ||||
| 			logger.Error("绘画任务执行失败:", err) | ||||
| 			if task.RetryCount > 5 { | ||||
| 				// 取消并删除任务 | ||||
| 				s.db.Where("id = ?", task.Id).Delete(&model.MidJourneyJob{}) | ||||
| 				continue | ||||
| 			} | ||||
| 			task.RetryCount += 1 | ||||
| 			s.taskQueue.RPush(task) | ||||
| 			// TODO: 执行失败通知聊天客户端 | ||||
| 			time.Sleep(time.Second * 3) | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		// 锁定任务执行通道,直到任务超时(5分钟) | ||||
| 		s.redis.Set(ctx, MjRunningJobKey, utils.JsonEncode(task), time.Minute*5) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (s *MjService) PushTask(task MjTask) { | ||||
| 	logger.Infof("add a new MidJourney Task: %+v", task) | ||||
| 	s.taskQueue.RPush(task) | ||||
| } | ||||
|  | ||||
| func (s *MjService) image(prompt string) error { | ||||
| 	logger.Infof("MJ 绘画参数:%+v", prompt) | ||||
| 	body := map[string]string{"prompt": prompt} | ||||
| 	url := fmt.Sprintf("%s/api/mj/image", s.config.ApiURL) | ||||
| 	var res types.BizVo | ||||
| 	r, err := s.client.R(). | ||||
| 		SetHeader("Authorization", s.config.Token). | ||||
| 		SetHeader("Content-Type", "application/json"). | ||||
| 		SetBody(body). | ||||
| 		SetSuccessResult(&res).Post(url) | ||||
| 	if err != nil || r.IsErrorState() { | ||||
| 		return fmt.Errorf("%v%v", r.String(), err) | ||||
| 	} | ||||
|  | ||||
| 	if res.Code != types.Success { | ||||
| 		return errors.New(res.Message) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| type MjUpscaleReq struct { | ||||
| 	Index       int32  `json:"index"` | ||||
| 	MessageId   string `json:"message_id"` | ||||
| 	MessageHash string `json:"message_hash"` | ||||
| } | ||||
|  | ||||
| func (s *MjService) upscale(upReq MjUpscaleReq) error { | ||||
| 	url := fmt.Sprintf("%s/api/mj/upscale", s.config.ApiURL) | ||||
| 	var res types.BizVo | ||||
| 	r, err := s.client.R(). | ||||
| 		SetHeader("Authorization", s.config.Token). | ||||
| 		SetHeader("Content-Type", "application/json"). | ||||
| 		SetBody(upReq). | ||||
| 		SetSuccessResult(&res).Post(url) | ||||
| 	if err != nil || r.IsErrorState() { | ||||
| 		return fmt.Errorf("%v%v", r.String(), err) | ||||
| 	} | ||||
|  | ||||
| 	if res.Code != types.Success { | ||||
| 		return errors.New(res.Message) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| type MjVariationReq struct { | ||||
| 	Index       int32  `json:"index"` | ||||
| 	MessageId   string `json:"message_id"` | ||||
| 	MessageHash string `json:"message_hash"` | ||||
| } | ||||
|  | ||||
| func (s *MjService) variation(upReq MjVariationReq) error { | ||||
| 	url := fmt.Sprintf("%s/api/mj/variation", s.config.ApiURL) | ||||
| 	var res types.BizVo | ||||
| 	r, err := s.client.R(). | ||||
| 		SetHeader("Authorization", s.config.Token). | ||||
| 		SetHeader("Content-Type", "application/json"). | ||||
| 		SetBody(upReq). | ||||
| 		SetSuccessResult(&res).Post(url) | ||||
| 	if err != nil || r.IsErrorState() { | ||||
| 		return fmt.Errorf("%v%v", r.String(), err) | ||||
| 	} | ||||
|  | ||||
| 	if res.Code != types.Success { | ||||
| 		return errors.New(res.Message) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										64
									
								
								api/service/oss/localstorage_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								api/service/oss/localstorage_service.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| package oss | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/utils" | ||||
| 	"fmt" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| type LocalStorageService struct { | ||||
| 	config   *types.LocalStorageConfig | ||||
| 	proxyURL string | ||||
| } | ||||
|  | ||||
| func NewLocalStorageService(config *types.AppConfig) LocalStorageService { | ||||
| 	return LocalStorageService{ | ||||
| 		config:   &config.OSS.Local, | ||||
| 		proxyURL: config.ProxyURL, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (s LocalStorageService) PutFile(ctx *gin.Context, name string) (string, error) { | ||||
| 	file, err := ctx.FormFile(name) | ||||
| 	if err != nil { | ||||
| 		return "", fmt.Errorf("error with get form: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	filePath, err := utils.GenUploadPath(s.config.BasePath, file.Filename) | ||||
| 	if err != nil { | ||||
| 		return "", fmt.Errorf("error with generate filename: %s", err.Error()) | ||||
| 	} | ||||
| 	// 将文件保存到指定路径 | ||||
| 	err = ctx.SaveUploadedFile(file, filePath) | ||||
| 	if err != nil { | ||||
| 		return "", fmt.Errorf("error with save upload file: %s", err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	return utils.GenUploadUrl(s.config.BasePath, s.config.BaseURL, filePath), nil | ||||
| } | ||||
|  | ||||
| func (s LocalStorageService) PutImg(imageURL string) (string, error) { | ||||
| 	filename := filepath.Base(imageURL) | ||||
| 	filePath, err := utils.GenUploadPath(s.config.BasePath, filename) | ||||
| 	if err != nil { | ||||
| 		return "", fmt.Errorf("error with generate image dir: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	err = utils.DownloadFile(imageURL, filePath, s.proxyURL) | ||||
| 	if err != nil { | ||||
| 		return "", fmt.Errorf("error with download image: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	return utils.GenUploadUrl(s.config.BasePath, s.config.BaseURL, filePath), nil | ||||
| } | ||||
|  | ||||
| func (s LocalStorageService) Delete(fileURL string) error { | ||||
| 	filePath := strings.Replace(fileURL, s.config.BaseURL, s.config.BasePath, 1) | ||||
| 	return os.Remove(filePath) | ||||
| } | ||||
|  | ||||
| var _ Uploader = LocalStorageService{} | ||||
							
								
								
									
										83
									
								
								api/service/oss/minio_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								api/service/oss/minio_service.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | ||||
| package oss | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/utils" | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/minio/minio-go/v7" | ||||
| 	"github.com/minio/minio-go/v7/pkg/credentials" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| type MinioService struct { | ||||
| 	config   *types.MinioConfig | ||||
| 	client   *minio.Client | ||||
| 	proxyURL string | ||||
| } | ||||
|  | ||||
| func NewMinioService(appConfig *types.AppConfig) (MinioService, error) { | ||||
| 	config := &appConfig.OSS.Minio | ||||
| 	minioClient, err := minio.New(config.Endpoint, &minio.Options{ | ||||
| 		Creds:  credentials.NewStaticV4(config.AccessKey, config.AccessSecret, ""), | ||||
| 		Secure: config.UseSSL, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return MinioService{}, err | ||||
| 	} | ||||
| 	return MinioService{config: config, client: minioClient, proxyURL: appConfig.ProxyURL}, nil | ||||
| } | ||||
|  | ||||
| func (s MinioService) PutImg(imageURL string) (string, error) { | ||||
| 	imageData, err := utils.DownloadImage(imageURL, s.proxyURL) | ||||
| 	if err != nil { | ||||
| 		return "", fmt.Errorf("error with download image: %v", err) | ||||
| 	} | ||||
| 	fileExt := filepath.Ext(filepath.Base(imageURL)) | ||||
| 	filename := fmt.Sprintf("%d%s", time.Now().UnixMicro(), fileExt) | ||||
| 	info, err := s.client.PutObject( | ||||
| 		context.Background(), | ||||
| 		s.config.Bucket, | ||||
| 		filename, | ||||
| 		strings.NewReader(string(imageData)), | ||||
| 		int64(len(imageData)), | ||||
| 		minio.PutObjectOptions{ContentType: "image/png"}) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	return fmt.Sprintf("%s/%s/%s", s.config.Domain, s.config.Bucket, info.Key), nil | ||||
| } | ||||
|  | ||||
| func (s MinioService) PutFile(ctx *gin.Context, name string) (string, error) { | ||||
| 	file, err := ctx.FormFile(name) | ||||
| 	if err != nil { | ||||
| 		return "", fmt.Errorf("error with get form: %v", err) | ||||
| 	} | ||||
| 	// Open the uploaded file | ||||
| 	fileReader, err := file.Open() | ||||
| 	if err != nil { | ||||
| 		return "", fmt.Errorf("error opening file: %v", err) | ||||
| 	} | ||||
| 	defer fileReader.Close() | ||||
|  | ||||
| 	fileExt := filepath.Ext(file.Filename) | ||||
| 	filename := fmt.Sprintf("%d%s", time.Now().UnixMicro(), fileExt) | ||||
| 	info, err := s.client.PutObject(ctx, s.config.Bucket, filename, fileReader, file.Size, minio.PutObjectOptions{ | ||||
| 		ContentType: file.Header.Get("Content-Type"), | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return "", fmt.Errorf("error uploading to MinIO: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	return fmt.Sprintf("%s/%s/%s", s.config.Domain, s.config.Bucket, info.Key), nil | ||||
| } | ||||
|  | ||||
| func (s MinioService) Delete(fileURL string) error { | ||||
| 	objectName := filepath.Base(fileURL) | ||||
| 	return s.client.RemoveObject(context.Background(), s.config.Bucket, objectName, minio.RemoveObjectOptions{}) | ||||
| } | ||||
|  | ||||
| var _ Uploader = MinioService{} | ||||
							
								
								
									
										98
									
								
								api/service/oss/qiniu_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								api/service/oss/qiniu_service.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,98 @@ | ||||
| package oss | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/utils" | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/qiniu/go-sdk/v7/auth/qbox" | ||||
| 	"github.com/qiniu/go-sdk/v7/storage" | ||||
| 	"path/filepath" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| type QiNiuService struct { | ||||
| 	config   *types.QiNiuConfig | ||||
| 	token    string | ||||
| 	uploader *storage.FormUploader | ||||
| 	manager  *storage.BucketManager | ||||
| 	proxyURL string | ||||
| 	dir      string | ||||
| } | ||||
|  | ||||
| func NewQiNiuService(appConfig *types.AppConfig) QiNiuService { | ||||
| 	config := &appConfig.OSS.QiNiu | ||||
| 	// build storage uploader | ||||
| 	zone, ok := storage.GetRegionByID(storage.RegionID(config.Zone)) | ||||
| 	if !ok { | ||||
| 		zone = storage.ZoneHuanan | ||||
| 	} | ||||
| 	storeConfig := storage.Config{Zone: &zone} | ||||
| 	formUploader := storage.NewFormUploader(&storeConfig) | ||||
| 	// generate token | ||||
| 	mac := qbox.NewMac(config.AccessKey, config.AccessSecret) | ||||
| 	putPolicy := storage.PutPolicy{ | ||||
| 		Scope: config.Bucket, | ||||
| 	} | ||||
| 	return QiNiuService{ | ||||
| 		config:   config, | ||||
| 		token:    putPolicy.UploadToken(mac), | ||||
| 		uploader: formUploader, | ||||
| 		manager:  storage.NewBucketManager(mac, &storeConfig), | ||||
| 		proxyURL: appConfig.ProxyURL, | ||||
| 		dir:      "chatgpt-plus", | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (s QiNiuService) PutFile(ctx *gin.Context, name string) (string, error) { | ||||
| 	// 解析表单 | ||||
| 	file, err := ctx.FormFile(name) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	// 打开上传文件 | ||||
| 	src, err := file.Open() | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	defer src.Close() | ||||
|  | ||||
| 	fileExt := filepath.Ext(file.Filename) | ||||
| 	key := fmt.Sprintf("%s/%d%s", s.dir, time.Now().UnixMicro(), fileExt) | ||||
| 	// 上传文件 | ||||
| 	ret := storage.PutRet{} | ||||
| 	extra := storage.PutExtra{} | ||||
| 	err = s.uploader.Put(ctx, &ret, s.token, key, src, file.Size, &extra) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	return fmt.Sprintf("%s/%s", s.config.Domain, ret.Key), nil | ||||
| } | ||||
|  | ||||
| func (s QiNiuService) PutImg(imageURL string) (string, error) { | ||||
| 	imageData, err := utils.DownloadImage(imageURL, s.proxyURL) | ||||
| 	if err != nil { | ||||
| 		return "", fmt.Errorf("error with download image: %v", err) | ||||
| 	} | ||||
| 	fileExt := filepath.Ext(filepath.Base(imageURL)) | ||||
| 	key := fmt.Sprintf("%s/%d%s", s.dir, time.Now().UnixMicro(), fileExt) | ||||
| 	ret := storage.PutRet{} | ||||
| 	extra := storage.PutExtra{} | ||||
| 	// 上传文件字节数据 | ||||
| 	err = s.uploader.Put(context.Background(), &ret, s.token, key, bytes.NewReader(imageData), int64(len(imageData)), &extra) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	return fmt.Sprintf("%s/%s", s.config.Domain, ret.Key), nil | ||||
| } | ||||
|  | ||||
| func (s QiNiuService) Delete(fileURL string) error { | ||||
| 	objectName := filepath.Base(fileURL) | ||||
| 	key := fmt.Sprintf("%s/%s", s.dir, objectName) | ||||
| 	return s.manager.Delete(s.config.Bucket, key) | ||||
| } | ||||
|  | ||||
| var _ Uploader = QiNiuService{} | ||||
							
								
								
									
										9
									
								
								api/service/oss/uploader.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								api/service/oss/uploader.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| package oss | ||||
|  | ||||
| import "github.com/gin-gonic/gin" | ||||
|  | ||||
| type Uploader interface { | ||||
| 	PutFile(ctx *gin.Context, name string) (string, error) | ||||
| 	PutImg(imageURL string) (string, error) | ||||
| 	Delete(fileURL string) error | ||||
| } | ||||
							
								
								
									
										42
									
								
								api/service/oss/uploader_manager.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								api/service/oss/uploader_manager.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| package oss | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core/types" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| type UploaderManager struct { | ||||
| 	handler Uploader | ||||
| } | ||||
|  | ||||
| const Local = "LOCAL" | ||||
| const Minio = "MINIO" | ||||
| const QiNiu = "QINIU" | ||||
|  | ||||
| func NewUploaderManager(config *types.AppConfig) (*UploaderManager, error) { | ||||
| 	active := Local | ||||
| 	if config.OSS.Active != "" { | ||||
| 		active = strings.ToUpper(config.OSS.Active) | ||||
| 	} | ||||
| 	var handler Uploader | ||||
| 	switch active { | ||||
| 	case Local: | ||||
| 		handler = NewLocalStorageService(config) | ||||
| 		break | ||||
| 	case Minio: | ||||
| 		service, err := NewMinioService(config) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		handler = service | ||||
| 		break | ||||
| 	case QiNiu: | ||||
| 		handler = NewQiNiuService(config) | ||||
| 	} | ||||
|  | ||||
| 	return &UploaderManager{handler: handler}, nil | ||||
| } | ||||
|  | ||||
| func (m *UploaderManager) GetUploadHandler() Uploader { | ||||
| 	return m.handler | ||||
| } | ||||
							
								
								
									
										5
									
								
								api/service/sms_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								api/service/sms_service.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| package service | ||||
|  | ||||
| type SmsService interface { | ||||
| 	SendVerifyCode(mobile string, code int) error | ||||
| } | ||||
| @@ -3,7 +3,6 @@ package store | ||||
| import ( | ||||
| 	"chatplus/store/vo" | ||||
| 	"encoding/json" | ||||
| 
 | ||||
| 	"github.com/syndtr/goleveldb/leveldb" | ||||
| 	"github.com/syndtr/goleveldb/leveldb/util" | ||||
| ) | ||||
| @@ -30,13 +29,13 @@ func (db *LevelDB) Put(key string, value interface{}) error { | ||||
| 	return db.driver.Put([]byte(key), bytes, nil) | ||||
| } | ||||
| 
 | ||||
| func (db *LevelDB) Get(key string) ([]byte, error) { | ||||
| func (db *LevelDB) Get(key string, value interface{}) error { | ||||
| 	bytes, err := db.driver.Get([]byte(key), nil) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	return bytes, nil | ||||
| 	return json.Unmarshal(bytes, &value) | ||||
| } | ||||
| 
 | ||||
| func (db *LevelDB) Search(prefix string) []string { | ||||
| @@ -3,7 +3,7 @@ package model | ||||
| // ApiKey OpenAI API 模型 | ||||
| type ApiKey struct { | ||||
| 	BaseModel | ||||
| 	UserId     uint   //用户ID,系统添加的用户 ID 为 0 | ||||
| 	Platform   string | ||||
| 	Value      string // API Key 的值 | ||||
| 	LastUsedAt int64  // 最后使用时间 | ||||
| } | ||||
							
								
								
									
										20
									
								
								api/store/model/chat_history.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								api/store/model/chat_history.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| package model | ||||
|  | ||||
| import "gorm.io/gorm" | ||||
|  | ||||
| type HistoryMessage struct { | ||||
| 	BaseModel | ||||
| 	ChatId     string // 会话 ID | ||||
| 	UserId     uint   // 用户 ID | ||||
| 	RoleId     uint   // 角色 ID | ||||
| 	Type       string | ||||
| 	Icon       string | ||||
| 	Tokens     int | ||||
| 	Content    string | ||||
| 	UseContext bool // 是否可以作为聊天上下文 | ||||
| 	DeletedAt  gorm.DeletedAt | ||||
| } | ||||
|  | ||||
| func (HistoryMessage) TableName() string { | ||||
| 	return "chatgpt_chat_history" | ||||
| } | ||||
							
								
								
									
										13
									
								
								api/store/model/chat_item.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								api/store/model/chat_item.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| package model | ||||
|  | ||||
| import "gorm.io/gorm" | ||||
|  | ||||
| type ChatItem struct { | ||||
| 	BaseModel | ||||
| 	ChatId    string `gorm:"column:chat_id;unique"` // 会话 ID | ||||
| 	UserId    uint   // 用户 ID | ||||
| 	RoleId    uint   // 角色 ID | ||||
| 	ModelId   uint   // 会话模型 | ||||
| 	Title     string // 会话标题 | ||||
| 	DeletedAt gorm.DeletedAt | ||||
| } | ||||
							
								
								
									
										10
									
								
								api/store/model/chat_model.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								api/store/model/chat_model.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| package model | ||||
|  | ||||
| type ChatModel struct { | ||||
| 	BaseModel | ||||
| 	Platform string | ||||
| 	Name     string | ||||
| 	Value    string // API Key 的值 | ||||
| 	SortNum  int | ||||
| 	Enabled  bool | ||||
| } | ||||
| @@ -8,5 +8,5 @@ type ChatRole struct { | ||||
| 	HelloMsg string // 打招呼的消息 | ||||
| 	Icon     string // 角色聊天图标 | ||||
| 	Enable   bool   // 是否启用被启用 | ||||
| 	Sort     int    //排序数字 | ||||
| 	SortNum  int    //排序数字 | ||||
| } | ||||
							
								
								
									
										20
									
								
								api/store/model/mj_job.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								api/store/model/mj_job.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| package model | ||||
|  | ||||
| import "time" | ||||
|  | ||||
| type MidJourneyJob struct { | ||||
| 	Id          uint `gorm:"primarykey;column:id"` | ||||
| 	Type        string | ||||
| 	UserId      int | ||||
| 	MessageId   string | ||||
| 	ReferenceId string | ||||
| 	ImgURL      string | ||||
| 	Hash        string // message hash | ||||
| 	Progress    int | ||||
| 	Prompt      string | ||||
| 	CreatedAt   time.Time | ||||
| } | ||||
|  | ||||
| func (MidJourneyJob) TableName() string { | ||||
| 	return "chatgpt_mj_jobs" | ||||
| } | ||||
							
								
								
									
										12
									
								
								api/store/model/reward.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								api/store/model/reward.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| package model | ||||
|  | ||||
| // 用户打赏 | ||||
|  | ||||
| type Reward struct { | ||||
| 	BaseModel | ||||
| 	UserId uint    // 用户 ID | ||||
| 	TxId   string  // 交易ID | ||||
| 	Amount float64 // 打赏金额 | ||||
| 	Remark string  // 打赏备注 | ||||
| 	Status bool    // 核销状态 | ||||
| } | ||||
| @@ -2,13 +2,13 @@ package model | ||||
| 
 | ||||
| type User struct { | ||||
| 	BaseModel | ||||
| 	Username    string `gorm:"index:username,unique"` | ||||
| 	Mobile      string | ||||
| 	Password    string | ||||
| 	Nickname    string | ||||
| 	Avatar      string | ||||
| 	Salt        string // 密码盐 | ||||
| 	Tokens      int64  // 剩余tokens | ||||
| 	TotalTokens int64  // 总消耗 tokens | ||||
| 	Calls       int    // 剩余对话次数 | ||||
| 	ImgCalls    int    // 剩余绘图次数 | ||||
| 	ChatConfig  string `gorm:"column:chat_config_json"` // 聊天配置 json | ||||
| 	ChatRoles   string `gorm:"column:chat_roles_json"`  // 聊天角色 | ||||
| 	ExpiredTime int64  // 账户到期时间 | ||||
							
								
								
									
										20
									
								
								api/store/redis.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								api/store/redis.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| package store | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core/types" | ||||
| 	"context" | ||||
| 	"github.com/go-redis/redis/v8" | ||||
| ) | ||||
|  | ||||
| func NewRedisClient(config *types.AppConfig) (*redis.Client, error) { | ||||
| 	client := redis.NewClient(&redis.Options{ | ||||
| 		Addr:     config.Redis.Url(), | ||||
| 		Password: config.Redis.Password, | ||||
| 		DB:       config.Redis.DB, | ||||
| 	}) | ||||
| 	_, err := client.Ping(context.Background()).Result() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return client, nil | ||||
| } | ||||
							
								
								
									
										41
									
								
								api/store/redis_queue.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								api/store/redis_queue.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| package store | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/utils" | ||||
| 	"context" | ||||
| 	"github.com/go-redis/redis/v8" | ||||
| ) | ||||
|  | ||||
| type RedisQueue struct { | ||||
| 	name   string | ||||
| 	client *redis.Client | ||||
| 	ctx    context.Context | ||||
| } | ||||
|  | ||||
| func NewRedisQueue(name string, client *redis.Client) *RedisQueue { | ||||
| 	return &RedisQueue{name: name, client: client, ctx: context.Background()} | ||||
| } | ||||
|  | ||||
| func (q *RedisQueue) RPush(value interface{}) { | ||||
| 	q.client.RPush(q.ctx, q.name, utils.JsonEncode(value)) | ||||
| } | ||||
|  | ||||
| func (q *RedisQueue) LPush(value interface{}) { | ||||
| 	q.client.LPush(q.ctx, q.name, utils.JsonEncode(value)) | ||||
| } | ||||
|  | ||||
| func (q *RedisQueue) LPop(value interface{}) error { | ||||
| 	result, err := q.client.BLPop(q.ctx, 0, q.name).Result() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return utils.JsonDecode(result[1], value) | ||||
| } | ||||
|  | ||||
| func (q *RedisQueue) RPop(value interface{}) error { | ||||
| 	result, err := q.client.BRPop(q.ctx, 0, q.name).Result() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return utils.JsonDecode(result[1], value) | ||||
| } | ||||
| @@ -3,7 +3,7 @@ package vo | ||||
| // ApiKey OpenAI API 模型 | ||||
| type ApiKey struct { | ||||
| 	BaseVo | ||||
| 	UserId     uint   `json:"user_id"`      //用户ID,系统添加的用户 ID 为 0 | ||||
| 	Platform   string `json:"platform"` | ||||
| 	Value      string `json:"value"`        // API Key 的值 | ||||
| 	LastUsedAt int64  `json:"last_used_at"` // 最后使用时间 | ||||
| } | ||||
							
								
								
									
										13
									
								
								api/store/vo/chat_history.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								api/store/vo/chat_history.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| package vo | ||||
|  | ||||
| type HistoryMessage struct { | ||||
| 	BaseVo | ||||
| 	ChatId     string `json:"chat_id"` | ||||
| 	UserId     uint   `json:"user_id"` | ||||
| 	RoleId     uint   `json:"role_id"` | ||||
| 	Type       string `json:"type"` | ||||
| 	Icon       string `json:"icon"` | ||||
| 	Tokens     int    `json:"tokens"` | ||||
| 	Content    string `json:"content"` | ||||
| 	UseContext bool   `json:"use_context"` | ||||
| } | ||||
							
								
								
									
										11
									
								
								api/store/vo/chat_item.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								api/store/vo/chat_item.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| package vo | ||||
|  | ||||
| type ChatItem struct { | ||||
| 	BaseVo | ||||
| 	UserId  uint   `json:"user_id"` | ||||
| 	Icon    string `json:"icon"` | ||||
| 	RoleId  uint   `json:"role_id"` | ||||
| 	ChatId  string `json:"chat_id"` | ||||
| 	ModelId uint   `json:"model_id"` | ||||
| 	Title   string `json:"title"` | ||||
| } | ||||
							
								
								
									
										9
									
								
								api/store/vo/chat_model.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								api/store/vo/chat_model.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| package vo | ||||
|  | ||||
| type ChatModel struct { | ||||
| 	BaseVo | ||||
| 	Platform string `json:"platform"` | ||||
| 	Name     string `json:"name"` | ||||
| 	Value    string `json:"value"` | ||||
| 	Enabled  bool   `json:"enabled"` | ||||
| } | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user