Compare commits
	
		
			47 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 9cbe36d4c6 | ||
|  | b25bb2cc53 | ||
|  | 79ded6018b | ||
|  | 59f316b341 | ||
|  | f307b8ba7a | ||
|  | 5034a20345 | ||
|  | 26944f9e39 | ||
|  | e64946c3b6 | ||
|  | e0a62d9b35 | ||
|  | 39dbffd8d0 | ||
|  | 952d6183ed | ||
|  | 3365a6008d | ||
|  | 2e13ddf405 | ||
|  | 1d3acc8ed3 | ||
|  | fa341bab30 | ||
|  | 036a6e3e41 | ||
|  | f4c6ca4554 | ||
|  | 327929243c | ||
|  | f4349c7a8c | ||
|  | 4b46d847f0 | ||
|  | c3f016eae8 | ||
|  | ebd3ef842f | ||
|  | 18c033d57f | ||
|  | b676f80110 | ||
|  | f7fbaa534d | ||
|  | ea93a22e14 | ||
|  | 9f7e6778c5 | ||
|  | 6c31a2bfa6 | ||
|  | f943669e18 | ||
|  | 3b26735998 | ||
|  | 79d25769ee | ||
|  | 1dd6800987 | ||
|  | 5e673a9ee0 | ||
|  | 92eb67a2af | ||
|  | b1bed59be2 | ||
|  | 0ac732a3a3 | ||
|  | bf3f68fa19 | ||
|  | 46a551df16 | ||
|  | 20a12462b1 | ||
|  | a49fb1940e | ||
|  | 32774d23c7 | ||
|  | 7ecd7eeba1 | ||
|  | 0cc9cf8b45 | ||
|  | d06f94bddd | ||
|  | b5955f08c9 | ||
|  | c120569894 | ||
|  | aa376f1737 | 
							
								
								
									
										29
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						| @@ -1,4 +1,33 @@ | ||||
| # 更新日志 | ||||
| ## v3.1.3 | ||||
| 1. 页面重构:重后 Home 页面,拆分成聊天,MJ绘画,SD 绘画,应用广场等多个功能菜单。 | ||||
| 2. 功能新增:新增 MidJourney 专业绘画页面,开放更高级的 MJ 绘画姿势。 | ||||
| 3. 功能优化:采用队列的方式控制绘画任务并发,简化任务回调通知逻辑,给任务回调加锁。 | ||||
| 4. 功能优化:精简用户表字段,删除用户名和昵称,只保留手机号。 | ||||
| 5. 功能优化:优化文件上传服务工厂实现,只创建激活的 Uploader 服务,节省资源。 | ||||
| 6. Bug修复:修复 JWT token 有效期计算错误的 Bug。 | ||||
|  | ||||
| ## v3.1.2 | ||||
| 1. 功能新增:新增七牛云 OSS 实现,目前已支持三种文件上传服务:Local, Minio, QiNiu OSS。 | ||||
| 2. 功能新增:新增桌面版,使用 electron 套壳网页版。 | ||||
| 3. Bug修复:自动去除众筹核销时候转账单号中的空格,防止复制的时候多复制了空格。 | ||||
| 4. 功能优化:ChatPlus.vue 页面支持通过 chat_id path variable 来定位到指定的聊天。 | ||||
| 5. 功能优化:取消导出聊天页面的授权验证 | ||||
| 6. 功能优化:所有路由跳转都使用绝对路径 | ||||
|  | ||||
| ## v3.1.1 | ||||
| 紧急修复版本,采用弹窗的方式显示验证码,解决验证码在低分辨率下被掩盖的Bug | ||||
|  | ||||
| ## v3.1.0(大版本更新) | ||||
| 1. 功能重构:将聊天模型独立拆分,以便支持多平台模型,目前已经内置支持 OPenAI,Azure 以及 ChatGLM,用户可以在这两个平台的模型中随意切换,体验不同的模型聊天。 | ||||
| 2. 功能重构:重写系统 API 授权机制,使用 JWT 替换传统的 session 会话授权,使得 API 授权变得更加灵活。 | ||||
| 3. 功能重构:重构文件夹上传服务,支持多种文件上传存储handler,目前已经实现本地存储和 minio oss 存储。 | ||||
| 4. 功能优化:更新头像自动删除旧的图片资源。 | ||||
| 5. 功能优化:将应用日志在终端输出的同时存盘,方便 docker 部署查看日志。 | ||||
| 6. 功能新增:允许用户配置自己的 OPenAI,Azure 以及 ChatGLM API KEY。 | ||||
| 7. 功能优化:优化移动版的行为验证码样式,修复低分辨率显示器验证码被遮挡的 Bug | ||||
| 8. 升级 gin, element-plus,redis 组件到最新版本。  | ||||
| 9. Bug修复:修复若干已知的的 Bug | ||||
|  | ||||
| ## v3.0.7 | ||||
|  | ||||
|   | ||||
							
								
								
									
										90
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -1,42 +1,48 @@ | ||||
| # ChatGPT-Plus | ||||
|  | ||||
| **ChatGPT-PLUS** 是基于 OpenAI API 实现的 ChatGPT 聊天系统。主要有如下特性: | ||||
| **ChatGPT-PLUS** 基于 AI 大语言模型 API 实现的 AI 助手全套开源解决方案,自带运营管理后台,开箱即用。集成了 OpenAI, Azure, ChatGLM,讯飞星火,文心一言等多个平台的大语言模型。主要有如下特性: | ||||
|  | ||||
| * 完整的开源系统,前端应用和后台管理系统皆可开箱即用。 | ||||
| * 聊天体验跟 ChatGPT 官方版本完全一致。 | ||||
| * 内置了各种预训练好的角色,比如小红书写手,英语翻译大师,苏格拉底,孔子,乔布斯,周报助手等。轻松满足你的各种聊天和应用需求。 | ||||
| * 支持 MidJourney AI 绘画集成,开箱即用。 | ||||
| * 支持使用个人微信二维码作为充值收费的支付渠道,无需企业支付通道。(可定制开发其他支付通道支持) | ||||
| * 集成插件 API 功能,可结合 GPT 开发各种强大的插件,已内置实现了微博热搜,今日头条,今日早报和 AI 绘画函数插件。 | ||||
|  | ||||
| ## 功能截图 | ||||
|  | ||||
| ### PC 端聊天界面 | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ### 新版聊天界面 | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ### MidJourney 专业绘画界面(v3.1.3) | ||||
|  | ||||
|  | ||||
| ### 自动调用函数插件 | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ### 用户设置 | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ### 登录页面 | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ### 管理后台 | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ### 移动端 Web 页面 | ||||
|  | ||||
| @@ -95,11 +101,12 @@ ChatGPT 的服务。 | ||||
|  | ||||
| ## TODOLIST | ||||
|  | ||||
| * [ ] 整合 Midjourney AI 绘画 API | ||||
| * [ ] 开发移动端聊天页面 | ||||
| * [x] 整合 Midjourney AI 绘画 API | ||||
| * [x] 开发移动端聊天页面 | ||||
| * [x] 接入微信支付功能 | ||||
| * [x] 支持 ChatGPT 函数功能,通过函数实现插件 | ||||
| * [ ] 接入语音和 TTS API,支持语音聊天 | ||||
| * [ ] 支持基于知识库的 AI 问答 | ||||
| * [ ] 开发桌面版应用 | ||||
| * [ ] 开发手机 App 客户端 | ||||
|  | ||||
| ## Docker 快速部署 | ||||
| @@ -119,7 +126,7 @@ cd docker/mysql | ||||
| # 创建 mysql 容器 | ||||
| docker-compose up -d | ||||
| # 导入数据库 | ||||
| docker exec -i chatgpt-plus-mysql sh -c 'exec mysql -uroot -p12345678' < ../../database/chatgpt_plus.sql | ||||
| docker exec -i chatgpt-plus-mysql sh -c 'exec mysql -uroot -p12345678' < ../../database/chatgpt_plus-v3.1.3.sql | ||||
| ``` | ||||
|  | ||||
| 如果你本地已经安装了 MySQL 服务,那么你只需手动导入数据库即可。 | ||||
| @@ -137,51 +144,59 @@ source database/chatgpt_plus.sql | ||||
|  | ||||
| ```toml | ||||
| Listen = "0.0.0.0:5678" | ||||
| ProxyURL = ["YOUR_PROXY_URL"] # 替换成你本地代理,如:http://127.0.0.1:7777 | ||||
| #ProxyURL = "" 如果你的服务器本身就在墙外,那么你直接留空就好了 | ||||
| ProxyURL = "" # 如 http://127.0.0.1:7777 | ||||
| MysqlDns = "root:12345678@tcp(172.22.11.200:3307)/chatgpt_plus?charset=utf8&parseTime=True&loc=Local" | ||||
| StaticDir = "./static" # 静态资源的目录 | ||||
| StaticUrl = "/static" # 静态资源访问 URL | ||||
| AesEncryptKey = "" | ||||
|  | ||||
| [Session] | ||||
|   SecretKey = "azyehq3ivunjhbntz78isj00i4hz2mt9xtddysfucxakadq4qbfrt0b7q3lnvg80" | ||||
|   Name = "CHAT_SESSION_ID" | ||||
|   Path = "/" | ||||
|   Domain = "" | ||||
|   SecretKey = "azyehq3ivunjhbntz78isj00i4hz2mt9xtddysfucxakadq4qbfrt0b7q3lnvg80" # 注意:这个是 JWT Token 授权密钥,生产环境请务必更换 | ||||
|   MaxAge = 86400 | ||||
|   Secure = false | ||||
|   HttpOnly = false | ||||
|   SameSite = 2 | ||||
|  | ||||
| [Manager] | ||||
|   Username = "admin" | ||||
|   Password = "admin123" # 如果是生产环境的话,这里管理员的密码记得修改 | ||||
|    | ||||
| [Redis] # redis 配置信息 | ||||
|   Host = "localhost"  | ||||
|   Port = 6379 | ||||
|   Password = "" | ||||
|   DB = 0 | ||||
|    | ||||
| [ApiConfig] # 微博热搜,今日头条等函数服务 API 配置,此为第三方插件服务,如需使用请联系作者开通 | ||||
|   ApiURL = "{URL}" | ||||
|   AppId = "{APP_ID}" | ||||
|   Token = "{TOKEN}" | ||||
|   ApiURL = "" | ||||
|   AppId = "" | ||||
|   Token = "" | ||||
|  | ||||
| [SmsConfig] # 阿里云短信服务配置 | ||||
|   AccessKey = "{YOUR_ACCESS_KEY}" | ||||
|   AccessSecret = "{YOUR_SECRET_KEY}" | ||||
|   AccessKey = "" | ||||
|   AccessSecret = "" | ||||
|   Product = "Dysmsapi" | ||||
|   Domain = "dysmsapi.aliyuncs.com" | ||||
|  | ||||
| [ExtConfig] # MidJourney和微信机器人服务 API 配置,开通此功能需要配合 chatpgt-plus-exts 项目部署 | ||||
|   ApiURL = "插件扩展 API 地址" | ||||
|   Token = "插件扩展 API Token" # 这个 token 随便填,只要确保跟 chatgpt-plus-exts 项目的 token 一样就行  | ||||
|   ApiURL = "" # 插件扩展 API 地址 | ||||
|   Token = "" # 这个 token 随便填,只要确保跟 chatgpt-plus-exts 项目的 token 一样就行  | ||||
|    | ||||
| [OSS] # OSS 配置,用于存储 MJ 绘画图片 | ||||
|    Active = "local" # 默认使用本地文件存储 | ||||
|    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" | ||||
|      Endpoint = "" # 如 172.22.11.200:9000 | ||||
|      AccessKey = "" # 自己去 Minio 控制台去创建一个 Access Key | ||||
|      AccessSecret = "" | ||||
|      Bucket = "chatgpt-plus" # 替换为你自己创建的 Bucket,注意要给 Bucket 设置公开的读权限,否则会出现图片无法显示。 | ||||
|      UseSSL = false | ||||
|      Domain = "minio 文件公开访问地址" # 地址必须是能够通过公网访问的,否则会出现图片无法显示。 | ||||
|      Domain = "" # 地址必须是能够通过公网访问的,否则会出现图片无法显示。 | ||||
|    [OSS.QiNiu] # 七牛云 OSS 配置 | ||||
|        Zone = "z2" # 区域,z0:华东,z1: 华北,na0:北美,as0:新加坡 | ||||
|        AccessKey = "" | ||||
|        AccessSecret = "" | ||||
|        Bucket = "" | ||||
|        Domain = "" # OSS Bucket 所绑定的域名,如 https://img.r9it.com | ||||
| ``` | ||||
|  | ||||
| > 如果要启用微信收款服务和 MidJourney | ||||
| @@ -202,6 +217,11 @@ location /api/ { | ||||
|        proxy_set_header Upgrade $http_upgrade; | ||||
|        proxy_set_header Connection $connection_upgrade; | ||||
|        proxy_pass http://172.28.173.76:6789; # 这里改成后端服务的内网 IP 地址 | ||||
|         | ||||
| # 静态资源转发 | ||||
| location /static/ { | ||||
|    proxy_pass http://172.22.11.47:5678; # 这里改成后端服务的内网 IP 地址 | ||||
| } | ||||
| } | ||||
| ``` | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| SHELL=/usr/bin/env bash | ||||
| NAME := chatgpt-v3 | ||||
| NAME := chatgpt-plus | ||||
| all: window linux darwin | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -1,54 +1,54 @@ | ||||
| Listen = "0.0.0.0:5678" | ||||
| ProxyURL = "http://172.22.11.200:7777" | ||||
| MysqlDns = "root:mysql_pass@tcp(localhost:3306)/chatgpt_plus?charset=utf8mb4&collation=utf8mb4_unicode_ci&parseTime=True&loc=Local" | ||||
| StaticDir = "./static" | ||||
| StaticUrl = "http://localhost:5678/static" | ||||
| AesEncryptKey = "{YOUR_AES_KEY}" | ||||
| ProxyURL = "" # 如 http://127.0.0.1:7777 | ||||
| MysqlDns = "root:12345678@tcp(172.22.11.200:3307)/chatgpt_plus?charset=utf8&parseTime=True&loc=Local" | ||||
| StaticDir = "./static" # 静态资源的目录 | ||||
| StaticUrl = "/static" # 静态资源访问 URL | ||||
| AesEncryptKey = "" | ||||
|  | ||||
| [Session] | ||||
|   Driver = "cookie" | ||||
|   SecretKey = "m0cjm3gsuw9jk73np1ni7r42koilybjcndlycjdmq7za3pbqn7w12fyok5pqh6q5" | ||||
|   Name = "CHAT_SESSION_ID" | ||||
|   Path = "/" | ||||
|   Domain = "localhost" | ||||
|   SecretKey = "azyehq3ivunjhbntz78isj00i4hz2mt9xtddysfucxakadq4qbfrt0b7q3lnvg80" # 注意:这个是 JWT Token 授权密钥,生产环境请务必更换 | ||||
|   MaxAge = 86400 | ||||
|   Secure = false | ||||
|   HttpOnly = false | ||||
|   SameSite = 2 | ||||
|  | ||||
| [Manager] | ||||
|   Username = "admin" | ||||
|   Password = "admin123" | ||||
|   Password = "admin123" # 如果是生产环境的话,这里管理员的密码记得修改 | ||||
|  | ||||
| [Redis] | ||||
| [Redis] # redis 配置信息 | ||||
|   Host = "localhost" | ||||
|   Port = 6379 | ||||
|   Password = "" | ||||
|   DB = 0 | ||||
|  | ||||
| [ApiConfig] | ||||
|   ApiURL = "{URL}" | ||||
|   AppId = "{APP_ID}" | ||||
|   Token = "{TOKEN}" | ||||
| [ApiConfig] # 微博热搜,今日头条等函数服务 API 配置,此为第三方插件服务,如需使用请联系作者开通 | ||||
|   ApiURL = "" | ||||
|   AppId = "" | ||||
|   Token = "" | ||||
|  | ||||
| [SmsConfig] | ||||
|   AccessKey = "{YOUR_ACCESS_KEY}" | ||||
|   AccessSecret = "{YOUR_SECRET_KEY}" | ||||
| [SmsConfig] # 阿里云短信服务配置 | ||||
|   AccessKey = "" | ||||
|   AccessSecret = "" | ||||
|   Product = "Dysmsapi" | ||||
|   Domain = "dysmsapi.aliyuncs.com" | ||||
|  | ||||
| [ExtConfig] | ||||
|   ApiURL = "插件扩展 API 地址" | ||||
|   Token = "插件扩展 API Token" | ||||
| [ExtConfig] # MidJourney和微信机器人服务 API 配置,开通此功能需要配合 chatpgt-plus-exts 项目部署 | ||||
|   ApiURL = "" # 插件扩展 API 地址 | ||||
|   Token = "" # 这个 token 随便填,只要确保跟 chatgpt-plus-exts 项目的 token 一样就行 | ||||
|  | ||||
|  [OSS] | ||||
|    Active = "local" | ||||
| [OSS] # OSS 配置,用于存储 MJ 绘画图片 | ||||
|    Active = "local" # 默认使用本地文件存储引擎 | ||||
|    [OSS.Local] | ||||
|      BasePath = "./static/upload" | ||||
|      BaseURL = "http://localhost:5678/static/upload" | ||||
|      BasePath = "./static/upload" # 本地文件上传根路径 | ||||
|      BaseURL = "http://localhost:5678/static/upload" # 本地上传文件根 URL 如果是线上,则直接设置为 /static/upload 即可 | ||||
|    [OSS.Minio] | ||||
|      Endpoint = "IP:端口" | ||||
|      AccessKey = "minio oss access key" | ||||
|      AccessSecret = "minio oss access secret" | ||||
|      Bucket = "chatgpt-plus" | ||||
|      Endpoint = "" # 如 172.22.11.200:9000 | ||||
|      AccessKey = "" # 自己去 Minio 控制台去创建一个 Access Key | ||||
|      AccessSecret = "" | ||||
|      Bucket = "chatgpt-plus" # 替换为你自己创建的 Bucket,注意要给 Bucket 设置公开的读权限,否则会出现图片无法显示。 | ||||
|      UseSSL = false | ||||
|      Domain = "minio 文件公开访问地址" | ||||
|      Domain = "" # 地址必须是能够通过公网访问的,否则会出现图片无法显示。 | ||||
|    [OSS.QiNiu] # 七牛云 OSS 配置 | ||||
|        Zone = "z2" # 区域,z0:华东,z1: 华北,na0:北美,as0:新加坡 | ||||
|        AccessKey = "" | ||||
|        AccessSecret = "" | ||||
|        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 { | ||||
| @@ -53,14 +53,13 @@ func NewServer(appConfig *types.AppConfig, functions map[string]function.Functio | ||||
| 	} | ||||
| } | ||||
|  | ||||
| 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) | ||||
| @@ -105,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) { | ||||
| @@ -151,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, Chat-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") | ||||
| 			//设置缓存时间 | ||||
| @@ -175,13 +138,16 @@ 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" || | ||||
| 			c.Request.URL.Path == "/api/mj/proxy" || | ||||
| 			strings.HasPrefix(c.Request.URL.Path, "/api/sms/") || | ||||
| 			strings.HasPrefix(c.Request.URL.Path, "/api/captcha/") || | ||||
| 			strings.HasPrefix(c.Request.URL.Path, "/static/") || | ||||
| @@ -190,29 +156,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" || c.Request.URL.Path == "/api/mj/client" { | ||||
| 			tokenString = c.Query("token") | ||||
| 		} else { | ||||
| 			tokenString = c.GetHeader(types.UserAuthHeader) | ||||
| 		} | ||||
| 		if tokenString == "" { | ||||
| 			resp.ERROR(c, "You should put Authorization in request headers") | ||||
| 			c.Abort() | ||||
| 			return | ||||
| 		} | ||||
| 		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,15 +22,8 @@ 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{}, | ||||
| 		ExtConfig: types.ChatPlusExtConfig{Token: utils.RandString(32)}, | ||||
|   | ||||
| @@ -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,21 +35,18 @@ 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 MjTask struct { | ||||
| 	ChatId      string | ||||
| 	MessageId   string | ||||
| 	MessageHash string | ||||
| 	UserId      uint | ||||
| 	RoleId      uint | ||||
| 	Icon        string | ||||
| type ChatModel struct { | ||||
| 	Id       uint     `json:"id"` | ||||
| 	Platform Platform `json:"platform"` | ||||
| 	Value    string   `json:"value"` | ||||
| } | ||||
|  | ||||
| type ApiError struct { | ||||
| @@ -70,5 +68,3 @@ var ModelToTokens = map[string]int{ | ||||
| 	"gpt-4":             8192, | ||||
| 	"gpt-4-32k":         32768, | ||||
| } | ||||
|  | ||||
| const TaskStorePrefix = "/tasks/" | ||||
|   | ||||
| @@ -2,7 +2,6 @@ package types | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| ) | ||||
|  | ||||
| type AppConfig struct { | ||||
| @@ -45,6 +44,7 @@ type OSSConfig struct { | ||||
| 	Active string | ||||
| 	Local  LocalStorageConfig | ||||
| 	Minio  MinioConfig | ||||
| 	QiNiu  QiNiuConfig | ||||
| } | ||||
| type MinioConfig struct { | ||||
| 	Endpoint     string | ||||
| @@ -55,6 +55,14 @@ type MinioConfig struct { | ||||
| 	Domain       string | ||||
| } | ||||
|  | ||||
| type QiNiuConfig struct { | ||||
| 	Zone         string | ||||
| 	AccessKey    string | ||||
| 	AccessSecret string | ||||
| 	Bucket       string | ||||
| 	Domain       string | ||||
| } | ||||
|  | ||||
| type LocalStorageConfig struct { | ||||
| 	BasePath string | ||||
| 	BaseURL  string | ||||
| @@ -64,6 +72,7 @@ type RedisConfig struct { | ||||
| 	Host     string | ||||
| 	Port     int | ||||
| 	Password string | ||||
| 	DB       int | ||||
| } | ||||
|  | ||||
| func (c RedisConfig) Url() string { | ||||
| @@ -76,37 +85,33 @@ 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 { | ||||
| @@ -115,6 +120,8 @@ type SystemConfig struct { | ||||
| 	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 绘画功能 | ||||
| } | ||||
|   | ||||
| @@ -83,15 +83,7 @@ var InnerFunctions = []Function{ | ||||
| 			Properties: map[string]Property{ | ||||
| 				"prompt": { | ||||
| 					Type:        "string", | ||||
| 					Description: "绘画内容描述,提示词,如果该参数中有中文的话,则需要翻译成英文", | ||||
| 				}, | ||||
| 				"ar": { | ||||
| 					Type:        "string", | ||||
| 					Description: "图片长宽比,如 --ar 4:3", | ||||
| 				}, | ||||
| 				"niji": { | ||||
| 					Type:        "string", | ||||
| 					Description: "动漫模型版本,例如 --niji 5", | ||||
| 					Description: "提示词,如果该参数中有中文的话,则需要翻译成英文。提示词中的参数作为提示的一部分,不要删除", | ||||
| 				}, | ||||
| 			}, | ||||
| 			Required: []string{}, | ||||
|   | ||||
| @@ -9,7 +9,7 @@ type MKey interface { | ||||
| 	string | int | ||||
| } | ||||
| type MValue interface { | ||||
| 	*WsClient | *ChatSession | context.CancelFunc | []interface{} | MjTask | ||||
| 	*WsClient | *ChatSession | context.CancelFunc | []interface{} | ||||
| } | ||||
| 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 | ||||
| } | ||||
|   | ||||
| @@ -34,4 +34,5 @@ const ( | ||||
| 	OkMsg       = "Success" | ||||
| 	ErrorMsg    = "系统开小差了" | ||||
| 	InvalidArgs = "非法参数或参数解析失败" | ||||
| 	NoData      = "No Data" | ||||
| ) | ||||
|   | ||||
							
								
								
									
										15
									
								
								api/go.mod
									
									
									
									
									
								
							
							
						
						| @@ -5,12 +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/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/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 | ||||
| @@ -19,9 +22,10 @@ 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 | ||||
| @@ -30,7 +34,6 @@ require ( | ||||
| 	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 | ||||
| @@ -41,12 +44,10 @@ require ( | ||||
| 	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/minio-go/v7 v7.0.62 // 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 | ||||
| @@ -60,6 +61,7 @@ require ( | ||||
| 	golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect | ||||
| 	golang.org/x/mod v0.11.0 // indirect | ||||
| 	golang.org/x/net v0.14.0 // indirect | ||||
| 	golang.org/x/sync v0.3.0 // indirect | ||||
| 	golang.org/x/text v0.12.0 // indirect | ||||
| 	golang.org/x/tools v0.10.0 // indirect | ||||
| 	google.golang.org/protobuf v1.30.0 // indirect | ||||
| @@ -73,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 | ||||
|   | ||||
							
								
								
									
										93
									
								
								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/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,8 +66,6 @@ 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= | ||||
| @@ -63,13 +73,6 @@ github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 h1:hR7/MlvK23p6+lIw9S | ||||
| github.com/google/pprof v0.0.0-20230602150820-91b7bce49751/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA= | ||||
| github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= | ||||
| github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||
| github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= | ||||
| github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= | ||||
| github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= | ||||
| github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= | ||||
| github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= | ||||
| github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= | ||||
| github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= | ||||
| github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= | ||||
| github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= | ||||
| github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= | ||||
| @@ -77,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= | ||||
| @@ -91,21 +93,21 @@ 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/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 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= | ||||
| 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= | ||||
| @@ -124,9 +126,10 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ | ||||
| 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= | ||||
| @@ -135,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= | ||||
| @@ -153,6 +159,9 @@ 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/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= | ||||
| @@ -180,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= | ||||
| @@ -197,50 +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= | ||||
| @@ -251,11 +274,11 @@ 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= | ||||
|   | ||||
| @@ -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,23 @@ 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)).Unix(), | ||||
| 		}) | ||||
| 		tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey)) | ||||
| 		if err != nil { | ||||
| 			resp.ERROR(c, "Save session failed") | ||||
| 			resp.ERROR(c, "Failed to generate token, "+err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 		manager.Password = "" // 清空密码] | ||||
| 		resp.SUCCESS(c, manager) | ||||
| 		// 保存到 redis | ||||
| 		key := "users/" + manager.Username | ||||
| 		if _, err := h.redis.Set(context.Background(), key, tokenString, 0).Result(); err != nil { | ||||
| 			resp.ERROR(c, "error with save token: "+err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 		resp.SUCCESS(c, tokenString) | ||||
| 	} else { | ||||
| 		resp.ERROR(c, "用户名或者密码错误") | ||||
| 	} | ||||
| @@ -52,11 +65,9 @@ func (h *ManagerHandler) Login(c *gin.Context) { | ||||
|  | ||||
| // Logout 注销 | ||||
| func (h *ManagerHandler) Logout(c *gin.Context) { | ||||
| 	session := sessions.Default(c) | ||||
| 	session.Delete(types.SessionAdmin) | ||||
| 	err := session.Save() | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, "Save session failed") | ||||
| 	key := h.GetUserKey(c) | ||||
| 	if _, err := h.redis.Del(c, key).Result(); err != nil { | ||||
| 		logger.Error("error with delete session: ", err) | ||||
| 	} else { | ||||
| 		resp.SUCCESS(c) | ||||
| 	} | ||||
| @@ -64,9 +75,8 @@ func (h *ManagerHandler) Logout(c *gin.Context) { | ||||
|  | ||||
| // Session 会话检测 | ||||
| func (h *ManagerHandler) Session(c *gin.Context) { | ||||
| 	session := sessions.Default(c) | ||||
| 	admin := session.Get(types.SessionAdmin) | ||||
| 	if admin == nil { | ||||
| 	token := c.GetHeader(types.AdminAuthHeader) | ||||
| 	if token == "" { | ||||
| 		resp.NotAuth(c) | ||||
| 	} else { | ||||
| 		resp.SUCCESS(c) | ||||
|   | ||||
| @@ -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
									
								
							
							
						
						| @@ -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) | ||||
|   | ||||
| @@ -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,10 +62,8 @@ 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"` | ||||
| @@ -89,7 +81,6 @@ 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, | ||||
| @@ -100,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, | ||||
| 		} | ||||
| @@ -204,7 +192,7 @@ func (h *UserHandler) LoginLog(c *gin.Context) { | ||||
| 	h.db.Model(&model.UserLoginLog{}).Count(&total) | ||||
| 	offset := (page - 1) * pageSize | ||||
| 	var items []model.UserLoginLog | ||||
| 	res := h.db.Offset(offset).Limit(pageSize).Find(&items) | ||||
| 	res := h.db.Offset(offset).Limit(pageSize).Order("id DESC").Find(&items) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "获取数据失败") | ||||
| 		return | ||||
|   | ||||
							
								
								
									
										288
									
								
								api/handler/azure_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,288 @@ | ||||
| package handler | ||||
|  | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/store/vo" | ||||
| 	"chatplus/utils" | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"gorm.io/gorm" | ||||
| 	"io" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 	"unicode/utf8" | ||||
| ) | ||||
|  | ||||
| // 将消息发送给 Azure API 并获取结果,通过 WebSocket 推送到客户端 | ||||
| func (h *ChatHandler) sendAzureMessage( | ||||
| 	chatCtx []interface{}, | ||||
| 	req types.ApiRequest, | ||||
| 	userVo vo.User, | ||||
| 	ctx context.Context, | ||||
| 	session *types.ChatSession, | ||||
| 	role model.ChatRole, | ||||
| 	prompt string, | ||||
| 	ws *types.WsClient) error { | ||||
| 	promptCreatedAt := time.Now() // 记录提问时间 | ||||
| 	start := time.Now() | ||||
| 	var apiKey = userVo.ChatConfig.ApiKeys[session.Model.Platform] | ||||
| 	response, err := h.doRequest(ctx, req, session.Model.Platform, &apiKey) | ||||
| 	logger.Info("HTTP请求完成,耗时:", time.Now().Sub(start)) | ||||
| 	if err != nil { | ||||
| 		if strings.Contains(err.Error(), "context canceled") { | ||||
| 			logger.Info("用户取消了请求:", prompt) | ||||
| 			return nil | ||||
| 		} else if strings.Contains(err.Error(), "no available key") { | ||||
| 			utils.ReplyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!") | ||||
| 			return nil | ||||
| 		} else { | ||||
| 			logger.Error(err) | ||||
| 		} | ||||
|  | ||||
| 		utils.ReplyMessage(ws, ErrorMsg) | ||||
| 		utils.ReplyMessage(ws, "") | ||||
| 		return err | ||||
| 	} else { | ||||
| 		defer response.Body.Close() | ||||
| 	} | ||||
|  | ||||
| 	contentType := response.Header.Get("Content-Type") | ||||
| 	if strings.Contains(contentType, "text/event-stream") { | ||||
| 		replyCreatedAt := time.Now() // 记录回复时间 | ||||
| 		// 循环读取 Chunk 消息 | ||||
| 		var message = types.Message{} | ||||
| 		var contents = make([]string, 0) | ||||
| 		var functionCall = false | ||||
| 		var functionName string | ||||
| 		var arguments = make([]string, 0) | ||||
| 		scanner := bufio.NewScanner(response.Body) | ||||
| 		for scanner.Scan() { | ||||
| 			line := scanner.Text() | ||||
| 			if !strings.Contains(line, "data:") || len(line) < 30 { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			var responseBody = types.ApiResponse{} | ||||
| 			err = json.Unmarshal([]byte(line[6:]), &responseBody) | ||||
| 			if err != nil || len(responseBody.Choices) == 0 { // 数据解析出错 | ||||
| 				logger.Error(err, line) | ||||
| 				utils.ReplyMessage(ws, ErrorMsg) | ||||
| 				utils.ReplyMessage(ws, "") | ||||
| 				break | ||||
| 			} | ||||
|  | ||||
| 			fun := responseBody.Choices[0].Delta.FunctionCall | ||||
| 			if functionCall && fun.Name == "" { | ||||
| 				arguments = append(arguments, fun.Arguments) | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			if !utils.IsEmptyValue(fun) { | ||||
| 				functionName = fun.Name | ||||
| 				f := h.App.Functions[functionName] | ||||
| 				if f != nil { | ||||
| 					functionCall = true | ||||
| 					utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart}) | ||||
| 					utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: fmt.Sprintf("正在调用函数 `%s` 作答 ...\n\n", f.Name())}) | ||||
| 					continue | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if responseBody.Choices[0].FinishReason == "function_call" { // 函数调用完毕 | ||||
| 				break | ||||
| 			} | ||||
|  | ||||
| 			// 初始化 role | ||||
| 			if responseBody.Choices[0].Delta.Role != "" && message.Role == "" { | ||||
| 				message.Role = responseBody.Choices[0].Delta.Role | ||||
| 				utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart}) | ||||
| 				continue | ||||
| 			} else if responseBody.Choices[0].FinishReason != "" { | ||||
| 				break // 输出完成或者输出中断了 | ||||
| 			} else { | ||||
| 				content := responseBody.Choices[0].Delta.Content | ||||
| 				contents = append(contents, utils.InterfaceToString(content)) | ||||
| 				utils.ReplyChunkMessage(ws, types.WsMessage{ | ||||
| 					Type:    types.WsMiddle, | ||||
| 					Content: utils.InterfaceToString(responseBody.Choices[0].Delta.Content), | ||||
| 				}) | ||||
| 			} | ||||
| 		} // end for | ||||
|  | ||||
| 		if err := scanner.Err(); err != nil { | ||||
| 			if strings.Contains(err.Error(), "context canceled") { | ||||
| 				logger.Info("用户取消了请求:", prompt) | ||||
| 			} else { | ||||
| 				logger.Error("信息读取出错:", err) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if functionCall { // 调用函数完成任务 | ||||
| 			var params map[string]interface{} | ||||
| 			_ = utils.JsonDecode(strings.Join(arguments, ""), ¶ms) | ||||
| 			logger.Debugf("函数名称: %s, 函数参数:%s", functionName, params) | ||||
|  | ||||
| 			// for creating image, check if the user's img_calls > 0 | ||||
| 			if functionName == types.FuncMidJourney && userVo.ImgCalls <= 0 { | ||||
| 				utils.ReplyMessage(ws, "**当前用户剩余绘图次数已用尽,请扫描下面二维码联系管理员!**") | ||||
| 				utils.ReplyMessage(ws, "") | ||||
| 			} else { | ||||
| 				f := h.App.Functions[functionName] | ||||
| 				if functionName == types.FuncMidJourney { | ||||
| 					params["user_id"] = userVo.Id | ||||
| 					params["role_id"] = role.Id | ||||
| 					params["chat_id"] = session.ChatId | ||||
| 					params["icon"] = "/images/avatar/mid_journey.png" | ||||
| 					params["session_id"] = session.SessionId | ||||
| 				} | ||||
| 				data, err := f.Invoke(params) | ||||
| 				if err != nil { | ||||
| 					msg := "调用函数出错:" + err.Error() | ||||
| 					utils.ReplyChunkMessage(ws, types.WsMessage{ | ||||
| 						Type:    types.WsMiddle, | ||||
| 						Content: msg, | ||||
| 					}) | ||||
| 					contents = append(contents, msg) | ||||
| 				} else { | ||||
| 					content := data | ||||
| 					if functionName == types.FuncMidJourney { | ||||
| 						content = fmt.Sprintf("绘画提示词:%s 已推送任务到 MidJourney 机器人,请耐心等待任务执行...", data) | ||||
| 						h.App.MjTaskClients.Put(session.SessionId, ws) | ||||
| 						// update user's img_calls | ||||
| 						h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1)) | ||||
| 					} | ||||
|  | ||||
| 					utils.ReplyChunkMessage(ws, types.WsMessage{ | ||||
| 						Type:    types.WsMiddle, | ||||
| 						Content: content, | ||||
| 					}) | ||||
| 					contents = append(contents, content) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// 消息发送成功 | ||||
| 		if len(contents) > 0 { | ||||
| 			// 更新用户的对话次数 | ||||
| 			if userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" { | ||||
| 				h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("calls", gorm.Expr("calls - ?", 1)) | ||||
| 			} | ||||
|  | ||||
| 			if message.Role == "" { | ||||
| 				message.Role = "assistant" | ||||
| 			} | ||||
| 			message.Content = strings.Join(contents, "") | ||||
| 			useMsg := types.Message{Role: "user", Content: prompt} | ||||
|  | ||||
| 			// 更新上下文消息,如果是调用函数则不需要更新上下文 | ||||
| 			if h.App.ChatConfig.EnableContext && functionCall == false { | ||||
| 				chatCtx = append(chatCtx, useMsg)  // 提问消息 | ||||
| 				chatCtx = append(chatCtx, message) // 回复消息 | ||||
| 				h.App.ChatContexts.Put(session.ChatId, chatCtx) | ||||
| 			} | ||||
|  | ||||
| 			// 追加聊天记录 | ||||
| 			if h.App.ChatConfig.EnableHistory { | ||||
| 				useContext := true | ||||
| 				if functionCall { | ||||
| 					useContext = false | ||||
| 				} | ||||
|  | ||||
| 				// for prompt | ||||
| 				promptToken, err := utils.CalcTokens(prompt, req.Model) | ||||
| 				if err != nil { | ||||
| 					logger.Error(err) | ||||
| 				} | ||||
| 				historyUserMsg := model.HistoryMessage{ | ||||
| 					UserId:     userVo.Id, | ||||
| 					ChatId:     session.ChatId, | ||||
| 					RoleId:     role.Id, | ||||
| 					Type:       types.PromptMsg, | ||||
| 					Icon:       userVo.Avatar, | ||||
| 					Content:    prompt, | ||||
| 					Tokens:     promptToken, | ||||
| 					UseContext: useContext, | ||||
| 				} | ||||
| 				historyUserMsg.CreatedAt = promptCreatedAt | ||||
| 				historyUserMsg.UpdatedAt = promptCreatedAt | ||||
| 				res := h.db.Save(&historyUserMsg) | ||||
| 				if res.Error != nil { | ||||
| 					logger.Error("failed to save prompt history message: ", res.Error) | ||||
| 				} | ||||
|  | ||||
| 				// 计算本次对话消耗的总 token 数量 | ||||
| 				var totalTokens = 0 | ||||
| 				if functionCall { // prompt + 函数名 + 参数 token | ||||
| 					tokens, _ := utils.CalcTokens(functionName, req.Model) | ||||
| 					totalTokens += tokens | ||||
| 					tokens, _ = utils.CalcTokens(utils.InterfaceToString(arguments), req.Model) | ||||
| 					totalTokens += tokens | ||||
| 				} else { | ||||
| 					totalTokens, _ = utils.CalcTokens(message.Content, req.Model) | ||||
| 				} | ||||
| 				totalTokens += getTotalTokens(req) | ||||
|  | ||||
| 				historyReplyMsg := model.HistoryMessage{ | ||||
| 					UserId:     userVo.Id, | ||||
| 					ChatId:     session.ChatId, | ||||
| 					RoleId:     role.Id, | ||||
| 					Type:       types.ReplyMsg, | ||||
| 					Icon:       role.Icon, | ||||
| 					Content:    message.Content, | ||||
| 					Tokens:     totalTokens, | ||||
| 					UseContext: useContext, | ||||
| 				} | ||||
| 				historyReplyMsg.CreatedAt = replyCreatedAt | ||||
| 				historyReplyMsg.UpdatedAt = replyCreatedAt | ||||
| 				res = h.db.Create(&historyReplyMsg) | ||||
| 				if res.Error != nil { | ||||
| 					logger.Error("failed to save reply history message: ", res.Error) | ||||
| 				} | ||||
|  | ||||
| 				// 更新用户信息 | ||||
| 				h.db.Model(&model.User{}).Where("id = ?", userVo.Id). | ||||
| 					UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", totalTokens)) | ||||
| 			} | ||||
|  | ||||
| 			// 保存当前会话 | ||||
| 			var chatItem model.ChatItem | ||||
| 			res := h.db.Where("chat_id = ?", session.ChatId).First(&chatItem) | ||||
| 			if res.Error != nil { | ||||
| 				chatItem.ChatId = session.ChatId | ||||
| 				chatItem.UserId = session.UserId | ||||
| 				chatItem.RoleId = role.Id | ||||
| 				chatItem.ModelId = session.Model.Id | ||||
| 				if utf8.RuneCountInString(prompt) > 30 { | ||||
| 					chatItem.Title = string([]rune(prompt)[:30]) + "..." | ||||
| 				} else { | ||||
| 					chatItem.Title = prompt | ||||
| 				} | ||||
| 				h.db.Create(&chatItem) | ||||
| 			} | ||||
| 		} | ||||
| 	} else { | ||||
| 		body, err := io.ReadAll(response.Body) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("error with reading response: %v", err) | ||||
| 		} | ||||
| 		var res types.ApiError | ||||
| 		err = json.Unmarshal(body, &res) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("error with decode response: %v", err) | ||||
| 		} | ||||
|  | ||||
| 		if strings.Contains(res.Error.Message, "maximum context length") { | ||||
| 			logger.Error(res.Error.Message) | ||||
| 			utils.ReplyMessage(ws, "当前会话上下文长度超出限制,已为您清空会话上下文!") | ||||
| 			h.App.ChatContexts.Delete(session.ChatId) | ||||
| 			return h.sendMessage(ctx, session, role, prompt, ws) | ||||
| 		} else { | ||||
| 			utils.ReplyMessage(ws, "请求 Azure API 失败:"+res.Error.Message) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
| @@ -2,8 +2,10 @@ package handler | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core" | ||||
| 	"chatplus/core/types" | ||||
| 	logger2 "chatplus/logger" | ||||
| 	"strconv" | ||||
| 	"chatplus/utils" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/gin-gonic/gin" | ||||
| @@ -20,47 +22,30 @@ func (h *BaseHandler) GetTrim(c *gin.Context, key string) string { | ||||
| } | ||||
|  | ||||
| func (h *BaseHandler) PostInt(c *gin.Context, key string, defaultValue int) int { | ||||
| 	return intValue(c.PostForm(key), defaultValue) | ||||
| 	return utils.IntValue(c.PostForm(key), defaultValue) | ||||
| } | ||||
|  | ||||
| func (h *BaseHandler) GetInt(c *gin.Context, key string, defaultValue int) int { | ||||
| 	return intValue(c.Query(key), defaultValue) | ||||
| } | ||||
|  | ||||
| func intValue(str string, defaultValue int) int { | ||||
| 	value, err := strconv.Atoi(str) | ||||
| 	if err != nil { | ||||
| 		return defaultValue | ||||
| 	} | ||||
| 	return value | ||||
| 	return utils.IntValue(c.Query(key), defaultValue) | ||||
| } | ||||
|  | ||||
| func (h *BaseHandler) GetFloat(c *gin.Context, key string) float64 { | ||||
| 	return floatValue(c.Query(key)) | ||||
| 	return utils.FloatValue(c.Query(key)) | ||||
| } | ||||
| func (h *BaseHandler) PostFloat(c *gin.Context, key string) float64 { | ||||
| 	return floatValue(c.PostForm(key)) | ||||
| } | ||||
|  | ||||
| func floatValue(str string) float64 { | ||||
| 	value, err := strconv.ParseFloat(str, 64) | ||||
| 	if err != nil { | ||||
| 		return 0 | ||||
| 	} | ||||
| 	return value | ||||
| 	return utils.FloatValue(c.PostForm(key)) | ||||
| } | ||||
|  | ||||
| func (h *BaseHandler) GetBool(c *gin.Context, key string) bool { | ||||
| 	return boolValue(c.Query(key)) | ||||
| 	return utils.BoolValue(c.Query(key)) | ||||
| } | ||||
| func (h *BaseHandler) PostBool(c *gin.Context, key string) bool { | ||||
| 	return boolValue(c.PostForm(key)) | ||||
| 	return utils.BoolValue(c.PostForm(key)) | ||||
| } | ||||
|  | ||||
| func boolValue(str string) bool { | ||||
| 	value, err := strconv.ParseBool(str) | ||||
| 	if err != nil { | ||||
| 		return false | ||||
| func (h *BaseHandler) GetUserKey(c *gin.Context) string { | ||||
| 	userId, ok := c.Get(types.LoginUserID) | ||||
| 	if !ok { | ||||
| 		return "" | ||||
| 	} | ||||
| 	return value | ||||
| 	return fmt.Sprintf("users/%v", userId) | ||||
| } | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| package handler | ||||
|  | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"bytes" | ||||
| 	"chatplus/core" | ||||
| 	"chatplus/core/types" | ||||
| @@ -14,16 +13,14 @@ 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 助手开小差了,请稍后再试。" | ||||
| @@ -32,10 +29,11 @@ type ChatHandler struct { | ||||
| 	BaseHandler | ||||
| 	db      *gorm.DB | ||||
| 	leveldb *store.LevelDB | ||||
| 	redis   *redis.Client | ||||
| } | ||||
|  | ||||
| func NewChatHandler(app *core.AppServer, db *gorm.DB, levelDB *store.LevelDB) *ChatHandler { | ||||
| 	handler := ChatHandler{db: db, leveldb: levelDB} | ||||
| 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 | ||||
| } | ||||
| @@ -53,7 +51,17 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) { | ||||
| 	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 == nil { | ||||
| @@ -66,7 +74,7 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) { | ||||
| 		session = &types.ChatSession{ | ||||
| 			SessionId: sessionId, | ||||
| 			ClientIP:  c.ClientIP(), | ||||
| 			Username:  user.Username, | ||||
| 			Username:  user.Mobile, | ||||
| 			UserId:    user.Id, | ||||
| 		} | ||||
| 		h.App.ChatSession.Put(sessionId, session) | ||||
| @@ -74,16 +82,18 @@ 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 | ||||
| 	logger.Infof("New websocket connected, IP: %s, Username: %s", c.Request.RemoteAddr, session.Username) | ||||
| 	client := types.NewWsClient(ws) | ||||
| 	session.Model = types.ChatModel{ | ||||
| 		Id:       chatModel.Id, | ||||
| 		Value:    chatModel.Value, | ||||
| 		Platform: types.Platform(chatModel.Platform)} | ||||
| 	logger.Infof("New websocket connected, IP: %s, Username: %s", c.ClientIP(), session.Username) | ||||
| 	var chatRole model.ChatRole | ||||
| 	res = h.db.First(&chatRole, roleId) | ||||
| 	if res.Error != nil || !chatRole.Enable { | ||||
| @@ -124,6 +134,7 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) { | ||||
| 			err = h.sendMessage(ctx, session, chatRole, message, client) | ||||
| 			if err != nil { | ||||
| 				logger.Error(err) | ||||
| 				utils.ReplyChunkMessage(client, types.WsMessage{Type: types.WsEnd}) | ||||
| 			} else { | ||||
| 				utils.ReplyChunkMessage(client, types.WsMessage{Type: types.WsEnd}) | ||||
| 				logger.Info("回答完毕: " + string(message)) | ||||
| @@ -133,9 +144,12 @@ 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.WsClient) error { | ||||
| 	promptCreatedAt := time.Now() // 记录提问时间 | ||||
| 	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) | ||||
| @@ -156,7 +170,7 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	if userVo.Calls <= 0 && userVo.ChatConfig.ApiKey == "" { | ||||
| 	if userVo.Calls <= 0 && userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" { | ||||
| 		utils.ReplyMessage(ws, "您的对话次数已经用尽,请联系管理员或者点击左下角菜单加入众筹获得100次对话!") | ||||
| 		utils.ReplyMessage(ws, "") | ||||
| 		return nil | ||||
| @@ -168,16 +182,34 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio | ||||
| 		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 { | ||||
| @@ -208,7 +240,7 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio | ||||
| 				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 | ||||
| @@ -232,341 +264,47 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio | ||||
| 		"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") { | ||||
| 			utils.ReplyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY🔑,您可以导入自己的 API KEY🔑 继续使用!🙏🙏🙏") | ||||
| 			return nil | ||||
| 		} else { | ||||
| 			logger.Error(err) | ||||
| 		} | ||||
|  | ||||
| 		utils.ReplyMessage(ws, ErrorMsg) | ||||
| 		utils.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) | ||||
| 					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) { | ||||
| 					functionCall = true | ||||
| 					functionName = fun.Name | ||||
| 					f := h.App.Functions[functionName] | ||||
| 					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 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(&user).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.ApiKey == "" { // 如果用户使用的是自己绑定的 API KEY 则不扣减对话次数 | ||||
| 					h.db.Model(&user).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 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) | ||||
| 					} | ||||
| 					//utils.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") { | ||||
| 			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) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	utils.ReplyChunkMessage(ws, types.WsMessage{ | ||||
| 		Type:    types.WsMiddle, | ||||
| 		Content: fmt.Sprintf("Not supported platform: %s", session.Model.Platform), | ||||
| 	}) | ||||
| 	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) | ||||
| } | ||||
|  | ||||
| // 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 | ||||
| 	} | ||||
|  | ||||
| 	// 如果没有传入 text 字段,则说明是获取当前 reply 总的 token 消耗(带上下文) | ||||
| 	if data.Text == "" { | ||||
| 		var item model.HistoryMessage | ||||
| 		userId, _ := c.Get(types.LoginUserID) | ||||
| 		res := h.db.Where("user_id = ?", userId).Last(&item) | ||||
| 		if res.Error != nil { | ||||
| 			resp.ERROR(c, res.Error.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 		resp.SUCCESS(c, item.Tokens) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	tokens, err := utils.CalcTokens(data.Text, data.Model) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| @@ -604,3 +342,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) | ||||
| } | ||||
|   | ||||
| @@ -35,14 +35,9 @@ func (h *ChatHandler) Update(c *gin.Context) { | ||||
| // 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 | ||||
|   | ||||
							
								
								
									
										44
									
								
								api/handler/chat_model_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,44 @@ | ||||
| package handler | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/store/vo" | ||||
| 	"chatplus/utils" | ||||
| 	"chatplus/utils/resp" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| type ChatModelHandler struct { | ||||
| 	BaseHandler | ||||
| 	db *gorm.DB | ||||
| } | ||||
|  | ||||
| func NewChatModelHandler(app *core.AppServer, db *gorm.DB) *ChatModelHandler { | ||||
| 	h := ChatModelHandler{db: db} | ||||
| 	h.App = app | ||||
| 	return &h | ||||
| } | ||||
|  | ||||
| // List 模型列表 | ||||
| func (h *ChatModelHandler) List(c *gin.Context) { | ||||
| 	var items []model.ChatModel | ||||
| 	var cms = make([]vo.ChatModel, 0) | ||||
| 	res := h.db.Where("enabled = ?", true).Order("sort_num ASC").Find(&items) | ||||
| 	if res.Error == nil { | ||||
| 		for _, item := range items { | ||||
| 			var cm vo.ChatModel | ||||
| 			err := utils.CopyObject(item, &cm) | ||||
| 			if err == nil { | ||||
| 				cm.Id = item.Id | ||||
| 				cm.CreatedAt = item.CreatedAt.Unix() | ||||
| 				cm.UpdatedAt = item.UpdatedAt.Unix() | ||||
| 				cms = append(cms, cm) | ||||
| 			} else { | ||||
| 				logger.Error(err) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	resp.SUCCESS(c, cms) | ||||
| } | ||||
| @@ -25,7 +25,7 @@ func NewChatRoleHandler(app *core.AppServer, db *gorm.DB) *ChatRoleHandler { | ||||
| // List get user list | ||||
| func (h *ChatRoleHandler) List(c *gin.Context) { | ||||
| 	var roles []model.ChatRole | ||||
| 	res := h.db.Where("enable", true).Order("sort ASC").Find(&roles) | ||||
| 	res := h.db.Where("enable", true).Order("sort_num ASC").Find(&roles) | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, "No roles found,"+res.Error.Error()) | ||||
| 		return | ||||
|   | ||||
							
								
								
									
										235
									
								
								api/handler/chatglm_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,235 @@ | ||||
| package handler | ||||
|  | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/store/vo" | ||||
| 	"chatplus/utils" | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"github.com/golang-jwt/jwt/v5" | ||||
| 	"gorm.io/gorm" | ||||
| 	"io" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 	"unicode/utf8" | ||||
| ) | ||||
|  | ||||
| // 将消息发送给 ChatGLM API 并获取结果,通过 WebSocket 推送到客户端 | ||||
| func (h *ChatHandler) sendChatGLMMessage( | ||||
| 	chatCtx []interface{}, | ||||
| 	req types.ApiRequest, | ||||
| 	userVo vo.User, | ||||
| 	ctx context.Context, | ||||
| 	session *types.ChatSession, | ||||
| 	role model.ChatRole, | ||||
| 	prompt string, | ||||
| 	ws *types.WsClient) error { | ||||
| 	promptCreatedAt := time.Now() // 记录提问时间 | ||||
| 	start := time.Now() | ||||
| 	var apiKey = userVo.ChatConfig.ApiKeys[session.Model.Platform] | ||||
| 	response, err := h.doRequest(ctx, req, session.Model.Platform, &apiKey) | ||||
| 	logger.Info("HTTP请求完成,耗时:", time.Now().Sub(start)) | ||||
| 	if err != nil { | ||||
| 		if strings.Contains(err.Error(), "context canceled") { | ||||
| 			logger.Info("用户取消了请求:", prompt) | ||||
| 			return nil | ||||
| 		} else if strings.Contains(err.Error(), "no available key") { | ||||
| 			utils.ReplyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!") | ||||
| 			return nil | ||||
| 		} else { | ||||
| 			logger.Error(err) | ||||
| 		} | ||||
|  | ||||
| 		utils.ReplyMessage(ws, ErrorMsg) | ||||
| 		utils.ReplyMessage(ws, "") | ||||
| 		return err | ||||
| 	} else { | ||||
| 		defer response.Body.Close() | ||||
| 	} | ||||
|  | ||||
| 	contentType := response.Header.Get("Content-Type") | ||||
| 	if strings.Contains(contentType, "text/event-stream") { | ||||
| 		replyCreatedAt := time.Now() // 记录回复时间 | ||||
| 		// 循环读取 Chunk 消息 | ||||
| 		var message = types.Message{} | ||||
| 		var contents = make([]string, 0) | ||||
| 		var event, content string | ||||
| 		scanner := bufio.NewScanner(response.Body) | ||||
| 		for scanner.Scan() { | ||||
| 			line := scanner.Text() | ||||
| 			if len(line) < 5 || strings.HasPrefix(line, "id:") { | ||||
| 				continue | ||||
| 			} | ||||
| 			if strings.HasPrefix(line, "event:") { | ||||
| 				event = line[6:] | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			if strings.HasPrefix(line, "data:") { | ||||
| 				content = line[5:] | ||||
| 			} | ||||
| 			switch event { | ||||
| 			case "add": | ||||
| 				if len(contents) == 0 { | ||||
| 					utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart}) | ||||
| 				} | ||||
| 				utils.ReplyChunkMessage(ws, types.WsMessage{ | ||||
| 					Type:    types.WsMiddle, | ||||
| 					Content: utils.InterfaceToString(content), | ||||
| 				}) | ||||
| 				contents = append(contents, content) | ||||
| 			case "finish": | ||||
| 				break | ||||
| 			case "error": | ||||
| 				utils.ReplyMessage(ws, fmt.Sprintf("**调用 ChatGLM API 出错:%s**", content)) | ||||
| 				break | ||||
| 			case "interrupted": | ||||
| 				utils.ReplyMessage(ws, "**调用 ChatGLM API 出错,当前输出被中断!**") | ||||
| 			} | ||||
|  | ||||
| 		} // end for | ||||
|  | ||||
| 		if err := scanner.Err(); err != nil { | ||||
| 			if strings.Contains(err.Error(), "context canceled") { | ||||
| 				logger.Info("用户取消了请求:", prompt) | ||||
| 			} else { | ||||
| 				logger.Error("信息读取出错:", err) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// 消息发送成功 | ||||
| 		if len(contents) > 0 { | ||||
| 			// 更新用户的对话次数 | ||||
| 			if userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" { | ||||
| 				h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("calls", gorm.Expr("calls - ?", 1)) | ||||
| 			} | ||||
|  | ||||
| 			if message.Role == "" { | ||||
| 				message.Role = "assistant" | ||||
| 			} | ||||
| 			message.Content = strings.Join(contents, "") | ||||
| 			useMsg := types.Message{Role: "user", Content: prompt} | ||||
|  | ||||
| 			// 更新上下文消息,如果是调用函数则不需要更新上下文 | ||||
| 			if h.App.ChatConfig.EnableContext { | ||||
| 				chatCtx = append(chatCtx, useMsg)  // 提问消息 | ||||
| 				chatCtx = append(chatCtx, message) // 回复消息 | ||||
| 				h.App.ChatContexts.Put(session.ChatId, chatCtx) | ||||
| 			} | ||||
|  | ||||
| 			// 追加聊天记录 | ||||
| 			if h.App.ChatConfig.EnableHistory { | ||||
| 				// for prompt | ||||
| 				promptToken, err := utils.CalcTokens(prompt, req.Model) | ||||
| 				if err != nil { | ||||
| 					logger.Error(err) | ||||
| 				} | ||||
| 				historyUserMsg := model.HistoryMessage{ | ||||
| 					UserId:     userVo.Id, | ||||
| 					ChatId:     session.ChatId, | ||||
| 					RoleId:     role.Id, | ||||
| 					Type:       types.PromptMsg, | ||||
| 					Icon:       userVo.Avatar, | ||||
| 					Content:    prompt, | ||||
| 					Tokens:     promptToken, | ||||
| 					UseContext: true, | ||||
| 				} | ||||
| 				historyUserMsg.CreatedAt = promptCreatedAt | ||||
| 				historyUserMsg.UpdatedAt = promptCreatedAt | ||||
| 				res := h.db.Save(&historyUserMsg) | ||||
| 				if res.Error != nil { | ||||
| 					logger.Error("failed to save prompt history message: ", res.Error) | ||||
| 				} | ||||
|  | ||||
| 				// for reply | ||||
| 				// 计算本次对话消耗的总 token 数量 | ||||
| 				replyToken, _ := utils.CalcTokens(message.Content, req.Model) | ||||
| 				totalTokens := replyToken + getTotalTokens(req) | ||||
| 				historyReplyMsg := model.HistoryMessage{ | ||||
| 					UserId:     userVo.Id, | ||||
| 					ChatId:     session.ChatId, | ||||
| 					RoleId:     role.Id, | ||||
| 					Type:       types.ReplyMsg, | ||||
| 					Icon:       role.Icon, | ||||
| 					Content:    message.Content, | ||||
| 					Tokens:     totalTokens, | ||||
| 					UseContext: true, | ||||
| 				} | ||||
| 				historyReplyMsg.CreatedAt = replyCreatedAt | ||||
| 				historyReplyMsg.UpdatedAt = replyCreatedAt | ||||
| 				res = h.db.Create(&historyReplyMsg) | ||||
| 				if res.Error != nil { | ||||
| 					logger.Error("failed to save reply history message: ", res.Error) | ||||
| 				} | ||||
| 				// 更新用户信息 | ||||
| 				h.db.Model(&model.User{}).Where("id = ?", userVo.Id). | ||||
| 					UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", totalTokens)) | ||||
| 			} | ||||
|  | ||||
| 			// 保存当前会话 | ||||
| 			var chatItem model.ChatItem | ||||
| 			res := h.db.Where("chat_id = ?", session.ChatId).First(&chatItem) | ||||
| 			if res.Error != nil { | ||||
| 				chatItem.ChatId = session.ChatId | ||||
| 				chatItem.UserId = session.UserId | ||||
| 				chatItem.RoleId = role.Id | ||||
| 				chatItem.ModelId = session.Model.Id | ||||
| 				if utf8.RuneCountInString(prompt) > 30 { | ||||
| 					chatItem.Title = string([]rune(prompt)[:30]) + "..." | ||||
| 				} else { | ||||
| 					chatItem.Title = prompt | ||||
| 				} | ||||
| 				h.db.Create(&chatItem) | ||||
| 			} | ||||
| 		} | ||||
| 	} else { | ||||
| 		body, err := io.ReadAll(response.Body) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("error with reading response: %v", err) | ||||
| 		} | ||||
|  | ||||
| 		var res struct { | ||||
| 			Code    int    `json:"code"` | ||||
| 			Success bool   `json:"success"` | ||||
| 			Msg     string `json:"msg"` | ||||
| 		} | ||||
| 		err = json.Unmarshal(body, &res) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("error with decode response: %v", err) | ||||
| 		} | ||||
| 		if !res.Success { | ||||
| 			utils.ReplyMessage(ws, "请求 ChatGLM 失败:"+res.Msg) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (h *ChatHandler) getChatGLMToken(apiKey string) (string, error) { | ||||
| 	ctx := context.Background() | ||||
| 	tokenString, err := h.redis.Get(ctx, apiKey).Result() | ||||
| 	if err == nil { | ||||
| 		return tokenString, nil | ||||
| 	} | ||||
|  | ||||
| 	expr := time.Hour * 2 | ||||
| 	key := strings.Split(apiKey, ".") | ||||
| 	if len(key) != 2 { | ||||
| 		return "", fmt.Errorf("invalid api key: %s", apiKey) | ||||
| 	} | ||||
| 	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ | ||||
| 		"api_key":   key[0], | ||||
| 		"timestamp": time.Now().Unix(), | ||||
| 		"exp":       time.Now().Add(expr).Add(time.Second * 10).Unix(), | ||||
| 	}) | ||||
| 	token.Header["alg"] = "HS256" | ||||
| 	token.Header["sign_type"] = "SIGN" | ||||
| 	delete(token.Header, "typ") | ||||
| 	// Sign and get the complete encoded token as a string using the secret | ||||
| 	tokenString, err = token.SignedString([]byte(key[1])) | ||||
| 	h.redis.Set(ctx, apiKey, tokenString, expr) | ||||
| 	return tokenString, err | ||||
| } | ||||
| @@ -3,24 +3,27 @@ package handler | ||||
| import ( | ||||
| 	"chatplus/core" | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/service/function" | ||||
| 	"chatplus/service" | ||||
| 	"chatplus/service/oss" | ||||
| 	"chatplus/store" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/store/vo" | ||||
| 	"chatplus/utils" | ||||
| 	"chatplus/utils/resp" | ||||
| 	"encoding/base64" | ||||
| 	"fmt" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/go-redis/redis/v8" | ||||
| 	"github.com/gorilla/websocket" | ||||
| 	"gorm.io/gorm" | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| type TaskStatus string | ||||
|  | ||||
| const ( | ||||
| 	Start    = TaskStatus("Started") | ||||
| 	Running  = TaskStatus("Running") | ||||
| 	Stopped  = TaskStatus("Stopped") | ||||
| 	Finished = TaskStatus("Finished") | ||||
| ) | ||||
| @@ -37,203 +40,521 @@ type Image struct { | ||||
|  | ||||
| type MidJourneyHandler struct { | ||||
| 	BaseHandler | ||||
| 	leveldb         *store.LevelDB | ||||
| 	redis           *redis.Client | ||||
| 	db              *gorm.DB | ||||
| 	mjFunc          function.FuncMidJourney | ||||
| 	mjService       *service.MjService | ||||
| 	uploaderManager *oss.UploaderManager | ||||
| 	lock            sync.Mutex | ||||
| 	clients         *types.LMap[string, *types.WsClient] | ||||
| } | ||||
|  | ||||
| func NewMidJourneyHandler( | ||||
| 	app *core.AppServer, | ||||
| 	leveldb *store.LevelDB, | ||||
| 	client *redis.Client, | ||||
| 	db *gorm.DB, | ||||
| 	manager *oss.UploaderManager, | ||||
| 	functions map[string]function.Function) *MidJourneyHandler { | ||||
| 	mjService *service.MjService) *MidJourneyHandler { | ||||
| 	h := MidJourneyHandler{ | ||||
| 		leveldb:         leveldb, | ||||
| 		redis:           client, | ||||
| 		db:              db, | ||||
| 		uploaderManager: manager, | ||||
| 		mjFunc:          functions[types.FuncMidJourney].(function.FuncMidJourney)} | ||||
| 		lock:            sync.Mutex{}, | ||||
| 		mjService:       mjService, | ||||
| 		clients:         types.NewLMap[string, *types.WsClient](), | ||||
| 	} | ||||
| 	h.App = app | ||||
| 	return &h | ||||
| } | ||||
|  | ||||
| type notifyData struct { | ||||
| 	MessageId   string     `json:"message_id"` | ||||
| 	ReferenceId string     `json:"reference_id"` | ||||
| 	Image       Image      `json:"image"` | ||||
| 	Content     string     `json:"content"` | ||||
| 	Prompt      string     `json:"prompt"` | ||||
| 	Status      TaskStatus `json:"status"` | ||||
| 	Progress    int        `json:"progress"` | ||||
| } | ||||
|  | ||||
| // Client WebSocket 客户端,用于通知任务状态变更 | ||||
| func (h *MidJourneyHandler) Client(c *gin.Context) { | ||||
| 	ws, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}).Upgrade(c.Writer, c.Request, nil) | ||||
| 	if err != nil { | ||||
| 		logger.Error(err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	sessionId := c.Query("session_id") | ||||
| 	client := types.NewWsClient(ws) | ||||
| 	// 删除旧的连接 | ||||
| 	h.clients.Delete(sessionId) | ||||
| 	h.clients.Put(sessionId, client) | ||||
| 	logger.Infof("New websocket connected, IP: %s", c.ClientIP()) | ||||
| } | ||||
|  | ||||
| func (h *MidJourneyHandler) Notify(c *gin.Context) { | ||||
| 	token := c.GetHeader("Authorization") | ||||
| 	if token != h.App.Config.ExtConfig.Token { | ||||
| 		resp.NotAuth(c) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var data 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"` | ||||
| 	} | ||||
| 	var data notifyData | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil || data.Prompt == "" { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	logger.Debugf("收到 MidJourney 回调请求:%+v", data) | ||||
|  | ||||
| 	// the job is saved | ||||
| 	h.lock.Lock() | ||||
| 	defer h.lock.Unlock() | ||||
|  | ||||
| 	err, finished := h.notifyHandler(c, data) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// 解除任务锁定 | ||||
| 	if finished && (data.Status == Finished || data.Status == Stopped) { | ||||
| 		h.redis.Del(c, service.MjRunningJobKey) | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
|  | ||||
| } | ||||
|  | ||||
| func (h *MidJourneyHandler) notifyHandler(c *gin.Context, data notifyData) (error, bool) { | ||||
| 	taskString, err := h.redis.Get(c, service.MjRunningJobKey).Result() | ||||
| 	if err != nil { // 过期任务,丢弃 | ||||
| 		logger.Warn("任务已过期:", err) | ||||
| 		return nil, true | ||||
| 	} | ||||
|  | ||||
| 	var task service.MjTask | ||||
| 	err = utils.JsonDecode(taskString, &task) | ||||
| 	if err != nil { // 非标准任务,丢弃 | ||||
| 		logger.Warn("任务解析失败:", err) | ||||
| 		return nil, false | ||||
| 	} | ||||
|  | ||||
| 	var job model.MidJourneyJob | ||||
| 	res := h.db.Where("message_id = ?", data.MessageId).First(&job) | ||||
| 	if res.Error == nil { | ||||
| 		resp.SUCCESS(c) | ||||
| 		return | ||||
| 	if res.Error == nil && data.Status == Finished { | ||||
| 		logger.Warn("重复消息:", data.MessageId) | ||||
| 		return nil, false | ||||
| 	} | ||||
|  | ||||
| 	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.GetActiveService().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 task.Src == service.TaskSrcImg { // 绘画任务 | ||||
| 		var job model.MidJourneyJob | ||||
| 		res := h.db.Where("id = ?", task.Id).First(&job) | ||||
| 		if res.Error != nil { | ||||
| 			logger.Error("error with save chat history message: ", res.Error) | ||||
| 			logger.Warn("非法任务:", res.Error) | ||||
| 			return nil, false | ||||
| 		} | ||||
|  | ||||
| 		// save the job | ||||
| 		job.UserId = task.UserId | ||||
| 		job.ChatId = task.ChatId | ||||
| 		job.MessageId = data.MessageId | ||||
| 		job.ReferenceId = data.ReferenceId | ||||
| 		job.Content = data.Content | ||||
| 		job.Progress = data.Progress | ||||
| 		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 data.Progress == 100 { | ||||
| 			imgURL, err := h.uploaderManager.GetUploadHandler().PutImg(data.Image.URL) | ||||
| 			if err != nil { | ||||
| 				logger.Error("error with download img: ", err.Error()) | ||||
| 				return err, false | ||||
| 			} | ||||
| 			job.ImgURL = imgURL | ||||
| 		} else { | ||||
| 			// 临时图片直接保存,访问的时候使用代理进行转发 | ||||
| 			job.ImgURL = data.Image.URL | ||||
| 		} | ||||
| 		res = h.db.Updates(&job) | ||||
| 		if res.Error != nil { | ||||
| 			logger.Error("error with save MidJourney Job: ", res.Error) | ||||
| 			logger.Error("error with update job: ", res.Error) | ||||
| 			return res.Error, false | ||||
| 		} | ||||
|  | ||||
| 		var jobVo vo.MidJourneyJob | ||||
| 		err := utils.CopyObject(job, &jobVo) | ||||
| 		if err == nil { | ||||
| 			if data.Progress < 100 { | ||||
| 				image, err := utils.DownloadImage(jobVo.ImgURL, h.App.Config.ProxyURL) | ||||
| 				if err == nil { | ||||
| 					jobVo.ImgURL = "data:image/png;base64," + base64.StdEncoding.EncodeToString(image) | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			// 推送任务到前端 | ||||
| 			client := h.clients.Get(task.SessionId) | ||||
| 			if client != nil { | ||||
| 				utils.ReplyChunkMessage(client, jobVo) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 	} else if task.Src == service.TaskSrcChat { // 聊天任务 | ||||
| 		wsClient := h.App.MjTaskClients.Get(task.SessionId) | ||||
| 		if data.Status == Finished { | ||||
| 			if wsClient != nil && data.ReferenceId != "" { | ||||
| 				content := fmt.Sprintf("**%s** 任务执行成功,正在从 MidJourney 服务器下载图片,请稍后...", data.Prompt) | ||||
| 				utils.ReplyMessage(wsClient, content) | ||||
| 			} | ||||
| 			// download image | ||||
| 			imgURL, err := h.uploaderManager.GetUploadHandler().PutImg(data.Image.URL) | ||||
| 			if err != nil { | ||||
| 				logger.Error("error with download image: ", err) | ||||
| 				if wsClient != nil && data.ReferenceId != "" { | ||||
| 					content := fmt.Sprintf("**%s** 图片下载失败:%s", data.Prompt, err.Error()) | ||||
| 					utils.ReplyMessage(wsClient, content) | ||||
| 				} | ||||
| 				return err, false | ||||
| 			} | ||||
|  | ||||
| 			tx := h.db.Begin() | ||||
| 			data.Image.URL = imgURL | ||||
| 			message := model.HistoryMessage{ | ||||
| 				UserId:     uint(task.UserId), | ||||
| 				ChatId:     task.ChatId, | ||||
| 				RoleId:     uint(task.RoleId), | ||||
| 				Type:       types.MjMsg, | ||||
| 				Icon:       task.Icon, | ||||
| 				Content:    utils.JsonEncode(data), | ||||
| 				Tokens:     0, | ||||
| 				UseContext: false, | ||||
| 			} | ||||
| 			res = tx.Create(&message) | ||||
| 			if res.Error != nil { | ||||
| 				return res.Error, false | ||||
| 			} | ||||
|  | ||||
| 			// save the job | ||||
| 			job.UserId = task.UserId | ||||
| 			job.Type = task.Type.String() | ||||
| 			job.MessageId = data.MessageId | ||||
| 			job.ReferenceId = data.ReferenceId | ||||
| 			job.Prompt = data.Prompt | ||||
| 			job.ImgURL = imgURL | ||||
| 			job.Progress = data.Progress | ||||
| 			job.Hash = data.Image.Hash | ||||
| 			job.CreatedAt = time.Now() | ||||
| 			res = tx.Create(&job) | ||||
| 			if res.Error != nil { | ||||
| 				tx.Rollback() | ||||
| 				return res.Error, false | ||||
| 			} | ||||
| 			tx.Commit() | ||||
| 		} | ||||
|  | ||||
| 		if wsClient == nil { // 客户端断线,则丢弃 | ||||
| 			logger.Errorf("Client is offline: %+v", data) | ||||
| 			return nil, true | ||||
| 		} | ||||
|  | ||||
| 		if data.Status == Finished { | ||||
| 			utils.ReplyChunkMessage(wsClient, types.WsMessage{Type: types.WsMjImg, Content: data}) | ||||
| 			utils.ReplyChunkMessage(wsClient, types.WsMessage{Type: types.WsEnd}) | ||||
| 			// 本次绘画完毕,移除客户端 | ||||
| 			h.App.MjTaskClients.Delete(task.SessionId) | ||||
| 		} else { | ||||
| 			// 使用代理临时转发图片 | ||||
| 			if data.Image.URL != "" { | ||||
| 				image, err := utils.DownloadImage(data.Image.URL, h.App.Config.ProxyURL) | ||||
| 				if err == nil { | ||||
| 					data.Image.URL = "data:image/png;base64," + base64.StdEncoding.EncodeToString(image) | ||||
| 				} | ||||
| 			} | ||||
| 			utils.ReplyChunkMessage(wsClient, types.WsMessage{Type: types.WsMjImg, Content: data}) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// 推送消息到客户端 | ||||
| 	wsClient := h.App.MjTaskClients.Get(data.Key) | ||||
| 	if wsClient == nil { // 客户端断线,则丢弃 | ||||
| 		logger.Errorf("Client is offline: %+v", data) | ||||
| 		resp.SUCCESS(c, "Client is offline") | ||||
| 	// 更新用户剩余绘图次数 | ||||
| 	// TODO: 放大图片是否需要消耗绘图次数? | ||||
| 	if data.Status == Finished { | ||||
| 		h.db.Model(&model.User{}).Where("id = ?", task.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1)) | ||||
| 	} | ||||
|  | ||||
| 	return nil, true | ||||
| } | ||||
|  | ||||
| func (h *MidJourneyHandler) checkLimits(c *gin.Context) bool { | ||||
| 	user, err := utils.GetLoginUser(c, h.db) | ||||
| 	if err != nil { | ||||
| 		resp.NotAuth(c) | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	if user.ImgCalls <= 0 { | ||||
| 		resp.ERROR(c, "您的绘图次数不足,请联系管理员充值!") | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	return true | ||||
|  | ||||
| } | ||||
|  | ||||
| // Image 创建一个绘画任务 | ||||
| func (h *MidJourneyHandler) Image(c *gin.Context) { | ||||
| 	var data struct { | ||||
| 		SessionId string  `json:"session_id"` | ||||
| 		Prompt    string  `json:"prompt"` | ||||
| 		Rate      string  `json:"rate"` | ||||
| 		Model     string  `json:"model"` | ||||
| 		Chaos     int     `json:"chaos"` | ||||
| 		Raw       bool    `json:"raw"` | ||||
| 		Seed      int64   `json:"seed"` | ||||
| 		Stylize   int     `json:"stylize"` | ||||
| 		Img       string  `json:"img"` | ||||
| 		Weight    float32 `json:"weight"` | ||||
| 	} | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 	if !h.checkLimits(c) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	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}) | ||||
| 	var prompt = data.Prompt | ||||
| 	if data.Rate != "" && !strings.Contains(prompt, "--ar") { | ||||
| 		prompt += " --ar " + data.Rate | ||||
| 	} | ||||
| 	resp.SUCCESS(c, "SUCCESS") | ||||
| 	if data.Seed > 0 && !strings.Contains(prompt, "--seed") { | ||||
| 		prompt += fmt.Sprintf(" --seed %d", data.Seed) | ||||
| 	} | ||||
| 	if data.Stylize > 0 && !strings.Contains(prompt, "--s") && !strings.Contains(prompt, "--stylize") { | ||||
| 		prompt += fmt.Sprintf(" --s %d", data.Stylize) | ||||
| 	} | ||||
| 	if data.Chaos > 0 && !strings.Contains(prompt, "--c") && !strings.Contains(prompt, "--chaos") { | ||||
| 		prompt += fmt.Sprintf(" --c %d", data.Chaos) | ||||
| 	} | ||||
| 	if data.Img != "" { | ||||
| 		prompt = fmt.Sprintf("%s %s", data.Img, prompt) | ||||
| 		if data.Weight > 0 { | ||||
| 			prompt += fmt.Sprintf(" --iw %f", data.Weight) | ||||
| 		} | ||||
| 	} | ||||
| 	if data.Raw { | ||||
| 		prompt += " --style raw" | ||||
| 	} | ||||
| 	if data.Model != "" && !strings.Contains(prompt, "--v") && !strings.Contains(prompt, "--niji") { | ||||
| 		prompt += data.Model | ||||
| 	} | ||||
|  | ||||
| 	idValue, _ := c.Get(types.LoginUserID) | ||||
| 	userId := utils.IntValue(utils.InterfaceToString(idValue), 0) | ||||
| 	job := model.MidJourneyJob{ | ||||
| 		Type:      service.Image.String(), | ||||
| 		UserId:    userId, | ||||
| 		Progress:  0, | ||||
| 		Prompt:    prompt, | ||||
| 		CreatedAt: time.Now(), | ||||
| 	} | ||||
| 	if res := h.db.Create(&job); res.Error != nil { | ||||
| 		resp.ERROR(c, "添加任务失败:"+res.Error.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	h.mjService.PushTask(service.MjTask{ | ||||
| 		Id:        int(job.Id), | ||||
| 		SessionId: data.SessionId, | ||||
| 		Src:       service.TaskSrcImg, | ||||
| 		Type:      service.Image, | ||||
| 		Prompt:    prompt, | ||||
| 		UserId:    userId, | ||||
| 	}) | ||||
|  | ||||
| 	var jobVo vo.MidJourneyJob | ||||
| 	err := utils.CopyObject(job, &jobVo) | ||||
| 	if err == nil { | ||||
| 		// 推送任务到前端 | ||||
| 		client := h.clients.Get(data.SessionId) | ||||
| 		if client != nil { | ||||
| 			utils.ReplyChunkMessage(client, jobVo) | ||||
| 		} | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| type reqVo struct { | ||||
| 	Src         string `json:"src"` | ||||
| 	Index       int32  `json:"index"` | ||||
| 	MessageId   string `json:"message_id"` | ||||
| 	MessageHash string `json:"message_hash"` | ||||
| 	SessionId   string `json:"session_id"` | ||||
| 	Key         string `json:"key"` | ||||
| 	Prompt      string `json:"prompt"` | ||||
| 	ChatId      string `json:"chat_id"` | ||||
| 	RoleId      int    `json:"role_id"` | ||||
| 	Icon        string `json:"icon"` | ||||
| } | ||||
|  | ||||
| // Upscale send upscale command to MidJourney Bot | ||||
| func (h *MidJourneyHandler) Upscale(c *gin.Context) { | ||||
| 	var data reqVo | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil || | ||||
| 		data.SessionId == "" || | ||||
| 		data.Key == "" { | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil || data.SessionId == "" { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 	wsClient := h.App.ChatClients.Get(data.SessionId) | ||||
| 	if wsClient == nil { | ||||
| 		resp.ERROR(c, "No Websocket client online") | ||||
|  | ||||
| 	if !h.checkLimits(c) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err := h.mjFunc.Upscale(function.MjUpscaleReq{ | ||||
| 	idValue, _ := c.Get(types.LoginUserID) | ||||
| 	jobId := 0 | ||||
| 	userId := utils.IntValue(utils.InterfaceToString(idValue), 0) | ||||
| 	src := service.TaskSrc(data.Src) | ||||
| 	if src == service.TaskSrcImg { | ||||
| 		job := model.MidJourneyJob{ | ||||
| 			Type:      service.Upscale.String(), | ||||
| 			UserId:    userId, | ||||
| 			Hash:      data.MessageHash, | ||||
| 			Progress:  0, | ||||
| 			Prompt:    data.Prompt, | ||||
| 			CreatedAt: time.Now(), | ||||
| 		} | ||||
| 		if res := h.db.Create(&job); res.Error == nil { | ||||
| 			jobId = int(job.Id) | ||||
| 		} else { | ||||
| 			resp.ERROR(c, "添加任务失败:"+res.Error.Error()) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		var jobVo vo.MidJourneyJob | ||||
| 		err := utils.CopyObject(job, &jobVo) | ||||
| 		if err == nil { | ||||
| 			// 推送任务到前端 | ||||
| 			client := h.clients.Get(data.SessionId) | ||||
| 			if client != nil { | ||||
| 				utils.ReplyChunkMessage(client, jobVo) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	h.mjService.PushTask(service.MjTask{ | ||||
| 		Id:          jobId, | ||||
| 		SessionId:   data.SessionId, | ||||
| 		Src:         src, | ||||
| 		Type:        service.Upscale, | ||||
| 		Prompt:      data.Prompt, | ||||
| 		UserId:      userId, | ||||
| 		RoleId:      data.RoleId, | ||||
| 		Icon:        data.Icon, | ||||
| 		ChatId:      data.ChatId, | ||||
| 		Index:       data.Index, | ||||
| 		MessageId:   data.MessageId, | ||||
| 		MessageHash: data.MessageHash, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
|  | ||||
| 	wsClient := h.App.ChatClients.Get(data.SessionId) | ||||
| 	if wsClient != nil { | ||||
| 		content := fmt.Sprintf("**%s** 已推送 upscale 任务到 MidJourney 机器人,请耐心等待任务执行...", data.Prompt) | ||||
| 		utils.ReplyMessage(wsClient, content) | ||||
| 		if h.App.MjTaskClients.Get(data.SessionId) == nil { | ||||
| 			h.App.MjTaskClients.Put(data.SessionId, wsClient) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	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) | ||||
| } | ||||
|  | ||||
| // Variation send variation command to MidJourney Bot | ||||
| func (h *MidJourneyHandler) Variation(c *gin.Context) { | ||||
| 	var data reqVo | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil || | ||||
| 		data.SessionId == "" || | ||||
| 		data.Key == "" { | ||||
| 	if err := c.ShouldBindJSON(&data); err != nil || data.SessionId == "" { | ||||
| 		resp.ERROR(c, types.InvalidArgs) | ||||
| 		return | ||||
| 	} | ||||
| 	wsClient := h.App.ChatClients.Get(data.SessionId) | ||||
| 	if wsClient == nil { | ||||
| 		resp.ERROR(c, "No Websocket client online") | ||||
|  | ||||
| 	if !h.checkLimits(c) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err := h.mjFunc.Variation(function.MjVariationReq{ | ||||
| 	idValue, _ := c.Get(types.LoginUserID) | ||||
| 	jobId := 0 | ||||
| 	userId := utils.IntValue(utils.InterfaceToString(idValue), 0) | ||||
| 	src := service.TaskSrc(data.Src) | ||||
| 	if src == service.TaskSrcImg { | ||||
| 		job := model.MidJourneyJob{ | ||||
| 			Type:      service.Variation.String(), | ||||
| 			UserId:    userId, | ||||
| 			ImgURL:    "", | ||||
| 			Hash:      data.MessageHash, | ||||
| 			Progress:  0, | ||||
| 			Prompt:    data.Prompt, | ||||
| 			CreatedAt: time.Now(), | ||||
| 		} | ||||
| 		if res := h.db.Create(&job); res.Error == nil { | ||||
| 			jobId = int(job.Id) | ||||
| 		} else { | ||||
| 			resp.ERROR(c, "添加任务失败:"+res.Error.Error()) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		var jobVo vo.MidJourneyJob | ||||
| 		err := utils.CopyObject(job, &jobVo) | ||||
| 		if err == nil { | ||||
| 			// 推送任务到前端 | ||||
| 			client := h.clients.Get(data.SessionId) | ||||
| 			if client != nil { | ||||
| 				utils.ReplyChunkMessage(client, jobVo) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	h.mjService.PushTask(service.MjTask{ | ||||
| 		Id:          jobId, | ||||
| 		SessionId:   data.SessionId, | ||||
| 		Src:         src, | ||||
| 		Type:        service.Variation, | ||||
| 		Prompt:      data.Prompt, | ||||
| 		UserId:      userId, | ||||
| 		RoleId:      data.RoleId, | ||||
| 		Icon:        data.Icon, | ||||
| 		ChatId:      data.ChatId, | ||||
| 		Index:       data.Index, | ||||
| 		MessageId:   data.MessageId, | ||||
| 		MessageHash: data.MessageHash, | ||||
| 	}) | ||||
| 	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) | ||||
|  | ||||
| 	// 从聊天窗口发送的请求,记录客户端信息 | ||||
| 	wsClient := h.App.ChatClients.Get(data.SessionId) | ||||
| 	if wsClient != nil { | ||||
| 		content := fmt.Sprintf("**%s** 已推送 variation 任务到 MidJourney 机器人,请耐心等待任务执行...", data.Prompt) | ||||
| 		utils.ReplyMessage(wsClient, content) | ||||
| 		if h.App.MjTaskClients.Get(data.SessionId) == nil { | ||||
| 			h.App.MjTaskClients.Put(data.SessionId, wsClient) | ||||
| 		} | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
| // JobList 获取 MJ 任务列表 | ||||
| func (h *MidJourneyHandler) JobList(c *gin.Context) { | ||||
| 	status := h.GetInt(c, "status", 0) | ||||
| 	var items []model.MidJourneyJob | ||||
| 	var res *gorm.DB | ||||
| 	userId, _ := c.Get(types.LoginUserID) | ||||
| 	if status == 1 { | ||||
| 		res = h.db.Where("user_id = ? AND progress = 100", userId).Order("id DESC").Find(&items) | ||||
| 	} else { | ||||
| 		res = h.db.Where("user_id = ? AND progress < 100", userId).Order("id ASC").Find(&items) | ||||
| 	} | ||||
| 	if res.Error != nil { | ||||
| 		resp.ERROR(c, types.NoData) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var jobs = make([]vo.MidJourneyJob, 0) | ||||
| 	for _, item := range items { | ||||
| 		var job vo.MidJourneyJob | ||||
| 		err := utils.CopyObject(item, &job) | ||||
| 		if err != nil { | ||||
| 			continue | ||||
| 		} | ||||
| 		if item.Progress < 100 && item.ImgURL != "" { // 正在运行中任务使用代理访问图片 | ||||
| 			image, err := utils.DownloadImage(item.ImgURL, h.App.Config.ProxyURL) | ||||
| 			if err == nil { | ||||
| 				job.ImgURL = "data:image/png;base64," + base64.StdEncoding.EncodeToString(image) | ||||
| 			} | ||||
| 		} | ||||
| 		jobs = append(jobs, job) | ||||
| 	} | ||||
| 	resp.SUCCESS(c, jobs) | ||||
| } | ||||
|   | ||||
							
								
								
									
										295
									
								
								api/handler/openai_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,295 @@ | ||||
| package handler | ||||
|  | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/store/vo" | ||||
| 	"chatplus/utils" | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"gorm.io/gorm" | ||||
| 	"io" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 	"unicode/utf8" | ||||
| ) | ||||
|  | ||||
| // 将消息发送给 OpenAI API 并获取结果,通过 WebSocket 推送到客户端 | ||||
| func (h *ChatHandler) sendOpenAiMessage( | ||||
| 	chatCtx []interface{}, | ||||
| 	req types.ApiRequest, | ||||
| 	userVo vo.User, | ||||
| 	ctx context.Context, | ||||
| 	session *types.ChatSession, | ||||
| 	role model.ChatRole, | ||||
| 	prompt string, | ||||
| 	ws *types.WsClient) error { | ||||
| 	promptCreatedAt := time.Now() // 记录提问时间 | ||||
| 	start := time.Now() | ||||
| 	var apiKey = userVo.ChatConfig.ApiKeys[session.Model.Platform] | ||||
| 	response, err := h.doRequest(ctx, req, session.Model.Platform, &apiKey) | ||||
| 	logger.Info("HTTP请求完成,耗时:", time.Now().Sub(start)) | ||||
| 	if err != nil { | ||||
| 		if strings.Contains(err.Error(), "context canceled") { | ||||
| 			logger.Info("用户取消了请求:", prompt) | ||||
| 			return nil | ||||
| 		} else if strings.Contains(err.Error(), "no available key") { | ||||
| 			utils.ReplyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!") | ||||
| 			return nil | ||||
| 		} else { | ||||
| 			logger.Error(err) | ||||
| 		} | ||||
|  | ||||
| 		utils.ReplyMessage(ws, ErrorMsg) | ||||
| 		utils.ReplyMessage(ws, "") | ||||
| 		return err | ||||
| 	} else { | ||||
| 		defer response.Body.Close() | ||||
| 	} | ||||
|  | ||||
| 	contentType := response.Header.Get("Content-Type") | ||||
| 	if strings.Contains(contentType, "text/event-stream") { | ||||
| 		replyCreatedAt := time.Now() // 记录回复时间 | ||||
| 		// 循环读取 Chunk 消息 | ||||
| 		var message = types.Message{} | ||||
| 		var contents = make([]string, 0) | ||||
| 		var functionCall = false | ||||
| 		var functionName string | ||||
| 		var arguments = make([]string, 0) | ||||
| 		scanner := bufio.NewScanner(response.Body) | ||||
| 		for scanner.Scan() { | ||||
| 			line := scanner.Text() | ||||
| 			if !strings.Contains(line, "data:") || len(line) < 30 { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			var responseBody = types.ApiResponse{} | ||||
| 			err = json.Unmarshal([]byte(line[6:]), &responseBody) | ||||
| 			if err != nil || len(responseBody.Choices) == 0 { // 数据解析出错 | ||||
| 				logger.Error(err, line) | ||||
| 				utils.ReplyMessage(ws, ErrorMsg) | ||||
| 				utils.ReplyMessage(ws, "") | ||||
| 				break | ||||
| 			} | ||||
|  | ||||
| 			fun := responseBody.Choices[0].Delta.FunctionCall | ||||
| 			if functionCall && fun.Name == "" { | ||||
| 				arguments = append(arguments, fun.Arguments) | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			if !utils.IsEmptyValue(fun) { | ||||
| 				functionName = fun.Name | ||||
| 				f := h.App.Functions[functionName] | ||||
| 				if f != nil { | ||||
| 					functionCall = true | ||||
| 					utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart}) | ||||
| 					utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: fmt.Sprintf("正在调用函数 `%s` 作答 ...\n\n", f.Name())}) | ||||
| 				} | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			if responseBody.Choices[0].FinishReason == "function_call" { // 函数调用完毕 | ||||
| 				break | ||||
| 			} | ||||
|  | ||||
| 			// 初始化 role | ||||
| 			if responseBody.Choices[0].Delta.Role != "" && message.Role == "" { | ||||
| 				message.Role = responseBody.Choices[0].Delta.Role | ||||
| 				utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart}) | ||||
| 				continue | ||||
| 			} else if responseBody.Choices[0].FinishReason != "" { | ||||
| 				break // 输出完成或者输出中断了 | ||||
| 			} else { | ||||
| 				content := responseBody.Choices[0].Delta.Content | ||||
| 				contents = append(contents, utils.InterfaceToString(content)) | ||||
| 				utils.ReplyChunkMessage(ws, types.WsMessage{ | ||||
| 					Type:    types.WsMiddle, | ||||
| 					Content: utils.InterfaceToString(responseBody.Choices[0].Delta.Content), | ||||
| 				}) | ||||
| 			} | ||||
| 		} // end for | ||||
|  | ||||
| 		if err := scanner.Err(); err != nil { | ||||
| 			if strings.Contains(err.Error(), "context canceled") { | ||||
| 				logger.Info("用户取消了请求:", prompt) | ||||
| 			} else { | ||||
| 				logger.Error("信息读取出错:", err) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if functionCall { // 调用函数完成任务 | ||||
| 			var params map[string]interface{} | ||||
| 			_ = utils.JsonDecode(strings.Join(arguments, ""), ¶ms) | ||||
| 			logger.Debugf("函数名称: %s, 函数参数:%s", functionName, params) | ||||
|  | ||||
| 			// for creating image, check if the user's img_calls > 0 | ||||
| 			if functionName == types.FuncMidJourney && userVo.ImgCalls <= 0 { | ||||
| 				utils.ReplyMessage(ws, "**当前用户剩余绘图次数已用尽,请扫描下面二维码联系管理员!**") | ||||
| 				utils.ReplyMessage(ws, "") | ||||
| 			} else { | ||||
| 				f := h.App.Functions[functionName] | ||||
| 				if functionName == types.FuncMidJourney { | ||||
| 					params["user_id"] = userVo.Id | ||||
| 					params["role_id"] = role.Id | ||||
| 					params["chat_id"] = session.ChatId | ||||
| 					params["icon"] = "/images/avatar/mid_journey.png" | ||||
| 					params["session_id"] = session.SessionId | ||||
| 				} | ||||
| 				data, err := f.Invoke(params) | ||||
| 				if err != nil { | ||||
| 					msg := "调用函数出错:" + err.Error() | ||||
| 					utils.ReplyChunkMessage(ws, types.WsMessage{ | ||||
| 						Type:    types.WsMiddle, | ||||
| 						Content: msg, | ||||
| 					}) | ||||
| 					contents = append(contents, msg) | ||||
| 				} else { | ||||
| 					content := data | ||||
| 					if functionName == types.FuncMidJourney { | ||||
| 						content = fmt.Sprintf("绘画提示词:%s 已推送任务到 MidJourney 机器人,请耐心等待任务执行...", data) | ||||
| 						h.App.MjTaskClients.Put(session.SessionId, ws) | ||||
| 						// update user's img_calls | ||||
| 						h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1)) | ||||
| 					} | ||||
|  | ||||
| 					utils.ReplyChunkMessage(ws, types.WsMessage{ | ||||
| 						Type:    types.WsMiddle, | ||||
| 						Content: content, | ||||
| 					}) | ||||
| 					contents = append(contents, content) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// 消息发送成功 | ||||
| 		if len(contents) > 0 { | ||||
| 			// 更新用户的对话次数 | ||||
| 			if userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" { | ||||
| 				h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("calls", gorm.Expr("calls - ?", 1)) | ||||
| 			} | ||||
|  | ||||
| 			if message.Role == "" { | ||||
| 				message.Role = "assistant" | ||||
| 			} | ||||
| 			message.Content = strings.Join(contents, "") | ||||
| 			useMsg := types.Message{Role: "user", Content: prompt} | ||||
|  | ||||
| 			// 更新上下文消息,如果是调用函数则不需要更新上下文 | ||||
| 			if h.App.ChatConfig.EnableContext && functionCall == false { | ||||
| 				chatCtx = append(chatCtx, useMsg)  // 提问消息 | ||||
| 				chatCtx = append(chatCtx, message) // 回复消息 | ||||
| 				h.App.ChatContexts.Put(session.ChatId, chatCtx) | ||||
| 			} | ||||
|  | ||||
| 			// 追加聊天记录 | ||||
| 			if h.App.ChatConfig.EnableHistory { | ||||
| 				useContext := true | ||||
| 				if functionCall { | ||||
| 					useContext = false | ||||
| 				} | ||||
|  | ||||
| 				// for prompt | ||||
| 				promptToken, err := utils.CalcTokens(prompt, req.Model) | ||||
| 				if err != nil { | ||||
| 					logger.Error(err) | ||||
| 				} | ||||
| 				historyUserMsg := model.HistoryMessage{ | ||||
| 					UserId:     userVo.Id, | ||||
| 					ChatId:     session.ChatId, | ||||
| 					RoleId:     role.Id, | ||||
| 					Type:       types.PromptMsg, | ||||
| 					Icon:       userVo.Avatar, | ||||
| 					Content:    prompt, | ||||
| 					Tokens:     promptToken, | ||||
| 					UseContext: useContext, | ||||
| 				} | ||||
| 				historyUserMsg.CreatedAt = promptCreatedAt | ||||
| 				historyUserMsg.UpdatedAt = promptCreatedAt | ||||
| 				res := h.db.Save(&historyUserMsg) | ||||
| 				if res.Error != nil { | ||||
| 					logger.Error("failed to save prompt history message: ", res.Error) | ||||
| 				} | ||||
|  | ||||
| 				// 计算本次对话消耗的总 token 数量 | ||||
| 				var totalTokens = 0 | ||||
| 				if functionCall { // prompt + 函数名 + 参数 token | ||||
| 					tokens, _ := utils.CalcTokens(functionName, req.Model) | ||||
| 					totalTokens += tokens | ||||
| 					tokens, _ = utils.CalcTokens(utils.InterfaceToString(arguments), req.Model) | ||||
| 					totalTokens += tokens | ||||
| 				} else { | ||||
| 					totalTokens, _ = utils.CalcTokens(message.Content, req.Model) | ||||
| 				} | ||||
| 				totalTokens += getTotalTokens(req) | ||||
|  | ||||
| 				historyReplyMsg := model.HistoryMessage{ | ||||
| 					UserId:     userVo.Id, | ||||
| 					ChatId:     session.ChatId, | ||||
| 					RoleId:     role.Id, | ||||
| 					Type:       types.ReplyMsg, | ||||
| 					Icon:       role.Icon, | ||||
| 					Content:    message.Content, | ||||
| 					Tokens:     totalTokens, | ||||
| 					UseContext: useContext, | ||||
| 				} | ||||
| 				historyReplyMsg.CreatedAt = replyCreatedAt | ||||
| 				historyReplyMsg.UpdatedAt = replyCreatedAt | ||||
| 				res = h.db.Create(&historyReplyMsg) | ||||
| 				if res.Error != nil { | ||||
| 					logger.Error("failed to save reply history message: ", res.Error) | ||||
| 				} | ||||
|  | ||||
| 				// 更新用户信息 | ||||
| 				h.db.Model(&model.User{}).Where("id = ?", userVo.Id). | ||||
| 					UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", totalTokens)) | ||||
| 			} | ||||
|  | ||||
| 			// 保存当前会话 | ||||
| 			var chatItem model.ChatItem | ||||
| 			res := h.db.Where("chat_id = ?", session.ChatId).First(&chatItem) | ||||
| 			if res.Error != nil { | ||||
| 				chatItem.ChatId = session.ChatId | ||||
| 				chatItem.UserId = session.UserId | ||||
| 				chatItem.RoleId = role.Id | ||||
| 				chatItem.ModelId = session.Model.Id | ||||
| 				if utf8.RuneCountInString(prompt) > 30 { | ||||
| 					chatItem.Title = string([]rune(prompt)[:30]) + "..." | ||||
| 				} else { | ||||
| 					chatItem.Title = prompt | ||||
| 				} | ||||
| 				h.db.Create(&chatItem) | ||||
| 			} | ||||
| 		} | ||||
| 	} else { | ||||
| 		body, err := io.ReadAll(response.Body) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("error with reading response: %v", err) | ||||
| 		} | ||||
| 		var res types.ApiError | ||||
| 		err = json.Unmarshal(body, &res) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("error with decode response: %v", err) | ||||
| 		} | ||||
|  | ||||
| 		// OpenAI API 调用异常处理 | ||||
| 		if strings.Contains(res.Error.Message, "This key is associated with a deactivated account") { | ||||
| 			utils.ReplyMessage(ws, "请求 OpenAI API 失败:API KEY 所关联的账户被禁用。") | ||||
| 			// 移除当前 API key | ||||
| 			h.db.Where("value = ?", apiKey).Delete(&model.ApiKey{}) | ||||
| 		} else if strings.Contains(res.Error.Message, "You exceeded your current quota") { | ||||
| 			utils.ReplyMessage(ws, "请求 OpenAI API 失败:API KEY 触发并发限制,请稍后再试。") | ||||
| 		} else if strings.Contains(res.Error.Message, "This model's maximum context length") { | ||||
| 			logger.Error(res.Error.Message) | ||||
| 			utils.ReplyMessage(ws, "当前会话上下文长度超出限制,已为您清空会话上下文!") | ||||
| 			h.App.ChatContexts.Delete(session.ChatId) | ||||
| 			return h.sendMessage(ctx, session, role, prompt, ws) | ||||
| 		} else { | ||||
| 			utils.ReplyMessage(ws, "请求 OpenAI API 失败:"+res.Error.Message) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
| @@ -8,6 +8,7 @@ import ( | ||||
| 	"chatplus/utils/resp" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gorm.io/gorm" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| type RewardHandler struct { | ||||
| @@ -75,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 { | ||||
|   | ||||
| @@ -21,11 +21,11 @@ func NewUploadHandler(app *core.AppServer, db *gorm.DB, manager *oss.UploaderMan | ||||
| } | ||||
|  | ||||
| func (h *UploadHandler) Upload(c *gin.Context) { | ||||
| 	fileURL, err := h.uploaderManager.GetActiveService().PutFile(c, "file") | ||||
| 	fileURL, err := h.uploaderManager.GetUploadHandler().PutFile(c, "file") | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	 | ||||
|  | ||||
| 	resp.SUCCESS(c, fileURL) | ||||
| } | ||||
|   | ||||
| @@ -3,17 +3,17 @@ package handler | ||||
| import ( | ||||
| 	"chatplus/core" | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/service/oss" | ||||
| 	"chatplus/store" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/store/vo" | ||||
| 	"chatplus/utils" | ||||
| 	"chatplus/utils/resp" | ||||
| 	"fmt" | ||||
| 	"github.com/go-redis/redis/v8" | ||||
| 	"github.com/golang-jwt/jwt/v5" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/gin-contrib/sessions" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/lionsoul2014/ip2region/binding/golang/xdb" | ||||
| 	"gorm.io/gorm" | ||||
| @@ -21,10 +21,10 @@ import ( | ||||
|  | ||||
| type UserHandler struct { | ||||
| 	BaseHandler | ||||
| 	db            *gorm.DB | ||||
| 	searcher      *xdb.Searcher | ||||
| 	leveldb       *store.LevelDB | ||||
| 	uploadManager *oss.UploaderManager | ||||
| 	db       *gorm.DB | ||||
| 	searcher *xdb.Searcher | ||||
| 	leveldb  *store.LevelDB | ||||
| 	redis    *redis.Client | ||||
| } | ||||
|  | ||||
| func NewUserHandler( | ||||
| @@ -32,8 +32,8 @@ func NewUserHandler( | ||||
| 	db *gorm.DB, | ||||
| 	searcher *xdb.Searcher, | ||||
| 	levelDB *store.LevelDB, | ||||
| 	manager *oss.UploaderManager) *UserHandler { | ||||
| 	handler := &UserHandler{db: db, searcher: searcher, leveldb: levelDB, uploadManager: manager} | ||||
| 	client *redis.Client) *UserHandler { | ||||
| 	handler := &UserHandler{db: db, searcher: searcher, leveldb: levelDB, redis: client} | ||||
| 	handler.App = app | ||||
| 	return handler | ||||
| } | ||||
| @@ -42,20 +42,18 @@ func NewUserHandler( | ||||
| 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 { | ||||
| @@ -69,7 +67,6 @@ func (h *UserHandler) Register(c *gin.Context) { | ||||
| 		var code int | ||||
| 		err := h.leveldb.Get(key, &code) | ||||
| 		if err != nil || code != data.Code { | ||||
| 			logger.Info(code) | ||||
| 			resp.ERROR(c, "短信验证码错误") | ||||
| 			return | ||||
| 		} | ||||
| @@ -77,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 | ||||
| 	} | ||||
|  | ||||
| @@ -99,21 +90,18 @@ 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, | ||||
| 		ImgCalls: h.App.SysConfig.InitImgCalls, | ||||
| @@ -134,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 | ||||
| @@ -164,31 +152,38 @@ func (h *UserHandler) Login(c *gin.Context) { | ||||
| 	user.LastLoginAt = time.Now().Unix() | ||||
| 	h.db.Model(&user).Updates(user) | ||||
|  | ||||
| 	err := utils.SetLoginUser(c, user) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, "保存会话失败") | ||||
| 		logger.Error("Error for save session: ", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	h.db.Create(&model.UserLoginLog{ | ||||
| 		UserId:       user.Id, | ||||
| 		Username:     user.Username, | ||||
| 		Username:     user.Mobile, | ||||
| 		LoginIp:      c.ClientIP(), | ||||
| 		LoginAddress: utils.Ip2Region(h.searcher, c.ClientIP()), | ||||
| 	}) | ||||
|  | ||||
| 	resp.SUCCESS(c) | ||||
| 	// 创建 token | ||||
| 	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ | ||||
| 		"user_id": user.Id, | ||||
| 		"expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)).Unix(), | ||||
| 	}) | ||||
| 	tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey)) | ||||
| 	if err != nil { | ||||
| 		resp.ERROR(c, "Failed to generate token, "+err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	// 保存到 redis | ||||
| 	key := fmt.Sprintf("users/%d", user.Id) | ||||
| 	if _, err := h.redis.Set(c, key, tokenString, 0).Result(); err != nil { | ||||
| 		resp.ERROR(c, "error with save token: "+err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	resp.SUCCESS(c, tokenString) | ||||
| } | ||||
|  | ||||
| // Logout 注 销 | ||||
| func (h *UserHandler) Logout(c *gin.Context) { | ||||
| 	sessionId := c.GetHeader(types.SessionName) | ||||
| 	session := sessions.Default(c) | ||||
| 	session.Delete(types.SessionUser) | ||||
| 	err := session.Save() | ||||
| 	if err != nil { | ||||
| 		logger.Error("Error for save session: ", err) | ||||
| 	sessionId := c.GetHeader(types.ChatTokenHeader) | ||||
| 	key := h.GetUserKey(c) | ||||
| 	if _, err := h.redis.Del(c, key).Result(); err != nil { | ||||
| 		logger.Error("error with delete session: ", err) | ||||
| 	} | ||||
| 	// 删除 websocket 会话列表 | ||||
| 	h.App.ChatSession.Delete(sessionId) | ||||
| @@ -218,15 +213,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"` | ||||
| 	ImgCalls   int              `json:"img_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) { | ||||
| @@ -262,38 +255,14 @@ func (h *UserHandler) ProfileUpdate(c *gin.Context) { | ||||
| 		return | ||||
| 	} | ||||
| 	h.db.First(&user, user.Id) | ||||
| 	user.Nickname = data.Nickname | ||||
| 	oldAvatar := user.Avatar | ||||
| 	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 | ||||
| 	} | ||||
|  | ||||
| 	// remove the old avatar image file | ||||
| 	if oldAvatar != data.Avatar { | ||||
| 		err = h.uploadManager.GetActiveService().Delete(oldAvatar) | ||||
| 		if err != nil { | ||||
| 			logger.Error("error with delete image: ", err) | ||||
| 		} | ||||
| 	} | ||||
| 	resp.SUCCESS(c) | ||||
| } | ||||
|  | ||||
|   | ||||
							
								
								
									
										40
									
								
								api/main.go
									
									
									
									
									
								
							
							
						
						| @@ -12,6 +12,7 @@ import ( | ||||
| 	"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) { | ||||
| @@ -116,6 +118,7 @@ func main() { | ||||
| 		fx.Provide(handler.NewRewardHandler), | ||||
| 		fx.Provide(handler.NewCaptchaHandler), | ||||
| 		fx.Provide(handler.NewMidJourneyHandler), | ||||
| 		fx.Provide(handler.NewChatModelHandler), | ||||
|  | ||||
| 		fx.Provide(admin.NewConfigHandler), | ||||
| 		fx.Provide(admin.NewAdminHandler), | ||||
| @@ -124,6 +127,7 @@ func main() { | ||||
| 		fx.Provide(admin.NewChatRoleHandler), | ||||
| 		fx.Provide(admin.NewRewardHandler), | ||||
| 		fx.Provide(admin.NewDashboardHandler), | ||||
| 		fx.Provide(admin.NewChatModelHandler), | ||||
|  | ||||
| 		// 创建服务 | ||||
| 		fx.Provide(service.NewAliYunSmsService), | ||||
| @@ -131,6 +135,12 @@ func main() { | ||||
| 			return service.NewCaptchaService(config.ApiConfig) | ||||
| 		}), | ||||
| 		fx.Provide(oss.NewUploaderManager), | ||||
| 		fx.Provide(service.NewMjService), | ||||
| 		fx.Invoke(func(mjService *service.MjService) { | ||||
| 			go func() { | ||||
| 				mjService.Run() | ||||
| 			}() | ||||
| 		}), | ||||
|  | ||||
| 		// 注册路由 | ||||
| 		fx.Invoke(func(s *core.AppServer, h *handler.ChatRoleHandler) { | ||||
| @@ -157,7 +167,7 @@ func main() { | ||||
| 			group.GET("remove", h.Remove) | ||||
| 			group.GET("history", h.History) | ||||
| 			group.GET("clear", h.Clear) | ||||
| 			group.GET("tokens", h.Tokens) | ||||
| 			group.POST("tokens", h.Tokens) | ||||
| 			group.GET("stop", h.StopGenerate) | ||||
| 		}), | ||||
| 		fx.Invoke(func(s *core.AppServer, h *handler.UploadHandler) { | ||||
| @@ -179,9 +189,13 @@ func main() { | ||||
| 			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) | ||||
| 			group := s.Engine.Group("/api/mj/") | ||||
| 			group.POST("notify", h.Notify) | ||||
| 			group.POST("image", h.Image) | ||||
| 			group.POST("upscale", h.Upscale) | ||||
| 			group.POST("variation", h.Variation) | ||||
| 			group.GET("jobs", h.JobList) | ||||
| 			group.Any("client", h.Client) | ||||
| 		}), | ||||
|  | ||||
| 		// 管理后台控制器 | ||||
| @@ -215,7 +229,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) { | ||||
| @@ -226,6 +240,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) | ||||
|   | ||||
							
								
								
									
										41
									
								
								api/service/function/func_mj.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,41 @@ | ||||
| package function | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/service" | ||||
| 	"chatplus/utils" | ||||
| ) | ||||
|  | ||||
| // AI 绘画函数 | ||||
|  | ||||
| type FuncMidJourney struct { | ||||
| 	name    string | ||||
| 	service *service.MjService | ||||
| } | ||||
|  | ||||
| func NewMidJourneyFunc(mjService *service.MjService) FuncMidJourney { | ||||
| 	return FuncMidJourney{ | ||||
| 		name:    "MidJourney AI 绘画", | ||||
| 		service: mjService} | ||||
| } | ||||
|  | ||||
| func (f FuncMidJourney) Invoke(params map[string]interface{}) (string, error) { | ||||
| 	logger.Infof("MJ 绘画参数:%+v", params) | ||||
| 	prompt := utils.InterfaceToString(params["prompt"]) | ||||
| 	f.service.PushTask(service.MjTask{ | ||||
| 		SessionId: utils.InterfaceToString(params["session_id"]), | ||||
| 		Src:       service.TaskSrcChat, | ||||
| 		Type:      service.Image, | ||||
| 		Prompt:    prompt, | ||||
| 		UserId:    utils.IntValue(utils.InterfaceToString(params["user_id"]), 0), | ||||
| 		RoleId:    utils.IntValue(utils.InterfaceToString(params["role_id"]), 0), | ||||
| 		Icon:      utils.InterfaceToString(params["icon"]), | ||||
| 		ChatId:    utils.InterfaceToString(params["chat_id"]), | ||||
| 	}) | ||||
| 	return prompt, nil | ||||
| } | ||||
|  | ||||
| func (f FuncMidJourney) Name() string { | ||||
| 	return f.name | ||||
| } | ||||
|  | ||||
| var _ Function = &FuncMidJourney{} | ||||
| @@ -3,6 +3,7 @@ package function | ||||
| import ( | ||||
| 	"chatplus/core/types" | ||||
| 	logger2 "chatplus/logger" | ||||
| 	"chatplus/service" | ||||
| ) | ||||
|  | ||||
| type Function interface { | ||||
| @@ -28,11 +29,11 @@ type dataItem struct { | ||||
| 	Remark string `json:"remark"` | ||||
| } | ||||
|  | ||||
| func NewFunctions(config *types.AppConfig) map[string]Function { | ||||
| func NewFunctions(config *types.AppConfig, mjService *service.MjService) map[string]Function { | ||||
| 	return map[string]Function{ | ||||
| 		types.FuncZaoBao:     NewZaoBao(config.ApiConfig), | ||||
| 		types.FuncWeibo:      NewWeiboHot(config.ApiConfig), | ||||
| 		types.FuncHeadLine:   NewHeadLines(config.ApiConfig), | ||||
| 		types.FuncMidJourney: NewMidJourneyFunc(config.ExtConfig), | ||||
| 		types.FuncMidJourney: NewMidJourneyFunc(mjService), | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,117 +0,0 @@ | ||||
| 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{} | ||||
							
								
								
									
										203
									
								
								api/service/mj_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,203 @@ | ||||
| package service | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core/types" | ||||
| 	logger2 "chatplus/logger" | ||||
| 	"chatplus/store" | ||||
| 	"chatplus/store/model" | ||||
| 	"chatplus/utils" | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"github.com/go-redis/redis/v8" | ||||
| 	"github.com/imroc/req/v3" | ||||
| 	"gorm.io/gorm" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| var logger = logger2.GetLogger() | ||||
|  | ||||
| // MJ 绘画服务 | ||||
|  | ||||
| const MjRunningJobKey = "MidJourney_Running_Job" | ||||
|  | ||||
| type TaskType string | ||||
|  | ||||
| func (t TaskType) String() string { | ||||
| 	return string(t) | ||||
| } | ||||
|  | ||||
| const ( | ||||
| 	Image     = TaskType("image") | ||||
| 	Upscale   = TaskType("upscale") | ||||
| 	Variation = TaskType("variation") | ||||
| ) | ||||
|  | ||||
| type TaskSrc string | ||||
|  | ||||
| const ( | ||||
| 	TaskSrcChat = TaskSrc("chat") | ||||
| 	TaskSrcImg  = TaskSrc("img") | ||||
| ) | ||||
|  | ||||
| type MjTask struct { | ||||
| 	Id          int      `json:"id"` | ||||
| 	SessionId   string   `json:"session_id"` | ||||
| 	Src         TaskSrc  `json:"src"` | ||||
| 	Type        TaskType `json:"type"` | ||||
| 	UserId      int      `json:"user_id"` | ||||
| 	Prompt      string   `json:"prompt,omitempty"` | ||||
| 	ChatId      string   `json:"chat_id,omitempty"` | ||||
| 	RoleId      int      `json:"role_id,omitempty"` | ||||
| 	Icon        string   `json:"icon,omitempty"` | ||||
| 	Index       int32    `json:"index,omitempty"` | ||||
| 	MessageId   string   `json:"message_id,omitempty"` | ||||
| 	MessageHash string   `json:"message_hash,omitempty"` | ||||
| 	RetryCount  int      `json:"retry_count"` | ||||
| } | ||||
|  | ||||
| type MjService struct { | ||||
| 	config    types.ChatPlusExtConfig | ||||
| 	client    *req.Client | ||||
| 	taskQueue *store.RedisQueue | ||||
| 	redis     *redis.Client | ||||
| 	db        *gorm.DB | ||||
| } | ||||
|  | ||||
| func NewMjService(appConfig *types.AppConfig, client *redis.Client, db *gorm.DB) *MjService { | ||||
| 	return &MjService{ | ||||
| 		config:    appConfig.ExtConfig, | ||||
| 		redis:     client, | ||||
| 		db:        db, | ||||
| 		taskQueue: store.NewRedisQueue("midjourney_task_queue", client), | ||||
| 		client:    req.C().SetTimeout(30 * time.Second)} | ||||
| } | ||||
|  | ||||
| func (s *MjService) Run() { | ||||
| 	logger.Info("Starting MidJourney job consumer.") | ||||
| 	ctx := context.Background() | ||||
| 	for { | ||||
| 		_, err := s.redis.Get(ctx, MjRunningJobKey).Result() | ||||
| 		if err == nil { | ||||
| 			time.Sleep(time.Second * 3) | ||||
| 			continue | ||||
| 		} | ||||
| 		var task MjTask | ||||
| 		err = s.taskQueue.LPop(&task) | ||||
| 		if err != nil { | ||||
| 			logger.Errorf("taking task with error: %v", err) | ||||
| 			continue | ||||
| 		} | ||||
| 		logger.Infof("Consuming Task: %+v", task) | ||||
| 		switch task.Type { | ||||
| 		case Image: | ||||
| 			err = s.image(task.Prompt) | ||||
| 			break | ||||
| 		case Upscale: | ||||
| 			err = s.upscale(MjUpscaleReq{ | ||||
| 				Index:       task.Index, | ||||
| 				MessageId:   task.MessageId, | ||||
| 				MessageHash: task.MessageHash, | ||||
| 			}) | ||||
| 			break | ||||
| 		case Variation: | ||||
| 			err = s.variation(MjVariationReq{ | ||||
| 				Index:       task.Index, | ||||
| 				MessageId:   task.MessageId, | ||||
| 				MessageHash: task.MessageHash, | ||||
| 			}) | ||||
| 		} | ||||
| 		if err != nil { | ||||
| 			logger.Error("绘画任务执行失败:", err) | ||||
| 			if task.RetryCount > 5 { | ||||
| 				// 取消并删除任务 | ||||
| 				s.db.Where("id = ?", task.Id).Delete(&model.MidJourneyJob{}) | ||||
| 				continue | ||||
| 			} | ||||
| 			task.RetryCount += 1 | ||||
| 			s.taskQueue.RPush(task) | ||||
| 			// TODO: 执行失败通知聊天客户端 | ||||
| 			time.Sleep(time.Second * 3) | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		// 锁定任务执行通道,直到任务超时(5分钟) | ||||
| 		s.redis.Set(ctx, MjRunningJobKey, utils.JsonEncode(task), time.Minute*5) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (s *MjService) PushTask(task MjTask) { | ||||
| 	logger.Infof("add a new MidJourney Task: %+v", task) | ||||
| 	s.taskQueue.RPush(task) | ||||
| } | ||||
|  | ||||
| func (s *MjService) image(prompt string) error { | ||||
| 	logger.Infof("MJ 绘画参数:%+v", prompt) | ||||
| 	body := map[string]string{"prompt": prompt} | ||||
| 	url := fmt.Sprintf("%s/api/mj/image", s.config.ApiURL) | ||||
| 	var res types.BizVo | ||||
| 	r, err := s.client.R(). | ||||
| 		SetHeader("Authorization", s.config.Token). | ||||
| 		SetHeader("Content-Type", "application/json"). | ||||
| 		SetBody(body). | ||||
| 		SetSuccessResult(&res).Post(url) | ||||
| 	if err != nil || r.IsErrorState() { | ||||
| 		return fmt.Errorf("%v%v", r.String(), err) | ||||
| 	} | ||||
|  | ||||
| 	if res.Code != types.Success { | ||||
| 		return errors.New(res.Message) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| type MjUpscaleReq struct { | ||||
| 	Index       int32  `json:"index"` | ||||
| 	MessageId   string `json:"message_id"` | ||||
| 	MessageHash string `json:"message_hash"` | ||||
| } | ||||
|  | ||||
| func (s *MjService) upscale(upReq MjUpscaleReq) error { | ||||
| 	url := fmt.Sprintf("%s/api/mj/upscale", s.config.ApiURL) | ||||
| 	var res types.BizVo | ||||
| 	r, err := s.client.R(). | ||||
| 		SetHeader("Authorization", s.config.Token). | ||||
| 		SetHeader("Content-Type", "application/json"). | ||||
| 		SetBody(upReq). | ||||
| 		SetSuccessResult(&res).Post(url) | ||||
| 	if err != nil || r.IsErrorState() { | ||||
| 		return fmt.Errorf("%v%v", r.String(), err) | ||||
| 	} | ||||
|  | ||||
| 	if res.Code != types.Success { | ||||
| 		return errors.New(res.Message) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| type MjVariationReq struct { | ||||
| 	Index       int32  `json:"index"` | ||||
| 	MessageId   string `json:"message_id"` | ||||
| 	MessageHash string `json:"message_hash"` | ||||
| } | ||||
|  | ||||
| func (s *MjService) variation(upReq MjVariationReq) error { | ||||
| 	url := fmt.Sprintf("%s/api/mj/variation", s.config.ApiURL) | ||||
| 	var res types.BizVo | ||||
| 	r, err := s.client.R(). | ||||
| 		SetHeader("Authorization", s.config.Token). | ||||
| 		SetHeader("Content-Type", "application/json"). | ||||
| 		SetBody(upReq). | ||||
| 		SetSuccessResult(&res).Post(url) | ||||
| 	if err != nil || r.IsErrorState() { | ||||
| 		return fmt.Errorf("%v%v", r.String(), err) | ||||
| 	} | ||||
|  | ||||
| 	if res.Code != types.Success { | ||||
| 		return errors.New(res.Message) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										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{} | ||||
| @@ -6,32 +6,37 @@ import ( | ||||
| ) | ||||
|  | ||||
| type UploaderManager struct { | ||||
| 	active         string | ||||
| 	uploadServices map[string]Uploader | ||||
| 	handler Uploader | ||||
| } | ||||
|  | ||||
| const uploaderLocal = "LOCAL" | ||||
| const uploaderMinio = "MINIO" | ||||
| const Local = "LOCAL" | ||||
| const Minio = "MINIO" | ||||
| const QiNiu = "QINIU" | ||||
|  | ||||
| func NewUploaderManager(config *types.AppConfig) (*UploaderManager, error) { | ||||
| 	services := make(map[string]Uploader) | ||||
| 	if config.OSS.Minio.AccessKey != "" { | ||||
| 		minioService, err := NewMinioService(config) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		services[uploaderMinio] = minioService | ||||
| 	} | ||||
| 	if config.OSS.Local.BasePath != "" { | ||||
| 		services[uploaderLocal] = NewLocalStorageService(config) | ||||
| 	} | ||||
| 	active := uploaderLocal | ||||
| 	active := Local | ||||
| 	if config.OSS.Active != "" { | ||||
| 		active = strings.ToUpper(config.OSS.Active) | ||||
| 	} | ||||
| 	return &UploaderManager{uploadServices: services, active: active}, nil | ||||
| 	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) GetActiveService() Uploader { | ||||
| 	return m.uploadServices[m.active] | ||||
| 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
									
								
							
							
						
						| @@ -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    //排序数字 | ||||
| } | ||||
|   | ||||
| @@ -4,14 +4,14 @@ import "time" | ||||
|  | ||||
| type MidJourneyJob struct { | ||||
| 	Id          uint `gorm:"primarykey;column:id"` | ||||
| 	UserId      uint | ||||
| 	ChatId      string | ||||
| 	Type        string | ||||
| 	UserId      int | ||||
| 	MessageId   string | ||||
| 	ReferenceId string | ||||
| 	Hash        string | ||||
| 	Content     string | ||||
| 	ImgURL      string | ||||
| 	Hash        string // message hash | ||||
| 	Progress    int | ||||
| 	Prompt      string | ||||
| 	Image       string | ||||
| 	CreatedAt   time.Time | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -2,13 +2,11 @@ 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 | ||||
|   | ||||
							
								
								
									
										20
									
								
								api/store/redis.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,20 @@ | ||||
| package store | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/core/types" | ||||
| 	"context" | ||||
| 	"github.com/go-redis/redis/v8" | ||||
| ) | ||||
|  | ||||
| func NewRedisClient(config *types.AppConfig) (*redis.Client, error) { | ||||
| 	client := redis.NewClient(&redis.Options{ | ||||
| 		Addr:     config.Redis.Url(), | ||||
| 		Password: config.Redis.Password, | ||||
| 		DB:       config.Redis.DB, | ||||
| 	}) | ||||
| 	_, err := client.Ping(context.Background()).Result() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return client, nil | ||||
| } | ||||
							
								
								
									
										41
									
								
								api/store/redis_queue.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,41 @@ | ||||
| package store | ||||
|  | ||||
| import ( | ||||
| 	"chatplus/utils" | ||||
| 	"context" | ||||
| 	"github.com/go-redis/redis/v8" | ||||
| ) | ||||
|  | ||||
| type RedisQueue struct { | ||||
| 	name   string | ||||
| 	client *redis.Client | ||||
| 	ctx    context.Context | ||||
| } | ||||
|  | ||||
| func NewRedisQueue(name string, client *redis.Client) *RedisQueue { | ||||
| 	return &RedisQueue{name: name, client: client, ctx: context.Background()} | ||||
| } | ||||
|  | ||||
| func (q *RedisQueue) RPush(value interface{}) { | ||||
| 	q.client.RPush(q.ctx, q.name, utils.JsonEncode(value)) | ||||
| } | ||||
|  | ||||
| func (q *RedisQueue) LPush(value interface{}) { | ||||
| 	q.client.LPush(q.ctx, q.name, utils.JsonEncode(value)) | ||||
| } | ||||
|  | ||||
| func (q *RedisQueue) LPop(value interface{}) error { | ||||
| 	result, err := q.client.BLPop(q.ctx, 0, q.name).Result() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return utils.JsonDecode(result[1], value) | ||||
| } | ||||
|  | ||||
| func (q *RedisQueue) RPop(value interface{}) error { | ||||
| 	result, err := q.client.BRPop(q.ctx, 0, q.name).Result() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return utils.JsonDecode(result[1], value) | ||||
| } | ||||
| @@ -3,7 +3,7 @@ package vo | ||||
| // ApiKey OpenAI API 模型 | ||||
| type ApiKey struct { | ||||
| 	BaseVo | ||||
| 	UserId     uint   `json:"user_id"`      //用户ID,系统添加的用户 ID 为 0 | ||||
| 	Platform   string `json:"platform"` | ||||
| 	Value      string `json:"value"`        // API Key 的值 | ||||
| 	LastUsedAt int64  `json:"last_used_at"` // 最后使用时间 | ||||
| } | ||||
|   | ||||
| @@ -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
									
								
							
							
						
						| @@ -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"`      // 排序 | ||||
| } | ||||
|   | ||||
							
								
								
									
										16
									
								
								api/store/vo/mj_job.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,16 @@ | ||||
| package vo | ||||
|  | ||||
| import "time" | ||||
|  | ||||
| type MidJourneyJob struct { | ||||
| 	Id          uint      `json:"id"` | ||||
| 	Type        string    `json:"type"` | ||||
| 	UserId      int       `json:"user_id"` | ||||
| 	MessageId   string    `json:"message_id"` | ||||
| 	ReferenceId string    `json:"reference_id"` | ||||
| 	ImgURL      string    `json:"img_url"` | ||||
| 	Hash        string    `json:"hash"` | ||||
| 	Progress    int       `json:"progress"` | ||||
| 	Prompt      string    `json:"prompt"` | ||||
| 	CreatedAt   time.Time `json:"created_at"` | ||||
| } | ||||
| @@ -4,18 +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"`  // 剩余对话次数 | ||||
| 	ImgCalls    int              `json:"img_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 | ||||
| } | ||||
|   | ||||
							
								
								
									
										194
									
								
								api/test/test.go
									
									
									
									
									
								
							
							
						
						| @@ -1,199 +1,13 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"chatplus/core" | ||||
| 	"chatplus/core/types" | ||||
| 	"chatplus/service/oss" | ||||
| 	"chatplus/utils" | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"github.com/lionsoul2014/ip2region/binding/golang/xdb" | ||||
| 	"github.com/pkoukk/tiktoken-go" | ||||
| 	"io" | ||||
| 	"log" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 	"net/url" | ||||
| ) | ||||
|  | ||||
| func main() { | ||||
| 	imageURL := "https://cdn.discordapp.com/attachments/1139552247693443184/1141619433752768572/lisamiller4099_A_beautiful_fairy_sister_from_Chinese_mythology__3162726e-5ee4-4f60-932b-6b78b375eaef.png" | ||||
| 	parse, _ := url.Parse("http://localhost:5678/static") | ||||
|  | ||||
| 	fmt.Println(filepath.Ext(filepath.Base(imageURL))) | ||||
| } | ||||
|  | ||||
| // Http client 取消操作 | ||||
| func testHttpClient(ctx context.Context) { | ||||
|  | ||||
| 	req, err := http.NewRequest("GET", "http://localhost:2345", nil) | ||||
| 	if err != nil { | ||||
| 		fmt.Println(err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	req = req.WithContext(ctx) | ||||
|  | ||||
| 	client := &http.Client{} | ||||
|  | ||||
| 	resp, err := client.Do(req) | ||||
| 	if err != nil { | ||||
| 		fmt.Println(err) | ||||
| 		return | ||||
| 	} | ||||
| 	defer func(Body io.ReadCloser) { | ||||
| 		err := Body.Close() | ||||
| 		if err != nil { | ||||
|  | ||||
| 		} | ||||
| 	}(resp.Body) | ||||
| 	_, err = io.ReadAll(resp.Body) | ||||
| 	for { | ||||
| 		time.Sleep(time.Second) | ||||
| 		fmt.Println(time.Now()) | ||||
| 		select { | ||||
| 		case <-ctx.Done(): | ||||
| 			fmt.Println("取消退出") | ||||
| 			return | ||||
| 		default: | ||||
| 			continue | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| } | ||||
|  | ||||
| func testDate() { | ||||
| 	fmt.Println(time.Unix(1683336167, 0).Format("2006-01-02 15:04:05")) | ||||
| } | ||||
|  | ||||
| func testIp2Region() { | ||||
| 	dbPath := "res/ip2region.xdb" | ||||
| 	// 1、从 dbPath 加载整个 xdb 到内存 | ||||
| 	cBuff, err := xdb.LoadContentFromFile(dbPath) | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("failed to load content from `%s`: %s\n", dbPath, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// 2、用全局的 cBuff 创建完全基于内存的查询对象。 | ||||
| 	searcher, err := xdb.NewWithBuffer(cBuff) | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("failed to create searcher with content: %s\n", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	str, err := searcher.SearchByStr("103.88.46.85") | ||||
| 	fmt.Println(str) | ||||
| 	if err != nil { | ||||
| 		log.Fatal(err) | ||||
| 	} | ||||
| 	arr := strings.Split(str, "|") | ||||
| 	fmt.Println(arr[2], arr[3], arr[4]) | ||||
|  | ||||
| } | ||||
|  | ||||
| func calTokens() { | ||||
| 	text := "须知少年凌云志,曾许人间第一流" | ||||
| 	encoding := "cl100k_base" | ||||
|  | ||||
| 	tke, err := tiktoken.GetEncoding(encoding) | ||||
| 	if err != nil { | ||||
| 		err = fmt.Errorf("getEncoding: %v", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// encode | ||||
| 	token := tke.Encode(text, nil, nil) | ||||
|  | ||||
| 	//tokens | ||||
| 	fmt.Println(token) | ||||
| 	// num_tokens | ||||
| 	fmt.Println(len(token)) | ||||
|  | ||||
| } | ||||
|  | ||||
| func testAesEncrypt() { | ||||
| 	// 加密 | ||||
| 	text := []byte("this is a secret text") | ||||
| 	key := utils.RandString(24) | ||||
| 	encrypt, err := utils.AesEncrypt(key, text) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	fmt.Println("加密密文:", encrypt) | ||||
| 	// 解密 | ||||
| 	decrypt, err := utils.AesDecrypt(key, encrypt) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	fmt.Println("解密明文:", string(decrypt)) | ||||
| } | ||||
|  | ||||
| func extractFunction() error { | ||||
| 	open, err := os.Open("res/data.txt") | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	reader := bufio.NewReader(open) | ||||
| 	var contents = make([]string, 0) | ||||
| 	var functionCall = false | ||||
| 	var functionName string | ||||
| 	for { | ||||
| 		line, err := reader.ReadString('\n') | ||||
| 		if err != nil { | ||||
| 			break | ||||
| 		} | ||||
| 		if !strings.Contains(line, "data:") { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		var responseBody = types.ApiResponse{} | ||||
| 		err = json.Unmarshal([]byte(line[6:]), &responseBody) | ||||
| 		if err != nil || len(responseBody.Choices) == 0 { // 数据解析出错 | ||||
| 			break | ||||
| 		} | ||||
|  | ||||
| 		function := responseBody.Choices[0].Delta.FunctionCall | ||||
| 		if functionCall && function.Name == "" { | ||||
| 			contents = append(contents, function.Arguments) | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		if !utils.IsEmptyValue(function) { | ||||
| 			functionCall = true | ||||
| 			functionName = function.Name | ||||
| 			continue | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	fmt.Println("函数名称: ", functionName) | ||||
| 	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) | ||||
| 	imgURLPrefix := fmt.Sprintf("%s://%s", parse.Scheme, parse.Host) | ||||
| 	fmt.Println(imgURLPrefix) | ||||
| } | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"reflect" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/lionsoul2014/ip2region/binding/golang/xdb" | ||||
| @@ -92,7 +93,7 @@ func IsEmptyValue(obj interface{}) bool { | ||||
| 	if obj == nil { | ||||
| 		return true | ||||
| 	} | ||||
| 	 | ||||
|  | ||||
| 	v := reflect.ValueOf(obj) | ||||
| 	switch v.Kind() { | ||||
| 	case reflect.Ptr, reflect.Interface: | ||||
| @@ -113,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 | ||||
| } | ||||
|   | ||||
| @@ -12,7 +12,7 @@ import ( | ||||
| var logger = logger2.GetLogger() | ||||
|  | ||||
| // ReplyChunkMessage 回复客户片段端消息 | ||||
| func ReplyChunkMessage(client *types.WsClient, message types.WsMessage) { | ||||
| func ReplyChunkMessage(client *types.WsClient, message interface{}) { | ||||
| 	msg, err := json.Marshal(message) | ||||
| 	if err != nil { | ||||
| 		logger.Errorf("Error for decoding json data: %v", err.Error()) | ||||
|   | ||||
| @@ -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"}) | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -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") | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
| -- https://www.phpmyadmin.net/ | ||||
| -- | ||||
| -- 主机: localhost | ||||
| -- 生成日期: 2023-07-25 17:07:04 | ||||
| -- 生成日期: 2023-09-05 13:56:27 | ||||
| -- 服务器版本: 8.0.33-0ubuntu0.22.04.2 | ||||
| -- PHP 版本: 8.1.18 | ||||
| 
 | ||||
| @@ -32,8 +32,8 @@ USE `chatgpt_plus`; | ||||
| 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', | ||||
|   `user_id` int NOT NULL COMMENT '用户 ID', | ||||
|   `last_used_at` int NOT NULL COMMENT '最后使用时间', | ||||
|   `created_at` datetime NOT NULL, | ||||
|   `updated_at` datetime NOT NULL | ||||
| @@ -57,7 +57,8 @@ CREATE TABLE `chatgpt_chat_history` ( | ||||
|   `tokens` smallint NOT NULL COMMENT '耗费 token 数量', | ||||
|   `use_context` tinyint(1) NOT NULL COMMENT '是否允许作为上下文语料', | ||||
|   `created_at` datetime NOT NULL, | ||||
|   `updated_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='聊天历史记录'; | ||||
| 
 | ||||
| -- -------------------------------------------------------- | ||||
| @@ -73,13 +74,43 @@ CREATE TABLE `chatgpt_chat_items` ( | ||||
|   `user_id` int NOT NULL COMMENT '用户 ID', | ||||
|   `role_id` int NOT NULL COMMENT '角色 ID', | ||||
|   `title` varchar(100) NOT NULL COMMENT '会话标题', | ||||
|   `model` varchar(30) NOT NULL COMMENT '会话使用的 AI 模型', | ||||
|   `model_id` int NOT NULL DEFAULT '0' COMMENT '模型 ID', | ||||
|   `created_at` datetime NOT NULL COMMENT '创建时间', | ||||
|   `updated_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` | ||||
| -- | ||||
| @@ -93,7 +124,7 @@ CREATE TABLE `chatgpt_chat_roles` ( | ||||
|   `hello_msg` varchar(255) NOT NULL COMMENT '打招呼信息', | ||||
|   `icon` varchar(255) NOT NULL COMMENT '角色图标', | ||||
|   `enable` tinyint(1) NOT NULL COMMENT '是否被启用', | ||||
|   `sort` smallint 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='聊天角色表'; | ||||
| @@ -102,24 +133,24 @@ CREATE TABLE `chatgpt_chat_roles` ( | ||||
| -- 转存表中的数据 `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'); | ||||
| 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'); | ||||
| 
 | ||||
| -- -------------------------------------------------------- | ||||
| 
 | ||||
| @@ -139,8 +170,28 @@ 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\":\"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 任务表'; | ||||
| 
 | ||||
| -- -------------------------------------------------------- | ||||
| 
 | ||||
| @@ -169,14 +220,13 @@ CREATE TABLE `chatgpt_rewards` ( | ||||
| 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 '手机号码', | ||||
|   `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 '头像', | ||||
|   `salt` char(12) NOT NULL COMMENT '密码盐', | ||||
|   `tokens` bigint NOT NULL DEFAULT '0' COMMENT '剩余 tokens', | ||||
|   `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', | ||||
| @@ -191,8 +241,8 @@ CREATE TABLE `chatgpt_users` ( | ||||
| -- 转存表中的数据 `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'); | ||||
| 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'); | ||||
| 
 | ||||
| -- -------------------------------------------------------- | ||||
| 
 | ||||
| @@ -211,25 +261,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, '找到属于你的幸福!💑💛'); | ||||
| 
 | ||||
| -- | ||||
| -- 转储表的索引 | ||||
| -- | ||||
| @@ -255,6 +286,12 @@ 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` | ||||
| -- | ||||
| @@ -269,6 +306,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 +333,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 | ||||
| -- | ||||
| @@ -316,6 +355,12 @@ ALTER TABLE `chatgpt_chat_history` | ||||
| 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` | ||||
| -- | ||||
| @@ -328,6 +373,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 +389,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=87; | ||||
| 
 | ||||
| -- | ||||
| -- 使用表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 */; | ||||
							
								
								
									
										474
									
								
								database/chatgpt_plus-v3.1.3.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,474 @@ | ||||
| -- phpMyAdmin SQL Dump | ||||
| -- version 5.1.3 | ||||
| -- https://www.phpmyadmin.net/ | ||||
| -- | ||||
| -- 主机: localhost | ||||
| -- 生成日期: 2023-09-20 10:38:57 | ||||
| -- 服务器版本: 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\":\"ChatPlus AI 智能助手\",\"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', | ||||
|   `type` varchar(20) DEFAULT 'image' COMMENT '任务类别', | ||||
|   `message_id` char(40) NOT NULL COMMENT '消息 ID', | ||||
|   `reference_id` char(40) DEFAULT NULL COMMENT '引用消息 ID', | ||||
|   `prompt` varchar(2000) NOT NULL COMMENT '会话提示词', | ||||
|   `img_url` varchar(255) DEFAULT NULL COMMENT '图片URL', | ||||
|   `hash` varchar(100) DEFAULT NULL COMMENT 'message hash', | ||||
|   `progress` smallint DEFAULT '0' COMMENT '任务进度', | ||||
|   `created_at` datetime NOT NULL | ||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='MidJourney 任务表'; | ||||
|  | ||||
| -- | ||||
| -- 转存表中的数据 `chatgpt_mj_jobs` | ||||
| -- | ||||
|  | ||||
| INSERT INTO `chatgpt_mj_jobs` (`id`, `user_id`, `type`, `message_id`, `reference_id`, `prompt`, `img_url`, `hash`, `progress`, `created_at`) VALUES | ||||
| (2, 4, 'image', '1152392223497924658', '', 'chrysanthemum, van gogh style --v 5.2 --s 750 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1694822120588799.png', '54689110-4701-4eed-b308-1acaf1c5efa1', 100, '2023-09-16 07:55:25'), | ||||
| (3, 4, 'upscale', '1152392353139658894', '1152392223497924658', 'chrysanthemum, van gogh style --v 5.2 --s 750 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1694822151252833.png', '15f3d9e6-9bd2-4e31-81ce-c8b126509b12', 100, '2023-09-16 07:55:55'), | ||||
| (4, 4, 'image', '1152393378244349953', '', 'A vintage girl wearing a cheongsam walking on the streets of 1990s Shanghai, China --v 5.2 --s 750 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1694822395949588.png', '319f953e-9ba4-4c8f-bd0c-87444a44b4a6', 100, '2023-09-16 08:00:00'), | ||||
| (5, 4, 'image', '1152394661802033162', '', 'A medieval girl wearing a cheongsam walking on the streets of 1990s Shanghai, China --ar 9:16 --v 5.2 --s 750 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1694822701966804.png', '7e522b23-2e88-40e2-814f-b9ad7ce1777b', 100, '2023-09-16 08:05:06'), | ||||
| (7, 4, 'image', '1152395485248753734', '', 'A Chinese girl wearing a blue cheongsam walking in the alleys of 1990s Shanghai --ar 16:9 --v 5.2 --s 750 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1694822897511213.png', '036aad69-f231-4b5c-b537-c45bd2027b05', 100, '2023-09-16 08:08:22'), | ||||
| (8, 4, 'image', '1152395938078392370', '', 'A Chinese girl wearing a cheongsam walking on a street paved with blue bricks --v 5.2 --s 750 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1694823005955584.png', 'd6336496-3115-47b7-bcf8-3bebda1d15ec', 100, '2023-09-16 08:10:10'), | ||||
| (10, 4, 'image', '1152395938078392371', '', 'A Chinese girl wearing a cheongsam walking on a street paved with blue bricks --v 5.2 --s 750 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1694823005955584.png', 'd6336496-3115-47b7-bcf8-3bebda1d15ec', 100, '2023-09-16 08:10:10'), | ||||
| (13, 4, 'upscale', '1152818976640991323', '1152395158298558464', 'A Chinese girl wearing a qipao walking on the streets of Shanghai in the 1990s --ar 2:3 --v 5.2 --s 750 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1694923865202276.png', '21965cd6-ef70-4b36-8d92-7b201dea77ff', 100, '2023-09-17 12:07:37'), | ||||
| (16, 4, 'image', '1152828784144298066', '', 'A tiger biting a crocodile --v 5.2 --s 750 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1694926203790426.png', '423b4a4f-9607-4dd1-a45b-a1a50be667e7', 100, '2023-09-17 12:50:11'), | ||||
| (18, 4, 'upscale', '1152831490305036359', '1152395158298558464', 'A Chinese girl wearing a qipao walking on the streets of Shanghai in the 1990s --ar 2:3 --v 5.2 --s 750 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1694926848947477.png', '21965cd6-ef70-4b36-8d92-7b201dea77ff', 100, '2023-09-17 13:00:32'), | ||||
| (19, 4, 'image', '1152395158298558464', '', 'A Chinese girl wearing a qipao walking on the streets of Shanghai in the 1990s --ar 2:3 --v 5.2 --s 750 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1694933593639122.png', '21965cd6-ef70-4b36-8d92-7b201dea77ff', 100, '2023-09-17 14:49:53'), | ||||
| (20, 4, 'image', '1152861794356953098', '', 'A huge digital screen with binary code of 0 and 1 --v 5.2 --s 750 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1694934140188981.png', '7b6a6910-0aa7-4eda-8580-ff882118c428', 100, '2023-09-17 15:02:38'), | ||||
| (21, 4, 'image', '1152862460366307349', '', 'A bear riding a yellow bicycle --v 5.2 --s 750 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1694934237667586.png', '0f95fd58-0385-438c-ac96-0ecaa57bfcd9', 100, '2023-09-17 15:04:06'), | ||||
| (22, 4, 'image', '1152865215201935502', '', 'The protagonist of the novel is Huang Rong. She is beautiful and charming, with a fairy-like temperament. She is wearing a white dress with a golden hairband, and her eyes are lively and divine. --s 750 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1694934895270654.png', '64531b1a-a335-498f-8c10-3eb70b5e2c3d', 100, '2023-09-17 15:15:02'), | ||||
| (23, 4, 'upscale', '1152865506961924108', '1152865215201935502', 'The protagonist of the novel is Huang Rong. She is beautiful and charming, with a fairy-like temperament. She is wearing a white dress with a golden hairband, and her eyes are lively and divine. --s 750 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1694934961735253.png', '64531b1a-a335-498f-8c10-3eb70b5e2c3d', 100, '2023-09-17 15:15:51'), | ||||
| (24, 4, 'upscale', '1152866160300265622', '1152861794356953098', 'A huge digital screen with binary code of 0 and 1 --v 5.2 --s 750 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1694935117008516.png', '7b6a6910-0aa7-4eda-8580-ff882118c428', 100, '2023-09-17 15:18:27'), | ||||
| (25, 4, 'upscale', '1152866236972154930', '1152862460366307349', 'A bear riding a yellow bicycle --v 5.2 --s 750 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1694935135247347.png', '0f95fd58-0385-438c-ac96-0ecaa57bfcd9', 100, '2023-09-17 15:18:39'), | ||||
| (26, 4, 'variation', '1152866487053324442', '1152395158298558464', 'A Chinese girl wearing a qipao walking on the streets of Shanghai in the 1990s --ar 2:3 --v 5.2 --s 750 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1694935197826267.png', '21965cd6-ef70-4b36-8d92-7b201dea77ff', 100, '2023-09-17 15:19:06'), | ||||
| (27, 4, 'image', '1152867045902397440', '', 'Draw a cyberpunk-style cat --s 750 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1694935330386877.png', '382ff1ce-57c2-45b0-b7fd-9820552171c3', 100, '2023-09-17 15:22:17'), | ||||
| (28, 4, 'upscale', '1152867208754634832', '1152867045902397440', 'Draw a cyberpunk-style cat --s 750 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1694935369578615.png', '382ff1ce-57c2-45b0-b7fd-9820552171c3', 100, '2023-09-17 15:22:37'), | ||||
| (35, 4, 'image', '1152893484747989063', '', 'Ma painting of a young girl in green sitting at a pond, in the style of Liu ye, traditional animation, cinematic lighting, book sculptures --ar 16:9 --s 300 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1694941638240646.png', 'd1dbf12e-9c93-4166-8c1e-8d24e4db2599', 100, '2023-09-17 17:06:26'), | ||||
| (36, 4, 'image', '1152894264607510578', '', 'Ma painting of a young girl in green sitting at a pond, in the style of Liu ye, traditional animation, cinematic lighting, book sculptures,looking at view --ar 16:9 --s 400 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1694941821446354.png', 'f9c96ec7-d684-47fb-9741-585b7a706091', 100, '2023-09-17 17:09:34'), | ||||
| (37, 4, 'upscale', '1152894426373431376', '1152894264607510578', 'Ma painting of a young girl in green sitting at a pond, in the style of Liu ye, traditional animation, cinematic lighting, book sculptures,looking at view --ar 16:9 --s 400 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1694941855758163.png', 'fc442093-d3a4-4dbf-ad2f-94d7c1b559fd', 100, '2023-09-17 17:10:46'), | ||||
| (38, 4, 'image', '1152894777944186920', '', 'A realistic beautiful natural landscape with hyper-detailed features --ar 9:16 --s 300 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1694941943692523.png', '7b43690e-56fc-471d-a955-c07317725ffc', 100, '2023-09-17 17:11:37'), | ||||
| (39, 4, 'image', '1152895012967813170', '', 'Beautiful girl, concept art, 8k intricate details, fairytale style --ar 9:16 --s 300 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1694942000095024.png', '179a4c49-d611-4d94-b5e6-8f1bee4f6168', 100, '2023-09-17 17:12:17'), | ||||
| (41, 4, 'image', '1152895765878943775', '', 'Beautiful chinese girl, concept art, 8k intricate details, fairytale style --ar 1:1 --s 300 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1694942195668005.png', 'c016641e-bc49-49fd-9a63-959851ae28e3', 100, '2023-09-17 17:15:29'), | ||||
| (42, 4, 'upscale', '1152896131739693056', '1152895765878943775', 'Beautiful chinese girl, concept art, 8k intricate details, fairytale style --ar 1:1 --s 300 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1694942263210895.png', '58ef8980-d09c-4b46-8f88-cbe3a609af07', 100, '2023-09-17 17:17:28'), | ||||
| (43, 4, 'upscale', '1152897653525463081', '1152867045902397440', 'Draw a cyberpunk-style cat --s 750 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1694942624549914.png', 'a1718c78-cced-4406-95cd-26a29cc69492', 100, '2023-09-17 17:23:36'), | ||||
| (44, 4, 'image', '1152900424832122880', '', 'A futuristic space captain with insane detail in a sci-fi cyberpunk panel concept inspired by Simon Stålenhag, Syd Mead, Ash Thorp, and KYZA. --ar 8:5 --v 5.2 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1694943287793108.png', '2cd10cd1-42fb-448f-b37d-9daff42e40c3', 100, '2023-09-17 17:34:02'), | ||||
| (45, 4, 'image', '1152901430592684092', '', 'Hulk holding Thor\'s hammer --ar 1:1 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1694943527996383.png', '18ee6c0d-d6e6-463c-83ad-9dc333ece5e4', 100, '2023-09-17 17:38:03'), | ||||
| (46, 4, 'image', '1152902324155596883', '', '<https://s.mj.run/qqeetJULXtc> 中国古装美女 --ar 16:9 --iw 0.700000 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1694943740306883.png', 'bf751a4f-1ada-498b-a783-75cba063d32a', 100, '2023-09-17 17:41:24'), | ||||
| (47, 4, 'image', '1152902857092255834', '', '<https://s.mj.run/qqeetJULXtc> Beautiful chinese girl, concept art, 8k intricate details, fairytale style --ar 16:9 --iw 0.250000 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1694943867599176.png', '84ba00c4-1d4d-4ddb-a42d-c760ee947fc3', 100, '2023-09-17 17:43:34'), | ||||
| (48, 4, 'image', '1152904587544645642', '', '<https://s.mj.run/qqeetJULXtc> a beautiful chinese girl --ar 16:9 --iw 0.250000 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1694944279770713.png', 'dace5c8e-d6fa-4d0d-8c0b-3d531e92fc71', 100, '2023-09-17 17:50:27'), | ||||
| (49, 4, 'upscale', '1152904665877446677', '1152902324155596883', '<https://s.mj.run/qqeetJULXtc> 中国古装美女 --ar 16:9 --iw 0.700000 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1694944296169482.png', 'e44c60c0-de59-4566-b000-612840e48624', 100, '2023-09-17 17:50:36'), | ||||
| (50, 4, 'upscale', '1152904720265003008', '1152901430592684092', 'Hulk holding Thor\'s hammer --ar 1:1 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1694944309948873.png', '56fd0a93-b6a3-4f4e-a783-b45d2a273984', 100, '2023-09-17 17:50:48'), | ||||
| (51, 4, 'upscale', '1152904833410547812', '1152904587544645642', '<https://s.mj.run/qqeetJULXtc> a beautiful chinese girl --ar 16:9 --iw 0.250000 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1694944337034950.png', '47f1d9dc-ab57-43db-8db0-048457b039a4', 100, '2023-09-17 17:52:08'), | ||||
| (54, 4, 'image', '1153595449643642891', '', 'A Minion wearing the clothes of Spiderman --ar 1:1 --v 5.2 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1695108991035705.png', 'acf4e646-05ca-4338-8197-1ce6ea57021e', 100, '2023-09-19 15:35:50'), | ||||
| (55, 4, 'image', '1153595903869980683', '', 'A Minion wearing the clothes of Spiderman,cyberpenk style --ar 1:1 --v 5.2 --style raw', 'http://nk.img.r9it.com/chatgpt-plus/1695109099106595.png', '779db1cd-669f-42dc-9504-ef903804b540', 100, '2023-09-19 15:37:27'), | ||||
| (57, 4, 'image', '1153598938423771157', '', 'A Minion wearing the clothes of Spiderman,cyberpunk style --ar 1:1 --niji 5', 'http://nk.img.r9it.com/chatgpt-plus/1695109822512284.png', 'ee550da3-04ea-4303-9218-f76d7fc1db90', 100, '2023-09-19 15:49:02'), | ||||
| (58, 4, 'image', '1153600706629095427', '', 'Hulk holding Thor\'s hammer --ar 1:1 --s 300 --niji 5', 'http://nk.img.r9it.com/chatgpt-plus/1695110244240899.png', 'cc2a7237-afa1-4f89-98d9-001dbb940fe1', 100, '2023-09-19 15:56:36'), | ||||
| (59, 4, 'upscale', '1153601879633301534', '1153600706629095427', 'Hulk holding Thor\'s hammer --ar 1:1 --s 300 --niji 5', 'http://nk.img.r9it.com/chatgpt-plus/1695110526175028.png', '2d65a126-0491-4a45-a5b1-1fa514df210c', 100, '2023-09-19 16:01:58'), | ||||
| (60, 4, 'upscale', '1153601976731455588', '1153598938423771157', 'A Minion wearing the clothes of Spiderman,cyberpunk style --ar 1:1 --niji 5', 'http://nk.img.r9it.com/chatgpt-plus/1695110551199363.png', 'c2b38c15-4198-464d-8ac9-6d99ddea9ec2', 100, '2023-09-19 16:02:21'), | ||||
| (62, 4, 'image', '1153856671089688596', '', 'Japanese Miyazaki Hayao style art of a white kitten looking ahead, the kitten\'s ears are black, the tail is black, the sky is blue with a few white clouds, Stones on the river bank, a few grasses on a short step, 8k studio ghibli art --ar 1:1 --v 5.2 --s 750', 'http://nk.img.r9it.com/chatgpt-plus/1695171271616739.png', '1e73c287-c531-4056-9127-f9a0513472a5', 100, '2023-09-20 08:53:54'), | ||||
| (63, 4, 'upscale', '1153857198544396308', '1153856671089688596', 'Japanese Miyazaki Hayao style art of a white kitten looking ahead, the kitten\'s ears are black, the tail is black, the sky is blue with a few white clouds, Stones on the river bank, a few grasses on a short step, 8k studio ghibli art --ar 1:1 --v 5.2 --s 750', 'http://nk.img.r9it.com/chatgpt-plus/1695171408120669.png', '25cc05ed-98cb-4e9c-81af-025abea981ce', 100, '2023-09-20 08:56:29'), | ||||
| (64, 4, 'upscale', '1153859005874196580', '1153856671089688596', 'Japanese Miyazaki Hayao style art of a white kitten looking ahead, the kitten\'s ears are black, the tail is black, the sky is blue with a few white clouds, Stones on the river bank, a few grasses on a short step, 8k studio ghibli art --ar 1:1 --v 5.2 --s 750', 'http://nk.img.r9it.com/chatgpt-plus/1695171825823440.png', 'a4cc01dd-80e9-443f-922e-6e9eebf36228', 100, '2023-09-20 09:03:41'), | ||||
| (65, 4, 'image', '1153859390328283137', '', 'Ultra realistic full body photo of an fashionable model walking the runway modeling summer designer novelty interesting complex unusual street sporty classy modern dolman sleeve outfit brave, rome positano beachy details cutouts and and gold tropical pattern structured tailored jacket sea beach starfish shell palm --ar 9:16 --v 5.2 --s 750', 'http://nk.img.r9it.com/chatgpt-plus/1695171926930040.png', 'd84c9558-c297-42e0-a5ea-18e7c0115b36', 100, '2023-09-20 09:04:37'), | ||||
| (66, 4, 'upscale', '1153860027321434143', '1153859390328283137', 'Ultra realistic full body photo of an fashionable model walking the runway modeling summer designer novelty interesting complex unusual street sporty classy modern dolman sleeve outfit brave, rome positano beachy details cutouts and and gold tropical pattern structured tailored jacket sea beach starfish shell palm --ar 9:16 --v 5.2 --s 750', 'http://nk.img.r9it.com/chatgpt-plus/1695172069378545.png', '00065a41-940b-449f-a05e-5590df1209c7', 100, '2023-09-20 09:07:36'), | ||||
| (67, 4, 'image', '1153861318269476944', '', 'Ultra realistic full body photo of an Chinese fashionable model walking the runway modeling summer designer novelty interesting complex unusual street sporty classy modern dolman sleeve outfit brave, rome positano beachy details cutouts and and gold tropical pattern structured tailored jacket sea beach starfish shell palm --ar 9:16 --v 5.2 --s 750', 'http://nk.img.r9it.com/chatgpt-plus/1695172377816320.png', 'fd6216a0-b00a-4b04-8daa-ea790ee2d07e', 100, '2023-09-20 09:12:16'), | ||||
| (68, 4, 'image', '1153861710931841097', '', 'Black Yamaha cruiser with an old man on it cruising down the coast, 8k masterpiece, perfect lighting, stunning details, shadow play, detailed hues --ar 1:1 --v 5.2 --s 750', 'http://nk.img.r9it.com/chatgpt-plus/1695172473475643.png', 'cb866a5c-9aa8-4b73-ac88-12c28b28cdd2', 100, '2023-09-20 09:13:56'), | ||||
| (69, 4, 'upscale', '1153861811922292877', '1153861318269476944', 'Ultra realistic full body photo of an Chinese fashionable model walking the runway modeling summer designer novelty interesting complex unusual street sporty classy modern dolman sleeve outfit brave, rome positano beachy details cutouts and and gold tropical pattern structured tailored jacket sea beach starfish shell palm --ar 9:16 --v 5.2 --s 750', 'http://nk.img.r9it.com/chatgpt-plus/1695172494907096.png', 'a36f0b9b-bdf4-4c7a-a20f-94c4ec2b1fa4', 100, '2023-09-20 09:14:28'), | ||||
| (70, 4, 'upscale', '1153861843975164007', '1153861710931841097', 'Black Yamaha cruiser with an old man on it cruising down the coast, 8k masterpiece, perfect lighting, stunning details, shadow play, detailed hues --ar 1:1 --v 5.2 --s 750', 'http://nk.img.r9it.com/chatgpt-plus/1695172508674444.png', 'd014e59f-d3c0-4534-ae7b-673081cd85ee', 100, '2023-09-20 09:14:54'), | ||||
| (71, 4, 'image', '1153864528820441150', '', 'studio ghibli\'s, castle in sky --ar 1:1 --v 5.2 --s 750', 'http://nk.img.r9it.com/chatgpt-plus/1695173143286485.png', 'ee80001d-5aba-47a1-bca4-0e648bd39ee8', 100, '2023-09-20 09:25:02'), | ||||
| (72, 4, 'image', '1153864871922913330', '', 'studio ghibli\'s, castle in sky --ar 1:1 --s 500 --style raw --v 5.2', 'http://nk.img.r9it.com/chatgpt-plus/1695173225095241.png', '789fdd68-8ccd-4be8-b34d-ac4e2e28c659', 100, '2023-09-20 09:26:25'), | ||||
| (73, 4, 'upscale', '1153864931985342525', '1153864528820441150', 'studio ghibli\'s, castle in sky --ar 1:1 --v 5.2 --s 750', 'http://nk.img.r9it.com/chatgpt-plus/1695173239968177.png', 'd9f3b642-8c88-442e-8eff-baeaf32cfd95', 100, '2023-09-20 09:26:39'), | ||||
| (74, 4, 'upscale', '1153864972284203038', '1153598938423771157', 'A Minion wearing the clothes of Spiderman,cyberpunk style --ar 1:1 --niji 5', 'http://nk.img.r9it.com/chatgpt-plus/1695173249582718.png', '67952ff1-fa64-4b54-ba5f-420b37480412', 100, '2023-09-20 09:26:51'); | ||||
|  | ||||
| -- -------------------------------------------------------- | ||||
|  | ||||
| -- | ||||
| -- 表的结构 `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_rewards` | ||||
| -- | ||||
|  | ||||
| INSERT INTO `chatgpt_rewards` (`id`, `user_id`, `tx_id`, `amount`, `remark`, `status`, `created_at`, `updated_at`) VALUES | ||||
| (1, 4, '10001071012023072001002934114618', '1.00', '打赏', 1, '2023-07-20 17:38:23', '2023-07-22 08:12:09'), | ||||
| (2, 4, '10000499012023072001225918328537', '0.50', '✨寻常小日,最为珍贵', 1, '2023-07-20 17:39:19', '2023-07-22 08:32:34'), | ||||
| (3, 4, 'q7jt5khp313vivccm6vwijt5', '123.45', '打赏你的', 1, '2023-08-10 17:05:06', '2023-09-06 17:07:23'), | ||||
| (4, 4, '123456789', '145.33', '充值100元', 1, '2023-08-10 17:09:35', '2023-09-06 17:06:57'), | ||||
| (5, 4, '1234567890', '168.33', '充值200元', 1, '2023-08-10 17:10:04', '2023-09-06 17:06:14'); | ||||
|  | ||||
| -- -------------------------------------------------------- | ||||
|  | ||||
| -- | ||||
| -- 表的结构 `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://img.r9it.com/chatgpt-plus/1693981355719469.png', 'ueedue5l', 29347, 5132, 63, 1727857836, 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\"]', 1694412538, '::1', '2023-06-12 16:47:17', '2023-09-11 14:08:59'); | ||||
|  | ||||
| -- -------------------------------------------------------- | ||||
|  | ||||
| -- | ||||
| -- 表的结构 `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 KEY `message_id` (`message_id`); | ||||
|  | ||||
| -- | ||||
| -- 表的索引 `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=75; | ||||
|  | ||||
| -- | ||||
| -- 使用表AUTO_INCREMENT `chatgpt_rewards` | ||||
| -- | ||||
| ALTER TABLE `chatgpt_rewards` | ||||
|   MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=6; | ||||
|  | ||||
| -- | ||||
| -- 使用表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 */; | ||||
							
								
								
									
										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`; | ||||
							
								
								
									
										12
									
								
								database/update-v3.1.3.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,12 @@ | ||||
| ALTER TABLE `chatgpt_mj_jobs` DROP `image`; | ||||
| ALTER TABLE `chatgpt_mj_jobs` DROP `hash`; | ||||
| ALTER TABLE `chatgpt_mj_jobs` DROP `content`; | ||||
| ALTER TABLE `chatgpt_mj_jobs` DROP `chat_id`; | ||||
| ALTER TABLE `chatgpt_mj_jobs` ADD `progress` SMALLINT(5) NULL DEFAULT '0' COMMENT '任务进度' AFTER `prompt`; | ||||
| ALTER TABLE `chatgpt_mj_jobs` ADD `hash` VARCHAR(100) NULL DEFAULT NULL COMMENT 'message hash' AFTER `prompt`; | ||||
| ALTER TABLE `chatgpt_mj_jobs` ADD `img_url` VARCHAR(255) NULL DEFAULT NULL COMMENT '图片URL' AFTER `prompt`; | ||||
|  | ||||
| -- 2023-09-15 | ||||
| ALTER TABLE `chatgpt_mj_jobs` ADD `type` VARCHAR(20) NULL DEFAULT 'image' COMMENT '任务类别' AFTER `user_id`; | ||||
| ALTER TABLE `chatgpt_mj_jobs` DROP INDEX `message_id`; | ||||
| ALTER TABLE `chatgpt_mj_jobs` ADD INDEX(`message_id`); | ||||
							
								
								
									
										2
									
								
								desktop/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,2 @@ | ||||
| node_modules | ||||
| dist | ||||
							
								
								
									
										
											BIN
										
									
								
								desktop/icons/logo.icns
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								desktop/icons/logo.ico
									
									
									
									
									
										Executable file
									
								
							
							
						
						| After Width: | Height: | Size: 26 KiB | 
							
								
								
									
										
											BIN
										
									
								
								desktop/icons/logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 30 KiB | 
							
								
								
									
										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
									
								
							
							
						
						| @@ -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
									
								
							
							
						
						| @@ -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,15 +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-hangzhou.aliyuncs.com/geekmaster/chatgpt-plus-vue:$version | ||||
|   docker push registry.cn-hangzhou.aliyuncs.com/geekmaster/chatgpt-plus-go:$version | ||||
|   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 | ||||
| @@ -1,54 +1,54 @@ | ||||
| Listen = "0.0.0.0:5678" | ||||
| ProxyURL = "http://172.22.11.200:7777" | ||||
| MysqlDns = "root:mysql_pass@tcp(localhost:3306)/chatgpt_plus?charset=utf8mb4&collation=utf8mb4_unicode_ci&parseTime=True&loc=Local" | ||||
| StaticDir = "./static" | ||||
| StaticUrl = "http://localhost:5678/static" | ||||
| AesEncryptKey = "{YOUR_AES_KEY}" | ||||
| ProxyURL = "" # 如 http://127.0.0.1:7777 | ||||
| MysqlDns = "root:12345678@tcp(172.22.11.200:3307)/chatgpt_plus?charset=utf8&parseTime=True&loc=Local" | ||||
| StaticDir = "./static" # 静态资源的目录 | ||||
| StaticUrl = "/static" # 静态资源访问 URL | ||||
| AesEncryptKey = "" | ||||
|  | ||||
| [Session] | ||||
|   Driver = "cookie" | ||||
|   SecretKey = "m0cjm3gsuw9jk73np1ni7r42koilybjcndlycjdmq7za3pbqn7w12fyok5pqh6q5" | ||||
|   Name = "CHAT_SESSION_ID" | ||||
|   Path = "/" | ||||
|   Domain = "localhost" | ||||
|   SecretKey = "azyehq3ivunjhbntz78isj00i4hz2mt9xtddysfucxakadq4qbfrt0b7q3lnvg80" # 注意:这个是 JWT Token 授权密钥,生产环境请务必更换 | ||||
|   MaxAge = 86400 | ||||
|   Secure = false | ||||
|   HttpOnly = false | ||||
|   SameSite = 2 | ||||
|  | ||||
| [Manager] | ||||
|   Username = "admin" | ||||
|   Password = "admin123" | ||||
|   Password = "admin123" # 如果是生产环境的话,这里管理员的密码记得修改 | ||||
|  | ||||
| [Redis] | ||||
| [Redis] # redis 配置信息 | ||||
|   Host = "localhost" | ||||
|   Port = 6379 | ||||
|   Password = "" | ||||
|   DB = 0 | ||||
|  | ||||
| [ApiConfig] | ||||
|   ApiURL = "{URL}" | ||||
|   AppId = "{APP_ID}" | ||||
|   Token = "{TOKEN}" | ||||
| [ApiConfig] # 微博热搜,今日头条等函数服务 API 配置,此为第三方插件服务,如需使用请联系作者开通 | ||||
|   ApiURL = "" | ||||
|   AppId = "" | ||||
|   Token = "" | ||||
|  | ||||
| [SmsConfig] | ||||
|   AccessKey = "{YOUR_ACCESS_KEY}" | ||||
|   AccessSecret = "{YOUR_SECRET_KEY}" | ||||
| [SmsConfig] # 阿里云短信服务配置 | ||||
|   AccessKey = "" | ||||
|   AccessSecret = "" | ||||
|   Product = "Dysmsapi" | ||||
|   Domain = "dysmsapi.aliyuncs.com" | ||||
|  | ||||
| [ExtConfig] | ||||
|   ApiURL = "插件扩展 API 地址" | ||||
|   Token = "插件扩展 API Token" | ||||
| [ExtConfig] # MidJourney和微信机器人服务 API 配置,开通此功能需要配合 chatpgt-plus-exts 项目部署 | ||||
|   ApiURL = "" # 插件扩展 API 地址 | ||||
|   Token = "" # 这个 token 随便填,只要确保跟 chatgpt-plus-exts 项目的 token 一样就行 | ||||
|  | ||||
| [OSS] | ||||
|    Active = "local" | ||||
| [OSS] # OSS 配置,用于存储 MJ 绘画图片 | ||||
|    Active = "local" # 默认使用本地文件存储引擎 | ||||
|    [OSS.Local] | ||||
|      BasePath = "./static/upload" | ||||
|      BaseURL = "http://localhost:5678/static/upload" | ||||
|      BasePath = "./static/upload" # 本地文件上传根路径 | ||||
|      BaseURL = "http://localhost:5678/static/upload" # 本地上传文件根 URL 如果是线上,则直接设置为 /static/upload 即可 | ||||
|    [OSS.Minio] | ||||
|      Endpoint = "IP:端口" | ||||
|      AccessKey = "minio oss access key" | ||||
|      AccessSecret = "minio oss access secret" | ||||
|      Bucket = "chatgpt-plus" | ||||
|      Endpoint = "" # 如 172.22.11.200:9000 | ||||
|      AccessKey = "" # 自己去 Minio 控制台去创建一个 Access Key | ||||
|      AccessSecret = "" | ||||
|      Bucket = "chatgpt-plus" # 替换为你自己创建的 Bucket,注意要给 Bucket 设置公开的读权限,否则会出现图片无法显示。 | ||||
|      UseSSL = false | ||||
|      Domain = "minio 文件公开访问地址" | ||||
|      Domain = "" # 地址必须是能够通过公网访问的,否则会出现图片无法显示。 | ||||
|    [OSS.QiNiu] # 七牛云 OSS 配置 | ||||
|        Zone = "z2" # 区域,z0:华东,z1: 华北,na0:北美,as0:新加坡 | ||||
|        AccessKey = "" | ||||
|        AccessSecret = "" | ||||
|        Bucket = "" | ||||
|        Domain = "" # OSS Bucket 所绑定的域名,如 https://img.r9it.com | ||||
| @@ -1,11 +1,12 @@ | ||||
| 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.3 | ||||
|     container_name: chatgpt-plus-api | ||||
|     restart: always | ||||
|     environment: | ||||
|       - DEBUG=false | ||||
|       - LOG_LEVEL=info | ||||
|       - CONFIG_FILE=config.toml | ||||
|     ports: | ||||
| @@ -13,12 +14,13 @@ services: | ||||
|     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.3 | ||||
|     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"] | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								docs/imgs/mj_image.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.2 MiB | 
							
								
								
									
										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
									
									
								
							
							
						
						| @@ -8,5 +8,4 @@ lerna-debug.log* | ||||
| node_modules | ||||
| dist | ||||
| dist.tar.gz | ||||
| .env.development | ||||
|  | ||||
|   | ||||
| Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 5.8 KiB | 
							
								
								
									
										
											BIN
										
									
								
								web/public/images/mj-niji.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 476 KiB | 
							
								
								
									
										
											BIN
										
									
								
								web/public/images/mj-normal.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 526 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"> | ||||
| @@ -35,7 +34,7 @@ html, body { | ||||
|     margin 0; | ||||
|  | ||||
|     .el-dialog__body { | ||||
|       //max-height 1440px | ||||
|       max-height 90vh | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										301
									
								
								web/src/assets/css/chat-plus.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,301 @@ | ||||
| #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 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; | ||||
| } | ||||
							
								
								
									
										397
									
								
								web/src/assets/css/chat-plus.styl
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,397 @@ | ||||
| $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; | ||||
|         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; | ||||
|           //background-color #343540 | ||||
|  | ||||
|           .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; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										264
									
								
								web/src/assets/css/image-mj.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,264 @@ | ||||
| .page-mj { | ||||
|   background-color: #282c34; | ||||
| } | ||||
| .page-mj .inner { | ||||
|   display: flex; | ||||
| /* 修改滚动条的颜色 */ | ||||
| /* 修改滚动条轨道的背景颜色 */ | ||||
| /* 修改滚动条的滑块颜色 */ | ||||
| /* 修改滚动条的滑块的悬停颜色 */ | ||||
| } | ||||
| .page-mj .inner .mj-box { | ||||
|   margin: 10px; | ||||
|   background-color: #262626; | ||||
|   border: 1px solid #454545; | ||||
|   min-width: 300px; | ||||
|   max-width: 300px; | ||||
|   padding: 10px; | ||||
|   border-radius: 10px; | ||||
|   color: #fff; | ||||
|   font-size: 14px; | ||||
| } | ||||
| .page-mj .inner .mj-box h2 { | ||||
|   font-weight: bold; | ||||
|   font-size: 20px; | ||||
|   text-align: center; | ||||
|   color: #47fff1; | ||||
| } | ||||
| .page-mj .inner .mj-box ::-webkit-scrollbar { | ||||
|   width: 0; | ||||
|   height: 0; | ||||
|   background-color: transparent; | ||||
| } | ||||
| .page-mj .inner .mj-box .mj-params { | ||||
|   margin-top: 10px; | ||||
|   overflow: auto; | ||||
| } | ||||
| .page-mj .inner .mj-box .mj-params .param-line { | ||||
|   padding: 0 10px; | ||||
| } | ||||
| .page-mj .inner .mj-box .mj-params .param-line .el-icon { | ||||
|   position: relative; | ||||
|   top: 3px; | ||||
| } | ||||
| .page-mj .inner .mj-box .mj-params .param-line .grid-content { | ||||
|   background-color: #383838; | ||||
|   border-radius: 5px; | ||||
|   padding: 8px 14px; | ||||
|   display: flex; | ||||
|   cursor: pointer; | ||||
| } | ||||
| .page-mj .inner .mj-box .mj-params .param-line .grid-content:hover { | ||||
|   background-color: #585858; | ||||
| } | ||||
| .page-mj .inner .mj-box .mj-params .param-line .grid-content .shape { | ||||
|   width: 16px; | ||||
|   height: 16px; | ||||
|   margin-right: 5px; | ||||
|   border: 1px solid #c4c4c4; | ||||
|   border-radius: 3px; | ||||
| } | ||||
| .page-mj .inner .mj-box .mj-params .param-line .grid-content .shape.vertical { | ||||
|   width: 12px; | ||||
|   height: 20px; | ||||
| } | ||||
| .page-mj .inner .mj-box .mj-params .param-line .grid-content .shape.horizontal { | ||||
|   height: 12px; | ||||
|   width: 20px; | ||||
|   position: relative; | ||||
|   top: 3px; | ||||
| } | ||||
| .page-mj .inner .mj-box .mj-params .param-line .grid-content.active { | ||||
|   color: #47fff1; | ||||
|   background-color: #585858; | ||||
| } | ||||
| .page-mj .inner .mj-box .mj-params .param-line .grid-content.active .shape { | ||||
|   border: 1px solid #47fff1; | ||||
| } | ||||
| .page-mj .inner .mj-box .mj-params .param-line .model { | ||||
|   background-color: #383838; | ||||
|   border: 1px solid #454545; | ||||
|   border-radius: 5px; | ||||
|   padding: 10px; | ||||
|   display: flex; | ||||
|   flex-flow: column; | ||||
|   align-items: center; | ||||
|   cursor: pointer; | ||||
| } | ||||
| .page-mj .inner .mj-box .mj-params .param-line .model:hover { | ||||
|   background-color: #585858; | ||||
| } | ||||
| .page-mj .inner .mj-box .mj-params .param-line .model .el-image { | ||||
|   height: 60px; | ||||
|   width: 100%; | ||||
| } | ||||
| .page-mj .inner .mj-box .mj-params .param-line .model .text { | ||||
|   margin-top: 6px; | ||||
| } | ||||
| .page-mj .inner .mj-box .mj-params .param-line .model.active { | ||||
|   color: #47fff1; | ||||
|   background-color: #585858; | ||||
|   border: 1px solid #47fff1; | ||||
| } | ||||
| .page-mj .inner .mj-box .mj-params .param-line .form-item-inner { | ||||
|   display: flex; | ||||
| } | ||||
| .page-mj .inner .mj-box .mj-params .param-line .form-item-inner .el-icon { | ||||
|   margin-left: 10px; | ||||
|   margin-top: 2px; | ||||
| } | ||||
| .page-mj .inner .mj-box .mj-params .param-line .img-uploader .el-upload { | ||||
|   border: 1px dashed var(--el-border-color); | ||||
|   border-radius: 6px; | ||||
|   cursor: pointer; | ||||
|   position: relative; | ||||
|   overflow: hidden; | ||||
|   width: 100%; | ||||
|   transition: var(--el-transition-duration-fast); | ||||
| } | ||||
| .page-mj .inner .mj-box .mj-params .param-line .img-uploader .el-upload:hover { | ||||
|   border-color: var(--el-color-primary); | ||||
| } | ||||
| .page-mj .inner .mj-box .mj-params .param-line .img-uploader .el-upload .el-icon.uploader-icon { | ||||
|   font-size: 28px; | ||||
|   color: #8c939d; | ||||
|   width: 100%; | ||||
|   height: 120px; | ||||
|   text-align: center; | ||||
| } | ||||
| .page-mj .inner .mj-box .mj-params .param-line.pt { | ||||
|   padding-top: 5px; | ||||
|   padding-bottom: 5px; | ||||
| } | ||||
| .page-mj .inner .mj-box .submit-btn { | ||||
|   padding: 10px 15px 0 15px; | ||||
|   text-align: center; | ||||
| } | ||||
| .page-mj .inner .mj-box .submit-btn .el-button { | ||||
|   width: 100%; | ||||
| } | ||||
| .page-mj .inner .mj-box .submit-btn .el-button span { | ||||
|   color: #2d3a4b; | ||||
| } | ||||
| .page-mj .inner .el-form .el-form-item__label { | ||||
|   color: #fff; | ||||
| } | ||||
| .page-mj .inner .el-form .el-input, | ||||
| .page-mj .inner .el-form .el-slider { | ||||
|   width: 180px; | ||||
| } | ||||
| .page-mj .inner ::-webkit-scrollbar { | ||||
|   width: 10px; /* 滚动条宽度 */ | ||||
| } | ||||
| .page-mj .inner ::-webkit-scrollbar-track { | ||||
|   background-color: #282c34; | ||||
| } | ||||
| .page-mj .inner ::-webkit-scrollbar-thumb { | ||||
|   background-color: #444; | ||||
|   border-radius: 10px; | ||||
| } | ||||
| .page-mj .inner ::-webkit-scrollbar-thumb:hover { | ||||
|   background-color: #666; | ||||
| } | ||||
| .page-mj .inner .task-list-box { | ||||
|   width: 100%; | ||||
|   padding: 10px; | ||||
|   color: #fff; | ||||
|   overflow-x: hidden; | ||||
| } | ||||
| .page-mj .inner .task-list-box .running-job-list .job-item { | ||||
|   width: 100%; | ||||
|   padding: 2px; | ||||
|   background-color: #555; | ||||
| } | ||||
| .page-mj .inner .task-list-box .running-job-list .job-item .job-item-inner { | ||||
|   position: relative; | ||||
|   height: 100%; | ||||
|   overflow: hidden; | ||||
| } | ||||
| .page-mj .inner .task-list-box .running-job-list .job-item .job-item-inner .progress { | ||||
|   position: absolute; | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
|   top: 0; | ||||
|   left: 0; | ||||
|   display: flex; | ||||
|   justify-content: center; | ||||
|   align-items: center; | ||||
| } | ||||
| .page-mj .inner .task-list-box .running-job-list .job-item .job-item-inner .progress span { | ||||
|   font-size: 20px; | ||||
|   color: #fff; | ||||
| } | ||||
| .page-mj .inner .task-list-box .finish-job-list .job-item { | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
| } | ||||
| .page-mj .inner .task-list-box .finish-job-list .job-item .opt .opt-line { | ||||
|   margin: 6px 0; | ||||
| } | ||||
| .page-mj .inner .task-list-box .finish-job-list .job-item .opt .opt-line ul { | ||||
|   display: flex; | ||||
|   flex-flow: row; | ||||
| } | ||||
| .page-mj .inner .task-list-box .finish-job-list .job-item .opt .opt-line ul li { | ||||
|   margin-right: 10px; | ||||
| } | ||||
| .page-mj .inner .task-list-box .finish-job-list .job-item .opt .opt-line ul li a { | ||||
|   padding: 3px 0; | ||||
|   width: 44px; | ||||
|   text-align: center; | ||||
|   border-radius: 5px; | ||||
|   display: block; | ||||
|   cursor: pointer; | ||||
|   background-color: #4e5058; | ||||
|   color: #fff; | ||||
| } | ||||
| .page-mj .inner .task-list-box .finish-job-list .job-item .opt .opt-line ul li a:hover { | ||||
|   background-color: #6d6f78; | ||||
| } | ||||
| .page-mj .inner .task-list-box .finish-job-list .job-item .opt .opt-line ul .show-prompt { | ||||
|   font-size: 20px; | ||||
|   cursor: pointer; | ||||
| } | ||||
| .page-mj .inner .task-list-box .el-image { | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
|   max-height: 240px; | ||||
| } | ||||
| .page-mj .inner .task-list-box .el-image img { | ||||
|   height: 240px; | ||||
| } | ||||
| .page-mj .inner .task-list-box .el-image .el-image-viewer__wrapper img { | ||||
|   width: auto; | ||||
|   height: auto; | ||||
| } | ||||
| .page-mj .inner .task-list-box .el-image .image-slot { | ||||
|   display: flex; | ||||
|   flex-flow: column; | ||||
|   justify-content: center; | ||||
|   align-items: center; | ||||
|   height: 100%; | ||||
|   min-height: 200px; | ||||
|   color: #fff; | ||||
| } | ||||
| .page-mj .inner .task-list-box .el-image .image-slot .iconfont { | ||||
|   font-size: 50px; | ||||
|   margin-bottom: 10px; | ||||
| } | ||||
| .page-mj .inner .task-list-box .el-image.upscale { | ||||
|   max-height: 304px; | ||||
| } | ||||
| .page-mj .inner .task-list-box .el-image.upscale img { | ||||
|   height: 304px; | ||||
| } | ||||
| .page-mj .inner .task-list-box .el-image.upscale .el-image-viewer__wrapper img { | ||||
|   width: auto; | ||||
|   height: auto; | ||||
| } | ||||
| .mj-list-item-prompt .el-icon { | ||||
|   margin-left: 10px; | ||||
|   cursor: pointer; | ||||
|   position: relative; | ||||
|   top: 2px; | ||||
| } | ||||
							
								
								
									
										350
									
								
								web/src/assets/css/image-mj.styl
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,350 @@ | ||||
| .page-mj { | ||||
|   background-color: #282c34; | ||||
|  | ||||
|   .inner { | ||||
|     display: flex; | ||||
|  | ||||
|     .mj-box { | ||||
|       margin 10px | ||||
|       background-color #262626 | ||||
|       border 1px solid #454545 | ||||
|       min-width 300px | ||||
|       max-width 300px | ||||
|       padding 10px | ||||
|       border-radius 10px | ||||
|       color #ffffff; | ||||
|       font-size 14px | ||||
|  | ||||
|       h2 { | ||||
|         font-weight: bold; | ||||
|         font-size 20px | ||||
|         text-align center | ||||
|         color #47fff1 | ||||
|       } | ||||
|  | ||||
|       // 隐藏滚动条 | ||||
|  | ||||
|       ::-webkit-scrollbar { | ||||
|         width: 0; | ||||
|         height: 0; | ||||
|         background-color: transparent; | ||||
|       } | ||||
|  | ||||
|       .mj-params { | ||||
|         margin-top 10px | ||||
|         overflow auto | ||||
|  | ||||
|  | ||||
|         .param-line { | ||||
|           padding 0 10px | ||||
|  | ||||
|           .el-icon { | ||||
|             position relative | ||||
|             top 3px | ||||
|           } | ||||
|  | ||||
|           .grid-content { | ||||
|             background-color #383838 | ||||
|             border-radius 5px | ||||
|             padding 8px 14px | ||||
|             display flex | ||||
|             cursor pointer | ||||
|  | ||||
|             &:hover { | ||||
|               background-color #585858 | ||||
|             } | ||||
|  | ||||
|             .shape { | ||||
|               width 16px | ||||
|               height 16px | ||||
|               margin-right 5px | ||||
|               border 1px solid #C4C4C4 | ||||
|               border-radius 3px | ||||
|             } | ||||
|  | ||||
|             .shape.vertical { | ||||
|               width 12px | ||||
|               height 20px | ||||
|             } | ||||
|  | ||||
|             .shape.horizontal { | ||||
|               height 12px | ||||
|               width 20px | ||||
|               position relative | ||||
|               top 3px | ||||
|             } | ||||
|           } | ||||
|  | ||||
|  | ||||
|           .grid-content.active { | ||||
|             color #47fff1 | ||||
|             background-color #585858 | ||||
|  | ||||
|             .shape { | ||||
|               border 1px solid #47fff1 | ||||
|             } | ||||
|           } | ||||
|  | ||||
|           .model { | ||||
|             background-color #383838 | ||||
|             border 1px solid #454545 | ||||
|             border-radius 5px | ||||
|             padding 10px | ||||
|             display flex | ||||
|             flex-flow column | ||||
|             align-items center | ||||
|             cursor pointer | ||||
|  | ||||
|             &:hover { | ||||
|               background-color #585858 | ||||
|             } | ||||
|  | ||||
|             .el-image { | ||||
|               height 60px | ||||
|               width 100% | ||||
|             } | ||||
|  | ||||
|             .text { | ||||
|               margin-top 6px | ||||
|             } | ||||
|  | ||||
|           } | ||||
|  | ||||
|           .model.active { | ||||
|             color #47fff1 | ||||
|             background-color #585858 | ||||
|             border 1px solid #47fff1 | ||||
|           } | ||||
|  | ||||
|           .form-item-inner { | ||||
|             display flex | ||||
|  | ||||
|             .el-icon { | ||||
|               margin-left 10px | ||||
|               margin-top 2px | ||||
|             } | ||||
|           } | ||||
|  | ||||
|  | ||||
|           .img-uploader { | ||||
|             .el-upload { | ||||
|               border: 1px dashed var(--el-border-color); | ||||
|               border-radius: 6px; | ||||
|               cursor: pointer; | ||||
|               position: relative; | ||||
|               overflow: hidden; | ||||
|               width 100% | ||||
|               transition: var(--el-transition-duration-fast); | ||||
|  | ||||
|               &:hover { | ||||
|                 border-color: var(--el-color-primary); | ||||
|               } | ||||
|  | ||||
|               .el-icon.uploader-icon { | ||||
|                 font-size: 28px | ||||
|                 color: #8c939d | ||||
|                 width 100% | ||||
|                 height: 120px | ||||
|                 text-align: center | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|  | ||||
|         } | ||||
|  | ||||
|         .param-line.pt { | ||||
|           padding-top 5px | ||||
|           padding-bottom 5px | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       .submit-btn { | ||||
|         padding 10px 15px 0 15px | ||||
|         text-align center | ||||
|  | ||||
|         .el-button { | ||||
|           width 100% | ||||
|  | ||||
|           span { | ||||
|             color #2D3A4B | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     .el-form { | ||||
|       .el-form-item__label { | ||||
|         color #ffffff | ||||
|       } | ||||
|  | ||||
|       .el-input, .el-slider { | ||||
|         width 180px | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     /* 修改滚动条的颜色 */ | ||||
|  | ||||
|     ::-webkit-scrollbar { | ||||
|       width: 10px; /* 滚动条宽度 */ | ||||
|     } | ||||
|  | ||||
|     /* 修改滚动条轨道的背景颜色 */ | ||||
|  | ||||
|     ::-webkit-scrollbar-track { | ||||
|       background-color: #282C34; | ||||
|     } | ||||
|  | ||||
|     /* 修改滚动条的滑块颜色 */ | ||||
|  | ||||
|     ::-webkit-scrollbar-thumb { | ||||
|       background-color: #444444; | ||||
|       border-radius 10px | ||||
|     } | ||||
|  | ||||
|     /* 修改滚动条的滑块的悬停颜色 */ | ||||
|  | ||||
|     ::-webkit-scrollbar-thumb:hover { | ||||
|       background-color: #666666; | ||||
|     } | ||||
|  | ||||
|     .task-list-box { | ||||
|       width 100% | ||||
|       padding 10px | ||||
|       color #ffffff | ||||
|       overflow-x hidden | ||||
|  | ||||
|       .running-job-list { | ||||
|         .job-item { | ||||
|           //border: 1px solid #454545; | ||||
|           width: 100%; | ||||
|           padding 2px | ||||
|           background-color #555555 | ||||
|  | ||||
|           .job-item-inner { | ||||
|             position relative | ||||
|             height 100% | ||||
|             overflow hidden | ||||
|  | ||||
|             .progress { | ||||
|               position absolute | ||||
|               width 100% | ||||
|               height 100% | ||||
|               top 0 | ||||
|               left 0 | ||||
|               display flex | ||||
|               justify-content center | ||||
|               align-items center | ||||
|  | ||||
|               span { | ||||
|                 font-size 20px | ||||
|                 color #ffffff | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|  | ||||
|  | ||||
|       .finish-job-list { | ||||
|         .job-item { | ||||
|           width 100% | ||||
|           height 100% | ||||
|  | ||||
|           .opt { | ||||
|             .opt-line { | ||||
|               margin 6px 0 | ||||
|  | ||||
|               ul { | ||||
|                 display flex | ||||
|                 flex-flow row | ||||
|  | ||||
|                 li { | ||||
|                   margin-right 10px | ||||
|  | ||||
|                   a { | ||||
|                     padding 3px 0 | ||||
|                     width 44px | ||||
|                     text-align center | ||||
|                     border-radius 5px | ||||
|                     display block | ||||
|                     cursor pointer | ||||
|                     background-color #4E5058 | ||||
|                     color #ffffff | ||||
|  | ||||
|                     &:hover { | ||||
|                       background-color #6D6F78 | ||||
|                     } | ||||
|                   } | ||||
|                 } | ||||
|  | ||||
|                 .show-prompt { | ||||
|                   font-size 20px | ||||
|                   cursor pointer | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|  | ||||
|         } | ||||
|  | ||||
|       } | ||||
|  | ||||
|       .el-image { | ||||
|         width 100% | ||||
|         height 100% | ||||
|         max-height 240px | ||||
|  | ||||
|         img { | ||||
|           height 240px | ||||
|         } | ||||
|  | ||||
|         .el-image-viewer__wrapper { | ||||
|           img { | ||||
|             width auto | ||||
|             height auto | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         .image-slot { | ||||
|           display flex | ||||
|           flex-flow column | ||||
|           justify-content center | ||||
|           align-items center | ||||
|           height 100% | ||||
|           min-height 200px | ||||
|           color #ffffff | ||||
|  | ||||
|           .iconfont { | ||||
|             font-size 50px | ||||
|             margin-bottom 10px | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       .el-image.upscale { | ||||
|         max-height 304px | ||||
|  | ||||
|         img { | ||||
|           height 304px | ||||
|         } | ||||
|  | ||||
|         .el-image-viewer__wrapper { | ||||
|           img { | ||||
|             width auto | ||||
|             height auto | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| .mj-list-item-prompt { | ||||
|   .el-icon { | ||||
|     margin-left 10px | ||||
|     cursor pointer | ||||
|     position relative | ||||
|     top 2px | ||||
|   } | ||||
| } | ||||
| @@ -1,7 +1,3 @@ | ||||
| * { | ||||
|   margin: 0; | ||||
|   padding: 0; | ||||
| } | ||||
| html, | ||||
| body, | ||||
| #app, | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| * { | ||||
|   margin: 0; | ||||
|   padding: 0; | ||||
| } | ||||
| //* { | ||||
| //  margin: 0; | ||||
| //  padding: 0; | ||||
| //} | ||||
|  | ||||
| html, | ||||
| body, | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| @font-face { | ||||
|   font-family: "iconfont"; /* Project id 4125778 */ | ||||
|   src: url('iconfont.woff2?t=1691463643989') format('woff2'), | ||||
|        url('iconfont.woff?t=1691463643989') format('woff'), | ||||
|        url('iconfont.ttf?t=1691463643989') format('truetype'); | ||||
|   src: url('iconfont.woff2?t=1694420182193') format('woff2'), | ||||
|        url('iconfont.woff?t=1694420182193') format('woff'), | ||||
|        url('iconfont.ttf?t=1694420182193') format('truetype'); | ||||
| } | ||||
|  | ||||
| .iconfont { | ||||
| @@ -13,6 +13,62 @@ | ||||
|   -moz-osx-font-smoothing: grayscale; | ||||
| } | ||||
|  | ||||
| .icon-book:before { | ||||
|   content: "\e622"; | ||||
| } | ||||
|  | ||||
| .icon-vip-user:before { | ||||
|   content: "\e605"; | ||||
| } | ||||
|  | ||||
| .icon-vip:before { | ||||
|   content: "\e688"; | ||||
| } | ||||
|  | ||||
| .icon-cart:before { | ||||
|   content: "\e603"; | ||||
| } | ||||
|  | ||||
| .icon-image-list:before { | ||||
|   content: "\e601"; | ||||
| } | ||||
|  | ||||
| .icon-share:before { | ||||
|   content: "\e63e"; | ||||
| } | ||||
|  | ||||
| .icon-palette:before { | ||||
|   content: "\e6da"; | ||||
| } | ||||
|  | ||||
| .icon-palette-pen:before { | ||||
|   content: "\e60c"; | ||||
| } | ||||
|  | ||||
| .icon-image:before { | ||||
|   content: "\e7de"; | ||||
| } | ||||
|  | ||||
| .icon-order:before { | ||||
|   content: "\e600"; | ||||
| } | ||||
|  | ||||
| .icon-service:before { | ||||
|   content: "\e62d"; | ||||
| } | ||||
|  | ||||
| .icon-like:before { | ||||
|   content: "\e640"; | ||||
| } | ||||
|  | ||||
| .icon-recharge:before { | ||||
|   content: "\e637"; | ||||
| } | ||||
|  | ||||
| .icon-model:before { | ||||
|   content: "\e867"; | ||||
| } | ||||
|  | ||||
| .icon-plugin:before { | ||||
|   content: "\e69d"; | ||||
| } | ||||
|   | ||||
| @@ -5,6 +5,104 @@ | ||||
|   "css_prefix_text": "icon-", | ||||
|   "description": "", | ||||
|   "glyphs": [ | ||||
|     { | ||||
|       "icon_id": "11836501", | ||||
|       "name": "知识库", | ||||
|       "font_class": "book", | ||||
|       "unicode": "e622", | ||||
|       "unicode_decimal": 58914 | ||||
|     }, | ||||
|     { | ||||
|       "icon_id": "1105", | ||||
|       "name": "认证用户", | ||||
|       "font_class": "vip-user", | ||||
|       "unicode": "e605", | ||||
|       "unicode_decimal": 58885 | ||||
|     }, | ||||
|     { | ||||
|       "icon_id": "1473442", | ||||
|       "name": "VIP", | ||||
|       "font_class": "vip", | ||||
|       "unicode": "e688", | ||||
|       "unicode_decimal": 59016 | ||||
|     }, | ||||
|     { | ||||
|       "icon_id": "1306", | ||||
|       "name": "购物车空", | ||||
|       "font_class": "cart", | ||||
|       "unicode": "e603", | ||||
|       "unicode_decimal": 58883 | ||||
|     }, | ||||
|     { | ||||
|       "icon_id": "1210", | ||||
|       "name": "多素材", | ||||
|       "font_class": "image-list", | ||||
|       "unicode": "e601", | ||||
|       "unicode_decimal": 58881 | ||||
|     }, | ||||
|     { | ||||
|       "icon_id": "6438267", | ||||
|       "name": "分享", | ||||
|       "font_class": "share", | ||||
|       "unicode": "e63e", | ||||
|       "unicode_decimal": 58942 | ||||
|     }, | ||||
|     { | ||||
|       "icon_id": "8361893", | ||||
|       "name": "绘画", | ||||
|       "font_class": "palette", | ||||
|       "unicode": "e6da", | ||||
|       "unicode_decimal": 59098 | ||||
|     }, | ||||
|     { | ||||
|       "icon_id": "17832791", | ||||
|       "name": "绘图、调色盘", | ||||
|       "font_class": "palette-pen", | ||||
|       "unicode": "e60c", | ||||
|       "unicode_decimal": 58892 | ||||
|     }, | ||||
|     { | ||||
|       "icon_id": "4766917", | ||||
|       "name": "image", | ||||
|       "font_class": "image", | ||||
|       "unicode": "e7de", | ||||
|       "unicode_decimal": 59358 | ||||
|     }, | ||||
|     { | ||||
|       "icon_id": "1375", | ||||
|       "name": "订单", | ||||
|       "font_class": "order", | ||||
|       "unicode": "e600", | ||||
|       "unicode_decimal": 58880 | ||||
|     }, | ||||
|     { | ||||
|       "icon_id": "6562297", | ||||
|       "name": "客服", | ||||
|       "font_class": "service", | ||||
|       "unicode": "e62d", | ||||
|       "unicode_decimal": 58925 | ||||
|     }, | ||||
|     { | ||||
|       "icon_id": "21598358", | ||||
|       "name": "喜欢", | ||||
|       "font_class": "like", | ||||
|       "unicode": "e640", | ||||
|       "unicode_decimal": 58944 | ||||
|     }, | ||||
|     { | ||||
|       "icon_id": "936873", | ||||
|       "name": "充值1", | ||||
|       "font_class": "recharge", | ||||
|       "unicode": "e637", | ||||
|       "unicode_decimal": 58935 | ||||
|     }, | ||||
|     { | ||||
|       "icon_id": "18991679", | ||||
|       "name": "model", | ||||
|       "font_class": "model", | ||||
|       "unicode": "e867", | ||||
|       "unicode_decimal": 59495 | ||||
|     }, | ||||
|     { | ||||
|       "icon_id": "5244045", | ||||
|       "name": "插件", | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| <template> | ||||
|   <el-dialog | ||||
|       v-model="showDialog" | ||||
|       :close-on-click-modal="false" | ||||
|       :show-close="mobile !== ''" | ||||
|       :close-on-click-modal="true" | ||||
|       style="max-width: 600px" | ||||
|       :before-close="close" | ||||
|       :title="title" | ||||
|   > | ||||
| @@ -16,14 +16,8 @@ | ||||
|           <el-input v-model="form.mobile"/> | ||||
|         </el-form-item> | ||||
|         <el-form-item label="手机验证码"> | ||||
|           <el-row :gutter="10"> | ||||
|             <el-col :span="12"> | ||||
|               <el-input v-model.number="form.code" maxlength="6"/> | ||||
|             </el-col> | ||||
|             <el-col :span="12"> | ||||
|               <send-msg size="" :mobile="form.mobile"/> | ||||
|             </el-col> | ||||
|           </el-row> | ||||
|           <el-input v-model.number="form.code" maxlength="6" style="max-width: 200px; margin-right: 10px;"/> | ||||
|           <send-msg size="" :mobile="form.mobile"/> | ||||
|         </el-form-item> | ||||
|       </el-form> | ||||
|     </div> | ||||
| @@ -64,10 +58,10 @@ const emits = defineEmits(['hide']); | ||||
|  | ||||
| const save = () => { | ||||
|   if (!validateMobile(form.value.mobile)) { | ||||
|     return ElMessage.error({message: "请输入正确的手机号码", appendTo: "#bind-mobile-form"}); | ||||
|     return ElMessage.error("请输入正确的手机号码"); | ||||
|   } | ||||
|   if (form.value.code === '') { | ||||
|     return ElMessage.error({message: "请输入短信验证码", appendTo: "#bind-mobile-form"}); | ||||
|     return ElMessage.error("请输入短信验证码"); | ||||
|   } | ||||
|  | ||||
|   httpPost('/api/user/bind/mobile', form.value).then(() => { | ||||
| @@ -78,7 +72,7 @@ const save = () => { | ||||
|       onClose: () => emits('hide', false) | ||||
|     }) | ||||
|   }).catch(e => { | ||||
|     ElMessage.error({message: "绑定失败:" + e.message, appendTo: "#bind-mobile-form"}); | ||||
|     ElMessage.error("绑定失败:" + e.message); | ||||
|   }) | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,257 +1,265 @@ | ||||
| <template> | ||||
|  | ||||
|     <div class="wg-cap-wrap" :style="{width: width}"> | ||||
|       <div class="wg-cap-wrap__header"> | ||||
|         <span>请在下图<em>依次</em>点击:</span> | ||||
|         <img class="wg-cap-wrap__thumb" v-if="thumbBase64Code" :src="thumbBase64Code" alt=" "> | ||||
|       </div> | ||||
|       <div class="wg-cap-wrap__body"> | ||||
|         <img class="wg-cap-wrap__picture" v-if="imageBase64Code" :src="imageBase64Code" alt=" " @click="handleClickPos($event)"> | ||||
|         <img class="wg-cap-wrap__loading" src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBzdHlsZT0ibWFyZ2luOiBhdXRvOyBiYWNrZ3JvdW5kOiByZ2JhKDI0MSwgMjQyLCAyNDMsIDApOyBkaXNwbGF5OiBibG9jazsgc2hhcGUtcmVuZGVyaW5nOiBhdXRvOyIgd2lkdGg9IjY0cHgiIGhlaWdodD0iNjRweCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaWRZTWlkIj4KICA8Y2lyY2xlIGN4PSI1MCIgY3k9IjM2LjgxMDEiIHI9IjEzIiBmaWxsPSIjM2U3Y2ZmIj4KICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9ImN5IiBkdXI9IjFzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgY2FsY01vZGU9InNwbGluZSIga2V5U3BsaW5lcz0iMC40NSAwIDAuOSAwLjU1OzAgMC40NSAwLjU1IDAuOSIga2V5VGltZXM9IjA7MC41OzEiIHZhbHVlcz0iMjM7Nzc7MjMiPjwvYW5pbWF0ZT4KICA8L2NpcmNsZT4KPC9zdmc+" alt="正在加载中..."> | ||||
|         <div v-for="(dot, key) in dots" :key="key" class="wg-cap-wrap__dot" :style="`top: ${dot.y}px; left:${dot.x}px;`"> | ||||
|           <span>{{ dot.index }}</span> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="wg-cap-wrap__footer"> | ||||
|         <div class="wg-cap-wrap__ico"> | ||||
|           <img @click="handleCloseEvent" | ||||
|                src="data:image/svg+xml;base64,PHN2ZyB0PSIxNjI2NjE0NDM5NDIzIiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9Ijg2NzUiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNNTEyIDIzLjI3MjcyN2MyNjkuOTE3MDkxIDAgNDg4LjcyNzI3MyAyMTguODEwMTgyIDQ4OC43MjcyNzMgNDg4LjcyNzI3M2E0ODYuNjMyNzI3IDQ4Ni42MzI3MjcgMCAwIDEtODQuOTQ1NDU1IDI3NS40MDk0NTUgNDYuNTQ1NDU1IDQ2LjU0NTQ1NSAwIDAgMS03Ni44NDY1NDUtNTIuNTI2NTQ2QTM5My41NDE4MTggMzkzLjU0MTgxOCAwIDAgMCA5MDcuNjM2MzY0IDUxMmMwLTIxOC41MDc2MzYtMTc3LjEyODcyNy0zOTUuNjM2MzY0LTM5NS42MzYzNjQtMzk1LjYzNjM2NFMxMTYuMzYzNjM2IDI5My40OTIzNjQgMTE2LjM2MzYzNiA1MTJzMTc3LjEyODcyNyAzOTUuNjM2MzY0IDM5NS42MzYzNjQgMzk1LjYzNjM2NGEzOTUuMTcwOTA5IDM5NS4xNzA5MDkgMCAwIDAgMTI1LjQ0LTIwLjI5MzgxOSA0Ni41NDU0NTUgNDYuNTQ1NDU1IDAgMCAxIDI5LjQ4NjU0NSA4OC4yOTY3MjhBNDg4LjI2MTgxOCA0ODguMjYxODE4IDAgMCAxIDUxMiAxMDAwLjcyNzI3M0MyNDIuMDgyOTA5IDEwMDAuNzI3MjczIDIzLjI3MjcyNyA3ODEuOTE3MDkxIDIzLjI3MjcyNyA1MTJTMjQyLjA4MjkwOSAyMy4yNzI3MjcgNTEyIDIzLjI3MjcyN3ogbS0xMTUuMiAzMDcuNzEyTDUxMiA0NDYuMTM4MTgybDExNS4yLTExNS4yYTQ2LjU0NTQ1NSA0Ni41NDU0NTUgMCAxIDEgNjUuODE1MjczIDY1Ljg2MTgxOEw1NzcuODYxODE4IDUxMmwxMTUuMiAxMTUuMmE0Ni41NDU0NTUgNDYuNTQ1NDU1IDAgMSAxLTY1Ljg2MTgxOCA2NS44MTUyNzNMNTEyIDU3Ny44NjE4MThsLTExNS4yIDExNS4yYTQ2LjU0NTQ1NSA0Ni41NDU0NTUgMCAxIDEtNjUuODE1MjczLTY1Ljg2MTgxOEw0NDYuMTM4MTgyIDUxMmwtMTE1LjItMTE1LjJhNDYuNTQ1NDU1IDQ2LjU0NTQ1NSAwIDEgMSA2NS44NjE4MTgtNjUuODE1MjczeiIgcC1pZD0iODY3NiIgZmlsbD0iIzcwNzA3MCI+PC9wYXRoPjwvc3ZnPg==" | ||||
|                alt="关闭"> | ||||
|           <img @click="handleRefreshEvent" | ||||
|                src="data:image/svg+xml;base64,PHN2ZyB0PSIxNjI2NjE0NDk5NjM4IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjEzNjAiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNMTg3LjQ1NiA0MjUuMDI0YTMzNiAzMzYgMCAwIDAgMzY4LjM4NCA0MjAuMjI0IDQ4IDQ4IDAgMCAxIDEyLjU0NCA5NS4xNjggNDMyIDQzMiAwIDAgMS00NzMuNjY0LTU0MC4xNmwtNTcuMjgtMTUuMzZhMTIuOCAxMi44IDAgMCAxLTYuMjcyLTIwLjkyOGwxNTkuMTY4LTE3OS40NTZhMTIuOCAxMi44IDAgMCAxIDIyLjE0NCA1Ljg4OGw0OC4wNjQgMjM1LjA3MmExMi44IDEyLjggMCAwIDEtMTUuODA4IDE0LjkxMmwtNTcuMjgtMTUuMzZ6TTgzNi40OCA1OTkuMDRhMzM2IDMzNiAwIDAgMC0zNjguMzg0LTQyMC4yMjQgNDggNDggMCAxIDEtMTIuNTQ0LTk1LjE2OCA0MzIgNDMyIDAgMCAxIDQ3My42NjQgNTQwLjE2bDU3LjI4IDE1LjM2YTEyLjggMTIuOCAwIDAgMSA2LjI3MiAyMC45MjhsLTE1OS4xNjggMTc5LjQ1NmExMi44IDEyLjggMCAwIDEtMjIuMTQ0LTUuODg4bC00OC4wNjQtMjM1LjA3MmExMi44IDEyLjggMCAwIDEgMTUuODA4LTE0LjkxMmw1Ny4yOCAxNS4zNnoiIGZpbGw9IiM3MDcwNzAiIHAtaWQ9IjEzNjEiPjwvcGF0aD48L3N2Zz4=" | ||||
|                alt="刷新"> | ||||
|         </div> | ||||
|         <div class="wg-cap-wrap__btn"> | ||||
|           <button @click="handleConfirmEvent">确认</button> | ||||
|         </div> | ||||
|   <div class="wg-cap-wrap" :style="{width: width}"> | ||||
|     <div class="wg-cap-wrap__header"> | ||||
|       <span>请在下图<em>依次</em>点击:</span> | ||||
|       <img class="wg-cap-wrap__thumb" v-if="thumbBase64Code" :src="thumbBase64Code" alt=" "> | ||||
|     </div> | ||||
|     <div class="wg-cap-wrap__body"> | ||||
|       <img class="wg-cap-wrap__picture" v-if="imageBase64Code" :src="imageBase64Code" alt=" " | ||||
|            @click="handleClickPos($event)"> | ||||
|       <img class="wg-cap-wrap__loading" | ||||
|            src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBzdHlsZT0ibWFyZ2luOiBhdXRvOyBiYWNrZ3JvdW5kOiByZ2JhKDI0MSwgMjQyLCAyNDMsIDApOyBkaXNwbGF5OiBibG9jazsgc2hhcGUtcmVuZGVyaW5nOiBhdXRvOyIgd2lkdGg9IjY0cHgiIGhlaWdodD0iNjRweCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaWRZTWlkIj4KICA8Y2lyY2xlIGN4PSI1MCIgY3k9IjM2LjgxMDEiIHI9IjEzIiBmaWxsPSIjM2U3Y2ZmIj4KICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9ImN5IiBkdXI9IjFzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgY2FsY01vZGU9InNwbGluZSIga2V5U3BsaW5lcz0iMC40NSAwIDAuOSAwLjU1OzAgMC40NSAwLjU1IDAuOSIga2V5VGltZXM9IjA7MC41OzEiIHZhbHVlcz0iMjM7Nzc7MjMiPjwvYW5pbWF0ZT4KICA8L2NpcmNsZT4KPC9zdmc+" | ||||
|            alt="正在加载中..."> | ||||
|       <div v-for="(dot, key) in dots" :key="key" class="wg-cap-wrap__dot" :style="`top: ${dot.y}px; left:${dot.x}px;`"> | ||||
|         <span>{{ dot.index }}</span> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div class="wg-cap-wrap__footer"> | ||||
|       <div class="wg-cap-wrap__ico"> | ||||
|         <img @click="handleCloseEvent" | ||||
|              src="data:image/svg+xml;base64,PHN2ZyB0PSIxNjI2NjE0NDM5NDIzIiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9Ijg2NzUiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNNTEyIDIzLjI3MjcyN2MyNjkuOTE3MDkxIDAgNDg4LjcyNzI3MyAyMTguODEwMTgyIDQ4OC43MjcyNzMgNDg4LjcyNzI3M2E0ODYuNjMyNzI3IDQ4Ni42MzI3MjcgMCAwIDEtODQuOTQ1NDU1IDI3NS40MDk0NTUgNDYuNTQ1NDU1IDQ2LjU0NTQ1NSAwIDAgMS03Ni44NDY1NDUtNTIuNTI2NTQ2QTM5My41NDE4MTggMzkzLjU0MTgxOCAwIDAgMCA5MDcuNjM2MzY0IDUxMmMwLTIxOC41MDc2MzYtMTc3LjEyODcyNy0zOTUuNjM2MzY0LTM5NS42MzYzNjQtMzk1LjYzNjM2NFMxMTYuMzYzNjM2IDI5My40OTIzNjQgMTE2LjM2MzYzNiA1MTJzMTc3LjEyODcyNyAzOTUuNjM2MzY0IDM5NS42MzYzNjQgMzk1LjYzNjM2NGEzOTUuMTcwOTA5IDM5NS4xNzA5MDkgMCAwIDAgMTI1LjQ0LTIwLjI5MzgxOSA0Ni41NDU0NTUgNDYuNTQ1NDU1IDAgMCAxIDI5LjQ4NjU0NSA4OC4yOTY3MjhBNDg4LjI2MTgxOCA0ODguMjYxODE4IDAgMCAxIDUxMiAxMDAwLjcyNzI3M0MyNDIuMDgyOTA5IDEwMDAuNzI3MjczIDIzLjI3MjcyNyA3ODEuOTE3MDkxIDIzLjI3MjcyNyA1MTJTMjQyLjA4MjkwOSAyMy4yNzI3MjcgNTEyIDIzLjI3MjcyN3ogbS0xMTUuMiAzMDcuNzEyTDUxMiA0NDYuMTM4MTgybDExNS4yLTExNS4yYTQ2LjU0NTQ1NSA0Ni41NDU0NTUgMCAxIDEgNjUuODE1MjczIDY1Ljg2MTgxOEw1NzcuODYxODE4IDUxMmwxMTUuMiAxMTUuMmE0Ni41NDU0NTUgNDYuNTQ1NDU1IDAgMSAxLTY1Ljg2MTgxOCA2NS44MTUyNzNMNTEyIDU3Ny44NjE4MThsLTExNS4yIDExNS4yYTQ2LjU0NTQ1NSA0Ni41NDU0NTUgMCAxIDEtNjUuODE1MjczLTY1Ljg2MTgxOEw0NDYuMTM4MTgyIDUxMmwtMTE1LjItMTE1LjJhNDYuNTQ1NDU1IDQ2LjU0NTQ1NSAwIDEgMSA2NS44NjE4MTgtNjUuODE1MjczeiIgcC1pZD0iODY3NiIgZmlsbD0iIzcwNzA3MCI+PC9wYXRoPjwvc3ZnPg==" | ||||
|              alt="关闭"> | ||||
|         <img @click="handleRefreshEvent" | ||||
|              src="data:image/svg+xml;base64,PHN2ZyB0PSIxNjI2NjE0NDk5NjM4IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjEzNjAiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNMTg3LjQ1NiA0MjUuMDI0YTMzNiAzMzYgMCAwIDAgMzY4LjM4NCA0MjAuMjI0IDQ4IDQ4IDAgMCAxIDEyLjU0NCA5NS4xNjggNDMyIDQzMiAwIDAgMS00NzMuNjY0LTU0MC4xNmwtNTcuMjgtMTUuMzZhMTIuOCAxMi44IDAgMCAxLTYuMjcyLTIwLjkyOGwxNTkuMTY4LTE3OS40NTZhMTIuOCAxMi44IDAgMCAxIDIyLjE0NCA1Ljg4OGw0OC4wNjQgMjM1LjA3MmExMi44IDEyLjggMCAwIDEtMTUuODA4IDE0LjkxMmwtNTcuMjgtMTUuMzZ6TTgzNi40OCA1OTkuMDRhMzM2IDMzNiAwIDAgMC0zNjguMzg0LTQyMC4yMjQgNDggNDggMCAxIDEtMTIuNTQ0LTk1LjE2OCA0MzIgNDMyIDAgMCAxIDQ3My42NjQgNTQwLjE2bDU3LjI4IDE1LjM2YTEyLjggMTIuOCAwIDAgMSA2LjI3MiAyMC45MjhsLTE1OS4xNjggMTc5LjQ1NmExMi44IDEyLjggMCAwIDEtMjIuMTQ0LTUuODg4bC00OC4wNjQtMjM1LjA3MmExMi44IDEyLjggMCAwIDEgMTUuODA4LTE0LjkxMmw1Ny4yOCAxNS4zNnoiIGZpbGw9IiM3MDcwNzAiIHAtaWQ9IjEzNjEiPjwvcGF0aD48L3N2Zz4=" | ||||
|              alt="刷新"> | ||||
|       </div> | ||||
|       <div class="wg-cap-wrap__btn"> | ||||
|         <el-button type="primary" @click="handleConfirmEvent">确认</el-button> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|  | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
|   export default { | ||||
|     name: 'CaptchaPlus', | ||||
|     mounted() { | ||||
| export default { | ||||
|   name: 'CaptchaPlus', | ||||
|   mounted() { | ||||
|     this.$emit('refresh') | ||||
|   }, | ||||
|   props: { | ||||
|     value: Boolean, | ||||
|     width: { | ||||
|       type: String, | ||||
|       default: '300px' | ||||
|     }, | ||||
|     calcPosType: { | ||||
|       type: String, | ||||
|       default: 'dom', | ||||
|       validator: value => ['dom', 'screen'].includes(value) | ||||
|     }, | ||||
|     maxDot: { | ||||
|       type: Number, | ||||
|       default: 5 | ||||
|       // validator: value => value > 10 | ||||
|     }, | ||||
|     imageBase64: String, | ||||
|     thumbBase64: String | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       dots: [], | ||||
|       imageBase64Code: '', | ||||
|       thumbBase64Code: '' | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
|     value() { | ||||
|       this.dots = [] | ||||
|       this.imageBase64Code = '' | ||||
|       this.thumbBase64Code = '' | ||||
|     }, | ||||
|     imageBase64(val) { | ||||
|       this.dots = [] | ||||
|       this.imageBase64Code = val | ||||
|     }, | ||||
|     thumbBase64(val) { | ||||
|       this.dots = [] | ||||
|       this.thumbBase64Code = val | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     /** | ||||
|      * @Description: 处理关闭事件 | ||||
|      */ | ||||
|     handleCloseEvent() { | ||||
|       this.$emit('close') | ||||
|       // this.dots = [] | ||||
|       // this.imageBase64Code = '' | ||||
|       // this.thumbBase64Code = '' | ||||
|     }, | ||||
|     /** | ||||
|      * @Description: 处理刷新事件 | ||||
|      */ | ||||
|     handleRefreshEvent() { | ||||
|       this.dots = [] | ||||
|       this.$emit('refresh') | ||||
|     }, | ||||
|     props: { | ||||
|       value: Boolean, | ||||
|       width: { | ||||
|         type: String, | ||||
|         default: '300px' | ||||
|       }, | ||||
|       calcPosType: { | ||||
|         type: String, | ||||
|         default: 'dom', | ||||
|         validator: value => ['dom', 'screen'].includes(value) | ||||
|       }, | ||||
|       maxDot: { | ||||
|         type: Number, | ||||
|         default: 5 | ||||
|         // validator: value => value > 10 | ||||
|       }, | ||||
|       imageBase64: String, | ||||
|       thumbBase64: String | ||||
|     /** | ||||
|      * @Description: 处理确认事件 | ||||
|      */ | ||||
|     handleConfirmEvent() { | ||||
|       this.$emit('confirm', this.dots) | ||||
|     }, | ||||
|     data() { | ||||
|     /** | ||||
|      * @Description: 处理dot | ||||
|      * @param ev | ||||
|      */ | ||||
|     handleClickPos(ev) { | ||||
|       if (this.dots.length >= this.maxDot) { | ||||
|         return | ||||
|       } | ||||
|       const e = ev || window.event | ||||
|       e.preventDefault() | ||||
|       const dom = e.currentTarget | ||||
|  | ||||
|       const {domX, domY} = this.getDomXY(dom) | ||||
|       // =============================================== | ||||
|       // @notice 如 getDomXY 不准确可尝试使用 calcLocationLeft 或 calcLocationTop | ||||
|       // const domX = this.calcLocationLeft(dom) | ||||
|       // const domY = this.calcLocationTop(dom) | ||||
|       // =============================================== | ||||
|       let mouseX = (navigator.vendor === 'Netscape') ? e.pageX : e.x + document.body.offsetTop | ||||
|       let mouseY = (navigator.vendor === 'Netscape') ? e.pageY : e.y + document.body.offsetTop | ||||
|  | ||||
|       if (this.calcPosType === 'screen') { | ||||
|         mouseX = (navigator.vendor === 'Netscape') ? e.clientX : e.x | ||||
|         mouseY = (navigator.vendor === 'Netscape') ? e.clientY : e.y | ||||
|       } | ||||
|  | ||||
|       // 计算点击的相对位置 | ||||
|       const xPos = mouseX - domX | ||||
|       const yPos = mouseY - domY | ||||
|  | ||||
|       // 转整形 | ||||
|       const xp = parseInt(xPos.toString()) | ||||
|       const yp = parseInt(yPos.toString()) | ||||
|  | ||||
|       // 减去点的一半 | ||||
|       this.dots.push({ | ||||
|         x: xp - 11, | ||||
|         y: yp - 11, | ||||
|         index: this.dots.length + 1 | ||||
|       }) | ||||
|       return false | ||||
|     }, | ||||
|     /** | ||||
|      * @Description: 找到元素的屏幕位置 | ||||
|      * @param el | ||||
|      */ | ||||
|     calcLocationLeft(el) { | ||||
|       let tmp = el.offsetLeft | ||||
|       let val = el.offsetParent | ||||
|       while (val != null) { | ||||
|         tmp += val.offsetLeft | ||||
|         val = val.offsetParent | ||||
|       } | ||||
|       return tmp | ||||
|     }, | ||||
|     /** | ||||
|      * @Description: 找到元素的屏幕位置 | ||||
|      * @param el | ||||
|      */ | ||||
|     calcLocationTop(el) { | ||||
|       let tmp = el.offsetTop | ||||
|       let val = el.offsetParent | ||||
|       while (val != null) { | ||||
|         tmp += val.offsetTop | ||||
|         val = val.offsetParent | ||||
|       } | ||||
|       return tmp | ||||
|     }, | ||||
|     /** | ||||
|      * @Description: 找到元素的屏幕位置 | ||||
|      * @param dom | ||||
|      */ | ||||
|     getDomXY(dom) { | ||||
|       let x = 0 | ||||
|       let y = 0 | ||||
|       if (dom.getBoundingClientRect) { | ||||
|         let box = dom.getBoundingClientRect(); | ||||
|         let D = document.documentElement; | ||||
|         x = box.left + Math.max(D.scrollLeft, document.body.scrollLeft) - D.clientLeft; | ||||
|         y = box.top + Math.max(D.scrollTop, document.body.scrollTop) - D.clientTop | ||||
|       } else { | ||||
|         while (dom !== document.body) { | ||||
|           x += dom.offsetLeft | ||||
|           y += dom.offsetTop | ||||
|           dom = dom.offsetParent | ||||
|         } | ||||
|       } | ||||
|       return { | ||||
|         dots: [], | ||||
|         imageBase64Code: '', | ||||
|         thumbBase64Code: '' | ||||
|       } | ||||
|     }, | ||||
|     watch: { | ||||
|       value() { | ||||
|         this.dots = [] | ||||
|         this.imageBase64Code = '' | ||||
|         this.thumbBase64Code = '' | ||||
|       }, | ||||
|       imageBase64(val) { | ||||
|         this.dots = [] | ||||
|         this.imageBase64Code = val | ||||
|       }, | ||||
|       thumbBase64(val) { | ||||
|         this.dots = [] | ||||
|         this.thumbBase64Code = val | ||||
|       } | ||||
|     }, | ||||
|     methods: { | ||||
|       /** | ||||
|        * @Description: 处理关闭事件 | ||||
|        */ | ||||
|       handleCloseEvent() { | ||||
|         this.$emit('close') | ||||
|         // this.dots = [] | ||||
|         // this.imageBase64Code = '' | ||||
|         // this.thumbBase64Code = '' | ||||
|       }, | ||||
|       /** | ||||
|        * @Description: 处理刷新事件 | ||||
|        */ | ||||
|       handleRefreshEvent() { | ||||
|         this.dots = [] | ||||
|         this.$emit('refresh') | ||||
|       }, | ||||
|       /** | ||||
|        * @Description: 处理确认事件 | ||||
|        */ | ||||
|       handleConfirmEvent() { | ||||
|         this.$emit('confirm', this.dots) | ||||
|       }, | ||||
|       /** | ||||
|        * @Description: 处理dot | ||||
|        * @param ev | ||||
|        */ | ||||
|       handleClickPos(ev) { | ||||
|         if (this.dots.length >= this.maxDot) { | ||||
|           return | ||||
|         } | ||||
|         const e = ev || window.event | ||||
|         e.preventDefault() | ||||
|         const dom = e.currentTarget | ||||
|  | ||||
|         const {domX, domY} = this.getDomXY(dom) | ||||
|         // =============================================== | ||||
|         // @notice 如 getDomXY 不准确可尝试使用 calcLocationLeft 或 calcLocationTop | ||||
|         // const domX = this.calcLocationLeft(dom) | ||||
|         // const domY = this.calcLocationTop(dom) | ||||
|         // =============================================== | ||||
|         let mouseX = (navigator.vendor === 'Netscape') ? e.pageX : e.x + document.body.offsetTop | ||||
|         let mouseY = (navigator.vendor === 'Netscape') ? e.pageY : e.y + document.body.offsetTop | ||||
|  | ||||
|         if (this.calcPosType === 'screen') { | ||||
|           mouseX = (navigator.vendor === 'Netscape') ? e.clientX : e.x | ||||
|           mouseY = (navigator.vendor === 'Netscape') ? e.clientY : e.y | ||||
|         } | ||||
|  | ||||
|         // 计算点击的相对位置 | ||||
|         const xPos = mouseX - domX | ||||
|         const yPos = mouseY - domY | ||||
|  | ||||
|         // 转整形 | ||||
|         const xp = parseInt(xPos.toString()) | ||||
|         const yp = parseInt(yPos.toString()) | ||||
|  | ||||
|         // 减去点的一半 | ||||
|         this.dots.push({ | ||||
|           x: xp - 11, | ||||
|           y: yp - 11, | ||||
|           index: this.dots.length + 1 | ||||
|         }) | ||||
|         return false | ||||
|       }, | ||||
|       /** | ||||
|        * @Description: 找到元素的屏幕位置 | ||||
|        * @param el | ||||
|        */ | ||||
|       calcLocationLeft(el) { | ||||
|         let tmp = el.offsetLeft | ||||
|         let val = el.offsetParent | ||||
|         while (val != null) { | ||||
|           tmp += val.offsetLeft | ||||
|           val = val.offsetParent | ||||
|         } | ||||
|         return tmp | ||||
|       }, | ||||
|       /** | ||||
|        * @Description: 找到元素的屏幕位置 | ||||
|        * @param el | ||||
|        */ | ||||
|       calcLocationTop(el) { | ||||
|         let tmp = el.offsetTop | ||||
|         let val = el.offsetParent | ||||
|         while (val != null) { | ||||
|           tmp += val.offsetTop | ||||
|           val = val.offsetParent | ||||
|         } | ||||
|         return tmp | ||||
|       }, | ||||
|       /** | ||||
|        * @Description: 找到元素的屏幕位置 | ||||
|        * @param dom | ||||
|        */ | ||||
|       getDomXY(dom){ | ||||
|         let x = 0 | ||||
|         let y = 0 | ||||
|         if (dom.getBoundingClientRect) { | ||||
|           let box = dom.getBoundingClientRect(); | ||||
|           let D = document.documentElement; | ||||
|           x = box.left + Math.max(D.scrollLeft, document.body.scrollLeft) - D.clientLeft; | ||||
|           y = box.top + Math.max(D.scrollTop, document.body.scrollTop) - D.clientTop | ||||
|         } | ||||
|         else{ | ||||
|           while (dom !== document.body) { | ||||
|             x += dom.offsetLeft | ||||
|             y += dom.offsetTop | ||||
|             dom = dom.offsetParent | ||||
|           } | ||||
|         } | ||||
|         return { | ||||
|           domX: x, | ||||
|           domY: y | ||||
|         } | ||||
|         domX: x, | ||||
|         domY: y | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style> | ||||
|   .wg-cap-wrap{ | ||||
|     background: #ffffff; | ||||
| <style scoped lang="stylus"> | ||||
| .wg-cap-wrap { | ||||
|   background: #ffffff; | ||||
|  | ||||
|     -webkit-border-radius: 10px; | ||||
|     -moz-border-radius: 10px; | ||||
|     border-radius: 10px; | ||||
|   -webkit-border-radius: 10px; | ||||
|   -moz-border-radius: 10px; | ||||
|   border-radius: 10px; | ||||
|  | ||||
|     -webkit-touch-callout: none; | ||||
|     -webkit-user-select: none; | ||||
|     -moz-user-select: none; | ||||
|     -ms-user-select: none; | ||||
|     user-select: none; | ||||
|   } | ||||
|   .wg-cap-wrap__header{ | ||||
|   -webkit-touch-callout: none; | ||||
|   -webkit-user-select: none; | ||||
|   -moz-user-select: none; | ||||
|   -ms-user-select: none; | ||||
|   user-select: none; | ||||
|  | ||||
|   .wg-cap-wrap__header { | ||||
|     height: 50px; | ||||
|     width: 100%; | ||||
|     font-size: 15px; | ||||
|  | ||||
|     display:-webkit-box; | ||||
|     display:-webkit-flex; | ||||
|     display:-ms-flexbox; | ||||
|     display:flex; | ||||
|     -webkit-box-align:center; | ||||
|     -webkit-align-items:center; | ||||
|     -ms-flex-align:center; | ||||
|     display: -webkit-box; | ||||
|     display: -webkit-flex; | ||||
|     display: -ms-flexbox; | ||||
|     display: flex; | ||||
|     -webkit-box-align: center; | ||||
|     -webkit-align-items: center; | ||||
|     -ms-flex-align: center; | ||||
|     align-items: center; | ||||
|   } | ||||
|   .wg-cap-wrap__header span{ | ||||
|     padding-right: 5px; | ||||
|   } | ||||
|   .wg-cap-wrap__header span em{ | ||||
|     padding: 0 3px; | ||||
|     font-weight: bold; | ||||
|     color: #3e7cff; | ||||
|     font-style: normal; | ||||
|   } | ||||
|   .wg-cap-wrap__header .wg-cap-wrap__image{ | ||||
|     -webkit-border-radius: 5px; | ||||
|     -moz-border-radius: 5px; | ||||
|     border-radius: 5px; | ||||
|     overflow: hidden; | ||||
|     text-align: center; | ||||
|     line-height: 1; | ||||
|   } | ||||
|   .wg-cap-wrap__header .wg-cap-wrap__thumb{ | ||||
|     min-width: 150px; | ||||
|     text-align: center; | ||||
|     line-height: 1; | ||||
|     max-height: 100%; | ||||
|   } | ||||
|   .wg-cap-wrap__header .wg-cap-wrap__thumb.wg-cap-wrap__hidden{ | ||||
|     display: none; | ||||
|  | ||||
|     span { | ||||
|       padding-right: 5px; | ||||
|  | ||||
|       em { | ||||
|         padding: 0 3px; | ||||
|         font-weight: bold; | ||||
|         color: #3e7cff; | ||||
|         font-style: normal; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     .wg-cap-wrap__image { | ||||
|       -webkit-border-radius: 5px; | ||||
|       -moz-border-radius: 5px; | ||||
|       border-radius: 5px; | ||||
|       overflow: hidden; | ||||
|       text-align: center; | ||||
|       line-height: 1; | ||||
|     } | ||||
|  | ||||
|     .wg-cap-wrap__thumb { | ||||
|       min-width: 150px; | ||||
|       text-align: center; | ||||
|       line-height: 1; | ||||
|       max-height: 100%; | ||||
|     } | ||||
|  | ||||
|     .wg-cap-wrap__thumb.wg-cap-wrap__hidden { | ||||
|       display: none; | ||||
|     } | ||||
|  | ||||
|   } | ||||
|  | ||||
|   .wg-cap-wrap__body{ | ||||
|   .wg-cap-wrap__body { | ||||
|     position: relative; | ||||
|     display: -webkit-box; | ||||
|     display: -moz-box; | ||||
| @@ -264,106 +272,78 @@ | ||||
|     -moz-border-radius: 5px; | ||||
|     border-radius: 5px; | ||||
|     overflow: hidden; | ||||
|   } | ||||
|   .wg-cap-wrap__body .wg-cap-wrap__picture{ | ||||
|     position: relative; | ||||
|     z-index: 10; | ||||
|     width: 100%; | ||||
|  | ||||
|     /*height: 100%;*/ | ||||
|     /*max-width: 100%;*/ | ||||
|     /*max-height: 100%;*/ | ||||
|     /*object-fit: cover;*/ | ||||
|     /*text-align: center;*/ | ||||
|   } | ||||
|   .wg-cap-wrap__body .wg-cap-wrap__picture.wg-cap-wrap__hidden{ | ||||
|     display: none; | ||||
|   } | ||||
|   .wg-cap-wrap__body .wg-cap-wrap__loading{ | ||||
|     position: absolute; | ||||
|     z-index: 9; | ||||
|     top: 50%; | ||||
|     left: 50%; | ||||
|     width: 68px; | ||||
|     height: 68px; | ||||
|     margin-left: -34px; | ||||
|     margin-top: -34px; | ||||
|     line-height: 68px; | ||||
|     text-align: center; | ||||
|   } | ||||
|   .wg-cap-wrap__body .wg-cap-wrap__dot{ | ||||
|     position: absolute; | ||||
|     z-index: 10; | ||||
|     width: 22px; | ||||
|     height: 22px; | ||||
|     color: #cedffe; | ||||
|     background: #3e7cff; | ||||
|     border: 2px solid #f7f9fb; | ||||
|     line-height: 20px; | ||||
|     text-align: center; | ||||
|     -webkit-border-radius: 22px; | ||||
|     -moz-border-radius: 22px; | ||||
|     border-radius: 22px; | ||||
|     cursor: default; | ||||
|     .wg-cap-wrap__picture { | ||||
|       position: relative; | ||||
|       z-index: 10; | ||||
|       width: 100%; | ||||
|     } | ||||
|  | ||||
|     .wg-cap-wrap__picture.wg-cap-wrap__hidden { | ||||
|       display: none; | ||||
|     } | ||||
|  | ||||
|     .wg-cap-wrap__loading { | ||||
|       position: absolute; | ||||
|       z-index: 9; | ||||
|       top: 50%; | ||||
|       left: 50%; | ||||
|       width: 68px; | ||||
|       height: 68px; | ||||
|       margin-left: -34px; | ||||
|       margin-top: -34px; | ||||
|       line-height: 68px; | ||||
|       text-align: center; | ||||
|     } | ||||
|  | ||||
|     .wg-cap-wrap__dot { | ||||
|       position: absolute; | ||||
|       z-index: 10; | ||||
|       width: 22px; | ||||
|       height: 22px; | ||||
|       color: #cedffe; | ||||
|       background: #3e7cff; | ||||
|       border: 2px solid #f7f9fb; | ||||
|       line-height: 20px; | ||||
|       text-align: center; | ||||
|       -webkit-border-radius: 22px; | ||||
|       -moz-border-radius: 22px; | ||||
|       border-radius: 22px; | ||||
|       cursor: default; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .wg-cap-wrap__footer { | ||||
|     width: 100%; | ||||
|     height: 40px; | ||||
|     color: #34383e; | ||||
|     display:-webkit-box; | ||||
|     display:-webkit-flex; | ||||
|     display:-ms-flexbox; | ||||
|     display:flex; | ||||
|     -webkit-box-align:center; | ||||
|     -webkit-align-items:center; | ||||
|     -ms-flex-align:center; | ||||
|     display: -webkit-box; | ||||
|     display: -webkit-flex; | ||||
|     display: -ms-flexbox; | ||||
|     display: flex; | ||||
|     -webkit-box-align: center; | ||||
|     -webkit-align-items: center; | ||||
|     -ms-flex-align: center; | ||||
|     align-items: center; | ||||
|     padding-top: 15px; | ||||
|  | ||||
|     .wg-cap-wrap__ico { | ||||
|       flex: 1; | ||||
|  | ||||
|       img { | ||||
|         width: 24px; | ||||
|         height: 24px; | ||||
|         color: #34383e; | ||||
|         margin: 0 5px; | ||||
|         cursor: pointer; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     .wg-cap-wrap__btn { | ||||
|       display flex | ||||
|       width: 120px; | ||||
|       justify-content right | ||||
|     } | ||||
|   } | ||||
|   .wg-cap-wrap__footer .wg-cap-wrap__ico{ | ||||
|     flex: 1; | ||||
|   } | ||||
|   .wg-cap-wrap__footer .wg-cap-wrap__ico img{ | ||||
|     width: 24px; | ||||
|     height: 24px; | ||||
|     color: #34383e; | ||||
|     margin: 0 5px; | ||||
|     cursor: pointer; | ||||
|   } | ||||
|   .wg-cap-wrap__footer .wg-cap-wrap__btn{ | ||||
|     width: 120px; | ||||
|     height: 40px; | ||||
|   } | ||||
|   .wg-cap-wrap__footer .wg-cap-wrap__btn button{ | ||||
|     width: 100%; | ||||
|     height: 40px; | ||||
|     letter-spacing: 2px; | ||||
|     text-align: center; | ||||
|     padding: 9px 15px; | ||||
|     font-size: 15px; | ||||
|     -webkit-border-radius: 5px; | ||||
|     -moz-border-radius: 5px; | ||||
|     border-radius: 5px; | ||||
|     display: inline-block; | ||||
|     line-height: 1; | ||||
|     white-space: nowrap; | ||||
|     cursor: pointer; | ||||
|     color: #fff; | ||||
|     background-color: #409eff; | ||||
|     border: 1px solid #409eff; | ||||
|     -webkit-appearance: none; | ||||
|     box-sizing: border-box; | ||||
|     outline: none; | ||||
|     margin: 0; | ||||
|     transition: .1s; | ||||
|     font-weight: 500; | ||||
|     -moz-user-select: none; | ||||
|     -webkit-user-select: none; | ||||
|   } | ||||
|   .wg-cap-wrap__footer .wg-cap-wrap__btn button:hover { | ||||
|     background: #66b1ff; | ||||
|     border-color: #66b1ff; | ||||
|     color: #fff; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -10,8 +10,9 @@ | ||||
|           <div class="text" v-html="data.html"></div> | ||||
|           <div class="images" v-if="data.image?.url !== ''"> | ||||
|             <el-image :src="data.image?.url" | ||||
|                       :zoom-rate="1.0" | ||||
|                       :zoom-rate="1.2" | ||||
|                       :preview-src-list="[data.image?.url]" | ||||
|                       fit="cover" | ||||
|                       :initial-index="0" loading="lazy"> | ||||
|               <template #placeholder> | ||||
|                 <div class="image-slot" | ||||
| @@ -70,6 +71,8 @@ import {getSessionId} from "@/store/session"; | ||||
| const props = defineProps({ | ||||
|   content: Object, | ||||
|   icon: String, | ||||
|   chatId: String, | ||||
|   roleId: Number, | ||||
|   createdAt: String | ||||
| }); | ||||
|  | ||||
| @@ -106,11 +109,14 @@ const send = (url, index) => { | ||||
|   emits('disable-input') | ||||
|   httpPost(url, { | ||||
|     index: index, | ||||
|     src: "chat", | ||||
|     message_id: data.value?.["message_id"], | ||||
|     message_hash: data.value?.["image"]?.hash, | ||||
|     session_id: getSessionId(), | ||||
|     key: data.value?.["key"], | ||||
|     prompt: data.value?.["prompt"], | ||||
|     chat_id: props.chatId, | ||||
|     role_id: props.roleId, | ||||
|     icon: props.icon, | ||||
|   }).then(() => { | ||||
|     ElMessage.success("任务推送成功,请耐心等待任务执行...") | ||||
|     loading.value = false | ||||
| @@ -140,8 +146,8 @@ const send = (url, index) => { | ||||
|       margin-right 20px; | ||||
|  | ||||
|       img { | ||||
|         width: 30px; | ||||
|         height: 30px; | ||||
|         width: 36px; | ||||
|         height: 36px; | ||||
|         border-radius: 10px; | ||||
|         padding: 1px; | ||||
|       } | ||||
|   | ||||
| @@ -19,9 +19,8 @@ | ||||
|  | ||||
| <script> | ||||
| import {defineComponent} from "vue" | ||||
| import {dateFormat} from "@/utils/libs"; | ||||
| import {Clock} from "@element-plus/icons-vue"; | ||||
| import {httpGet} from "@/utils/http"; | ||||
| import {httpPost} from "@/utils/http"; | ||||
|  | ||||
| export default defineComponent({ | ||||
|   name: 'ChatPrompt', | ||||
| @@ -56,7 +55,7 @@ export default defineComponent({ | ||||
|   }, | ||||
|   mounted() { | ||||
|     if (!this.finalTokens) { | ||||
|       httpGet(`/api/chat/tokens?text=${this.content}&model=${this.model}`).then(res => { | ||||
|       httpPost("/api/chat/tokens", {text: this.content, model: this.model}).then(res => { | ||||
|         this.finalTokens = res.data; | ||||
|       }) | ||||
|     } | ||||
| @@ -83,8 +82,8 @@ export default defineComponent({ | ||||
|       margin-right 20px; | ||||
|  | ||||
|       img { | ||||
|         width: 30px; | ||||
|         height: 30px; | ||||
|         width: 36px; | ||||
|         height: 36px; | ||||
|         border-radius: 10px; | ||||
|         padding: 1px; | ||||
|       } | ||||
|   | ||||
| @@ -86,8 +86,8 @@ export default defineComponent({ | ||||
|         margin-right 20px; | ||||
|  | ||||
|         img { | ||||
|           width: 30px; | ||||
|           height: 30px; | ||||
|           width: 36px; | ||||
|           height: 36px; | ||||
|           border-radius: 10px; | ||||
|           padding: 1px; | ||||
|         } | ||||
|   | ||||