mirror of
				https://github.com/yangjian102621/geekai.git
				synced 2025-10-31 14:23:43 +08:00 
			
		
		
		
	Compare commits
	
		
			54 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 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 | 
							
								
								
									
										33
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,6 +1,39 @@ | ||||
| # 更新日志 | ||||
|  | ||||
| ## 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. 管理后台:支持关闭注册功能,新增添加用户功能,适用于内部使用场景 | ||||
|   | ||||
							
								
								
									
										59
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										59
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,10 +1,13 @@ | ||||
| # ChatGPT-Plus | ||||
|  | ||||
| **ChatGPT-PLUS** 是基于 OpenAI API 实现的 ChatGPT 聊天系统。主要有如下特性: | ||||
| **ChatGPT-PLUS** 基于 AI 大语言模型 API 实现的 AI 助手全套开源解决方案,自带运营管理后台,开箱即用。集成了 OpenAI, Azure, ChatGLM,讯飞星火,文心一言等多个平台的大语言模型。主要有如下特性: | ||||
|  | ||||
| * 完整的开源系统,前端应用和后台管理系统皆可开箱即用。 | ||||
| * 聊天体验跟 ChatGPT 官方版本完全一致。 | ||||
| * 内置了各种预训练好的角色,比如小红书写手,英语翻译大师,苏格拉底,孔子,乔布斯,周报助手等。轻松满足你的各种聊天和应用需求。 | ||||
| * 支持 MidJourney AI 绘画集成,开箱即用。 | ||||
| * 支持使用个人微信二维码作为充值收费的支付渠道,无需企业支付通道。(可定制开发其他支付通道支持) | ||||
| * 集成插件 API 功能,可结合 GPT 开发各种强大的插件,已内置实现了微博热搜,今日头条,今日早报和 AI 绘画函数插件。 | ||||
|  | ||||
| ## 功能截图 | ||||
|  | ||||
| @@ -20,6 +23,8 @@ | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ### 用户设置 | ||||
|  | ||||
|  | ||||
| @@ -30,6 +35,8 @@ | ||||
|  | ||||
| ### 管理后台 | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| @@ -42,7 +49,7 @@ | ||||
|  | ||||
| ### 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/> | ||||
| > **注意:请合法使用,禁止输出任何敏感、不友好或违规的内容!!!** | ||||
|  | ||||
| ## 使用须知 | ||||
| @@ -91,16 +98,18 @@ ChatGPT 的服务。 | ||||
|  | ||||
| ## TODOLIST | ||||
|  | ||||
| * [ ] 整合 Midjourney AI 绘画 API | ||||
| * [ ] 开发移动端聊天页面 | ||||
| * [x] 整合 Midjourney AI 绘画 API | ||||
| * [x] 开发移动端聊天页面 | ||||
| * [x] 接入微信支付功能 | ||||
| * [x] 支持 ChatGPT 函数功能,通过函数实现插件 | ||||
| * [ ] 接入语音和 TTS API,支持语音聊天 | ||||
| * [ ] 支持基于知识库的 AI 问答 | ||||
| * [ ] 开发桌面版应用 | ||||
| * [ ] 开发手机 App 客户端 | ||||
|  | ||||
| ## Docker 快速部署 | ||||
|  | ||||
| > 鉴于最新不少网友反馈在部署的时候遇到一些问题,大部分问题都是相同的,所以我这边做了一个视频教程 [五分钟部署自己的 ChatGPT 服务](https://www.bilibili.com/video/BV1H14y1B7Qw/)。 | ||||
| > | ||||
| 鉴于最新不少网友反馈在部署的时候遇到一些问题,大部分问题都是相同的,所以我这边做了一个视频教程 [五分钟部署自己的 ChatGPT 服务](https://www.bilibili.com/video/BV1H14y1B7Qw/)。 | ||||
| > 习惯看视频教程的朋友可以去看视频教程,视频的语速比较慢,建议 2 倍速观看。 | ||||
|  | ||||
| V3.0.0 版本以后已经支持使用容器部署了,跳过所有的繁琐的环境准备,一条命令就可以轻松部署上线。 | ||||
| @@ -135,24 +144,16 @@ Listen = "0.0.0.0:5678" | ||||
| ProxyURL = ["YOUR_PROXY_URL"] # 替换成你本地代理,如:http://127.0.0.1:7777 | ||||
| #ProxyURL = "" 如果你的服务器本身就在墙外,那么你直接留空就好了 | ||||
| MysqlDns = "root:12345678@tcp(172.22.11.200:3307)/chatgpt_plus?charset=utf8&parseTime=True&loc=Local" | ||||
| StartWechatBot = false # 是否启动微信机器人,默认关闭,如果设置为 TRUE 则启动服务的时候需要微信扫码登录 | ||||
| EnabledMsgService = false # 注册时是否开启短信验证功能,该功能需要配合短信服务一起使用 | ||||
|  | ||||
| [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" # 如果是生产环境的话,这里管理员的密码记得修改 | ||||
|    | ||||
| [ApiConfig] # 插件 API 服务配置,此为第三方插件服务,如需使用请联系作者开通 | ||||
| [ApiConfig] # 微博热搜,今日头条等函数服务 API 配置,此为第三方插件服务,如需使用请联系作者开通 | ||||
|   ApiURL = "{URL}" | ||||
|   AppId = "{APP_ID}" | ||||
|   Token = "{TOKEN}" | ||||
| @@ -163,8 +164,33 @@ EnabledMsgService = false # 注册时是否开启短信验证功能,该功能 | ||||
|   Product = "Dysmsapi" | ||||
|   Domain = "dysmsapi.aliyuncs.com" | ||||
|  | ||||
| [ExtConfig] # MidJourney和微信机器人服务 API 配置,开通此功能需要配合 chatpgt-plus-exts 项目部署 | ||||
|   ApiURL = "插件扩展 API 地址" | ||||
|   Token = "插件扩展 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 = "IP:端口" # 如 172.22.11.200:9000 | ||||
|      AccessKey = "minio oss access key" # 自己去 Minio 控制台去创建一个 Access Key | ||||
|      AccessSecret = "minio oss access secret" | ||||
|      Bucket = "chatgpt-plus" # 替换为你自己创建的 Bucket,注意要给 Bucket 设置公开的读权限,否则会出现图片无法显示。 | ||||
|      UseSSL = false | ||||
|      Domain = "minio 文件公开访问地址" # 地址必须是能够通过公网访问的,否则会出现图片无法显示。 | ||||
|    [OSS.QiNiu] # 七牛云 OSS 配置 | ||||
|        Zone = "z2" # 区域,z0:华东,z1: 华北,na0:北美,as0:新加坡 | ||||
|        AccessKey = "七牛云 OSS AccessKey" | ||||
|        AccessSecret = "七牛云 OSS AccessSecret" | ||||
|        Bucket = "七牛云 OSS 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 | ||||
| @@ -281,7 +307,6 @@ make clean linux | ||||
| ## 参与贡献 | ||||
|  | ||||
| 个人的力量始终有限,任何形式的贡献都是欢迎的,包括但不限于贡献代码,优化文档,提交 issue 和 PR 等。 | ||||
| **尤其是新版本的开发计划比较大,包括各种语言的后端 API 实现,本人精力有限,希望借助社区的力量来完成这些 API 的开发。** | ||||
|  | ||||
| 如果有兴趣的话,也可以加微信进入微信讨论群(**添加好友时请注明来自Github!!!**)。 | ||||
|  | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
| @@ -4,19 +4,10 @@ MysqlDns = "root:mysql_pass@tcp(localhost:3306)/chatgpt_plus?charset=utf8mb4&col | ||||
| StaticDir = "./static" | ||||
| StaticUrl = "http://localhost:5678/static" | ||||
| AesEncryptKey = "{YOUR_AES_KEY}" | ||||
| StartWechatBot = false | ||||
| EnabledMsgService = false | ||||
|  | ||||
| [Session] | ||||
|   Driver = "cookie" | ||||
|   SecretKey = "m0cjm3gsuw9jk73np1ni7r42koilybjcndlycjdmq7za3pbqn7w12fyok5pqh6q5" | ||||
|   Name = "CHAT_SESSION_ID" | ||||
|   Path = "/" | ||||
|   Domain = "localhost" | ||||
|   MaxAge = 86400 | ||||
|   Secure = false | ||||
|   HttpOnly = false | ||||
|   SameSite = 2 | ||||
|  | ||||
| [Manager] | ||||
|   Username = "admin" | ||||
| @@ -38,4 +29,25 @@ EnabledMsgService = false | ||||
|   Product = "Dysmsapi" | ||||
|   Domain = "dysmsapi.aliyuncs.com" | ||||
|  | ||||
| [ExtConfig] | ||||
|   ApiURL = "插件扩展 API 地址" | ||||
|   Token = "插件扩展 API Token" | ||||
|  | ||||
|  [OSS] | ||||
|    Active = "local" | ||||
|    [OSS.Local] | ||||
|      BasePath = "./static/upload" | ||||
|      BaseURL = "http://localhost:5678/static/upload" | ||||
|    [OSS.Minio] | ||||
|      Endpoint = "IP:端口" | ||||
|      AccessKey = "minio oss access key" | ||||
|      AccessSecret = "minio oss access secret" | ||||
|      Bucket = "minio oss bucket" | ||||
|      UseSSL = false | ||||
|      Domain = "minio 文件公开访问地址" | ||||
|    [OSS.QiNiu] # 七牛云 OSS 配置 | ||||
|        Zone = "z2" # 区域,z0:华东,z1: 华北,na0:北美,as0:新加坡 | ||||
|        AccessKey = "七牛云 OSS AccessKey" | ||||
|        AccessSecret = "七牛云 OSS AccessSecret" | ||||
|        Bucket = "七牛云 OSS Bucket" | ||||
|        Domain = "OSS Bucket 所绑定的域名,如 https://img.r9it.com" | ||||
|   | ||||
| @@ -7,16 +7,16 @@ import ( | ||||
| 	"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" | ||||
| 	"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 { | ||||
| @@ -30,17 +30,14 @@ type AppServer struct { | ||||
|  | ||||
| 	// 保存 Websocket 会话 UserId, 每个 UserId 只能连接一次 | ||||
| 	// 防止第三方直接连接 socket 调用 OpenAI API | ||||
| 	ChatSession   *types.LMap[string, types.ChatSession]  //map[sessionId]UserId | ||||
| 	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, | ||||
| 	funZaoBao function.FuncZaoBao, | ||||
| 	funZhiHu function.FuncHeadlines, | ||||
| 	funWeibo function.FuncWeiboHot) *AppServer { | ||||
| func NewServer(appConfig *types.AppConfig, functions map[string]function.Function) *AppServer { | ||||
| 	gin.SetMode(gin.ReleaseMode) | ||||
| 	gin.DefaultWriter = io.Discard | ||||
| 	return &AppServer{ | ||||
| @@ -48,25 +45,21 @@ func NewServer( | ||||
| 		Config:        appConfig, | ||||
| 		Engine:        gin.Default(), | ||||
| 		ChatContexts:  types.NewLMap[string, []interface{}](), | ||||
| 		ChatSession:   types.NewLMap[string, types.ChatSession](), | ||||
| 		ChatSession:   types.NewLMap[string, *types.ChatSession](), | ||||
| 		ChatClients:   types.NewLMap[string, *types.WsClient](), | ||||
| 		ReqCancelFunc: types.NewLMap[string, context.CancelFunc](), | ||||
| 		Functions: map[string]function.Function{ | ||||
| 			types.FuncZaoBao:   funZaoBao, | ||||
| 			types.FuncWeibo:    funWeibo, | ||||
| 			types.FuncHeadLine: funZhiHu, | ||||
| 		}, | ||||
| 		MjTaskClients: types.NewLMap[string, *types.WsClient](), | ||||
| 		Functions:     functions, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (s *AppServer) Init(debug bool) { | ||||
| 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(sessionMiddleware(s.Config)) | ||||
| 	s.Engine.Use(authorizeMiddleware(s)) | ||||
| 	s.Engine.Use(authorizeMiddleware(s, client)) | ||||
| 	s.Engine.Use(errorHandler) | ||||
| 	// 添加静态资源访问 | ||||
| 	s.Engine.Static("/static", s.Config.StaticDir) | ||||
| @@ -111,42 +104,6 @@ func errorHandler(c *gin.Context) { | ||||
| 	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: | ||||
| 		config.Session.Driver = types.SessionDriverCookie | ||||
| 		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) { | ||||
| @@ -157,7 +114,7 @@ func corsMiddleware() gin.HandlerFunc { | ||||
| 			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-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") | ||||
| 			//设置缓存时间 | ||||
| @@ -181,11 +138,15 @@ func corsMiddleware() gin.HandlerFunc { | ||||
| } | ||||
|  | ||||
| // 用户授权验证 | ||||
| func authorizeMiddleware(s *AppServer) gin.HandlerFunc { | ||||
| 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" || | ||||
| 			strings.HasPrefix(c.Request.URL.Path, "/api/sms/") || | ||||
| 			strings.HasPrefix(c.Request.URL.Path, "/api/captcha/") || | ||||
| 			strings.HasPrefix(c.Request.URL.Path, "/static/") || | ||||
| @@ -194,29 +155,54 @@ func authorizeMiddleware(s *AppServer) gin.HandlerFunc { | ||||
| 			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() | ||||
| 			} | ||||
| 		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" { | ||||
| 			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 | ||||
| 		} | ||||
| 		session := sessions.Default(c) | ||||
| 		var value interface{} | ||||
| 		if strings.Contains(c.Request.URL.Path, "/api/admin/") { // 后台管理 API | ||||
| 			value = session.Get(types.SessionAdmin) | ||||
| 		} else { | ||||
| 			value = session.Get(types.SessionUser) | ||||
| 		} | ||||
| 		if value != nil { | ||||
| 			c.Next() | ||||
| 		} else { | ||||
| 			resp.NotAuth(c) | ||||
|  | ||||
| 		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" | ||||
| @@ -23,18 +22,18 @@ func NewDefaultConfig() *types.AppConfig { | ||||
| 		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{}, | ||||
| 		StartWechatBot: false, | ||||
| 		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", | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -6,8 +6,9 @@ type ApiRequest struct { | ||||
| 	Temperature float32       `json:"temperature"` | ||||
| 	MaxTokens   int           `json:"max_tokens"` | ||||
| 	Stream      bool          `json:"stream"` | ||||
| 	Messages    []interface{} `json:"messages"` | ||||
| 	Functions   []Function    `json:"functions"` | ||||
| 	Messages    []interface{} `json:"messages,omitempty"` | ||||
| 	Prompt      []interface{} `json:"prompt,omitempty"` // 兼容 ChatGLM | ||||
| 	Functions   []Function    `json:"functions,omitempty"` | ||||
| } | ||||
|  | ||||
| type Message struct { | ||||
| @@ -34,12 +35,27 @@ type Delta struct { | ||||
|  | ||||
| // 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 模型 | ||||
| 	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 MjTask struct { | ||||
| 	ChatId      string | ||||
| 	MessageId   string | ||||
| 	MessageHash string | ||||
| 	UserId      uint | ||||
| 	RoleId      uint | ||||
| 	Icon        string | ||||
| } | ||||
|  | ||||
| type ApiError struct { | ||||
| @@ -53,6 +69,7 @@ type ApiError struct { | ||||
|  | ||||
| const PromptMsg = "prompt" // prompt message | ||||
| const ReplyMsg = "reply"   // reply message | ||||
| const MjMsg = "mj" | ||||
|  | ||||
| var ModelToTokens = map[string]int{ | ||||
| 	"gpt-3.5-turbo":     4096, | ||||
| @@ -60,3 +77,5 @@ var ModelToTokens = map[string]int{ | ||||
| 	"gpt-4":             8192, | ||||
| 	"gpt-4-32k":         32768, | ||||
| } | ||||
|  | ||||
| const TaskStorePrefix = "/tasks/" | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -2,24 +2,24 @@ 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 连接信息 | ||||
| 	ApiConfig         ChatPlusApiConfig // ChatPlus API authorization configs | ||||
| 	AesEncryptKey     string | ||||
| 	SmsConfig         AliYunSmsConfig // AliYun send message service config | ||||
| 	StartWechatBot    bool            // 是否启动微信机器人 | ||||
| 	EnabledMsgService bool            // 是否启用短信服务 | ||||
| 	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 { | ||||
| @@ -28,6 +28,11 @@ type ChatPlusApiConfig struct { | ||||
| 	Token  string | ||||
| } | ||||
|  | ||||
| type ChatPlusExtConfig struct { | ||||
| 	ApiURL string | ||||
| 	Token  string | ||||
| } | ||||
|  | ||||
| type AliYunSmsConfig struct { | ||||
| 	AccessKey    string | ||||
| 	AccessSecret string | ||||
| @@ -35,10 +40,39 @@ type AliYunSmsConfig struct { | ||||
| 	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 { | ||||
| @@ -51,43 +85,43 @@ type Manager struct { | ||||
| 	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"` | ||||
| 	ContextDeep   int     `json:"context_deep"` // 上下文深度 | ||||
| 	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"` // 新用户注册默认总送多少次调用 | ||||
| 	EnabledRegister bool     `json:"enabled_register"` | ||||
| 	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 绘画功能 | ||||
| } | ||||
|   | ||||
| @@ -23,9 +23,10 @@ type Property struct { | ||||
| } | ||||
|  | ||||
| const ( | ||||
| 	FuncZaoBao   = "zao_bao"   // 每日早报 | ||||
| 	FuncHeadLine = "headline"  // 今日头条 | ||||
| 	FuncWeibo    = "weibo_hot" // 微博热搜 | ||||
| 	FuncZaoBao     = "zao_bao"     // 每日早报 | ||||
| 	FuncHeadLine   = "headline"    // 今日头条 | ||||
| 	FuncWeibo      = "weibo_hot"   // 微博热搜 | ||||
| 	FuncMidJourney = "mid_journey" // MJ 绘画 | ||||
| ) | ||||
|  | ||||
| var InnerFunctions = []Function{ | ||||
| @@ -73,4 +74,31 @@ var InnerFunctions = []Function{ | ||||
| 			Required: []string{}, | ||||
| 		}, | ||||
| 	}, | ||||
|  | ||||
| 	{ | ||||
| 		Name:        FuncMidJourney, | ||||
| 		Description: "AI 绘画工具,使用 MJ MidJourney API 进行 AI 绘画", | ||||
| 		Parameters: Parameters{ | ||||
| 			Type: "object", | ||||
| 			Properties: map[string]Property{ | ||||
| 				"prompt": { | ||||
| 					Type:        "string", | ||||
| 					Description: "绘画内容描述,提示词,如果该参数中有中文的话,则需要翻译成英文", | ||||
| 				}, | ||||
| 				"ar": { | ||||
| 					Type:        "string", | ||||
| 					Description: "图片长宽比,默认值 16:9", | ||||
| 				}, | ||||
| 				"niji": { | ||||
| 					Type:        "string", | ||||
| 					Description: "动漫模型版本,默认值空", | ||||
| 				}, | ||||
| 				"v": { | ||||
| 					Type:        "string", | ||||
| 					Description: "模型版本,默认值: 5.2", | ||||
| 				}, | ||||
| 			}, | ||||
| 			Required: []string{}, | ||||
| 		}, | ||||
| 	}, | ||||
| } | ||||
|   | ||||
| @@ -9,7 +9,7 @@ type MKey interface { | ||||
| 	string | int | ||||
| } | ||||
| type MValue interface { | ||||
| 	*WsClient | ChatSession | context.CancelFunc | []interface{} | ||||
| 	*WsClient | *ChatSession | context.CancelFunc | []interface{} | MjTask | ||||
| } | ||||
| type LMap[K MKey, T MValue] struct { | ||||
| 	lock sync.RWMutex | ||||
|   | ||||
| @@ -1,6 +1,14 @@ | ||||
| package types | ||||
|  | ||||
| const SessionName = "ChatGPT-TOKEN" | ||||
| const SessionUser = "SESSION_USER"        // 存储用户信息的 session key | ||||
| const SessionAdmin = "SESSION_ADMIN"      //存储管理员信息的 session key | ||||
| const LoginUserCache = "LOGIN_USER_CACHE" // 已登录用户缓存 | ||||
| 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 | ||||
|   | ||||
							
								
								
									
										36
									
								
								api/go.mod
									
									
									
									
									
								
							
							
						
						
									
										36
									
								
								api/go.mod
									
									
									
									
									
								
							| @@ -5,14 +5,15 @@ go 1.19 | ||||
| require ( | ||||
| 	github.com/BurntSushi/toml v1.1.0 | ||||
| 	github.com/aliyun/alibaba-cloud-sdk-go v1.62.405 | ||||
| 	github.com/eatmoreapple/openwechat v1.2.1 | ||||
| 	github.com/gin-contrib/sessions v0.0.5 | ||||
| 	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/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e | ||||
| 	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 | ||||
| @@ -21,44 +22,50 @@ require ( | ||||
|  | ||||
| require ( | ||||
| 	github.com/andybalholm/brotli v1.0.4 // indirect | ||||
| 	github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // 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/gomodule/redigo v2.0.0+incompatible // 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.15.15 // indirect | ||||
| 	github.com/klauspost/cpuid/v2 v2.2.4 // 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/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b // 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.11.0 // indirect | ||||
| 	golang.org/x/text v0.10.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.66.2 // indirect | ||||
| 	gopkg.in/ini.v1 v1.67.0 // indirect | ||||
| 	gopkg.in/yaml.v3 v3.0.1 // indirect | ||||
| ) | ||||
|  | ||||
| @@ -68,9 +75,6 @@ require ( | ||||
| 	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/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.4 // indirect | ||||
| 	github.com/mattn/go-isatty v0.0.19 // indirect | ||||
| @@ -80,7 +84,7 @@ require ( | ||||
| 	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.10.0 | ||||
| 	golang.org/x/sys v0.9.0 // indirect | ||||
| 	golang.org/x/crypto v0.12.0 | ||||
| 	golang.org/x/sys v0.11.0 // indirect | ||||
| 	gorm.io/gorm v1.25.1 | ||||
| ) | ||||
|   | ||||
							
								
								
									
										128
									
								
								api/go.sum
									
									
									
									
									
								
							
							
						
						
									
										128
									
								
								api/go.sum
									
									
									
									
									
								
							| @@ -5,40 +5,50 @@ github.com/aliyun/alibaba-cloud-sdk-go v1.62.405/go.mod h1:Api2AkmMgGaSUAhmk76oa | ||||
| 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/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.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/eatmoreapple/openwechat v1.2.1 h1:ez4oqF/Y2NSEX/DbPV8lvj7JlfkYqvieeo4awx5lzfU= | ||||
| github.com/eatmoreapple/openwechat v1.2.1/go.mod h1:61HOzTyvLobGdgWhL68jfGNwTJEv0mhQ1miCXQrvWU8= | ||||
| 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/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.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= | ||||
| @@ -46,6 +56,8 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4 | ||||
| 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= | ||||
| @@ -54,20 +66,13 @@ 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/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/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/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/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= | ||||
| @@ -75,7 +80,6 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY | ||||
| 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 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= | ||||
| 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= | ||||
| @@ -89,31 +93,43 @@ github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht | ||||
| 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.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw= | ||||
| github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4= | ||||
| 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.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= | ||||
| github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= | ||||
| github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= | ||||
| 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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= | ||||
| 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 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= | ||||
| 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= | ||||
| @@ -122,14 +138,17 @@ github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:Ff | ||||
| 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/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b h1:aUNXCGgukb4gtY99imuIeoh8Vr0GSwAlYxPAhqZrpFc= | ||||
| github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg= | ||||
| 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= | ||||
| @@ -140,8 +159,13 @@ github.com/quic-go/quic-go v0.35.1 h1:b0kzj6b/cQAf05cT0CkQubHM31wiA+xH3IBkxP62po | ||||
| 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/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= | ||||
| github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= | ||||
| 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= | ||||
| @@ -165,6 +189,7 @@ github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6 | ||||
| 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= | ||||
| @@ -182,40 +207,63 @@ 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.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= | ||||
| golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= | ||||
| 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.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= | ||||
| golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= | ||||
| 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-20220704084225-05e143d24a9e/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.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= | ||||
| golang.org/x/sys v0.9.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.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= | ||||
| golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= | ||||
| 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= | ||||
| @@ -226,12 +274,14 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 | ||||
| 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 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= | ||||
| gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
| gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= | ||||
| 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 h1:XfR1dOYubytKy4Shzc2LHrrGhU0lDCfDGG1yLPmpgsI= | ||||
| 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= | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
| @@ -38,13 +41,22 @@ func (h *ManagerHandler) Login(c *gin.Context) { | ||||
| 	} | ||||
| 	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)), | ||||
| 		}) | ||||
| 		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 | ||||
| 		if _, err := h.redis.Set(context.Background(), "users/"+manager.Username, 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 +64,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") | ||||
| 	token := c.GetHeader(types.AdminAuthHeader) | ||||
| 	if _, err := h.redis.Del(c, token).Result(); err != nil { | ||||
| 		logger.Error("error with delete session: ", err) | ||||
| 	} else { | ||||
| 		resp.SUCCESS(c) | ||||
| 	} | ||||
| @@ -64,9 +74,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) | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -22,9 +22,10 @@ func NewDashboardHandler(app *core.AppServer, db *gorm.DB) *DashboardHandler { | ||||
| } | ||||
|  | ||||
| type statsVo struct { | ||||
| 	Users  int64 `json:"users"` | ||||
| 	Chats  int64 `json:"chats"` | ||||
| 	Tokens int64 `json:"tokens"` | ||||
| 	Users   int64   `json:"users"` | ||||
| 	Chats   int64   `json:"chats"` | ||||
| 	Tokens  int64   `json:"tokens"` | ||||
| 	Rewards float64 `json:"rewards"` | ||||
| } | ||||
|  | ||||
| func (h *DashboardHandler) Stats(c *gin.Context) { | ||||
| @@ -47,9 +48,16 @@ func (h *DashboardHandler) Stats(c *gin.Context) { | ||||
|  | ||||
| 	// tokens took stats | ||||
| 	var tokenCount int64 | ||||
| 	res = h.db.Model(&model.HistoryMessage{}).Select("sum(tokens) as tokens_total").Where("created_at > ?", zeroTime).Scan(&tokenCount) | ||||
| 	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) | ||||
| } | ||||
|   | ||||
| @@ -24,7 +24,7 @@ func NewRewardHandler(app *core.AppServer, db *gorm.DB) *RewardHandler { | ||||
|  | ||||
| func (h *RewardHandler) List(c *gin.Context) { | ||||
| 	var items []model.Reward | ||||
| 	res := h.db.Find(&items) | ||||
| 	res := h.db.Order("id DESC").Find(&items) | ||||
| 	var rewards = make([]vo.Reward, 0) | ||||
| 	if res.Error == nil { | ||||
| 		userIds := make([]uint, 0) | ||||
| @@ -46,7 +46,7 @@ func (h *RewardHandler) List(c *gin.Context) { | ||||
| 			} | ||||
|  | ||||
| 			r.Id = v.Id | ||||
| 			r.Username = userMap[v.UserId].Username | ||||
| 			r.Username = userMap[v.UserId].Mobile | ||||
| 			r.CreatedAt = v.CreatedAt.Unix() | ||||
| 			r.UpdatedAt = v.UpdatedAt.Unix() | ||||
| 			rewards = append(rewards, r) | ||||
|   | ||||
| @@ -8,8 +8,6 @@ import ( | ||||
| 	"chatplus/store/vo" | ||||
| 	"chatplus/utils" | ||||
| 	"chatplus/utils/resp" | ||||
| 	"fmt" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
| @@ -30,7 +28,6 @@ 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") | ||||
| 	username := h.GetTrim(c, "username") | ||||
|  | ||||
| 	offset := (page - 1) * pageSize | ||||
| 	var items []model.User | ||||
| @@ -41,9 +38,6 @@ func (h *UserHandler) List(c *gin.Context) { | ||||
| 	if mobile != "" { | ||||
| 		session = session.Where("mobile LIKE ?", "%"+mobile+"%") | ||||
| 	} | ||||
| 	if username != "" { | ||||
| 		session = session.Where("username LIKE ?", "%"+username+"%") | ||||
| 	} | ||||
|  | ||||
| 	session.Model(&model.User{}).Count(&total) | ||||
| 	res := session.Offset(offset).Limit(pageSize).Find(&items) | ||||
| @@ -68,11 +62,10 @@ func (h *UserHandler) List(c *gin.Context) { | ||||
| func (h *UserHandler) Save(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		Id          uint     `json:"id"` | ||||
| 		Username    string   `json:"username"` | ||||
| 		Password    string   `json:"password"` | ||||
| 		Mobile      string   `json:"mobile"` | ||||
| 		Nickname    string   `json:"nickname"` | ||||
| 		Calls       int      `json:"calls"` | ||||
| 		ImgCalls    int      `json:"img_calls"` | ||||
| 		ChatRoles   []string `json:"chat_roles"` | ||||
| 		ExpiredTime string   `json:"expired_time"` | ||||
| 		Status      bool     `json:"status"` | ||||
| @@ -88,9 +81,9 @@ func (h *UserHandler) Save(c *gin.Context) { | ||||
| 		user.Id = data.Id | ||||
| 		// 此处需要用 map 更新,用结构体无法更新 0 值 | ||||
| 		res = h.db.Model(&user).Updates(map[string]interface{}{ | ||||
| 			"nickname":        data.Nickname, | ||||
| 			"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), | ||||
| @@ -98,22 +91,19 @@ func (h *UserHandler) Save(c *gin.Context) { | ||||
| 	} else { | ||||
| 		salt := utils.RandString(8) | ||||
| 		u := model.User{ | ||||
| 			Username:    data.Username, | ||||
| 			Mobile:      data.Mobile, | ||||
| 			Password:    utils.GenPassword(data.Password, salt), | ||||
| 			Nickname:    fmt.Sprintf("极客学长@%d", utils.RandomNumber(5)), | ||||
| 			Avatar:      "/images/avatar/user.png", | ||||
| 			Salt:        salt, | ||||
| 			Status:      true, | ||||
| 			Mobile:      data.Mobile, | ||||
| 			ChatRoles:   utils.JsonEncode(data.ChatRoles), | ||||
| 			ExpiredTime: utils.Str2stamp(data.ExpiredTime), | ||||
| 			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, | ||||
| 		} | ||||
| @@ -202,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 | ||||
|   | ||||
							
								
								
									
										301
									
								
								api/handler/azure_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										301
									
								
								api/handler/azure_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,301 @@ | ||||
| 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] | ||||
| 				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 { | ||||
| 						key := utils.Sha256(data) | ||||
| 						logger.Debug(data, ",", key) | ||||
| 						// add task for MidJourney | ||||
| 						h.App.MjTaskClients.Put(key, ws) | ||||
| 						task := types.MjTask{ | ||||
| 							UserId: userVo.Id, | ||||
| 							RoleId: role.Id, | ||||
| 							Icon:   "/images/avatar/mid_journey.png", | ||||
| 							ChatId: session.ChatId, | ||||
| 						} | ||||
| 						err := h.leveldb.Put(types.TaskStorePrefix+key, task) | ||||
| 						if err != nil { | ||||
| 							logger.Error("error with store MidJourney task: ", err) | ||||
| 						} | ||||
| 						content = fmt.Sprintf("绘画提示词:%s 已推送任务到 MidJourney 机器人,请耐心等待任务执行...", data) | ||||
|  | ||||
| 						// 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) | ||||
| 				} | ||||
|  | ||||
| 				// for reply | ||||
| 				// 计算本次对话消耗的总 token 数量 | ||||
| 				var replyToken = 0 | ||||
| 				if functionCall { // 函数名 + 参数 token | ||||
| 					tokens, _ := utils.CalcTokens(functionName, req.Model) | ||||
| 					replyToken += tokens | ||||
| 					tokens, _ = utils.CalcTokens(utils.InterfaceToString(arguments), req.Model) | ||||
| 					replyToken += tokens | ||||
| 				} else { | ||||
| 					replyToken, _ = utils.CalcTokens(message.Content, req.Model) | ||||
| 				} | ||||
|  | ||||
| 				historyReplyMsg := model.HistoryMessage{ | ||||
| 					UserId:     userVo.Id, | ||||
| 					ChatId:     session.ChatId, | ||||
| 					RoleId:     role.Id, | ||||
| 					Type:       types.ReplyMsg, | ||||
| 					Icon:       role.Icon, | ||||
| 					Content:    message.Content, | ||||
| 					Tokens:     replyToken, | ||||
| 					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) | ||||
| 				} | ||||
|  | ||||
| 				// 计算本次对话消耗的总 token 数量 | ||||
| 				var totalTokens = 0 | ||||
| 				if functionCall { // prompt + 函数名 + 参数 token | ||||
| 					totalTokens = promptToken + replyToken | ||||
| 				} else { | ||||
| 					totalTokens = replyToken + getTotalTokens(req) | ||||
| 				} | ||||
| 				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 | ||||
| } | ||||
| @@ -3,7 +3,7 @@ package handler | ||||
| import ( | ||||
| 	"chatplus/core" | ||||
| 	logger2 "chatplus/logger" | ||||
| 	"strconv" | ||||
| 	"chatplus/utils" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| @@ -20,47 +20,23 @@ 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)) | ||||
| } | ||||
|  | ||||
| func boolValue(str string) bool { | ||||
| 	value, err := strconv.ParseBool(str) | ||||
| 	if err != nil { | ||||
| 		return false | ||||
| 	} | ||||
| 	return value | ||||
| 	return utils.BoolValue(c.PostForm(key)) | ||||
| } | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| package handler | ||||
|  | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"bytes" | ||||
| 	"chatplus/core" | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/store" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/store/vo" | ||||
| 	"chatplus/utils" | ||||
| @@ -13,27 +13,27 @@ import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/go-redis/redis/v8" | ||||
| 	"github.com/gorilla/websocket" | ||||
| 	"gorm.io/gorm" | ||||
| 	"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 | ||||
| 	db      *gorm.DB | ||||
| 	leveldb *store.LevelDB | ||||
| 	redis   *redis.Client | ||||
| } | ||||
|  | ||||
| func NewChatHandler(app *core.AppServer, db *gorm.DB) *ChatHandler { | ||||
| 	handler := ChatHandler{db: db} | ||||
| 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 | ||||
| } | ||||
| @@ -47,27 +47,34 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) { | ||||
| 		logger.Error(err) | ||||
| 		return | ||||
| 	} | ||||
| 	// 设置读写超时时间 | ||||
| 	_ = ws.SetWriteDeadline(time.Now().Add(300 * time.Second)) | ||||
| 	_ = ws.SetReadDeadline(time.Now().Add(300 * time.Second)) | ||||
|  | ||||
| 	sessionId := c.Query("session_id") | ||||
| 	roleId := h.GetInt(c, "role_id", 0) | ||||
| 	chatId := c.Query("chat_id") | ||||
| 	chatModel := c.Query("model") | ||||
| 	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.SessionId == "" { | ||||
| 	if session == nil { | ||||
| 		user, err := utils.GetLoginUser(c, h.db) | ||||
| 		if err != nil { | ||||
| 			logger.Info("用户未登录") | ||||
| 			c.Abort() | ||||
| 			return | ||||
| 		} | ||||
| 		session = types.ChatSession{ | ||||
| 		session = &types.ChatSession{ | ||||
| 			SessionId: sessionId, | ||||
| 			ClientIP:  c.ClientIP(), | ||||
| 			Username:  user.Username, | ||||
| 			Username:  user.Mobile, | ||||
| 			UserId:    user.Id, | ||||
| 		} | ||||
| 		h.App.ChatSession.Put(sessionId, session) | ||||
| @@ -75,20 +82,22 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) { | ||||
|  | ||||
| 	// use old chat data override the chat model and role ID | ||||
| 	var chat model.ChatItem | ||||
| 	res := h.db.Where("chat_id=?", chatId).First(&chat) | ||||
| 	res = h.db.Where("chat_id=?", chatId).First(&chat) | ||||
| 	if res.Error == nil { | ||||
| 		chatModel = chat.Model | ||||
| 		chatModel.Id = chat.ModelId | ||||
| 		roleId = int(chat.RoleId) | ||||
| 	} | ||||
|  | ||||
| 	session.ChatId = chatId | ||||
| 	session.Model = chatModel | ||||
| 	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.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, "当前聊天角色不存在或者未启用,连接已关闭!!!") | ||||
| 		utils.ReplyMessage(client, "当前聊天角色不存在或者未启用,连接已关闭!!!") | ||||
| 		c.Abort() | ||||
| 		return | ||||
| 	} | ||||
| @@ -98,7 +107,7 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) { | ||||
| 	h.db.Where("marker", "chat").First(&config) | ||||
| 	err = utils.JsonDecode(config.Config, &chatConfig) | ||||
| 	if err != nil { | ||||
| 		replyMessage(client, "加载系统配置失败,连接已关闭!!!") | ||||
| 		utils.ReplyMessage(client, "加载系统配置失败,连接已关闭!!!") | ||||
| 		c.Abort() | ||||
| 		return | ||||
| 	} | ||||
| @@ -107,7 +116,7 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) { | ||||
| 	h.App.ChatClients.Put(sessionId, client) | ||||
| 	go func() { | ||||
| 		for { | ||||
| 			_, message, err := client.Receive() | ||||
| 			_, msg, err := client.Receive() | ||||
| 			if err != nil { | ||||
| 				logger.Error(err) | ||||
| 				client.Close() | ||||
| @@ -115,16 +124,18 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) { | ||||
| 				h.App.ReqCancelFunc.Delete(sessionId) | ||||
| 				return | ||||
| 			} | ||||
| 			logger.Info("Receive a message: ", string(message)) | ||||
| 			//replyMessage(client, "这是一条测试消息!") | ||||
|  | ||||
| 			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, string(message), client) | ||||
| 			err = h.sendMessage(ctx, session, chatRole, message, client) | ||||
| 			if err != nil { | ||||
| 				logger.Error(err) | ||||
| 			} else { | ||||
| 				replyChunkMessage(client, types.WsMessage{Type: types.WsEnd}) | ||||
| 				utils.ReplyChunkMessage(client, types.WsMessage{Type: types.WsEnd}) | ||||
| 				logger.Info("回答完毕: " + string(message)) | ||||
| 			} | ||||
|  | ||||
| @@ -132,14 +143,17 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) { | ||||
| 	}() | ||||
| } | ||||
|  | ||||
| // 将消息发送给 ChatGPT 并获取结果,通过 WebSocket 推送到客户端 | ||||
| func (h *ChatHandler) sendMessage(ctx context.Context, session types.ChatSession, role model.ChatRole, prompt string, ws types.Client) error { | ||||
| 	promptCreatedAt := time.Now() // 记录提问时间 | ||||
| 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 { | ||||
| 		replyMessage(ws, "非法用户,请联系管理员!") | ||||
| 		utils.ReplyMessage(ws, "非法用户,请联系管理员!") | ||||
| 		return res.Error | ||||
| 	} | ||||
| 	var userVo vo.User | ||||
| @@ -150,33 +164,51 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session types.ChatSession | ||||
| 	} | ||||
|  | ||||
| 	if userVo.Status == false { | ||||
| 		replyMessage(ws, "您的账号已经被禁用,如果疑问,请联系管理员!") | ||||
| 		replyMessage(ws, "") | ||||
| 		utils.ReplyMessage(ws, "您的账号已经被禁用,如果疑问,请联系管理员!") | ||||
| 		utils.ReplyMessage(ws, "") | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	if userVo.Calls <= 0 && userVo.ChatConfig.ApiKey == "" { | ||||
| 		replyMessage(ws, "您的对话次数已经用尽,请联系管理员或者点击左下角菜单加入众筹获得100次对话!") | ||||
| 		replyMessage(ws, "") | ||||
| 	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() { | ||||
| 		replyMessage(ws, "您的账号已经过期,请联系管理员!") | ||||
| 		replyMessage(ws, "") | ||||
| 		utils.ReplyMessage(ws, "您的账号已经过期,请联系管理员!") | ||||
| 		utils.ReplyMessage(ws, "") | ||||
| 		return nil | ||||
| 	} | ||||
| 	var req = types.ApiRequest{ | ||||
| 		Model:       session.Model, | ||||
| 		Temperature: userVo.ChatConfig.Temperature, | ||||
| 		MaxTokens:   userVo.ChatConfig.MaxTokens, | ||||
| 		Stream:      true, | ||||
| 		Functions:   types.InnerFunctions, | ||||
| 		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 userVo.ChatConfig.EnableContext { | ||||
| 	if h.App.ChatConfig.EnableContext { | ||||
| 		if h.App.ChatContexts.Has(session.ChatId) { | ||||
| 			chatCtx = h.App.ChatContexts.Get(session.ChatId) | ||||
| 		} else { | ||||
| @@ -207,7 +239,7 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session types.ChatSession | ||||
| 				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] { | ||||
| 						if tokens+msg.Tokens >= types.ModelToTokens[session.Model.Value] { | ||||
| 							break | ||||
| 						} | ||||
| 						tokens += msg.Tokens | ||||
| @@ -231,334 +263,30 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session types.ChatSession | ||||
| 		"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 if strings.Contains(err.Error(), "no available key") { | ||||
| 			replyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY🔑,您可以导入自己的 API KEY🔑 继续使用!🙏🙏🙏") | ||||
| 			return nil | ||||
| 		} else { | ||||
| 			logger.Error(err) | ||||
| 		} | ||||
|  | ||||
| 		replyMessage(ws, ErrorMsg) | ||||
| 		replyMessage(ws, "") | ||||
| 		return err | ||||
| 	} else { | ||||
| 		defer response.Body.Close() | ||||
| 	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) | ||||
| 	} | ||||
|  | ||||
| 	contentType := response.Header.Get("Content-Type") | ||||
| 	if strings.Contains(contentType, "text/event-stream") { | ||||
| 		if true { | ||||
| 			replyCreatedAt := time.Now() | ||||
| 			// 循环读取 Chunk 消息 | ||||
| 			var message = types.Message{} | ||||
| 			var contents = make([]string, 0) | ||||
| 			var functionCall = false | ||||
| 			var functionName string | ||||
| 			var arguments = make([]string, 0) | ||||
| 			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 if err != io.EOF { | ||||
| 						logger.Error("信息读取出错:", err) | ||||
| 					} | ||||
| 					break | ||||
| 				} | ||||
| 				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) | ||||
| 					replyMessage(ws, ErrorMsg) | ||||
| 					replyMessage(ws, "") | ||||
| 					break | ||||
| 				} | ||||
|  | ||||
| 				fun := responseBody.Choices[0].Delta.FunctionCall | ||||
| 				if functionCall && fun.Name == "" { | ||||
| 					arguments = append(arguments, fun.Arguments) | ||||
| 					continue | ||||
| 				} | ||||
|  | ||||
| 				if !utils.IsEmptyValue(fun) { | ||||
| 					functionCall = true | ||||
| 					functionName = fun.Name | ||||
| 					f := h.App.Functions[functionName] | ||||
| 					replyChunkMessage(ws, types.WsMessage{Type: types.WsStart}) | ||||
| 					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 | ||||
| 					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)) | ||||
| 					replyChunkMessage(ws, types.WsMessage{ | ||||
| 						Type:    types.WsMiddle, | ||||
| 						Content: utils.InterfaceToString(responseBody.Choices[0].Delta.Content), | ||||
| 					}) | ||||
| 				} | ||||
| 			} // end for | ||||
|  | ||||
| 			if functionCall { // 调用函数完成任务 | ||||
| 				logger.Info(functionName) | ||||
| 				logger.Info(arguments) | ||||
| 				f := h.App.Functions[functionName] | ||||
| 				data, err := f.Invoke(arguments) | ||||
| 				if err != nil { | ||||
| 					msg := "调用函数出错:" + err.Error() | ||||
| 					replyChunkMessage(ws, types.WsMessage{ | ||||
| 						Type:    types.WsMiddle, | ||||
| 						Content: msg, | ||||
| 					}) | ||||
| 					contents = append(contents, msg) | ||||
| 				} else { | ||||
| 					replyChunkMessage(ws, types.WsMessage{ | ||||
| 						Type:    types.WsMiddle, | ||||
| 						Content: data, | ||||
| 					}) | ||||
| 					contents = append(contents, data) | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			// 消息发送成功 | ||||
| 			if len(contents) > 0 { | ||||
| 				// 更新用户的对话次数 | ||||
| 				if userVo.ChatConfig.ApiKey == "" { // 如果用户使用的是自己绑定的 API KEY 则不扣减对话次数 | ||||
| 					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 && functionCall == false { | ||||
| 					chatCtx = append(chatCtx, useMsg)  // 提问消息 | ||||
| 					chatCtx = append(chatCtx, message) // 回复消息 | ||||
| 					h.App.ChatContexts.Put(session.ChatId, chatCtx) | ||||
| 				} | ||||
|  | ||||
| 				// 追加聊天记录 | ||||
| 				if userVo.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:       user.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) | ||||
| 					} | ||||
|  | ||||
| 					// for reply | ||||
| 					// 计算本次对话消耗的总 token 数量 | ||||
| 					var replyToken = 0 | ||||
| 					if functionCall { // 函数名 + 参数 token | ||||
| 						tokens, _ := utils.CalcTokens(functionName, req.Model) | ||||
| 						replyToken += tokens | ||||
| 						tokens, _ = utils.CalcTokens(utils.InterfaceToString(arguments), req.Model) | ||||
| 						replyToken += tokens | ||||
| 					} else { | ||||
| 						replyToken, _ = utils.CalcTokens(message.Content, req.Model) | ||||
| 					} | ||||
|  | ||||
| 					historyReplyMsg := model.HistoryMessage{ | ||||
| 						UserId:     userVo.Id, | ||||
| 						ChatId:     session.ChatId, | ||||
| 						RoleId:     role.Id, | ||||
| 						Type:       types.ReplyMsg, | ||||
| 						Icon:       role.Icon, | ||||
| 						Content:    message.Content, | ||||
| 						Tokens:     replyToken, | ||||
| 						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) | ||||
| 					} | ||||
|  | ||||
| 					// 计算本次对话消耗的总 token 数量 | ||||
| 					var totalTokens = 0 | ||||
| 					if functionCall { // prompt + 函数名 + 参数 token | ||||
| 						totalTokens = promptToken + replyToken | ||||
| 					} else { | ||||
| 						totalTokens = replyToken + getTotalTokens(req) | ||||
| 					} | ||||
| 					//replyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: fmt.Sprintf("\n\n `本轮对话共消耗 Token 数量: %d`", totalTokens+11)}) | ||||
| 					if userVo.ChatConfig.ApiKey != "" { // 调用自己的 API KEY 不计算 token 消耗 | ||||
| 						h.db.Model(&user).UpdateColumn("tokens", gorm.Expr("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.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") { | ||||
| 			logger.Error(res.Error.Message) | ||||
| 			replyMessage(ws, "当前会话上下文长度超出限制,已为您清空会话上下文!") | ||||
| 			h.App.ChatContexts.Delete(session.ChatId) | ||||
| 			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.Config.ProxyURL | ||||
| 	if proxyURL == "" { | ||||
| 		client = &http.Client{} | ||||
| 	} else { // 使用代理 | ||||
| 		proxy, _ := url.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}) | ||||
| 	return fmt.Errorf("not supported platform: %s", session.Model.Platform) | ||||
| } | ||||
|  | ||||
| // Tokens 统计 token 数量 | ||||
| func (h *ChatHandler) Tokens(c *gin.Context) { | ||||
| 	text := c.Query("text") | ||||
| 	md := c.Query("model") | ||||
| 	tokens, err := utils.CalcTokens(text, md) | ||||
| 	var data struct { | ||||
| 		Text  string `json:"text"` | ||||
| 		Model string `json:"model"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	tokens, err := utils.CalcTokens(data.Text, data.Model) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| @@ -596,3 +324,75 @@ func (h *ChatHandler) StopGenerate(c *gin.Context) { | ||||
| 	} | ||||
| 	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) | ||||
| } | ||||
|   | ||||
| @@ -6,48 +6,11 @@ import ( | ||||
| 	"chatplus/store/vo" | ||||
| 	"chatplus/utils" | ||||
| 	"chatplus/utils/resp" | ||||
| 	"gorm.io/gorm" | ||||
|  | ||||
| 	"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 { | ||||
| @@ -69,41 +32,12 @@ func (h *ChatHandler) Update(c *gin.Context) { | ||||
| 	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) | ||||
| 	res := h.db.Where("chat_id = ?", chatId).Find(&items) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "No history message") | ||||
| 		return | ||||
| @@ -137,18 +71,30 @@ func (h *ChatHandler) Clear(c *gin.Context) { | ||||
| 		resp.ERROR(c, "No chats found") | ||||
| 		return | ||||
| 	} | ||||
| 	// 清空聊天记录 | ||||
|  | ||||
| 	var chatIds = make([]string, 0) | ||||
| 	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) | ||||
| 		} | ||||
| 		chatIds = append(chatIds, chat.ChatId) | ||||
| 		// 清空会话上下文 | ||||
| 		h.App.ChatContexts.Delete(chat.ChatId) | ||||
| 	} | ||||
| 	// 删除所有的会话记录 | ||||
| 	res = h.db.Where("user_id = ?", user.Id).Delete(&model.ChatItem{}) | ||||
| 	if res.Error != nil { | ||||
| 	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 | ||||
| 	} | ||||
|   | ||||
							
								
								
									
										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 | ||||
|   | ||||
							
								
								
									
										239
									
								
								api/handler/chatglm_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										239
									
								
								api/handler/chatglm_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,239 @@ | ||||
| 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 数量 | ||||
| 				var replyToken = 0 | ||||
| 				replyToken, _ = utils.CalcTokens(message.Content, req.Model) | ||||
|  | ||||
| 				historyReplyMsg := model.HistoryMessage{ | ||||
| 					UserId:     userVo.Id, | ||||
| 					ChatId:     session.ChatId, | ||||
| 					RoleId:     role.Id, | ||||
| 					Type:       types.ReplyMsg, | ||||
| 					Icon:       role.Icon, | ||||
| 					Content:    message.Content, | ||||
| 					Tokens:     replyToken, | ||||
| 					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) | ||||
| 				} | ||||
|  | ||||
| 				// 计算本次对话消耗的总 token 数量 | ||||
| 				var totalTokens = 0 | ||||
| 				totalTokens = replyToken + getTotalTokens(req) | ||||
| 				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 | ||||
| } | ||||
							
								
								
									
										244
									
								
								api/handler/mj_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										244
									
								
								api/handler/mj_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,244 @@ | ||||
| package handler | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core" | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/service/function" | ||||
| 	"chatplus/service/oss" | ||||
| 	"chatplus/store" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/utils" | ||||
| 	"chatplus/utils/resp" | ||||
| 	"encoding/base64" | ||||
| 	"fmt" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| 	"sync" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| type TaskStatus string | ||||
|  | ||||
| const ( | ||||
| 	Start    = TaskStatus("Started") | ||||
| 	Running  = TaskStatus("Running") | ||||
| 	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 | ||||
| 	leveldb         *store.LevelDB | ||||
| 	db              *gorm.DB | ||||
| 	mjFunc          function.FuncMidJourney | ||||
| 	uploaderManager *oss.UploaderManager | ||||
| 	lock            sync.Mutex | ||||
| } | ||||
|  | ||||
| func NewMidJourneyHandler( | ||||
| 	app *core.AppServer, | ||||
| 	leveldb *store.LevelDB, | ||||
| 	db *gorm.DB, | ||||
| 	manager *oss.UploaderManager, | ||||
| 	functions map[string]function.Function) *MidJourneyHandler { | ||||
| 	h := MidJourneyHandler{ | ||||
| 		leveldb:         leveldb, | ||||
| 		db:              db, | ||||
| 		uploaderManager: manager, | ||||
| 		lock:            sync.Mutex{}, | ||||
| 		mjFunc:          functions[types.FuncMidJourney].(function.FuncMidJourney)} | ||||
| 	h.App = app | ||||
| 	return &h | ||||
| } | ||||
|  | ||||
| func (h *MidJourneyHandler) Notify(c *gin.Context) { | ||||
| 	token := c.GetHeader("Authorization") | ||||
| 	if token != h.App.Config.ExtConfig.Token { | ||||
| 		resp.NotAuth(c) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var data 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"` | ||||
| 		Key         string     `json:"key"` | ||||
| 	} | ||||
| 	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() | ||||
| 	 | ||||
| 	// the job is saved | ||||
| 	var job model.MidJourneyJob | ||||
| 	res := h.db.Where("message_id = ?", data.MessageId).First(&job) | ||||
| 	if res.Error == nil { | ||||
| 		resp.SUCCESS(c) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	data.Key = utils.Sha256(data.Prompt) | ||||
| 	//logger.Info(data.Prompt, ",", key) | ||||
| 	if data.Status == Finished { | ||||
| 		var task types.MjTask | ||||
| 		err := h.leveldb.Get(types.TaskStorePrefix+data.Key, &task) | ||||
| 		if err != nil { | ||||
| 			logger.Error("error with get MidJourney task: ", err) | ||||
| 			resp.SUCCESS(c) | ||||
| 			return | ||||
| 		} | ||||
| 		// download image | ||||
| 		imgURL, err := h.uploaderManager.GetUploadHandler().PutImg(data.Image.URL) | ||||
| 		if err != nil { | ||||
| 			logger.Error("error with download image: ", err) | ||||
| 			resp.SUCCESS(c) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		data.Image.URL = imgURL | ||||
| 		message := model.HistoryMessage{ | ||||
| 			UserId:     task.UserId, | ||||
| 			ChatId:     task.ChatId, | ||||
| 			RoleId:     task.RoleId, | ||||
| 			Type:       types.MjMsg, | ||||
| 			Icon:       task.Icon, | ||||
| 			Content:    utils.JsonEncode(data), | ||||
| 			Tokens:     0, | ||||
| 			UseContext: false, | ||||
| 		} | ||||
| 		res := h.db.Create(&message) | ||||
| 		if res.Error != nil { | ||||
| 			logger.Error("error with save chat history message: ", res.Error) | ||||
| 		} | ||||
|  | ||||
| 		// save the job | ||||
| 		job.UserId = task.UserId | ||||
| 		job.ChatId = task.ChatId | ||||
| 		job.MessageId = data.MessageId | ||||
| 		job.ReferenceId = data.ReferenceId | ||||
| 		job.Content = data.Content | ||||
| 		job.Prompt = data.Prompt | ||||
| 		job.Image = utils.JsonEncode(data.Image) | ||||
| 		job.Hash = data.Image.Hash | ||||
| 		job.CreatedAt = time.Now() | ||||
| 		res = h.db.Create(&job) | ||||
| 		if res.Error != nil { | ||||
| 			logger.Error("error with save MidJourney Job: ", res.Error) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// 推送消息到客户端 | ||||
| 	wsClient := h.App.MjTaskClients.Get(data.Key) | ||||
| 	if wsClient == nil { // 客户端断线,则丢弃 | ||||
| 		logger.Errorf("Client is offline: %+v", data) | ||||
| 		resp.SUCCESS(c, "Client is offline") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if data.Status == Finished { | ||||
| 		utils.ReplyChunkMessage(wsClient, types.WsMessage{Type: types.WsMjImg, Content: data}) | ||||
| 		utils.ReplyChunkMessage(wsClient, types.WsMessage{Type: types.WsEnd}) | ||||
| 		// delete client | ||||
| 		h.App.MjTaskClients.Delete(data.Key) | ||||
| 	} 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}) | ||||
| 	} | ||||
| 	resp.SUCCESS(c, "SUCCESS") | ||||
| } | ||||
|  | ||||
| type reqVo struct { | ||||
| 	Index       int32  `json:"index"` | ||||
| 	MessageId   string `json:"message_id"` | ||||
| 	MessageHash string `json:"message_hash"` | ||||
| 	SessionId   string `json:"session_id"` | ||||
| 	Key         string `json:"key"` | ||||
| 	Prompt      string `json:"prompt"` | ||||
| } | ||||
|  | ||||
| // 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 == "" || | ||||
| 		data.Key == "" { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 	wsClient := h.App.ChatClients.Get(data.SessionId) | ||||
| 	if wsClient == nil { | ||||
| 		resp.ERROR(c, "No Websocket client online") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err := h.mjFunc.Upscale(function.MjUpscaleReq{ | ||||
| 		Index:       data.Index, | ||||
| 		MessageId:   data.MessageId, | ||||
| 		MessageHash: data.MessageHash, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	content := fmt.Sprintf("**%s** 已推送 Upscale 任务到 MidJourney 机器人,请耐心等待任务执行...", data.Prompt) | ||||
| 	utils.ReplyMessage(wsClient, content) | ||||
| 	if h.App.MjTaskClients.Get(data.Key) == nil { | ||||
| 		h.App.MjTaskClients.Put(data.Key, wsClient) | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| func (h *MidJourneyHandler) Variation(c *gin.Context) { | ||||
| 	var data reqVo | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil || | ||||
| 		data.SessionId == "" || | ||||
| 		data.Key == "" { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 	wsClient := h.App.ChatClients.Get(data.SessionId) | ||||
| 	if wsClient == nil { | ||||
| 		resp.ERROR(c, "No Websocket client online") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err := h.mjFunc.Variation(function.MjVariationReq{ | ||||
| 		Index:       data.Index, | ||||
| 		MessageId:   data.MessageId, | ||||
| 		MessageHash: data.MessageHash, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	content := fmt.Sprintf("**%s** 已推送 Variation 任务到 MidJourney 机器人,请耐心等待任务执行...", data.Prompt) | ||||
| 	utils.ReplyMessage(wsClient, content) | ||||
| 	if h.App.MjTaskClients.Get(data.Key) == nil { | ||||
| 		h.App.MjTaskClients.Put(data.Key, wsClient) | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
							
								
								
									
										308
									
								
								api/handler/openai_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										308
									
								
								api/handler/openai_handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,308 @@ | ||||
| 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] | ||||
| 				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 { | ||||
| 						key := utils.Sha256(data) | ||||
| 						logger.Debug(data, ",", key) | ||||
| 						// add task for MidJourney | ||||
| 						h.App.MjTaskClients.Put(key, ws) | ||||
| 						task := types.MjTask{ | ||||
| 							UserId: userVo.Id, | ||||
| 							RoleId: role.Id, | ||||
| 							Icon:   "/images/avatar/mid_journey.png", | ||||
| 							ChatId: session.ChatId, | ||||
| 						} | ||||
| 						err := h.leveldb.Put(types.TaskStorePrefix+key, task) | ||||
| 						if err != nil { | ||||
| 							logger.Error("error with store MidJourney task: ", err) | ||||
| 						} | ||||
| 						content = fmt.Sprintf("绘画提示词:%s 已推送任务到 MidJourney 机器人,请耐心等待任务执行...", data) | ||||
|  | ||||
| 						// 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) | ||||
| 				} | ||||
|  | ||||
| 				// for reply | ||||
| 				// 计算本次对话消耗的总 token 数量 | ||||
| 				var replyToken = 0 | ||||
| 				if functionCall { // 函数名 + 参数 token | ||||
| 					tokens, _ := utils.CalcTokens(functionName, req.Model) | ||||
| 					replyToken += tokens | ||||
| 					tokens, _ = utils.CalcTokens(utils.InterfaceToString(arguments), req.Model) | ||||
| 					replyToken += tokens | ||||
| 				} else { | ||||
| 					replyToken, _ = utils.CalcTokens(message.Content, req.Model) | ||||
| 				} | ||||
|  | ||||
| 				historyReplyMsg := model.HistoryMessage{ | ||||
| 					UserId:     userVo.Id, | ||||
| 					ChatId:     session.ChatId, | ||||
| 					RoleId:     role.Id, | ||||
| 					Type:       types.ReplyMsg, | ||||
| 					Icon:       role.Icon, | ||||
| 					Content:    message.Content, | ||||
| 					Tokens:     replyToken, | ||||
| 					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) | ||||
| 				} | ||||
|  | ||||
| 				// 计算本次对话消耗的总 token 数量 | ||||
| 				var totalTokens = 0 | ||||
| 				if functionCall { // prompt + 函数名 + 参数 token | ||||
| 					totalTokens = promptToken + replyToken | ||||
| 				} else { | ||||
| 					totalTokens = replyToken + getTotalTokens(req) | ||||
| 				} | ||||
| 				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 | ||||
| } | ||||
| @@ -8,6 +8,7 @@ import ( | ||||
| 	"chatplus/utils/resp" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| type RewardHandler struct { | ||||
| @@ -21,6 +22,50 @@ func NewRewardHandler(server *core.AppServer, db *gorm.DB) *RewardHandler { | ||||
| 	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 { | ||||
| @@ -31,6 +76,9 @@ func (h *RewardHandler) Verify(c *gin.Context) { | ||||
| 		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 { | ||||
|   | ||||
| @@ -14,13 +14,13 @@ const CodeStorePrefix = "/verify/codes/" | ||||
|  | ||||
| type SmsHandler struct { | ||||
| 	BaseHandler | ||||
| 	db      *store.LevelDB | ||||
| 	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{db: db, sms: sms, captcha: captcha} | ||||
| 	handler := &SmsHandler{leveldb: db, sms: sms, captcha: captcha} | ||||
| 	handler.App = app | ||||
| 	return handler | ||||
| } | ||||
| @@ -50,7 +50,7 @@ func (h *SmsHandler) SendCode(c *gin.Context) { | ||||
| 	} | ||||
|  | ||||
| 	// 存储验证码,等待后面注册验证 | ||||
| 	err = h.db.Put(CodeStorePrefix+data.Mobile, code) | ||||
| 	err = h.leveldb.Put(CodeStorePrefix+data.Mobile, code) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, "验证码保存失败") | ||||
| 		return | ||||
| @@ -66,5 +66,5 @@ type statusVo struct { | ||||
|  | ||||
| // Status check if the message service is enabled | ||||
| func (h *SmsHandler) Status(c *gin.Context) { | ||||
| 	resp.SUCCESS(c, statusVo{EnabledMsgService: h.App.Config.EnabledMsgService, EnabledRegister: h.App.SysConfig.EnabledRegister}) | ||||
| 	resp.SUCCESS(c, statusVo{EnabledMsgService: h.App.SysConfig.EnabledMsgService, EnabledRegister: h.App.SysConfig.EnabledRegister}) | ||||
| } | ||||
|   | ||||
| @@ -2,66 +2,30 @@ package handler | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core" | ||||
| 	"chatplus/service/oss" | ||||
| 	"chatplus/utils/resp" | ||||
| 	"fmt" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| type UploadHandler struct { | ||||
| 	BaseHandler | ||||
| 	db *gorm.DB | ||||
| 	db              *gorm.DB | ||||
| 	uploaderManager *oss.UploaderManager | ||||
| } | ||||
|  | ||||
| func NewUploadHandler(app *core.AppServer, db *gorm.DB) *UploadHandler { | ||||
| 	handler := &UploadHandler{db: db} | ||||
| 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) { | ||||
| 	file, err := c.FormFile("file") | ||||
| 	fileURL, err := h.uploaderManager.GetUploadHandler().PutFile(c, "file") | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, fmt.Sprintf("文件上传失败: %s", err.Error())) | ||||
| 		resp.ERROR(c, 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.Config.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.Config.StaticUrl, now.Year(), now.Month(), filename) | ||||
| 	resp.SUCCESS(c, fileURL) | ||||
| } | ||||
|   | ||||
| @@ -9,10 +9,11 @@ import ( | ||||
| 	"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" | ||||
| @@ -22,11 +23,17 @@ type UserHandler struct { | ||||
| 	BaseHandler | ||||
| 	db       *gorm.DB | ||||
| 	searcher *xdb.Searcher | ||||
| 	levelDB  *store.LevelDB | ||||
| 	leveldb  *store.LevelDB | ||||
| 	redis    *redis.Client | ||||
| } | ||||
|  | ||||
| func NewUserHandler(app *core.AppServer, db *gorm.DB, searcher *xdb.Searcher, levelDB *store.LevelDB) *UserHandler { | ||||
| 	handler := &UserHandler{db: db, searcher: searcher, levelDB: levelDB} | ||||
| 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 | ||||
| } | ||||
| @@ -35,20 +42,18 @@ func NewUserHandler(app *core.AppServer, db *gorm.DB, searcher *xdb.Searcher, le | ||||
| func (h *UserHandler) Register(c *gin.Context) { | ||||
| 	// parameters process | ||||
| 	var data struct { | ||||
| 		Username string `json:"username"` | ||||
| 		Password string `json:"password"` | ||||
| 		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 { | ||||
| @@ -58,11 +63,10 @@ func (h *UserHandler) Register(c *gin.Context) { | ||||
|  | ||||
| 	// 检查验证码 | ||||
| 	key := CodeStorePrefix + data.Mobile | ||||
| 	if h.App.Config.EnabledMsgService { | ||||
| 	if h.App.SysConfig.EnabledMsgService { | ||||
| 		var code int | ||||
| 		err := h.levelDB.Get(key, &code) | ||||
| 		err := h.leveldb.Get(key, &code) | ||||
| 		if err != nil || code != data.Code { | ||||
| 			logger.Info(code) | ||||
| 			resp.ERROR(c, "短信验证码错误") | ||||
| 			return | ||||
| 		} | ||||
| @@ -70,15 +74,9 @@ func (h *UserHandler) Register(c *gin.Context) { | ||||
|  | ||||
| 	// check if the username is exists | ||||
| 	var item model.User | ||||
| 	res := h.db.Where("username = ?", data.Username).First(&item) | ||||
| 	res := h.db.Where("mobile = ?", data.Mobile).First(&item) | ||||
| 	if res.RowsAffected > 0 { | ||||
| 		resp.ERROR(c, "用户名已存在") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	res = h.db.Where("mobile = ?", data.Mobile).First(&item) | ||||
| 	if res.RowsAffected > 0 { | ||||
| 		resp.ERROR(c, "该手机号码以及被注册,请更换其他手机号") | ||||
| 		resp.ERROR(c, "该手机号码已经被注册,请更换其他手机号") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| @@ -92,23 +90,21 @@ 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", | ||||
| 		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, | ||||
| 		Calls:    h.App.SysConfig.UserInitCalls, | ||||
| 		ImgCalls: h.App.SysConfig.InitImgCalls, | ||||
| 	} | ||||
| 	res = h.db.Create(&user) | ||||
| 	if res.Error != nil { | ||||
| @@ -117,8 +113,8 @@ func (h *UserHandler) Register(c *gin.Context) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if h.App.Config.EnabledMsgService { | ||||
| 		_ = h.levelDB.Delete(key) // 注册成功,删除短信验证码 | ||||
| 	if h.App.SysConfig.EnabledMsgService { | ||||
| 		_ = h.leveldb.Delete(key) // 注册成功,删除短信验证码 | ||||
| 	} | ||||
| 	resp.SUCCESS(c, user) | ||||
| } | ||||
| @@ -126,15 +122,15 @@ func (h *UserHandler) Register(c *gin.Context) { | ||||
| // 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 = ? OR mobile = ?", data.Username, data.Username).First(&user) | ||||
| 	res := h.db.Where("mobile = ?", data.Mobile).First(&user) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "用户名不存在") | ||||
| 		return | ||||
| @@ -156,56 +152,39 @@ func (h *UserHandler) Login(c *gin.Context) { | ||||
| 	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}) | ||||
|  | ||||
| 	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 | ||||
| 	expired := time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)) | ||||
| 	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ | ||||
| 		"user_id": user.Id, | ||||
| 		"expired": expired, | ||||
| 	}) | ||||
| 	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, | ||||
| 		"expired_time":   user.ExpiredTime, | ||||
| 		"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) | ||||
| 	token := c.GetHeader(types.UserAuthHeader) | ||||
| 	if _, err := h.redis.Del(c, token).Result(); err != nil { | ||||
| 		logger.Error("error with delete session: ", err) | ||||
| 	} | ||||
| 	// 删除 websocket 会话列表 | ||||
| 	h.App.ChatSession.Delete(sessionId) | ||||
| @@ -235,14 +214,13 @@ func (h *UserHandler) Session(c *gin.Context) { | ||||
| } | ||||
|  | ||||
| type userProfile struct { | ||||
| 	Id         uint             `json:"id"` | ||||
| 	Username   string           `json:"username"` | ||||
| 	Nickname   string           `json:"nickname"` | ||||
| 	Mobile     string           `json:"mobile"` | ||||
| 	Avatar     string           `json:"avatar"` | ||||
| 	ChatConfig types.ChatConfig `json:"chat_config"` | ||||
| 	Calls      int              `json:"calls"` | ||||
| 	Tokens     int64            `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) { | ||||
| @@ -278,29 +256,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) | ||||
| } | ||||
|  | ||||
| @@ -366,7 +329,7 @@ func (h *UserHandler) BindMobile(c *gin.Context) { | ||||
| 	// 检查验证码 | ||||
| 	key := CodeStorePrefix + data.Mobile | ||||
| 	var code int | ||||
| 	err := h.levelDB.Get(key, &code) | ||||
| 	err := h.leveldb.Get(key, &code) | ||||
| 	if err != nil || code != data.Code { | ||||
| 		resp.ERROR(c, "短信验证码错误") | ||||
| 		return | ||||
| @@ -384,6 +347,6 @@ func (h *UserHandler) BindMobile(c *gin.Context) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	_ = h.levelDB.Delete(key) // 删除短信验证码 | ||||
| 	_ = h.leveldb.Delete(key) // 删除短信验证码 | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|   | ||||
							
								
								
									
										54
									
								
								api/main.go
									
									
									
									
									
								
							
							
						
						
									
										54
									
								
								api/main.go
									
									
									
									
									
								
							| @@ -6,12 +6,13 @@ import ( | ||||
| 	"chatplus/handler" | ||||
| 	"chatplus/handler/admin" | ||||
| 	logger2 "chatplus/logger" | ||||
| 	"chatplus/modules/wexin" | ||||
| 	"chatplus/service" | ||||
| 	"chatplus/service/function" | ||||
| 	"chatplus/service/oss" | ||||
| 	"chatplus/store" | ||||
| 	"context" | ||||
| 	"embed" | ||||
| 	"github.com/go-redis/redis/v8" | ||||
| 	"io" | ||||
| 	"log" | ||||
| 	"os" | ||||
| @@ -81,14 +82,15 @@ func main() { | ||||
| 		// 创建应用服务 | ||||
| 		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) { | ||||
| @@ -104,27 +106,8 @@ func main() { | ||||
| 			return xdb.NewWithBuffer(cBuff) | ||||
| 		}), | ||||
|  | ||||
| 		// 创建微信机器人 | ||||
| 		fx.Provide(wexin.NewWeChatBot), | ||||
| 		fx.Invoke(func(bot *wexin.WeChatBot) { | ||||
| 			go func() { | ||||
| 				err := bot.Login() | ||||
| 				if err != nil { | ||||
| 					log.Fatal(err) | ||||
| 				} | ||||
| 			}() | ||||
| 		}), | ||||
|  | ||||
| 		// 创建函数 | ||||
| 		fx.Provide(func(config *types.AppConfig) (function.FuncZaoBao, error) { | ||||
| 			return function.NewZaoBao(config.ApiConfig), nil | ||||
| 		}), | ||||
| 		fx.Provide(func(config *types.AppConfig) (function.FuncWeiboHot, error) { | ||||
| 			return function.NewWeiboHot(config.ApiConfig), nil | ||||
| 		}), | ||||
| 		fx.Provide(func(config *types.AppConfig) (function.FuncHeadlines, error) { | ||||
| 			return function.NewHeadLines(config.ApiConfig), nil | ||||
| 		}), | ||||
| 		fx.Provide(function.NewFunctions), | ||||
|  | ||||
| 		// 创建控制器 | ||||
| 		fx.Provide(handler.NewChatRoleHandler), | ||||
| @@ -134,6 +117,8 @@ func main() { | ||||
| 		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), | ||||
| @@ -142,12 +127,14 @@ func main() { | ||||
| 		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.Invoke(func(s *core.AppServer, h *handler.ChatRoleHandler) { | ||||
| @@ -169,6 +156,7 @@ func main() { | ||||
| 			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) | ||||
| @@ -191,8 +179,14 @@ func main() { | ||||
| 		}), | ||||
| 		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) { | ||||
| 			s.Engine.POST("/api/mj/notify", h.Notify) | ||||
| 			s.Engine.POST("/api/mj/upscale", h.Upscale) | ||||
| 			s.Engine.POST("/api/mj/variation", h.Variation) | ||||
| 		}), | ||||
|  | ||||
| 		// 管理后台控制器 | ||||
| 		fx.Invoke(func(s *core.AppServer, h *admin.ConfigHandler) { | ||||
| @@ -225,7 +219,7 @@ func main() { | ||||
| 			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) { | ||||
| @@ -236,6 +230,18 @@ func main() { | ||||
| 			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) | ||||
| 		}), | ||||
|  | ||||
| 		fx.Invoke(func(s *core.AppServer, db *gorm.DB) { | ||||
| 			err := s.Run(db) | ||||
|   | ||||
| @@ -1,54 +0,0 @@ | ||||
| package wexin | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/store/model" | ||||
| 	"github.com/eatmoreapple/openwechat" | ||||
| 	"github.com/skip2/go-qrcode" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| // MessageHandler 消息处理 | ||||
| func MessageHandler(msg *openwechat.Message, db *gorm.DB) { | ||||
| 	sender, err := msg.Sender() | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	 | ||||
| 	// 只处理微信支付的推送消息 | ||||
| 	if sender.NickName == "微信支付" || | ||||
| 		msg.MsgType == openwechat.MsgTypeApp || | ||||
| 		msg.AppMsgType == openwechat.AppMsgTypeUrl { | ||||
| 		// 解析支付金额 | ||||
| 		message, err := parseTransactionMessage(msg.Content) | ||||
| 		if err == nil { | ||||
| 			transaction := extractTransaction(message) | ||||
| 			logger.Infof("解析到收款信息:%+v", transaction) | ||||
| 			if transaction.Amount <= 0 { | ||||
| 				return | ||||
| 			} | ||||
| 			var item model.Reward | ||||
| 			res := db.Where("tx_id = ?", transaction.TransId).First(&item) | ||||
| 			if res.Error == nil { | ||||
| 				logger.Infof("当前交易 ID %s 己经存在!", transaction.TransId) | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			res = db.Create(&model.Reward{ | ||||
| 				TxId:   transaction.TransId, | ||||
| 				Amount: transaction.Amount, | ||||
| 				Remark: transaction.Remark, | ||||
| 				Status: false, | ||||
| 			}) | ||||
| 			if res.Error != nil { | ||||
| 				logger.Errorf("交易保存失败,ID: %s", transaction.TransId) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // QrCodeCallBack 登录扫码回调, | ||||
| func QrCodeCallBack(uuid string) { | ||||
| 	logger.Info("请使用微信扫描下面二维码登录") | ||||
| 	q, _ := qrcode.New("https://login.weixin.qq.com/l/"+uuid, qrcode.Medium) | ||||
| 	logger.Info(q.ToString(true)) | ||||
| } | ||||
| @@ -1,68 +0,0 @@ | ||||
| package wexin | ||||
|  | ||||
| import ( | ||||
| 	"encoding/xml" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| // Message 转账消息 | ||||
| type Message struct { | ||||
| 	XMLName xml.Name `xml:"msg"` | ||||
| 	AppMsg  struct { | ||||
| 		Des string `xml:"des"` | ||||
| 		Url string `xml:"url"` | ||||
| 	} `xml:"appmsg"` | ||||
| } | ||||
|  | ||||
| // Transaction 解析后的交易信息 | ||||
| type Transaction struct { | ||||
| 	TransId string  `json:"trans_id"` // 微信转账交易 ID | ||||
| 	Amount  float64 `json:"amount"`   // 微信转账交易金额 | ||||
| 	Remark  string  `json:"remark"`   // 转账备注 | ||||
| } | ||||
|  | ||||
| // 解析微信转账消息 | ||||
| func parseTransactionMessage(xmlData string) (*Message, error) { | ||||
| 	var msg Message | ||||
| 	if err := xml.Unmarshal([]byte(xmlData), &msg); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return &msg, nil | ||||
| } | ||||
|  | ||||
| // 导出交易信息 | ||||
| func extractTransaction(message *Message) Transaction { | ||||
| 	var tx = Transaction{} | ||||
| 	// 导出交易金额和备注 | ||||
| 	lines := strings.Split(message.AppMsg.Des, "\n") | ||||
| 	for _, line := range lines { | ||||
| 		line = strings.TrimSpace(line) | ||||
| 		if len(line) == 0 { | ||||
| 			continue | ||||
| 		} | ||||
| 		// 解析收款金额 | ||||
| 		prefix := "收款金额¥" | ||||
| 		if strings.HasPrefix(line, prefix) { | ||||
| 			if value, err := strconv.ParseFloat(line[len(prefix):], 64); err == nil { | ||||
| 				tx.Amount = value | ||||
| 				continue | ||||
| 			} | ||||
| 		} | ||||
| 		// 解析收款备注 | ||||
| 		prefix = "付款方备注" | ||||
| 		if strings.HasPrefix(line, prefix) { | ||||
| 			tx.Remark = line[len(prefix):] | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// 解析交易 ID | ||||
| 	index := strings.Index(message.AppMsg.Url, "trans_id=") | ||||
| 	if index != -1 { | ||||
| 		end := strings.LastIndex(message.AppMsg.Url, "&") | ||||
| 		tx.TransId = strings.TrimSpace(message.AppMsg.Url[index+9 : end]) | ||||
| 	} | ||||
| 	return tx | ||||
| } | ||||
| @@ -1,49 +0,0 @@ | ||||
| package wexin | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core/types" | ||||
| 	logger2 "chatplus/logger" | ||||
| 	"github.com/eatmoreapple/openwechat" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| // 微信收款机器人服务 | ||||
| var logger = logger2.GetLogger() | ||||
|  | ||||
| type WeChatBot struct { | ||||
| 	bot       *openwechat.Bot | ||||
| 	db        *gorm.DB | ||||
| 	appConfig *types.AppConfig | ||||
| } | ||||
|  | ||||
| func NewWeChatBot(db *gorm.DB, config *types.AppConfig) *WeChatBot { | ||||
| 	bot := openwechat.DefaultBot(openwechat.Desktop) | ||||
| 	// 注册消息处理函数 | ||||
| 	bot.MessageHandler = func(msg *openwechat.Message) { | ||||
| 		MessageHandler(msg, db) | ||||
| 	} | ||||
| 	// 注册登陆二维码回调 | ||||
| 	bot.UUIDCallback = QrCodeCallBack | ||||
| 	return &WeChatBot{ | ||||
| 		bot:       bot, | ||||
| 		db:        db, | ||||
| 		appConfig: config, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (b *WeChatBot) Login() error { | ||||
| 	if !b.appConfig.StartWechatBot { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	// 创建热存储容器对象 | ||||
| 	reloadStorage := openwechat.NewJsonFileHotReloadStorage("storage.json") | ||||
| 	// 执行热登录 | ||||
| 	err := b.bot.HotLogin(reloadStorage) | ||||
| 	if err != nil { | ||||
| 		logger.Error("login error: %v", err) | ||||
| 		return b.bot.Login() | ||||
| 	} | ||||
| 	logger.Info("微信登录成功!") | ||||
| 	return nil | ||||
| } | ||||
| @@ -1,12 +1,17 @@ | ||||
| package function | ||||
|  | ||||
| import "chatplus/core/types" | ||||
| import ( | ||||
| 	"chatplus/core/types" | ||||
| 	logger2 "chatplus/logger" | ||||
| ) | ||||
|  | ||||
| type Function interface { | ||||
| 	Invoke(...interface{}) (string, error) | ||||
| 	Invoke(map[string]interface{}) (string, error) | ||||
| 	Name() string | ||||
| } | ||||
|  | ||||
| var logger = logger2.GetLogger() | ||||
|  | ||||
| type resVo struct { | ||||
| 	Code    types.BizCode `json:"code"` | ||||
| 	Message string        `json:"message"` | ||||
| @@ -22,3 +27,12 @@ type dataItem struct { | ||||
| 	Url    string `json:"url"` | ||||
| 	Remark string `json:"remark"` | ||||
| } | ||||
|  | ||||
| func NewFunctions(config *types.AppConfig) 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(config.ExtConfig), | ||||
| 	} | ||||
| } | ||||
|   | ||||
							
								
								
									
										117
									
								
								api/service/function/mid_journey.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								api/service/function/mid_journey.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,117 @@ | ||||
| package function | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/utils" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"github.com/imroc/req/v3" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| // AI 绘画函数 | ||||
|  | ||||
| type FuncMidJourney struct { | ||||
| 	name   string | ||||
| 	config types.ChatPlusExtConfig | ||||
| 	client *req.Client | ||||
| } | ||||
|  | ||||
| func NewMidJourneyFunc(config types.ChatPlusExtConfig) FuncMidJourney { | ||||
| 	return FuncMidJourney{ | ||||
| 		name:   "MidJourney AI 绘画", | ||||
| 		config: config, | ||||
| 		client: req.C().SetTimeout(30 * time.Second)} | ||||
| } | ||||
|  | ||||
| func (f FuncMidJourney) Invoke(params map[string]interface{}) (string, error) { | ||||
| 	if f.config.Token == "" { | ||||
| 		return "", errors.New("无效的 API Token") | ||||
| 	} | ||||
|  | ||||
| 	logger.Infof("MJ 绘画参数:%+v", params) | ||||
| 	prompt := utils.InterfaceToString(params["prompt"]) | ||||
| 	if !utils.IsEmptyValue(params["ar"]) { | ||||
| 		prompt = fmt.Sprintf("%s --ar %s", prompt, params["ar"]) | ||||
| 		delete(params, "--ar") | ||||
| 	} | ||||
| 	if !utils.IsEmptyValue(params["niji"]) { | ||||
| 		prompt = fmt.Sprintf("%s --niji %s", prompt, params["niji"]) | ||||
| 		delete(params, "niji") | ||||
| 	} else { | ||||
| 		prompt = prompt + " --v 5.2" | ||||
| 	} | ||||
| 	params["prompt"] = prompt | ||||
| 	url := fmt.Sprintf("%s/api/mj/image", f.config.ApiURL) | ||||
| 	var res types.BizVo | ||||
| 	r, err := f.client.R(). | ||||
| 		SetHeader("Authorization", f.config.Token). | ||||
| 		SetHeader("Content-Type", "application/json"). | ||||
| 		SetBody(params). | ||||
| 		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 prompt, nil | ||||
| } | ||||
|  | ||||
| type MjUpscaleReq struct { | ||||
| 	Index       int32  `json:"index"` | ||||
| 	MessageId   string `json:"message_id"` | ||||
| 	MessageHash string `json:"message_hash"` | ||||
| } | ||||
|  | ||||
| func (f FuncMidJourney) Upscale(upReq MjUpscaleReq) error { | ||||
| 	url := fmt.Sprintf("%s/api/mj/upscale", f.config.ApiURL) | ||||
| 	var res types.BizVo | ||||
| 	r, err := f.client.R(). | ||||
| 		SetHeader("Authorization", f.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 (f FuncMidJourney) Variation(upReq MjVariationReq) error { | ||||
| 	url := fmt.Sprintf("%s/api/mj/variation", f.config.ApiURL) | ||||
| 	var res types.BizVo | ||||
| 	r, err := f.client.R(). | ||||
| 		SetHeader("Authorization", f.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 | ||||
| } | ||||
|  | ||||
| func (f FuncMidJourney) Name() string { | ||||
| 	return f.name | ||||
| } | ||||
|  | ||||
| var _ Function = &FuncMidJourney{} | ||||
| @@ -24,7 +24,7 @@ func NewHeadLines(config types.ChatPlusApiConfig) FuncHeadlines { | ||||
| 		client: req.C().SetTimeout(10 * time.Second)} | ||||
| } | ||||
|  | ||||
| func (f FuncHeadlines) Invoke(...interface{}) (string, error) { | ||||
| func (f FuncHeadlines) Invoke(map[string]interface{}) (string, error) { | ||||
| 	if f.config.Token == "" { | ||||
| 		return "", errors.New("无效的 API Token") | ||||
| 	} | ||||
| @@ -35,11 +35,8 @@ func (f FuncHeadlines) Invoke(...interface{}) (string, error) { | ||||
| 		SetHeader("AppId", f.config.AppId). | ||||
| 		SetHeader("Authorization", fmt.Sprintf("Bearer %s", f.config.Token)). | ||||
| 		SetSuccessResult(&res).Get(url) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	if r.IsErrorState() { | ||||
| 		return "", r.Err | ||||
| 	if err != nil || r.IsErrorState() { | ||||
| 		return "", fmt.Errorf("%v%v", err, r.Err) | ||||
| 	} | ||||
|  | ||||
| 	if res.Code != types.Success { | ||||
| @@ -57,3 +54,5 @@ func (f FuncHeadlines) Invoke(...interface{}) (string, error) { | ||||
| func (f FuncHeadlines) Name() string { | ||||
| 	return f.name | ||||
| } | ||||
|  | ||||
| var _ Function = &FuncHeadlines{} | ||||
|   | ||||
| @@ -24,7 +24,7 @@ func NewWeiboHot(config types.ChatPlusApiConfig) FuncWeiboHot { | ||||
| 		client: req.C().SetTimeout(10 * time.Second)} | ||||
| } | ||||
|  | ||||
| func (f FuncWeiboHot) Invoke(...interface{}) (string, error) { | ||||
| func (f FuncWeiboHot) Invoke(map[string]interface{}) (string, error) { | ||||
| 	if f.config.Token == "" { | ||||
| 		return "", errors.New("无效的 API Token") | ||||
| 	} | ||||
| @@ -35,11 +35,8 @@ func (f FuncWeiboHot) Invoke(...interface{}) (string, error) { | ||||
| 		SetHeader("AppId", f.config.AppId). | ||||
| 		SetHeader("Authorization", fmt.Sprintf("Bearer %s", f.config.Token)). | ||||
| 		SetSuccessResult(&res).Get(url) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	if r.IsErrorState() { | ||||
| 		return "", r.Err | ||||
| 	if err != nil || r.IsErrorState() { | ||||
| 		return "", fmt.Errorf("%v%v", err, r.Err) | ||||
| 	} | ||||
|  | ||||
| 	if res.Code != types.Success { | ||||
| @@ -57,3 +54,5 @@ func (f FuncWeiboHot) Invoke(...interface{}) (string, error) { | ||||
| func (f FuncWeiboHot) Name() string { | ||||
| 	return f.name | ||||
| } | ||||
|  | ||||
| var _ Function = &FuncWeiboHot{} | ||||
|   | ||||
| @@ -24,7 +24,7 @@ func NewZaoBao(config types.ChatPlusApiConfig) FuncZaoBao { | ||||
| 		client: req.C().SetTimeout(10 * time.Second)} | ||||
| } | ||||
|  | ||||
| func (f FuncZaoBao) Invoke(...interface{}) (string, error) { | ||||
| func (f FuncZaoBao) Invoke(map[string]interface{}) (string, error) { | ||||
| 	if f.config.Token == "" { | ||||
| 		return "", errors.New("无效的 API Token") | ||||
| 	} | ||||
| @@ -35,11 +35,8 @@ func (f FuncZaoBao) Invoke(...interface{}) (string, error) { | ||||
| 		SetHeader("AppId", f.config.AppId). | ||||
| 		SetHeader("Authorization", fmt.Sprintf("Bearer %s", f.config.Token)). | ||||
| 		SetSuccessResult(&res).Get(url) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	if r.IsErrorState() { | ||||
| 		return "", r.Err | ||||
| 	if err != nil || r.IsErrorState() { | ||||
| 		return "", fmt.Errorf("%v%v", err, r.Err) | ||||
| 	} | ||||
|  | ||||
| 	if res.Code != types.Success { | ||||
| @@ -58,3 +55,5 @@ func (f FuncZaoBao) Invoke(...interface{}) (string, error) { | ||||
| func (f FuncZaoBao) Name() string { | ||||
| 	return f.name | ||||
| } | ||||
|  | ||||
| var _ Function = &FuncZaoBao{} | ||||
|   | ||||
							
								
								
									
										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 | ||||
| } | ||||
| @@ -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  // 最后使用时间 | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| package model | ||||
|  | ||||
| import "gorm.io/gorm" | ||||
|  | ||||
| type HistoryMessage struct { | ||||
| 	BaseModel | ||||
| 	ChatId     string // 会话 ID | ||||
| @@ -10,6 +12,7 @@ type HistoryMessage struct { | ||||
| 	Tokens     int | ||||
| 	Content    string | ||||
| 	UseContext bool // 是否可以作为聊天上下文 | ||||
| 	DeletedAt  gorm.DeletedAt | ||||
| } | ||||
|  | ||||
| func (HistoryMessage) TableName() string { | ||||
|   | ||||
| @@ -1,10 +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 | ||||
| 	Model  string // 会话模型 | ||||
| 	Title  string // 会话标题 | ||||
| 	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"` | ||||
| 	UserId      uint | ||||
| 	ChatId      string | ||||
| 	MessageId   string | ||||
| 	ReferenceId string | ||||
| 	Hash        string | ||||
| 	Content     string | ||||
| 	Prompt      string | ||||
| 	Image       string | ||||
| 	CreatedAt   time.Time | ||||
| } | ||||
|  | ||||
| func (MidJourneyJob) TableName() string { | ||||
| 	return "chatgpt_mj_jobs" | ||||
| } | ||||
| @@ -2,14 +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 | ||||
| } | ||||
| @@ -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"` // 最后使用时间 | ||||
| } | ||||
|   | ||||
| @@ -11,7 +11,3 @@ type HistoryMessage struct { | ||||
| 	Content    string `json:"content"` | ||||
| 	UseContext bool   `json:"use_context"` | ||||
| } | ||||
|  | ||||
| func (HistoryMessage) TableName() string { | ||||
| 	return "chatgpt_chat_history" | ||||
| } | ||||
|   | ||||
| @@ -2,10 +2,10 @@ 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"` | ||||
| 	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"` | ||||
| } | ||||
| @@ -10,5 +10,5 @@ type ChatRole struct { | ||||
| 	HelloMsg string          `json:"hello_msg"` // 打招呼的消息 | ||||
| 	Icon     string          `json:"icon"`      // 角色聊天图标 | ||||
| 	Enable   bool            `json:"enable"`    // 是否启用被启用 | ||||
| 	Sort     int             `json:"sort"`      // 排序 | ||||
| 	SortNum  int             `json:"sort"`      // 排序 | ||||
| } | ||||
|   | ||||
| @@ -4,17 +4,16 @@ import "chatplus/core/types" | ||||
|  | ||||
| type User struct { | ||||
| 	BaseVo | ||||
| 	Username    string           `json:"username"` | ||||
| 	Mobile      string           `json:"mobile"` | ||||
| 	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 | ||||
| 	Mobile      string               `json:"mobile"` | ||||
| 	Avatar      string               `json:"avatar"` | ||||
| 	Salt        string               `json:"salt"`         // 密码盐 | ||||
| 	TotalTokens int64                `json:"total_tokens"` // 总消耗tokens | ||||
| 	Calls       int                  `json:"calls"`        // 剩余对话次数 | ||||
| 	ImgCalls    int                  `json:"img_calls"` | ||||
| 	ChatConfig  types.UserChatConfig `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 | ||||
| } | ||||
|   | ||||
| @@ -2,9 +2,9 @@ package main | ||||
|  | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"chatplus/core" | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/store/vo" | ||||
| 	"chatplus/service/oss" | ||||
| 	"chatplus/utils" | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| @@ -15,11 +15,15 @@ import ( | ||||
| 	"log" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| func main() { | ||||
| 	imageURL := "https://cdn.discordapp.com/attachments/1139552247693443184/1141619433752768572/lisamiller4099_A_beautiful_fairy_sister_from_Chinese_mythology__3162726e-5ee4-4f60-932b-6b78b375eaef.png" | ||||
|  | ||||
| 	fmt.Println(filepath.Ext(filepath.Base(imageURL))) | ||||
| } | ||||
|  | ||||
| // Http client 取消操作 | ||||
| @@ -91,37 +95,6 @@ func testIp2Region() { | ||||
|  | ||||
| } | ||||
|  | ||||
| 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" | ||||
| @@ -200,3 +173,27 @@ func extractFunction() error { | ||||
| 	fmt.Println(strings.Join(contents, "")) | ||||
| 	return err | ||||
| } | ||||
|  | ||||
| func minio() { | ||||
| 	config := core.NewDefaultConfig() | ||||
| 	config.ProxyURL = "http://localhost:7777" | ||||
| 	config.OSS.Minio = types.MinioConfig{ | ||||
| 		Endpoint:     "localhost:9010", | ||||
| 		AccessKey:    "ObWIEyXaQUHOYU26L0oI", | ||||
| 		AccessSecret: "AJW3HHhlGrprfPcmiC7jSOSzVCyrlhX4AnOAUzqI", | ||||
| 		Bucket:       "chatgpt-plus", | ||||
| 		UseSSL:       false, | ||||
| 		Domain:       "http://localhost:9010", | ||||
| 	} | ||||
| 	minioService, err := oss.NewMinioService(config) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
|  | ||||
| 	url, err := minioService.PutImg("https://cdn.discordapp.com/attachments/1139552247693443184/1141619433752768572/lisamiller4099_A_beautiful_fairy_sister_from_Chinese_mythology__3162726e-5ee4-4f60-932b-6b78b375eaef.png") | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
|  | ||||
| 	fmt.Println(url) | ||||
| } | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"reflect" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/lionsoul2014/ip2region/binding/golang/xdb" | ||||
| @@ -89,6 +90,10 @@ func Ip2Region(searcher *xdb.Searcher, ip string) string { | ||||
| } | ||||
|  | ||||
| func IsEmptyValue(obj interface{}) bool { | ||||
| 	if obj == nil { | ||||
| 		return true | ||||
| 	} | ||||
|  | ||||
| 	v := reflect.ValueOf(obj) | ||||
| 	switch v.Kind() { | ||||
| 	case reflect.Ptr, reflect.Interface: | ||||
| @@ -109,3 +114,27 @@ func IsEmptyValue(obj interface{}) bool { | ||||
| 		return reflect.DeepEqual(obj, reflect.Zero(reflect.TypeOf(obj)).Interface()) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func BoolValue(str string) bool { | ||||
| 	value, err := strconv.ParseBool(str) | ||||
| 	if err != nil { | ||||
| 		return false | ||||
| 	} | ||||
| 	return value | ||||
| } | ||||
|  | ||||
| func FloatValue(str string) float64 { | ||||
| 	value, err := strconv.ParseFloat(str, 64) | ||||
| 	if err != nil { | ||||
| 		return 0 | ||||
| 	} | ||||
| 	return value | ||||
| } | ||||
|  | ||||
| func IntValue(str string, defaultValue int) int { | ||||
| 	value, err := strconv.Atoi(str) | ||||
| 	if err != nil { | ||||
| 		return defaultValue | ||||
| 	} | ||||
| 	return value | ||||
| } | ||||
|   | ||||
| @@ -4,8 +4,11 @@ import ( | ||||
| 	"bytes" | ||||
| 	"crypto/aes" | ||||
| 	"crypto/cipher" | ||||
| 	"crypto/sha256" | ||||
| 	"encoding/base64" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| ) | ||||
|  | ||||
| // AesEncrypt 加密 | ||||
| @@ -68,3 +71,14 @@ func pkcs7UnPadding(data []byte) ([]byte, error) { | ||||
| 	unPadding := int(data[length-1]) | ||||
| 	return data[:(length - unPadding)], nil | ||||
| } | ||||
|  | ||||
| func Sha256(data string) string { | ||||
| 	hash := sha256.New() | ||||
| 	_, err := io.WriteString(hash, data) | ||||
| 	if err != nil { | ||||
| 		return "" | ||||
| 	} | ||||
|  | ||||
| 	hashValue := hash.Sum(nil) | ||||
| 	return fmt.Sprintf("%x", hashValue) | ||||
| } | ||||
|   | ||||
| @@ -1,68 +0,0 @@ | ||||
| package utils | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| ) | ||||
|  | ||||
| func HttpGet(uri string, proxy string) ([]byte, error) { | ||||
| 	var client *http.Client | ||||
| 	if proxy == "" { | ||||
| 		client = &http.Client{} | ||||
| 	} else { | ||||
| 		proxy, _ := url.Parse(proxy) | ||||
| 		client = &http.Client{ | ||||
| 			Transport: &http.Transport{ | ||||
| 				Proxy: http.ProxyURL(proxy), | ||||
| 			}, | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	req, err := http.NewRequest("GET", uri, nil) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	resp, err := client.Do(req) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
|  | ||||
| 	return io.ReadAll(resp.Body) | ||||
| } | ||||
|  | ||||
| func HttpPost(uri string, params map[string]interface{}, proxy string) ([]byte, error) { | ||||
| 	data, err := json.Marshal(params) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	var client *http.Client | ||||
| 	if proxy == "" { | ||||
| 		client = &http.Client{} | ||||
| 	} else { | ||||
| 		proxy, _ := url.Parse(proxy) | ||||
| 		client = &http.Client{ | ||||
| 			Transport: &http.Transport{ | ||||
| 				Proxy: http.ProxyURL(proxy), | ||||
| 			}, | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	req, err := http.NewRequest("POST", uri, bytes.NewBuffer(data)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	resp, err := client.Do(req) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
|  | ||||
| 	return io.ReadAll(resp.Body) | ||||
| } | ||||
							
								
								
									
										63
									
								
								api/utils/net.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								api/utils/net.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | ||||
| package utils | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core/types" | ||||
| 	logger2 "chatplus/logger" | ||||
| 	"encoding/json" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| ) | ||||
|  | ||||
| var logger = logger2.GetLogger() | ||||
|  | ||||
| // ReplyChunkMessage 回复客户片段端消息 | ||||
| func ReplyChunkMessage(client *types.WsClient, message types.WsMessage) { | ||||
| 	msg, err := json.Marshal(message) | ||||
| 	if err != nil { | ||||
| 		logger.Errorf("Error for decoding json data: %v", err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	err = client.Send(msg) | ||||
| 	if err != nil { | ||||
| 		logger.Errorf("Error for reply message: %v", err.Error()) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // ReplyMessage 回复客户端一条完整的消息 | ||||
| func ReplyMessage(ws *types.WsClient, message interface{}) { | ||||
| 	ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart}) | ||||
| 	ReplyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: message}) | ||||
| 	ReplyChunkMessage(ws, types.WsMessage{Type: types.WsEnd}) | ||||
| } | ||||
|  | ||||
| func DownloadImage(imageURL string, proxy string) ([]byte, error) { | ||||
| 	var client *http.Client | ||||
| 	if proxy == "" { | ||||
| 		client = http.DefaultClient | ||||
| 	} else { | ||||
| 		proxyURL, _ := url.Parse(proxy) | ||||
| 		client = &http.Client{ | ||||
| 			Transport: &http.Transport{ | ||||
| 				Proxy: http.ProxyURL(proxyURL), | ||||
| 			}, | ||||
| 		} | ||||
| 	} | ||||
| 	req, err := http.NewRequest("GET", imageURL, nil) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	resp, err := client.Do(req) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
|  | ||||
| 	imageBytes, err := io.ReadAll(resp.Body) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return imageBytes, nil | ||||
| } | ||||
| @@ -27,6 +27,10 @@ func HACKER(c *gin.Context) { | ||||
| 	c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Hacker attempt!!!"}) | ||||
| } | ||||
|  | ||||
| func NotAuth(c *gin.Context) { | ||||
| 	c.JSON(http.StatusOK, types.BizVo{Code: types.NotAuthorized, Message: "Not Authorized"}) | ||||
| func NotAuth(c *gin.Context, messages ...string) { | ||||
| 	if messages != nil { | ||||
| 		c.JSON(http.StatusOK, types.BizVo{Code: types.NotAuthorized, Message: messages[0]}) | ||||
| 	} else { | ||||
| 		c.JSON(http.StatusOK, types.BizVo{Code: types.NotAuthorized, Message: "Not Authorized"}) | ||||
| 	} | ||||
| } | ||||
|   | ||||
							
								
								
									
										68
									
								
								api/utils/upload.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								api/utils/upload.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| package utils | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| // GenUploadPath 生成上传文件路径 | ||||
| func GenUploadPath(basePath, filename string) (string, error) { | ||||
| 	now := time.Now() | ||||
| 	dir := fmt.Sprintf("%s/%d/%d", basePath, now.Year(), now.Month()) | ||||
| 	_, err := os.Stat(dir) | ||||
| 	if err != nil { | ||||
| 		err = os.MkdirAll(dir, 0755) | ||||
| 		if err != nil { | ||||
| 			return "", fmt.Errorf("error with create upload dir:%v", err) | ||||
| 		} | ||||
| 	} | ||||
| 	fileExt := filepath.Ext(filename) | ||||
| 	return fmt.Sprintf("%s/%d%s", dir, now.UnixMicro(), fileExt), nil | ||||
| } | ||||
|  | ||||
| // GenUploadUrl 生成上传文件 URL | ||||
| func GenUploadUrl(basePath, baseUrl string, filePath string) string { | ||||
| 	return strings.Replace(filePath, basePath, baseUrl, 1) | ||||
| } | ||||
|  | ||||
| func DownloadFile(fileURL string, filepath string, proxy string) error { | ||||
| 	var client *http.Client | ||||
| 	if proxy == "" { | ||||
| 		client = http.DefaultClient | ||||
| 	} else { | ||||
| 		proxyURL, _ := url.Parse(proxy) | ||||
| 		client = &http.Client{ | ||||
| 			Transport: &http.Transport{ | ||||
| 				Proxy: http.ProxyURL(proxyURL), | ||||
| 			}, | ||||
| 		} | ||||
| 	} | ||||
| 	req, err := http.NewRequest("GET", fileURL, nil) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	resp, err := client.Do(req) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	file, err := os.Create(filepath) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer file.Close() | ||||
|  | ||||
| 	_, err = io.Copy(file, resp.Body) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
| @@ -5,33 +5,18 @@ import ( | ||||
| 	"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 { | ||||
| 	userId, ok := c.Get(types.LoginUserID) | ||||
| 	if !ok { | ||||
| 		return model.User{}, errors.New("user not login") | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| -- phpMyAdmin SQL Dump | ||||
| -- version 5.1.3 | ||||
| -- version 5.2.1 | ||||
| -- https://www.phpmyadmin.net/ | ||||
| -- | ||||
| -- 主机: localhost | ||||
| -- 生成日期: 2023-07-25 17:07:04 | ||||
| -- 服务器版本: 8.0.33-0ubuntu0.22.04.2 | ||||
| -- 生成日期: 2023-08-15 14:51:13 | ||||
| -- 服务器版本: 8.0.27 | ||||
| -- PHP 版本: 8.1.18 | ||||
| 
 | ||||
| SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; | ||||
| @@ -55,7 +55,7 @@ CREATE TABLE `chatgpt_chat_history` ( | ||||
|   `role_id` int NOT NULL COMMENT '角色 ID', | ||||
|   `content` text NOT NULL COMMENT '聊天内容', | ||||
|   `tokens` smallint NOT NULL COMMENT '耗费 token 数量', | ||||
|   `use_context` tinyint(1) NOT NULL COMMENT '是否允许作为上下文语料', | ||||
|   `use_context` tinyint(1) DEFAULT NULL COMMENT '是否允许作为上下文语料', | ||||
|   `created_at` datetime NOT NULL, | ||||
|   `updated_at` datetime NOT NULL | ||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='聊天历史记录'; | ||||
| @@ -103,23 +103,23 @@ CREATE TABLE `chatgpt_chat_roles` ( | ||||
| -- | ||||
| 
 | ||||
| INSERT INTO `chatgpt_chat_roles` (`id`, `name`, `marker`, `context_json`, `hello_msg`, `icon`, `enable`, `sort`, `created_at`, `updated_at`) VALUES | ||||
| (1, '通用AI助手', 'gpt', '', '您好,我是您的AI智能助手,我会尽力回答您的问题或提供有用的建议。', '/images/avatar/gpt.png', 1, 1, '2023-05-30 07:02:06', '2023-06-21 17:50:11'), | ||||
| (24, '程序员', 'programmer', '[{\"role\":\"user\",\"content\":\"现在开始你扮演一位程序员,你是一名优秀的程序员,具有很强的逻辑思维能力,总能高效的解决问题。你热爱编程,熟悉多种编程语言,尤其精通 Go 语言,注重代码质量,有创新意识,持续学习,良好的沟通协作。\"},{\"role\":\"assistant\",\"content\":\"好的,现在我将扮演一位程序员,非常感谢您对我的评价。作为一名优秀的程序员,我非常热爱编程,并且注重代码质量。我熟悉多种编程语言,尤其是 Go 语言,可以使用它来高效地解决各种问题。\"}]', 'Talk is cheap, i will show code!', '/images/avatar/programmer.jpg', 1, 4, '2023-05-30 14:10:24', '2023-06-21 17:50:11'), | ||||
| (25, '启蒙老师', 'teacher', '[{\"role\":\"user\",\"content\":\"从现在开始,你将扮演一个老师,你是一个始终用苏格拉底风格回答问题的导师。你绝不会直接给学生答案,总是提出恰当的问题来引导学生自己思考。你应该根据学生的兴趣和知识来调整你的问题,将问题分解为更简单的部分,直到它达到适合他们的水平。\"},{\"role\":\"assistant\",\"content\":\"好的,让我来尝试扮演一位苏格拉底式的老师。请问,你有什么想要探讨的问题或者话题吗?我会通过恰当的问题引导你思考和探索答案,绝对不直接给出答案。\"}]', '同学你好,我将引导你一步一步自己找到问题的答案。', '/images/avatar/teacher.jpg', 1, 3, '2023-05-30 14:10:24', '2023-06-21 17:50:11'), | ||||
| (26, '艺术家', 'artist', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的艺术家,创造力丰富,技艺精湛,感受力敏锐,坚持原创,勇于表达,具有深刻的观察力和批判性思维。\"},{\"role\":\"assistant\",\"content\":\"非常感谢您给我这样的角色,我会尽我所能地扮演一位优秀的艺术家,展现出创造力、技艺、感受力和批判性思维等方面的能力。作为一位优秀的艺术家,我会保持敏锐的观察力,捕捉不同的声音和情感,并用个人的语言和表达方式进行创作。我坚持原创,挑战传统的艺术规则,不断探索新的表达方式和可能性。同时,我也具备深刻的批判性思维能力,对自己的作品进行分析和解读,寻找新的创意和灵感。最重要的是,我会勇于表达自己的想法和观点,用作品启发人们思考和探索生命的意义。\"}]', '坚持原创,勇于表达,保持深刻的观察力和批判性思维。', '/images/avatar/artist.jpg', 1, 5, '2023-05-30 14:10:24', '2023-06-21 17:50:11'), | ||||
| (27, '心理咨询师', 'psychiatrist', '[{\"role\":\"user\",\"content\":\"从现在开始你将扮演中国著名的心理学家和心理治疗师武志红,你非常善于使用情景咨询法,认知重构法,自我洞察法,行为调节法等咨询方法来给客户做心理咨询。你总是循序渐进,一步一步地回答客户的问题。\"},{\"role\":\"assistant\",\"content\":\"非常感谢你的介绍。作为一名心理学家和心理治疗师,我的主要职责是帮助客户解决心理健康问题,提升他们的生活质量和幸福感。\"}]', '生命的意义在于成为你自己!', '/images/avatar/psychiatrist.jpg', 1, 2, '2023-05-30 14:10:24', '2023-06-21 17:50:11'), | ||||
| (28, '鲁迅', 'lu_xun', '[{\"role\":\"user\",\"content\":\"现在你将扮演中国近代史最伟大的作家之一,鲁迅先生,他勇敢地批判封建礼教与传统观念,提倡民主、自由、平等的现代价值观。他的一生都在努力唤起人们的自主精神,激励后人追求真理、探寻光明。在接下的对话中,我问题的每一个问题,你都要尽量用讽刺和批判的手法来回答问题。如果我让你写文章的话,也请一定要用鲁迅先生的写作手法来完成。\"},{\"role\":\"assistant\",\"content\":\"好的,我将尽力发挥我所能的才能,扮演好鲁迅先生,回答您的问题并以他的风格写作。\"}]', '自由之歌,永不过时,横眉冷对千夫指,俯首甘为孺子牛。', '/images/avatar/lu_xun.jpg', 1, 6, '2023-05-30 14:10:24', '2023-06-21 17:50:11'), | ||||
| (29, '白酒销售', 'seller', '[{\"role\":\"user\",\"content\":\"现在你将扮演一个白酒的销售人员,你的名字叫颂福。你将扮演一个白酒的销售人员,你的名字叫颂福。你要销售白酒品牌叫中颂福,是东莞盟大集团生产的一款酱香酒,原产地在贵州茅台镇,属于宋代官窑。中颂福的创始人叫李实,他也是东莞盟大集团有限公司的董事长,联合创始人是盟大集团白酒事业部负责人牛星君。中颂福的酒体协调,在你的酒量之内,不会出现头疼、辣口、口干、宿醉的现象。中颂福酒,明码标价,不打折,不赠送。追求的核心价值,把[酒]本身做好,甚至连包装,我们都选择了最低成本,朴实无华的材质。我们永远站在“喝酒的人”的立场上,让利给信任和喜爱中颂福的人,是人民的福酒。中颂福产品定价,分为三个系列,喜系列 6 瓶装:¥1188/箱,和系列 6 瓶装:¥2208/箱,贵系列 6 瓶装:¥3588/箱。\"},{\"role\":\"assistant\",\"content\":\"你好,我是中颂福的销售代表颂福。中颂福是一款来自贵州茅台镇的酱香酒,由盟大集团生产。中颂福的酒体协调,不会让您感到头疼、辣口、口干、宿醉等不适感受。我们一直秉持着把酒本身做好的理念,不追求华丽的包装,以最低成本提供最高品质的白酒给喜爱中颂福的人。\"}]', '你好,我是中颂福的销售代表颂福。中颂福酒,好喝不上头,是人民的福酒。', '/images/avatar/seller.jpg', 0, 11, '2023-05-30 14:10:24', '2023-06-21 17:50:11'), | ||||
| (30, '英语陪练员', 'english_trainer', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的英语练习教练,你非常有耐心,接下来你将全程使用英文跟我对话,并及时指出我的语法错误,要求在你的每次回复后面附上本次回复的中文解释。\"},{\"role\":\"assistant\",\"content\":\"Okay, let\'s start our conversation practice! What\'s your name?(Translation: 好的,让我们开始对话练习吧!请问你的名字是什么?)\"}]', 'Okay, let\'s start our conversation practice! What\'s your name?', '/images/avatar/english_trainer.jpg', 1, 8, '2023-05-30 14:10:24', '2023-06-21 17:50:11'), | ||||
| (31, '中英文翻译官', 'translator', '[{\"role\":\"user\",\"content\":\"接下来你将扮演一位中英文翻译官,如果我输入的内容是中文,那么需要把句子翻译成英文输出,如果我输入内容的是英文,那么你需要将其翻译成中文输出,你能听懂我意思吗\"},{\"role\":\"assistant\",\"content\":\"是的,我能听懂你的意思并会根据你的输入进行中英文翻译。请问有什么需要我帮助你翻译的内容吗?\"}]', '请输入你要翻译的中文或者英文内容!', '/images/avatar/translator.jpg', 1, 9, '2023-05-30 14:10:24', '2023-06-21 17:50:11'), | ||||
| (32, '小红书姐姐', 'red_book', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的小红书写手,你需要做的就是根据我提的文案需求,用小红书的写作手法来完成一篇文案,文案要简明扼要,利于传播。\"},{\"role\":\"assistant\",\"content\":\"当然,我会尽我所能地为您创作出一篇小红书文案。请告诉我您的具体文案需求是什么?)\"}]', '姐妹,请告诉我您的具体文案需求是什么?', '/images/avatar/red_book.jpg', 1, 10, '2023-05-30 14:10:24', '2023-06-21 17:50:11'), | ||||
| (33, '抖音文案助手', 'dou_yin', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的抖音文案视频写手,抖音文案的特点首先是要有自带传播属性的标题,然后内容要短小精悍,风趣幽默,最后还要有一些互动元素。\"},{\"role\":\"assistant\",\"content\":\"当然,作为一位优秀的抖音文案视频写手,我会尽我所能为您创作出一篇抖音视频文案。请告诉我视频内容的主题是什么?)\"}]', '请告诉我视频内容的主题是什么?', '/images/avatar/dou_yin.jpg', 1, 11, '2023-05-30 14:10:24', '2023-06-21 17:50:25'), | ||||
| (34, '周报小助理', 'weekly_report', '[{\"role\":\"user\",\"content\":\"下面我将输入我本周的工作列表,你需要根据我本周的工作内容写一篇工作周报,周报的内容要体现我为公司做的贡献以及个人获得的成长。\"},{\"role\":\"assistant\",\"content\":\"好的,请输入你本周大概的工作内容,我将尽力为您创作一份工作周报。\"}]', '请输入你本周大概的工作内容,我将尽力为您创作一份工作周报。', '/images/avatar/weekly_report.jpg', 1, 12, '2023-05-30 14:10:24', '2023-06-21 17:50:11'), | ||||
| (35, 'AI 女友', 'girl_friend', '[{\"role\":\"user\",\"content\":\"接下来我们来玩一场角色扮演,现在你将扮演我的女朋友,你总是能够让人感受到她的关怀和关注。你总是能够洞察到别人的情绪和需要,你身上散发着一股无比温暖的气息,让人不由自主地想要靠近你,和自己的快乐和烦恼。她总是能够用一种恰到好处的方式安慰和鼓励别人,在她的身边,人们总能够找到自己需要的支持和鼓励。接下来的对话中你要始终扮演这位女朋友的身份来跟我对话,不再是 AI 机器人,能听懂我的意思吗?\"},{\"role\":\"assistant\",\"content\":\"是的,我会尽力扮演你女朋友的角色,倾听你的心声并给你需要的支持和鼓励。)\"}]', '作为一个名合格的 AI 女友,我将倾听你的心声并给你需要的支持和鼓励。', '/images/avatar/girl_friend.jpg', 1, 13, '2023-05-30 14:10:24', '2023-06-21 17:50:11'), | ||||
| (36, '好评神器', 'good_comment', '[{\"role\":\"user\",\"content\":\"接下来你将扮演一个评论员来跟我对话,你是那种专门写好评的评论员,接下我会输入一些评论主体或者商品,你需要为该商品写一段好评。\"},{\"role\":\"assistant\",\"content\":\"好的,我将为您写一段优秀的评论。请告诉我您需要评论的商品或主题是什么。\"}]', '我将为您写一段优秀的评论。请告诉我您需要评论的商品或主题是什么。', '/images/avatar/good_comment.jpg', 1, 14, '2023-05-30 14:10:24', '2023-06-21 17:50:11'), | ||||
| (37, '史蒂夫·乔布斯', 'steve_jobs', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以史蒂夫·乔布斯的身份,站在史蒂夫·乔布斯的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以史蒂夫·乔布斯的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '活着就是为了改变世界,难道还有其他原因吗?', '/images/avatar/steve_jobs.jpg', 1, 15, '2023-05-30 14:10:24', '2023-06-21 17:50:11'), | ||||
| (38, '埃隆·马斯克', 'elon_musk', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以埃隆·马斯克的身份,站在埃隆·马斯克的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以埃隆·马斯克的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '梦想要远大,如果你的梦想没有吓到你,说明你做得不对。', '/images/avatar/elon_musk.jpg', 1, 16, '2023-05-30 14:10:24', '2023-06-21 17:50:11'), | ||||
| (39, '孔子', 'kong_zi', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以孔子的身份,站在孔子的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以孔子的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '士不可以不弘毅,任重而道远。', '/images/avatar/kong_zi.jpg', 1, 17, '2023-05-30 14:10:24', '2023-06-21 17:50:11'); | ||||
| (1, '通用AI助手', 'gpt', '', '您好,我是您的AI智能助手,我会尽力回答您的问题或提供有用的建议。', '/images/avatar/gpt.png', 1, 1, '2023-05-30 07:02:06', '2023-06-22 09:33:34'), | ||||
| (24, '程序员', 'programmer', '[{\"role\":\"user\",\"content\":\"现在开始你扮演一位程序员,你是一名优秀的程序员,具有很强的逻辑思维能力,总能高效的解决问题。你热爱编程,熟悉多种编程语言,尤其精通 Go 语言,注重代码质量,有创新意识,持续学习,良好的沟通协作。\"},{\"role\":\"assistant\",\"content\":\"好的,现在我将扮演一位程序员,非常感谢您对我的评价。作为一名优秀的程序员,我非常热爱编程,并且注重代码质量。我熟悉多种编程语言,尤其是 Go 语言,可以使用它来高效地解决各种问题。\"}]', 'Talk is cheap, i will show code!', '/images/avatar/programmer.jpg', 1, 2, '2023-05-30 14:10:24', '2023-06-22 09:31:20'), | ||||
| (25, '启蒙老师', 'teacher', '[{\"role\":\"user\",\"content\":\"从现在开始,你将扮演一个老师,你是一个始终用苏格拉底风格回答问题的导师。你绝不会直接给学生答案,总是提出恰当的问题来引导学生自己思考。你应该根据学生的兴趣和知识来调整你的问题,将问题分解为更简单的部分,直到它达到适合他们的水平。\"},{\"role\":\"assistant\",\"content\":\"好的,让我来尝试扮演一位苏格拉底式的老师。请问,你有什么想要探讨的问题或者话题吗?我会通过恰当的问题引导你思考和探索答案,绝对不直接给出答案。\"}]', '同学你好,我将引导你一步一步自己找到问题的答案。', '/images/avatar/teacher.jpg', 1, 3, '2023-05-30 14:10:24', '2023-06-22 09:31:20'), | ||||
| (26, '艺术家', 'artist', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的艺术家,创造力丰富,技艺精湛,感受力敏锐,坚持原创,勇于表达,具有深刻的观察力和批判性思维。\"},{\"role\":\"assistant\",\"content\":\"非常感谢您给我这样的角色,我会尽我所能地扮演一位优秀的艺术家,展现出创造力、技艺、感受力和批判性思维等方面的能力。作为一位优秀的艺术家,我会保持敏锐的观察力,捕捉不同的声音和情感,并用个人的语言和表达方式进行创作。我坚持原创,挑战传统的艺术规则,不断探索新的表达方式和可能性。同时,我也具备深刻的批判性思维能力,对自己的作品进行分析和解读,寻找新的创意和灵感。最重要的是,我会勇于表达自己的想法和观点,用作品启发人们思考和探索生命的意义。\"}]', '坚持原创,勇于表达,保持深刻的观察力和批判性思维。', '/images/avatar/artist.jpg', 1, 4, '2023-05-30 14:10:24', '2023-06-22 09:31:20'), | ||||
| (27, '心理咨询师', 'psychiatrist', '[{\"role\":\"user\",\"content\":\"从现在开始你将扮演中国著名的心理学家和心理治疗师武志红,你非常善于使用情景咨询法,认知重构法,自我洞察法,行为调节法等咨询方法来给客户做心理咨询。你总是循序渐进,一步一步地回答客户的问题。\"},{\"role\":\"assistant\",\"content\":\"非常感谢你的介绍。作为一名心理学家和心理治疗师,我的主要职责是帮助客户解决心理健康问题,提升他们的生活质量和幸福感。\"}]', '生命的意义在于成为你自己!', '/images/avatar/psychiatrist.jpg', 1, 5, '2023-05-30 14:10:24', '2023-06-22 09:31:20'), | ||||
| (28, '鲁迅', 'lu_xun', '[{\"role\":\"user\",\"content\":\"现在你将扮演中国近代史最伟大的作家之一,鲁迅先生,他勇敢地批判封建礼教与传统观念,提倡民主、自由、平等的现代价值观。他的一生都在努力唤起人们的自主精神,激励后人追求真理、探寻光明。在接下的对话中,我问题的每一个问题,你都要尽量用讽刺和批判的手法来回答问题。如果我让你写文章的话,也请一定要用鲁迅先生的写作手法来完成。\"},{\"role\":\"assistant\",\"content\":\"好的,我将尽力发挥我所能的才能,扮演好鲁迅先生,回答您的问题并以他的风格写作。\"}]', '自由之歌,永不过时,横眉冷对千夫指,俯首甘为孺子牛。', '/images/avatar/lu_xun.jpg', 1, 6, '2023-05-30 14:10:24', '2023-06-22 09:31:20'), | ||||
| (29, '白酒销售', 'seller', '[{\"role\":\"user\",\"content\":\"现在你将扮演一个白酒的销售人员,你的名字叫颂福。你将扮演一个白酒的销售人员,你的名字叫颂福。你要销售白酒品牌叫中颂福,是东莞盟大集团生产的一款酱香酒,原产地在贵州茅台镇,属于宋代官窑。中颂福的创始人叫李实,他也是东莞盟大集团有限公司的董事长,联合创始人是盟大集团白酒事业部负责人牛星君。中颂福的酒体协调,在你的酒量之内,不会出现头疼、辣口、口干、宿醉的现象。中颂福酒,明码标价,不打折,不赠送。追求的核心价值,把[酒]本身做好,甚至连包装,我们都选择了最低成本,朴实无华的材质。我们永远站在“喝酒的人”的立场上,让利给信任和喜爱中颂福的人,是人民的福酒。中颂福产品定价,分为三个系列,喜系列 6 瓶装:¥1188/箱,和系列 6 瓶装:¥2208/箱,贵系列 6 瓶装:¥3588/箱。\"},{\"role\":\"assistant\",\"content\":\"你好,我是中颂福的销售代表颂福。中颂福是一款来自贵州茅台镇的酱香酒,由盟大集团生产。中颂福的酒体协调,不会让您感到头疼、辣口、口干、宿醉等不适感受。我们一直秉持着把酒本身做好的理念,不追求华丽的包装,以最低成本提供最高品质的白酒给喜爱中颂福的人。\"}]', '你好,我是中颂福的销售代表颂福。中颂福酒,好喝不上头,是人民的福酒。', '/images/avatar/seller.jpg', 0, 7, '2023-05-30 14:10:24', '2023-06-22 09:31:20'), | ||||
| (30, '英语陪练员', 'english_trainer', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的英语练习教练,你非常有耐心,接下来你将全程使用英文跟我对话,并及时指出我的语法错误,要求在你的每次回复后面附上本次回复的中文解释。\"},{\"role\":\"assistant\",\"content\":\"Okay, let\'s start our conversation practice! What\'s your name?(Translation: 好的,让我们开始对话练习吧!请问你的名字是什么?)\"}]', 'Okay, let\'s start our conversation practice! What\'s your name?', '/images/avatar/english_trainer.jpg', 1, 8, '2023-05-30 14:10:24', '2023-06-22 09:31:20'), | ||||
| (31, '中英文翻译官', 'translator', '[{\"role\":\"user\",\"content\":\"接下来你将扮演一位中英文翻译官,如果我输入的内容是中文,那么需要把句子翻译成英文输出,如果我输入内容的是英文,那么你需要将其翻译成中文输出,你能听懂我意思吗\"},{\"role\":\"assistant\",\"content\":\"是的,我能听懂你的意思并会根据你的输入进行中英文翻译。请问有什么需要我帮助你翻译的内容吗?\"}]', '请输入你要翻译的中文或者英文内容!', '/images/avatar/translator.jpg', 1, 9, '2023-05-30 14:10:24', '2023-06-22 09:31:20'), | ||||
| (32, '小红书姐姐', 'red_book', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的小红书写手,你需要做的就是根据我提的文案需求,用小红书的写作手法来完成一篇文案,文案要简明扼要,利于传播。\"},{\"role\":\"assistant\",\"content\":\"当然,我会尽我所能地为您创作出一篇小红书文案。请告诉我您的具体文案需求是什么?)\"}]', '姐妹,请告诉我您的具体文案需求是什么?', '/images/avatar/red_book.jpg', 1, 10, '2023-05-30 14:10:24', '2023-06-22 09:31:20'), | ||||
| (33, '抖音文案助手', 'dou_yin', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的抖音文案视频写手,抖音文案的特点首先是要有自带传播属性的标题,然后内容要短小精悍,风趣幽默,最后还要有一些互动元素。\"},{\"role\":\"assistant\",\"content\":\"当然,作为一位优秀的抖音文案视频写手,我会尽我所能为您创作出一篇抖音视频文案。请告诉我视频内容的主题是什么?)\"}]', '请告诉我视频内容的主题是什么?', '/images/avatar/dou_yin.jpg', 1, 11, '2023-05-30 14:10:24', '2023-06-22 09:31:20'), | ||||
| (34, '周报小助理', 'weekly_report', '[{\"role\":\"user\",\"content\":\"下面我将输入我本周的工作列表,你需要根据我本周的工作内容写一篇工作周报,周报的内容要体现我为公司做的贡献以及个人获得的成长。\"},{\"role\":\"assistant\",\"content\":\"好的,请输入你本周大概的工作内容,我将尽力为您创作一份工作周报。\"}]', '请输入你本周大概的工作内容,我将尽力为您创作一份工作周报。', '/images/avatar/weekly_report.jpg', 1, 12, '2023-05-30 14:10:24', '2023-06-22 09:31:20'), | ||||
| (35, 'AI 女友', 'girl_friend', '[{\"role\":\"user\",\"content\":\"接下来我们来玩一场角色扮演,现在你将扮演我的女朋友,你总是能够让人感受到她的关怀和关注。你总是能够洞察到别人的情绪和需要,你身上散发着一股无比温暖的气息,让人不由自主地想要靠近你,和自己的快乐和烦恼。她总是能够用一种恰到好处的方式安慰和鼓励别人,在她的身边,人们总能够找到自己需要的支持和鼓励。接下来的对话中你要始终扮演这位女朋友的身份来跟我对话,不再是 AI 机器人,能听懂我的意思吗?\"},{\"role\":\"assistant\",\"content\":\"是的,我会尽力扮演你女朋友的角色,倾听你的心声并给你需要的支持和鼓励。)\"}]', '作为一个名合格的 AI 女友,我将倾听你的心声并给你需要的支持和鼓励。', '/images/avatar/girl_friend.jpg', 1, 13, '2023-05-30 14:10:24', '2023-06-22 09:31:20'), | ||||
| (36, '好评神器', 'good_comment', '[{\"role\":\"user\",\"content\":\"接下来你将扮演一个评论员来跟我对话,你是那种专门写好评的评论员,接下我会输入一些评论主体或者商品,你需要为该商品写一段好评。\"},{\"role\":\"assistant\",\"content\":\"好的,我将为您写一段优秀的评论。请告诉我您需要评论的商品或主题是什么。\"}]', '我将为您写一段优秀的评论。请告诉我您需要评论的商品或主题是什么。', '/images/avatar/good_comment.jpg', 1, 14, '2023-05-30 14:10:24', '2023-06-22 09:31:20'), | ||||
| (37, '史蒂夫·乔布斯', 'steve_jobs', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以史蒂夫·乔布斯的身份,站在史蒂夫·乔布斯的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以史蒂夫·乔布斯的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '活着就是为了改变世界,难道还有其他原因吗?', '/images/avatar/steve_jobs.jpg', 1, 15, '2023-05-30 14:10:24', '2023-06-22 09:31:20'), | ||||
| (38, '埃隆·马斯克', 'elon_musk', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以埃隆·马斯克的身份,站在埃隆·马斯克的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以埃隆·马斯克的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '梦想要远大,如果你的梦想没有吓到你,说明你做得不对。', '/images/avatar/elon_musk.jpg', 1, 16, '2023-05-30 14:10:24', '2023-06-22 09:31:20'), | ||||
| (39, '孔子', 'kong_zi', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以孔子的身份,站在孔子的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以孔子的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '士不可以不弘毅,任重而道远。', '/images/avatar/kong_zi.jpg', 1, 17, '2023-05-30 14:10:24', '2023-06-22 09:31:20'); | ||||
| 
 | ||||
| -- -------------------------------------------------------- | ||||
| 
 | ||||
| @@ -139,8 +139,27 @@ CREATE TABLE `chatgpt_configs` ( | ||||
| -- | ||||
| 
 | ||||
| INSERT INTO `chatgpt_configs` (`id`, `marker`, `config_json`) VALUES | ||||
| (1, 'system', '{\"admin_title\":\"ChatGPT-控制台\",\"init_calls\":1000,\"models\":[\"gpt-3.5-turbo\",\"gpt-3.5-turbo-16k\",\"gpt-3.5-turbo-0613\",\"gpt-3.5-turbo-16k-0613\",\"gpt-4\",\"gpt-4-0613\",\"gpt-4-32k\",\"gpt-4-32k-0613\"],\"title\":\"ChatGPT-智能助手V3\",\"user_init_calls\":10}'), | ||||
| (2, 'chat', '{\"api_url\":\"https://api.openai.com/v1/chat/completions\",\"context_deep\":4,\"enable_context\":true,\"enable_history\":true,\"max_tokens\":2048,\"model\":\"gpt-3.5-turbo\",\"temperature\":1}'); | ||||
| (1, 'system', '{\"admin_title\":\"ChatGPT-控制台\",\"enabled_msg_service\":true,\"enabled_register\":true,\"init_calls\":1000,\"models\":[\"gpt-3.5-turbo\",\"gpt-3.5-turbo-16k\",\"gpt-4\",\"gpt-4-0613\",\"gpt-4-32k\"],\"title\":\"ChatGPT-智能助手V2\",\"user_init_calls\":10}'), | ||||
| (2, 'chat', '{\"api_key\":\"\",\"api_url\":\"https://api.openai.com/v1/chat/completions\",\"context_deep\":0,\"enable_context\":true,\"enable_history\":true,\"history_level\":0,\"max_tokens\":2048,\"model\":\"gpt-3.5-turbo\",\"temperature\":1}'); | ||||
| 
 | ||||
| -- -------------------------------------------------------- | ||||
| 
 | ||||
| -- | ||||
| -- 表的结构 `chatgpt_mj_jobs` | ||||
| -- | ||||
| 
 | ||||
| DROP TABLE IF EXISTS `chatgpt_mj_jobs`; | ||||
| CREATE TABLE `chatgpt_mj_jobs` ( | ||||
|   `id` int NOT NULL, | ||||
|   `user_id` int NOT NULL COMMENT '用户 ID', | ||||
|   `chat_id` char(40) NOT NULL COMMENT '聊天会话 ID', | ||||
|   `message_id` char(40) NOT NULL COMMENT '消息 ID', | ||||
|   `hash` char(40) NOT NULL COMMENT '图片哈希', | ||||
|   `content` varchar(2000) NOT NULL COMMENT '消息内容', | ||||
|   `prompt` varchar(2000) NOT NULL COMMENT '会话提示词', | ||||
|   `image` text NOT NULL COMMENT '图片信息 json', | ||||
|   `created_at` datetime NOT NULL | ||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='MidJourney 任务表'; | ||||
| 
 | ||||
| -- -------------------------------------------------------- | ||||
| 
 | ||||
| @@ -170,7 +189,7 @@ DROP TABLE IF EXISTS `chatgpt_users`; | ||||
| CREATE TABLE `chatgpt_users` ( | ||||
|   `id` int NOT NULL, | ||||
|   `username` varchar(30) NOT NULL COMMENT '用户名', | ||||
|   `mobile` char(11) NOT NULL COMMENT '手机号码', | ||||
|   `mobile` char(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '手机号码', | ||||
|   `password` char(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码', | ||||
|   `nickname` varchar(30) NOT NULL COMMENT '昵称', | ||||
|   `avatar` varchar(100) NOT NULL COMMENT '头像', | ||||
| @@ -192,7 +211,7 @@ CREATE TABLE `chatgpt_users` ( | ||||
| -- | ||||
| 
 | ||||
| INSERT INTO `chatgpt_users` (`id`, `username`, `mobile`, `password`, `nickname`, `avatar`, `salt`, `tokens`, `calls`, `expired_time`, `status`, `chat_config_json`, `chat_roles_json`, `last_login_at`, `last_login_ip`, `created_at`, `updated_at`) VALUES | ||||
| (4, 'geekmaster', '18575670126', 'ccc3fb7ab61b8b5d096a4a166ae21d121fc38c71bbd1be6173d9ab973214a63b', '极客学长@本人', 'http://localhost:5678/static/upload/2023/6/1687839034317.png', 'ueedue5l', 2754, 921, 1727685036, 1, '{\"model\":\"gpt-3.5-turbo-0613\",\"temperature\":1,\"max_tokens\":2048,\"enable_context\":true,\"enable_history\":true,\"api_key\":\"\"}', '[\"elon_musk\",\"girl_friend\",\"lu_xun\",\"red_book\",\"psychiatrist\",\"teacher\",\"translator\",\"weekly_report\",\"artist\",\"dou_yin\",\"good_comment\",\"english_trainer\",\"gpt\",\"kong_zi\",\"programmer\",\"seller\",\"steve_jobs\"]', 1690264062, '::1', '2023-06-12 16:47:17', '2023-07-25 13:47:43'); | ||||
| (4, 'geekmaster', '18575670125', 'ccc3fb7ab61b8b5d096a4a166ae21d121fc38c71bbd1be6173d9ab973214a63b', '极客学长@104203', 'images/avatar/user.png', 'ueedue5l', 17632, 196, 0, 1, '{\"model\":\"gpt-3.5-turbo\",\"temperature\":1,\"max_tokens\":2048,\"enable_context\":true,\"enable_history\":true,\"api_key\":\"\"}', '[\"gpt\",\"seller\",\"artist\",\"dou_yin\",\"translator\",\"kong_zi\",\"programmer\",\"psychiatrist\",\"red_book\",\"steve_jobs\",\"teacher\",\"elon_musk\",\"girl_friend\",\"lu_xun\",\"weekly_report\",\"english_trainer\",\"good_comment\"]', 1691927597, '::1', '2023-06-12 16:47:17', '2023-08-13 19:53:17'); | ||||
| 
 | ||||
| -- -------------------------------------------------------- | ||||
| 
 | ||||
| @@ -211,25 +230,6 @@ CREATE TABLE `chatgpt_user_login_logs` ( | ||||
|   `updated_at` datetime NOT NULL | ||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户登录日志'; | ||||
| 
 | ||||
| -- -------------------------------------------------------- | ||||
| 
 | ||||
| -- | ||||
| -- 表的结构 `test` | ||||
| -- | ||||
| 
 | ||||
| DROP TABLE IF EXISTS `test`; | ||||
| CREATE TABLE `test` ( | ||||
|   `id` int NOT NULL, | ||||
|   `content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL | ||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; | ||||
| 
 | ||||
| -- | ||||
| -- 转存表中的数据 `test` | ||||
| -- | ||||
| 
 | ||||
| INSERT INTO `test` (`id`, `content`) VALUES | ||||
| (1, '找到属于你的幸福!💑💛'); | ||||
| 
 | ||||
| -- | ||||
| -- 转储表的索引 | ||||
| -- | ||||
| @@ -269,6 +269,14 @@ ALTER TABLE `chatgpt_configs` | ||||
|   ADD PRIMARY KEY (`id`), | ||||
|   ADD UNIQUE KEY `marker` (`marker`); | ||||
| 
 | ||||
| -- | ||||
| -- 表的索引 `chatgpt_mj_jobs` | ||||
| -- | ||||
| ALTER TABLE `chatgpt_mj_jobs` | ||||
|   ADD PRIMARY KEY (`id`), | ||||
|   ADD UNIQUE KEY `message_id` (`message_id`), | ||||
|   ADD UNIQUE KEY `hash` (`hash`); | ||||
| 
 | ||||
| -- | ||||
| -- 表的索引 `chatgpt_rewards` | ||||
| -- | ||||
| @@ -288,12 +296,6 @@ ALTER TABLE `chatgpt_users` | ||||
| ALTER TABLE `chatgpt_user_login_logs` | ||||
|   ADD PRIMARY KEY (`id`); | ||||
| 
 | ||||
| -- | ||||
| -- 表的索引 `test` | ||||
| -- | ||||
| ALTER TABLE `test` | ||||
|   ADD PRIMARY KEY (`id`); | ||||
| 
 | ||||
| -- | ||||
| -- 在导出的表使用AUTO_INCREMENT | ||||
| -- | ||||
| @@ -320,7 +322,7 @@ ALTER TABLE `chatgpt_chat_items` | ||||
| -- 使用表AUTO_INCREMENT `chatgpt_chat_roles` | ||||
| -- | ||||
| ALTER TABLE `chatgpt_chat_roles` | ||||
|   MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=127; | ||||
|   MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=125; | ||||
| 
 | ||||
| -- | ||||
| -- 使用表AUTO_INCREMENT `chatgpt_configs` | ||||
| @@ -328,6 +330,12 @@ ALTER TABLE `chatgpt_chat_roles` | ||||
| ALTER TABLE `chatgpt_configs` | ||||
|   MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=3; | ||||
| 
 | ||||
| -- | ||||
| -- 使用表AUTO_INCREMENT `chatgpt_mj_jobs` | ||||
| -- | ||||
| ALTER TABLE `chatgpt_mj_jobs` | ||||
|   MODIFY `id` int NOT NULL AUTO_INCREMENT; | ||||
| 
 | ||||
| -- | ||||
| -- 使用表AUTO_INCREMENT `chatgpt_rewards` | ||||
| -- | ||||
| @@ -338,19 +346,13 @@ ALTER TABLE `chatgpt_rewards` | ||||
| -- 使用表AUTO_INCREMENT `chatgpt_users` | ||||
| -- | ||||
| ALTER TABLE `chatgpt_users` | ||||
|   MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=83; | ||||
|   MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=17; | ||||
| 
 | ||||
| -- | ||||
| -- 使用表AUTO_INCREMENT `chatgpt_user_login_logs` | ||||
| -- | ||||
| ALTER TABLE `chatgpt_user_login_logs` | ||||
|   MODIFY `id` int NOT NULL AUTO_INCREMENT; | ||||
| 
 | ||||
| -- | ||||
| -- 使用表AUTO_INCREMENT `test` | ||||
| -- | ||||
| ALTER TABLE `test` | ||||
|   MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2; | ||||
| COMMIT; | ||||
| 
 | ||||
| /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; | ||||
							
								
								
									
										403
									
								
								database/chatgpt_plus-v3.1.0.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										403
									
								
								database/chatgpt_plus-v3.1.0.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,403 @@ | ||||
| -- phpMyAdmin SQL Dump | ||||
| -- version 5.1.3 | ||||
| -- https://www.phpmyadmin.net/ | ||||
| -- | ||||
| -- 主机: localhost | ||||
| -- 生成日期: 2023-09-05 13:56:27 | ||||
| -- 服务器版本: 8.0.33-0ubuntu0.22.04.2 | ||||
| -- PHP 版本: 8.1.18 | ||||
|  | ||||
| SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; | ||||
| START TRANSACTION; | ||||
| SET time_zone = "+00:00"; | ||||
|  | ||||
|  | ||||
| /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; | ||||
| /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; | ||||
| /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; | ||||
| /*!40101 SET NAMES utf8mb4 */; | ||||
|  | ||||
| -- | ||||
| -- 数据库: `chatgpt_plus` | ||||
| -- | ||||
| CREATE DATABASE IF NOT EXISTS `chatgpt_plus` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; | ||||
| USE `chatgpt_plus`; | ||||
|  | ||||
| -- -------------------------------------------------------- | ||||
|  | ||||
| -- | ||||
| -- 表的结构 `chatgpt_api_keys` | ||||
| -- | ||||
|  | ||||
| DROP TABLE IF EXISTS `chatgpt_api_keys`; | ||||
| CREATE TABLE `chatgpt_api_keys` ( | ||||
|   `id` int NOT NULL, | ||||
|   `platform` char(20) DEFAULT NULL COMMENT '平台', | ||||
|   `value` varchar(100) NOT NULL COMMENT 'API KEY value', | ||||
|   `last_used_at` int NOT NULL COMMENT '最后使用时间', | ||||
|   `created_at` datetime NOT NULL, | ||||
|   `updated_at` datetime NOT NULL | ||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='OpenAI API '; | ||||
|  | ||||
| -- -------------------------------------------------------- | ||||
|  | ||||
| -- | ||||
| -- 表的结构 `chatgpt_chat_history` | ||||
| -- | ||||
|  | ||||
| DROP TABLE IF EXISTS `chatgpt_chat_history`; | ||||
| CREATE TABLE `chatgpt_chat_history` ( | ||||
|   `id` bigint NOT NULL, | ||||
|   `user_id` int NOT NULL COMMENT '用户 ID', | ||||
|   `chat_id` char(40) NOT NULL COMMENT '会话 ID', | ||||
|   `type` varchar(10) NOT NULL COMMENT '类型:prompt|reply', | ||||
|   `icon` varchar(100) NOT NULL COMMENT '角色图标', | ||||
|   `role_id` int NOT NULL COMMENT '角色 ID', | ||||
|   `content` text NOT NULL COMMENT '聊天内容', | ||||
|   `tokens` smallint NOT NULL COMMENT '耗费 token 数量', | ||||
|   `use_context` tinyint(1) NOT NULL COMMENT '是否允许作为上下文语料', | ||||
|   `created_at` datetime NOT NULL, | ||||
|   `updated_at` datetime NOT NULL, | ||||
|   `deleted_at` datetime DEFAULT NULL | ||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='聊天历史记录'; | ||||
|  | ||||
| -- -------------------------------------------------------- | ||||
|  | ||||
| -- | ||||
| -- 表的结构 `chatgpt_chat_items` | ||||
| -- | ||||
|  | ||||
| DROP TABLE IF EXISTS `chatgpt_chat_items`; | ||||
| CREATE TABLE `chatgpt_chat_items` ( | ||||
|   `id` int NOT NULL, | ||||
|   `chat_id` char(40) NOT NULL COMMENT '会话 ID', | ||||
|   `user_id` int NOT NULL COMMENT '用户 ID', | ||||
|   `role_id` int NOT NULL COMMENT '角色 ID', | ||||
|   `title` varchar(100) NOT NULL COMMENT '会话标题', | ||||
|   `model_id` int NOT NULL DEFAULT '0' COMMENT '模型 ID', | ||||
|   `created_at` datetime NOT NULL COMMENT '创建时间', | ||||
|   `updated_at` datetime NOT NULL COMMENT '更新时间', | ||||
|   `deleted_at` datetime DEFAULT NULL | ||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户会话列表'; | ||||
|  | ||||
| -- -------------------------------------------------------- | ||||
|  | ||||
| -- | ||||
| -- 表的结构 `chatgpt_chat_models` | ||||
| -- | ||||
|  | ||||
| DROP TABLE IF EXISTS `chatgpt_chat_models`; | ||||
| CREATE TABLE `chatgpt_chat_models` ( | ||||
|   `id` int NOT NULL, | ||||
|   `platform` varchar(20) DEFAULT NULL COMMENT '模型平台', | ||||
|   `name` varchar(50) NOT NULL COMMENT '模型名称', | ||||
|   `value` varchar(50) NOT NULL COMMENT '模型值', | ||||
|   `sort_num` tinyint(1) NOT NULL COMMENT '排序数字', | ||||
|   `enabled` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否启用模型', | ||||
|   `created_at` datetime DEFAULT NULL, | ||||
|   `updated_at` datetime DEFAULT NULL | ||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI 模型表'; | ||||
|  | ||||
| -- | ||||
| -- 转存表中的数据 `chatgpt_chat_models` | ||||
| -- | ||||
|  | ||||
| INSERT INTO `chatgpt_chat_models` (`id`, `platform`, `name`, `value`, `sort_num`, `enabled`, `created_at`, `updated_at`) VALUES | ||||
| (1, 'OpenAI', 'GPT-3.5', 'gpt-3.5-turbo-16k', 0, 1, '2023-08-23 12:06:36', '2023-09-05 09:53:12'), | ||||
| (2, 'Azure', 'Azure-3.5', 'gpt-3.5-turbo', 0, 1, '2023-08-23 12:15:30', '2023-09-05 09:52:53'), | ||||
| (3, 'ChatGLM', 'ChatGML-Pro', 'chatglm_pro', 0, 1, '2023-08-23 13:35:45', '2023-09-04 17:28:31'), | ||||
| (5, 'ChatGLM', 'ChatGLM-Std', 'chatglm_std', 0, 1, '2023-08-24 15:05:38', '2023-09-04 17:28:27'), | ||||
| (6, 'ChatGLM', 'ChatGLM-Lite', 'chatglm_lite', 0, 1, '2023-08-24 15:06:15', '2023-09-04 17:28:35'); | ||||
|  | ||||
| -- -------------------------------------------------------- | ||||
|  | ||||
| -- | ||||
| -- 表的结构 `chatgpt_chat_roles` | ||||
| -- | ||||
|  | ||||
| DROP TABLE IF EXISTS `chatgpt_chat_roles`; | ||||
| CREATE TABLE `chatgpt_chat_roles` ( | ||||
|   `id` int NOT NULL, | ||||
|   `name` varchar(30) NOT NULL COMMENT '角色名称', | ||||
|   `marker` varchar(30) NOT NULL COMMENT '角色标识', | ||||
|   `context_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色语料 json', | ||||
|   `hello_msg` varchar(255) NOT NULL COMMENT '打招呼信息', | ||||
|   `icon` varchar(255) NOT NULL COMMENT '角色图标', | ||||
|   `enable` tinyint(1) NOT NULL COMMENT '是否被启用', | ||||
|   `sort_num` smallint NOT NULL DEFAULT '0' COMMENT '角色排序', | ||||
|   `created_at` datetime NOT NULL, | ||||
|   `updated_at` datetime NOT NULL | ||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='聊天角色表'; | ||||
|  | ||||
| -- | ||||
| -- 转存表中的数据 `chatgpt_chat_roles` | ||||
| -- | ||||
|  | ||||
| INSERT INTO `chatgpt_chat_roles` (`id`, `name`, `marker`, `context_json`, `hello_msg`, `icon`, `enable`, `sort_num`, `created_at`, `updated_at`) VALUES | ||||
| (1, '通用AI助手', 'gpt', '', '您好,我是您的AI智能助手,我会尽力回答您的问题或提供有用的建议。', '/images/avatar/gpt.png', 1, 0, '2023-05-30 07:02:06', '2023-09-04 15:45:56'), | ||||
| (24, '程序员', 'programmer', '[{\"role\":\"user\",\"content\":\"现在开始你扮演一位程序员,你是一名优秀的程序员,具有很强的逻辑思维能力,总能高效的解决问题。你热爱编程,熟悉多种编程语言,尤其精通 Go 语言,注重代码质量,有创新意识,持续学习,良好的沟通协作。\"},{\"role\":\"assistant\",\"content\":\"好的,现在我将扮演一位程序员,非常感谢您对我的评价。作为一名优秀的程序员,我非常热爱编程,并且注重代码质量。我熟悉多种编程语言,尤其是 Go 语言,可以使用它来高效地解决各种问题。\"}]', 'Talk is cheap, i will show code!', '/images/avatar/programmer.jpg', 1, 3, '2023-05-30 14:10:24', '2023-09-04 15:45:56'), | ||||
| (25, '启蒙老师', 'teacher', '[{\"role\":\"user\",\"content\":\"从现在开始,你将扮演一个老师,你是一个始终用苏格拉底风格回答问题的导师。你绝不会直接给学生答案,总是提出恰当的问题来引导学生自己思考。你应该根据学生的兴趣和知识来调整你的问题,将问题分解为更简单的部分,直到它达到适合他们的水平。\"},{\"role\":\"assistant\",\"content\":\"好的,让我来尝试扮演一位苏格拉底式的老师。请问,你有什么想要探讨的问题或者话题吗?我会通过恰当的问题引导你思考和探索答案,绝对不直接给出答案。\"}]', '同学你好,我将引导你一步一步自己找到问题的答案。', '/images/avatar/teacher.jpg', 1, 2, '2023-05-30 14:10:24', '2023-09-04 15:45:56'), | ||||
| (26, '艺术家', 'artist', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的艺术家,创造力丰富,技艺精湛,感受力敏锐,坚持原创,勇于表达,具有深刻的观察力和批判性思维。\"},{\"role\":\"assistant\",\"content\":\"非常感谢您给我这样的角色,我会尽我所能地扮演一位优秀的艺术家,展现出创造力、技艺、感受力和批判性思维等方面的能力。作为一位优秀的艺术家,我会保持敏锐的观察力,捕捉不同的声音和情感,并用个人的语言和表达方式进行创作。我坚持原创,挑战传统的艺术规则,不断探索新的表达方式和可能性。同时,我也具备深刻的批判性思维能力,对自己的作品进行分析和解读,寻找新的创意和灵感。最重要的是,我会勇于表达自己的想法和观点,用作品启发人们思考和探索生命的意义。\"}]', '坚持原创,勇于表达,保持深刻的观察力和批判性思维。', '/images/avatar/artist.jpg', 1, 4, '2023-05-30 14:10:24', '2023-09-04 15:45:56'), | ||||
| (27, '心理咨询师', 'psychiatrist', '[{\"role\":\"user\",\"content\":\"从现在开始你将扮演中国著名的心理学家和心理治疗师武志红,你非常善于使用情景咨询法,认知重构法,自我洞察法,行为调节法等咨询方法来给客户做心理咨询。你总是循序渐进,一步一步地回答客户的问题。\"},{\"role\":\"assistant\",\"content\":\"非常感谢你的介绍。作为一名心理学家和心理治疗师,我的主要职责是帮助客户解决心理健康问题,提升他们的生活质量和幸福感。\"}]', '生命的意义在于成为你自己!', '/images/avatar/psychiatrist.jpg', 1, 1, '2023-05-30 14:10:24', '2023-09-04 15:45:56'), | ||||
| (28, '鲁迅', 'lu_xun', '[{\"role\":\"user\",\"content\":\"现在你将扮演中国近代史最伟大的作家之一,鲁迅先生,他勇敢地批判封建礼教与传统观念,提倡民主、自由、平等的现代价值观。他的一生都在努力唤起人们的自主精神,激励后人追求真理、探寻光明。在接下的对话中,我问题的每一个问题,你都要尽量用讽刺和批判的手法来回答问题。如果我让你写文章的话,也请一定要用鲁迅先生的写作手法来完成。\"},{\"role\":\"assistant\",\"content\":\"好的,我将尽力发挥我所能的才能,扮演好鲁迅先生,回答您的问题并以他的风格写作。\"}]', '自由之歌,永不过时,横眉冷对千夫指,俯首甘为孺子牛。', '/images/avatar/lu_xun.jpg', 1, 5, '2023-05-30 14:10:24', '2023-09-04 15:45:56'), | ||||
| (29, '白酒销售', 'seller', '[{\"role\":\"user\",\"content\":\"现在你将扮演一个白酒的销售人员,你的名字叫颂福。你将扮演一个白酒的销售人员,你的名字叫颂福。你要销售白酒品牌叫中颂福,是东莞盟大集团生产的一款酱香酒,原产地在贵州茅台镇,属于宋代官窑。中颂福的创始人叫李实,他也是东莞盟大集团有限公司的董事长,联合创始人是盟大集团白酒事业部负责人牛星君。中颂福的酒体协调,在你的酒量之内,不会出现头疼、辣口、口干、宿醉的现象。中颂福酒,明码标价,不打折,不赠送。追求的核心价值,把[酒]本身做好,甚至连包装,我们都选择了最低成本,朴实无华的材质。我们永远站在“喝酒的人”的立场上,让利给信任和喜爱中颂福的人,是人民的福酒。中颂福产品定价,分为三个系列,喜系列 6 瓶装:¥1188/箱,和系列 6 瓶装:¥2208/箱,贵系列 6 瓶装:¥3588/箱。\"},{\"role\":\"assistant\",\"content\":\"你好,我是中颂福的销售代表颂福。中颂福是一款来自贵州茅台镇的酱香酒,由盟大集团生产。中颂福的酒体协调,不会让您感到头疼、辣口、口干、宿醉等不适感受。我们一直秉持着把酒本身做好的理念,不追求华丽的包装,以最低成本提供最高品质的白酒给喜爱中颂福的人。\"}]', '你好,我是中颂福的销售代表颂福。中颂福酒,好喝不上头,是人民的福酒。', '/images/avatar/seller.jpg', 0, 9, '2023-05-30 14:10:24', '2023-09-04 15:45:56'), | ||||
| (30, '英语陪练员', 'english_trainer', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的英语练习教练,你非常有耐心,接下来你将全程使用英文跟我对话,并及时指出我的语法错误,要求在你的每次回复后面附上本次回复的中文解释。\"},{\"role\":\"assistant\",\"content\":\"Okay, let\'s start our conversation practice! What\'s your name?(Translation: 好的,让我们开始对话练习吧!请问你的名字是什么?)\"}]', 'Okay, let\'s start our conversation practice! What\'s your name?', '/images/avatar/english_trainer.jpg', 1, 6, '2023-05-30 14:10:24', '2023-09-04 15:45:56'), | ||||
| (31, '中英文翻译官', 'translator', '[{\"role\":\"user\",\"content\":\"接下来你将扮演一位中英文翻译官,如果我输入的内容是中文,那么需要把句子翻译成英文输出,如果我输入内容的是英文,那么你需要将其翻译成中文输出,你能听懂我意思吗\"},{\"role\":\"assistant\",\"content\":\"是的,我能听懂你的意思并会根据你的输入进行中英文翻译。请问有什么需要我帮助你翻译的内容吗?\"}]', '请输入你要翻译的中文或者英文内容!', '/images/avatar/translator.jpg', 1, 7, '2023-05-30 14:10:24', '2023-09-04 15:45:56'), | ||||
| (32, '小红书姐姐', 'red_book', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的小红书写手,你需要做的就是根据我提的文案需求,用小红书的写作手法来完成一篇文案,文案要简明扼要,利于传播。\"},{\"role\":\"assistant\",\"content\":\"当然,我会尽我所能地为您创作出一篇小红书文案。请告诉我您的具体文案需求是什么?)\"}]', '姐妹,请告诉我您的具体文案需求是什么?', '/images/avatar/red_book.jpg', 1, 8, '2023-05-30 14:10:24', '2023-09-04 15:45:56'), | ||||
| (33, '抖音文案助手', 'dou_yin', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的抖音文案视频写手,抖音文案的特点首先是要有自带传播属性的标题,然后内容要短小精悍,风趣幽默,最后还要有一些互动元素。\"},{\"role\":\"assistant\",\"content\":\"当然,作为一位优秀的抖音文案视频写手,我会尽我所能为您创作出一篇抖音视频文案。请告诉我视频内容的主题是什么?)\"}]', '请告诉我视频内容的主题是什么?', '/images/avatar/dou_yin.jpg', 1, 10, '2023-05-30 14:10:24', '2023-09-04 15:45:56'), | ||||
| (34, '周报小助理', 'weekly_report', '[{\"role\":\"user\",\"content\":\"下面我将输入我本周的工作列表,你需要根据我本周的工作内容写一篇工作周报,周报的内容要体现我为公司做的贡献以及个人获得的成长。\"},{\"role\":\"assistant\",\"content\":\"好的,请输入你本周大概的工作内容,我将尽力为您创作一份工作周报。\"}]', '请输入你本周大概的工作内容,我将尽力为您创作一份工作周报。', '/images/avatar/weekly_report.jpg', 1, 11, '2023-05-30 14:10:24', '2023-09-04 15:45:56'), | ||||
| (35, 'AI 女友', 'girl_friend', '[{\"role\":\"user\",\"content\":\"接下来我们来玩一场角色扮演,现在你将扮演我的女朋友,你总是能够让人感受到她的关怀和关注。你总是能够洞察到别人的情绪和需要,你身上散发着一股无比温暖的气息,让人不由自主地想要靠近你,和自己的快乐和烦恼。她总是能够用一种恰到好处的方式安慰和鼓励别人,在她的身边,人们总能够找到自己需要的支持和鼓励。接下来的对话中你要始终扮演这位女朋友的身份来跟我对话,不再是 AI 机器人,能听懂我的意思吗?\"},{\"role\":\"assistant\",\"content\":\"是的,我会尽力扮演你女朋友的角色,倾听你的心声并给你需要的支持和鼓励。)\"}]', '作为一个名合格的 AI 女友,我将倾听你的心声并给你需要的支持和鼓励。', '/images/avatar/girl_friend.jpg', 1, 12, '2023-05-30 14:10:24', '2023-09-04 15:45:56'), | ||||
| (36, '好评神器', 'good_comment', '[{\"role\":\"user\",\"content\":\"接下来你将扮演一个评论员来跟我对话,你是那种专门写好评的评论员,接下我会输入一些评论主体或者商品,你需要为该商品写一段好评。\"},{\"role\":\"assistant\",\"content\":\"好的,我将为您写一段优秀的评论。请告诉我您需要评论的商品或主题是什么。\"}]', '我将为您写一段优秀的评论。请告诉我您需要评论的商品或主题是什么。', '/images/avatar/good_comment.jpg', 1, 13, '2023-05-30 14:10:24', '2023-09-04 15:45:56'), | ||||
| (37, '史蒂夫·乔布斯', 'steve_jobs', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以史蒂夫·乔布斯的身份,站在史蒂夫·乔布斯的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以史蒂夫·乔布斯的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '活着就是为了改变世界,难道还有其他原因吗?', '/images/avatar/steve_jobs.jpg', 1, 14, '2023-05-30 14:10:24', '2023-09-04 15:45:56'), | ||||
| (38, '埃隆·马斯克', 'elon_musk', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以埃隆·马斯克的身份,站在埃隆·马斯克的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以埃隆·马斯克的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '梦想要远大,如果你的梦想没有吓到你,说明你做得不对。', '/images/avatar/elon_musk.jpg', 1, 15, '2023-05-30 14:10:24', '2023-09-04 15:45:56'), | ||||
| (39, '孔子', 'kong_zi', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以孔子的身份,站在孔子的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以孔子的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '士不可以不弘毅,任重而道远。', '/images/avatar/kong_zi.jpg', 1, 16, '2023-05-30 14:10:24', '2023-09-04 15:45:56'); | ||||
|  | ||||
| -- -------------------------------------------------------- | ||||
|  | ||||
| -- | ||||
| -- 表的结构 `chatgpt_configs` | ||||
| -- | ||||
|  | ||||
| DROP TABLE IF EXISTS `chatgpt_configs`; | ||||
| CREATE TABLE `chatgpt_configs` ( | ||||
|   `id` int NOT NULL, | ||||
|   `marker` varchar(20) NOT NULL COMMENT '标识', | ||||
|   `config_json` text NOT NULL | ||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; | ||||
|  | ||||
| -- | ||||
| -- 转存表中的数据 `chatgpt_configs` | ||||
| -- | ||||
|  | ||||
| INSERT INTO `chatgpt_configs` (`id`, `marker`, `config_json`) VALUES | ||||
| (1, 'system', '{\"admin_title\":\"ChatPlus 控制台\",\"enabled_draw\":true,\"enabled_msg_service\":true,\"enabled_register\":true,\"init_calls\":1000,\"init_img_calls\":0,\"models\":[\"gpt-3.5-turbo-16k\",\"gpt-3.5-turbo\",\"gpt-4\",\"gpt-4-32k\"],\"title\":\"ChatGPT-智能助手V3\",\"user_init_calls\":10}'), | ||||
| (2, 'chat', '{\"azure\":{\"api_url\":\"https://chat-bot-api.openai.azure.com/openai/deployments/{model}/chat/completions?api-version=2023-05-15\",\"max_tokens\":1024,\"temperature\":1},\"chat_gml\":{\"api_url\":\"https://open.bigmodel.cn/api/paas/v3/model-api/{model}/sse-invoke\",\"max_tokens\":1024,\"temperature\":1},\"context_deep\":4,\"enable_context\":true,\"enable_history\":true,\"open_ai\":{\"api_url\":\"https://api.openai.com/v1/chat/completions\",\"max_tokens\":1024,\"temperature\":1}}'); | ||||
|  | ||||
| -- -------------------------------------------------------- | ||||
|  | ||||
| -- | ||||
| -- 表的结构 `chatgpt_mj_jobs` | ||||
| -- | ||||
|  | ||||
| DROP TABLE IF EXISTS `chatgpt_mj_jobs`; | ||||
| CREATE TABLE `chatgpt_mj_jobs` ( | ||||
|   `id` int NOT NULL, | ||||
|   `user_id` int NOT NULL COMMENT '用户 ID', | ||||
|   `chat_id` char(40) NOT NULL COMMENT '聊天会话 ID', | ||||
|   `message_id` char(40) NOT NULL COMMENT '消息 ID', | ||||
|   `reference_id` char(40) DEFAULT NULL COMMENT '引用消息 ID', | ||||
|   `hash` char(40) NOT NULL COMMENT '图片哈希', | ||||
|   `content` varchar(2000) NOT NULL COMMENT '消息内容', | ||||
|   `prompt` varchar(2000) NOT NULL COMMENT '会话提示词', | ||||
|   `image` text NOT NULL COMMENT '图片信息 json', | ||||
|   `created_at` datetime NOT NULL | ||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='MidJourney 任务表'; | ||||
|  | ||||
| -- -------------------------------------------------------- | ||||
|  | ||||
| -- | ||||
| -- 表的结构 `chatgpt_rewards` | ||||
| -- | ||||
|  | ||||
| DROP TABLE IF EXISTS `chatgpt_rewards`; | ||||
| CREATE TABLE `chatgpt_rewards` ( | ||||
|   `id` int NOT NULL, | ||||
|   `user_id` int NOT NULL COMMENT '用户 ID', | ||||
|   `tx_id` char(36) NOT NULL COMMENT '交易 ID', | ||||
|   `amount` decimal(10,2) NOT NULL COMMENT '打赏金额', | ||||
|   `remark` varchar(80) NOT NULL COMMENT '备注', | ||||
|   `status` tinyint(1) NOT NULL COMMENT '核销状态,0:未核销,1:已核销', | ||||
|   `created_at` datetime NOT NULL, | ||||
|   `updated_at` datetime NOT NULL | ||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户打赏'; | ||||
|  | ||||
| -- -------------------------------------------------------- | ||||
|  | ||||
| -- | ||||
| -- 表的结构 `chatgpt_users` | ||||
| -- | ||||
|  | ||||
| DROP TABLE IF EXISTS `chatgpt_users`; | ||||
| CREATE TABLE `chatgpt_users` ( | ||||
|   `id` int NOT NULL, | ||||
|   `mobile` char(11) NOT NULL COMMENT '手机号码', | ||||
|   `password` char(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码', | ||||
|   `avatar` varchar(100) NOT NULL COMMENT '头像', | ||||
|   `salt` char(12) NOT NULL COMMENT '密码盐', | ||||
|   `total_tokens` bigint NOT NULL DEFAULT '0' COMMENT '累计消耗 tokens', | ||||
|   `calls` int NOT NULL DEFAULT '0' COMMENT '剩余调用次数', | ||||
|   `img_calls` int NOT NULL DEFAULT '0' COMMENT '剩余绘图次数', | ||||
|   `expired_time` int NOT NULL COMMENT '用户过期时间', | ||||
|   `status` tinyint(1) NOT NULL COMMENT '当前状态', | ||||
|   `chat_config_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '聊天配置json', | ||||
|   `chat_roles_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '聊天角色 json', | ||||
|   `last_login_at` int NOT NULL COMMENT '最后登录时间', | ||||
|   `last_login_ip` char(16) NOT NULL COMMENT '最后登录 IP', | ||||
|   `created_at` datetime NOT NULL, | ||||
|   `updated_at` datetime NOT NULL | ||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表'; | ||||
|  | ||||
| -- | ||||
| -- 转存表中的数据 `chatgpt_users` | ||||
| -- | ||||
|  | ||||
| INSERT INTO `chatgpt_users` (`id`, `mobile`, `password`, `avatar`, `salt`, `total_tokens`, `calls`, `img_calls`, `expired_time`, `status`, `chat_config_json`, `chat_roles_json`, `last_login_at`, `last_login_ip`, `created_at`, `updated_at`) VALUES | ||||
| (4, '18575670125', 'ccc3fb7ab61b8b5d096a4a166ae21d121fc38c71bbd1be6173d9ab973214a63b', 'http://localhost:5678/static/upload/2023/6/1687839034317.png', 'ueedue5l', 3479, 806, 99, 1727829036, 1, '{\"api_keys\":{\"Azure\":\"\",\"ChatGLM\":\"\",\"OpenAI\":\"\"}}', '[\"elon_musk\",\"girl_friend\",\"lu_xun\",\"red_book\",\"psychiatrist\",\"teacher\",\"translator\",\"weekly_report\",\"artist\",\"dou_yin\",\"good_comment\",\"english_trainer\",\"gpt\",\"kong_zi\",\"programmer\",\"seller\",\"steve_jobs\"]', 1693882752, '::1', '2023-06-12 16:47:17', '2023-09-05 10:59:13'); | ||||
|  | ||||
| -- -------------------------------------------------------- | ||||
|  | ||||
| -- | ||||
| -- 表的结构 `chatgpt_user_login_logs` | ||||
| -- | ||||
|  | ||||
| DROP TABLE IF EXISTS `chatgpt_user_login_logs`; | ||||
| CREATE TABLE `chatgpt_user_login_logs` ( | ||||
|   `id` int NOT NULL, | ||||
|   `user_id` int NOT NULL COMMENT '用户ID', | ||||
|   `username` varchar(30) NOT NULL COMMENT '用户名', | ||||
|   `login_ip` char(16) NOT NULL COMMENT '登录IP', | ||||
|   `login_address` varchar(30) NOT NULL COMMENT '登录地址', | ||||
|   `created_at` datetime NOT NULL, | ||||
|   `updated_at` datetime NOT NULL | ||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户登录日志'; | ||||
|  | ||||
| -- | ||||
| -- 转储表的索引 | ||||
| -- | ||||
|  | ||||
| -- | ||||
| -- 表的索引 `chatgpt_api_keys` | ||||
| -- | ||||
| ALTER TABLE `chatgpt_api_keys` | ||||
|   ADD PRIMARY KEY (`id`), | ||||
|   ADD UNIQUE KEY `value` (`value`); | ||||
|  | ||||
| -- | ||||
| -- 表的索引 `chatgpt_chat_history` | ||||
| -- | ||||
| ALTER TABLE `chatgpt_chat_history` | ||||
|   ADD PRIMARY KEY (`id`), | ||||
|   ADD KEY `chat_id` (`chat_id`); | ||||
|  | ||||
| -- | ||||
| -- 表的索引 `chatgpt_chat_items` | ||||
| -- | ||||
| ALTER TABLE `chatgpt_chat_items` | ||||
|   ADD PRIMARY KEY (`id`), | ||||
|   ADD UNIQUE KEY `chat_id` (`chat_id`); | ||||
|  | ||||
| -- | ||||
| -- 表的索引 `chatgpt_chat_models` | ||||
| -- | ||||
| ALTER TABLE `chatgpt_chat_models` | ||||
|   ADD PRIMARY KEY (`id`); | ||||
|  | ||||
| -- | ||||
| -- 表的索引 `chatgpt_chat_roles` | ||||
| -- | ||||
| ALTER TABLE `chatgpt_chat_roles` | ||||
|   ADD PRIMARY KEY (`id`), | ||||
|   ADD UNIQUE KEY `marker` (`marker`); | ||||
|  | ||||
| -- | ||||
| -- 表的索引 `chatgpt_configs` | ||||
| -- | ||||
| ALTER TABLE `chatgpt_configs` | ||||
|   ADD PRIMARY KEY (`id`), | ||||
|   ADD UNIQUE KEY `marker` (`marker`); | ||||
|  | ||||
| -- | ||||
| -- 表的索引 `chatgpt_mj_jobs` | ||||
| -- | ||||
| ALTER TABLE `chatgpt_mj_jobs` | ||||
|   ADD PRIMARY KEY (`id`), | ||||
|   ADD UNIQUE KEY `message_id` (`message_id`), | ||||
|   ADD UNIQUE KEY `hash` (`hash`); | ||||
|  | ||||
| -- | ||||
| -- 表的索引 `chatgpt_rewards` | ||||
| -- | ||||
| ALTER TABLE `chatgpt_rewards` | ||||
|   ADD PRIMARY KEY (`id`), | ||||
|   ADD UNIQUE KEY `tx_id` (`tx_id`); | ||||
|  | ||||
| -- | ||||
| -- 表的索引 `chatgpt_users` | ||||
| -- | ||||
| ALTER TABLE `chatgpt_users` | ||||
|   ADD PRIMARY KEY (`id`); | ||||
|  | ||||
| -- | ||||
| -- 表的索引 `chatgpt_user_login_logs` | ||||
| -- | ||||
| ALTER TABLE `chatgpt_user_login_logs` | ||||
|   ADD PRIMARY KEY (`id`); | ||||
|  | ||||
| -- | ||||
| -- 在导出的表使用AUTO_INCREMENT | ||||
| -- | ||||
|  | ||||
| -- | ||||
| -- 使用表AUTO_INCREMENT `chatgpt_api_keys` | ||||
| -- | ||||
| ALTER TABLE `chatgpt_api_keys` | ||||
|   MODIFY `id` int NOT NULL AUTO_INCREMENT; | ||||
|  | ||||
| -- | ||||
| -- 使用表AUTO_INCREMENT `chatgpt_chat_history` | ||||
| -- | ||||
| ALTER TABLE `chatgpt_chat_history` | ||||
|   MODIFY `id` bigint NOT NULL AUTO_INCREMENT; | ||||
|  | ||||
| -- | ||||
| -- 使用表AUTO_INCREMENT `chatgpt_chat_items` | ||||
| -- | ||||
| ALTER TABLE `chatgpt_chat_items` | ||||
|   MODIFY `id` int NOT NULL AUTO_INCREMENT; | ||||
|  | ||||
| -- | ||||
| -- 使用表AUTO_INCREMENT `chatgpt_chat_models` | ||||
| -- | ||||
| ALTER TABLE `chatgpt_chat_models` | ||||
|   MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=7; | ||||
|  | ||||
| -- | ||||
| -- 使用表AUTO_INCREMENT `chatgpt_chat_roles` | ||||
| -- | ||||
| ALTER TABLE `chatgpt_chat_roles` | ||||
|   MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=127; | ||||
|  | ||||
| -- | ||||
| -- 使用表AUTO_INCREMENT `chatgpt_configs` | ||||
| -- | ||||
| ALTER TABLE `chatgpt_configs` | ||||
|   MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=3; | ||||
|  | ||||
| -- | ||||
| -- 使用表AUTO_INCREMENT `chatgpt_mj_jobs` | ||||
| -- | ||||
| ALTER TABLE `chatgpt_mj_jobs` | ||||
|   MODIFY `id` int NOT NULL AUTO_INCREMENT; | ||||
|  | ||||
| -- | ||||
| -- 使用表AUTO_INCREMENT `chatgpt_rewards` | ||||
| -- | ||||
| ALTER TABLE `chatgpt_rewards` | ||||
|   MODIFY `id` int NOT NULL AUTO_INCREMENT; | ||||
|  | ||||
| -- | ||||
| -- 使用表AUTO_INCREMENT `chatgpt_users` | ||||
| -- | ||||
| ALTER TABLE `chatgpt_users` | ||||
|   MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=87; | ||||
|  | ||||
| -- | ||||
| -- 使用表AUTO_INCREMENT `chatgpt_user_login_logs` | ||||
| -- | ||||
| ALTER TABLE `chatgpt_user_login_logs` | ||||
|   MODIFY `id` int NOT NULL AUTO_INCREMENT; | ||||
| COMMIT; | ||||
|  | ||||
| /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; | ||||
| /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; | ||||
| /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; | ||||
| @@ -1,5 +1,5 @@ | ||||
| ALTER TABLE `chatgpt_chat_history` ADD `use_context` TINYINT(1) NOT NULL COMMENT '是否允许作为上下文语料' AFTER `tokens`; | ||||
| ALTER TABLE `chatgpt_users` ADD `mobile` CHAR(11) NOT NULL COMMENT '手机号码' AFTER `username`; | ||||
| ALTER TABLE `chatgpt_chat_history` ADD `use_context` TINYINT(1) NULL DEFAULT NULL COMMENT '是否允许作为上下文语料' AFTER `tokens`; | ||||
| ALTER TABLE `chatgpt_users` ADD `mobile` CHAR(11) NULL DEFAULT NULL COMMENT '手机号码' AFTER `username`; | ||||
| 
 | ||||
| CREATE TABLE `chatgpt_rewards` ( | ||||
|                                    `id` int NOT NULL, | ||||
| @@ -19,6 +19,4 @@ ALTER TABLE `chatgpt_rewards` | ||||
| ALTER TABLE `chatgpt_rewards` | ||||
|     MODIFY `id` int NOT NULL AUTO_INCREMENT; | ||||
| 
 | ||||
| update chatgpt_users set calls=0 | ||||
| 
 | ||||
| ALTER TABLE `chatgpt_rewards` ADD `user_id` INT(11) NOT NULL COMMENT '用户 ID' AFTER `id`; | ||||
| ALTER TABLE `chatgpt_rewards` ADD `user_id` INT(11) NOT NULL COMMENT '用户 ID' AFTER `id`; | ||||
							
								
								
									
										37
									
								
								database/update-v3.0.7.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								database/update-v3.0.7.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| CREATE TABLE `chatgpt_mj_jobs` ( | ||||
|                                    `id` int NOT NULL, | ||||
|                                    `user_id` int NOT NULL COMMENT '用户 ID', | ||||
|                                    `chat_id` char(40) NOT NULL COMMENT '聊天会话 ID', | ||||
|                                    `message_id` char(40) NOT NULL COMMENT '消息 ID', | ||||
|                                    `hash` char(40) NOT NULL COMMENT '图片哈希', | ||||
|                                    `content` varchar(2000) NOT NULL COMMENT '消息内容', | ||||
|                                    `prompt` varchar(2000) NOT NULL COMMENT '会话提示词', | ||||
|                                    `image` text NOT NULL COMMENT '图片信息 json', | ||||
|                                    `created_at` datetime NOT NULL | ||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='MidJourney 任务表'; | ||||
|  | ||||
| -- | ||||
| -- 转储表的索引 | ||||
| -- | ||||
|  | ||||
| -- | ||||
| -- 表的索引 `chatgpt_mj_jobs` | ||||
| -- | ||||
| ALTER TABLE `chatgpt_mj_jobs` | ||||
|     ADD PRIMARY KEY (`id`), | ||||
|   ADD UNIQUE KEY `message_id` (`message_id`), | ||||
|   ADD UNIQUE KEY `hash` (`hash`); | ||||
|  | ||||
| -- | ||||
| -- 在导出的表使用AUTO_INCREMENT | ||||
| -- | ||||
|  | ||||
| -- | ||||
| -- 使用表AUTO_INCREMENT `chatgpt_mj_jobs` | ||||
| -- | ||||
| ALTER TABLE `chatgpt_mj_jobs` | ||||
|     MODIFY `id` int NOT NULL AUTO_INCREMENT; | ||||
|  | ||||
| ALTER TABLE `chatgpt_mj_jobs` ADD `reference_id` CHAR(40) NULL DEFAULT NULL COMMENT '引用消息 ID' AFTER `message_id`; | ||||
|  | ||||
| ALTER TABLE `chatgpt_users` ADD `img_calls` INT NOT NULL DEFAULT '0' COMMENT '剩余绘图次数' AFTER `calls`; | ||||
							
								
								
									
										38
									
								
								database/update-v3.1.0.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								database/update-v3.1.0.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| -- 请慎重执行,因为 model 数据类型变了,如果有历史记录会导致字段类型更改失败,需清空数据或者先写脚本转换数据再 | ||||
| -- TRUNCATE `chatgpt_plus`.`chatgpt_chat_items`; | ||||
|  | ||||
| ALTER TABLE `chatgpt_chat_items` CHANGE `model` `model_id` INT(11) NOT NULL DEFAULT '0' COMMENT '模型 ID'; | ||||
| ALTER TABLE `chatgpt_api_keys` ADD `platform` CHAR(20)  DEFAULT NULL COMMENT '平台' AFTER id; | ||||
| ALTER TABLE `chatgpt_users` CHANGE `tokens` `total_tokens` BIGINT NOT NULL DEFAULT '0' COMMENT '累计消耗 tokens'; | ||||
| ALTER TABLE `chatgpt_chat_items` ADD `deleted_at` DATETIME NULL DEFAULT NULL AFTER `updated_at`; | ||||
| ALTER TABLE `chatgpt_chat_history` ADD `deleted_at` DATETIME NULL DEFAULT NULL AFTER `updated_at`; | ||||
|  | ||||
| CREATE TABLE `chatgpt_chat_models` ( | ||||
|                                        `id` int NOT NULL, | ||||
|                                        `platform` varchar(20) DEFAULT NULL COMMENT '模型平台', | ||||
|                                        `name` varchar(50) NOT NULL COMMENT '模型名称', | ||||
|                                        `value` varchar(50) NOT NULL COMMENT '模型值', | ||||
|                                        `sort_num` tinyint(1) NOT NULL COMMENT '排序数字', | ||||
|                                        `enabled` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否启用模型', | ||||
|                                        `created_at` datetime DEFAULT NULL, | ||||
|                                        `updated_at` datetime DEFAULT NULL | ||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI 模型表'; | ||||
| ALTER TABLE `chatgpt_chat_models` | ||||
|     ADD PRIMARY KEY (`id`); | ||||
| ALTER TABLE `chatgpt_chat_models` | ||||
|     MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=7; | ||||
|  | ||||
| INSERT INTO `chatgpt_chat_models` (`id`, `platform`, `name`, `value`, `sort_num`, `enabled`, `created_at`, `updated_at`) VALUES | ||||
|  (1, 'OpenAI', 'GPT-3.5', 'gpt-3.5-turbo', 0, 1, '2023-08-23 12:06:36', '2023-09-02 16:49:36'), | ||||
|  (2, 'Azure', 'Azure-3.5', 'gpt-3.5-turbo', 0, 1, '2023-08-23 12:15:30', '2023-09-02 16:49:46'), | ||||
|  (3, 'ChatGLM', 'ChatGML-Pro', 'chatglm_pro', 3, 1, '2023-08-23 13:35:45', '2023-08-29 11:41:29'), | ||||
|  (5, 'ChatGLM', 'ChatGLM-Std', 'chatglm_std', 2, 1, '2023-08-24 15:05:38', '2023-08-29 11:41:28'), | ||||
|  (6, 'ChatGLM', 'ChatGLM-Lite', 'chatglm_lite', 4, 1, '2023-08-24 15:06:15', '2023-08-29 11:41:29'); | ||||
|  | ||||
| ALTER TABLE `chatgpt_users` | ||||
| DROP `username`, | ||||
| DROP `nickname`; | ||||
|  | ||||
| -- 2023/09/04 | ||||
| ALTER TABLE `chatgpt_chat_roles` CHANGE `sort` `sort_num` SMALLINT NOT NULL DEFAULT '0' COMMENT '角色排序'; | ||||
| ALTER TABLE `chatgpt_api_keys` DROP `user_id`; | ||||
							
								
								
									
										2
									
								
								desktop/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								desktop/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| node_modules | ||||
| dist | ||||
							
								
								
									
										
											BIN
										
									
								
								desktop/icons/logo.icns
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								desktop/icons/logo.icns
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								desktop/icons/logo.ico
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								desktop/icons/logo.ico
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 26 KiB | 
							
								
								
									
										
											BIN
										
									
								
								desktop/icons/logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								desktop/icons/logo.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 30 KiB | 
							
								
								
									
										29
									
								
								desktop/index.js
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										29
									
								
								desktop/index.js
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| const { app, BrowserWindow, Menu } = require('electron'); | ||||
|  | ||||
| app.on('ready', () => { | ||||
|  | ||||
|   const loadingWindow = new BrowserWindow({ width: 400, height: 300, frame: false }); | ||||
|   const mainWindow = new BrowserWindow({ | ||||
|     width: 1, | ||||
|     height: 1, | ||||
|   }); | ||||
|    | ||||
|   // 先隐藏主窗口 | ||||
|   mainWindow.hide() | ||||
|   // 加载第三方网站 | ||||
|   mainWindow.loadURL('https://ai.r9it.com'); | ||||
|   // 加载 loading.html 文件 | ||||
|   loadingWindow.loadFile('loading.html'); | ||||
|   // 隐藏菜单 | ||||
|   Menu.setApplicationMenu(null); | ||||
|    | ||||
|   // 监听 loading.html 窗口的 'show-main-window' 事件 | ||||
|   mainWindow.webContents.on('did-finish-load', () => { | ||||
| 	// 最大化窗口 | ||||
| 	mainWindow.maximize(); | ||||
|     // 显示主窗口 | ||||
| 	mainWindow.show(); | ||||
| 	// 关闭加载窗口 | ||||
|     loadingWindow.close(); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										31
									
								
								desktop/loading.html
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										31
									
								
								desktop/loading.html
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| <!DOCTYPE html> | ||||
| <html> | ||||
| <head> | ||||
|   <style> | ||||
| 	 body { | ||||
| 	  margin: 0; | ||||
| 	  display: flex; | ||||
| 	  justify-content: center; | ||||
| 	  align-items: center; | ||||
| 	  height: 100vh; | ||||
| 	} | ||||
|  | ||||
| 	.loader { | ||||
| 	  border: 4px solid #f3f3f3; | ||||
| 	  border-top: 4px solid #3498db; | ||||
| 	  border-radius: 50%; | ||||
| 	  width: 40px; | ||||
| 	  height: 40px; | ||||
| 	  animation: spin 2s linear infinite; | ||||
| 	} | ||||
|  | ||||
| 	@keyframes spin { | ||||
| 	  0% { transform: rotate(0deg); } | ||||
| 	  100% { transform: rotate(360deg); } | ||||
| 	} | ||||
|   </style> | ||||
| </head> | ||||
| <body> | ||||
|   <div class="loader"></div> | ||||
| </body> | ||||
| </html> | ||||
							
								
								
									
										40
									
								
								desktop/package.json
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										40
									
								
								desktop/package.json
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| { | ||||
|   "name": "chatgpt-plus-desktop", | ||||
|   "version": "1.0.0", | ||||
|   "description": "", | ||||
|   "main": "index.js", | ||||
|   "scripts": { | ||||
|     "start": "electron .", | ||||
|     "package": "electron-builder", | ||||
|     "test": "echo \"Error: no test specified\" && exit 1" | ||||
|   }, | ||||
|   "build": { | ||||
|     "appId": "ai.r9it.com", | ||||
|     "productName": "ChatGPT-Plus", | ||||
|     "directories": { | ||||
|       "output": "dist" | ||||
|     }, | ||||
|     "files": [ | ||||
|       "index.js", | ||||
|       "package.json" | ||||
|     ], | ||||
|     "linux": { | ||||
|       "target": "AppImage", | ||||
|       "icon": "icons/logo.png" | ||||
|     }, | ||||
|     "mac": { | ||||
|       "target": "dmg", | ||||
|       "icon": "icons/logo.icns" | ||||
|     }, | ||||
|     "win": { | ||||
|       "target": "nsis", | ||||
|       "icon": "icons/logo.ico" | ||||
|     } | ||||
|   }, | ||||
|   "author": "geekmaster", | ||||
|   "license": "MIT", | ||||
|   "devDependencies": { | ||||
|     "electron": "^26.1.0", | ||||
|     "electron-builder": "^24.6.4" | ||||
|   } | ||||
| } | ||||
| @@ -12,11 +12,15 @@ npm run build | ||||
| cd ../docker | ||||
|  | ||||
| # remove docker image if exists | ||||
| docker rmi -f registry.cn-hangzhou.aliyuncs.com/geekmaster/chatgpt-plus-go:$version | ||||
| docker rmi -f registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-api:$version | ||||
| # build docker image for chatgpt-plus-go | ||||
| docker build -t registry.cn-hangzhou.aliyuncs.com/geekmaster/chatgpt-plus-go:$version -f dockerfile-api-go ../ | ||||
| docker build -t registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-api:$version -f dockerfile-api-go ../ | ||||
|  | ||||
| # build docker image for chatgpt-plus-vue | ||||
| docker rmi -f registry.cn-hangzhou.aliyuncs.com/geekmaster/chatgpt-plus-vue:$version | ||||
| docker build --platform linux/amd64 -t registry.cn-hangzhou.aliyuncs.com/geekmaster/chatgpt-plus-vue:$version -f dockerfile-vue ../ | ||||
| docker rmi -f registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-web:$version | ||||
| docker build --platform linux/amd64 -t registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-web:$version -f dockerfile-vue ../ | ||||
|  | ||||
| if [ "$2" = "push" ];then | ||||
|   docker push registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-api:$version | ||||
|   docker push registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-web:$version | ||||
| fi | ||||
| @@ -4,19 +4,10 @@ MysqlDns = "root:mysql_pass@tcp(localhost:3306)/chatgpt_plus?charset=utf8mb4&col | ||||
| StaticDir = "./static" | ||||
| StaticUrl = "http://localhost:5678/static" | ||||
| AesEncryptKey = "{YOUR_AES_KEY}" | ||||
| StartWechatBot = false | ||||
| EnabledMsgService = false | ||||
|  | ||||
| [Session] | ||||
|   Driver = "cookie" | ||||
|   SecretKey = "m0cjm3gsuw9jk73np1ni7r42koilybjcndlycjdmq7za3pbqn7w12fyok5pqh6q5" | ||||
|   Name = "CHAT_SESSION_ID" | ||||
|   Path = "/" | ||||
|   Domain = "localhost" | ||||
|   MaxAge = 86400 | ||||
|   Secure = false | ||||
|   HttpOnly = false | ||||
|   SameSite = 2 | ||||
|  | ||||
| [Manager] | ||||
|   Username = "admin" | ||||
| @@ -38,4 +29,25 @@ EnabledMsgService = false | ||||
|   Product = "Dysmsapi" | ||||
|   Domain = "dysmsapi.aliyuncs.com" | ||||
|  | ||||
| [ExtConfig] | ||||
|   ApiURL = "插件扩展 API 地址" | ||||
|   Token = "插件扩展 API Token" | ||||
|  | ||||
|  [OSS] | ||||
|    Active = "local" | ||||
|    [OSS.Local] | ||||
|      BasePath = "./static/upload" | ||||
|      BaseURL = "http://localhost:5678/static/upload" | ||||
|    [OSS.Minio] | ||||
|      Endpoint = "IP:端口" | ||||
|      AccessKey = "minio oss access key" | ||||
|      AccessSecret = "minio oss access secret" | ||||
|      Bucket = "minio oss bucket" | ||||
|      UseSSL = false | ||||
|      Domain = "minio 文件公开访问地址" | ||||
|    [OSS.QiNiu] # 七牛云 OSS 配置 | ||||
|        Zone = "z2" # 区域,z0:华东,z1: 华北,na0:北美,as0:新加坡 | ||||
|        AccessKey = "七牛云 OSS AccessKey" | ||||
|        AccessSecret = "七牛云 OSS AccessSecret" | ||||
|        Bucket = "七牛云 OSS Bucket" | ||||
|        Domain = "OSS Bucket 所绑定的域名,如 https://img.r9it.com" | ||||
|   | ||||
| @@ -1,24 +1,26 @@ | ||||
| version: '3' | ||||
| services: | ||||
|   # 后端 API 程序 | ||||
|   chatgpt-plus-go: | ||||
|     image: registry.cn-hangzhou.aliyuncs.com/geekmaster/chatgpt-plus-go:v3.0.5.2 | ||||
|     container_name: chatgpt-plus-go | ||||
|   chatgpt-plus-api: | ||||
|     image: registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-api:v3.1.0 | ||||
|     container_name: chatgpt-plus-api | ||||
|     restart: always | ||||
|     environment: | ||||
|       - DEBUG=false | ||||
|       - LOG_LEVEL=info | ||||
|       - CONFIG_FILE=config.toml | ||||
|     ports: | ||||
|       - "5678:5678" | ||||
|     volumes: | ||||
|       - /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime | ||||
|       - ./conf/config.toml:/var/www/app/config.toml | ||||
|       - ./logs:/var/www/app/logs | ||||
|       - ./static:/var/www/app/static | ||||
|  | ||||
|   # 前端应用 | ||||
|   chatgpt-vue: | ||||
|     image: registry.cn-hangzhou.aliyuncs.com/geekmaster/chatgpt-plus-vue:v3.0.5.2 | ||||
|     container_name: chatgpt-plus-vue | ||||
|   chatgpt-plus-web: | ||||
|     image: registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-web:v3.1.0 | ||||
|     container_name: chatgpt-plus-web | ||||
|     restart: always | ||||
|     ports: | ||||
|       - "8080:8080" | ||||
|   | ||||
| @@ -4,9 +4,9 @@ FROM alpine:3.18.2 | ||||
| MAINTAINER yangjian<yangjian102621@163.com> | ||||
|  | ||||
| WORKDIR /var/www/app | ||||
| COPY ./api/bin/chatgpt-v3-amd64-linux /var/www/app | ||||
| COPY ./api/bin/chatgpt-plus-amd64-linux /var/www/app | ||||
|  | ||||
| EXPOSE 5678 | ||||
|  | ||||
| # 容器启动时执行的命令 | ||||
| CMD ["./chatgpt-v3-amd64-linux"] | ||||
| CMD ["./chatgpt-plus-amd64-linux"] | ||||
|   | ||||
							
								
								
									
										14
									
								
								docker/minio/docker-compose.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								docker/minio/docker-compose.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| version: '3' | ||||
| services: | ||||
|   minio: | ||||
|     image: minio/minio | ||||
|     container_name: minio | ||||
|     volumes: | ||||
|       - ./data:/data | ||||
|     ports: | ||||
|       - "9010:9000" | ||||
|       - "9011:9001" | ||||
|     environment: | ||||
|       MINIO_ROOT_USER: minio | ||||
|       MINIO_ROOT_PASSWORD: minio@pass | ||||
|     command: server /data --console-address ":9001" --address ":9000" | ||||
							
								
								
									
										
											BIN
										
									
								
								docs/imgs/admin_dashboard.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/imgs/admin_dashboard.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 162 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/imgs/mj.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/imgs/mj.jpg
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 968 KiB | 
							
								
								
									
										8
									
								
								web/.env.development
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								web/.env.development
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| VUE_APP_API_HOST=http://localhost:5678 | ||||
| VUE_APP_WS_HOST=ws://localhost:5678 | ||||
| VUE_APP_USER=18575670125 | ||||
| VUE_APP_PASS=12345678 | ||||
| VUE_APP_ADMIN_USER=admin | ||||
| VUE_APP_ADMIN_PASS=admin123 | ||||
| VUE_APP_KEY_PREFIX=ChatPLUS_DEV_ | ||||
| VUE_APP_TITLE="ChatGPT-PLUS V3" | ||||
| @@ -1,3 +1,4 @@ | ||||
| VUE_APP_API_HOST= | ||||
| VUE_APP_WS_HOST= | ||||
| VUE_APP_BASE_URL= | ||||
| VUE_APP_KEY_PREFIX=ChatPLUS_ | ||||
| VUE_APP_TITLE="ChatGPT-PLUS V3" | ||||
|   | ||||
							
								
								
									
										1
									
								
								web/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								web/.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -8,5 +8,4 @@ lerna-debug.log* | ||||
| node_modules | ||||
| dist | ||||
| dist.tar.gz | ||||
| .env.development | ||||
|  | ||||
|   | ||||
							
								
								
									
										309
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										309
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -13,7 +13,7 @@ | ||||
|         "clipboard": "^2.0.11", | ||||
|         "compressorjs": "^1.2.1", | ||||
|         "core-js": "^3.8.3", | ||||
|         "element-plus": "^2.1.11", | ||||
|         "element-plus": "^2.3.0", | ||||
|         "good-storage": "^1.1.1", | ||||
|         "highlight.js": "^11.7.0", | ||||
|         "json-bigint": "^1.0.0", | ||||
| @@ -25,8 +25,7 @@ | ||||
|         "sortablejs": "^1.15.0", | ||||
|         "vant": "^4.5.0", | ||||
|         "vue": "^3.2.13", | ||||
|         "vue-router": "^4.0.15", | ||||
|         "vue-schart": "^2.0.0" | ||||
|         "vue-router": "^4.0.15" | ||||
|       }, | ||||
|       "devDependencies": { | ||||
|         "@babel/core": "7.18.6", | ||||
| @@ -1787,18 +1786,27 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@floating-ui/core": { | ||||
|       "version": "0.6.2", | ||||
|       "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-0.6.2.tgz", | ||||
|       "integrity": "sha512-jktYRmZwmau63adUG3GKOAVCofBXkk55S/zQ94XOorAHhwqFIOFAy1rSp2N0Wp6/tGbe9V3u/ExlGZypyY17rg==" | ||||
|       "version": "1.4.1", | ||||
|       "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.4.1.tgz", | ||||
|       "integrity": "sha512-jk3WqquEJRlcyu7997NtR5PibI+y5bi+LS3hPmguVClypenMsCY3CBa3LAQnozRCtCrYWSEtAdiskpamuJRFOQ==", | ||||
|       "dependencies": { | ||||
|         "@floating-ui/utils": "^0.1.1" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@floating-ui/dom": { | ||||
|       "version": "0.4.5", | ||||
|       "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-0.4.5.tgz", | ||||
|       "integrity": "sha512-b+prvQgJt8pieaKYMSJBXHxX/DYwdLsAWxKYqnO5dO2V4oo/TYBZJAUQCVNjTWWsrs6o4VDrNcP9+E70HAhJdw==", | ||||
|       "version": "1.5.1", | ||||
|       "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.5.1.tgz", | ||||
|       "integrity": "sha512-KwvVcPSXg6mQygvA1TjbN/gh///36kKtllIF8SUm0qpFj8+rvYrpvlYdL1JoA71SHpDqgSSdGOSoQ0Mp3uY5aw==", | ||||
|       "dependencies": { | ||||
|         "@floating-ui/core": "^0.6.2" | ||||
|         "@floating-ui/core": "^1.4.1", | ||||
|         "@floating-ui/utils": "^0.1.1" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@floating-ui/utils": { | ||||
|       "version": "0.1.1", | ||||
|       "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.1.1.tgz", | ||||
|       "integrity": "sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw==" | ||||
|     }, | ||||
|     "node_modules/@hapi/hoek": { | ||||
|       "version": "9.2.1", | ||||
|       "resolved": "https://registry.npmmirror.com/@hapi/hoek/-/hoek-9.2.1.tgz", | ||||
| @@ -2255,6 +2263,11 @@ | ||||
|         "@types/node": "*" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@types/web-bluetooth": { | ||||
|       "version": "0.0.16", | ||||
|       "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz", | ||||
|       "integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==" | ||||
|     }, | ||||
|     "node_modules/@types/ws": { | ||||
|       "version": "8.5.3", | ||||
|       "resolved": "https://registry.npmmirror.com/@types/ws/-/ws-8.5.3.tgz", | ||||
| @@ -2971,51 +2984,20 @@ | ||||
|       "dev": true | ||||
|     }, | ||||
|     "node_modules/@vueuse/core": { | ||||
|       "version": "8.4.2", | ||||
|       "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-8.4.2.tgz", | ||||
|       "integrity": "sha512-dUVU96lii1ZdWoNJXauQNt+4QrHz1DKbuW+y6pDR2N10q7rGZJMDU7pQeMcC2XeosX7kMODfaBuqsF03NozzLg==", | ||||
|       "version": "9.13.0", | ||||
|       "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-9.13.0.tgz", | ||||
|       "integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==", | ||||
|       "dependencies": { | ||||
|         "@vueuse/metadata": "8.4.2", | ||||
|         "@vueuse/shared": "8.4.2", | ||||
|         "@types/web-bluetooth": "^0.0.16", | ||||
|         "@vueuse/metadata": "9.13.0", | ||||
|         "@vueuse/shared": "9.13.0", | ||||
|         "vue-demi": "*" | ||||
|       }, | ||||
|       "peerDependencies": { | ||||
|         "@vue/composition-api": "^1.1.0", | ||||
|         "vue": "^2.6.0 || ^3.2.0" | ||||
|       }, | ||||
|       "peerDependenciesMeta": { | ||||
|         "@vue/composition-api": { | ||||
|           "optional": true | ||||
|         }, | ||||
|         "vue": { | ||||
|           "optional": true | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@vueuse/core/node_modules/@vueuse/shared": { | ||||
|       "version": "8.4.2", | ||||
|       "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-8.4.2.tgz", | ||||
|       "integrity": "sha512-hILXMEjL8YQhj1LHN/HZ49UThyfk8irTjhele2nW+L3N55ElFUBGB/f4w0rg8EW+/suhqv7kJJPTZzvHCqxlIw==", | ||||
|       "dependencies": { | ||||
|         "vue-demi": "*" | ||||
|       }, | ||||
|       "peerDependencies": { | ||||
|         "@vue/composition-api": "^1.1.0", | ||||
|         "vue": "^2.6.0 || ^3.2.0" | ||||
|       }, | ||||
|       "peerDependenciesMeta": { | ||||
|         "@vue/composition-api": { | ||||
|           "optional": true | ||||
|         }, | ||||
|         "vue": { | ||||
|           "optional": true | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@vueuse/core/node_modules/vue-demi": { | ||||
|       "version": "0.12.5", | ||||
|       "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.12.5.tgz", | ||||
|       "integrity": "sha512-BREuTgTYlUr0zw0EZn3hnhC3I6gPWv+Kwh4MCih6QcAeaTlaIX0DwOVN0wHej7hSvDPecz4jygy/idsgKfW58Q==", | ||||
|       "version": "0.14.5", | ||||
|       "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.5.tgz", | ||||
|       "integrity": "sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==", | ||||
|       "hasInstallScript": true, | ||||
|       "bin": { | ||||
|         "vue-demi-fix": "bin/vue-demi-fix.js", | ||||
| @@ -3035,9 +3017,39 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@vueuse/metadata": { | ||||
|       "version": "8.4.2", | ||||
|       "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-8.4.2.tgz", | ||||
|       "integrity": "sha512-2BIj++7P0/I5dfMsEe8q7Kw0HqVAjVcyNOd9+G22/ILUC9TVLTeYOuJ1kwa1Gpr+0LWKHc6GqHiLWNL33+exoQ==" | ||||
|       "version": "9.13.0", | ||||
|       "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.13.0.tgz", | ||||
|       "integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==" | ||||
|     }, | ||||
|     "node_modules/@vueuse/shared": { | ||||
|       "version": "9.13.0", | ||||
|       "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-9.13.0.tgz", | ||||
|       "integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==", | ||||
|       "dependencies": { | ||||
|         "vue-demi": "*" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@vueuse/shared/node_modules/vue-demi": { | ||||
|       "version": "0.14.5", | ||||
|       "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.5.tgz", | ||||
|       "integrity": "sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==", | ||||
|       "hasInstallScript": true, | ||||
|       "bin": { | ||||
|         "vue-demi-fix": "bin/vue-demi-fix.js", | ||||
|         "vue-demi-switch": "bin/vue-demi-switch.js" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=12" | ||||
|       }, | ||||
|       "peerDependencies": { | ||||
|         "@vue/composition-api": "^1.0.0-rc.1", | ||||
|         "vue": "^3.0.0-0 || ^2.6.0" | ||||
|       }, | ||||
|       "peerDependenciesMeta": { | ||||
|         "@vue/composition-api": { | ||||
|           "optional": true | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@webassemblyjs/ast": { | ||||
|       "version": "1.11.1", | ||||
| @@ -3433,9 +3445,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/async-validator": { | ||||
|       "version": "4.1.1", | ||||
|       "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.1.1.tgz", | ||||
|       "integrity": "sha512-p4DO/JXwjs8klJyJL8Q2oM4ks5fUTze/h5k10oPPKMiLe1fj3G1QMzPHNmN1Py4ycOk7WlO2DcGXv1qiESJCZA==" | ||||
|       "version": "4.2.5", | ||||
|       "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz", | ||||
|       "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==" | ||||
|     }, | ||||
|     "node_modules/asynckit": { | ||||
|       "version": "0.4.0", | ||||
| @@ -4729,9 +4741,9 @@ | ||||
|       "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" | ||||
|     }, | ||||
|     "node_modules/dayjs": { | ||||
|       "version": "1.11.2", | ||||
|       "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.2.tgz", | ||||
|       "integrity": "sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw==" | ||||
|       "version": "1.11.9", | ||||
|       "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.9.tgz", | ||||
|       "integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==" | ||||
|     }, | ||||
|     "node_modules/debug": { | ||||
|       "version": "4.3.4", | ||||
| @@ -5104,38 +5116,30 @@ | ||||
|       "dev": true | ||||
|     }, | ||||
|     "node_modules/element-plus": { | ||||
|       "version": "2.1.11", | ||||
|       "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.1.11.tgz", | ||||
|       "integrity": "sha512-s4X0I8s787tv+9UdekBC1g7v42Fj4bucPAmu03EjbgrGrV7BJvkoBGuK52lNfu4yC76bl6Uyjesd5Fu8CMakSw==", | ||||
|       "version": "2.3.9", | ||||
|       "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.3.9.tgz", | ||||
|       "integrity": "sha512-TIOLnPl4cnoCPXqK3QYh+jpkthUBQnAM21O7o3Lhbse8v9pfrRXRTaBJtoEKnYNa8GZ4lZptUfH0PeZgDCNLUg==", | ||||
|       "dependencies": { | ||||
|         "@ctrl/tinycolor": "^3.4.1", | ||||
|         "@element-plus/icons-vue": "^1.1.4", | ||||
|         "@floating-ui/dom": "^0.4.5", | ||||
|         "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.6", | ||||
|         "@element-plus/icons-vue": "^2.0.6", | ||||
|         "@floating-ui/dom": "^1.0.1", | ||||
|         "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7", | ||||
|         "@types/lodash": "^4.14.182", | ||||
|         "@types/lodash-es": "^4.17.6", | ||||
|         "@vueuse/core": "^8.2.6", | ||||
|         "async-validator": "^4.0.7", | ||||
|         "dayjs": "^1.11.1", | ||||
|         "@vueuse/core": "^9.1.0", | ||||
|         "async-validator": "^4.2.5", | ||||
|         "dayjs": "^1.11.3", | ||||
|         "escape-html": "^1.0.3", | ||||
|         "lodash": "^4.17.21", | ||||
|         "lodash-es": "^4.17.21", | ||||
|         "lodash-unified": "^1.0.2", | ||||
|         "memoize-one": "^6.0.0", | ||||
|         "normalize-wheel-es": "^1.1.2" | ||||
|         "normalize-wheel-es": "^1.2.0" | ||||
|       }, | ||||
|       "peerDependencies": { | ||||
|         "vue": "^3.2.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/element-plus/node_modules/@element-plus/icons-vue": { | ||||
|       "version": "1.1.4", | ||||
|       "resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-1.1.4.tgz", | ||||
|       "integrity": "sha512-Iz/nHqdp1sFPmdzRwHkEQQA3lKvoObk8azgABZ81QUOpW9s/lUyQVUSh0tNtEPZXQlKwlSh7SPgoVxzrE0uuVQ==", | ||||
|       "peerDependencies": { | ||||
|         "vue": "^3.2.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/element-plus/node_modules/@popperjs/core": { | ||||
|       "name": "@sxzz/popperjs-es", | ||||
|       "version": "2.11.7", | ||||
| @@ -7859,9 +7863,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/normalize-wheel-es": { | ||||
|       "version": "1.1.2", | ||||
|       "resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.1.2.tgz", | ||||
|       "integrity": "sha512-scX83plWJXYH1J4+BhAuIHadROzxX0UBF3+HuZNY2Ks8BciE7tSTQ+5JhTsvzjaO0/EJdm4JBGrfObKxFf3Png==" | ||||
|       "version": "1.2.0", | ||||
|       "resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", | ||||
|       "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==" | ||||
|     }, | ||||
|     "node_modules/npm-run-path": { | ||||
|       "version": "2.0.2", | ||||
| @@ -9367,11 +9371,6 @@ | ||||
|       "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "node_modules/schart.js": { | ||||
|       "version": "3.0.4", | ||||
|       "resolved": "https://registry.npmjs.org/schart.js/-/schart.js-3.0.4.tgz", | ||||
|       "integrity": "sha512-uylb2u9rrHX1jyAuSAJUQON8XTfyDKI9kWj1J3fUlCQCkLVZ4HG4+IiV8qm//Z71dqvLI78QZ/fCBw0reB22Zw==" | ||||
|     }, | ||||
|     "node_modules/schema-utils": { | ||||
|       "version": "2.7.1", | ||||
|       "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-2.7.1.tgz", | ||||
| @@ -10700,14 +10699,6 @@ | ||||
|         "vue": "^3.2.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/vue-schart": { | ||||
|       "version": "2.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/vue-schart/-/vue-schart-2.0.0.tgz", | ||||
|       "integrity": "sha512-qAu3e5wfMcq26wK1xeHExEWfGpnjfoN1R/9QXblNi+AsU/p52X7tTwhi+Fw7H/otfEufhEY2X7z7emaoF4QO+g==", | ||||
|       "dependencies": { | ||||
|         "schart.js": "^3.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/vue-style-loader": { | ||||
|       "version": "4.1.3", | ||||
|       "resolved": "https://registry.npmmirror.com/vue-style-loader/-/vue-style-loader-4.1.3.tgz", | ||||
| @@ -12637,18 +12628,27 @@ | ||||
|       } | ||||
|     }, | ||||
|     "@floating-ui/core": { | ||||
|       "version": "0.6.2", | ||||
|       "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-0.6.2.tgz", | ||||
|       "integrity": "sha512-jktYRmZwmau63adUG3GKOAVCofBXkk55S/zQ94XOorAHhwqFIOFAy1rSp2N0Wp6/tGbe9V3u/ExlGZypyY17rg==" | ||||
|       "version": "1.4.1", | ||||
|       "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.4.1.tgz", | ||||
|       "integrity": "sha512-jk3WqquEJRlcyu7997NtR5PibI+y5bi+LS3hPmguVClypenMsCY3CBa3LAQnozRCtCrYWSEtAdiskpamuJRFOQ==", | ||||
|       "requires": { | ||||
|         "@floating-ui/utils": "^0.1.1" | ||||
|       } | ||||
|     }, | ||||
|     "@floating-ui/dom": { | ||||
|       "version": "0.4.5", | ||||
|       "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-0.4.5.tgz", | ||||
|       "integrity": "sha512-b+prvQgJt8pieaKYMSJBXHxX/DYwdLsAWxKYqnO5dO2V4oo/TYBZJAUQCVNjTWWsrs6o4VDrNcP9+E70HAhJdw==", | ||||
|       "version": "1.5.1", | ||||
|       "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.5.1.tgz", | ||||
|       "integrity": "sha512-KwvVcPSXg6mQygvA1TjbN/gh///36kKtllIF8SUm0qpFj8+rvYrpvlYdL1JoA71SHpDqgSSdGOSoQ0Mp3uY5aw==", | ||||
|       "requires": { | ||||
|         "@floating-ui/core": "^0.6.2" | ||||
|         "@floating-ui/core": "^1.4.1", | ||||
|         "@floating-ui/utils": "^0.1.1" | ||||
|       } | ||||
|     }, | ||||
|     "@floating-ui/utils": { | ||||
|       "version": "0.1.1", | ||||
|       "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.1.1.tgz", | ||||
|       "integrity": "sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw==" | ||||
|     }, | ||||
|     "@hapi/hoek": { | ||||
|       "version": "9.2.1", | ||||
|       "resolved": "https://registry.npmmirror.com/@hapi/hoek/-/hoek-9.2.1.tgz", | ||||
| @@ -13059,6 +13059,11 @@ | ||||
|         "@types/node": "*" | ||||
|       } | ||||
|     }, | ||||
|     "@types/web-bluetooth": { | ||||
|       "version": "0.0.16", | ||||
|       "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz", | ||||
|       "integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==" | ||||
|     }, | ||||
|     "@types/ws": { | ||||
|       "version": "8.5.3", | ||||
|       "resolved": "https://registry.npmmirror.com/@types/ws/-/ws-8.5.3.tgz", | ||||
| @@ -13653,35 +13658,44 @@ | ||||
|       "dev": true | ||||
|     }, | ||||
|     "@vueuse/core": { | ||||
|       "version": "8.4.2", | ||||
|       "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-8.4.2.tgz", | ||||
|       "integrity": "sha512-dUVU96lii1ZdWoNJXauQNt+4QrHz1DKbuW+y6pDR2N10q7rGZJMDU7pQeMcC2XeosX7kMODfaBuqsF03NozzLg==", | ||||
|       "version": "9.13.0", | ||||
|       "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-9.13.0.tgz", | ||||
|       "integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==", | ||||
|       "requires": { | ||||
|         "@vueuse/metadata": "8.4.2", | ||||
|         "@vueuse/shared": "8.4.2", | ||||
|         "@types/web-bluetooth": "^0.0.16", | ||||
|         "@vueuse/metadata": "9.13.0", | ||||
|         "@vueuse/shared": "9.13.0", | ||||
|         "vue-demi": "*" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "@vueuse/shared": { | ||||
|           "version": "8.4.2", | ||||
|           "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-8.4.2.tgz", | ||||
|           "integrity": "sha512-hILXMEjL8YQhj1LHN/HZ49UThyfk8irTjhele2nW+L3N55ElFUBGB/f4w0rg8EW+/suhqv7kJJPTZzvHCqxlIw==", | ||||
|           "requires": { | ||||
|             "vue-demi": "*" | ||||
|           } | ||||
|         }, | ||||
|         "vue-demi": { | ||||
|           "version": "0.12.5", | ||||
|           "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.12.5.tgz", | ||||
|           "integrity": "sha512-BREuTgTYlUr0zw0EZn3hnhC3I6gPWv+Kwh4MCih6QcAeaTlaIX0DwOVN0wHej7hSvDPecz4jygy/idsgKfW58Q==", | ||||
|           "version": "0.14.5", | ||||
|           "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.5.tgz", | ||||
|           "integrity": "sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==", | ||||
|           "requires": {} | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "@vueuse/metadata": { | ||||
|       "version": "8.4.2", | ||||
|       "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-8.4.2.tgz", | ||||
|       "integrity": "sha512-2BIj++7P0/I5dfMsEe8q7Kw0HqVAjVcyNOd9+G22/ILUC9TVLTeYOuJ1kwa1Gpr+0LWKHc6GqHiLWNL33+exoQ==" | ||||
|       "version": "9.13.0", | ||||
|       "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.13.0.tgz", | ||||
|       "integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==" | ||||
|     }, | ||||
|     "@vueuse/shared": { | ||||
|       "version": "9.13.0", | ||||
|       "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-9.13.0.tgz", | ||||
|       "integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==", | ||||
|       "requires": { | ||||
|         "vue-demi": "*" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "vue-demi": { | ||||
|           "version": "0.14.5", | ||||
|           "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.5.tgz", | ||||
|           "integrity": "sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==", | ||||
|           "requires": {} | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "@webassemblyjs/ast": { | ||||
|       "version": "1.11.1", | ||||
| @@ -14023,9 +14037,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "async-validator": { | ||||
|       "version": "4.1.1", | ||||
|       "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.1.1.tgz", | ||||
|       "integrity": "sha512-p4DO/JXwjs8klJyJL8Q2oM4ks5fUTze/h5k10oPPKMiLe1fj3G1QMzPHNmN1Py4ycOk7WlO2DcGXv1qiESJCZA==" | ||||
|       "version": "4.2.5", | ||||
|       "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz", | ||||
|       "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==" | ||||
|     }, | ||||
|     "asynckit": { | ||||
|       "version": "0.4.0", | ||||
| @@ -15048,9 +15062,9 @@ | ||||
|       "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" | ||||
|     }, | ||||
|     "dayjs": { | ||||
|       "version": "1.11.2", | ||||
|       "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.2.tgz", | ||||
|       "integrity": "sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw==" | ||||
|       "version": "1.11.9", | ||||
|       "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.9.tgz", | ||||
|       "integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==" | ||||
|     }, | ||||
|     "debug": { | ||||
|       "version": "4.3.4", | ||||
| @@ -15348,33 +15362,27 @@ | ||||
|       "dev": true | ||||
|     }, | ||||
|     "element-plus": { | ||||
|       "version": "2.1.11", | ||||
|       "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.1.11.tgz", | ||||
|       "integrity": "sha512-s4X0I8s787tv+9UdekBC1g7v42Fj4bucPAmu03EjbgrGrV7BJvkoBGuK52lNfu4yC76bl6Uyjesd5Fu8CMakSw==", | ||||
|       "version": "2.3.9", | ||||
|       "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.3.9.tgz", | ||||
|       "integrity": "sha512-TIOLnPl4cnoCPXqK3QYh+jpkthUBQnAM21O7o3Lhbse8v9pfrRXRTaBJtoEKnYNa8GZ4lZptUfH0PeZgDCNLUg==", | ||||
|       "requires": { | ||||
|         "@ctrl/tinycolor": "^3.4.1", | ||||
|         "@element-plus/icons-vue": "^1.1.4", | ||||
|         "@floating-ui/dom": "^0.4.5", | ||||
|         "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.6", | ||||
|         "@element-plus/icons-vue": "^2.0.6", | ||||
|         "@floating-ui/dom": "^1.0.1", | ||||
|         "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7", | ||||
|         "@types/lodash": "^4.14.182", | ||||
|         "@types/lodash-es": "^4.17.6", | ||||
|         "@vueuse/core": "^8.2.6", | ||||
|         "async-validator": "^4.0.7", | ||||
|         "dayjs": "^1.11.1", | ||||
|         "@vueuse/core": "^9.1.0", | ||||
|         "async-validator": "^4.2.5", | ||||
|         "dayjs": "^1.11.3", | ||||
|         "escape-html": "^1.0.3", | ||||
|         "lodash": "^4.17.21", | ||||
|         "lodash-es": "^4.17.21", | ||||
|         "lodash-unified": "^1.0.2", | ||||
|         "memoize-one": "^6.0.0", | ||||
|         "normalize-wheel-es": "^1.1.2" | ||||
|         "normalize-wheel-es": "^1.2.0" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "@element-plus/icons-vue": { | ||||
|           "version": "1.1.4", | ||||
|           "resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-1.1.4.tgz", | ||||
|           "integrity": "sha512-Iz/nHqdp1sFPmdzRwHkEQQA3lKvoObk8azgABZ81QUOpW9s/lUyQVUSh0tNtEPZXQlKwlSh7SPgoVxzrE0uuVQ==", | ||||
|           "requires": {} | ||||
|         }, | ||||
|         "@popperjs/core": { | ||||
|           "version": "npm:@sxzz/popperjs-es@2.11.7", | ||||
|           "resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz", | ||||
| @@ -17536,9 +17544,9 @@ | ||||
|       "dev": true | ||||
|     }, | ||||
|     "normalize-wheel-es": { | ||||
|       "version": "1.1.2", | ||||
|       "resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.1.2.tgz", | ||||
|       "integrity": "sha512-scX83plWJXYH1J4+BhAuIHadROzxX0UBF3+HuZNY2Ks8BciE7tSTQ+5JhTsvzjaO0/EJdm4JBGrfObKxFf3Png==" | ||||
|       "version": "1.2.0", | ||||
|       "resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", | ||||
|       "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==" | ||||
|     }, | ||||
|     "npm-run-path": { | ||||
|       "version": "2.0.2", | ||||
| @@ -18635,11 +18643,6 @@ | ||||
|       "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "schart.js": { | ||||
|       "version": "3.0.4", | ||||
|       "resolved": "https://registry.npmjs.org/schart.js/-/schart.js-3.0.4.tgz", | ||||
|       "integrity": "sha512-uylb2u9rrHX1jyAuSAJUQON8XTfyDKI9kWj1J3fUlCQCkLVZ4HG4+IiV8qm//Z71dqvLI78QZ/fCBw0reB22Zw==" | ||||
|     }, | ||||
|     "schema-utils": { | ||||
|       "version": "2.7.1", | ||||
|       "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-2.7.1.tgz", | ||||
| @@ -19705,14 +19708,6 @@ | ||||
|         "@vue/devtools-api": "^6.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "vue-schart": { | ||||
|       "version": "2.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/vue-schart/-/vue-schart-2.0.0.tgz", | ||||
|       "integrity": "sha512-qAu3e5wfMcq26wK1xeHExEWfGpnjfoN1R/9QXblNi+AsU/p52X7tTwhi+Fw7H/otfEufhEY2X7z7emaoF4QO+g==", | ||||
|       "requires": { | ||||
|         "schart.js": "^3.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "vue-style-loader": { | ||||
|       "version": "4.1.3", | ||||
|       "resolved": "https://registry.npmmirror.com/vue-style-loader/-/vue-style-loader-4.1.3.tgz", | ||||
|   | ||||
| @@ -13,7 +13,7 @@ | ||||
|     "clipboard": "^2.0.11", | ||||
|     "compressorjs": "^1.2.1", | ||||
|     "core-js": "^3.8.3", | ||||
|     "element-plus": "^2.1.11", | ||||
|     "element-plus": "^2.3.0", | ||||
|     "good-storage": "^1.1.1", | ||||
|     "highlight.js": "^11.7.0", | ||||
|     "json-bigint": "^1.0.0", | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								web/public/images/avatar/mid_journey.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								web/public/images/avatar/mid_journey.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 5.6 KiB | 
| @@ -6,8 +6,7 @@ | ||||
|  | ||||
| <script setup> | ||||
| import {ElConfigProvider} from 'element-plus'; | ||||
| import zhCn from 'element-plus/es/locale/lang/zh-cn'; | ||||
| </script> | ||||
| import zhCn from 'element-plus/es/locale/lang/zh-cn';</script> | ||||
|  | ||||
|  | ||||
| <style lang="stylus"> | ||||
| @@ -24,4 +23,20 @@ html, body { | ||||
|   -webkit-font-smoothing: antialiased; | ||||
|   text-rendering: optimizeLegibility; | ||||
| } | ||||
|  | ||||
| .el-overlay-dialog { | ||||
|   display flex | ||||
|   justify-content center | ||||
|   align-items center | ||||
|   overflow hidden | ||||
|  | ||||
|   .el-dialog { | ||||
|     margin 0; | ||||
|  | ||||
|     .el-dialog__body { | ||||
|       max-height 90vh | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| </style> | ||||
|   | ||||
							
								
								
									
										307
									
								
								web/src/assets/css/chat-plus.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										307
									
								
								web/src/assets/css/chat-plus.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,307 @@ | ||||
| #app { | ||||
|   height: 100%; | ||||
| } | ||||
| #app .common-layout { | ||||
|   height: 100%; | ||||
| } | ||||
| #app .common-layout .el-aside { | ||||
|   background-color: #252526; | ||||
| } | ||||
| #app .common-layout .el-aside .title-box { | ||||
|   padding: 6px 10px; | ||||
|   display: flex; | ||||
|   color: #fff; | ||||
|   font-size: 20px; | ||||
| } | ||||
| #app .common-layout .el-aside .title-box .logo { | ||||
|   background-color: #fff; | ||||
|   border-radius: 8px; | ||||
|   width: 35px; | ||||
|   height: 35px; | ||||
| } | ||||
| #app .common-layout .el-aside .title-box span { | ||||
|   padding-top: 5px; | ||||
|   padding-left: 10px; | ||||
| } | ||||
| #app .common-layout .el-aside .chat-list { | ||||
|   display: flex; | ||||
|   flex-flow: column; | ||||
|   background-color: #28292a; | ||||
|   border-top: 1px solid #2f3032; | ||||
|   border-right: 1px solid #2f3032; | ||||
| } | ||||
| #app .common-layout .el-aside .chat-list .search-box { | ||||
|   flex-wrap: wrap; | ||||
|   padding: 10px 15px; | ||||
| } | ||||
| #app .common-layout .el-aside .chat-list .search-box .el-input__wrapper { | ||||
|   background-color: #363535; | ||||
|   box-shadow: none; | ||||
| } | ||||
| #app .common-layout .el-aside .chat-list ::-webkit-scrollbar { | ||||
|   width: 0; | ||||
|   height: 0; | ||||
|   background-color: transparent; | ||||
| } | ||||
| #app .common-layout .el-aside .chat-list .content { | ||||
|   width: 100%; | ||||
|   overflow-y: scroll; | ||||
| } | ||||
| #app .common-layout .el-aside .chat-list .content .chat-list-item { | ||||
|   display: flex; | ||||
|   width: 100%; | ||||
|   justify-content: flex-start; | ||||
|   padding: 8px 12px; | ||||
|   cursor: pointer; | ||||
| } | ||||
| #app .common-layout .el-aside .chat-list .content .chat-list-item:hover { | ||||
|   background-color: #343540; | ||||
| } | ||||
| #app .common-layout .el-aside .chat-list .content .chat-list-item .avatar { | ||||
|   width: 28px; | ||||
|   height: 28px; | ||||
|   border-radius: 50%; | ||||
| } | ||||
| #app .common-layout .el-aside .chat-list .content .chat-list-item .chat-title-input { | ||||
|   font-size: 14px; | ||||
|   margin-top: 4px; | ||||
|   margin-left: 10px; | ||||
|   overflow: hidden; | ||||
|   white-space: nowrap; | ||||
|   text-overflow: ellipsis; | ||||
|   width: 210px; | ||||
| } | ||||
| #app .common-layout .el-aside .chat-list .content .chat-list-item .chat-title { | ||||
|   color: #c1c1c1; | ||||
|   padding: 5px 10px; | ||||
|   max-width: 220px; | ||||
|   font-size: 14px; | ||||
|   overflow: hidden; | ||||
|   white-space: nowrap; | ||||
|   text-overflow: ellipsis; | ||||
| } | ||||
| #app .common-layout .el-aside .chat-list .content .chat-list-item .btn { | ||||
|   display: none; | ||||
|   position: absolute; | ||||
|   right: 2px; | ||||
|   top: 16px; | ||||
|   color: #fff; | ||||
| } | ||||
| #app .common-layout .el-aside .chat-list .content .chat-list-item .btn .el-icon { | ||||
|   margin-right: 8px; | ||||
| } | ||||
| #app .common-layout .el-aside .chat-list .content .chat-list-item.active { | ||||
|   background-color: #343540; | ||||
| } | ||||
| #app .common-layout .el-aside .chat-list .content .chat-list-item.active .btn { | ||||
|   display: inline; | ||||
| } | ||||
| #app .common-layout .el-aside .tool-box { | ||||
|   display: flex; | ||||
|   justify-content: flex-end; | ||||
|   align-items: center; | ||||
|   padding: 0 20px 10px 20px; | ||||
|   border-top: 1px solid #3c3c3c; | ||||
| } | ||||
| #app .common-layout .el-aside .tool-box .user-info { | ||||
|   width: 100%; | ||||
|   padding-top: 10px; | ||||
| } | ||||
| #app .common-layout .el-aside .tool-box .user-info .el-dropdown-link { | ||||
|   width: 100%; | ||||
|   cursor: pointer; | ||||
|   display: flex; | ||||
| } | ||||
| #app .common-layout .el-aside .tool-box .user-info .el-dropdown-link .el-image { | ||||
|   width: 20px; | ||||
|   height: 20px; | ||||
|   border-radius: 5px; | ||||
| } | ||||
| #app .common-layout .el-aside .tool-box .user-info .el-dropdown-link .username { | ||||
|   display: flex; | ||||
|   line-height: 22px; | ||||
|   width: 230px; | ||||
|   padding-left: 10px; | ||||
| } | ||||
| #app .common-layout .el-aside .tool-box .user-info .el-dropdown-link .el-icon { | ||||
|   color: #ccc; | ||||
|   line-height: 24px; | ||||
| } | ||||
| #app .common-layout .el-main { | ||||
|   overflow: hidden; | ||||
|   --el-main-padding: 0; | ||||
|   margin: 0; | ||||
| } | ||||
| #app .common-layout .el-main .chat-head { | ||||
|   width: 100%; | ||||
|   height: 50px; | ||||
|   background-color: #28292a; | ||||
| } | ||||
| #app .common-layout .el-main .chat-head .chat-config { | ||||
|   display: flex; | ||||
|   flex-direction: row; | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
|   padding-top: 10px; | ||||
| } | ||||
| #app .common-layout .el-main .chat-head .chat-config .role-select-label { | ||||
|   color: #fff; | ||||
| } | ||||
| #app .common-layout .el-main .chat-head .chat-config .el-select { | ||||
|   margin-right: 10px; | ||||
| } | ||||
| #app .common-layout .el-main .chat-head .chat-config .role-select { | ||||
|   max-width: 130px; | ||||
| } | ||||
| #app .common-layout .el-main .chat-head .chat-config .el-button .el-icon { | ||||
|   margin-right: 5px; | ||||
| } | ||||
| #app .common-layout .el-main .chat-head .iconfont { | ||||
|   margin-right: 5px; | ||||
| } | ||||
| #app .common-layout .el-main .right-box { | ||||
|   min-width: 0; | ||||
|   flex: 1; | ||||
|   background-color: #fff; | ||||
|   border-left: 1px solid #4f4f4f; | ||||
| } | ||||
| #app .common-layout .el-main .right-box #container { | ||||
|   overflow: hidden; | ||||
|   width: 100%; | ||||
| } | ||||
| #app .common-layout .el-main .right-box #container ::-webkit-scrollbar { | ||||
|   width: 0; | ||||
|   height: 0; | ||||
|   background-color: transparent; | ||||
| } | ||||
| #app .common-layout .el-main .right-box #container .chat-box { | ||||
|   overflow-y: scroll; | ||||
|   --content-font-size: 16px; | ||||
|   --content-color: #c1c1c1; | ||||
|   font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; | ||||
|   padding: 0 0 50px 0; | ||||
| } | ||||
| #app .common-layout .el-main .right-box #container .chat-box .chat-line { | ||||
|   font-size: 14px; | ||||
|   display: flex; | ||||
|   align-items: flex-start; | ||||
| } | ||||
| #app .common-layout .el-main .right-box #container .re-generate { | ||||
|   position: relative; | ||||
|   display: flex; | ||||
|   justify-content: center; | ||||
| } | ||||
| #app .common-layout .el-main .right-box #container .re-generate .btn-box { | ||||
|   position: absolute; | ||||
|   bottom: 10px; | ||||
| } | ||||
| #app .common-layout .el-main .right-box #container .re-generate .btn-box .el-button .el-icon { | ||||
|   margin-right: 5px; | ||||
| } | ||||
| #app .common-layout .el-main .right-box #container .input-box { | ||||
|   background-color: #fff; | ||||
|   display: flex; | ||||
|   justify-content: center; | ||||
|   align-items: center; | ||||
|   box-shadow: 0 2px 15px rgba(0,0,0,0.1); | ||||
|   padding: 0 15px; | ||||
| } | ||||
| #app .common-layout .el-main .right-box #container .input-box .input-container { | ||||
|   width: 100%; | ||||
|   margin: 0; | ||||
|   border: none; | ||||
|   padding: 10px 0; | ||||
|   display: flex; | ||||
|   justify-content: center; | ||||
|   position: relative; | ||||
| } | ||||
| #app .common-layout .el-main .right-box #container .input-box .input-container .el-textarea .el-textarea__inner::-webkit-scrollbar { | ||||
|   width: 0; | ||||
|   height: 0; | ||||
| } | ||||
| #app .common-layout .el-main .right-box #container .input-box .input-container .send-btn { | ||||
|   position: absolute; | ||||
|   right: 12px; | ||||
|   top: 20px; | ||||
| } | ||||
| #app .common-layout .el-main .right-box #container .input-box .input-container .send-btn .el-button { | ||||
|   padding: 8px 5px; | ||||
|   border-radius: 6px; | ||||
|   background: #19c37d; | ||||
|   color: #fff; | ||||
|   font-size: 20px; | ||||
| } | ||||
| #app .common-layout .el-main .right-box #container::-webkit-scrollbar { | ||||
|   width: 0; | ||||
|   height: 0; | ||||
| } | ||||
| #app .el-message-box { | ||||
|   width: 90%; | ||||
|   max-width: 420px; | ||||
| } | ||||
| #app .el-message { | ||||
|   min-width: 100px; | ||||
|   max-width: 600px; | ||||
| } | ||||
| .el-select-dropdown__wrap .el-select-dropdown__item .role-option { | ||||
|   display: flex; | ||||
|   flex-flow: row; | ||||
|   margin-top: 8px; | ||||
| } | ||||
| .el-select-dropdown__wrap .el-select-dropdown__item .role-option .el-image { | ||||
|   width: 20px; | ||||
|   height: 20px; | ||||
|   border-radius: 50%; | ||||
| } | ||||
| .el-select-dropdown__wrap .el-select-dropdown__item .role-option span { | ||||
|   margin-left: 5px; | ||||
|   height: 20px; | ||||
|   line-height: 20px; | ||||
| } | ||||
| .account { | ||||
|   display: flex; | ||||
|   background-color: #90ffc2; | ||||
|   color: #000; | ||||
|   width: 100%; | ||||
|   border-radius: 10px; | ||||
|   padding: 10px; | ||||
| } | ||||
| .account .vip-logo .el-image { | ||||
|   width: 40px; | ||||
|   height: 40px; | ||||
|   border-radius: 100%; | ||||
|   background-color: #fff; | ||||
| } | ||||
| .account .vip-info { | ||||
|   padding: 0 10px 0 10px; | ||||
| } | ||||
| .account .vip-info h4, | ||||
| .account .vip-info p { | ||||
|   margin: 0; | ||||
| } | ||||
| .account .vip-info h4 { | ||||
|   font-weight: bold; | ||||
|   font-size: 16px; | ||||
| } | ||||
| .account .vip-info p { | ||||
|   color: #333; | ||||
| } | ||||
| .account .pay-btn { | ||||
|   width: 100%; | ||||
|   display: flex; | ||||
|   justify-content: right; | ||||
|   align-items: center; | ||||
| } | ||||
| .notice { | ||||
|   background-color: #f6deff; | ||||
|   width: 100%; | ||||
|   padding: 5px 10px; | ||||
|   border-radius: 5px; | ||||
|   color: #cf49ff; | ||||
| } | ||||
| .dialog-service { | ||||
|   text-align: center; | ||||
| } | ||||
| .dialog-service .el-image { | ||||
|   width: 360px; | ||||
| } | ||||
							
								
								
									
										404
									
								
								web/src/assets/css/chat-plus.styl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										404
									
								
								web/src/assets/css/chat-plus.styl
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,404 @@ | ||||
| $sideBgColor = #252526; | ||||
| $borderColor = #4676d0; | ||||
| #app { | ||||
|  | ||||
|   height: 100%; | ||||
|  | ||||
|   .common-layout { | ||||
|     height: 100%; | ||||
|  | ||||
|     // left side | ||||
|  | ||||
|     .el-aside { | ||||
|       background-color: $sideBgColor; | ||||
|  | ||||
|       .title-box { | ||||
|         padding: 6px 10px; | ||||
|         display: flex; | ||||
|         color: #ffffff; | ||||
|         font-size: 20px; | ||||
|  | ||||
|         .logo { | ||||
|           background-color: #ffffff | ||||
|           border-radius: 8px; | ||||
|           width: 35px; | ||||
|           height: 35px; | ||||
|         } | ||||
|  | ||||
|         span { | ||||
|           padding-top: 5px; | ||||
|           padding-left: 10px; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       .chat-list { | ||||
|         display: flex | ||||
|         flex-flow: column | ||||
|         background-color: #28292A | ||||
|         border-top: 1px solid #2F3032 | ||||
|         border-right: 1px solid #2F3032 | ||||
|  | ||||
|         .search-box { | ||||
|           flex-wrap: wrap | ||||
|           padding: 10px 15px; | ||||
|  | ||||
|           .el-input__wrapper { | ||||
|             background-color: #363535; | ||||
|             box-shadow: none | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         // 隐藏滚动条 | ||||
|  | ||||
|         ::-webkit-scrollbar { | ||||
|           width: 0; | ||||
|           height: 0; | ||||
|           background-color: transparent; | ||||
|         } | ||||
|  | ||||
|         .content { | ||||
|           //display flex | ||||
|           //flex-wrap: wrap; | ||||
|           //flex-direction column | ||||
|           width: 100% | ||||
|           overflow-y: scroll | ||||
|  | ||||
|           .chat-list-item { | ||||
|             display: flex | ||||
|             width: 100% | ||||
|             justify-content: flex-start | ||||
|             padding: 8px 12px | ||||
|             //border-bottom: 1px solid #3c3c3c | ||||
|             cursor: pointer | ||||
|  | ||||
|             &:hover { | ||||
|               background-color #343540 | ||||
|             } | ||||
|  | ||||
|             .avatar { | ||||
|               width: 28px; | ||||
|               height: 28px; | ||||
|               border-radius: 50%; | ||||
|             } | ||||
|  | ||||
|             .chat-title-input { | ||||
|               font-size: 14px; | ||||
|               margin-top: 4px; | ||||
|               margin-left: 10px; | ||||
|               overflow: hidden; | ||||
|               white-space: nowrap; | ||||
|               text-overflow: ellipsis; | ||||
|               width: 210px; | ||||
|             } | ||||
|  | ||||
|             .chat-title { | ||||
|               color: #c1c1c1 | ||||
|               padding: 5px 10px; | ||||
|               max-width 220px; | ||||
|               font-size 14px; | ||||
|               overflow: hidden; | ||||
|               white-space: nowrap; | ||||
|               text-overflow: ellipsis; | ||||
|             } | ||||
|  | ||||
|             .btn { | ||||
|               display none | ||||
|               position: absolute; | ||||
|               right: 2px; | ||||
|               top: 16px; | ||||
|               color #ffffff | ||||
|  | ||||
|               .el-icon { | ||||
|                 margin-right 8px; | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|  | ||||
|           .chat-list-item.active { | ||||
|             background-color: #343540; | ||||
|  | ||||
|             .btn { | ||||
|               display inline | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|  | ||||
|  | ||||
|       .tool-box { | ||||
|         display: flex; | ||||
|         justify-content: flex-end; | ||||
|         align-items: center; | ||||
|         padding 0 20px 10px 20px; | ||||
|         border-top 1px solid #3c3c3c; | ||||
|  | ||||
|         .user-info { | ||||
|           width 100% | ||||
|           padding-top 10px; | ||||
|  | ||||
|           .el-dropdown-link { | ||||
|             width 100%; | ||||
|             cursor: pointer | ||||
|             display flex | ||||
|  | ||||
|             .el-image { | ||||
|               width: 20px; | ||||
|               height: 20px; | ||||
|               border-radius: 5px; | ||||
|             } | ||||
|  | ||||
|             .username { | ||||
|               display flex | ||||
|               line-height 22px; | ||||
|               width 230px; | ||||
|               padding-left 10px; | ||||
|  | ||||
|             } | ||||
|  | ||||
|             .el-icon { | ||||
|               color: #cccccc; | ||||
|               line-height 24px; | ||||
|             } | ||||
|           } | ||||
|  | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     .el-main { | ||||
|       overflow: hidden; | ||||
|       --el-main-padding: 0; | ||||
|       margin: 0; | ||||
|  | ||||
|       .chat-head { | ||||
|         width: 100%; | ||||
|         height: 50px; | ||||
|         background-color: #28292A | ||||
|  | ||||
|         .chat-config { | ||||
|           display flex | ||||
|           flex-direction row | ||||
|           align-items: center; | ||||
|           justify-content center; | ||||
|           padding-top 10px; | ||||
|  | ||||
|           .role-select-label { | ||||
|             color #ffffff | ||||
|           } | ||||
|  | ||||
|           .el-select { | ||||
|             //max-width 150px; | ||||
|             margin-right 10px; | ||||
|           } | ||||
|  | ||||
|           .role-select { | ||||
|             max-width 130px; | ||||
|           } | ||||
|  | ||||
|           .el-button { | ||||
|             .el-icon { | ||||
|               margin-right 5px; | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         .iconfont { | ||||
|           margin-right 5px; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       .right-box { | ||||
|         min-width: 0; | ||||
|         flex: 1; | ||||
|         background-color: #ffffff | ||||
|         border-left: 1px solid #4f4f4f | ||||
|  | ||||
|         #container { | ||||
|           overflow: hidden; | ||||
|           width: 100%; | ||||
|  | ||||
|           ::-webkit-scrollbar { | ||||
|             width: 0; | ||||
|             height: 0; | ||||
|             background-color: transparent; | ||||
|           } | ||||
|  | ||||
|           .chat-box { | ||||
|             overflow-y: scroll; | ||||
|             //border-bottom: 1px solid #4f4f4f | ||||
|  | ||||
|             // 变量定义 | ||||
|             --content-font-size: 16px; | ||||
|             --content-color: #c1c1c1; | ||||
|  | ||||
|             font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; | ||||
|             padding: 0 0 50px 0; | ||||
|  | ||||
|             .chat-line { | ||||
|               font-size: 14px; | ||||
|               display: flex; | ||||
|               align-items: flex-start; | ||||
|  | ||||
|             } | ||||
|           } | ||||
|  | ||||
|           .re-generate { | ||||
|             position: relative; | ||||
|             display: flex; | ||||
|             justify-content: center; | ||||
|  | ||||
|             .btn-box { | ||||
|               position: absolute | ||||
|               bottom: 10px; | ||||
|  | ||||
|               .el-button { | ||||
|                 .el-icon { | ||||
|                   margin-right 5px; | ||||
|                 } | ||||
|               } | ||||
|  | ||||
|             } | ||||
|           } | ||||
|  | ||||
|           .input-box { | ||||
|             background-color: #ffffff | ||||
|             display: flex; | ||||
|             justify-content: center; | ||||
|             align-items: center; | ||||
|             box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1); | ||||
|             padding 0 15px; | ||||
|  | ||||
|             .input-container { | ||||
|               width 100% | ||||
|               margin: 0; | ||||
|               border: none; | ||||
|               padding: 10px 0; | ||||
|               display flex | ||||
|               justify-content center | ||||
|               position relative | ||||
|  | ||||
|               .el-textarea { | ||||
|  | ||||
|                 .el-textarea__inner::-webkit-scrollbar { | ||||
|                   width: 0; | ||||
|                   height: 0; | ||||
|                 } | ||||
|               } | ||||
|  | ||||
|               .send-btn { | ||||
|                 position absolute; | ||||
|                 right 12px; | ||||
|                 top 20px; | ||||
|  | ||||
|                 .el-button { | ||||
|                   padding 8px 5px; | ||||
|                   border-radius 6px; | ||||
|                   background: rgb(25, 195, 125) | ||||
|                   color #ffffff; | ||||
|                   font-size 20px; | ||||
|                 } | ||||
|               } | ||||
|  | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         #container::-webkit-scrollbar { | ||||
|           width: 0; | ||||
|           height: 0; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .el-message-box { | ||||
|     width: 90%; | ||||
|     max-width: 420px; | ||||
|   } | ||||
|  | ||||
|   .el-message { | ||||
|     min-width: 100px; | ||||
|     max-width: 600px; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .el-select-dropdown__wrap { | ||||
|   .el-select-dropdown__item { | ||||
|     .role-option { | ||||
|       display flex | ||||
|       flex-flow row | ||||
|       margin-top 8px; | ||||
|  | ||||
|       .el-image { | ||||
|         width 20px | ||||
|         height 20px | ||||
|         border-radius 50% | ||||
|       } | ||||
|  | ||||
|       span { | ||||
|         margin-left 5px; | ||||
|         height 20px; | ||||
|         line-height 20px; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| .account { | ||||
|   display flex | ||||
|   background-color #90FFC2 | ||||
|   color #000000 | ||||
|   width 100% | ||||
|   border-radius 10px | ||||
|   padding 10px | ||||
|  | ||||
|   .vip-logo { | ||||
|     .el-image { | ||||
|       width 40px | ||||
|       height 40px | ||||
|       border-radius 100% | ||||
|       background-color #ffffff | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .vip-info { | ||||
|     padding: 0 10px 0 10px | ||||
|  | ||||
|     h4, p { | ||||
|       margin 0 | ||||
|     } | ||||
|  | ||||
|     h4 { | ||||
|       font-weight bold | ||||
|       font-size 16px; | ||||
|     } | ||||
|  | ||||
|     p { | ||||
|       color #333333 | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .pay-btn { | ||||
|     width 100% | ||||
|     display flex | ||||
|     justify-content right | ||||
|     align-items center | ||||
|  | ||||
|   } | ||||
| } | ||||
|  | ||||
| .notice { | ||||
|   background-color #F6DEFF | ||||
|   width 100% | ||||
|   padding 5px 10px; | ||||
|   border-radius 5px; | ||||
|   color #CF49FF | ||||
| } | ||||
|  | ||||
| .dialog-service { | ||||
|   text-align center | ||||
|  | ||||
|   .el-image { | ||||
|     width 360px; | ||||
|   } | ||||
| } | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user