mirror of
				https://github.com/yangjian102621/geekai.git
				synced 2025-11-04 08:13:43 +08:00 
			
		
		
		
	Compare commits
	
		
			177 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					7fe4212684 | ||
| 
						 | 
					8bdda64794 | ||
| 
						 | 
					ec08c24dca | ||
| 
						 | 
					a992a5b3b3 | ||
| 
						 | 
					0f05970141 | ||
| 
						 | 
					e5e762efcd | ||
| 
						 | 
					b3d0c1ef9c | ||
| 
						 | 
					397078f7ff | ||
| 
						 | 
					3ad8065e20 | ||
| 
						 | 
					66c7717f04 | ||
| 
						 | 
					412f8ecc6c | ||
| 
						 | 
					51dcf642b3 | ||
| 
						 | 
					bfeea555b2 | ||
| 
						 | 
					479f94c372 | ||
| 
						 | 
					0140713e86 | ||
| 
						 | 
					15b2ec9721 | ||
| 
						 | 
					c9cd082855 | ||
| 
						 | 
					d7c002890c | ||
| 
						 | 
					348dd22279 | ||
| 
						 | 
					3e99b4cbf6 | ||
| 
						 | 
					6968da3ac7 | ||
| 
						 | 
					bf1c1b84c3 | ||
| 
						 | 
					c70314d930 | ||
| 
						 | 
					9104ca8e49 | ||
| 
						 | 
					2af33b3630 | ||
| 
						 | 
					654e795545 | ||
| 
						 | 
					c62ba2451e | ||
| 
						 | 
					d72d1b8a99 | ||
| 
						 | 
					b939d6016b | ||
| 
						 | 
					36a2626ccc | ||
| 
						 | 
					bd057a4cc9 | ||
| 
						 | 
					dc24a8c781 | ||
| 
						 | 
					59fa21779b | ||
| 
						 | 
					a140671aad | ||
| 
						 | 
					5fe8990fb4 | ||
| 
						 | 
					12799b7159 | ||
| 
						 | 
					9929746b1d | ||
| 
						 | 
					d70035ff0c | ||
| 
						 | 
					eec90274d8 | ||
| 
						 | 
					e8fff55c42 | ||
| 
						 | 
					3cf3cdd705 | ||
| 
						 | 
					9801fce659 | ||
| 
						 | 
					4c1f51110b | ||
| 
						 | 
					913d538587 | ||
| 
						 | 
					9e704365fc | ||
| 
						 | 
					485bdbc56a | ||
| 
						 | 
					7000168fd4 | ||
| 
						 | 
					5694f97a6b | ||
| 
						 | 
					b677d3fac7 | ||
| 
						 | 
					dc6719cf54 | ||
| 
						 | 
					7de5b55091 | ||
| 
						 | 
					76c5101092 | ||
| 
						 | 
					2f8d2f4854 | ||
| 
						 | 
					b1ee34ba0c | ||
| 
						 | 
					069ad6a09a | ||
| 
						 | 
					bf1403c818 | ||
| 
						 | 
					bcc622a24d | ||
| 
						 | 
					a06a81a415 | ||
| 
						 | 
					d1950acd01 | ||
| 
						 | 
					039b70eed2 | ||
| 
						 | 
					d8e4308b1b | ||
| 
						 | 
					434fbb3463 | ||
| 
						 | 
					de3eb8969c | ||
| 
						 | 
					fbd6eac877 | ||
| 
						 | 
					1fecab177b | ||
| 
						 | 
					b1b385c455 | ||
| 
						 | 
					3c6e86d04b | ||
| 
						 | 
					3d2035d08a | ||
| 
						 | 
					da86f916d8 | ||
| 
						 | 
					e7a07f7e92 | ||
| 
						 | 
					b01e6387fc | ||
| 
						 | 
					d86aca0f5d | ||
| 
						 | 
					09414fe36a | ||
| 
						 | 
					df0e7508db | ||
| 
						 | 
					92b1f01118 | ||
| 
						 | 
					8fb8bd932b | ||
| 
						 | 
					3f74b94784 | ||
| 
						 | 
					e9467341fa | ||
| 
						 | 
					131e051ddc | ||
| 
						 | 
					f626fe3166 | ||
| 
						 | 
					6bc57b6132 | ||
| 
						 | 
					d972e97c88 | ||
| 
						 | 
					3991f4daec | ||
| 
						 | 
					f6b567d6fc | ||
| 
						 | 
					8addba8203 | ||
| 
						 | 
					3ab930a107 | ||
| 
						 | 
					de512a5ea2 | ||
| 
						 | 
					113cfae2dc | ||
| 
						 | 
					33aebf9cb5 | ||
| 
						 | 
					6e58ddf681 | ||
| 
						 | 
					cae5c049e4 | ||
| 
						 | 
					ff76e4bd89 | ||
| 
						 | 
					a0a506a3c4 | ||
| 
						 | 
					aa5a4a9977 | ||
| 
						 | 
					abf4f061c1 | ||
| 
						 | 
					245cd3ee1a | ||
| 
						 | 
					45cb29d9a0 | ||
| 
						 | 
					d974b1ff0e | ||
| 
						 | 
					56269170cb | ||
| 
						 | 
					4290c4ca22 | ||
| 
						 | 
					7f7c8e831e | ||
| 
						 | 
					8f057ca9d1 | ||
| 
						 | 
					4a56621ec3 | ||
| 
						 | 
					a398e7a550 | ||
| 
						 | 
					96816c12ca | ||
| 
						 | 
					9984926f69 | ||
| 
						 | 
					a2a6081027 | ||
| 
						 | 
					5a10ed37a7 | ||
| 
						 | 
					1a9dd9de0b | ||
| 
						 | 
					0dae5bef71 | ||
| 
						 | 
					b4413ed726 | ||
| 
						 | 
					5e1fe88b8b | ||
| 
						 | 
					91ed41b536 | ||
| 
						 | 
					024c0032eb | ||
| 
						 | 
					4a9f7e3bce | ||
| 
						 | 
					cf4dcc34ec | ||
| 
						 | 
					4d612c15af | ||
| 
						 | 
					8aec87cc02 | ||
| 
						 | 
					442e411cde | ||
| 
						 | 
					acec0194de | ||
| 
						 | 
					8557f5b94a | ||
| 
						 | 
					babef8baae | ||
| 
						 | 
					efd4ab46f5 | ||
| 
						 | 
					ae8239e5de | ||
| 
						 | 
					f0994ba457 | ||
| 
						 | 
					dae91ed243 | ||
| 
						 | 
					de42a428e6 | ||
| 
						 | 
					63c7041e1f | ||
| 
						 | 
					b1263ddc69 | ||
| 
						 | 
					7e50e17aaf | ||
| 
						 | 
					a7265c4251 | ||
| 
						 | 
					6f39f639bd | ||
| 
						 | 
					a7db123437 | ||
| 
						 | 
					241c714a8b | ||
| 
						 | 
					67ac3cfe32 | ||
| 
						 | 
					c926e0afcc | ||
| 
						 | 
					5bc07e6d57 | ||
| 
						 | 
					c3666a9a71 | ||
| 
						 | 
					23b5ffa97d | ||
| 
						 | 
					a2c7a75705 | ||
| 
						 | 
					d68f2ef12c | ||
| 
						 | 
					67d30353f0 | ||
| 
						 | 
					4813163eac | ||
| 
						 | 
					5c5210625e | ||
| 
						 | 
					a4a1eec30b | ||
| 
						 | 
					d35164506a | ||
| 
						 | 
					1ed08f01ea | ||
| 
						 | 
					eca07ab830 | ||
| 
						 | 
					3512715704 | ||
| 
						 | 
					6d07881141 | ||
| 
						 | 
					251fe626f2 | ||
| 
						 | 
					5fee3a9288 | ||
| 
						 | 
					9b68d8101e | ||
| 
						 | 
					cfe6f27d48 | ||
| 
						 | 
					b314dd0900 | ||
| 
						 | 
					950fab6374 | ||
| 
						 | 
					9d1f5c42ce | ||
| 
						 | 
					a84046390b | ||
| 
						 | 
					aa29323a8a | ||
| 
						 | 
					d5617b7c3a | ||
| 
						 | 
					1ef60a9e5e | ||
| 
						 | 
					fb6e395ad8 | ||
| 
						 | 
					d9216060bc | ||
| 
						 | 
					bcaa9a92e5 | ||
| 
						 | 
					576adc9036 | ||
| 
						 | 
					00de18be9a | ||
| 
						 | 
					c61d32816a | ||
| 
						 | 
					f3fbb0b89c | ||
| 
						 | 
					e311a39632 | ||
| 
						 | 
					51407abe44 | ||
| 
						 | 
					8a470b1038 | ||
| 
						 | 
					baddabaa16 | ||
| 
						 | 
					427b434ce3 | ||
| 
						 | 
					5f921965e6 | ||
| 
						 | 
					1e705c8ed5 | ||
| 
						 | 
					c584b82ddb | ||
| 
						 | 
					72418ce4d7 | 
							
								
								
									
										115
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										115
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -1,11 +1,121 @@
 | 
			
		||||
# 更新日志
 | 
			
		||||
## v3.2.6
 | 
			
		||||
* 功能优化:恢复关闭注册系统配置项,管理员可以在后台关闭用户注册,只允许内部添加账号
 | 
			
		||||
* 功能优化:兼用旧版本微信收款消息解析
 | 
			
		||||
* 功能优化:优化订单扫码支付状态轮询功能,当关闭二维码时取消轮询,节约网络资源
 | 
			
		||||
* 功能新增:新增图片发布功能,画廊只显示用户已发布的图片
 | 
			
		||||
* 功能新增:后台新增配置微信客服二维码,可以上传自己的微信客服二维码
 | 
			
		||||
* 功能新增:新增网站公告,可以在管理后台自定义配置
 | 
			
		||||
* 功能新增:新增阿里通义千问大模型支持
 | 
			
		||||
* Bug修复:修复 MJ 放大任务失败时候 img_call 会增加的 Bug
 | 
			
		||||
* 功能优化:新增虎皮椒和PayJS订单状态校验功能,增加安全性
 | 
			
		||||
* Bug修复:修复微信转账交易 ID 提取失败 Bug
 | 
			
		||||
* 功能优化:给所有的 websocket 连接加上心跳,解决 "close 1006 (abnormal closure): unexpected EOF" Bug
 | 
			
		||||
* 功能新增:新增短信宝短信平台发送平台集成
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## v3.2.5
 | 
			
		||||
* 功能新增:**重磅更新!!!** 新增 MidJourney-Plus API 支持,一秒配置,开箱即用,高效稳定。
 | 
			
		||||
* 功能新增:**重磅更新!!!** 新增 GPT4-ALL 和 GPTs 模型支持,你只需花几块钱,可以丝滑享受 ChatGPT-Plus 会员的所有功能,无需再订阅 Plus 账号了!!!
 | 
			
		||||
* 功能优化:增强 markdown 图片和引用块解析。
 | 
			
		||||
* 功能新增:新增用户文件管理,目前一支持上传文件跟 GPT 进行多态对话。
 | 
			
		||||
* 功能优化:function call 兼用中转 API。
 | 
			
		||||
* Bug修复:修复部分已知的 Bug。
 | 
			
		||||
 | 
			
		||||
## v3.2.4.1
 | 
			
		||||
* 功能新增:新增 PayJs 支付通道
 | 
			
		||||
* Bug修复:紧急修复后台添加用户失败问题
 | 
			
		||||
* Bug修复:紧急修复使用中转 API-KEY 无法绘图的问题
 | 
			
		||||
* Bug修复:允许用户关闭手机和邮箱注册通道,移除验证码依赖
 | 
			
		||||
 | 
			
		||||
## v3.2.4
 | 
			
		||||
 | 
			
		||||
* 功能新增:重磅更新,支持邮箱注册
 | 
			
		||||
* 功能优化:优化函数调用授权
 | 
			
		||||
* 功能优化:给用户表新增 nickname 字段
 | 
			
		||||
* 功能优化:管理后台给聊天角色增加启用/禁用开关
 | 
			
		||||
* Bug修复:SD绘画出现重复扣减绘图次数
 | 
			
		||||
* 功能优化:优化聊天对话导出样式,适应移动端
 | 
			
		||||
* 功能新增:众筹核销可以选择兑换对话还是绘图的额度
 | 
			
		||||
* Bug修复:修复[从历史记录获取reply有并发风险 #92](https://github.com/yangjian102621/chatgpt-plus/issues/92)
 | 
			
		||||
* Bug修复:修复 MidJourney 绘图任务调度Bug,为 task_id 建议唯一索引
 | 
			
		||||
* 功能重构:重构了 API KEY模块,支持为每个 API KEY 都设置不同的 API 地址,并可以单独开启是否使用代理。
 | 
			
		||||
 | 
			
		||||
## v3.2.3
 | 
			
		||||
 | 
			
		||||
* 功能重构:重构函数工具模块,设计成可以后台动态管理函数。支持添加自定义函数实现
 | 
			
		||||
* 功能新增:为充值产品数据表添加 img_calls 字段,支持充值绘图次数
 | 
			
		||||
* Bug修复:修复 [MJ 机器人空指针异常的 Bug](https://github.com/yangjian102621/chatgpt-plus/issues/73)
 | 
			
		||||
* Bug修复:确保相同 Prompt 的绘图任务的 Upscale 和 Variation 任务调度给相同的频道
 | 
			
		||||
* 功能新增:新增删除绘图任何和图片功能
 | 
			
		||||
* Bug修复:修复虎皮椒支付二维码重复扫码时报错问题
 | 
			
		||||
* 功能优化:自动将 AI 绘画中的中文提示词翻译成英文
 | 
			
		||||
* 功能优化:优化AI绘画的大图压缩算法,新增图片缓存
 | 
			
		||||
* 功能优化:支持为 MJ 绘图 API 增加反代功能,提高图片的加载速度,大大降低绘图任务的失败率
 | 
			
		||||
* Bug修复:修复[Azure Api 更换api-version参数后请求失败的问题](https://github.com/yangjian102621/chatgpt-plus/pull/71)
 | 
			
		||||
* Bug修复:修复科大讯飞 V1.5 API 请求失败的问题
 | 
			
		||||
* Bug修复:绘图失败后,自动恢复用户的剩余绘图次数
 | 
			
		||||
* 功能新增:为移动端新增 SD 绘图功能,分享功能
 | 
			
		||||
 | 
			
		||||
## v3.2.2
 | 
			
		||||
 | 
			
		||||
* 功能重构:重构 MidJourney 和 Stable-Diffusion 绘图模块,支持使用多组配置创建池子提供绘画服务
 | 
			
		||||
* 功能新增:AI绘画页面增加翻译和重写提示词功能
 | 
			
		||||
* 功能优化:OSS上传组件支持在 Bucket 下设置二级目录
 | 
			
		||||
* Bug修复:修复阿里云 OSS 访问路径错误
 | 
			
		||||
* 功能优化:在 AI 绘图页面使用 HTTP 轮询替换 Websocket
 | 
			
		||||
 | 
			
		||||
## v3.2.1
 | 
			
		||||
 | 
			
		||||
* 功能优化:切换角色和模型的时候自动创建新的对话
 | 
			
		||||
* Bug修复:修复文件上传失败No such file bug
 | 
			
		||||
* 功能新增:MidJourney 绘画页面新增提示词翻译功能,新增多个绘画参数
 | 
			
		||||
* Bug修复:[PC端对话在刷新后异常](https://github.com/yangjian102621/chatgpt-plus/issues/59)
 | 
			
		||||
* 功能新增:增加 arm64 架构打包脚本
 | 
			
		||||
* 功能新增:支持 dall-e3 绘图的 API 地址自定义配置
 | 
			
		||||
* 功能新增:新增虎皮椒支付功能接入,支持微信和支付宝通道
 | 
			
		||||
 | 
			
		||||
## v3.2.0
 | 
			
		||||
 | 
			
		||||
* 功能新增:新增邀请注册功能
 | 
			
		||||
* 功能优化:增加中间件自动对HTTP请求的参数去掉首尾空格
 | 
			
		||||
* 功能优化:增加中间件自动为大图片生成缩略图
 | 
			
		||||
* 功能优化:MidJourney 页面图片加载优化,实现图片预览懒加载
 | 
			
		||||
* 功能新增:新增 DALL-E-3 绘画支持,并作为对话页面默认绘画插件
 | 
			
		||||
* Bug修复:修复阿里云 OSS 域名设置不起做用的bug
 | 
			
		||||
* Bug修复:修复MidJourney绘图失败后重复添加到队列的问题
 | 
			
		||||
 | 
			
		||||
## v3.1.9
 | 
			
		||||
 | 
			
		||||
* 功能新增:增加讯飞星火大模型 v3.0 支持
 | 
			
		||||
* 功能新增:新增找回密码功能
 | 
			
		||||
* 功能新增:支持 Markdown 代码复制功能
 | 
			
		||||
* Bug修复: xxl-job 任务调度失败的 Bug
 | 
			
		||||
* 功能优化:优化前端页面菜单图标,使用自定义图标替换 icon-font
 | 
			
		||||
* Bug修复:Stable-Diffusion 绘画成功之后没有扣减用户画图次数
 | 
			
		||||
* 功能优化:优化会员充值页面 ItemList 组件
 | 
			
		||||
* 功能优化:给首页 Logo 增加链接
 | 
			
		||||
* Bug修复:[新建会话时,提示"请输入合法的手机号" ](https://github.com/yangjian102621/chatgpt-plus/issues/51)
 | 
			
		||||
* Bug修复:聊天上下文失效问题
 | 
			
		||||
* 功能优化:关闭注册时显示联系管理员二维码
 | 
			
		||||
* 功能优化:移除 leveldb 依赖,使用 redis 替换相应的功能
 | 
			
		||||
* Bug修复:后台启用用户 VIP 不生效问题
 | 
			
		||||
* 功能优化:充值支付页面的支付说明文字可以后台配置
 | 
			
		||||
* Bug修复:ChatGLM,百度文心,科大讯飞模型输出代码不换行问题
 | 
			
		||||
 | 
			
		||||
## v3.1.8
 | 
			
		||||
 | 
			
		||||
1. 功能新增:新增会员套餐充值,点卡充值,订单系统,集成支付宝支付通道
 | 
			
		||||
2. Bug修复:修复 MidJourney API 参数版本更新导致调用失败的 Bug
 | 
			
		||||
3. 功能优化:将聊天报错信息定义为统一常量,方便修改
 | 
			
		||||
2. Bug修复:修复 MidJourney API 参数版本更新导致调用失败问题
 | 
			
		||||
3. Bug修复:修复 Stable Diffusion 调用后没有更新绘图调用次数问题
 | 
			
		||||
4. Bug修复:修复七牛云上传报错 expired token
 | 
			
		||||
5. Bug修复:修复高权重模型导致的对话次数为负数的漏洞
 | 
			
		||||
6. 功能优化:将聊天报错信息定义为统一常量,方便修改
 | 
			
		||||
7. 功能优化:优化 markdown 表格显示样式,覆写 Element-Plus 表格样式
 | 
			
		||||
8. 功能优化:增加倒数计时组件,定期自动清理未支付的订单
 | 
			
		||||
 | 
			
		||||
## v3.1.7
 | 
			
		||||
 | 
			
		||||
1. 功能新增:支持文心4.0 AI 模型
 | 
			
		||||
2. 功能新增:可以在管理后台为用户绑定指定的 AI 模型,如只给某个用户使用 GPT-4 模型
 | 
			
		||||
3. 功能新增:模型新增权重字段,不同的模型每次调用耗费的点数可以设置不同,比如GPT4是GPT3.5的10倍
 | 
			
		||||
@@ -13,6 +123,7 @@
 | 
			
		||||
5. 功能优化:优化 MidJourney 专业绘画页面图片预览样式
 | 
			
		||||
 | 
			
		||||
## v3.1.6
 | 
			
		||||
 | 
			
		||||
1. 功能新增:新增AI 绘画照片墙功能页面,供用户查看所有的 AI 绘画作品
 | 
			
		||||
2. 功能新增:新增 AI 角色应用功能页面,用户可以添加自己感兴趣的应用
 | 
			
		||||
3. 功能优化:优化瀑布流组件的页面布局
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										378
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										378
									
								
								README.md
									
									
									
									
									
								
							@@ -9,8 +9,9 @@ ChatGLM,讯飞星火,文心一言等多个平台的大语言模型。集成了
 | 
			
		||||
* 支持 OPenAI,Azure,文心一言,讯飞星火,清华 ChatGLM等多个大语言模型。
 | 
			
		||||
* 支持 MidJourney / Stable Diffusion AI 绘画集成,开箱即用。
 | 
			
		||||
* 支持使用个人微信二维码作为充值收费的支付渠道,无需企业支付通道。
 | 
			
		||||
* 已集成支付宝支付功能,支持多种会员套餐和点卡购买功能。
 | 
			
		||||
* 集成插件 API 功能,可结合大语言模型的 function 功能开发各种强大的插件,已内置实现了微博热搜,今日头条,今日早报和 AI 绘画函数插件。
 | 
			
		||||
* 已集成支付宝支付功能,微信支付,支持多种会员套餐和点卡购买功能。
 | 
			
		||||
* 集成插件 API 功能,可结合大语言模型的 function 功能开发各种强大的插件,已内置实现了微博热搜,今日头条,今日早报和 AI
 | 
			
		||||
  绘画函数插件。
 | 
			
		||||
 | 
			
		||||
## 功能截图
 | 
			
		||||
 | 
			
		||||
@@ -23,17 +24,28 @@ ChatGLM,讯飞星火,文心一言等多个平台的大语言模型。集成了
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### MidJourney 专业绘画界面
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### Stable-Diffusion 专业绘画页面
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### 绘图作品展
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### AI应用列表
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### 会员充值
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### 自动调用函数插件
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
@@ -51,50 +63,40 @@ ChatGLM,讯飞星火,文心一言等多个平台的大语言模型。集成了
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### 7. 体验地址
 | 
			
		||||
### 体验地址
 | 
			
		||||
 | 
			
		||||
> 免费体验地址:[https://ai.r9it.com/chat](https://ai.r9it.com/chat) <br/>
 | 
			
		||||
> **注意:请合法使用,禁止输出任何敏感、不友好或违规的内容!!!**
 | 
			
		||||
 | 
			
		||||
## 快速部署
 | 
			
		||||
 | 
			
		||||
**演示站不提供任何充值点卡售卖或者VIP充值服务。** 如果您体验过后觉得还不错的话,可以花两分钟用下面的一键部署脚本自己部署一套。
 | 
			
		||||
 | 
			
		||||
```shell
 | 
			
		||||
bash -c "$(curl -fsSL https://img.r9it.com/tmp/install-v3.2.5-400fea2598.sh)"
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
目前仅支持 Ubuntu 和 Centos 系统。 部署成功之后可以访问下面地址
 | 
			
		||||
 | 
			
		||||
* 前端访问地址:http://localhost:8080/chat 使用移动设备访问会自动跳转到移动端页面。
 | 
			
		||||
* 后台管理地址:http://localhost:8080/admin
 | 
			
		||||
* 移动端地址:http://localhost:8080/mobile
 | 
			
		||||
* 初始后台管理账号:admin/admin123
 | 
			
		||||
* 初始前端体验账号:18575670125/12345678
 | 
			
		||||
 | 
			
		||||
服务启动成功之后不能立刻使用,需要先登录管理后台 -> API-KEY 去添加一个 OpenAI 或者文心一言,科大讯飞等至少一个平台的 API
 | 
			
		||||
KEY。
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
另外,如果您目前还没有 OpenAI 的 API KEY的,推荐您去 https://gpt.bemore.lol 购买,**无需魔法,高速稳定,且价格还远低于 OpenAI
 | 
			
		||||
官方**。
 | 
			
		||||
 | 
			
		||||
## 使用须知
 | 
			
		||||
 | 
			
		||||
1. 本项目基于 MIT 协议,免费开放全部源代码,可以作为个人学习使用或者商用。
 | 
			
		||||
2. 如需商用必须保留版权信息,请自觉遵守。确保合法合规使用,在运营过程中产生的一切任何后果自负,与作者无关。
 | 
			
		||||
 | 
			
		||||
## 项目介绍
 | 
			
		||||
 | 
			
		||||
这一套完整的系统,包括前端聊天应用和一个后台管理系统。系统有用户鉴权,你可以自己使用,也可以部署直接给 C 端用户提供
 | 
			
		||||
ChatGPT 的服务。
 | 
			
		||||
 | 
			
		||||
### 项目的技术架构
 | 
			
		||||
 | 
			
		||||
新版的系统前后端都进行大改动的重构,后端还是用的 Gin Web 框架,但是作者整合了 fx 自动注入框架,整个后端应用结构非常简洁,特别适合二次开发。
 | 
			
		||||
另外,数据存储用 MySQL 替换了 leveldb, 因为要对 C 端,后期会涉及到很多业务数据查询统计,leveldb 已经完全不够用了。
 | 
			
		||||
 | 
			
		||||
> Gin + fx + MySQL
 | 
			
		||||
 | 
			
		||||
3.0 版本之后会陆续添加其他语言的 API 实现,比如 PHP,Java 等。考虑到作者精力有限,api 目录已经添加了,有兴趣的同学自主去认领各自擅长的语言去实现。
 | 
			
		||||
 | 
			
		||||
前端的框架还是:
 | 
			
		||||
 | 
			
		||||
> Vue3 + Element-Plus
 | 
			
		||||
 | 
			
		||||
前后台的页面风格已经全部变了,几乎所有页面样式代码都重写了。逻辑代码还是沿用之前的,毕竟功能没有太大的变化。
 | 
			
		||||
 | 
			
		||||
此次重构改版主要是为了后面功能的扩展准备了。
 | 
			
		||||
 | 
			
		||||
新版本已经实现的功能如下:
 | 
			
		||||
 | 
			
		||||
1. 引入用户体系,新增用户注册和登录功能。
 | 
			
		||||
2. 聊天页面改版,实现了跟 ChatGPT 官方版本一致的聊天体验。
 | 
			
		||||
3. 创建会话的时候可以选择聊天角色和模型。
 | 
			
		||||
4. 新增聊天设置功能,用户可以导入自己的 API KEY
 | 
			
		||||
5. 保存聊天记录,支持聊天上下文。
 | 
			
		||||
6. 重构后台管理模块,更友好,扩展性更好的后台管理系统。
 | 
			
		||||
7. 引入 ip2region 组件,记录用户的登录IP和地址。
 | 
			
		||||
8. 支持会话搜索过滤。
 | 
			
		||||
9. 支持微信支付充值
 | 
			
		||||
 | 
			
		||||
## 项目地址
 | 
			
		||||
 | 
			
		||||
* Github 地址:https://github.com/yangjian102621/chatgpt-plus
 | 
			
		||||
@@ -106,310 +108,25 @@ ChatGPT 的服务。
 | 
			
		||||
 | 
			
		||||
## TODOLIST
 | 
			
		||||
 | 
			
		||||
* [x] 整合 Midjourney AI 绘画 API
 | 
			
		||||
* [x] 开发移动端聊天页面
 | 
			
		||||
* [x] 接入微信收款功能
 | 
			
		||||
* [x] 支持 ChatGPT 函数功能,通过函数实现插件
 | 
			
		||||
* [x] 开发桌面版应用
 | 
			
		||||
* [x] 开发手机 App 客户端
 | 
			
		||||
* [x] 支付宝支付功能
 | 
			
		||||
* [ ] 支持基于知识库的 AI 问答
 | 
			
		||||
* [ ] 会员推广功能
 | 
			
		||||
* [ ] 会员邀请注册推广功能
 | 
			
		||||
* [ ] 微信支付功能
 | 
			
		||||
 | 
			
		||||
## Docker 快速部署
 | 
			
		||||
## 项目文档
 | 
			
		||||
 | 
			
		||||
>
 | 
			
		||||
鉴于最新不少网友反馈在部署的时候遇到一些问题,大部分问题都是相同的,所以我这边做了一个视频教程 [五分钟部署自己的 ChatGPT 服务](https://www.bilibili.com/video/BV1H14y1B7Qw/)。
 | 
			
		||||
> 习惯看视频教程的朋友可以去看视频教程,视频的语速比较慢,建议 2 倍速观看。
 | 
			
		||||
最新的部署视频教程:[https://www.bilibili.com/video/BV1Cc411t7CX/](https://www.bilibili.com/video/BV1Cc411t7CX/)
 | 
			
		||||
 | 
			
		||||
V3.0.0 版本以后已经支持使用容器部署了,跳过所有的繁琐的环境准备,一条命令就可以轻松部署上线。
 | 
			
		||||
详细的部署和开发文档请参考 [**ChatGPT-Plus 文档**](https://ai.r9it.com/docs/)。
 | 
			
		||||
 | 
			
		||||
### 1. 导入数据库
 | 
			
		||||
加微信进入微信讨论群可获取 **一键部署脚本(添加好友时请注明来自Github!!!)。**
 | 
			
		||||
 | 
			
		||||
首先我们需要创建一个 MySQL 容器,并导入初始数据库。
 | 
			
		||||
 | 
			
		||||
```shell
 | 
			
		||||
cd docker/mysql
 | 
			
		||||
# 创建 mysql 容器
 | 
			
		||||
docker-compose up -d
 | 
			
		||||
# 导入数据库
 | 
			
		||||
docker exec -i chatgpt-plus-mysql sh -c 'exec mysql -uroot -p12345678' < ../../database/chatgpt_plus-v3.1.8.sql
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
如果你本地已经安装了 MySQL 服务,那么你只需手动导入数据库即可。
 | 
			
		||||
 | 
			
		||||
```shell
 | 
			
		||||
# 连接数据库
 | 
			
		||||
mysql -u username -p password
 | 
			
		||||
# 导入数据库
 | 
			
		||||
source database/chatgpt_plus.sql
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 2. 修改配置文档
 | 
			
		||||
 | 
			
		||||
修改配置文档 `docker/conf/config.toml` 配置文档,修改代理地址和管理员密码:
 | 
			
		||||
 | 
			
		||||
```toml
 | 
			
		||||
Listen = "0.0.0.0:5678"
 | 
			
		||||
ProxyURL = "" # 如 http://127.0.0.1:7777
 | 
			
		||||
MysqlDns = "root:12345678@tcp(172.22.11.200:3307)/chatgpt_plus?charset=utf8&parseTime=True&loc=Local"
 | 
			
		||||
StaticDir = "./static" # 静态资源的目录
 | 
			
		||||
StaticUrl = "/static" # 静态资源访问 URL
 | 
			
		||||
AesEncryptKey = ""
 | 
			
		||||
WeChatBot = false # 是否启动微信机器人
 | 
			
		||||
 | 
			
		||||
[Session]
 | 
			
		||||
  SecretKey = "azyehq3ivunjhbntz78isj00i4hz2mt9xtddysfucxakadq4qbfrt0b7q3lnvg80" # 注意:这个是 JWT Token 授权密钥,生产环境请务必更换
 | 
			
		||||
  MaxAge = 86400
 | 
			
		||||
 | 
			
		||||
[Manager]
 | 
			
		||||
  Username = "admin"
 | 
			
		||||
  Password = "admin123" # 如果是生产环境的话,这里管理员的密码记得修改
 | 
			
		||||
  
 | 
			
		||||
[Redis] # redis 配置信息
 | 
			
		||||
  Host = "localhost" 
 | 
			
		||||
  Port = 6379
 | 
			
		||||
  Password = ""
 | 
			
		||||
  DB = 0
 | 
			
		||||
  
 | 
			
		||||
[ApiConfig] # 微博热搜,今日头条等函数服务 API 配置,此为第三方插件服务,如需使用请联系作者开通
 | 
			
		||||
  ApiURL = ""
 | 
			
		||||
  AppId = ""
 | 
			
		||||
  Token = ""
 | 
			
		||||
 | 
			
		||||
[SmsConfig] # 阿里云短信服务配置
 | 
			
		||||
  AccessKey = ""
 | 
			
		||||
  AccessSecret = ""
 | 
			
		||||
  Product = "Dysmsapi"
 | 
			
		||||
  Domain = "dysmsapi.aliyuncs.com"
 | 
			
		||||
 | 
			
		||||
[ExtConfig] # MidJourney和微信机器人服务 API 配置,开通此功能需要配合 chatpgt-plus-exts 项目部署
 | 
			
		||||
  ApiURL = "" # 插件扩展 API 地址
 | 
			
		||||
  Token = "" # 这个 token 随便填,只要确保跟 chatgpt-plus-exts 项目的 token 一样就行 
 | 
			
		||||
  
 | 
			
		||||
[OSS] # OSS 配置,用于存储 MJ 绘画图片
 | 
			
		||||
   Active = "local" # 默认使用本地文件存储引擎
 | 
			
		||||
   [OSS.Local]
 | 
			
		||||
     BasePath = "./static/upload" # 本地文件上传根路径
 | 
			
		||||
     BaseURL = "http://localhost:5678/static/upload" # 本地上传文件根 URL 如果是线上,则直接设置为 /static/upload 即可
 | 
			
		||||
   [OSS.Minio]
 | 
			
		||||
     Endpoint = "" # 如 172.22.11.200:9000
 | 
			
		||||
     AccessKey = "" # 自己去 Minio 控制台去创建一个 Access Key
 | 
			
		||||
     AccessSecret = ""
 | 
			
		||||
     Bucket = "chatgpt-plus" # 替换为你自己创建的 Bucket,注意要给 Bucket 设置公开的读权限,否则会出现图片无法显示。
 | 
			
		||||
     UseSSL = false
 | 
			
		||||
     Domain = "" # 地址必须是能够通过公网访问的,否则会出现图片无法显示。
 | 
			
		||||
   [OSS.QiNiu] # 七牛云 OSS 配置
 | 
			
		||||
       Zone = "z2" # 区域,z0:华东,z1: 华北,na0:北美,as0:新加坡
 | 
			
		||||
       AccessKey = ""
 | 
			
		||||
       AccessSecret = ""
 | 
			
		||||
       Bucket = ""
 | 
			
		||||
       Domain = "" # OSS Bucket 所绑定的域名,如 https://img.r9it.com
 | 
			
		||||
       
 | 
			
		||||
[MjConfig] # MidJourney AI 绘画配置
 | 
			
		||||
  Enabled = false # 是否启动 MidJourney 机器人服务
 | 
			
		||||
  UserToken = "" # 用户授权 Token
 | 
			
		||||
  BotToken = "" # Discord 机器人 Token
 | 
			
		||||
  GuildId = "" # 服务器 ID
 | 
			
		||||
  ChanelId = "" # 频道 ID
 | 
			
		||||
 | 
			
		||||
[SdConfig]
 | 
			
		||||
  Enabled = false # 是否启动 Stable Diffusion 机器人服务
 | 
			
		||||
  ApiURL = "http://172.22.11.200:7860" # stable-diffusion-webui API 地址
 | 
			
		||||
  ApiKey = "" # 如果开启了授权,这里需要配置授权的 ApiKey
 | 
			
		||||
  Txt2ImgJsonPath = "res/text2img.json" # 文生图的 API 请求报文 json 模板,允许自定义请求json报文,因为不同版本的 API 绘图的参数以及 fn_index 会不同。
 | 
			
		||||
  
 | 
			
		||||
[XXLConfig] # xxl-job 配置,需要你部署 XXL-JOB 定时任务工具,用来定期清理未支付订单和清理过期 VIP,如果你没有启用支付服务,则该服务也无需启动
 | 
			
		||||
  Enabled = false # 是否启用 XXL JOB 服务
 | 
			
		||||
  ServerAddr = "http://172.22.11.47:8080/xxl-job-admin" # xxl-job-admin 管理地址
 | 
			
		||||
  ExecutorIp = "172.22.11.47" # 执行器 IP 地址
 | 
			
		||||
  ExecutorPort = "9999" # 执行器服务端口
 | 
			
		||||
  AccessToken = "xxl-job-api-token" # 执行器 API 通信 token
 | 
			
		||||
  RegistryKey = "chatgpt-plus" # 任务注册 key
 | 
			
		||||
 | 
			
		||||
[AlipayConfig]
 | 
			
		||||
  Enabled = false # 启用支付宝支付通道
 | 
			
		||||
  SandBox = false # 是否启用沙盒模式
 | 
			
		||||
  UserId = "2088721020750581" # 商户ID
 | 
			
		||||
  AppId = "9021000131658023" # App Id
 | 
			
		||||
  PrivateKey = "certs/alipay/privateKey.txt" # 应用私钥
 | 
			
		||||
  PublicKey = "certs/alipay/appPublicCert.crt" # 应用公钥证书
 | 
			
		||||
  AlipayPublicKey = "certs/alipay/alipayPublicCert.crt" # 支付宝公钥证书
 | 
			
		||||
  RootCert = "certs/alipay/alipayRootCert.crt" # 支付宝根证书
 | 
			
		||||
  NotifyURL = "http://r9it.com:6004/api/payment/alipay/notify" # 支付异步回调地址
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
> 1. 如果你不知道如何获取 Discord 用户 Token 和 Bot Token
 | 
			
		||||
     请查参考 [Midjourney|如何集成到自己的平台](https://zhuanlan.zhihu.com/p/631079476)。
 | 
			
		||||
> 2. `Txt2ImgJsonPath`
 | 
			
		||||
     的默认用的是使用最广泛的 [stable-diffusion-webui](https://github.com/AUTOMATIC1111/stable-diffusion-webui) 项目的
 | 
			
		||||
     API,如果你用的是其他版本,比如秋叶的懒人包部署的,那么请将对应的 text2img 的参数报文复制放在 `res/text2img.json`
 | 
			
		||||
     文件中即可。
 | 
			
		||||
 | 
			
		||||
修改 nginx 配置文档 `docker/conf/nginx/conf.d/chatgpt-plus.conf`,把后端转发的地址改成当前主机的内网 IP 地址。
 | 
			
		||||
 | 
			
		||||
```shell
 | 
			
		||||
 # 这里配置后端 API 的转发
 | 
			
		||||
location /api/ {
 | 
			
		||||
       proxy_http_version 1.1;
 | 
			
		||||
       proxy_connect_timeout 300s;
 | 
			
		||||
       proxy_read_timeout 300s;
 | 
			
		||||
       proxy_send_timeout 12s;
 | 
			
		||||
       proxy_set_header Host $host;
 | 
			
		||||
       proxy_set_header X-Real-IP $remote_addr;
 | 
			
		||||
       proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 | 
			
		||||
       proxy_set_header Upgrade $http_upgrade;
 | 
			
		||||
       proxy_set_header Connection $connection_upgrade;
 | 
			
		||||
       proxy_pass http://172.28.173.76:6789; # 这里改成后端服务的内网 IP 地址
 | 
			
		||||
       
 | 
			
		||||
# 静态资源转发
 | 
			
		||||
location /static/ {
 | 
			
		||||
   proxy_pass http://172.22.11.47:5678; # 这里改成后端服务的内网 IP 地址
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 3. 启动应用
 | 
			
		||||
 | 
			
		||||
先修改 `docker/docker-compose.yaml` 文件中的镜像地址,改成最新的版本:
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
version: '3'
 | 
			
		||||
services:
 | 
			
		||||
  # 后端 API 镜像
 | 
			
		||||
  chatgpt-plus-api:
 | 
			
		||||
    image: registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-api:v3.1.8 #这里改成最新的 release 版本
 | 
			
		||||
    container_name: chatgpt-plus-api
 | 
			
		||||
    restart: always
 | 
			
		||||
    environment:
 | 
			
		||||
      - DEBUG=false
 | 
			
		||||
      - LOG_LEVEL=info
 | 
			
		||||
      - CONFIG_FILE=config.toml
 | 
			
		||||
    ports:
 | 
			
		||||
      - "5678:5678"
 | 
			
		||||
    volumes:
 | 
			
		||||
      - /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime
 | 
			
		||||
      - ./conf/config.toml:/var/www/app/config.toml
 | 
			
		||||
      - ./logs:/var/www/app/logs
 | 
			
		||||
      - ./static:/var/www/app/static
 | 
			
		||||
 | 
			
		||||
  # 前端应用镜像
 | 
			
		||||
  chatgpt-plus-web:
 | 
			
		||||
    image: registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-web:v3.1.8 #这里改成最新的 release 版本
 | 
			
		||||
    container_name: chatgpt-plus-web
 | 
			
		||||
    restart: always
 | 
			
		||||
    ports:
 | 
			
		||||
      - "8080:8080" # 这边是对外的端口,支持 8080,80和443
 | 
			
		||||
    volumes:
 | 
			
		||||
      - ./logs/nginx:/var/log/nginx
 | 
			
		||||
      - ./conf/nginx/conf.d:/etc/nginx/conf.d
 | 
			
		||||
      - ./conf/nginx/nginx.conf:/etc/nginx/nginx.conf
 | 
			
		||||
      - ./ssl:/etc/nginx/ssl
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```shell
 | 
			
		||||
cd docker
 | 
			
		||||
docker-compose up -d
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
* 前端访问地址:http://localhost:8080/chat
 | 
			
		||||
* 后台管理地址:http://localhost:8080/admin
 | 
			
		||||
* 移动端地址:http://localhost:8080/mobile
 | 
			
		||||
 | 
			
		||||
> 注意:你得访问后台管理系统 http://localhost:8080/admin
 | 
			
		||||
> 输入你前面配置文档中设置的管理员用户名和密码登录。
 | 
			
		||||
> 然后进入 `API KEY 管理` 菜单,添加一个 OpenAI 的 API KEY 才可以正常开启 AI 对话。
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
最后进入前端聊天页面 [http://localhost:8080/chat](http://localhost:8080/chat)
 | 
			
		||||
你可以注册新用户,也可以使用系统默认有个账号:`18575670125/12345678` 登录聊天。
 | 
			
		||||
 | 
			
		||||
祝你使用愉快!!!
 | 
			
		||||
 | 
			
		||||
## 本地开发调试
 | 
			
		||||
 | 
			
		||||
本地开发同样要分别运行前端和后端程序。
 | 
			
		||||
 | 
			
		||||
### 运行后端程序
 | 
			
		||||
 | 
			
		||||
1. 同样你首先要 [导入数据库](#1-导入数据库)
 | 
			
		||||
2. 然后 [修改配置文档](#2-修改配置文档)
 | 
			
		||||
3. 运行后端程序:
 | 
			
		||||
 | 
			
		||||
    ```shell
 | 
			
		||||
    cd api 
 | 
			
		||||
    # 1. 先下载依赖
 | 
			
		||||
    go mod tidy
 | 
			
		||||
    # 2. 运行程序
 | 
			
		||||
    go run main.go
 | 
			
		||||
    # 如果你安装了 fresh 可以使用 fresh 实现热启动
 | 
			
		||||
    fresh -c fresh.conf
 | 
			
		||||
    ```
 | 
			
		||||
 | 
			
		||||
### 运行前端程序
 | 
			
		||||
 | 
			
		||||
同样先拷贝配置文档:
 | 
			
		||||
 | 
			
		||||
```shell
 | 
			
		||||
cd web
 | 
			
		||||
cp .env.production .env.development
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
编辑 `.env.development` 文件,修改后端 API 的访问路径:
 | 
			
		||||
 | 
			
		||||
```ini
 | 
			
		||||
VUE_APP_API_HOST=http://localhost:5678
 | 
			
		||||
VUE_APP_WS_HOST=ws://localhost:5678
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
配置好了之后就可以运行前端应用了:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
# 安装依赖
 | 
			
		||||
npm install
 | 
			
		||||
# 运行
 | 
			
		||||
npm run dev
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
* 前端页面:http://localhost:8888/chat
 | 
			
		||||
* 后台管理页面:http://localhost:8888/admin
 | 
			
		||||
 | 
			
		||||
## 项目打包
 | 
			
		||||
 | 
			
		||||
由于本项目是采用异构开发的方式,所项目打包分成两步:首先编译后端程序,然后再打包前端应用。
 | 
			
		||||
 | 
			
		||||
### 打包前端
 | 
			
		||||
 | 
			
		||||
```shell
 | 
			
		||||
cd web
 | 
			
		||||
npm run build
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 打包后端
 | 
			
		||||
 | 
			
		||||
你可以根据个人需求将项目打包成 windows/linux/darwin 平台项目。
 | 
			
		||||
 | 
			
		||||
```shell
 | 
			
		||||
cd api
 | 
			
		||||
# for all platforms
 | 
			
		||||
make clean all
 | 
			
		||||
# for linux only
 | 
			
		||||
make clean linux
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
打包后的可执行文件在 `bin` 目录下。
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
## 参与贡献
 | 
			
		||||
 | 
			
		||||
个人的力量始终有限,任何形式的贡献都是欢迎的,包括但不限于贡献代码,优化文档,提交 issue 和 PR 等。
 | 
			
		||||
 | 
			
		||||
如果有兴趣的话,也可以加微信进入微信讨论群(**添加好友时请注明来自Github!!!**)。
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
#### 特此声明:不接受在微信或者微信群给开发者提 Bug,有问题或者优化建议请提交 Issue 和 PR。非常感谢您的配合!
 | 
			
		||||
#### 特此声明:由于个人时间有限,不接受在微信或者微信群给开发者提 Bug,有问题或者优化建议请提交 Issue 和 PR。非常感谢您的配合!
 | 
			
		||||
 | 
			
		||||
### Commit 类型
 | 
			
		||||
 | 
			
		||||
@@ -425,8 +142,7 @@ make clean linux
 | 
			
		||||
 | 
			
		||||
如果你觉得这个项目对你有帮助,并且情况允许的话,可以请作者喝杯咖啡,非常感谢你的支持~
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								api/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								api/.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -18,4 +18,3 @@ data
 | 
			
		||||
config.toml
 | 
			
		||||
static/upload 
 | 
			
		||||
storage.json
 | 
			
		||||
certs/alipay/*
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										19
									
								
								api/Makefile
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								api/Makefile
									
									
									
									
									
								
							@@ -1,19 +1,14 @@
 | 
			
		||||
SHELL=/usr/bin/env bash
 | 
			
		||||
NAME := chatgpt-plus
 | 
			
		||||
all: window linux darwin
 | 
			
		||||
all: amd64 arm64
 | 
			
		||||
 | 
			
		||||
amd64:
 | 
			
		||||
	CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/$(NAME)-linux main.go
 | 
			
		||||
.PHONY: amd64
 | 
			
		||||
 | 
			
		||||
window:
 | 
			
		||||
	CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o bin/$(NAME)-amd64.exe main.go
 | 
			
		||||
.PHONY: window
 | 
			
		||||
 | 
			
		||||
linux:
 | 
			
		||||
	CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/$(NAME)-amd64-linux main.go
 | 
			
		||||
.PHONY: linux
 | 
			
		||||
 | 
			
		||||
darwin:
 | 
			
		||||
	CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o bin/$(NAME)-amd64-darwin main.go
 | 
			
		||||
.PHONY: darwin
 | 
			
		||||
arm64:
 | 
			
		||||
	CGO_ENABLED=0 GOOS=linux GOARCH=arm64 GOARM=7 go build -o bin/$(NAME)-linux main.go
 | 
			
		||||
.PHONY: arm64
 | 
			
		||||
 | 
			
		||||
clean:
 | 
			
		||||
	rm -rf bin/$(NAME)-*
 | 
			
		||||
 
 | 
			
		||||
@@ -33,9 +33,21 @@ WeChatBot = false
 | 
			
		||||
  Sign = ""
 | 
			
		||||
  CodeTempId = ""
 | 
			
		||||
 | 
			
		||||
[ExtConfig] # MidJourney和微信机器人服务 API 配置,开通此功能需要配合 chatpgt-plus-exts 项目部署
 | 
			
		||||
  ApiURL = "" # 插件扩展 API 地址
 | 
			
		||||
  Token = "" # 这个 token 随便填,只要确保跟 chatgpt-plus-exts 项目的 token 一样就行
 | 
			
		||||
[Sms] # Sms 配置,用于发送短信
 | 
			
		||||
   Active = "Ali" # 当前启用的短信服务,默认使用阿里云
 | 
			
		||||
   [Sms.SmsBao]
 | 
			
		||||
      Username = ""
 | 
			
		||||
      Password = ""
 | 
			
		||||
      Domain = "api.smsbao.com"
 | 
			
		||||
      Sign = "【极客学长】"
 | 
			
		||||
      CodeTemplate = "您的验证码是{code}。5分钟有效,若非本人操作,请忽略本短信。"
 | 
			
		||||
   [Sms.Ali]
 | 
			
		||||
      AccessKey = ""
 | 
			
		||||
      AccessSecret = ""
 | 
			
		||||
      Product = "Dysmsapi"
 | 
			
		||||
      Domain = "dysmsapi.aliyuncs.com"
 | 
			
		||||
      Sign = ""
 | 
			
		||||
      CodeTempId = ""
 | 
			
		||||
 | 
			
		||||
[OSS] # OSS 配置,用于存储 MJ 绘画图片
 | 
			
		||||
   Active = "local" # 默认使用本地文件存储引擎
 | 
			
		||||
@@ -56,18 +68,28 @@ WeChatBot = false
 | 
			
		||||
       Bucket = ""
 | 
			
		||||
       Domain = "" # OSS Bucket 所绑定的域名,如 https://img.r9it.com
 | 
			
		||||
 | 
			
		||||
[MjConfig]
 | 
			
		||||
[[MjConfigs]]
 | 
			
		||||
  Enabled = false
 | 
			
		||||
  UserToken = ""
 | 
			
		||||
  BotToken = ""
 | 
			
		||||
  GuildId = ""
 | 
			
		||||
  ChanelId = ""
 | 
			
		||||
  UseCDN = false #是否使用反向代理访问,设置为true下面的设置才会生效
 | 
			
		||||
  DiscordAPI = "https://mj.r9it.com:8001" # discord API 反代地址
 | 
			
		||||
  DiscordCDN = "https://mj.r9it.com:8002" # mj 图片反代地址
 | 
			
		||||
  DiscordGateway = "wss://mj.r9it.com:8003" # discord 机器人反代地址
 | 
			
		||||
 | 
			
		||||
[SdConfig]
 | 
			
		||||
[[MjPlusConfigs]]
 | 
			
		||||
  Enabled = false
 | 
			
		||||
  ApiURL = "http://172.22.11.200:7860"
 | 
			
		||||
  ApiURL = "https://api.chatgpt-plus.net" # 目前暂时不支持更改
 | 
			
		||||
  ApiKey = "sk-xxx"
 | 
			
		||||
  NotifyURL = "https://ai.r9it.com/api/mj/notify" # 这里需要改成你的域名
 | 
			
		||||
 | 
			
		||||
[[SdConfigs]]
 | 
			
		||||
  Enabled = false
 | 
			
		||||
  ApiURL = ""
 | 
			
		||||
  ApiKey = ""
 | 
			
		||||
  Txt2ImgJsonPath = "res/text2img.json"
 | 
			
		||||
  Txt2ImgJsonPath = "res/sd/text2img.json"
 | 
			
		||||
 | 
			
		||||
[XXLConfig] # xxl-job 配置,需要你部署 XXL-JOB 定时任务工具,用来定期清理未支付订单和清理过期 VIP,如果你没有启用支付服务,则该服务也无需启动
 | 
			
		||||
  Enabled = false # 是否启用 XXL JOB 服务
 | 
			
		||||
@@ -86,4 +108,27 @@ WeChatBot = false
 | 
			
		||||
  PublicKey = "certs/alipay/appPublicCert.crt" # 应用公钥证书
 | 
			
		||||
  AlipayPublicKey = "certs/alipay/alipayPublicCert.crt" # 支付宝公钥证书
 | 
			
		||||
  RootCert = "certs/alipay/alipayRootCert.crt" # 支付宝根证书
 | 
			
		||||
  NotifyURL = "http://r9it.com:6004/api/payment/alipay/notify" # 支付异步回调地址
 | 
			
		||||
  NotifyURL = "https://ai.r9it.com/api/payment/alipay/notify" # 支付异步回调地址
 | 
			
		||||
 | 
			
		||||
[HuPiPayConfig]
 | 
			
		||||
  Enabled = false
 | 
			
		||||
  Name = "wechat"
 | 
			
		||||
  AppId = "201906161477"
 | 
			
		||||
  AppSecret = "7f403199d510fb2c6f0b9f2311800e7c"
 | 
			
		||||
  PayURL = "https://api.xunhupay.com/payment/do.html"
 | 
			
		||||
  NotifyURL = "https://ai.r9it.com/api/payment/hupipay/notify"
 | 
			
		||||
 | 
			
		||||
[SmtpConfig] # 注意,阿里云服务器禁用了25号端口,所以如果需要使用邮件功能,请别用阿里云服务器
 | 
			
		||||
  Host = "smtp.163.com"
 | 
			
		||||
  Port = 25
 | 
			
		||||
  AppName = "极客学长"
 | 
			
		||||
  From = "test@163.com" # 发件邮箱人地址
 | 
			
		||||
  Password = "" #邮箱 stmp 服务授权码
 | 
			
		||||
 | 
			
		||||
[JPayConfig] # PayJs 支付配置
 | 
			
		||||
  Enabled = false
 | 
			
		||||
  Name = "wechat" # 请不要改动
 | 
			
		||||
  AppId = "" # 商户 ID
 | 
			
		||||
  PrivateKey = "" # 秘钥
 | 
			
		||||
  ApiURL = "https://payjs.cn/api/native"
 | 
			
		||||
  NotifyURL = "https://ai.r9it.com/api/payment/payjs/notify" # 异步回调地址,域名改成你自己的
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
package core
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/service/fun"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
@@ -11,9 +11,14 @@ import (
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/go-redis/redis/v8"
 | 
			
		||||
	"github.com/golang-jwt/jwt/v5"
 | 
			
		||||
	"github.com/nfnt/resize"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"image"
 | 
			
		||||
	"image/jpeg"
 | 
			
		||||
	"io"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"runtime/debug"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
@@ -33,10 +38,9 @@ type AppServer struct {
 | 
			
		||||
	ChatSession   *types.LMap[string, *types.ChatSession] //map[sessionId]UserId
 | 
			
		||||
	ChatClients   *types.LMap[string, *types.WsClient]    // map[sessionId]Websocket 连接集合
 | 
			
		||||
	ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function
 | 
			
		||||
	Functions     map[string]fun.Function
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewServer(appConfig *types.AppConfig, functions map[string]fun.Function) *AppServer {
 | 
			
		||||
func NewServer(appConfig *types.AppConfig) *AppServer {
 | 
			
		||||
	gin.SetMode(gin.ReleaseMode)
 | 
			
		||||
	gin.DefaultWriter = io.Discard
 | 
			
		||||
	return &AppServer{
 | 
			
		||||
@@ -47,7 +51,6 @@ func NewServer(appConfig *types.AppConfig, functions map[string]fun.Function) *A
 | 
			
		||||
		ChatSession:   types.NewLMap[string, *types.ChatSession](),
 | 
			
		||||
		ChatClients:   types.NewLMap[string, *types.WsClient](),
 | 
			
		||||
		ReqCancelFunc: types.NewLMap[string, context.CancelFunc](),
 | 
			
		||||
		Functions:     functions,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -57,7 +60,9 @@ func (s *AppServer) Init(debug bool, client *redis.Client) {
 | 
			
		||||
		logger.Info("Enabled debug mode")
 | 
			
		||||
	}
 | 
			
		||||
	s.Engine.Use(corsMiddleware())
 | 
			
		||||
	s.Engine.Use(staticResourceMiddleware())
 | 
			
		||||
	s.Engine.Use(authorizeMiddleware(s, client))
 | 
			
		||||
	s.Engine.Use(parameterHandlerMiddleware())
 | 
			
		||||
	s.Engine.Use(errorHandler)
 | 
			
		||||
	// 添加静态资源访问
 | 
			
		||||
	s.Engine.Static("/static", s.Config.StaticDir)
 | 
			
		||||
@@ -139,16 +144,19 @@ func corsMiddleware() 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/user/resetPass" ||
 | 
			
		||||
			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/role/list" ||
 | 
			
		||||
			c.Request.URL.Path == "/api/mj/jobs" ||
 | 
			
		||||
			c.Request.URL.Path == "/api/mj/proxy" ||
 | 
			
		||||
			c.Request.URL.Path == "/api/mj/client" ||
 | 
			
		||||
			c.Request.URL.Path == "/api/mj/notify" ||
 | 
			
		||||
			c.Request.URL.Path == "/api/invite/hits" ||
 | 
			
		||||
			c.Request.URL.Path == "/api/sd/jobs" ||
 | 
			
		||||
			strings.HasPrefix(c.Request.URL.Path, "/api/test") ||
 | 
			
		||||
			strings.HasPrefix(c.Request.URL.Path, "/api/function/") ||
 | 
			
		||||
			strings.HasPrefix(c.Request.URL.Path, "/api/sms/") ||
 | 
			
		||||
			strings.HasPrefix(c.Request.URL.Path, "/api/captcha/") ||
 | 
			
		||||
			strings.HasPrefix(c.Request.URL.Path, "/api/payment/") ||
 | 
			
		||||
@@ -161,9 +169,7 @@ func authorizeMiddleware(s *AppServer, client *redis.Client) gin.HandlerFunc {
 | 
			
		||||
		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" ||
 | 
			
		||||
			c.Request.URL.Path == "/api/sd/client" {
 | 
			
		||||
		} else if c.Request.URL.Path == "/api/chat/new" {
 | 
			
		||||
			tokenString = c.Query("token")
 | 
			
		||||
		} else {
 | 
			
		||||
			tokenString = c.GetHeader(types.UserAuthHeader)
 | 
			
		||||
@@ -211,3 +217,130 @@ func authorizeMiddleware(s *AppServer, client *redis.Client) gin.HandlerFunc {
 | 
			
		||||
		c.Set(types.LoginUserID, claims["user_id"])
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 统一参数处理
 | 
			
		||||
func parameterHandlerMiddleware() gin.HandlerFunc {
 | 
			
		||||
	return func(c *gin.Context) {
 | 
			
		||||
		// GET 参数处理
 | 
			
		||||
		params := c.Request.URL.Query()
 | 
			
		||||
		for key, values := range params {
 | 
			
		||||
			for i, value := range values {
 | 
			
		||||
				params[key][i] = strings.TrimSpace(value)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		// update get parameters
 | 
			
		||||
		c.Request.URL.RawQuery = params.Encode()
 | 
			
		||||
		// skip file upload requests
 | 
			
		||||
		contentType := c.Request.Header.Get("Content-Type")
 | 
			
		||||
		if strings.Contains(contentType, "multipart/form-data") {
 | 
			
		||||
			c.Next()
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if strings.Contains(contentType, "application/json") {
 | 
			
		||||
			// process POST JSON request body
 | 
			
		||||
			bodyBytes, err := io.ReadAll(c.Request.Body)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				c.Next()
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// 还原请求体
 | 
			
		||||
			c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
 | 
			
		||||
			// 将请求体解析为 JSON
 | 
			
		||||
			var jsonData map[string]interface{}
 | 
			
		||||
			if err := c.ShouldBindJSON(&jsonData); err != nil {
 | 
			
		||||
				c.Next()
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// 对 JSON 数据中的字符串值去除两端空格
 | 
			
		||||
			trimJSONStrings(jsonData)
 | 
			
		||||
			// 更新请求体
 | 
			
		||||
			c.Request.Body = io.NopCloser(bytes.NewBufferString(utils.JsonEncode(jsonData)))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c.Next()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 递归对 JSON 数据中的字符串值去除两端空格
 | 
			
		||||
func trimJSONStrings(data interface{}) {
 | 
			
		||||
	switch v := data.(type) {
 | 
			
		||||
	case map[string]interface{}:
 | 
			
		||||
		for key, value := range v {
 | 
			
		||||
			switch valueType := value.(type) {
 | 
			
		||||
			case string:
 | 
			
		||||
				v[key] = strings.TrimSpace(valueType)
 | 
			
		||||
			case map[string]interface{}, []interface{}:
 | 
			
		||||
				trimJSONStrings(value)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	case []interface{}:
 | 
			
		||||
		for i, value := range v {
 | 
			
		||||
			switch valueType := value.(type) {
 | 
			
		||||
			case string:
 | 
			
		||||
				v[i] = strings.TrimSpace(valueType)
 | 
			
		||||
			case map[string]interface{}, []interface{}:
 | 
			
		||||
				trimJSONStrings(value)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 静态资源中间件
 | 
			
		||||
func staticResourceMiddleware() gin.HandlerFunc {
 | 
			
		||||
	return func(c *gin.Context) {
 | 
			
		||||
 | 
			
		||||
		url := c.Request.URL.String()
 | 
			
		||||
		// 拦截生成缩略图请求
 | 
			
		||||
		if strings.HasPrefix(url, "/static/") && strings.Contains(url, "?imageView2") {
 | 
			
		||||
			r := strings.SplitAfter(url, "imageView2")
 | 
			
		||||
			size := strings.Split(r[1], "/")
 | 
			
		||||
			if len(size) != 8 {
 | 
			
		||||
				c.String(http.StatusNotFound, "invalid thumb args")
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			with := utils.IntValue(size[3], 0)
 | 
			
		||||
			height := utils.IntValue(size[5], 0)
 | 
			
		||||
			quality := utils.IntValue(size[7], 75)
 | 
			
		||||
 | 
			
		||||
			// 打开图片文件
 | 
			
		||||
			filePath := strings.TrimLeft(c.Request.URL.Path, "/")
 | 
			
		||||
			file, err := os.Open(filePath)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				c.String(http.StatusNotFound, "Image not found")
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			defer file.Close()
 | 
			
		||||
 | 
			
		||||
			// 解码图片
 | 
			
		||||
			img, _, err := image.Decode(file)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				c.String(http.StatusInternalServerError, "Error decoding image")
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			var newImg image.Image
 | 
			
		||||
			if height == 0 || with == 0 {
 | 
			
		||||
				// 固定宽度,高度自适应
 | 
			
		||||
				newImg = resize.Resize(uint(with), uint(height), img, resize.Lanczos3)
 | 
			
		||||
			} else {
 | 
			
		||||
				// 生成缩略图
 | 
			
		||||
				newImg = resize.Thumbnail(uint(with), uint(height), img, resize.Lanczos3)
 | 
			
		||||
			}
 | 
			
		||||
			var buffer bytes.Buffer
 | 
			
		||||
			err = jpeg.Encode(&buffer, newImg, &jpeg.Options{Quality: quality})
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// 设置图片缓存有效期为一年 (365天)
 | 
			
		||||
			c.Header("Cache-Control", "max-age=31536000, public")
 | 
			
		||||
			// 直接输出图像数据流
 | 
			
		||||
			c.Data(http.StatusOK, "image/jpeg", buffer.Bytes())
 | 
			
		||||
			c.Abort() // 中断请求
 | 
			
		||||
		}
 | 
			
		||||
		c.Next()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -14,13 +14,12 @@ var logger = logger2.GetLogger()
 | 
			
		||||
 | 
			
		||||
func NewDefaultConfig() *types.AppConfig {
 | 
			
		||||
	return &types.AppConfig{
 | 
			
		||||
		Listen:        "0.0.0.0:5678",
 | 
			
		||||
		ProxyURL:      "",
 | 
			
		||||
		Manager:       types.Manager{Username: "admin", Password: "admin123"},
 | 
			
		||||
		StaticDir:     "./static",
 | 
			
		||||
		StaticUrl:     "http://localhost/5678/static",
 | 
			
		||||
		Redis:         types.RedisConfig{Host: "localhost", Port: 6379, Password: ""},
 | 
			
		||||
		AesEncryptKey: utils.RandString(24),
 | 
			
		||||
		Listen:    "0.0.0.0:5678",
 | 
			
		||||
		ProxyURL:  "",
 | 
			
		||||
		Manager:   types.Manager{Username: "admin", Password: "admin123"},
 | 
			
		||||
		StaticDir: "./static",
 | 
			
		||||
		StaticUrl: "http://localhost/5678/static",
 | 
			
		||||
		Redis:     types.RedisConfig{Host: "localhost", Port: 6379, Password: ""},
 | 
			
		||||
		Session: types.Session{
 | 
			
		||||
			SecretKey: utils.RandString(64),
 | 
			
		||||
			MaxAge:    86400,
 | 
			
		||||
@@ -33,8 +32,6 @@ func NewDefaultConfig() *types.AppConfig {
 | 
			
		||||
				BasePath: "./static/upload",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		MjConfig:     types.MidJourneyConfig{Enabled: false},
 | 
			
		||||
		SdConfig:     types.StableDiffusionConfig{Enabled: false, Txt2ImgJsonPath: "res/text2img.json"},
 | 
			
		||||
		WeChatBot:    false,
 | 
			
		||||
		AlipayConfig: types.AlipayConfig{Enabled: false, SandBox: false},
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,13 @@ type ApiRequest struct {
 | 
			
		||||
	Stream      bool          `json:"stream"`
 | 
			
		||||
	Messages    []interface{} `json:"messages,omitempty"`
 | 
			
		||||
	Prompt      []interface{} `json:"prompt,omitempty"` // 兼容 ChatGLM
 | 
			
		||||
	Functions   []Function    `json:"functions,omitempty"`
 | 
			
		||||
	Tools       []interface{} `json:"tools,omitempty"`
 | 
			
		||||
	Functions   []interface{} `json:"functions,omitempty"` // 兼容中转平台
 | 
			
		||||
 | 
			
		||||
	ToolChoice string `json:"tool_choice,omitempty"`
 | 
			
		||||
 | 
			
		||||
	Input      map[string]interface{} `json:"input,omitempty"`      //兼容阿里通义千问
 | 
			
		||||
	Parameters map[string]interface{} `json:"parameters,omitempty"` //兼容阿里通义千问
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Message struct {
 | 
			
		||||
@@ -27,10 +33,14 @@ type ChoiceItem struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Delta struct {
 | 
			
		||||
	Role         string       `json:"role"`
 | 
			
		||||
	Name         string       `json:"name"`
 | 
			
		||||
	Content      interface{}  `json:"content"`
 | 
			
		||||
	FunctionCall FunctionCall `json:"function_call,omitempty"`
 | 
			
		||||
	Role         string      `json:"role"`
 | 
			
		||||
	Name         string      `json:"name"`
 | 
			
		||||
	Content      interface{} `json:"content"`
 | 
			
		||||
	ToolCalls    []ToolCall  `json:"tool_calls,omitempty"`
 | 
			
		||||
	FunctionCall struct {
 | 
			
		||||
		Name      string `json:"name,omitempty"`
 | 
			
		||||
		Arguments string `json:"arguments,omitempty"`
 | 
			
		||||
	} `json:"function_call,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ChatSession 聊天会话对象
 | 
			
		||||
@@ -61,7 +71,6 @@ type ApiError struct {
 | 
			
		||||
 | 
			
		||||
const PromptMsg = "prompt" // prompt message
 | 
			
		||||
const ReplyMsg = "reply"   // reply message
 | 
			
		||||
const MjMsg = "mj"
 | 
			
		||||
 | 
			
		||||
var ModelToTokens = map[string]int{
 | 
			
		||||
	"gpt-3.5-turbo":     4096,
 | 
			
		||||
@@ -74,4 +83,12 @@ var ModelToTokens = map[string]int{
 | 
			
		||||
	"ernie_bot_turbo":   8192, // 文心一言
 | 
			
		||||
	"general":           8192, // 科大讯飞
 | 
			
		||||
	"general2":          8192,
 | 
			
		||||
	"general3":          8192,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetModelMaxToken(model string) int {
 | 
			
		||||
	if token, ok := ModelToTokens[model]; ok {
 | 
			
		||||
		return token
 | 
			
		||||
	}
 | 
			
		||||
	return 4096
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,21 +9,33 @@ type AppConfig struct {
 | 
			
		||||
	Listen        string
 | 
			
		||||
	Session       Session
 | 
			
		||||
	ProxyURL      string
 | 
			
		||||
	MysqlDns      string            // mysql 连接地址
 | 
			
		||||
	Manager       Manager           // 后台管理员账户信息
 | 
			
		||||
	StaticDir     string            // 静态资源目录
 | 
			
		||||
	StaticUrl     string            // 静态资源 URL
 | 
			
		||||
	Redis         RedisConfig       // redis 连接信息
 | 
			
		||||
	ApiConfig     ChatPlusApiConfig // ChatPlus API authorization configs
 | 
			
		||||
	AesEncryptKey string
 | 
			
		||||
	SmsConfig     AliYunSmsConfig       // AliYun send message service config
 | 
			
		||||
	OSS           OSSConfig             // OSS config
 | 
			
		||||
	MjConfig      MidJourneyConfig      // mj 绘画配置
 | 
			
		||||
	WeChatBot     bool                  // 是否启用微信机器人
 | 
			
		||||
	SdConfig      StableDiffusionConfig // sd 绘画配置
 | 
			
		||||
	MysqlDns      string                  // mysql 连接地址
 | 
			
		||||
	Manager       Manager                 // 后台管理员账户信息
 | 
			
		||||
	StaticDir     string                  // 静态资源目录
 | 
			
		||||
	StaticUrl     string                  // 静态资源 URL
 | 
			
		||||
	Redis         RedisConfig             // redis 连接信息
 | 
			
		||||
	ApiConfig     ChatPlusApiConfig       // ChatPlus API authorization configs
 | 
			
		||||
	SMS           SMSConfig               // send mobile message config
 | 
			
		||||
	OSS           OSSConfig               // OSS config
 | 
			
		||||
	MjConfigs     []MidJourneyConfig      // mj AI draw service pool
 | 
			
		||||
	MjPlusConfigs []MidJourneyPlusConfig  // MJ plus config
 | 
			
		||||
	ImgCdnURL     string                  // 图片反代加速地址
 | 
			
		||||
	WeChatBot     bool                    // 是否启用微信机器人
 | 
			
		||||
	SdConfigs     []StableDiffusionConfig // sd AI draw service pool
 | 
			
		||||
 | 
			
		||||
	XXLConfig    XXLConfig
 | 
			
		||||
	AlipayConfig AlipayConfig
 | 
			
		||||
	XXLConfig     XXLConfig
 | 
			
		||||
	AlipayConfig  AlipayConfig
 | 
			
		||||
	HuPiPayConfig HuPiPayConfig
 | 
			
		||||
	SmtpConfig    SmtpConfig // 邮件发送配置
 | 
			
		||||
	JPayConfig    JPayConfig // payjs 支付配置
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type SmtpConfig struct {
 | 
			
		||||
	Host     string
 | 
			
		||||
	Port     int
 | 
			
		||||
	AppName  string // 应用名称
 | 
			
		||||
	From     string // 发件人邮箱地址
 | 
			
		||||
	Password string // 发件人邮箱密码
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ChatPlusApiConfig struct {
 | 
			
		||||
@@ -33,15 +45,14 @@ type ChatPlusApiConfig struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type MidJourneyConfig struct {
 | 
			
		||||
	Enabled   bool
 | 
			
		||||
	UserToken string
 | 
			
		||||
	BotToken  string
 | 
			
		||||
	GuildId   string // Server ID
 | 
			
		||||
	ChanelId  string // Chanel ID
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type WeChatConfig struct {
 | 
			
		||||
	Enabled bool
 | 
			
		||||
	Enabled        bool
 | 
			
		||||
	UserToken      string
 | 
			
		||||
	BotToken       string
 | 
			
		||||
	GuildId        string // Server ID
 | 
			
		||||
	ChanelId       string // Chanel ID
 | 
			
		||||
	UseCDN         bool
 | 
			
		||||
	DiscordAPI     string
 | 
			
		||||
	DiscordGateway string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type StableDiffusionConfig struct {
 | 
			
		||||
@@ -51,17 +62,15 @@ type StableDiffusionConfig struct {
 | 
			
		||||
	Txt2ImgJsonPath string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type AliYunSmsConfig struct {
 | 
			
		||||
	AccessKey    string
 | 
			
		||||
	AccessSecret string
 | 
			
		||||
	Product      string
 | 
			
		||||
	Domain       string
 | 
			
		||||
	Sign         string // 短信签名
 | 
			
		||||
	CodeTempId   string // 验证码短信模板 ID
 | 
			
		||||
type MidJourneyPlusConfig struct {
 | 
			
		||||
	Enabled   bool // 如果启用了 MidJourney Plus,将会自动禁用原生的MidJourney服务
 | 
			
		||||
	ApiURL    string
 | 
			
		||||
	ApiKey    string
 | 
			
		||||
	NotifyURL string // 任务进度更新回调地址
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type AlipayConfig struct {
 | 
			
		||||
	Enabled         bool   // 是否启用该服务
 | 
			
		||||
	Enabled         bool   // 是否启用该支付通道
 | 
			
		||||
	SandBox         bool   // 是否沙盒环境
 | 
			
		||||
	AppId           string // 应用 ID
 | 
			
		||||
	UserId          string // 支付宝用户 ID
 | 
			
		||||
@@ -72,6 +81,25 @@ type AlipayConfig struct {
 | 
			
		||||
	NotifyURL       string // 异步通知回调
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type HuPiPayConfig struct { //虎皮椒第四方支付配置
 | 
			
		||||
	Enabled   bool   // 是否启用该支付通道
 | 
			
		||||
	Name      string // 支付名称,如:wechat/alipay
 | 
			
		||||
	AppId     string // App ID
 | 
			
		||||
	AppSecret string // app 密钥
 | 
			
		||||
	ApiURL    string // 支付网关
 | 
			
		||||
	NotifyURL string // 异步通知回调
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// JPayConfig PayJs 支付配置
 | 
			
		||||
type JPayConfig struct {
 | 
			
		||||
	Enabled    bool
 | 
			
		||||
	Name       string // 支付名称,默认 wechat
 | 
			
		||||
	AppId      string // 商户 ID
 | 
			
		||||
	PrivateKey string // 私钥
 | 
			
		||||
	ApiURL     string // API 网关
 | 
			
		||||
	NotifyURL  string // 异步回调地址
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type XXLConfig struct { // XXL 任务调度配置
 | 
			
		||||
	Enabled      bool
 | 
			
		||||
	ServerAddr   string
 | 
			
		||||
@@ -109,6 +137,7 @@ type ChatConfig struct {
 | 
			
		||||
	EnableContext bool `json:"enable_context"` // 是否开启聊天上下文
 | 
			
		||||
	EnableHistory bool `json:"enable_history"` // 是否允许保存聊天记录
 | 
			
		||||
	ContextDeep   int  `json:"context_deep"`   // 上下文深度
 | 
			
		||||
	DallImgNum    int  `json:"dall_img_num"`   // dall-e3 出图数量
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Platform string
 | 
			
		||||
@@ -118,33 +147,44 @@ const Azure = Platform("Azure")
 | 
			
		||||
const ChatGLM = Platform("ChatGLM")
 | 
			
		||||
const Baidu = Platform("Baidu")
 | 
			
		||||
const XunFei = Platform("XunFei")
 | 
			
		||||
const QWen = Platform("QWen")
 | 
			
		||||
 | 
			
		||||
// UserChatConfig 用户的聊天配置
 | 
			
		||||
type UserChatConfig struct {
 | 
			
		||||
	ApiKeys map[Platform]string `json:"api_keys"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type InviteReward struct {
 | 
			
		||||
	ChatCalls int `json:"chat_calls"`
 | 
			
		||||
	ImgCalls  int `json:"img_calls"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ModelAPIConfig struct {
 | 
			
		||||
	ApiURL      string  `json:"api_url,omitempty"`
 | 
			
		||||
	Temperature float32 `json:"temperature"`
 | 
			
		||||
	MaxTokens   int     `json:"max_tokens"`
 | 
			
		||||
	ApiKey      string  `json:"api_key"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type SystemConfig struct {
 | 
			
		||||
	Title           string   `json:"title"`
 | 
			
		||||
	AdminTitle      string   `json:"admin_title"`
 | 
			
		||||
	Models          []string `json:"models"`
 | 
			
		||||
	UserInitCalls   int      `json:"user_init_calls"` // 新用户注册默认总送多少次调用
 | 
			
		||||
	InitImgCalls    int      `json:"init_img_calls"`
 | 
			
		||||
	VipMonthCalls   int      `json:"vip_month_calls"` // 会员每个赠送的调用次数
 | 
			
		||||
	EnabledRegister bool     `json:"enabled_register"`
 | 
			
		||||
	EnabledMsg      bool     `json:"enabled_msg"`       // 启用短信验证码服务
 | 
			
		||||
	EnabledDraw     bool     `json:"enabled_draw"`      // 启动 AI 绘画功能
 | 
			
		||||
	RewardImg       string   `json:"reward_img"`        // 众筹收款二维码地址
 | 
			
		||||
	EnabledFunction bool     `json:"enabled_function"`  // 启用 API 函数功能
 | 
			
		||||
	EnabledReward   bool     `json:"enabled_reward"`    // 启用众筹功能
 | 
			
		||||
	EnabledAlipay   bool     `json:"enabled_alipay"`    // 是否启用支付宝支付通道
 | 
			
		||||
	OrderPayTimeout int      `json:"order_pay_timeout"` //订单支付超时时间
 | 
			
		||||
	DefaultModels   []string `json:"default_models"`    // 默认开通的 AI 模型
 | 
			
		||||
	Title            string `json:"title"`
 | 
			
		||||
	AdminTitle       string `json:"admin_title"`
 | 
			
		||||
	InitChatCalls    int    `json:"init_chat_calls"`     // 新用户注册赠送对话次数
 | 
			
		||||
	InitImgCalls     int    `json:"init_img_calls"`      // 新用户注册赠送绘图次数
 | 
			
		||||
	VipMonthCalls    int    `json:"vip_month_calls"`     // VIP 会员每月赠送的对话次数
 | 
			
		||||
	VipMonthImgCalls int    `json:"vip_month_img_calls"` // VIP 会员每月赠送绘图次数
 | 
			
		||||
 | 
			
		||||
	RegisterWays    []string `json:"register_ways"`    // 注册方式:支持手机,邮箱注册
 | 
			
		||||
	EnabledRegister bool     `json:"enabled_register"` // 是否开放注册
 | 
			
		||||
 | 
			
		||||
	RewardImg     string  `json:"reward_img"`      // 众筹收款二维码地址
 | 
			
		||||
	EnabledReward bool    `json:"enabled_reward"`  // 启用众筹功能
 | 
			
		||||
	ChatCallPrice float64 `json:"chat_call_price"` // 对话单次调用费用
 | 
			
		||||
	ImgCallPrice  float64 `json:"img_call_price"`  // 绘图单次调用费用
 | 
			
		||||
 | 
			
		||||
	OrderPayTimeout  int      `json:"order_pay_timeout"`   //订单支付超时时间
 | 
			
		||||
	DefaultModels    []string `json:"default_models"`      // 默认开通的 AI 模型
 | 
			
		||||
	OrderPayInfoText string   `json:"order_pay_info_text"` // 订单支付页面说明文字
 | 
			
		||||
	InviteChatCalls  int      `json:"invite_chat_calls"`   // 邀请用户注册奖励对话次数
 | 
			
		||||
	InviteImgCalls   int      `json:"invite_img_calls"`    // 邀请用户注册奖励绘图次数
 | 
			
		||||
 | 
			
		||||
	WechatCardURL string `json:"wechat_card_url"` // 微信客服地址
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,11 @@
 | 
			
		||||
package types
 | 
			
		||||
 | 
			
		||||
type FunctionCall struct {
 | 
			
		||||
	Name      string `json:"name"`
 | 
			
		||||
	Arguments string `json:"arguments"`
 | 
			
		||||
type ToolCall struct {
 | 
			
		||||
	Type     string `json:"type"`
 | 
			
		||||
	Function struct {
 | 
			
		||||
		Name      string `json:"name"`
 | 
			
		||||
		Arguments string `json:"arguments"`
 | 
			
		||||
	} `json:"function"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Function struct {
 | 
			
		||||
@@ -21,72 +24,3 @@ type Property struct {
 | 
			
		||||
	Type        string `json:"type"`
 | 
			
		||||
	Description string `json:"description"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	FuncZaoBao     = "zao_bao"     // 每日早报
 | 
			
		||||
	FuncHeadLine   = "headline"    // 今日头条
 | 
			
		||||
	FuncWeibo      = "weibo_hot"   // 微博热搜
 | 
			
		||||
	FuncMidJourney = "mid_journey" // MJ 绘画
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var InnerFunctions = []Function{
 | 
			
		||||
	{
 | 
			
		||||
		Name:        FuncZaoBao,
 | 
			
		||||
		Description: "每日早报,获取当天全球的热门新闻事件列表",
 | 
			
		||||
		Parameters: Parameters{
 | 
			
		||||
 | 
			
		||||
			Type: "object",
 | 
			
		||||
			Properties: map[string]Property{
 | 
			
		||||
				"text": {
 | 
			
		||||
					Type:        "string",
 | 
			
		||||
					Description: "",
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			Required: []string{},
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		Name:        FuncWeibo,
 | 
			
		||||
		Description: "新浪微博热搜榜,微博当日热搜榜单",
 | 
			
		||||
		Parameters: Parameters{
 | 
			
		||||
			Type: "object",
 | 
			
		||||
			Properties: map[string]Property{
 | 
			
		||||
				"text": {
 | 
			
		||||
					Type:        "string",
 | 
			
		||||
					Description: "",
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			Required: []string{},
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	{
 | 
			
		||||
		Name:        FuncHeadLine,
 | 
			
		||||
		Description: "今日头条,给用户推荐当天的头条新闻,周榜热文",
 | 
			
		||||
		Parameters: Parameters{
 | 
			
		||||
			Type: "object",
 | 
			
		||||
			Properties: map[string]Property{
 | 
			
		||||
				"text": {
 | 
			
		||||
					Type:        "string",
 | 
			
		||||
					Description: "",
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			Required: []string{},
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	{
 | 
			
		||||
		Name:        FuncMidJourney,
 | 
			
		||||
		Description: "AI 绘画工具,使用 MJ MidJourney API 进行 AI 绘画",
 | 
			
		||||
		Parameters: Parameters{
 | 
			
		||||
			Type: "object",
 | 
			
		||||
			Properties: map[string]Property{
 | 
			
		||||
				"prompt": {
 | 
			
		||||
					Type:        "string",
 | 
			
		||||
					Description: "提示词,如果该参数中有中文的话,则需要翻译成英文。提示词中的参数作为提示的一部分,不要删除",
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			Required: []string{},
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type MKey interface {
 | 
			
		||||
	string | int
 | 
			
		||||
	string | int | uint
 | 
			
		||||
}
 | 
			
		||||
type MValue interface {
 | 
			
		||||
	*WsClient | *ChatSession | context.CancelFunc | []interface{}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,9 +9,10 @@ const (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type OrderRemark struct {
 | 
			
		||||
	Days     int     `json:"days"`  // 有效期
 | 
			
		||||
	Calls    int     `json:"calls"` // 增加调用次数
 | 
			
		||||
	Name     string  `json:"name"`  // 产品名称
 | 
			
		||||
	Days     int     `json:"days"`      // 有效期
 | 
			
		||||
	Calls    int     `json:"calls"`     // 增加对话次数
 | 
			
		||||
	ImgCalls int     `json:"img_calls"` // 增加绘图次数
 | 
			
		||||
	Name     string  `json:"name"`      // 产品名称
 | 
			
		||||
	Price    float64 `json:"price"`
 | 
			
		||||
	Discount float64 `json:"discount"`
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ type MiniOssConfig struct {
 | 
			
		||||
	AccessKey    string
 | 
			
		||||
	AccessSecret string
 | 
			
		||||
	Bucket       string
 | 
			
		||||
	SubDir       string
 | 
			
		||||
	UseSSL       bool
 | 
			
		||||
	Domain       string
 | 
			
		||||
}
 | 
			
		||||
@@ -21,6 +22,7 @@ type QiNiuOssConfig struct {
 | 
			
		||||
	AccessKey    string
 | 
			
		||||
	AccessSecret string
 | 
			
		||||
	Bucket       string
 | 
			
		||||
	SubDir       string
 | 
			
		||||
	Domain       string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -29,6 +31,7 @@ type AliYunOssConfig struct {
 | 
			
		||||
	AccessKey    string
 | 
			
		||||
	AccessSecret string
 | 
			
		||||
	Bucket       string
 | 
			
		||||
	SubDir       string
 | 
			
		||||
	Domain       string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										26
									
								
								api/core/types/sms.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								api/core/types/sms.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
			
		||||
package types
 | 
			
		||||
 | 
			
		||||
type SMSConfig struct {
 | 
			
		||||
	Active string
 | 
			
		||||
	Ali    SmsConfigAli
 | 
			
		||||
	Bao    SmsConfigBao
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SmsConfigAli 阿里云短信平台配置
 | 
			
		||||
type SmsConfigAli struct {
 | 
			
		||||
	AccessKey    string
 | 
			
		||||
	AccessSecret string
 | 
			
		||||
	Product      string
 | 
			
		||||
	Domain       string
 | 
			
		||||
	Sign         string // 短信签名
 | 
			
		||||
	CodeTempId   string // 验证码短信模板 ID
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SmsConfigBao 短信宝平台配置
 | 
			
		||||
type SmsConfigBao struct {
 | 
			
		||||
	Username     string //短信宝平台注册的用户名
 | 
			
		||||
	Password     string //短信宝平台注册的密码
 | 
			
		||||
	Domain       string //域名
 | 
			
		||||
	Sign         string // 短信签名
 | 
			
		||||
	CodeTemplate string // 验证码短信模板 匹配
 | 
			
		||||
}
 | 
			
		||||
@@ -11,28 +11,16 @@ const (
 | 
			
		||||
	TaskImage     = TaskType("image")
 | 
			
		||||
	TaskUpscale   = TaskType("upscale")
 | 
			
		||||
	TaskVariation = TaskType("variation")
 | 
			
		||||
	TaskTxt2Img   = TaskType("text2img")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// TaskSrc 任务来源
 | 
			
		||||
type TaskSrc string
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	TaskSrcChat = TaskSrc("chat") // 来自聊天页面
 | 
			
		||||
	TaskSrcImg  = TaskSrc("img")  // 专业绘画页面
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// MjTask MidJourney 任务
 | 
			
		||||
type MjTask struct {
 | 
			
		||||
	Id          int      `json:"id"`
 | 
			
		||||
	ChannelId   string   `json:"channel_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       int      `json:"index,omitempty"`
 | 
			
		||||
	MessageId   string   `json:"message_id,omitempty"`
 | 
			
		||||
	MessageHash string   `json:"message_hash,omitempty"`
 | 
			
		||||
@@ -42,7 +30,6 @@ type MjTask struct {
 | 
			
		||||
type SdTask struct {
 | 
			
		||||
	Id         int          `json:"id"` // job 数据库ID
 | 
			
		||||
	SessionId  string       `json:"session_id"`
 | 
			
		||||
	Src        TaskSrc      `json:"src"`
 | 
			
		||||
	Type       TaskType     `json:"type"`
 | 
			
		||||
	UserId     int          `json:"user_id"`
 | 
			
		||||
	Prompt     string       `json:"prompt,omitempty"`
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,6 @@ require (
 | 
			
		||||
	github.com/BurntSushi/toml v1.1.0
 | 
			
		||||
	github.com/aliyun/alibaba-cloud-sdk-go v1.62.405
 | 
			
		||||
	github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible
 | 
			
		||||
	github.com/bwmarrin/discordgo v0.27.1
 | 
			
		||||
	github.com/eatmoreapple/openwechat v1.2.1
 | 
			
		||||
	github.com/gin-gonic/gin v1.9.1
 | 
			
		||||
	github.com/go-redis/redis/v8 v8.11.5
 | 
			
		||||
@@ -19,7 +18,6 @@ require (
 | 
			
		||||
	github.com/qiniu/go-sdk/v7 v7.17.1
 | 
			
		||||
	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
 | 
			
		||||
	github.com/smartwalle/alipay/v3 v3.2.15
 | 
			
		||||
	github.com/syndtr/goleveldb v1.0.0
 | 
			
		||||
	go.uber.org/zap v1.23.0
 | 
			
		||||
	gopkg.in/natefinch/lumberjack.v2 v2.2.1
 | 
			
		||||
	gorm.io/driver/mysql v1.4.7
 | 
			
		||||
@@ -27,6 +25,8 @@ require (
 | 
			
		||||
 | 
			
		||||
require github.com/xxl-job/xxl-job-executor-go v1.2.0
 | 
			
		||||
 | 
			
		||||
require github.com/bg5t/mydiscordgo v0.28.1
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/andybalholm/brotli v1.0.4 // indirect
 | 
			
		||||
	github.com/bytedance/sonic v1.9.1 // indirect
 | 
			
		||||
@@ -87,7 +87,6 @@ require (
 | 
			
		||||
	github.com/go-playground/locales v0.14.1 // indirect
 | 
			
		||||
	github.com/go-playground/universal-translator v0.18.1 // indirect
 | 
			
		||||
	github.com/go-playground/validator/v10 v10.14.0 // indirect
 | 
			
		||||
	github.com/golang/snappy v0.0.1 // indirect
 | 
			
		||||
	github.com/json-iterator/go v1.1.12 // indirect
 | 
			
		||||
	github.com/leodido/go-urn v1.2.4 // indirect
 | 
			
		||||
	github.com/mattn/go-isatty v0.0.19 // indirect
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										21
									
								
								api/go.sum
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								api/go.sum
									
									
									
									
									
								
							@@ -7,8 +7,8 @@ github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible/go.mod h1:T/Aws4fEfogEE9
 | 
			
		||||
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/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY=
 | 
			
		||||
github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
 | 
			
		||||
github.com/bg5t/mydiscordgo v0.28.1 h1:mVH0ZWstVdJffCi/EXJAYQDtXwIKAJYVXLmECu1hEK8=
 | 
			
		||||
github.com/bg5t/mydiscordgo v0.28.1/go.mod h1:n3aba73N18k1DzM0t0mGE8rwW3Z+vwTvI8pcsBgxN/8=
 | 
			
		||||
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=
 | 
			
		||||
@@ -29,7 +29,6 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
 | 
			
		||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 | 
			
		||||
github.com/eatmoreapple/openwechat v1.2.1 h1:ez4oqF/Y2NSEX/DbPV8lvj7JlfkYqvieeo4awx5lzfU=
 | 
			
		||||
github.com/eatmoreapple/openwechat v1.2.1/go.mod h1:61HOzTyvLobGdgWhL68jfGNwTJEv0mhQ1miCXQrvWU8=
 | 
			
		||||
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=
 | 
			
		||||
@@ -68,12 +67,8 @@ github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJ
 | 
			
		||||
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
 | 
			
		||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
 | 
			
		||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
 | 
			
		||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 | 
			
		||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 | 
			
		||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
 | 
			
		||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 | 
			
		||||
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
 | 
			
		||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 | 
			
		||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
			
		||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 | 
			
		||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 | 
			
		||||
@@ -89,7 +84,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/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 | 
			
		||||
github.com/imroc/req/v3 v3.37.2 h1:vEemuA0cq9zJ6lhe+mSRhsZm951bT0CdiSH47+KTn6I=
 | 
			
		||||
github.com/imroc/req/v3 v3.37.2/go.mod h1:DECzjVIrj6jcUr5n6e+z0ygmCO93rx4Jy0RjOEe1YCI=
 | 
			
		||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
 | 
			
		||||
@@ -138,12 +132,9 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY
 | 
			
		||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
 | 
			
		||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
 | 
			
		||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
 | 
			
		||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 | 
			
		||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 | 
			
		||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
 | 
			
		||||
github.com/onsi/ginkgo/v2 v2.10.0 h1:sfUl4qgLdvkChZrWCYndY2EAu9BRIw1YphNAzy1VNWs=
 | 
			
		||||
github.com/onsi/ginkgo/v2 v2.10.0/go.mod h1:UDQOh5wbQUlMnkLfVaIUMtQ1Vus92oM+P2JX1aulgcE=
 | 
			
		||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 | 
			
		||||
github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU=
 | 
			
		||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A=
 | 
			
		||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
 | 
			
		||||
@@ -199,8 +190,6 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
 | 
			
		||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 | 
			
		||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
 | 
			
		||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 | 
			
		||||
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
 | 
			
		||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
 | 
			
		||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
 | 
			
		||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
 | 
			
		||||
github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=
 | 
			
		||||
@@ -242,7 +231,6 @@ 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=
 | 
			
		||||
@@ -251,13 +239,11 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
 | 
			
		||||
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=
 | 
			
		||||
@@ -304,15 +290,12 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
 | 
			
		||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 | 
			
		||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 | 
			
		||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 | 
			
		||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 | 
			
		||||
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 | 
			
		||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
 | 
			
		||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 | 
			
		||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
 | 
			
		||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
 | 
			
		||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
 | 
			
		||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
 | 
			
		||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 | 
			
		||||
 
 | 
			
		||||
@@ -5,13 +5,10 @@ import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/handler"
 | 
			
		||||
	logger2 "chatplus/logger"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"context"
 | 
			
		||||
	"github.com/go-redis/redis/v8"
 | 
			
		||||
	"github.com/golang-jwt/jwt/v5"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
@@ -82,67 +79,3 @@ func (h *ManagerHandler) Session(c *gin.Context) {
 | 
			
		||||
		resp.SUCCESS(c)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Migrate 数据修正
 | 
			
		||||
func (h *ManagerHandler) Migrate(c *gin.Context) {
 | 
			
		||||
	opt := c.Query("opt")
 | 
			
		||||
	switch opt {
 | 
			
		||||
	case "user":
 | 
			
		||||
		// 将用户订阅角色的数据结构从 map 改成数组
 | 
			
		||||
		var users []model.User
 | 
			
		||||
		h.db.Find(&users)
 | 
			
		||||
		for _, u := range users {
 | 
			
		||||
			var m map[string]int
 | 
			
		||||
			var roleKeys = make([]string, 0)
 | 
			
		||||
			err := utils.JsonDecode(u.ChatRoles, &m)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			for k := range m {
 | 
			
		||||
				roleKeys = append(roleKeys, k)
 | 
			
		||||
			}
 | 
			
		||||
			u.ChatRoles = utils.JsonEncode(roleKeys)
 | 
			
		||||
			h.db.Updates(&u)
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
		break
 | 
			
		||||
	case "role":
 | 
			
		||||
		// 修改角色图片,改成绝对路径
 | 
			
		||||
		var roles []model.ChatRole
 | 
			
		||||
		h.db.Find(&roles)
 | 
			
		||||
		for _, r := range roles {
 | 
			
		||||
			if !strings.HasPrefix(r.Icon, "/") {
 | 
			
		||||
				r.Icon = "/" + r.Icon
 | 
			
		||||
				h.db.Updates(&r)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		break
 | 
			
		||||
	case "history":
 | 
			
		||||
		// 修改角色图片,改成绝对路径
 | 
			
		||||
		var message []model.HistoryMessage
 | 
			
		||||
		h.db.Find(&message)
 | 
			
		||||
		for _, r := range message {
 | 
			
		||||
			if !strings.HasPrefix(r.Icon, "/") {
 | 
			
		||||
				r.Icon = "/" + r.Icon
 | 
			
		||||
				h.db.Updates(&r)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
		break
 | 
			
		||||
 | 
			
		||||
	case "avatar":
 | 
			
		||||
		// 更新用户的头像地址
 | 
			
		||||
		var users []model.User
 | 
			
		||||
		h.db.Find(&users)
 | 
			
		||||
		for _, u := range users {
 | 
			
		||||
			if !strings.HasPrefix(u.Avatar, "/") {
 | 
			
		||||
				u.Avatar = "/" + u.Avatar
 | 
			
		||||
				h.db.Updates(&u)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		break
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, "SUCCESS")
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,12 @@ func (h *ApiKeyHandler) Save(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Id       uint   `json:"id"`
 | 
			
		||||
		Platform string `json:"platform"`
 | 
			
		||||
		Name     string `json:"name"`
 | 
			
		||||
		Type     string `json:"type"`
 | 
			
		||||
		Value    string `json:"value"`
 | 
			
		||||
		ApiURL   string `json:"api_url"`
 | 
			
		||||
		Enabled  bool   `json:"enabled"`
 | 
			
		||||
		UseProxy bool   `json:"use_proxy"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
@@ -40,7 +45,12 @@ func (h *ApiKeyHandler) Save(c *gin.Context) {
 | 
			
		||||
	}
 | 
			
		||||
	apiKey.Platform = data.Platform
 | 
			
		||||
	apiKey.Value = data.Value
 | 
			
		||||
	res := h.db.Debug().Save(&apiKey)
 | 
			
		||||
	apiKey.Type = data.Type
 | 
			
		||||
	apiKey.ApiURL = data.ApiURL
 | 
			
		||||
	apiKey.Enabled = data.Enabled
 | 
			
		||||
	apiKey.UseProxy = data.UseProxy
 | 
			
		||||
	apiKey.Name = data.Name
 | 
			
		||||
	res := h.db.Save(&apiKey)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "更新数据库失败!")
 | 
			
		||||
		return
 | 
			
		||||
@@ -78,6 +88,26 @@ func (h *ApiKeyHandler) List(c *gin.Context) {
 | 
			
		||||
	resp.SUCCESS(c, keys)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *ApiKeyHandler) Set(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Id    uint        `json:"id"`
 | 
			
		||||
		Filed string      `json:"filed"`
 | 
			
		||||
		Value interface{} `json:"value"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	res := h.db.Model(&model.ApiKey{}).Where("id = ?", data.Id).Update(data.Filed, data.Value)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "更新数据库失败!")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *ApiKeyHandler) Remove(c *gin.Context) {
 | 
			
		||||
	id := h.GetInt(c, "id", 0)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -31,6 +31,7 @@ func (h *ChatModelHandler) Save(c *gin.Context) {
 | 
			
		||||
		Value     string `json:"value"`
 | 
			
		||||
		Enabled   bool   `json:"enabled"`
 | 
			
		||||
		SortNum   int    `json:"sort_num"`
 | 
			
		||||
		Open      bool   `json:"open"`
 | 
			
		||||
		Platform  string `json:"platform"`
 | 
			
		||||
		Weight    int    `json:"weight"`
 | 
			
		||||
		CreatedAt int64  `json:"created_at"`
 | 
			
		||||
@@ -40,7 +41,14 @@ func (h *ChatModelHandler) Save(c *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	item := model.ChatModel{Platform: data.Platform, Name: data.Name, Value: data.Value, Enabled: data.Enabled, SortNum: data.SortNum, Weight: data.Weight}
 | 
			
		||||
	item := model.ChatModel{
 | 
			
		||||
		Platform: data.Platform,
 | 
			
		||||
		Name:     data.Name,
 | 
			
		||||
		Value:    data.Value,
 | 
			
		||||
		Enabled:  data.Enabled,
 | 
			
		||||
		SortNum:  data.SortNum,
 | 
			
		||||
		Open:     data.Open,
 | 
			
		||||
		Weight:   data.Weight}
 | 
			
		||||
	item.Id = data.Id
 | 
			
		||||
	if item.Id > 0 {
 | 
			
		||||
		item.CreatedAt = time.Unix(data.CreatedAt, 0)
 | 
			
		||||
@@ -89,10 +97,11 @@ func (h *ChatModelHandler) List(c *gin.Context) {
 | 
			
		||||
	resp.SUCCESS(c, cms)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *ChatModelHandler) Enable(c *gin.Context) {
 | 
			
		||||
func (h *ChatModelHandler) Set(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Id      uint `json:"id"`
 | 
			
		||||
		Enabled bool `json:"enabled"`
 | 
			
		||||
		Id    uint        `json:"id"`
 | 
			
		||||
		Filed string      `json:"filed"`
 | 
			
		||||
		Value interface{} `json:"value"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
@@ -100,7 +109,7 @@ func (h *ChatModelHandler) Enable(c *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	res := h.db.Model(&model.ChatModel{}).Where("id = ?", data.Id).Update("enabled", data.Enabled)
 | 
			
		||||
	res := h.db.Model(&model.ChatModel{}).Where("id = ?", data.Id).Update(data.Filed, data.Value)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "更新数据库失败!")
 | 
			
		||||
		return
 | 
			
		||||
 
 | 
			
		||||
@@ -98,6 +98,26 @@ func (h *ChatRoleHandler) Sort(c *gin.Context) {
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *ChatRoleHandler) Set(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Id    uint        `json:"id"`
 | 
			
		||||
		Filed string      `json:"filed"`
 | 
			
		||||
		Value interface{} `json:"value"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	res := h.db.Model(&model.ChatRole{}).Where("id = ?", data.Id).Update(data.Filed, data.Value)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "更新数据库失败!")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *ChatRoleHandler) Remove(c *gin.Context) {
 | 
			
		||||
	id := h.GetInt(c, "id", 0)
 | 
			
		||||
	if id <= 0 {
 | 
			
		||||
 
 | 
			
		||||
@@ -33,8 +33,9 @@ func (h *ConfigHandler) Update(c *gin.Context) {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	str := utils.JsonEncode(&data.Config)
 | 
			
		||||
	config := model.Config{Key: data.Key, Config: str}
 | 
			
		||||
 | 
			
		||||
	value := utils.JsonEncode(&data.Config)
 | 
			
		||||
	config := model.Config{Key: data.Key, Config: value}
 | 
			
		||||
	res := h.db.FirstOrCreate(&config, model.Config{Key: data.Key})
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, res.Error.Error())
 | 
			
		||||
@@ -42,7 +43,7 @@ func (h *ConfigHandler) Update(c *gin.Context) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if config.Id > 0 {
 | 
			
		||||
		config.Config = str
 | 
			
		||||
		config.Config = value
 | 
			
		||||
		res := h.db.Updates(&config)
 | 
			
		||||
		if res.Error != nil {
 | 
			
		||||
			resp.ERROR(c, res.Error.Error())
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ package admin
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/handler"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
@@ -22,10 +23,10 @@ func NewDashboardHandler(app *core.AppServer, db *gorm.DB) *DashboardHandler {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type statsVo struct {
 | 
			
		||||
	Users   int64   `json:"users"`
 | 
			
		||||
	Chats   int64   `json:"chats"`
 | 
			
		||||
	Tokens  int64   `json:"tokens"`
 | 
			
		||||
	Rewards float64 `json:"rewards"`
 | 
			
		||||
	Users  int64   `json:"users"`
 | 
			
		||||
	Chats  int64   `json:"chats"`
 | 
			
		||||
	Tokens int     `json:"tokens"`
 | 
			
		||||
	Income float64 `json:"income"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *DashboardHandler) Stats(c *gin.Context) {
 | 
			
		||||
@@ -47,17 +48,24 @@ func (h *DashboardHandler) Stats(c *gin.Context) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// tokens took stats
 | 
			
		||||
	var tokenCount int64
 | 
			
		||||
	res = h.db.Model(&model.HistoryMessage{}).Select("sum(tokens) as total").Where("created_at > ?", zeroTime).Scan(&tokenCount)
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		stats.Tokens = tokenCount
 | 
			
		||||
	var historyMessages []model.HistoryMessage
 | 
			
		||||
	res = h.db.Where("created_at > ?", zeroTime).Find(&historyMessages)
 | 
			
		||||
	for _, item := range historyMessages {
 | 
			
		||||
		stats.Tokens += item.Tokens
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// reward revenue
 | 
			
		||||
	var amount float64
 | 
			
		||||
	res = h.db.Model(&model.Reward{}).Select("sum(amount) as total").Where("created_at > ?", zeroTime).Scan(&amount)
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		stats.Rewards = amount
 | 
			
		||||
	// 众筹收入
 | 
			
		||||
	var rewards []model.Reward
 | 
			
		||||
	res = h.db.Where("created_at > ?", zeroTime).Find(&rewards)
 | 
			
		||||
	for _, item := range rewards {
 | 
			
		||||
		stats.Income += item.Amount
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 订单收入
 | 
			
		||||
	var orders []model.Order
 | 
			
		||||
	res = h.db.Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", zeroTime).Find(&orders)
 | 
			
		||||
	for _, item := range orders {
 | 
			
		||||
		stats.Income += item.Amount
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c, stats)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										124
									
								
								api/handler/admin/function_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								api/handler/admin/function_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,124 @@
 | 
			
		||||
package admin
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/handler"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
 | 
			
		||||
	"github.com/golang-jwt/jwt/v5"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type FunctionHandler struct {
 | 
			
		||||
	handler.BaseHandler
 | 
			
		||||
	db *gorm.DB
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewFunctionHandler(app *core.AppServer, db *gorm.DB) *FunctionHandler {
 | 
			
		||||
	h := FunctionHandler{db: db}
 | 
			
		||||
	h.App = app
 | 
			
		||||
	return &h
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *FunctionHandler) Save(c *gin.Context) {
 | 
			
		||||
	var data vo.Function
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var f = model.Function{
 | 
			
		||||
		Id:          data.Id,
 | 
			
		||||
		Name:        data.Name,
 | 
			
		||||
		Label:       data.Label,
 | 
			
		||||
		Description: data.Description,
 | 
			
		||||
		Parameters:  utils.JsonEncode(data.Parameters),
 | 
			
		||||
		Action:      data.Action,
 | 
			
		||||
		Token:       data.Token,
 | 
			
		||||
		Enabled:     data.Enabled,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	res := h.db.Save(&f)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "error with save data:"+res.Error.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	data.Id = f.Id
 | 
			
		||||
	resp.SUCCESS(c, data)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *FunctionHandler) Set(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Id    uint        `json:"id"`
 | 
			
		||||
		Filed string      `json:"filed"`
 | 
			
		||||
		Value interface{} `json:"value"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	res := h.db.Model(&model.Function{}).Where("id = ?", data.Id).Update(data.Filed, data.Value)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "更新数据库失败!")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *FunctionHandler) List(c *gin.Context) {
 | 
			
		||||
	var items []model.Function
 | 
			
		||||
	res := h.db.Find(&items)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "No data found")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	functions := make([]vo.Function, 0)
 | 
			
		||||
	for _, v := range items {
 | 
			
		||||
		var f vo.Function
 | 
			
		||||
		err := utils.CopyObject(v, &f)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		functions = append(functions, f)
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c, functions)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *FunctionHandler) Remove(c *gin.Context) {
 | 
			
		||||
	id := h.GetInt(c, "id", 0)
 | 
			
		||||
 | 
			
		||||
	if id > 0 {
 | 
			
		||||
		res := h.db.Delete(&model.Function{Id: uint(id)})
 | 
			
		||||
		if res.Error != nil {
 | 
			
		||||
			resp.ERROR(c, "更新数据库失败!")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GenToken generate function api access token
 | 
			
		||||
func (h *FunctionHandler) GenToken(c *gin.Context) {
 | 
			
		||||
	// 创建 token
 | 
			
		||||
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
 | 
			
		||||
		"user_id": 0,
 | 
			
		||||
		"expired": 0,
 | 
			
		||||
	})
 | 
			
		||||
	tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Error("error with generate token", err)
 | 
			
		||||
		resp.ERROR(c)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, tokenString)
 | 
			
		||||
}
 | 
			
		||||
@@ -44,6 +44,8 @@ func (h *OrderHandler) List(c *gin.Context) {
 | 
			
		||||
		end := utils.Str2stamp(data.PayTime[1] + " 00:00:00")
 | 
			
		||||
		session = session.Where("pay_time >= ? AND pay_time <= ?", start, end)
 | 
			
		||||
	}
 | 
			
		||||
	session = session.Where("status = ?", types.OrderPaidSuccess)
 | 
			
		||||
 | 
			
		||||
	var total int64
 | 
			
		||||
	session.Model(&model.Order{}).Count(&total)
 | 
			
		||||
	var items []model.Order
 | 
			
		||||
 
 | 
			
		||||
@@ -33,6 +33,7 @@ func (h *ProductHandler) Save(c *gin.Context) {
 | 
			
		||||
		Enabled   bool    `json:"enabled"`
 | 
			
		||||
		Days      int     `json:"days"`
 | 
			
		||||
		Calls     int     `json:"calls"`
 | 
			
		||||
		ImgCalls  int     `json:"img_calls"`
 | 
			
		||||
		CreatedAt int64   `json:"created_at"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
@@ -40,7 +41,14 @@ func (h *ProductHandler) Save(c *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	item := model.Product{Name: data.Name, Price: data.Price, Discount: data.Discount, Days: data.Days, Calls: data.Calls, Enabled: data.Enabled}
 | 
			
		||||
	item := model.Product{
 | 
			
		||||
		Name:     data.Name,
 | 
			
		||||
		Price:    data.Price,
 | 
			
		||||
		Discount: data.Discount,
 | 
			
		||||
		Days:     data.Days,
 | 
			
		||||
		Calls:    data.Calls,
 | 
			
		||||
		ImgCalls: data.ImgCalls,
 | 
			
		||||
		Enabled:  data.Enabled}
 | 
			
		||||
	item.Id = data.Id
 | 
			
		||||
	if item.Id > 0 {
 | 
			
		||||
		item.CreatedAt = time.Unix(data.CreatedAt, 0)
 | 
			
		||||
 
 | 
			
		||||
@@ -46,7 +46,7 @@ func (h *RewardHandler) List(c *gin.Context) {
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			r.Id = v.Id
 | 
			
		||||
			r.Username = userMap[v.UserId].Mobile
 | 
			
		||||
			r.Username = userMap[v.UserId].Username
 | 
			
		||||
			r.CreatedAt = v.CreatedAt.Unix()
 | 
			
		||||
			r.UpdatedAt = v.UpdatedAt.Unix()
 | 
			
		||||
			rewards = append(rewards, r)
 | 
			
		||||
@@ -55,3 +55,16 @@ func (h *RewardHandler) List(c *gin.Context) {
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, rewards)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *RewardHandler) Remove(c *gin.Context) {
 | 
			
		||||
	id := h.GetInt(c, "id", 0)
 | 
			
		||||
 | 
			
		||||
	if id > 0 {
 | 
			
		||||
		res := h.db.Where("id = ?", id).Delete(&model.Reward{})
 | 
			
		||||
		if res.Error != nil {
 | 
			
		||||
			resp.ERROR(c, "更新数据库失败!")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,8 @@ import (
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
@@ -27,7 +29,7 @@ func NewUserHandler(app *core.AppServer, db *gorm.DB) *UserHandler {
 | 
			
		||||
func (h *UserHandler) List(c *gin.Context) {
 | 
			
		||||
	page := h.GetInt(c, "page", 1)
 | 
			
		||||
	pageSize := h.GetInt(c, "page_size", 20)
 | 
			
		||||
	mobile := h.GetTrim(c, "mobile")
 | 
			
		||||
	username := h.GetTrim(c, "username")
 | 
			
		||||
 | 
			
		||||
	offset := (page - 1) * pageSize
 | 
			
		||||
	var items []model.User
 | 
			
		||||
@@ -35,8 +37,8 @@ func (h *UserHandler) List(c *gin.Context) {
 | 
			
		||||
	var total int64
 | 
			
		||||
 | 
			
		||||
	session := h.db.Session(&gorm.Session{})
 | 
			
		||||
	if mobile != "" {
 | 
			
		||||
		session = session.Where("mobile LIKE ?", "%"+mobile+"%")
 | 
			
		||||
	if username != "" {
 | 
			
		||||
		session = session.Where("username LIKE ?", "%"+username+"%")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	session.Model(&model.User{}).Count(&total)
 | 
			
		||||
@@ -63,13 +65,14 @@ func (h *UserHandler) Save(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Id          uint     `json:"id"`
 | 
			
		||||
		Password    string   `json:"password"`
 | 
			
		||||
		Mobile      string   `json:"mobile"`
 | 
			
		||||
		Username    string   `json:"username"`
 | 
			
		||||
		Calls       int      `json:"calls"`
 | 
			
		||||
		ImgCalls    int      `json:"img_calls"`
 | 
			
		||||
		ChatRoles   []string `json:"chat_roles"`
 | 
			
		||||
		ChatModels  []string `json:"chat_models"`
 | 
			
		||||
		ExpiredTime string   `json:"expired_time"`
 | 
			
		||||
		Status      bool     `json:"status"`
 | 
			
		||||
		Vip         bool     `json:"vip"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
@@ -82,10 +85,11 @@ func (h *UserHandler) Save(c *gin.Context) {
 | 
			
		||||
		user.Id = data.Id
 | 
			
		||||
		// 此处需要用 map 更新,用结构体无法更新 0 值
 | 
			
		||||
		res = h.db.Model(&user).Updates(map[string]interface{}{
 | 
			
		||||
			"mobile":           data.Mobile,
 | 
			
		||||
			"username":         data.Username,
 | 
			
		||||
			"calls":            data.Calls,
 | 
			
		||||
			"img_calls":        data.ImgCalls,
 | 
			
		||||
			"status":           data.Status,
 | 
			
		||||
			"vip":              data.Vip,
 | 
			
		||||
			"chat_roles_json":  utils.JsonEncode(data.ChatRoles),
 | 
			
		||||
			"chat_models_json": utils.JsonEncode(data.ChatModels),
 | 
			
		||||
			"expired_time":     utils.Str2stamp(data.ExpiredTime),
 | 
			
		||||
@@ -93,7 +97,8 @@ func (h *UserHandler) Save(c *gin.Context) {
 | 
			
		||||
	} else {
 | 
			
		||||
		salt := utils.RandString(8)
 | 
			
		||||
		u := model.User{
 | 
			
		||||
			Mobile:      data.Mobile,
 | 
			
		||||
			Username:    data.Username,
 | 
			
		||||
			Nickname:    fmt.Sprintf("极客学长@%d", utils.RandomNumber(6)),
 | 
			
		||||
			Password:    utils.GenPassword(data.Password, salt),
 | 
			
		||||
			Avatar:      "/images/avatar/user.png",
 | 
			
		||||
			Salt:        salt,
 | 
			
		||||
 
 | 
			
		||||
@@ -49,3 +49,11 @@ func (h *BaseHandler) GetUserKey(c *gin.Context) string {
 | 
			
		||||
	}
 | 
			
		||||
	return fmt.Sprintf("users/%v", userId)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *BaseHandler) GetLoginUserId(c *gin.Context) uint {
 | 
			
		||||
	userId, ok := c.Get(types.LoginUserID)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return 0
 | 
			
		||||
	}
 | 
			
		||||
	return uint(utils.IntValue(utils.InterfaceToString(userId), 0))
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -39,7 +39,10 @@ func (h *ChatModelHandler) List(c *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	res := h.db.Where("enabled = ?", true).Where("value IN ?", models).Order("sort_num ASC").Find(&items)
 | 
			
		||||
	// 查询用户有权限访问的模型以及所有开放的模型
 | 
			
		||||
	res := h.db.Where("enabled = ?", true).Where(
 | 
			
		||||
		h.db.Where("value IN ?", models).Or("open =?", true),
 | 
			
		||||
	).Order("sort_num ASC").Find(&items)
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		for _, item := range items {
 | 
			
		||||
			var cm vo.ChatModel
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"io"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
@@ -29,7 +29,7 @@ func (h *ChatHandler) sendAzureMessage(
 | 
			
		||||
	ws *types.WsClient) error {
 | 
			
		||||
	promptCreatedAt := time.Now() // 记录提问时间
 | 
			
		||||
	start := time.Now()
 | 
			
		||||
	var apiKey = userVo.ChatConfig.ApiKeys[session.Model.Platform]
 | 
			
		||||
	var apiKey = model.ApiKey{}
 | 
			
		||||
	response, err := h.doRequest(ctx, req, session.Model.Platform, &apiKey)
 | 
			
		||||
	logger.Info("HTTP请求完成,耗时:", time.Now().Sub(start))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -56,9 +56,6 @@ func (h *ChatHandler) sendAzureMessage(
 | 
			
		||||
		// 循环读取 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()
 | 
			
		||||
@@ -68,34 +65,17 @@ func (h *ChatHandler) sendAzureMessage(
 | 
			
		||||
 | 
			
		||||
			var responseBody = types.ApiResponse{}
 | 
			
		||||
			err = json.Unmarshal([]byte(line[6:]), &responseBody)
 | 
			
		||||
			if err != nil || len(responseBody.Choices) == 0 { // 数据解析出错
 | 
			
		||||
			if err != nil { // 数据解析出错
 | 
			
		||||
				logger.Error(err, line)
 | 
			
		||||
				utils.ReplyMessage(ws, ErrorMsg)
 | 
			
		||||
				utils.ReplyMessage(ws, ErrImg)
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			fun := responseBody.Choices[0].Delta.FunctionCall
 | 
			
		||||
			if functionCall && fun.Name == "" {
 | 
			
		||||
				arguments = append(arguments, fun.Arguments)
 | 
			
		||||
			if len(responseBody.Choices) == 0 {
 | 
			
		||||
				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
 | 
			
		||||
@@ -121,50 +101,6 @@ func (h *ChatHandler) sendAzureMessage(
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		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, ErrImg)
 | 
			
		||||
			} 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.mjService.ChatClients.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 {
 | 
			
		||||
			// 更新用户的对话次数
 | 
			
		||||
@@ -177,7 +113,7 @@ func (h *ChatHandler) sendAzureMessage(
 | 
			
		||||
			useMsg := types.Message{Role: "user", Content: prompt}
 | 
			
		||||
 | 
			
		||||
			// 更新上下文消息,如果是调用函数则不需要更新上下文
 | 
			
		||||
			if h.App.ChatConfig.EnableContext && functionCall == false {
 | 
			
		||||
			if h.App.ChatConfig.EnableContext {
 | 
			
		||||
				chatCtx = append(chatCtx, useMsg)  // 提问消息
 | 
			
		||||
				chatCtx = append(chatCtx, message) // 回复消息
 | 
			
		||||
				h.App.ChatContexts.Put(session.ChatId, chatCtx)
 | 
			
		||||
@@ -185,11 +121,6 @@ func (h *ChatHandler) sendAzureMessage(
 | 
			
		||||
 | 
			
		||||
			// 追加聊天记录
 | 
			
		||||
			if h.App.ChatConfig.EnableHistory {
 | 
			
		||||
				useContext := true
 | 
			
		||||
				if functionCall {
 | 
			
		||||
					useContext = false
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// for prompt
 | 
			
		||||
				promptToken, err := utils.CalcTokens(prompt, req.Model)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
@@ -201,9 +132,9 @@ func (h *ChatHandler) sendAzureMessage(
 | 
			
		||||
					RoleId:     role.Id,
 | 
			
		||||
					Type:       types.PromptMsg,
 | 
			
		||||
					Icon:       userVo.Avatar,
 | 
			
		||||
					Content:    prompt,
 | 
			
		||||
					Content:    template.HTMLEscapeString(prompt),
 | 
			
		||||
					Tokens:     promptToken,
 | 
			
		||||
					UseContext: useContext,
 | 
			
		||||
					UseContext: true,
 | 
			
		||||
				}
 | 
			
		||||
				historyUserMsg.CreatedAt = promptCreatedAt
 | 
			
		||||
				historyUserMsg.UpdatedAt = promptCreatedAt
 | 
			
		||||
@@ -213,15 +144,7 @@ func (h *ChatHandler) sendAzureMessage(
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// 计算本次对话消耗的总 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, _ := utils.CalcTokens(message.Content, req.Model)
 | 
			
		||||
				totalTokens += getTotalTokens(req)
 | 
			
		||||
 | 
			
		||||
				historyReplyMsg := model.HistoryMessage{
 | 
			
		||||
@@ -232,7 +155,7 @@ func (h *ChatHandler) sendAzureMessage(
 | 
			
		||||
					Icon:       role.Icon,
 | 
			
		||||
					Content:    message.Content,
 | 
			
		||||
					Tokens:     totalTokens,
 | 
			
		||||
					UseContext: useContext,
 | 
			
		||||
					UseContext: true,
 | 
			
		||||
				}
 | 
			
		||||
				historyReplyMsg.CreatedAt = replyCreatedAt
 | 
			
		||||
				historyReplyMsg.UpdatedAt = replyCreatedAt
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strings"
 | 
			
		||||
@@ -45,7 +46,7 @@ func (h *ChatHandler) sendBaiduMessage(
 | 
			
		||||
	ws *types.WsClient) error {
 | 
			
		||||
	promptCreatedAt := time.Now() // 记录提问时间
 | 
			
		||||
	start := time.Now()
 | 
			
		||||
	var apiKey = userVo.ChatConfig.ApiKeys[session.Model.Platform]
 | 
			
		||||
	var apiKey = model.ApiKey{}
 | 
			
		||||
	response, err := h.doRequest(ctx, req, session.Model.Platform, &apiKey)
 | 
			
		||||
	logger.Info("HTTP请求完成,耗时:", time.Now().Sub(start))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -84,6 +85,11 @@ func (h *ChatHandler) sendBaiduMessage(
 | 
			
		||||
				content = line[5:]
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// 处理代码换行
 | 
			
		||||
			if len(content) == 0 {
 | 
			
		||||
				content = "\n"
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			var resp baiduResp
 | 
			
		||||
			err := utils.JsonDecode(content, &resp)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
@@ -151,7 +157,7 @@ func (h *ChatHandler) sendBaiduMessage(
 | 
			
		||||
					RoleId:     role.Id,
 | 
			
		||||
					Type:       types.PromptMsg,
 | 
			
		||||
					Icon:       userVo.Avatar,
 | 
			
		||||
					Content:    prompt,
 | 
			
		||||
					Content:    template.HTMLEscapeString(prompt),
 | 
			
		||||
					Tokens:     promptToken,
 | 
			
		||||
					UseContext: true,
 | 
			
		||||
				}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,8 +6,6 @@ import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/handler"
 | 
			
		||||
	logger2 "chatplus/logger"
 | 
			
		||||
	"chatplus/service/mj"
 | 
			
		||||
	"chatplus/store"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
@@ -16,40 +14,45 @@ import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/go-redis/redis/v8"
 | 
			
		||||
	"github.com/gorilla/websocket"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/go-redis/redis/v8"
 | 
			
		||||
	"github.com/gorilla/websocket"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const ErrorMsg = "抱歉,AI 助手开小差了,请稍后再试。"
 | 
			
		||||
const ErrImg = ""
 | 
			
		||||
 | 
			
		||||
var ErrImg = ""
 | 
			
		||||
 | 
			
		||||
var logger = logger2.GetLogger()
 | 
			
		||||
 | 
			
		||||
type ChatHandler struct {
 | 
			
		||||
	handler.BaseHandler
 | 
			
		||||
	db        *gorm.DB
 | 
			
		||||
	leveldb   *store.LevelDB
 | 
			
		||||
	redis     *redis.Client
 | 
			
		||||
	mjService *mj.Service
 | 
			
		||||
	db    *gorm.DB
 | 
			
		||||
	redis *redis.Client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewChatHandler(app *core.AppServer, db *gorm.DB, levelDB *store.LevelDB, redis *redis.Client, service *mj.Service) *ChatHandler {
 | 
			
		||||
func NewChatHandler(app *core.AppServer, db *gorm.DB, redis *redis.Client) *ChatHandler {
 | 
			
		||||
	h := ChatHandler{
 | 
			
		||||
		db:        db,
 | 
			
		||||
		leveldb:   levelDB,
 | 
			
		||||
		redis:     redis,
 | 
			
		||||
		mjService: service,
 | 
			
		||||
		db:    db,
 | 
			
		||||
		redis: redis,
 | 
			
		||||
	}
 | 
			
		||||
	h.App = app
 | 
			
		||||
	return &h
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *ChatHandler) Init() {
 | 
			
		||||
	// 如果后台有上传微信客服微信二维码,则覆盖
 | 
			
		||||
	if h.App.SysConfig.WechatCardURL != "" {
 | 
			
		||||
		ErrImg = fmt.Sprintf("", h.App.SysConfig.WechatCardURL)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var chatConfig types.ChatConfig
 | 
			
		||||
 | 
			
		||||
// ChatHandle 处理聊天 WebSocket 请求
 | 
			
		||||
@@ -86,7 +89,7 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) {
 | 
			
		||||
		session = &types.ChatSession{
 | 
			
		||||
			SessionId: sessionId,
 | 
			
		||||
			ClientIP:  c.ClientIP(),
 | 
			
		||||
			Username:  user.Mobile,
 | 
			
		||||
			Username:  user.Username,
 | 
			
		||||
			UserId:    user.Id,
 | 
			
		||||
		}
 | 
			
		||||
		h.App.ChatSession.Put(sessionId, session)
 | 
			
		||||
@@ -131,7 +134,6 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) {
 | 
			
		||||
		for {
 | 
			
		||||
			_, msg, err := client.Receive()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				logger.Error(err)
 | 
			
		||||
				client.Close()
 | 
			
		||||
				h.App.ChatClients.Delete(sessionId)
 | 
			
		||||
				cancelFunc := h.App.ReqCancelFunc.Get(sessionId)
 | 
			
		||||
@@ -142,19 +144,30 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) {
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			message := string(msg)
 | 
			
		||||
			logger.Info("Receive a message: ", message)
 | 
			
		||||
			//utils.ReplyMessage(client, "这是一条测试消息!")
 | 
			
		||||
			var message types.WsMessage
 | 
			
		||||
			err = utils.JsonDecode(string(msg), &message)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// 心跳消息
 | 
			
		||||
			if message.Type == "heartbeat" {
 | 
			
		||||
				logger.Debug("收到 Chat 心跳消息:", message.Content)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			logger.Info("Receive a message: ", message.Content)
 | 
			
		||||
 | 
			
		||||
			ctx, cancel := context.WithCancel(context.Background())
 | 
			
		||||
			h.App.ReqCancelFunc.Put(sessionId, cancel)
 | 
			
		||||
			// 回复消息
 | 
			
		||||
			err = h.sendMessage(ctx, session, chatRole, message, client)
 | 
			
		||||
			err = h.sendMessage(ctx, session, chatRole, utils.InterfaceToString(message.Content), 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))
 | 
			
		||||
				logger.Infof("回答完毕: %v", message.Content)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
@@ -162,11 +175,13 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSession, role model.ChatRole, prompt string, ws *types.WsClient) error {
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if r := recover(); r != nil {
 | 
			
		||||
			logger.Error("Recover message from error: ", r)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
	if !h.App.Debug {
 | 
			
		||||
		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)
 | 
			
		||||
@@ -220,23 +235,60 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
 | 
			
		||||
	case types.Baidu:
 | 
			
		||||
		req.Temperature = h.App.ChatConfig.OpenAI.Temperature
 | 
			
		||||
		// TODO: 目前只支持 ERNIE-Bot-turbo 模型,如果是 ERNIE-Bot 模型则需要增加函数支持
 | 
			
		||||
		break
 | 
			
		||||
	case types.OpenAI:
 | 
			
		||||
		req.Temperature = h.App.ChatConfig.OpenAI.Temperature
 | 
			
		||||
		req.MaxTokens = h.App.ChatConfig.OpenAI.MaxTokens
 | 
			
		||||
		// OpenAI 支持函数功能
 | 
			
		||||
		if h.App.SysConfig.EnabledFunction {
 | 
			
		||||
			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)
 | 
			
		||||
		var items []model.Function
 | 
			
		||||
		res := h.db.Where("enabled", true).Find(&items)
 | 
			
		||||
		if res.Error != nil {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var tools = make([]interface{}, 0)
 | 
			
		||||
		var functions = make([]interface{}, 0)
 | 
			
		||||
		for _, v := range items {
 | 
			
		||||
			var parameters map[string]interface{}
 | 
			
		||||
			err = utils.JsonDecode(v.Parameters, ¶meters)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			required := parameters["required"]
 | 
			
		||||
			delete(parameters, "required")
 | 
			
		||||
			tools = append(tools, gin.H{
 | 
			
		||||
				"type": "function",
 | 
			
		||||
				"function": gin.H{
 | 
			
		||||
					"name":        v.Name,
 | 
			
		||||
					"description": v.Description,
 | 
			
		||||
					"parameters":  parameters,
 | 
			
		||||
					"required":    required,
 | 
			
		||||
				},
 | 
			
		||||
			})
 | 
			
		||||
			functions = append(functions, gin.H{
 | 
			
		||||
				"name":        v.Name,
 | 
			
		||||
				"description": v.Description,
 | 
			
		||||
				"parameters":  parameters,
 | 
			
		||||
				"required":    required,
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		//if len(tools) > 0 {
 | 
			
		||||
		//	req.Tools = tools
 | 
			
		||||
		//	req.ToolChoice = "auto"
 | 
			
		||||
		//}
 | 
			
		||||
		if len(functions) > 0 {
 | 
			
		||||
			req.Functions = functions
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	case types.XunFei:
 | 
			
		||||
		req.Temperature = h.App.ChatConfig.XunFei.Temperature
 | 
			
		||||
		req.MaxTokens = h.App.ChatConfig.XunFei.MaxTokens
 | 
			
		||||
		break
 | 
			
		||||
	case types.QWen:
 | 
			
		||||
		req.Input = map[string]interface{}{"messages": []map[string]string{{"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": prompt}}}
 | 
			
		||||
		req.Parameters = map[string]interface{}{}
 | 
			
		||||
		break
 | 
			
		||||
	default:
 | 
			
		||||
		utils.ReplyMessage(ws, "不支持的平台:"+session.Model.Platform+",请联系管理员!")
 | 
			
		||||
		utils.ReplyMessage(ws, ErrImg)
 | 
			
		||||
@@ -251,18 +303,15 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
 | 
			
		||||
		} else {
 | 
			
		||||
			// calculate the tokens of current request, to prevent to exceeding the max tokens num
 | 
			
		||||
			tokens := req.MaxTokens
 | 
			
		||||
			for _, f := range types.InnerFunctions {
 | 
			
		||||
				tks, _ := utils.CalcTokens(utils.JsonEncode(f), req.Model)
 | 
			
		||||
				tokens += tks
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			tks, _ := utils.CalcTokens(utils.JsonEncode(req.Tools), req.Model)
 | 
			
		||||
			tokens += tks
 | 
			
		||||
			// loading the role context
 | 
			
		||||
			var messages []types.Message
 | 
			
		||||
			err := utils.JsonDecode(role.Context, &messages)
 | 
			
		||||
			if err == nil {
 | 
			
		||||
				for _, v := range messages {
 | 
			
		||||
					tks, _ := utils.CalcTokens(v.Content, req.Model)
 | 
			
		||||
					if tokens+tks >= types.ModelToTokens[req.Model] {
 | 
			
		||||
					if tokens+tks >= types.GetModelMaxToken(req.Model) {
 | 
			
		||||
						break
 | 
			
		||||
					}
 | 
			
		||||
					tokens += tks
 | 
			
		||||
@@ -277,7 +326,7 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
 | 
			
		||||
				if res.Error == nil {
 | 
			
		||||
					for i := len(historyMessages) - 1; i >= 0; i-- {
 | 
			
		||||
						msg := historyMessages[i]
 | 
			
		||||
						if tokens+msg.Tokens >= types.ModelToTokens[session.Model.Value] {
 | 
			
		||||
						if tokens+msg.Tokens >= types.GetModelMaxToken(session.Model.Value) {
 | 
			
		||||
							break
 | 
			
		||||
						}
 | 
			
		||||
						tokens += msg.Tokens
 | 
			
		||||
@@ -313,7 +362,8 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
 | 
			
		||||
		return h.sendBaiduMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
 | 
			
		||||
	case types.XunFei:
 | 
			
		||||
		return h.sendXunFeiMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
 | 
			
		||||
 | 
			
		||||
	case types.QWen:
 | 
			
		||||
		return h.sendQWenMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
 | 
			
		||||
	}
 | 
			
		||||
	utils.ReplyChunkMessage(ws, types.WsMessage{
 | 
			
		||||
		Type:    types.WsMiddle,
 | 
			
		||||
@@ -325,8 +375,9 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
 | 
			
		||||
// Tokens 统计 token 数量
 | 
			
		||||
func (h *ChatHandler) Tokens(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Text  string `json:"text"`
 | 
			
		||||
		Model string `json:"model"`
 | 
			
		||||
		Text   string `json:"text"`
 | 
			
		||||
		Model  string `json:"model"`
 | 
			
		||||
		ChatId string `json:"chat_id"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
@@ -334,10 +385,10 @@ func (h *ChatHandler) Tokens(c *gin.Context) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 如果没有传入 text 字段,则说明是获取当前 reply 总的 token 消耗(带上下文)
 | 
			
		||||
	if data.Text == "" {
 | 
			
		||||
	if data.Text == "" && data.ChatId != "" {
 | 
			
		||||
		var item model.HistoryMessage
 | 
			
		||||
		userId, _ := c.Get(types.LoginUserID)
 | 
			
		||||
		res := h.db.Where("user_id = ?", userId).Last(&item)
 | 
			
		||||
		res := h.db.Where("user_id = ?", userId).Where("chat_id = ?", data.ChatId).Last(&item)
 | 
			
		||||
		if res.Error != nil {
 | 
			
		||||
			resp.ERROR(c, res.Error.Error())
 | 
			
		||||
			return
 | 
			
		||||
@@ -387,39 +438,41 @@ func (h *ChatHandler) StopGenerate(c *gin.Context) {
 | 
			
		||||
 | 
			
		||||
// 发送请求到 OpenAI 服务器
 | 
			
		||||
// useOwnApiKey: 是否使用了用户自己的 API KEY
 | 
			
		||||
func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platform types.Platform, apiKey *string) (*http.Response, error) {
 | 
			
		||||
 | 
			
		||||
func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platform types.Platform, apiKey *model.ApiKey) (*http.Response, error) {
 | 
			
		||||
	res := h.db.Where("platform = ?", platform).Where("type = ?", "chat").Where("enabled = ?", true).Order("last_used_at ASC").First(apiKey)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		return nil, errors.New("no available key, please import key")
 | 
			
		||||
	}
 | 
			
		||||
	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)
 | 
			
		||||
		apiURL = strings.Replace(apiKey.ApiURL, "{model}", md, 1)
 | 
			
		||||
		break
 | 
			
		||||
	case types.ChatGLM:
 | 
			
		||||
		apiURL = strings.Replace(h.App.ChatConfig.ChatGML.ApiURL, "{model}", req.Model, 1)
 | 
			
		||||
		apiURL = strings.Replace(apiKey.ApiURL, "{model}", req.Model, 1)
 | 
			
		||||
		req.Prompt = req.Messages // 使用 prompt 字段替代 message 字段
 | 
			
		||||
		req.Messages = nil
 | 
			
		||||
		break
 | 
			
		||||
	case types.Baidu:
 | 
			
		||||
		apiURL = strings.Replace(h.App.ChatConfig.Baidu.ApiURL, "{model}", req.Model, 1)
 | 
			
		||||
		apiURL = strings.Replace(apiKey.ApiURL, "{model}", req.Model, 1)
 | 
			
		||||
		break
 | 
			
		||||
	case types.QWen:
 | 
			
		||||
		apiURL = apiKey.ApiURL
 | 
			
		||||
		req.Messages = nil
 | 
			
		||||
		break
 | 
			
		||||
	default:
 | 
			
		||||
		apiURL = h.App.ChatConfig.OpenAI.ApiURL
 | 
			
		||||
	}
 | 
			
		||||
	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")
 | 
			
		||||
		if req.Model == "gpt-4-all" || strings.HasPrefix(req.Model, "gpt-4-gizmo-g-") {
 | 
			
		||||
			apiURL = "https://gpt.bemore.lol/v1/chat/completions"
 | 
			
		||||
		} else {
 | 
			
		||||
			apiURL = apiKey.ApiURL
 | 
			
		||||
		}
 | 
			
		||||
		// 更新 API KEY 的最后使用时间
 | 
			
		||||
		h.db.Model(&key).UpdateColumn("last_used_at", time.Now().Unix())
 | 
			
		||||
		*apiKey = key.Value
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 更新 API KEY 的最后使用时间
 | 
			
		||||
	h.db.Model(apiKey).UpdateColumn("last_used_at", time.Now().Unix())
 | 
			
		||||
	// 百度文心,需要串接 access_token
 | 
			
		||||
	if platform == types.Baidu {
 | 
			
		||||
		token, err := h.getBaiduToken(*apiKey)
 | 
			
		||||
		token, err := h.getBaiduToken(apiKey.Value)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
@@ -427,6 +480,8 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platf
 | 
			
		||||
		apiURL = fmt.Sprintf("%s?access_token=%s", apiURL, token)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logger.Debugf(utils.JsonEncode(req))
 | 
			
		||||
 | 
			
		||||
	// 创建 HttpClient 请求对象
 | 
			
		||||
	var client *http.Client
 | 
			
		||||
	requestBody, err := json.Marshal(req)
 | 
			
		||||
@@ -440,8 +495,9 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platf
 | 
			
		||||
 | 
			
		||||
	request = request.WithContext(ctx)
 | 
			
		||||
	request.Header.Set("Content-Type", "application/json")
 | 
			
		||||
	proxyURL := h.App.Config.ProxyURL
 | 
			
		||||
	if proxyURL != "" && platform == types.OpenAI { // 使用代理
 | 
			
		||||
	var proxyURL string
 | 
			
		||||
	if h.App.Config.ProxyURL != "" && apiKey.UseProxy { // 使用代理
 | 
			
		||||
		proxyURL = h.App.Config.ProxyURL
 | 
			
		||||
		proxy, _ := url.Parse(proxyURL)
 | 
			
		||||
		client = &http.Client{
 | 
			
		||||
			Transport: &http.Transport{
 | 
			
		||||
@@ -451,23 +507,27 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platf
 | 
			
		||||
	} else {
 | 
			
		||||
		client = http.DefaultClient
 | 
			
		||||
	}
 | 
			
		||||
	logger.Infof("Sending %s request, KEY: %s, PROXY: %s, Model: %s", platform, *apiKey, proxyURL, req.Model)
 | 
			
		||||
	logger.Debugf("Sending %s request, ApiURL:%s, Password:%s, PROXY: %s, Model: %s", platform, apiURL, apiKey.Value, proxyURL, req.Model)
 | 
			
		||||
	switch platform {
 | 
			
		||||
	case types.Azure:
 | 
			
		||||
		request.Header.Set("api-key", *apiKey)
 | 
			
		||||
		request.Header.Set("api-key", apiKey.Value)
 | 
			
		||||
		break
 | 
			
		||||
	case types.ChatGLM:
 | 
			
		||||
		token, err := h.getChatGLMToken(*apiKey)
 | 
			
		||||
		token, err := h.getChatGLMToken(apiKey.Value)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		logger.Info(token)
 | 
			
		||||
		request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
 | 
			
		||||
		break
 | 
			
		||||
	case types.Baidu:
 | 
			
		||||
		request.RequestURI = ""
 | 
			
		||||
	case types.OpenAI:
 | 
			
		||||
		request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiKey))
 | 
			
		||||
		request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey.Value))
 | 
			
		||||
		break
 | 
			
		||||
	case types.QWen:
 | 
			
		||||
		request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey.Value))
 | 
			
		||||
		request.Header.Set("X-DashScope-SSE", "enable")
 | 
			
		||||
		break
 | 
			
		||||
	}
 | 
			
		||||
	return client.Do(request)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/golang-jwt/jwt/v5"
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"io"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
@@ -29,7 +30,7 @@ func (h *ChatHandler) sendChatGLMMessage(
 | 
			
		||||
	ws *types.WsClient) error {
 | 
			
		||||
	promptCreatedAt := time.Now() // 记录提问时间
 | 
			
		||||
	start := time.Now()
 | 
			
		||||
	var apiKey = userVo.ChatConfig.ApiKeys[session.Model.Platform]
 | 
			
		||||
	var apiKey = model.ApiKey{}
 | 
			
		||||
	response, err := h.doRequest(ctx, req, session.Model.Platform, &apiKey)
 | 
			
		||||
	logger.Info("HTTP请求完成,耗时:", time.Now().Sub(start))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -71,6 +72,10 @@ func (h *ChatHandler) sendChatGLMMessage(
 | 
			
		||||
			if strings.HasPrefix(line, "data:") {
 | 
			
		||||
				content = line[5:]
 | 
			
		||||
			}
 | 
			
		||||
			// 处理代码换行
 | 
			
		||||
			if len(content) == 0 {
 | 
			
		||||
				content = "\n"
 | 
			
		||||
			}
 | 
			
		||||
			switch event {
 | 
			
		||||
			case "add":
 | 
			
		||||
				if len(contents) == 0 {
 | 
			
		||||
@@ -131,7 +136,7 @@ func (h *ChatHandler) sendChatGLMMessage(
 | 
			
		||||
					RoleId:     role.Id,
 | 
			
		||||
					Type:       types.PromptMsg,
 | 
			
		||||
					Icon:       userVo.Avatar,
 | 
			
		||||
					Content:    prompt,
 | 
			
		||||
					Content:    template.HTMLEscapeString(prompt),
 | 
			
		||||
					Tokens:     promptToken,
 | 
			
		||||
					UseContext: true,
 | 
			
		||||
				}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,11 +9,13 @@ import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"io"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
	"unicode/utf8"
 | 
			
		||||
 | 
			
		||||
	req2 "github.com/imroc/req/v3"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// OPenAI 消息发送实现
 | 
			
		||||
@@ -28,7 +30,7 @@ func (h *ChatHandler) sendOpenAiMessage(
 | 
			
		||||
	ws *types.WsClient) error {
 | 
			
		||||
	promptCreatedAt := time.Now() // 记录提问时间
 | 
			
		||||
	start := time.Now()
 | 
			
		||||
	var apiKey = userVo.ChatConfig.ApiKeys[session.Model.Platform]
 | 
			
		||||
	var apiKey = model.ApiKey{}
 | 
			
		||||
	response, err := h.doRequest(ctx, req, session.Model.Platform, &apiKey)
 | 
			
		||||
	logger.Info("HTTP请求完成,耗时:", time.Now().Sub(start))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -55,8 +57,8 @@ func (h *ChatHandler) sendOpenAiMessage(
 | 
			
		||||
		// 循环读取 Chunk 消息
 | 
			
		||||
		var message = types.Message{}
 | 
			
		||||
		var contents = make([]string, 0)
 | 
			
		||||
		var functionCall = false
 | 
			
		||||
		var functionName string
 | 
			
		||||
		var function model.Function
 | 
			
		||||
		var toolCall = false
 | 
			
		||||
		var arguments = make([]string, 0)
 | 
			
		||||
		scanner := bufio.NewScanner(response.Body)
 | 
			
		||||
		for scanner.Scan() {
 | 
			
		||||
@@ -74,24 +76,37 @@ func (h *ChatHandler) sendOpenAiMessage(
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			var tool types.ToolCall
 | 
			
		||||
			if len(responseBody.Choices[0].Delta.ToolCalls) > 0 {
 | 
			
		||||
				tool = responseBody.Choices[0].Delta.ToolCalls[0]
 | 
			
		||||
				if toolCall && tool.Function.Name == "" {
 | 
			
		||||
					arguments = append(arguments, tool.Function.Arguments)
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// 兼容 Function Call
 | 
			
		||||
			fun := responseBody.Choices[0].Delta.FunctionCall
 | 
			
		||||
			if functionCall && fun.Name == "" {
 | 
			
		||||
			if fun.Name != "" {
 | 
			
		||||
				tool = *new(types.ToolCall)
 | 
			
		||||
				tool.Function.Name = fun.Name
 | 
			
		||||
			} else if toolCall {
 | 
			
		||||
				arguments = append(arguments, fun.Arguments)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if !utils.IsEmptyValue(fun) {
 | 
			
		||||
				functionName = fun.Name
 | 
			
		||||
				f := h.App.Functions[functionName]
 | 
			
		||||
				if f != nil {
 | 
			
		||||
					functionCall = true
 | 
			
		||||
			if !utils.IsEmptyValue(tool) {
 | 
			
		||||
				res := h.db.Where("name = ?", tool.Function.Name).First(&function)
 | 
			
		||||
				if res.Error == nil {
 | 
			
		||||
					toolCall = 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())})
 | 
			
		||||
					utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: fmt.Sprintf("正在调用工具 `%s` 作答 ...\n\n", function.Label)})
 | 
			
		||||
				}
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if responseBody.Choices[0].FinishReason == "function_call" { // 函数调用完毕
 | 
			
		||||
			if responseBody.Choices[0].FinishReason == "tool_calls" ||
 | 
			
		||||
				responseBody.Choices[0].FinishReason == "function_call" { // 函数调用完毕
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
@@ -120,47 +135,35 @@ func (h *ChatHandler) sendOpenAiMessage(
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if functionCall { // 调用函数完成任务
 | 
			
		||||
		if toolCall { // 调用函数完成任务
 | 
			
		||||
			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, ErrImg)
 | 
			
		||||
			logger.Debugf("函数名称: %s, 函数参数:%s", function.Name, params)
 | 
			
		||||
			params["user_id"] = userVo.Id
 | 
			
		||||
			var apiRes types.BizVo
 | 
			
		||||
			r, err := req2.C().R().SetHeader("Content-Type", "application/json").
 | 
			
		||||
				SetHeader("Authorization", function.Token).
 | 
			
		||||
				SetBody(params).
 | 
			
		||||
				SetSuccessResult(&apiRes).Post(function.Action)
 | 
			
		||||
			errMsg := ""
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				errMsg = err.Error()
 | 
			
		||||
			} else if r.IsErrorState() {
 | 
			
		||||
				errMsg = r.Status
 | 
			
		||||
			}
 | 
			
		||||
			if errMsg != "" || apiRes.Code != types.Success {
 | 
			
		||||
				msg := "调用函数工具出错:" + apiRes.Message + errMsg
 | 
			
		||||
				utils.ReplyChunkMessage(ws, types.WsMessage{
 | 
			
		||||
					Type:    types.WsMiddle,
 | 
			
		||||
					Content: msg,
 | 
			
		||||
				})
 | 
			
		||||
				contents = append(contents, msg)
 | 
			
		||||
			} 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.mjService.ChatClients.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)
 | 
			
		||||
				}
 | 
			
		||||
				utils.ReplyChunkMessage(ws, types.WsMessage{
 | 
			
		||||
					Type:    types.WsMiddle,
 | 
			
		||||
					Content: apiRes.Data,
 | 
			
		||||
				})
 | 
			
		||||
				contents = append(contents, utils.InterfaceToString(apiRes.Data))
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@@ -176,7 +179,7 @@ func (h *ChatHandler) sendOpenAiMessage(
 | 
			
		||||
			useMsg := types.Message{Role: "user", Content: prompt}
 | 
			
		||||
 | 
			
		||||
			// 更新上下文消息,如果是调用函数则不需要更新上下文
 | 
			
		||||
			if h.App.ChatConfig.EnableContext && functionCall == false {
 | 
			
		||||
			if h.App.ChatConfig.EnableContext && toolCall == false {
 | 
			
		||||
				chatCtx = append(chatCtx, useMsg)  // 提问消息
 | 
			
		||||
				chatCtx = append(chatCtx, message) // 回复消息
 | 
			
		||||
				h.App.ChatContexts.Put(session.ChatId, chatCtx)
 | 
			
		||||
@@ -185,7 +188,7 @@ func (h *ChatHandler) sendOpenAiMessage(
 | 
			
		||||
			// 追加聊天记录
 | 
			
		||||
			if h.App.ChatConfig.EnableHistory {
 | 
			
		||||
				useContext := true
 | 
			
		||||
				if functionCall {
 | 
			
		||||
				if toolCall {
 | 
			
		||||
					useContext = false
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
@@ -200,7 +203,7 @@ func (h *ChatHandler) sendOpenAiMessage(
 | 
			
		||||
					RoleId:     role.Id,
 | 
			
		||||
					Type:       types.PromptMsg,
 | 
			
		||||
					Icon:       userVo.Avatar,
 | 
			
		||||
					Content:    prompt,
 | 
			
		||||
					Content:    template.HTMLEscapeString(prompt),
 | 
			
		||||
					Tokens:     promptToken,
 | 
			
		||||
					UseContext: useContext,
 | 
			
		||||
				}
 | 
			
		||||
@@ -213,8 +216,8 @@ func (h *ChatHandler) sendOpenAiMessage(
 | 
			
		||||
 | 
			
		||||
				// 计算本次对话消耗的总 token 数量
 | 
			
		||||
				var totalTokens = 0
 | 
			
		||||
				if functionCall { // prompt + 函数名 + 参数 token
 | 
			
		||||
					tokens, _ := utils.CalcTokens(functionName, req.Model)
 | 
			
		||||
				if toolCall { // prompt + 函数名 + 参数 token
 | 
			
		||||
					tokens, _ := utils.CalcTokens(function.Name, req.Model)
 | 
			
		||||
					totalTokens += tokens
 | 
			
		||||
					tokens, _ = utils.CalcTokens(utils.InterfaceToString(arguments), req.Model)
 | 
			
		||||
					totalTokens += tokens
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										229
									
								
								api/handler/chatimpl/qwen_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										229
									
								
								api/handler/chatimpl/qwen_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,229 @@
 | 
			
		||||
package chatimpl
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"io"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
	"unicode/utf8"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type qWenResp struct {
 | 
			
		||||
	Output struct {
 | 
			
		||||
		FinishReason string `json:"finish_reason"`
 | 
			
		||||
		Text         string `json:"text"`
 | 
			
		||||
	} `json:"output"`
 | 
			
		||||
	Usage struct {
 | 
			
		||||
		TotalTokens  int `json:"total_tokens"`
 | 
			
		||||
		InputTokens  int `json:"input_tokens"`
 | 
			
		||||
		OutputTokens int `json:"output_tokens"`
 | 
			
		||||
	} `json:"usage"`
 | 
			
		||||
	RequestID string `json:"request_id"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 通义千问消息发送实现
 | 
			
		||||
func (h *ChatHandler) sendQWenMessage(
 | 
			
		||||
	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 = model.ApiKey{}
 | 
			
		||||
	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, ErrImg)
 | 
			
		||||
		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)
 | 
			
		||||
		scanner := bufio.NewScanner(response.Body)
 | 
			
		||||
 | 
			
		||||
		var content, lastText, newText string
 | 
			
		||||
 | 
			
		||||
		for scanner.Scan() {
 | 
			
		||||
			line := scanner.Text()
 | 
			
		||||
			if len(line) < 5 || strings.HasPrefix(line, "id:") ||
 | 
			
		||||
				strings.HasPrefix(line, "event:") || strings.HasPrefix(line, ":HTTP_STATUS/200") {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			if strings.HasPrefix(line, "data:") {
 | 
			
		||||
				content = line[5:]
 | 
			
		||||
			}
 | 
			
		||||
			// 处理代码换行
 | 
			
		||||
			if len(content) == 0 {
 | 
			
		||||
				content = "\n"
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			var resp qWenResp
 | 
			
		||||
			err := utils.JsonDecode(content, &resp)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				logger.Error("error with parse data line: ", err)
 | 
			
		||||
				utils.ReplyMessage(ws, fmt.Sprintf("**解析数据行失败:%s**", err))
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if len(contents) == 0 { // 发送消息头
 | 
			
		||||
				utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			//通过比较 lastText(上一次的文本)和 currentText(当前的文本),
 | 
			
		||||
			//提取出新添加的文本部分。然后只将这部分新文本发送到客户端。
 | 
			
		||||
			//每次循环结束后,lastText 会更新为当前的完整文本,以便于下一次循环进行比较。
 | 
			
		||||
			currentText := resp.Output.Text
 | 
			
		||||
			if currentText != lastText {
 | 
			
		||||
				// 提取新增文本
 | 
			
		||||
				newText = strings.Replace(currentText, lastText, "", 1)
 | 
			
		||||
				utils.ReplyChunkMessage(ws, types.WsMessage{
 | 
			
		||||
					Type:    types.WsMiddle,
 | 
			
		||||
					Content: utils.InterfaceToString(newText),
 | 
			
		||||
				})
 | 
			
		||||
				lastText = currentText // 更新 lastText
 | 
			
		||||
			}
 | 
			
		||||
			contents = append(contents, newText)
 | 
			
		||||
 | 
			
		||||
			if resp.Output.FinishReason == "stop" {
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		} //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 {
 | 
			
		||||
			// 更新用户的对话次数
 | 
			
		||||
			h.subUserCalls(userVo, session)
 | 
			
		||||
 | 
			
		||||
			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:    template.HTMLEscapeString(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.incUserTokenFee(userVo.Id, 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:"error_code"`
 | 
			
		||||
			Msg  string `json:"error_msg"`
 | 
			
		||||
		}
 | 
			
		||||
		err = json.Unmarshal(body, &res)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("error with decode response: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
		utils.ReplyMessage(ws, "请求通义千问大模型 API 失败:"+res.Msg)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
@@ -12,6 +12,7 @@ import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/gorilla/websocket"
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
@@ -48,6 +49,12 @@ type xunFeiResp struct {
 | 
			
		||||
	} `json:"payload"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var Model2URL = map[string]string{
 | 
			
		||||
	"general":   "v1.1",
 | 
			
		||||
	"generalv2": "v2.1",
 | 
			
		||||
	"generalv3": "v3.1",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 科大讯飞消息发送实现
 | 
			
		||||
 | 
			
		||||
func (h *ChatHandler) sendXunFeiMessage(
 | 
			
		||||
@@ -60,35 +67,25 @@ func (h *ChatHandler) sendXunFeiMessage(
 | 
			
		||||
	prompt string,
 | 
			
		||||
	ws *types.WsClient) error {
 | 
			
		||||
	promptCreatedAt := time.Now() // 记录提问时间
 | 
			
		||||
	var apiKey = userVo.ChatConfig.ApiKeys[session.Model.Platform]
 | 
			
		||||
	if apiKey == "" {
 | 
			
		||||
		var key model.ApiKey
 | 
			
		||||
		res := h.db.Where("platform = ?", session.Model.Platform).Order("last_used_at ASC").First(&key)
 | 
			
		||||
		if res.Error != nil {
 | 
			
		||||
			utils.ReplyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
		// 更新 API KEY 的最后使用时间
 | 
			
		||||
		h.db.Model(&key).UpdateColumn("last_used_at", time.Now().Unix())
 | 
			
		||||
		apiKey = key.Value
 | 
			
		||||
	var apiKey model.ApiKey
 | 
			
		||||
	res := h.db.Where("platform = ?", session.Model.Platform).Where("type = ?", "chat").Where("enabled = ?", true).Order("last_used_at ASC").First(&apiKey)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		utils.ReplyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	// 更新 API KEY 的最后使用时间
 | 
			
		||||
	h.db.Model(&apiKey).UpdateColumn("last_used_at", time.Now().Unix())
 | 
			
		||||
 | 
			
		||||
	d := websocket.Dialer{
 | 
			
		||||
		HandshakeTimeout: 5 * time.Second,
 | 
			
		||||
	}
 | 
			
		||||
	key := strings.Split(apiKey, "|")
 | 
			
		||||
	key := strings.Split(apiKey.Value, "|")
 | 
			
		||||
	if len(key) != 3 {
 | 
			
		||||
		utils.ReplyMessage(ws, "非法的 API KEY!")
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var apiURL string
 | 
			
		||||
	if req.Model == "generalv2" {
 | 
			
		||||
		apiURL = strings.Replace(h.App.ChatConfig.XunFei.ApiURL, "{version}", "v2.1", 1)
 | 
			
		||||
	} else {
 | 
			
		||||
		apiURL = strings.Replace(h.App.ChatConfig.XunFei.ApiURL, "{version}", "v1.1", 1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	apiURL := strings.Replace(apiKey.ApiURL, "{version}", Model2URL[req.Model], 1)
 | 
			
		||||
	wsURL, err := assembleAuthUrl(apiURL, key[1], key[2])
 | 
			
		||||
	//握手并建立websocket 连接
 | 
			
		||||
	conn, resp, err := d.Dial(wsURL, nil)
 | 
			
		||||
@@ -138,6 +135,10 @@ func (h *ChatHandler) sendXunFeiMessage(
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		content = result.Payload.Choices.Text[0].Content
 | 
			
		||||
		// 处理代码换行
 | 
			
		||||
		if len(content) == 0 {
 | 
			
		||||
			content = "\n"
 | 
			
		||||
		}
 | 
			
		||||
		contents = append(contents, content)
 | 
			
		||||
		// 第一个结果
 | 
			
		||||
		if result.Payload.Choices.Status == 0 {
 | 
			
		||||
@@ -194,7 +195,7 @@ func (h *ChatHandler) sendXunFeiMessage(
 | 
			
		||||
				RoleId:     role.Id,
 | 
			
		||||
				Type:       types.PromptMsg,
 | 
			
		||||
				Icon:       userVo.Avatar,
 | 
			
		||||
				Content:    prompt,
 | 
			
		||||
				Content:    template.HTMLEscapeString(prompt),
 | 
			
		||||
				Tokens:     promptToken,
 | 
			
		||||
				UseContext: true,
 | 
			
		||||
			}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										279
									
								
								api/handler/function_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										279
									
								
								api/handler/function_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,279 @@
 | 
			
		||||
package handler
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/service/oss"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/golang-jwt/jwt/v5"
 | 
			
		||||
	"github.com/imroc/req/v3"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type FunctionHandler struct {
 | 
			
		||||
	BaseHandler
 | 
			
		||||
	db            *gorm.DB
 | 
			
		||||
	config        types.ChatPlusApiConfig
 | 
			
		||||
	uploadManager *oss.UploaderManager
 | 
			
		||||
	proxyURL      string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewFunctionHandler(server *core.AppServer, db *gorm.DB, config *types.AppConfig, manager *oss.UploaderManager) *FunctionHandler {
 | 
			
		||||
	return &FunctionHandler{
 | 
			
		||||
		BaseHandler: BaseHandler{
 | 
			
		||||
			App: server,
 | 
			
		||||
		},
 | 
			
		||||
		db:            db,
 | 
			
		||||
		config:        config.ApiConfig,
 | 
			
		||||
		uploadManager: manager,
 | 
			
		||||
		proxyURL:      config.ProxyURL,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type resVo struct {
 | 
			
		||||
	Code    types.BizCode `json:"code"`
 | 
			
		||||
	Message string        `json:"message"`
 | 
			
		||||
	Data    struct {
 | 
			
		||||
		Title     string     `json:"title"`
 | 
			
		||||
		UpdatedAt string     `json:"updated_at"`
 | 
			
		||||
		Items     []dataItem `json:"items"`
 | 
			
		||||
	} `json:"data"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type dataItem struct {
 | 
			
		||||
	Title  string `json:"title"`
 | 
			
		||||
	Url    string `json:"url"`
 | 
			
		||||
	Remark string `json:"remark"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// check authorization
 | 
			
		||||
func (h *FunctionHandler) checkAuth(c *gin.Context) error {
 | 
			
		||||
	tokenString := c.GetHeader(types.UserAuthHeader)
 | 
			
		||||
	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(h.App.Config.Session.SecretKey), nil
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("error with parse auth token: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	claims, ok := token.Claims.(jwt.MapClaims)
 | 
			
		||||
	if !ok || !token.Valid {
 | 
			
		||||
		return errors.New("token is invalid")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	expr := utils.IntValue(utils.InterfaceToString(claims["expired"]), 0)
 | 
			
		||||
	if expr > 0 && int64(expr) < time.Now().Unix() {
 | 
			
		||||
		return errors.New("token is expired")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WeiBo 微博热搜
 | 
			
		||||
func (h *FunctionHandler) WeiBo(c *gin.Context) {
 | 
			
		||||
	if err := h.checkAuth(c); err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if h.config.Token == "" {
 | 
			
		||||
		resp.ERROR(c, "无效的 API Token")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	url := fmt.Sprintf("%s/api/weibo/fetch", h.config.ApiURL)
 | 
			
		||||
	var res resVo
 | 
			
		||||
	r, err := req.C().R().
 | 
			
		||||
		SetHeader("AppId", h.config.AppId).
 | 
			
		||||
		SetHeader("Authorization", fmt.Sprintf("Bearer %s", h.config.Token)).
 | 
			
		||||
		SetSuccessResult(&res).Get(url)
 | 
			
		||||
	if err != nil || r.IsErrorState() {
 | 
			
		||||
		resp.ERROR(c, fmt.Sprintf("%v%v", err, r.Err))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if res.Code != types.Success {
 | 
			
		||||
		resp.ERROR(c, res.Message)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	builder := make([]string, 0)
 | 
			
		||||
	builder = append(builder, fmt.Sprintf("**%s**,最新更新:%s", res.Data.Title, res.Data.UpdatedAt))
 | 
			
		||||
	for i, v := range res.Data.Items {
 | 
			
		||||
		builder = append(builder, fmt.Sprintf("%d、 [%s](%s) [热度:%s]", i+1, v.Title, v.Url, v.Remark))
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c, strings.Join(builder, "\n\n"))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ZaoBao 今日早报
 | 
			
		||||
func (h *FunctionHandler) ZaoBao(c *gin.Context) {
 | 
			
		||||
	if err := h.checkAuth(c); err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if h.config.Token == "" {
 | 
			
		||||
		resp.ERROR(c, "无效的 API Token")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	url := fmt.Sprintf("%s/api/zaobao/fetch", h.config.ApiURL)
 | 
			
		||||
	var res resVo
 | 
			
		||||
	r, err := req.C().R().
 | 
			
		||||
		SetHeader("AppId", h.config.AppId).
 | 
			
		||||
		SetHeader("Authorization", fmt.Sprintf("Bearer %s", h.config.Token)).
 | 
			
		||||
		SetSuccessResult(&res).Get(url)
 | 
			
		||||
	if err != nil || r.IsErrorState() {
 | 
			
		||||
		resp.ERROR(c, fmt.Sprintf("%v%v", err, r.Err))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if res.Code != types.Success {
 | 
			
		||||
		resp.ERROR(c, res.Message)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	builder := make([]string, 0)
 | 
			
		||||
	builder = append(builder, fmt.Sprintf("**%s 早报:**", res.Data.UpdatedAt))
 | 
			
		||||
	for _, v := range res.Data.Items {
 | 
			
		||||
		builder = append(builder, v.Title)
 | 
			
		||||
	}
 | 
			
		||||
	builder = append(builder, fmt.Sprintf("%s", res.Data.Title))
 | 
			
		||||
	resp.SUCCESS(c, strings.Join(builder, "\n\n"))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type imgReq struct {
 | 
			
		||||
	Model  string `json:"model"`
 | 
			
		||||
	Prompt string `json:"prompt"`
 | 
			
		||||
	N      int    `json:"n"`
 | 
			
		||||
	Size   string `json:"size"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type imgRes struct {
 | 
			
		||||
	Created int64 `json:"created"`
 | 
			
		||||
	Data    []struct {
 | 
			
		||||
		RevisedPrompt string `json:"revised_prompt"`
 | 
			
		||||
		Url           string `json:"url"`
 | 
			
		||||
	} `json:"data"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ErrRes struct {
 | 
			
		||||
	Error struct {
 | 
			
		||||
		Code    interface{} `json:"code"`
 | 
			
		||||
		Message string      `json:"message"`
 | 
			
		||||
		Param   interface{} `json:"param"`
 | 
			
		||||
		Type    string      `json:"type"`
 | 
			
		||||
	} `json:"error"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Dall3 DallE3 AI 绘图
 | 
			
		||||
func (h *FunctionHandler) Dall3(c *gin.Context) {
 | 
			
		||||
	if err := h.checkAuth(c); err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var params map[string]interface{}
 | 
			
		||||
	if err := c.ShouldBindJSON(¶ms); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logger.Debugf("绘画参数:%+v", params)
 | 
			
		||||
	// check img calls
 | 
			
		||||
	var user model.User
 | 
			
		||||
	tx := h.db.Where("id = ?", params["user_id"]).First(&user)
 | 
			
		||||
	if tx.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "当前用户不存在!")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if user.ImgCalls <= 0 {
 | 
			
		||||
		resp.ERROR(c, "当前用户的绘图次数额度不足!")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	prompt := utils.InterfaceToString(params["prompt"])
 | 
			
		||||
	// get image generation API KEY
 | 
			
		||||
	var apiKey model.ApiKey
 | 
			
		||||
	tx = h.db.Where("platform = ?", types.OpenAI).Where("type = ?", "img").Where("enabled = ?", true).Order("last_used_at ASC").First(&apiKey)
 | 
			
		||||
	if tx.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "获取绘图 API KEY 失败: "+tx.Error.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// get image generation api URL
 | 
			
		||||
	var conf model.Config
 | 
			
		||||
	var chatConfig types.ChatConfig
 | 
			
		||||
	tx = h.db.Where("marker", "chat").First(&conf)
 | 
			
		||||
	if tx.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "error with get chat configs:"+tx.Error.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err := utils.JsonDecode(conf.Config, &chatConfig)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, "error with decode chat config: "+err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// translate prompt
 | 
			
		||||
	const translatePromptTemplate = "Translate the following painting prompt words into English keyword phrases. Without any explanation, directly output the keyword phrases separated by commas. The content to be translated is: [%s]"
 | 
			
		||||
	pt, err := utils.OpenAIRequest(h.db, fmt.Sprintf(translatePromptTemplate, params["prompt"]), h.App.Config.ProxyURL)
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		prompt = pt
 | 
			
		||||
	}
 | 
			
		||||
	imgNum := chatConfig.DallImgNum
 | 
			
		||||
	if imgNum <= 0 {
 | 
			
		||||
		imgNum = 1
 | 
			
		||||
	}
 | 
			
		||||
	var res imgRes
 | 
			
		||||
	var errRes ErrRes
 | 
			
		||||
	var request *req.Request
 | 
			
		||||
	if apiKey.UseProxy && h.proxyURL != "" {
 | 
			
		||||
		request = req.C().SetProxyURL(h.proxyURL).R()
 | 
			
		||||
	} else {
 | 
			
		||||
		request = req.C().R()
 | 
			
		||||
	}
 | 
			
		||||
	logger.Debugf("Sending %s request, ApiURL:%s, Password:%s, PROXY: %s", apiKey.Platform, apiKey.ApiURL, apiKey.Value, h.proxyURL)
 | 
			
		||||
	r, err := request.SetHeader("Content-Type", "application/json").
 | 
			
		||||
		SetHeader("Authorization", "Bearer "+apiKey.Value).
 | 
			
		||||
		SetBody(imgReq{
 | 
			
		||||
			Model:  "dall-e-3",
 | 
			
		||||
			Prompt: prompt,
 | 
			
		||||
			N:      imgNum,
 | 
			
		||||
			Size:   "1024x1024",
 | 
			
		||||
		}).
 | 
			
		||||
		SetErrorResult(&errRes).
 | 
			
		||||
		SetSuccessResult(&res).Post(apiKey.ApiURL)
 | 
			
		||||
	if r.IsErrorState() {
 | 
			
		||||
		resp.ERROR(c, "请求 OpenAI API 失败: "+errRes.Error.Message)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// 更新 API KEY 的最后使用时间
 | 
			
		||||
	h.db.Model(&apiKey).UpdateColumn("last_used_at", time.Now().Unix())
 | 
			
		||||
	// 存储图片
 | 
			
		||||
	imgURL, err := h.uploadManager.GetUploadHandler().PutImg(res.Data[0].Url, false)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, "下载图片失败: "+err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	content := fmt.Sprintf("下面是根据您的描述创作的图片,它描绘了 【%s】 的场景。 \n\n\n", prompt, imgURL)
 | 
			
		||||
	// update user's img_calls
 | 
			
		||||
	h.db.Model(&model.User{}).Where("id = ?", user.Id).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, content)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										96
									
								
								api/handler/invite_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								api/handler/invite_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,96 @@
 | 
			
		||||
package handler
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// InviteHandler 用户邀请
 | 
			
		||||
type InviteHandler struct {
 | 
			
		||||
	BaseHandler
 | 
			
		||||
	db *gorm.DB
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewInviteHandler(app *core.AppServer, db *gorm.DB) *InviteHandler {
 | 
			
		||||
	h := InviteHandler{db: db}
 | 
			
		||||
	h.App = app
 | 
			
		||||
	return &h
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Code 获取当前用户邀请码
 | 
			
		||||
func (h *InviteHandler) Code(c *gin.Context) {
 | 
			
		||||
	userId := h.GetLoginUserId(c)
 | 
			
		||||
	var inviteCode model.InviteCode
 | 
			
		||||
	res := h.db.Where("user_id = ?", userId).First(&inviteCode)
 | 
			
		||||
	// 如果邀请码不存在,则创建一个
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		code := strings.ToUpper(utils.RandString(8))
 | 
			
		||||
		for {
 | 
			
		||||
			res = h.db.Where("code = ?", code).First(&inviteCode)
 | 
			
		||||
			if res.Error != nil { // 不存在相同的邀请码则退出
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		inviteCode.UserId = userId
 | 
			
		||||
		inviteCode.Code = code
 | 
			
		||||
		h.db.Create(&inviteCode)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var codeVo vo.InviteCode
 | 
			
		||||
	err := utils.CopyObject(inviteCode, &codeVo)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, "拷贝对象失败")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, codeVo)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// List Log 用户邀请记录
 | 
			
		||||
func (h *InviteHandler) List(c *gin.Context) {
 | 
			
		||||
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Page     int `json:"page"`
 | 
			
		||||
		PageSize int `json:"page_size"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	userId := h.GetLoginUserId(c)
 | 
			
		||||
	session := h.db.Session(&gorm.Session{}).Where("inviter_id = ?", userId)
 | 
			
		||||
	var total int64
 | 
			
		||||
	session.Model(&model.InviteLog{}).Count(&total)
 | 
			
		||||
	var items []model.InviteLog
 | 
			
		||||
	var list = make([]vo.InviteLog, 0)
 | 
			
		||||
	offset := (data.Page - 1) * data.PageSize
 | 
			
		||||
	res := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&items)
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		for _, item := range items {
 | 
			
		||||
			var v vo.InviteLog
 | 
			
		||||
			err := utils.CopyObject(item, &v)
 | 
			
		||||
			if err == nil {
 | 
			
		||||
				v.Id = item.Id
 | 
			
		||||
				v.CreatedAt = item.CreatedAt.Unix()
 | 
			
		||||
				list = append(list, v)
 | 
			
		||||
			} else {
 | 
			
		||||
				logger.Error(err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, list))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Hits 访问邀请码
 | 
			
		||||
func (h *InviteHandler) Hits(c *gin.Context) {
 | 
			
		||||
	code := c.Query("code")
 | 
			
		||||
	h.db.Model(&model.InviteCode{}).Where("code = ?", code).UpdateColumn("hits", gorm.Expr("hits + ?", 1))
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
@@ -3,58 +3,45 @@ package handler
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/service"
 | 
			
		||||
	"chatplus/service/mj"
 | 
			
		||||
	"chatplus/service/mj/plus"
 | 
			
		||||
	"chatplus/service/oss"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/go-redis/redis/v8"
 | 
			
		||||
	"github.com/gorilla/websocket"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/gorilla/websocket"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type MidJourneyHandler struct {
 | 
			
		||||
	BaseHandler
 | 
			
		||||
	redis     *redis.Client
 | 
			
		||||
	db        *gorm.DB
 | 
			
		||||
	mjService *mj.Service
 | 
			
		||||
	pool      *mj.ServicePool
 | 
			
		||||
	snowflake *service.Snowflake
 | 
			
		||||
	uploader  *oss.UploaderManager
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewMidJourneyHandler(
 | 
			
		||||
	app *core.AppServer,
 | 
			
		||||
	client *redis.Client,
 | 
			
		||||
	db *gorm.DB,
 | 
			
		||||
	mjService *mj.Service) *MidJourneyHandler {
 | 
			
		||||
func NewMidJourneyHandler(app *core.AppServer, db *gorm.DB, snowflake *service.Snowflake, pool *mj.ServicePool, manager *oss.UploaderManager) *MidJourneyHandler {
 | 
			
		||||
	h := MidJourneyHandler{
 | 
			
		||||
		redis:     client,
 | 
			
		||||
		db:        db,
 | 
			
		||||
		mjService: mjService,
 | 
			
		||||
		snowflake: snowflake,
 | 
			
		||||
		pool:      pool,
 | 
			
		||||
		uploader:  manager,
 | 
			
		||||
	}
 | 
			
		||||
	h.App = app
 | 
			
		||||
	return &h
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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.mjService.Clients.Put(sessionId, client)
 | 
			
		||||
	logger.Infof("New websocket connected, IP: %s", c.ClientIP())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *MidJourneyHandler) checkLimits(c *gin.Context) bool {
 | 
			
		||||
func (h *MidJourneyHandler) preCheck(c *gin.Context) bool {
 | 
			
		||||
	user, err := utils.GetLoginUser(c, h.db)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.NotAuth(c)
 | 
			
		||||
@@ -66,20 +53,42 @@ func (h *MidJourneyHandler) checkLimits(c *gin.Context) bool {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !h.pool.HasAvailableService() {
 | 
			
		||||
		resp.ERROR(c, "MidJourney 池子中没有没有可用的服务!")
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return true
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Image 创建一个绘画任务
 | 
			
		||||
func (h *MidJourneyHandler) Image(c *gin.Context) {
 | 
			
		||||
	if !h.App.Config.MjConfig.Enabled {
 | 
			
		||||
		resp.ERROR(c, "MidJourney service is disabled")
 | 
			
		||||
// 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)
 | 
			
		||||
		c.Abort()
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	userId := h.GetInt(c, "user_id", 0)
 | 
			
		||||
	if userId == 0 {
 | 
			
		||||
		logger.Info("Invalid user ID")
 | 
			
		||||
		c.Abort()
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	client := types.NewWsClient(ws)
 | 
			
		||||
	h.pool.Clients.Put(uint(userId), client)
 | 
			
		||||
	logger.Infof("New websocket connected, IP: %s", c.RemoteIP())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Image 创建一个绘画任务
 | 
			
		||||
func (h *MidJourneyHandler) Image(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		SessionId string  `json:"session_id"`
 | 
			
		||||
		Prompt    string  `json:"prompt"`
 | 
			
		||||
		NegPrompt string  `json:"neg_prompt"`
 | 
			
		||||
		Rate      string  `json:"rate"`
 | 
			
		||||
		Model     string  `json:"model"`
 | 
			
		||||
		Chaos     int     `json:"chaos"`
 | 
			
		||||
@@ -87,13 +96,15 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
 | 
			
		||||
		Seed      int64   `json:"seed"`
 | 
			
		||||
		Stylize   int     `json:"stylize"`
 | 
			
		||||
		Img       string  `json:"img"`
 | 
			
		||||
		Tile      bool    `json:"tile"`
 | 
			
		||||
		Quality   float32 `json:"quality"`
 | 
			
		||||
		Weight    float32 `json:"weight"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if !h.checkLimits(c) {
 | 
			
		||||
	if !h.preCheck(c) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -119,48 +130,59 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
 | 
			
		||||
	if data.Raw {
 | 
			
		||||
		prompt += " --style raw"
 | 
			
		||||
	}
 | 
			
		||||
	if data.Quality > 0 {
 | 
			
		||||
		prompt += fmt.Sprintf(" --q %.2f", data.Quality)
 | 
			
		||||
	}
 | 
			
		||||
	if data.NegPrompt != "" {
 | 
			
		||||
		prompt += fmt.Sprintf(" --no %s", data.NegPrompt)
 | 
			
		||||
	}
 | 
			
		||||
	if data.Tile {
 | 
			
		||||
		prompt += " --tile "
 | 
			
		||||
	}
 | 
			
		||||
	if data.Model != "" && !strings.Contains(prompt, "--v") && !strings.Contains(prompt, "--niji") {
 | 
			
		||||
		prompt += data.Model
 | 
			
		||||
		prompt += fmt.Sprintf(" %s", data.Model)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	idValue, _ := c.Get(types.LoginUserID)
 | 
			
		||||
	userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
 | 
			
		||||
	// generate task id
 | 
			
		||||
	taskId, err := h.snowflake.Next(true)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, "error with generate task id: "+err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	job := model.MidJourneyJob{
 | 
			
		||||
		Type:      types.TaskImage.String(),
 | 
			
		||||
		UserId:    userId,
 | 
			
		||||
		TaskId:    taskId,
 | 
			
		||||
		Progress:  0,
 | 
			
		||||
		Prompt:    prompt,
 | 
			
		||||
		CreatedAt: time.Now(),
 | 
			
		||||
	}
 | 
			
		||||
	if res := h.db.Create(&job); res.Error != nil {
 | 
			
		||||
	if res := h.db.Create(&job); res.Error != nil || res.RowsAffected == 0 {
 | 
			
		||||
		resp.ERROR(c, "添加任务失败:"+res.Error.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.mjService.PushTask(types.MjTask{
 | 
			
		||||
	h.pool.PushTask(types.MjTask{
 | 
			
		||||
		Id:        int(job.Id),
 | 
			
		||||
		SessionId: data.SessionId,
 | 
			
		||||
		Src:       types.TaskSrcImg,
 | 
			
		||||
		Type:      types.TaskImage,
 | 
			
		||||
		Prompt:    prompt,
 | 
			
		||||
		Prompt:    fmt.Sprintf("%s %s", taskId, prompt),
 | 
			
		||||
		UserId:    userId,
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	var jobVo vo.MidJourneyJob
 | 
			
		||||
	err := utils.CopyObject(job, &jobVo)
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		// 推送任务到前端
 | 
			
		||||
		client := h.mjService.Clients.Get(data.SessionId)
 | 
			
		||||
		if client != nil {
 | 
			
		||||
			utils.ReplyChunkMessage(client, jobVo)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	client := h.pool.Clients.Get(uint(job.UserId))
 | 
			
		||||
	_ = client.Send([]byte("Task Updated"))
 | 
			
		||||
 | 
			
		||||
	// update user's img calls
 | 
			
		||||
	h.db.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type reqVo struct {
 | 
			
		||||
	Src         string `json:"src"`
 | 
			
		||||
	Index       int    `json:"index"`
 | 
			
		||||
	ChannelId   string `json:"channel_id"`
 | 
			
		||||
	MessageId   string `json:"message_id"`
 | 
			
		||||
	MessageHash string `json:"message_hash"`
 | 
			
		||||
	SessionId   string `json:"session_id"`
 | 
			
		||||
@@ -178,65 +200,42 @@ func (h *MidJourneyHandler) Upscale(c *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !h.checkLimits(c) {
 | 
			
		||||
	if !h.preCheck(c) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	idValue, _ := c.Get(types.LoginUserID)
 | 
			
		||||
	jobId := 0
 | 
			
		||||
	userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
 | 
			
		||||
	src := types.TaskSrc(data.Src)
 | 
			
		||||
	if src == types.TaskSrcImg {
 | 
			
		||||
		job := model.MidJourneyJob{
 | 
			
		||||
			Type:      types.TaskUpscale.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.mjService.Clients.Get(data.SessionId)
 | 
			
		||||
			if client != nil {
 | 
			
		||||
				utils.ReplyChunkMessage(client, jobVo)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	taskId, _ := h.snowflake.Next(true)
 | 
			
		||||
	job := model.MidJourneyJob{
 | 
			
		||||
		Type:        types.TaskUpscale.String(),
 | 
			
		||||
		ReferenceId: data.MessageId,
 | 
			
		||||
		UserId:      userId,
 | 
			
		||||
		TaskId:      taskId,
 | 
			
		||||
		Progress:    0,
 | 
			
		||||
		Prompt:      data.Prompt,
 | 
			
		||||
		CreatedAt:   time.Now(),
 | 
			
		||||
	}
 | 
			
		||||
	h.mjService.PushTask(types.MjTask{
 | 
			
		||||
		Id:          jobId,
 | 
			
		||||
	if res := h.db.Create(&job); res.Error != nil || res.RowsAffected == 0 {
 | 
			
		||||
		resp.ERROR(c, "添加任务失败:"+res.Error.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.pool.PushTask(types.MjTask{
 | 
			
		||||
		Id:          int(job.Id),
 | 
			
		||||
		SessionId:   data.SessionId,
 | 
			
		||||
		Src:         src,
 | 
			
		||||
		Type:        types.TaskUpscale,
 | 
			
		||||
		Prompt:      data.Prompt,
 | 
			
		||||
		UserId:      userId,
 | 
			
		||||
		RoleId:      data.RoleId,
 | 
			
		||||
		Icon:        data.Icon,
 | 
			
		||||
		ChatId:      data.ChatId,
 | 
			
		||||
		ChannelId:   data.ChannelId,
 | 
			
		||||
		Index:       data.Index,
 | 
			
		||||
		MessageId:   data.MessageId,
 | 
			
		||||
		MessageHash: data.MessageHash,
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	if src == types.TaskSrcChat {
 | 
			
		||||
		wsClient := h.App.ChatClients.Get(data.SessionId)
 | 
			
		||||
		if wsClient != nil {
 | 
			
		||||
			content := fmt.Sprintf("**%s** 已推送 upscale 任务到 MidJourney 机器人,请耐心等待任务执行...", data.Prompt)
 | 
			
		||||
			utils.ReplyMessage(wsClient, content)
 | 
			
		||||
			if h.mjService.ChatClients.Get(data.SessionId) == nil {
 | 
			
		||||
				h.mjService.ChatClients.Put(data.SessionId, wsClient)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	client := h.pool.Clients.Get(uint(job.UserId))
 | 
			
		||||
	_ = client.Send([]byte("Task Updated"))
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -248,67 +247,45 @@ func (h *MidJourneyHandler) Variation(c *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !h.checkLimits(c) {
 | 
			
		||||
	if !h.preCheck(c) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	idValue, _ := c.Get(types.LoginUserID)
 | 
			
		||||
	jobId := 0
 | 
			
		||||
	userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
 | 
			
		||||
	src := types.TaskSrc(data.Src)
 | 
			
		||||
	if src == types.TaskSrcImg {
 | 
			
		||||
		job := model.MidJourneyJob{
 | 
			
		||||
			Type:      types.TaskVariation.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.mjService.Clients.Get(data.SessionId)
 | 
			
		||||
			if client != nil {
 | 
			
		||||
				utils.ReplyChunkMessage(client, jobVo)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	taskId, _ := h.snowflake.Next(true)
 | 
			
		||||
	job := model.MidJourneyJob{
 | 
			
		||||
		Type:        types.TaskVariation.String(),
 | 
			
		||||
		ChannelId:   data.ChannelId,
 | 
			
		||||
		ReferenceId: data.MessageId,
 | 
			
		||||
		UserId:      userId,
 | 
			
		||||
		TaskId:      taskId,
 | 
			
		||||
		Progress:    0,
 | 
			
		||||
		Prompt:      data.Prompt,
 | 
			
		||||
		CreatedAt:   time.Now(),
 | 
			
		||||
	}
 | 
			
		||||
	h.mjService.PushTask(types.MjTask{
 | 
			
		||||
		Id:          jobId,
 | 
			
		||||
	if res := h.db.Create(&job); res.Error != nil || res.RowsAffected == 0 {
 | 
			
		||||
		resp.ERROR(c, "添加任务失败:"+res.Error.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.pool.PushTask(types.MjTask{
 | 
			
		||||
		Id:          int(job.Id),
 | 
			
		||||
		SessionId:   data.SessionId,
 | 
			
		||||
		Src:         src,
 | 
			
		||||
		Type:        types.TaskVariation,
 | 
			
		||||
		Prompt:      data.Prompt,
 | 
			
		||||
		UserId:      userId,
 | 
			
		||||
		RoleId:      data.RoleId,
 | 
			
		||||
		Icon:        data.Icon,
 | 
			
		||||
		ChatId:      data.ChatId,
 | 
			
		||||
		Index:       data.Index,
 | 
			
		||||
		ChannelId:   data.ChannelId,
 | 
			
		||||
		MessageId:   data.MessageId,
 | 
			
		||||
		MessageHash: data.MessageHash,
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	if src == types.TaskSrcChat {
 | 
			
		||||
		// 从聊天窗口发送的请求,记录客户端信息
 | 
			
		||||
		wsClient := h.mjService.ChatClients.Get(data.SessionId)
 | 
			
		||||
		if wsClient != nil {
 | 
			
		||||
			content := fmt.Sprintf("**%s** 已推送 variation 任务到 MidJourney 机器人,请耐心等待任务执行...", data.Prompt)
 | 
			
		||||
			utils.ReplyMessage(wsClient, content)
 | 
			
		||||
			if h.mjService.Clients.Get(data.SessionId) == nil {
 | 
			
		||||
				h.mjService.Clients.Put(data.SessionId, wsClient)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	client := h.pool.Clients.Get(uint(job.UserId))
 | 
			
		||||
	_ = client.Send([]byte("Task Updated"))
 | 
			
		||||
 | 
			
		||||
	// update user's img calls
 | 
			
		||||
	h.db.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -318,6 +295,7 @@ func (h *MidJourneyHandler) JobList(c *gin.Context) {
 | 
			
		||||
	userId := h.GetInt(c, "user_id", 0)
 | 
			
		||||
	page := h.GetInt(c, "page", 0)
 | 
			
		||||
	pageSize := h.GetInt(c, "page_size", 0)
 | 
			
		||||
	publish := h.GetBool(c, "publish")
 | 
			
		||||
 | 
			
		||||
	session := h.db.Session(&gorm.Session{})
 | 
			
		||||
	if status == 1 {
 | 
			
		||||
@@ -328,6 +306,9 @@ func (h *MidJourneyHandler) JobList(c *gin.Context) {
 | 
			
		||||
	if userId > 0 {
 | 
			
		||||
		session = session.Where("user_id = ?", userId)
 | 
			
		||||
	}
 | 
			
		||||
	if publish {
 | 
			
		||||
		session = session.Where("publish = ?", publish)
 | 
			
		||||
	}
 | 
			
		||||
	if page > 0 && pageSize > 0 {
 | 
			
		||||
		offset := (page - 1) * pageSize
 | 
			
		||||
		session = session.Offset(offset).Limit(pageSize)
 | 
			
		||||
@@ -347,20 +328,96 @@ func (h *MidJourneyHandler) JobList(c *gin.Context) {
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		if item.Progress < 100 {
 | 
			
		||||
			// 30 分钟还没完成的任务直接删除
 | 
			
		||||
			if time.Now().Sub(item.CreatedAt) > time.Minute*30 {
 | 
			
		||||
				h.db.Delete(&item)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			if item.ImgURL != "" { // 正在运行中任务使用代理访问图片
 | 
			
		||||
				image, err := utils.DownloadImage(item.ImgURL, h.App.Config.ProxyURL)
 | 
			
		||||
 | 
			
		||||
		if job.Progress == -1 {
 | 
			
		||||
			h.db.Delete(&model.MidJourneyJob{Id: job.Id})
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if item.Progress < 100 && item.ImgURL == "" && item.OrgURL != "" {
 | 
			
		||||
			// 正在运行中任务使用代理访问图片
 | 
			
		||||
			if h.App.Config.ImgCdnURL != "" {
 | 
			
		||||
				job.ImgURL = strings.ReplaceAll(job.OrgURL, "https://cdn.discordapp.com", h.App.Config.ImgCdnURL)
 | 
			
		||||
			} else {
 | 
			
		||||
				image, err := utils.DownloadImage(item.OrgURL, 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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Remove remove task image
 | 
			
		||||
func (h *MidJourneyHandler) Remove(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Id     uint   `json:"id"`
 | 
			
		||||
		UserId uint   `json:"user_id"`
 | 
			
		||||
		ImgURL string `json:"img_url"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// remove job recode
 | 
			
		||||
	res := h.db.Delete(&model.MidJourneyJob{Id: data.Id})
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, res.Error.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// remove image
 | 
			
		||||
	err := h.uploader.GetUploadHandler().Delete(data.ImgURL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Error("remove image failed: ", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	client := h.pool.Clients.Get(data.UserId)
 | 
			
		||||
	_ = client.Send([]byte("Task Updated"))
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Notify MidJourney Plus 服务任务回调处理
 | 
			
		||||
func (h *MidJourneyHandler) Notify(c *gin.Context) {
 | 
			
		||||
	var data plus.CBReq
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		logger.Error("非法任务回调:%+v", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	err := h.pool.Notify(data)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Error(err)
 | 
			
		||||
	} else {
 | 
			
		||||
		userId := h.GetLoginUserId(c)
 | 
			
		||||
		client := h.pool.Clients.Get(userId)
 | 
			
		||||
		if client != nil {
 | 
			
		||||
			_ = client.Send([]byte("Task Updated"))
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Publish 发布图片到画廊显示
 | 
			
		||||
func (h *MidJourneyHandler) Publish(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Id     uint `json:"id"`
 | 
			
		||||
		Action bool `json:"action"` // 发布动作,true => 发布,false => 取消分享
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	res := h.db.Model(&model.MidJourneyJob{Id: data.Id}).UpdateColumn("publish", data.Action)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "更新数据库失败")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -11,41 +11,59 @@ import (
 | 
			
		||||
	"embed"
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"math"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	PayWayAlipay = "支付宝"
 | 
			
		||||
	PayWayWechat = "微信支付"
 | 
			
		||||
	PayWayXunHu  = "虎皮椒"
 | 
			
		||||
	PayWayJs     = "PayJS"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// PaymentHandler 支付服务回调 handler
 | 
			
		||||
type PaymentHandler struct {
 | 
			
		||||
	BaseHandler
 | 
			
		||||
	alipayService *payment.AlipayService
 | 
			
		||||
	snowflake     *service.Snowflake
 | 
			
		||||
	db            *gorm.DB
 | 
			
		||||
	fs            embed.FS
 | 
			
		||||
	lock          sync.Mutex
 | 
			
		||||
	alipayService  *payment.AlipayService
 | 
			
		||||
	huPiPayService *payment.HuPiPayService
 | 
			
		||||
	js             *payment.PayJS
 | 
			
		||||
	snowflake      *service.Snowflake
 | 
			
		||||
	db             *gorm.DB
 | 
			
		||||
	fs             embed.FS
 | 
			
		||||
	lock           sync.Mutex
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewPaymentHandler(server *core.AppServer, alipayService *payment.AlipayService, snowflake *service.Snowflake, db *gorm.DB, fs embed.FS) *PaymentHandler {
 | 
			
		||||
	h := PaymentHandler{lock: sync.Mutex{}}
 | 
			
		||||
func NewPaymentHandler(
 | 
			
		||||
	server *core.AppServer,
 | 
			
		||||
	alipayService *payment.AlipayService,
 | 
			
		||||
	huPiPayService *payment.HuPiPayService,
 | 
			
		||||
	js *payment.PayJS,
 | 
			
		||||
	snowflake *service.Snowflake,
 | 
			
		||||
	db *gorm.DB,
 | 
			
		||||
	fs embed.FS) *PaymentHandler {
 | 
			
		||||
	h := PaymentHandler{
 | 
			
		||||
		alipayService:  alipayService,
 | 
			
		||||
		huPiPayService: huPiPayService,
 | 
			
		||||
		js:             js,
 | 
			
		||||
		snowflake:      snowflake,
 | 
			
		||||
		fs:             fs,
 | 
			
		||||
		db:             db,
 | 
			
		||||
		lock:           sync.Mutex{},
 | 
			
		||||
	}
 | 
			
		||||
	h.App = server
 | 
			
		||||
	h.alipayService = alipayService
 | 
			
		||||
	h.snowflake = snowflake
 | 
			
		||||
	h.db = db
 | 
			
		||||
	h.fs = fs
 | 
			
		||||
	return &h
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *PaymentHandler) Alipay(c *gin.Context) {
 | 
			
		||||
func (h *PaymentHandler) DoPay(c *gin.Context) {
 | 
			
		||||
	orderNo := h.GetTrim(c, "order_no")
 | 
			
		||||
	payWay := h.GetTrim(c, "pay_way")
 | 
			
		||||
 | 
			
		||||
	if orderNo == "" {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
@@ -60,21 +78,41 @@ func (h *PaymentHandler) Alipay(c *gin.Context) {
 | 
			
		||||
 | 
			
		||||
	// 更新扫码状态
 | 
			
		||||
	h.db.Model(&order).UpdateColumn("status", types.OrderScanned)
 | 
			
		||||
	// 生成支付链接
 | 
			
		||||
	notifyURL := h.App.Config.AlipayConfig.NotifyURL
 | 
			
		||||
	returnURL := "" // 关闭同步回跳
 | 
			
		||||
	amount := fmt.Sprintf("%.2f", order.Amount)
 | 
			
		||||
	if payWay == "alipay" { // 支付宝
 | 
			
		||||
		// 生成支付链接
 | 
			
		||||
		notifyURL := h.App.Config.AlipayConfig.NotifyURL
 | 
			
		||||
		returnURL := "" // 关闭同步回跳
 | 
			
		||||
		amount := fmt.Sprintf("%.2f", order.Amount)
 | 
			
		||||
 | 
			
		||||
	uri, err := h.alipayService.PayUrlMobile(order.OrderNo, notifyURL, returnURL, amount, order.Subject)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, "error with generate pay url: "+err.Error())
 | 
			
		||||
		uri, err := h.alipayService.PayUrlMobile(order.OrderNo, notifyURL, returnURL, amount, order.Subject)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			resp.ERROR(c, "error with generate pay url: "+err.Error())
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c.Redirect(302, uri)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	} else if payWay == "hupi" { // 虎皮椒支付
 | 
			
		||||
		params := payment.HuPiPayReq{
 | 
			
		||||
			Version:      "1.1",
 | 
			
		||||
			TradeOrderId: orderNo,
 | 
			
		||||
			TotalFee:     fmt.Sprintf("%f", order.Amount),
 | 
			
		||||
			Title:        order.Subject,
 | 
			
		||||
			NotifyURL:    h.App.Config.HuPiPayConfig.NotifyURL,
 | 
			
		||||
			WapName:      "极客学长",
 | 
			
		||||
		}
 | 
			
		||||
		r, err := h.huPiPayService.Pay(params)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			resp.ERROR(c, err.Error())
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	c.Redirect(302, uri)
 | 
			
		||||
		c.Redirect(302, r.URL)
 | 
			
		||||
	}
 | 
			
		||||
	resp.ERROR(c, "Invalid operations")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// OrderQuery 清单状态查询
 | 
			
		||||
// OrderQuery 查询订单状态
 | 
			
		||||
func (h *PaymentHandler) OrderQuery(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		OrderNo string `json:"order_no"`
 | 
			
		||||
@@ -111,16 +149,12 @@ func (h *PaymentHandler) OrderQuery(c *gin.Context) {
 | 
			
		||||
	resp.SUCCESS(c, gin.H{"status": order.Status})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AlipayQrcode 生成支付宝支付 URL 二维码
 | 
			
		||||
func (h *PaymentHandler) AlipayQrcode(c *gin.Context) {
 | 
			
		||||
	if !h.App.SysConfig.EnabledAlipay || h.alipayService == nil {
 | 
			
		||||
		resp.ERROR(c, "当前支付通道已经关闭,请联系管理员开通!")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
// PayQrcode 生成支付 URL 二维码
 | 
			
		||||
func (h *PaymentHandler) PayQrcode(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		ProductId uint `json:"product_id"`
 | 
			
		||||
		UserId    int  `json:"user_id"`
 | 
			
		||||
		PayWay    string `json:"pay_way"` // 支付方式
 | 
			
		||||
		ProductId uint   `json:"product_id"`
 | 
			
		||||
		UserId    int    `json:"user_id"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
@@ -134,7 +168,7 @@ func (h *PaymentHandler) AlipayQrcode(c *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	orderNo, err := h.snowflake.Next()
 | 
			
		||||
	orderNo, err := h.snowflake.Next(false)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, "error with generate trade no: "+err.Error())
 | 
			
		||||
		return
 | 
			
		||||
@@ -146,44 +180,86 @@ func (h *PaymentHandler) AlipayQrcode(c *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var payWay string
 | 
			
		||||
	var notifyURL string
 | 
			
		||||
	switch data.PayWay {
 | 
			
		||||
	case "hupi":
 | 
			
		||||
		payWay = PayWayXunHu
 | 
			
		||||
		notifyURL = h.App.Config.HuPiPayConfig.NotifyURL
 | 
			
		||||
	case "payjs":
 | 
			
		||||
		payWay = PayWayJs
 | 
			
		||||
		notifyURL = h.App.Config.JPayConfig.NotifyURL
 | 
			
		||||
	default:
 | 
			
		||||
		payWay = PayWayAlipay
 | 
			
		||||
		notifyURL = h.App.Config.AlipayConfig.NotifyURL
 | 
			
		||||
	}
 | 
			
		||||
	// 创建订单
 | 
			
		||||
	remark := types.OrderRemark{
 | 
			
		||||
		Days:     product.Days,
 | 
			
		||||
		Calls:    product.Calls,
 | 
			
		||||
		ImgCalls: product.ImgCalls,
 | 
			
		||||
		Name:     product.Name,
 | 
			
		||||
		Price:    product.Price,
 | 
			
		||||
		Discount: product.Discount,
 | 
			
		||||
	}
 | 
			
		||||
	order := model.Order{
 | 
			
		||||
		UserId:    user.Id,
 | 
			
		||||
		Mobile:    user.Mobile,
 | 
			
		||||
		Username:  user.Username,
 | 
			
		||||
		ProductId: product.Id,
 | 
			
		||||
		OrderNo:   orderNo,
 | 
			
		||||
		Subject:   product.Name,
 | 
			
		||||
		Amount:    product.Price - product.Discount,
 | 
			
		||||
		Status:    types.OrderNotPaid,
 | 
			
		||||
		PayWay:    PayWayAlipay,
 | 
			
		||||
		PayWay:    payWay,
 | 
			
		||||
		Remark:    utils.JsonEncode(remark),
 | 
			
		||||
	}
 | 
			
		||||
	res = h.db.Create(&order)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
	if res.Error != nil || res.RowsAffected == 0 {
 | 
			
		||||
		resp.ERROR(c, "error with create order: "+res.Error.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 生成二维码图片
 | 
			
		||||
	file, err := h.fs.Open("res/img/alipay.jpg")
 | 
			
		||||
	// PayJs 单独处理,只能用官方生成的二维码
 | 
			
		||||
	if data.PayWay == "payjs" {
 | 
			
		||||
		params := payment.JPayReq{
 | 
			
		||||
			TotalFee:   int(math.Ceil(order.Amount * 100)),
 | 
			
		||||
			OutTradeNo: order.OrderNo,
 | 
			
		||||
			Subject:    product.Name,
 | 
			
		||||
		}
 | 
			
		||||
		r := h.js.Pay(params)
 | 
			
		||||
		if r.IsOK() {
 | 
			
		||||
			resp.SUCCESS(c, gin.H{"order_no": order.OrderNo, "image": r.Qrcode})
 | 
			
		||||
			return
 | 
			
		||||
		} else {
 | 
			
		||||
			resp.ERROR(c, "error with generating payment qrcode: "+r.ReturnMsg)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var logo string
 | 
			
		||||
	if data.PayWay == "alipay" {
 | 
			
		||||
		logo = "res/img/alipay.jpg"
 | 
			
		||||
	} else if data.PayWay == "hupi" {
 | 
			
		||||
		if h.App.Config.HuPiPayConfig.Name == "wechat" {
 | 
			
		||||
			logo = "res/img/wechat-pay.jpg"
 | 
			
		||||
		} else {
 | 
			
		||||
			logo = "res/img/alipay.jpg"
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	file, err := h.fs.Open(logo)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		resp.ERROR(c, "error with open qrcode log file: "+err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	parse, err := url.Parse(h.App.Config.AlipayConfig.NotifyURL)
 | 
			
		||||
 | 
			
		||||
	parse, err := url.Parse(notifyURL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	imageURL := fmt.Sprintf("%s://%s/api/payment/alipay?order_no=%s", parse.Scheme, parse.Host, orderNo)
 | 
			
		||||
	imageURL := fmt.Sprintf("%s://%s/api/payment/doPay?order_no=%s&pay_way=%s", parse.Scheme, parse.Host, orderNo, data.PayWay)
 | 
			
		||||
	imgData, err := utils.GenQrcode(imageURL, 400, file)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
@@ -193,6 +269,127 @@ func (h *PaymentHandler) AlipayQrcode(c *gin.Context) {
 | 
			
		||||
	resp.SUCCESS(c, gin.H{"order_no": orderNo, "image": fmt.Sprintf("data:image/jpg;base64, %s", imgDataBase64), "url": imageURL})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 异步通知回调公共逻辑
 | 
			
		||||
func (h *PaymentHandler) notify(orderNo string, tradeNo string) error {
 | 
			
		||||
	var order model.Order
 | 
			
		||||
	res := h.db.Where("order_no = ?", orderNo).First(&order)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		err := fmt.Errorf("error with fetch order: %v", res.Error)
 | 
			
		||||
		logger.Error(err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.lock.Lock()
 | 
			
		||||
	defer h.lock.Unlock()
 | 
			
		||||
 | 
			
		||||
	// 已支付订单,直接返回
 | 
			
		||||
	if order.Status == types.OrderPaidSuccess {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var user model.User
 | 
			
		||||
	res = h.db.First(&user, order.UserId)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		err := fmt.Errorf("error with fetch user info: %v", res.Error)
 | 
			
		||||
		logger.Error(err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var remark types.OrderRemark
 | 
			
		||||
	err := utils.JsonDecode(order.Remark, &remark)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		err := fmt.Errorf("error with decode order remark: %v", err)
 | 
			
		||||
		logger.Error(err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if user.Vip { // 已经是 VIP 用户
 | 
			
		||||
		if remark.Days > 0 { // 只延期 VIP,不增加调用次数
 | 
			
		||||
			user.ExpiredTime = time.Unix(user.ExpiredTime, 0).AddDate(0, 0, remark.Days).Unix()
 | 
			
		||||
		} else { // 充值点卡,直接增加次数即可
 | 
			
		||||
			user.Calls += remark.Calls
 | 
			
		||||
			user.ImgCalls += remark.ImgCalls
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	} else { // 非 VIP 用户
 | 
			
		||||
		if remark.Days > 0 { // vip 套餐:days > 0, calls == 0
 | 
			
		||||
			user.ExpiredTime = time.Now().AddDate(0, 0, remark.Days).Unix()
 | 
			
		||||
			user.Calls += h.App.SysConfig.VipMonthCalls
 | 
			
		||||
			user.ImgCalls += h.App.SysConfig.VipMonthImgCalls
 | 
			
		||||
			user.Vip = true
 | 
			
		||||
 | 
			
		||||
		} else { //点卡:days == 0, calls > 0
 | 
			
		||||
			user.Calls += remark.Calls
 | 
			
		||||
			user.ImgCalls += remark.ImgCalls
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 更新用户信息
 | 
			
		||||
	res = h.db.Updates(&user)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		err := fmt.Errorf("error with update user info: %v", res.Error)
 | 
			
		||||
		logger.Error(err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 更新订单状态
 | 
			
		||||
	order.PayTime = time.Now().Unix()
 | 
			
		||||
	order.Status = types.OrderPaidSuccess
 | 
			
		||||
	order.TradeNo = tradeNo
 | 
			
		||||
	res = h.db.Updates(&order)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		err := fmt.Errorf("error with update order info: %v", res.Error)
 | 
			
		||||
		logger.Error(err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 更新产品销量
 | 
			
		||||
	h.db.Model(&model.Product{}).Where("id = ?", order.ProductId).UpdateColumn("sales", gorm.Expr("sales + ?", 1))
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetPayWays 获取支付方式
 | 
			
		||||
func (h *PaymentHandler) GetPayWays(c *gin.Context) {
 | 
			
		||||
	data := gin.H{}
 | 
			
		||||
	if h.App.Config.AlipayConfig.Enabled {
 | 
			
		||||
		data["alipay"] = gin.H{"name": "alipay"}
 | 
			
		||||
	}
 | 
			
		||||
	if h.App.Config.HuPiPayConfig.Enabled {
 | 
			
		||||
		data["hupi"] = gin.H{"name": h.App.Config.HuPiPayConfig.Name}
 | 
			
		||||
	}
 | 
			
		||||
	if h.App.Config.JPayConfig.Enabled {
 | 
			
		||||
		data["payjs"] = gin.H{"name": h.App.Config.JPayConfig.Name}
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c, data)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HuPiPayNotify 虎皮椒支付异步回调
 | 
			
		||||
func (h *PaymentHandler) HuPiPayNotify(c *gin.Context) {
 | 
			
		||||
	err := c.Request.ParseForm()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.String(http.StatusOK, "fail")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	orderNo := c.Request.Form.Get("trade_order_id")
 | 
			
		||||
	tradeNo := c.Request.Form.Get("open_order_id")
 | 
			
		||||
	logger.Infof("收到虎皮椒订单支付回调,订单 NO:%s,交易流水号:%s", orderNo, tradeNo)
 | 
			
		||||
 | 
			
		||||
	if err = h.huPiPayService.Check(tradeNo); err != nil {
 | 
			
		||||
		logger.Error("订单校验失败:", err)
 | 
			
		||||
		c.String(http.StatusOK, "fail")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	err = h.notify(orderNo, tradeNo)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.String(http.StatusOK, "fail")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c.String(http.StatusOK, "success")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AlipayNotify 支付宝支付回调
 | 
			
		||||
func (h *PaymentHandler) AlipayNotify(c *gin.Context) {
 | 
			
		||||
	err := c.Request.ParseForm()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -200,74 +397,55 @@ func (h *PaymentHandler) AlipayNotify(c *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// TODO:这里最好用支付宝的公钥签名签证一下交易真假
 | 
			
		||||
	//res := h.alipayService.TradeVerify(c.Request.Form)
 | 
			
		||||
	r := h.alipayService.TradeQuery(c.Request.Form.Get("out_trade_no"))
 | 
			
		||||
	logger.Infof("验证支付结果:%+v", r)
 | 
			
		||||
	if !r.Success() {
 | 
			
		||||
	// TODO:验证交易签名
 | 
			
		||||
	res := h.alipayService.TradeVerify(c.Request.Form)
 | 
			
		||||
	logger.Infof("验证支付结果:%+v", res)
 | 
			
		||||
	if !res.Success() {
 | 
			
		||||
		logger.Error("订单校验失败:", res.Message)
 | 
			
		||||
		c.String(http.StatusOK, "fail")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.lock.Lock()
 | 
			
		||||
	defer h.lock.Unlock()
 | 
			
		||||
 | 
			
		||||
	var order model.Order
 | 
			
		||||
	res := h.db.Where("order_no = ?", r.OutTradeNo).First(&order)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		logger.Error(res.Error)
 | 
			
		||||
		c.String(http.StatusOK, "fail")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	var user model.User
 | 
			
		||||
	res = h.db.First(&user, order.UserId)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		logger.Error(res.Error)
 | 
			
		||||
		c.String(http.StatusOK, "fail")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	var remark types.OrderRemark
 | 
			
		||||
	err = utils.JsonDecode(order.Remark, &remark)
 | 
			
		||||
	tradeNo := c.Request.Form.Get("trade_no")
 | 
			
		||||
	err = h.notify(res.OutTradeNo, tradeNo)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.String(http.StatusOK, "fail")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c.String(http.StatusOK, "success")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PayJsNotify PayJs 支付异步回调
 | 
			
		||||
func (h *PaymentHandler) PayJsNotify(c *gin.Context) {
 | 
			
		||||
	err := c.Request.ParseForm()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.String(http.StatusOK, "fail")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	orderNo := c.Request.Form.Get("out_trade_no")
 | 
			
		||||
	returnCode := c.Request.Form.Get("return_code")
 | 
			
		||||
	logger.Infof("收到订单支付回调,订单 NO:%s,支付结果代码:%v", orderNo, returnCode)
 | 
			
		||||
	// 支付失败
 | 
			
		||||
	if returnCode != "1" {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 校验订单支付状态
 | 
			
		||||
	tradeNo := c.Request.Form.Get("payjs_order_id")
 | 
			
		||||
	err = h.js.Check(tradeNo)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Error("订单校验失败:", err)
 | 
			
		||||
		c.String(http.StatusOK, "fail")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = h.notify(orderNo, tradeNo)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Error(res.Error)
 | 
			
		||||
		c.String(http.StatusOK, "fail")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// 1. 点卡:days == 0, calls > 0
 | 
			
		||||
	// 2. vip 套餐:days > 0, calls == 0
 | 
			
		||||
	if remark.Days > 0 {
 | 
			
		||||
		if user.ExpiredTime > time.Now().Unix() {
 | 
			
		||||
			user.ExpiredTime = time.Unix(user.ExpiredTime, 0).AddDate(0, 0, remark.Days).Unix()
 | 
			
		||||
		} else {
 | 
			
		||||
			user.ExpiredTime = time.Now().AddDate(0, 0, remark.Days).Unix()
 | 
			
		||||
		}
 | 
			
		||||
		user.Vip = true
 | 
			
		||||
 | 
			
		||||
	} else if !user.Vip { // 充值点卡的非 VIP 用户
 | 
			
		||||
		user.ExpiredTime = time.Now().AddDate(0, 0, 30).Unix()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if remark.Calls > 0 { // 充值点卡
 | 
			
		||||
		user.Calls += remark.Calls
 | 
			
		||||
	} else {
 | 
			
		||||
		user.Calls += h.App.SysConfig.VipMonthCalls
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 更新用户信息
 | 
			
		||||
	res = h.db.Updates(&user)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		logger.Error(res.Error)
 | 
			
		||||
		c.String(http.StatusOK, "fail")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 更新订单状态
 | 
			
		||||
	order.PayTime = time.Now().Unix()
 | 
			
		||||
	order.Status = types.OrderPaidSuccess
 | 
			
		||||
	h.db.Updates(&order)
 | 
			
		||||
 | 
			
		||||
	// 更新产品销量
 | 
			
		||||
	h.db.Model(&model.Product{}).Where("id = ?", order.ProductId).UpdateColumn("sales", gorm.Expr("sales + ?", 1))
 | 
			
		||||
 | 
			
		||||
	c.String(http.StatusOK, "success")
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										63
									
								
								api/handler/prompt_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								api/handler/prompt_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,63 @@
 | 
			
		||||
package handler
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const rewritePromptTemplate = "Please rewrite the following text into AI painting prompt words, and please try to add detailed description of the picture, painting style, scene, rendering effect, picture light and other elements. Please output directly in English without any explanation, within 150 words. The text to be rewritten is: [%s]"
 | 
			
		||||
const translatePromptTemplate = "Translate the following painting prompt words into English keyword phrases. Without any explanation, directly output the keyword phrases separated by commas. The content to be translated is: [%s]"
 | 
			
		||||
 | 
			
		||||
type PromptHandler struct {
 | 
			
		||||
	BaseHandler
 | 
			
		||||
	db *gorm.DB
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewPromptHandler(app *core.AppServer, db *gorm.DB) *PromptHandler {
 | 
			
		||||
	h := &PromptHandler{db: db}
 | 
			
		||||
	h.App = app
 | 
			
		||||
	return h
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Rewrite translate and rewrite prompt with ChatGPT
 | 
			
		||||
func (h *PromptHandler) Rewrite(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Prompt string `json:"prompt"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	content, err := utils.OpenAIRequest(h.db, fmt.Sprintf(rewritePromptTemplate, data.Prompt), h.App.Config.ProxyURL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, content)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *PromptHandler) Translate(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Prompt string `json:"prompt"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	content, err := utils.OpenAIRequest(h.db, fmt.Sprintf(translatePromptTemplate, data.Prompt), h.App.Config.ProxyURL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, content)
 | 
			
		||||
}
 | 
			
		||||
@@ -4,20 +4,24 @@ import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"math"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type RewardHandler struct {
 | 
			
		||||
	BaseHandler
 | 
			
		||||
	db *gorm.DB
 | 
			
		||||
	db   *gorm.DB
 | 
			
		||||
	lock sync.Mutex
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewRewardHandler(server *core.AppServer, db *gorm.DB) *RewardHandler {
 | 
			
		||||
	h := RewardHandler{db: db}
 | 
			
		||||
	h := RewardHandler{db: db, lock: sync.Mutex{}}
 | 
			
		||||
	h.App = server
 | 
			
		||||
	return &h
 | 
			
		||||
}
 | 
			
		||||
@@ -26,15 +30,25 @@ func NewRewardHandler(server *core.AppServer, db *gorm.DB) *RewardHandler {
 | 
			
		||||
func (h *RewardHandler) Verify(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		TxId string `json:"tx_id"`
 | 
			
		||||
		Type string `json:"type"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	user, err := utils.GetLoginUser(c, h.db)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.HACKER(c)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 移除转账单号中间的空格,防止有人复制的时候多复制了空格
 | 
			
		||||
	data.TxId = strings.ReplaceAll(data.TxId, " ", "")
 | 
			
		||||
 | 
			
		||||
	h.lock.Lock()
 | 
			
		||||
	defer h.lock.Unlock()
 | 
			
		||||
 | 
			
		||||
	var item model.Reward
 | 
			
		||||
	res := h.db.Where("tx_id = ?", data.TxId).First(&item)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
@@ -47,15 +61,17 @@ func (h *RewardHandler) Verify(c *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	user, err := utils.GetLoginUser(c, h.db)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.HACKER(c)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tx := h.db.Begin()
 | 
			
		||||
	calls := (item.Amount + 0.1) * 10
 | 
			
		||||
	res = h.db.Model(&user).UpdateColumn("calls", gorm.Expr("calls + ?", calls))
 | 
			
		||||
	exchange := vo.RewardExchange{}
 | 
			
		||||
	if data.Type == "chat" {
 | 
			
		||||
		calls := math.Ceil(item.Amount / h.App.SysConfig.ChatCallPrice)
 | 
			
		||||
		exchange.Calls = int(calls)
 | 
			
		||||
		res = h.db.Model(&user).UpdateColumn("calls", gorm.Expr("calls + ?", calls))
 | 
			
		||||
	} else if data.Type == "img" {
 | 
			
		||||
		calls := math.Ceil(item.Amount / h.App.SysConfig.ImgCallPrice)
 | 
			
		||||
		exchange.ImgCalls = int(calls)
 | 
			
		||||
		res = h.db.Model(&user).UpdateColumn("img_calls", gorm.Expr("img_calls + ?", calls))
 | 
			
		||||
	}
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "更新数据库失败!")
 | 
			
		||||
		return
 | 
			
		||||
@@ -64,6 +80,7 @@ func (h *RewardHandler) Verify(c *gin.Context) {
 | 
			
		||||
	// 更新核销状态
 | 
			
		||||
	item.Status = true
 | 
			
		||||
	item.UserId = user.Id
 | 
			
		||||
	item.Exchange = utils.JsonEncode(exchange)
 | 
			
		||||
	res = h.db.Updates(&item)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		tx.Rollback()
 | 
			
		||||
 
 | 
			
		||||
@@ -3,52 +3,39 @@ package handler
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/service/oss"
 | 
			
		||||
	"chatplus/service/sd"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/go-redis/redis/v8"
 | 
			
		||||
	"github.com/gorilla/websocket"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type SdJobHandler struct {
 | 
			
		||||
	BaseHandler
 | 
			
		||||
	redis   *redis.Client
 | 
			
		||||
	db      *gorm.DB
 | 
			
		||||
	service *sd.Service
 | 
			
		||||
	redis    *redis.Client
 | 
			
		||||
	db       *gorm.DB
 | 
			
		||||
	pool     *sd.ServicePool
 | 
			
		||||
	uploader *oss.UploaderManager
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewSdJobHandler(app *core.AppServer, redisCli *redis.Client, db *gorm.DB, service *sd.Service) *SdJobHandler {
 | 
			
		||||
func NewSdJobHandler(app *core.AppServer, db *gorm.DB, pool *sd.ServicePool, manager *oss.UploaderManager) *SdJobHandler {
 | 
			
		||||
	h := SdJobHandler{
 | 
			
		||||
		redis:   redisCli,
 | 
			
		||||
		db:      db,
 | 
			
		||||
		service: service,
 | 
			
		||||
		db:       db,
 | 
			
		||||
		pool:     pool,
 | 
			
		||||
		uploader: manager,
 | 
			
		||||
	}
 | 
			
		||||
	h.App = app
 | 
			
		||||
	return &h
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Client WebSocket 客户端,用于通知任务状态变更
 | 
			
		||||
func (h *SdJobHandler) 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.service.Clients.Put(sessionId, client)
 | 
			
		||||
	logger.Infof("New websocket connected, IP: %s", c.ClientIP())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *SdJobHandler) checkLimits(c *gin.Context) bool {
 | 
			
		||||
	user, err := utils.GetLoginUser(c, h.db)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -56,6 +43,11 @@ func (h *SdJobHandler) checkLimits(c *gin.Context) bool {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !h.pool.HasAvailableService() {
 | 
			
		||||
		resp.ERROR(c, "Stable-Diffusion 池子中没有没有可用的服务!")
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if user.ImgCalls <= 0 {
 | 
			
		||||
		resp.ERROR(c, "您的绘图次数不足,请联系管理员充值!")
 | 
			
		||||
		return false
 | 
			
		||||
@@ -67,11 +59,6 @@ func (h *SdJobHandler) checkLimits(c *gin.Context) bool {
 | 
			
		||||
 | 
			
		||||
// Image 创建一个绘画任务
 | 
			
		||||
func (h *SdJobHandler) Image(c *gin.Context) {
 | 
			
		||||
	if !h.App.Config.SdConfig.Enabled {
 | 
			
		||||
		resp.ERROR(c, "Stable Diffusion service is disabled")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !h.checkLimits(c) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
@@ -129,7 +116,6 @@ func (h *SdJobHandler) Image(c *gin.Context) {
 | 
			
		||||
		Params:    utils.JsonEncode(params),
 | 
			
		||||
		Prompt:    data.Prompt,
 | 
			
		||||
		Progress:  0,
 | 
			
		||||
		Started:   false,
 | 
			
		||||
		CreatedAt: time.Now(),
 | 
			
		||||
	}
 | 
			
		||||
	res := h.db.Create(&job)
 | 
			
		||||
@@ -138,24 +124,18 @@ func (h *SdJobHandler) Image(c *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.service.PushTask(types.SdTask{
 | 
			
		||||
	h.pool.PushTask(types.SdTask{
 | 
			
		||||
		Id:        int(job.Id),
 | 
			
		||||
		SessionId: data.SessionId,
 | 
			
		||||
		Src:       types.TaskSrcImg,
 | 
			
		||||
		Type:      types.TaskImage,
 | 
			
		||||
		Prompt:    data.Prompt,
 | 
			
		||||
		Params:    params,
 | 
			
		||||
		UserId:    userId,
 | 
			
		||||
	})
 | 
			
		||||
	var jobVo vo.SdJob
 | 
			
		||||
	err := utils.CopyObject(job, &jobVo)
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		// 推送任务到前端
 | 
			
		||||
		client := h.service.Clients.Get(data.SessionId)
 | 
			
		||||
		if client != nil {
 | 
			
		||||
			utils.ReplyChunkMessage(client, jobVo)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// update user's img calls
 | 
			
		||||
	h.db.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -165,6 +145,7 @@ func (h *SdJobHandler) JobList(c *gin.Context) {
 | 
			
		||||
	userId := h.GetInt(c, "user_id", 0)
 | 
			
		||||
	page := h.GetInt(c, "page", 0)
 | 
			
		||||
	pageSize := h.GetInt(c, "page_size", 0)
 | 
			
		||||
	publish := h.GetBool(c, "publish")
 | 
			
		||||
 | 
			
		||||
	session := h.db.Session(&gorm.Session{})
 | 
			
		||||
	if status == 1 {
 | 
			
		||||
@@ -175,6 +156,9 @@ func (h *SdJobHandler) JobList(c *gin.Context) {
 | 
			
		||||
	if userId > 0 {
 | 
			
		||||
		session = session.Where("user_id = ?", userId)
 | 
			
		||||
	}
 | 
			
		||||
	if publish {
 | 
			
		||||
		session = session.Where("publish", publish)
 | 
			
		||||
	}
 | 
			
		||||
	if page > 0 && pageSize > 0 {
 | 
			
		||||
		offset := (page - 1) * pageSize
 | 
			
		||||
		session = session.Offset(offset).Limit(pageSize)
 | 
			
		||||
@@ -194,14 +178,73 @@ func (h *SdJobHandler) JobList(c *gin.Context) {
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if job.Progress == -1 {
 | 
			
		||||
			h.db.Delete(&model.SdJob{Id: job.Id})
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if item.Progress < 100 {
 | 
			
		||||
			// 30 分钟还没完成的任务直接删除
 | 
			
		||||
			if time.Now().Sub(item.CreatedAt) > time.Minute*30 {
 | 
			
		||||
			// 5 分钟还没完成的任务直接删除
 | 
			
		||||
			if time.Now().Sub(item.CreatedAt) > time.Minute*5 {
 | 
			
		||||
				h.db.Delete(&item)
 | 
			
		||||
				// 退回绘图次数
 | 
			
		||||
				h.db.Model(&model.User{}).Where("id = ?", item.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls + ?", 1))
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			// 正在运行中任务使用代理访问图片
 | 
			
		||||
			image, err := utils.DownloadImage(item.ImgURL, "")
 | 
			
		||||
			if err == nil {
 | 
			
		||||
				job.ImgURL = "data:image/png;base64," + base64.StdEncoding.EncodeToString(image)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		jobs = append(jobs, job)
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c, jobs)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Remove remove task image
 | 
			
		||||
func (h *SdJobHandler) Remove(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Id     uint   `json:"id"`
 | 
			
		||||
		ImgURL string `json:"img_url"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// remove job recode
 | 
			
		||||
	res := h.db.Delete(&model.SdJob{Id: data.Id})
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, res.Error.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// remove image
 | 
			
		||||
	err := h.uploader.GetUploadHandler().Delete(data.ImgURL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Error("remove image failed: ", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Publish 发布/取消发布图片到画廊显示
 | 
			
		||||
func (h *SdJobHandler) Publish(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Id     uint `json:"id"`
 | 
			
		||||
		Action bool `json:"action"` // 发布动作,true => 发布,false => 取消分享
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	res := h.db.Model(&model.SdJob{Id: data.Id}).UpdateColumn("publish", true)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "更新数据库失败")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,33 +4,42 @@ import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/service"
 | 
			
		||||
	"chatplus/store"
 | 
			
		||||
	"chatplus/service/sms"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/go-redis/redis/v8"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const CodeStorePrefix = "/verify/codes/"
 | 
			
		||||
 | 
			
		||||
type SmsHandler struct {
 | 
			
		||||
	BaseHandler
 | 
			
		||||
	leveldb *store.LevelDB
 | 
			
		||||
	sms     *service.AliYunSmsService
 | 
			
		||||
	redis   *redis.Client
 | 
			
		||||
	sms     *sms.ServiceManager
 | 
			
		||||
	smtp    *service.SmtpService
 | 
			
		||||
	captcha *service.CaptchaService
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewSmsHandler(app *core.AppServer, db *store.LevelDB, sms *service.AliYunSmsService, captcha *service.CaptchaService) *SmsHandler {
 | 
			
		||||
	handler := &SmsHandler{leveldb: db, sms: sms, captcha: captcha}
 | 
			
		||||
func NewSmsHandler(
 | 
			
		||||
	app *core.AppServer,
 | 
			
		||||
	client *redis.Client,
 | 
			
		||||
	sms *sms.ServiceManager,
 | 
			
		||||
	smtp *service.SmtpService,
 | 
			
		||||
	captcha *service.CaptchaService) *SmsHandler {
 | 
			
		||||
	handler := &SmsHandler{redis: client, sms: sms, captcha: captcha, smtp: smtp}
 | 
			
		||||
	handler.App = app
 | 
			
		||||
	return handler
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SendCode 发送验证码短信
 | 
			
		||||
// SendCode 发送验证码
 | 
			
		||||
func (h *SmsHandler) SendCode(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Mobile string `json:"mobile"`
 | 
			
		||||
		Key    string `json:"key"`
 | 
			
		||||
		Dots   string `json:"dots"`
 | 
			
		||||
		Receiver string `json:"receiver"` // 接收者
 | 
			
		||||
		Key      string `json:"key"`
 | 
			
		||||
		Dots     string `json:"dots"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
@@ -43,14 +52,28 @@ func (h *SmsHandler) SendCode(c *gin.Context) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	code := utils.RandomNumber(6)
 | 
			
		||||
	err := h.sms.SendVerifyCode(data.Mobile, code)
 | 
			
		||||
	var err error
 | 
			
		||||
	if strings.Contains(data.Receiver, "@") { // email
 | 
			
		||||
		if !utils.ContainsStr(h.App.SysConfig.RegisterWays, "email") {
 | 
			
		||||
			resp.ERROR(c, "系统已禁用邮箱注册!")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		err = h.smtp.SendVerifyCode(data.Receiver, code)
 | 
			
		||||
	} else {
 | 
			
		||||
		if !utils.ContainsStr(h.App.SysConfig.RegisterWays, "mobile") {
 | 
			
		||||
			resp.ERROR(c, "系统已禁用手机号注册!")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		err = h.sms.GetService().SendVerifyCode(data.Receiver, code)
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 存储验证码,等待后面注册验证
 | 
			
		||||
	err = h.leveldb.Put(CodeStorePrefix+data.Mobile, code)
 | 
			
		||||
	_, err = h.redis.Set(c, CodeStorePrefix+data.Receiver, code, 0).Result()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, "验证码保存失败")
 | 
			
		||||
		return
 | 
			
		||||
@@ -58,13 +81,3 @@ func (h *SmsHandler) SendCode(c *gin.Context) {
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type statusVo struct {
 | 
			
		||||
	EnabledMsgService bool `json:"enabled_msg_service"`
 | 
			
		||||
	EnabledRegister   bool `json:"enabled_register"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Status check if the message service is enabled
 | 
			
		||||
func (h *SmsHandler) Status(c *gin.Context) {
 | 
			
		||||
	resp.SUCCESS(c, statusVo{EnabledMsgService: h.App.SysConfig.EnabledMsg, EnabledRegister: h.App.SysConfig.EnabledRegister})
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										228
									
								
								api/handler/test_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										228
									
								
								api/handler/test_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,228 @@
 | 
			
		||||
package handler
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/service"
 | 
			
		||||
	"chatplus/service/payment"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/imroc/req/v3"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type TestHandler struct {
 | 
			
		||||
	db        *gorm.DB
 | 
			
		||||
	snowflake *service.Snowflake
 | 
			
		||||
	js        *payment.PayJS
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewTestHandler(db *gorm.DB, snowflake *service.Snowflake, js *payment.PayJS) *TestHandler {
 | 
			
		||||
	return &TestHandler{db: db, snowflake: snowflake, js: js}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type reqBody struct {
 | 
			
		||||
	BotType       string        `json:"botType"`
 | 
			
		||||
	Prompt        string        `json:"prompt"`
 | 
			
		||||
	Base64Array   []interface{} `json:"base64Array,omitempty"`
 | 
			
		||||
	AccountFilter struct {
 | 
			
		||||
		InstanceId          string        `json:"instanceId"`
 | 
			
		||||
		Modes               []interface{} `json:"modes"`
 | 
			
		||||
		Remix               bool          `json:"remix"`
 | 
			
		||||
		RemixAutoConsidered bool          `json:"remixAutoConsidered"`
 | 
			
		||||
	} `json:"accountFilter,omitempty"`
 | 
			
		||||
	NotifyHook string `json:"notifyHook"`
 | 
			
		||||
	State      string `json:"state,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type resBody struct {
 | 
			
		||||
	Code        int    `json:"code"`
 | 
			
		||||
	Description string `json:"description"`
 | 
			
		||||
	Properties  struct {
 | 
			
		||||
	} `json:"properties"`
 | 
			
		||||
	Result string `json:"result"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *TestHandler) Test(c *gin.Context) {
 | 
			
		||||
	image(c)
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func upscale(c *gin.Context) {
 | 
			
		||||
	apiURL := "https://api.openai1s.cn/mj/submit/action"
 | 
			
		||||
	token := "sk-QpBaQn9Z5vngsjJaFdDfC9Db90C845EaB5E764578a7d292a"
 | 
			
		||||
	body := map[string]string{
 | 
			
		||||
		"customId":   "MJ::JOB::upsample::1::c80a8eb1-f2d1-4f40-8785-97eb99b7ba0a",
 | 
			
		||||
		"taskId":     "1704880156226095",
 | 
			
		||||
		"notifyHook": "http://r9it.com:6004/api/test/mj",
 | 
			
		||||
	}
 | 
			
		||||
	var res resBody
 | 
			
		||||
	var resErr errRes
 | 
			
		||||
	r, err := req.C().R().
 | 
			
		||||
		SetHeader("Authorization", "Bearer "+token).
 | 
			
		||||
		SetBody(body).
 | 
			
		||||
		SetSuccessResult(&res).
 | 
			
		||||
		SetErrorResult(&resErr).
 | 
			
		||||
		Post(apiURL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, "请求出错:"+err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if r.IsErrorState() {
 | 
			
		||||
		resp.ERROR(c, "返回错误状态:"+resErr.Error.Message)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, res)
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type queryRes struct {
 | 
			
		||||
	Action  string `json:"action"`
 | 
			
		||||
	Buttons []struct {
 | 
			
		||||
		CustomId string `json:"customId"`
 | 
			
		||||
		Emoji    string `json:"emoji"`
 | 
			
		||||
		Label    string `json:"label"`
 | 
			
		||||
		Style    int    `json:"style"`
 | 
			
		||||
		Type     int    `json:"type"`
 | 
			
		||||
	} `json:"buttons"`
 | 
			
		||||
	Description string `json:"description"`
 | 
			
		||||
	FailReason  string `json:"failReason"`
 | 
			
		||||
	FinishTime  int    `json:"finishTime"`
 | 
			
		||||
	Id          string `json:"id"`
 | 
			
		||||
	ImageUrl    string `json:"imageUrl"`
 | 
			
		||||
	Progress    string `json:"progress"`
 | 
			
		||||
	Prompt      string `json:"prompt"`
 | 
			
		||||
	PromptEn    string `json:"promptEn"`
 | 
			
		||||
	Properties  struct {
 | 
			
		||||
	} `json:"properties"`
 | 
			
		||||
	StartTime  int    `json:"startTime"`
 | 
			
		||||
	State      string `json:"state"`
 | 
			
		||||
	Status     string `json:"status"`
 | 
			
		||||
	SubmitTime int    `json:"submitTime"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func query(c *gin.Context) {
 | 
			
		||||
	apiURL := "https://api.openai1s.cn/mj/task/1704960661008372/fetch"
 | 
			
		||||
	token := "sk-QpBaQn9Z5vngsjJaFdDfC9Db90C845EaB5E764578a7d292a"
 | 
			
		||||
	var res queryRes
 | 
			
		||||
	r, err := req.C().R().SetHeader("Authorization", "Bearer "+token).
 | 
			
		||||
		SetSuccessResult(&res).
 | 
			
		||||
		Get(apiURL)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, "请求出错:"+err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if r.IsErrorState() {
 | 
			
		||||
		resp.ERROR(c, "返回错误状态:"+r.Status)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, res)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type errRes struct {
 | 
			
		||||
	Error struct {
 | 
			
		||||
		Message string `json:"message"`
 | 
			
		||||
	} `json:"error"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func image(c *gin.Context) {
 | 
			
		||||
	apiURL := "https://api.openai1s.cn/mj-fast/mj/submit/imagine"
 | 
			
		||||
	token := "sk-QpBaQn9Z5vngsjJaFdDfC9Db90C845EaB5E764578a7d292a"
 | 
			
		||||
	body := reqBody{
 | 
			
		||||
		BotType:    "MID_JOURNEY",
 | 
			
		||||
		Prompt:     "一个中国美女,手上拿着一桶爆米花,脸上带着迷人的微笑,白色衣服 --s 750 --v 6",
 | 
			
		||||
		NotifyHook: "http://r9it.com:6004/api/test/mj",
 | 
			
		||||
	}
 | 
			
		||||
	var res resBody
 | 
			
		||||
	var resErr errRes
 | 
			
		||||
	r, err := req.C().R().
 | 
			
		||||
		SetHeader("Authorization", "Bearer "+token).
 | 
			
		||||
		SetBody(body).
 | 
			
		||||
		SetSuccessResult(&res).
 | 
			
		||||
		SetErrorResult(&resErr).
 | 
			
		||||
		Post(apiURL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, "请求出错:"+err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if r.IsErrorState() {
 | 
			
		||||
		resp.ERROR(c, "返回错误状态:"+resErr.Error.Message)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, res)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type cbReq struct {
 | 
			
		||||
	Id          string      `json:"id"`
 | 
			
		||||
	Action      string      `json:"action"`
 | 
			
		||||
	Status      string      `json:"status"`
 | 
			
		||||
	Prompt      string      `json:"prompt"`
 | 
			
		||||
	PromptEn    string      `json:"promptEn"`
 | 
			
		||||
	Description string      `json:"description"`
 | 
			
		||||
	SubmitTime  int64       `json:"submitTime"`
 | 
			
		||||
	StartTime   int64       `json:"startTime"`
 | 
			
		||||
	FinishTime  int64       `json:"finishTime"`
 | 
			
		||||
	Progress    string      `json:"progress"`
 | 
			
		||||
	ImageUrl    string      `json:"imageUrl"`
 | 
			
		||||
	FailReason  interface{} `json:"failReason"`
 | 
			
		||||
	Properties  struct {
 | 
			
		||||
		FinalPrompt string `json:"finalPrompt"`
 | 
			
		||||
	} `json:"properties"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *TestHandler) Mj(c *gin.Context) {
 | 
			
		||||
	var data cbReq
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		logger.Error(err)
 | 
			
		||||
	}
 | 
			
		||||
	logger.Debugf("任务ID:%s,任务进度:%s,图片地址:%s, 最终提示词:%s", data.Id, data.Progress, data.ImageUrl, data.Properties.FinalPrompt)
 | 
			
		||||
	apiURL := "https://api.openai1s.cn/mj/task/" + data.Id + "/fetch"
 | 
			
		||||
	token := "sk-QpBaQn9Z5vngsjJaFdDfC9Db90C845EaB5E764578a7d292a"
 | 
			
		||||
	var res queryRes
 | 
			
		||||
	_, _ = req.C().R().SetHeader("Authorization", "Bearer "+token).
 | 
			
		||||
		SetSuccessResult(&res).
 | 
			
		||||
		Get(apiURL)
 | 
			
		||||
 | 
			
		||||
	fmt.Println(res.State, ",", res.ImageUrl, ",", res.Progress)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *TestHandler) initUserNickname(c *gin.Context) {
 | 
			
		||||
	var users []model.User
 | 
			
		||||
	tx := h.db.Find(&users)
 | 
			
		||||
	if tx.Error != nil {
 | 
			
		||||
		resp.ERROR(c, tx.Error.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, u := range users {
 | 
			
		||||
		u.Nickname = fmt.Sprintf("极客学长@%d", utils.RandomNumber(6))
 | 
			
		||||
		h.db.Updates(&u)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *TestHandler) initMjTaskId(c *gin.Context) {
 | 
			
		||||
	var jobs []model.MidJourneyJob
 | 
			
		||||
	tx := h.db.Find(&jobs)
 | 
			
		||||
	if tx.Error != nil {
 | 
			
		||||
		resp.ERROR(c, tx.Error.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, job := range jobs {
 | 
			
		||||
		id, _ := h.snowflake.Next(true)
 | 
			
		||||
		job.TaskId = id
 | 
			
		||||
		h.db.Updates(&job)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
@@ -3,9 +3,13 @@ package handler
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/service/oss"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type UploadHandler struct {
 | 
			
		||||
@@ -21,11 +25,46 @@ func NewUploadHandler(app *core.AppServer, db *gorm.DB, manager *oss.UploaderMan
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *UploadHandler) Upload(c *gin.Context) {
 | 
			
		||||
	fileURL, err := h.uploaderManager.GetUploadHandler().PutFile(c, "file")
 | 
			
		||||
	file, err := h.uploaderManager.GetUploadHandler().PutFile(c, "file")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, fileURL)
 | 
			
		||||
	userId := h.GetLoginUserId(c)
 | 
			
		||||
	res := h.db.Create(&model.File{
 | 
			
		||||
		UserId:    userId,
 | 
			
		||||
		Name:      file.Name,
 | 
			
		||||
		URL:       file.URL,
 | 
			
		||||
		Ext:       file.Ext,
 | 
			
		||||
		Size:      file.Size,
 | 
			
		||||
		CreatedAt: time.Time{},
 | 
			
		||||
	})
 | 
			
		||||
	if res.Error != nil || res.RowsAffected == 0 {
 | 
			
		||||
		resp.ERROR(c, "error with update database: "+res.Error.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, file)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *UploadHandler) List(c *gin.Context) {
 | 
			
		||||
	userId := h.GetLoginUserId(c)
 | 
			
		||||
	var items []model.File
 | 
			
		||||
	var files = make([]vo.File, 0)
 | 
			
		||||
	h.db.Debug().Where("user_id = ?", userId).Find(&items)
 | 
			
		||||
	if len(items) > 0 {
 | 
			
		||||
		for _, v := range items {
 | 
			
		||||
			var file vo.File
 | 
			
		||||
			err := utils.CopyObject(v, &file)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				logger.Error(err)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			file.CreatedAt = v.CreatedAt.Unix()
 | 
			
		||||
			files = append(files, file)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, files)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,17 +3,17 @@ package handler
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/store"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/go-redis/redis/v8"
 | 
			
		||||
	"github.com/golang-jwt/jwt/v5"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-redis/redis/v8"
 | 
			
		||||
	"github.com/golang-jwt/jwt/v5"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/lionsoul2014/ip2region/binding/golang/xdb"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
@@ -23,7 +23,6 @@ type UserHandler struct {
 | 
			
		||||
	BaseHandler
 | 
			
		||||
	db       *gorm.DB
 | 
			
		||||
	searcher *xdb.Searcher
 | 
			
		||||
	leveldb  *store.LevelDB
 | 
			
		||||
	redis    *redis.Client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -31,9 +30,8 @@ func NewUserHandler(
 | 
			
		||||
	app *core.AppServer,
 | 
			
		||||
	db *gorm.DB,
 | 
			
		||||
	searcher *xdb.Searcher,
 | 
			
		||||
	levelDB *store.LevelDB,
 | 
			
		||||
	client *redis.Client) *UserHandler {
 | 
			
		||||
	handler := &UserHandler{db: db, searcher: searcher, leveldb: levelDB, redis: client}
 | 
			
		||||
	handler := &UserHandler{db: db, searcher: searcher, redis: client}
 | 
			
		||||
	handler.App = app
 | 
			
		||||
	return handler
 | 
			
		||||
}
 | 
			
		||||
@@ -42,51 +40,59 @@ func NewUserHandler(
 | 
			
		||||
func (h *UserHandler) Register(c *gin.Context) {
 | 
			
		||||
	// parameters process
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Mobile   string `json:"mobile"`
 | 
			
		||||
		Password string `json:"password"`
 | 
			
		||||
		Code     int    `json:"code"`
 | 
			
		||||
		Username   string `json:"username"`
 | 
			
		||||
		Password   string `json:"password"`
 | 
			
		||||
		Code       string `json:"code"`
 | 
			
		||||
		InviteCode string `json:"invite_code"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	data.Password = strings.TrimSpace(data.Password)
 | 
			
		||||
 | 
			
		||||
	if len(data.Mobile) < 10 {
 | 
			
		||||
		resp.ERROR(c, "请输入合法的手机号")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if len(data.Password) < 8 {
 | 
			
		||||
		resp.ERROR(c, "密码长度不能少于8个字符")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 检查验证码
 | 
			
		||||
	key := CodeStorePrefix + data.Mobile
 | 
			
		||||
	if h.App.SysConfig.EnabledMsg {
 | 
			
		||||
		var code int
 | 
			
		||||
		err := h.leveldb.Get(key, &code)
 | 
			
		||||
	var key string
 | 
			
		||||
	if utils.ContainsStr(h.App.SysConfig.RegisterWays, "email") ||
 | 
			
		||||
		utils.ContainsStr(h.App.SysConfig.RegisterWays, "mobile") {
 | 
			
		||||
		key = CodeStorePrefix + data.Username
 | 
			
		||||
		code, err := h.redis.Get(c, key).Result()
 | 
			
		||||
		if err != nil || code != data.Code {
 | 
			
		||||
			resp.ERROR(c, "短信验证码错误")
 | 
			
		||||
			resp.ERROR(c, "验证码错误")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 验证邀请码
 | 
			
		||||
	inviteCode := model.InviteCode{}
 | 
			
		||||
	if data.InviteCode != "" {
 | 
			
		||||
		res := h.db.Where("code = ?", data.InviteCode).First(&inviteCode)
 | 
			
		||||
		if res.Error != nil {
 | 
			
		||||
			resp.ERROR(c, "无效的邀请码")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// check if the username is exists
 | 
			
		||||
	var item model.User
 | 
			
		||||
	res := h.db.Where("mobile = ?", data.Mobile).First(&item)
 | 
			
		||||
	res := h.db.Where("username = ?", data.Username).First(&item)
 | 
			
		||||
	if res.RowsAffected > 0 {
 | 
			
		||||
		resp.ERROR(c, "该手机号码已经被注册,请更换其他手机号")
 | 
			
		||||
		resp.ERROR(c, "该用户名已经被注册")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	salt := utils.RandString(8)
 | 
			
		||||
	user := model.User{
 | 
			
		||||
		Username:   data.Username,
 | 
			
		||||
		Password:   utils.GenPassword(data.Password, salt),
 | 
			
		||||
		Nickname:   fmt.Sprintf("极客学长@%d", utils.RandomNumber(6)),
 | 
			
		||||
		Avatar:     "/images/avatar/user.png",
 | 
			
		||||
		Salt:       salt,
 | 
			
		||||
		Status:     true,
 | 
			
		||||
		Mobile:     data.Mobile,
 | 
			
		||||
		ChatRoles:  utils.JsonEncode([]string{"gpt"}),               // 默认只订阅通用助手角色
 | 
			
		||||
		ChatModels: utils.JsonEncode(h.App.SysConfig.DefaultModels), // 默认开通的模型
 | 
			
		||||
		ChatConfig: utils.JsonEncode(types.UserChatConfig{
 | 
			
		||||
@@ -96,9 +102,10 @@ func (h *UserHandler) Register(c *gin.Context) {
 | 
			
		||||
				types.ChatGLM: "",
 | 
			
		||||
			},
 | 
			
		||||
		}),
 | 
			
		||||
		Calls:    h.App.SysConfig.UserInitCalls,
 | 
			
		||||
		Calls:    h.App.SysConfig.InitChatCalls,
 | 
			
		||||
		ImgCalls: h.App.SysConfig.InitImgCalls,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	res = h.db.Create(&user)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "保存数据失败")
 | 
			
		||||
@@ -106,10 +113,29 @@ func (h *UserHandler) Register(c *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if h.App.SysConfig.EnabledMsg {
 | 
			
		||||
		_ = h.leveldb.Delete(key) // 注册成功,删除短信验证码
 | 
			
		||||
	// 记录邀请关系
 | 
			
		||||
	if data.InviteCode != "" {
 | 
			
		||||
		// 增加邀请数量
 | 
			
		||||
		h.db.Model(&model.InviteCode{}).Where("code = ?", data.InviteCode).UpdateColumn("reg_num", gorm.Expr("reg_num + ?", 1))
 | 
			
		||||
		if h.App.SysConfig.InviteChatCalls > 0 {
 | 
			
		||||
			h.db.Model(&model.User{}).Where("id = ?", inviteCode.UserId).UpdateColumn("calls", gorm.Expr("calls + ?", h.App.SysConfig.InviteChatCalls))
 | 
			
		||||
		}
 | 
			
		||||
		if h.App.SysConfig.InviteImgCalls > 0 {
 | 
			
		||||
			h.db.Model(&model.User{}).Where("id = ?", inviteCode.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls + ?", h.App.SysConfig.InviteImgCalls))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 添加邀请记录
 | 
			
		||||
		h.db.Create(&model.InviteLog{
 | 
			
		||||
			InviterId:  inviteCode.UserId,
 | 
			
		||||
			UserId:     user.Id,
 | 
			
		||||
			Username:   user.Username,
 | 
			
		||||
			InviteCode: inviteCode.Code,
 | 
			
		||||
			Reward:     utils.JsonEncode(types.InviteReward{ChatCalls: h.App.SysConfig.InviteChatCalls, ImgCalls: h.App.SysConfig.InviteImgCalls}),
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_ = h.redis.Del(c, key) // 注册成功,删除短信验证码
 | 
			
		||||
 | 
			
		||||
	// 自动登录创建 token
 | 
			
		||||
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
 | 
			
		||||
		"user_id": user.Id,
 | 
			
		||||
@@ -132,7 +158,7 @@ func (h *UserHandler) Register(c *gin.Context) {
 | 
			
		||||
// Login 用户登录
 | 
			
		||||
func (h *UserHandler) Login(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Mobile   string `json:"username"`
 | 
			
		||||
		Username string `json:"username"`
 | 
			
		||||
		Password string `json:"password"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
@@ -140,7 +166,7 @@ func (h *UserHandler) Login(c *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	var user model.User
 | 
			
		||||
	res := h.db.Where("mobile = ?", data.Mobile).First(&user)
 | 
			
		||||
	res := h.db.Where("username = ?", data.Username).First(&user)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "用户名不存在")
 | 
			
		||||
		return
 | 
			
		||||
@@ -164,7 +190,7 @@ func (h *UserHandler) Login(c *gin.Context) {
 | 
			
		||||
 | 
			
		||||
	h.db.Create(&model.UserLoginLog{
 | 
			
		||||
		UserId:       user.Id,
 | 
			
		||||
		Username:     user.Mobile,
 | 
			
		||||
		Username:     user.Username,
 | 
			
		||||
		LoginIp:      c.ClientIP(),
 | 
			
		||||
		LoginAddress: utils.Ip2Region(h.searcher, c.ClientIP()),
 | 
			
		||||
	})
 | 
			
		||||
@@ -224,7 +250,8 @@ func (h *UserHandler) Session(c *gin.Context) {
 | 
			
		||||
 | 
			
		||||
type userProfile struct {
 | 
			
		||||
	Id          uint                 `json:"id"`
 | 
			
		||||
	Mobile      string               `json:"mobile"`
 | 
			
		||||
	Nickname    string               `json:"nickname"`
 | 
			
		||||
	Username    string               `json:"username"`
 | 
			
		||||
	Avatar      string               `json:"avatar"`
 | 
			
		||||
	ChatConfig  types.UserChatConfig `json:"chat_config"`
 | 
			
		||||
	Calls       int                  `json:"calls"`
 | 
			
		||||
@@ -269,7 +296,7 @@ func (h *UserHandler) ProfileUpdate(c *gin.Context) {
 | 
			
		||||
	}
 | 
			
		||||
	h.db.First(&user, user.Id)
 | 
			
		||||
	user.Avatar = data.Avatar
 | 
			
		||||
	user.ChatConfig = utils.JsonEncode(data.ChatConfig)
 | 
			
		||||
	user.Nickname = data.Nickname
 | 
			
		||||
	res := h.db.Updates(&user)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "更新用户信息失败")
 | 
			
		||||
@@ -279,8 +306,8 @@ func (h *UserHandler) ProfileUpdate(c *gin.Context) {
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Password 更新密码
 | 
			
		||||
func (h *UserHandler) Password(c *gin.Context) {
 | 
			
		||||
// UpdatePass 更新密码
 | 
			
		||||
func (h *UserHandler) UpdatePass(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		OldPass  string `json:"old_pass"`
 | 
			
		||||
		Password string `json:"password"`
 | 
			
		||||
@@ -319,46 +346,83 @@ func (h *UserHandler) Password(c *gin.Context) {
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// BindMobile 绑定手机号
 | 
			
		||||
func (h *UserHandler) BindMobile(c *gin.Context) {
 | 
			
		||||
// ResetPass 重置密码
 | 
			
		||||
func (h *UserHandler) ResetPass(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Mobile string `json:"mobile"`
 | 
			
		||||
		Code   int    `json:"code"`
 | 
			
		||||
		Username string `json:"username"`
 | 
			
		||||
		Code     string `json:"code"`     // 验证码
 | 
			
		||||
		Password string `json:"password"` // 新密码
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 检查手机号是否被其他账号绑定
 | 
			
		||||
	var item model.User
 | 
			
		||||
	res := h.db.Where("mobile = ?", data.Mobile).First(&item)
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		resp.ERROR(c, "该手机号已经被其他账号绑定")
 | 
			
		||||
	var user model.User
 | 
			
		||||
	res := h.db.Where("username", data.Username).First(&user)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "用户不存在!")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 检查验证码
 | 
			
		||||
	key := CodeStorePrefix + data.Mobile
 | 
			
		||||
	var code int
 | 
			
		||||
	err := h.leveldb.Get(key, &code)
 | 
			
		||||
	key := CodeStorePrefix + data.Username
 | 
			
		||||
	code, err := h.redis.Get(c, key).Result()
 | 
			
		||||
	if err != nil || code != data.Code {
 | 
			
		||||
		resp.ERROR(c, "短信验证码错误")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	password := utils.GenPassword(data.Password, user.Salt)
 | 
			
		||||
	user.Password = password
 | 
			
		||||
	res = h.db.Updates(&user)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c)
 | 
			
		||||
	} else {
 | 
			
		||||
		h.redis.Del(c, key)
 | 
			
		||||
		resp.SUCCESS(c)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// BindUsername 重置账号
 | 
			
		||||
func (h *UserHandler) BindUsername(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Username string `json:"username"`
 | 
			
		||||
		Code     string `json:"code"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 检查验证码
 | 
			
		||||
	key := CodeStorePrefix + data.Username
 | 
			
		||||
	code, err := h.redis.Get(c, key).Result()
 | 
			
		||||
	if err != nil || code != data.Code {
 | 
			
		||||
		resp.ERROR(c, "验证码错误")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 检查手机号是否被其他账号绑定
 | 
			
		||||
	var item model.User
 | 
			
		||||
	res := h.db.Where("username = ?", data.Username).First(&item)
 | 
			
		||||
	if res.Error == nil {
 | 
			
		||||
		resp.ERROR(c, "该账号已经被其他账号绑定")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	user, err := utils.GetLoginUser(c, h.db)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.NotAuth(c)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	res = h.db.Model(&user).UpdateColumn("mobile", data.Mobile)
 | 
			
		||||
	res = h.db.Model(&user).UpdateColumn("username", data.Username)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "更新数据库失败")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_ = h.leveldb.Delete(key) // 删除短信验证码
 | 
			
		||||
	_ = h.redis.Del(c, key) // 删除短信验证码
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										138
									
								
								api/main.go
									
									
									
									
									
								
							
							
						
						
									
										138
									
								
								api/main.go
									
									
									
									
									
								
							@@ -8,16 +8,15 @@ import (
 | 
			
		||||
	"chatplus/handler/chatimpl"
 | 
			
		||||
	logger2 "chatplus/logger"
 | 
			
		||||
	"chatplus/service"
 | 
			
		||||
	"chatplus/service/fun"
 | 
			
		||||
	"chatplus/service/mj"
 | 
			
		||||
	"chatplus/service/oss"
 | 
			
		||||
	"chatplus/service/payment"
 | 
			
		||||
	"chatplus/service/sd"
 | 
			
		||||
	"chatplus/service/sms"
 | 
			
		||||
	"chatplus/service/wx"
 | 
			
		||||
	"chatplus/store"
 | 
			
		||||
	"context"
 | 
			
		||||
	"embed"
 | 
			
		||||
	"github.com/go-redis/redis/v8"
 | 
			
		||||
	"io"
 | 
			
		||||
	"log"
 | 
			
		||||
	"os"
 | 
			
		||||
@@ -26,6 +25,8 @@ import (
 | 
			
		||||
	"syscall"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-redis/redis/v8"
 | 
			
		||||
 | 
			
		||||
	"github.com/lionsoul2014/ip2region/binding/golang/xdb"
 | 
			
		||||
	"go.uber.org/fx"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
@@ -57,19 +58,15 @@ func main() {
 | 
			
		||||
	if configFile == "" {
 | 
			
		||||
		configFile = "config.toml"
 | 
			
		||||
	}
 | 
			
		||||
	var debug bool
 | 
			
		||||
	debugEnv := os.Getenv("DEBUG")
 | 
			
		||||
	if debugEnv == "" {
 | 
			
		||||
		debug = true
 | 
			
		||||
	} else {
 | 
			
		||||
		debug, _ = strconv.ParseBool(os.Getenv("DEBUG"))
 | 
			
		||||
	}
 | 
			
		||||
	debug, _ := strconv.ParseBool(os.Getenv("APP_DEBUG"))
 | 
			
		||||
	logger.Info("Loading config file: ", configFile)
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if err := recover(); err != nil {
 | 
			
		||||
			logger.Error("Panic Error:", err)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
	if !debug {
 | 
			
		||||
		defer func() {
 | 
			
		||||
			if err := recover(); err != nil {
 | 
			
		||||
				logger.Error("Panic Error:", err)
 | 
			
		||||
			}
 | 
			
		||||
		}()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	app := fx.New(
 | 
			
		||||
		// 初始化配置应用配置
 | 
			
		||||
@@ -94,7 +91,6 @@ func main() {
 | 
			
		||||
		// 初始化数据库
 | 
			
		||||
		fx.Provide(store.NewGormConfig),
 | 
			
		||||
		fx.Provide(store.NewMysql),
 | 
			
		||||
		fx.Provide(store.NewLevelDB),
 | 
			
		||||
		fx.Provide(store.NewRedisClient),
 | 
			
		||||
 | 
			
		||||
		fx.Provide(func() embed.FS {
 | 
			
		||||
@@ -115,9 +111,6 @@ func main() {
 | 
			
		||||
			return xdb.NewWithBuffer(cBuff)
 | 
			
		||||
		}),
 | 
			
		||||
 | 
			
		||||
		// 创建函数
 | 
			
		||||
		fx.Provide(fun.NewFunctions),
 | 
			
		||||
 | 
			
		||||
		// 创建控制器
 | 
			
		||||
		fx.Provide(handler.NewChatRoleHandler),
 | 
			
		||||
		fx.Provide(handler.NewUserHandler),
 | 
			
		||||
@@ -145,13 +138,16 @@ func main() {
 | 
			
		||||
		fx.Provide(admin.NewOrderHandler),
 | 
			
		||||
 | 
			
		||||
		// 创建服务
 | 
			
		||||
		fx.Provide(service.NewAliYunSmsService),
 | 
			
		||||
		fx.Provide(sms.NewSendServiceManager),
 | 
			
		||||
		fx.Provide(func(config *types.AppConfig) *service.CaptchaService {
 | 
			
		||||
			return service.NewCaptchaService(config.ApiConfig)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Provide(oss.NewUploaderManager),
 | 
			
		||||
		fx.Provide(mj.NewService),
 | 
			
		||||
 | 
			
		||||
		// 邮件服务
 | 
			
		||||
		fx.Provide(service.NewSmtpService),
 | 
			
		||||
 | 
			
		||||
		// 微信机器人服务
 | 
			
		||||
		fx.Provide(wx.NewWeChatBot),
 | 
			
		||||
		fx.Invoke(func(config *types.AppConfig, bot *wx.Bot) {
 | 
			
		||||
@@ -163,36 +159,22 @@ func main() {
 | 
			
		||||
			}
 | 
			
		||||
		}),
 | 
			
		||||
 | 
			
		||||
		// MidJourney 机器人
 | 
			
		||||
		fx.Provide(mj.NewBot),
 | 
			
		||||
		fx.Provide(mj.NewClient),
 | 
			
		||||
		fx.Invoke(func(config *types.AppConfig, bot *mj.Bot) {
 | 
			
		||||
			if config.MjConfig.Enabled {
 | 
			
		||||
				err := bot.Run()
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					log.Fatal("MidJourney 服务启动失败:", err)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(config *types.AppConfig, mjService *mj.Service) {
 | 
			
		||||
			if config.MjConfig.Enabled {
 | 
			
		||||
				go func() {
 | 
			
		||||
					mjService.Run()
 | 
			
		||||
				}()
 | 
			
		||||
		// MidJourney service pool
 | 
			
		||||
		fx.Provide(mj.NewServicePool),
 | 
			
		||||
		fx.Invoke(func(pool *mj.ServicePool) {
 | 
			
		||||
			if pool.HasAvailableService() {
 | 
			
		||||
				pool.DownloadImages()
 | 
			
		||||
				pool.CheckTaskNotify()
 | 
			
		||||
				pool.SyncTaskProgress()
 | 
			
		||||
			}
 | 
			
		||||
		}),
 | 
			
		||||
 | 
			
		||||
		// Stable Diffusion 机器人
 | 
			
		||||
		fx.Provide(sd.NewService),
 | 
			
		||||
		fx.Invoke(func(config *types.AppConfig, service *sd.Service) {
 | 
			
		||||
			if config.SdConfig.Enabled {
 | 
			
		||||
				go func() {
 | 
			
		||||
					service.Run()
 | 
			
		||||
				}()
 | 
			
		||||
			}
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Provide(sd.NewServicePool),
 | 
			
		||||
 | 
			
		||||
		fx.Provide(payment.NewAlipayService),
 | 
			
		||||
		fx.Provide(payment.NewHuPiPay),
 | 
			
		||||
		fx.Provide(payment.NewPayJS),
 | 
			
		||||
		fx.Provide(service.NewSnowflake),
 | 
			
		||||
		fx.Provide(service.NewXXLJobExecutor),
 | 
			
		||||
		fx.Invoke(func(exec *service.XXLJobExecutor, config *types.AppConfig) {
 | 
			
		||||
@@ -217,8 +199,9 @@ func main() {
 | 
			
		||||
			group.GET("session", h.Session)
 | 
			
		||||
			group.GET("profile", h.Profile)
 | 
			
		||||
			group.POST("profile/update", h.ProfileUpdate)
 | 
			
		||||
			group.POST("password", h.Password)
 | 
			
		||||
			group.POST("bind/mobile", h.BindMobile)
 | 
			
		||||
			group.POST("password", h.UpdatePass)
 | 
			
		||||
			group.POST("bind/username", h.BindUsername)
 | 
			
		||||
			group.POST("resetPass", h.ResetPass)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *chatimpl.ChatHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/chat/")
 | 
			
		||||
@@ -234,10 +217,10 @@ func main() {
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.UploadHandler) {
 | 
			
		||||
			s.Engine.POST("/api/upload", h.Upload)
 | 
			
		||||
			s.Engine.GET("/api/upload/list", h.List)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.SmsHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/sms/")
 | 
			
		||||
			group.GET("status", h.Status)
 | 
			
		||||
			group.POST("code", h.SendCode)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.CaptchaHandler) {
 | 
			
		||||
@@ -251,17 +234,21 @@ func main() {
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.MidJourneyHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/mj/")
 | 
			
		||||
			group.Any("client", h.Client)
 | 
			
		||||
			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)
 | 
			
		||||
			group.POST("remove", h.Remove)
 | 
			
		||||
			group.POST("notify", h.Notify)
 | 
			
		||||
			group.POST("publish", h.Publish)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.SdJobHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/sd")
 | 
			
		||||
			group.POST("image", h.Image)
 | 
			
		||||
			group.GET("jobs", h.JobList)
 | 
			
		||||
			group.Any("client", h.Client)
 | 
			
		||||
			group.POST("remove", h.Remove)
 | 
			
		||||
			group.POST("publish", h.Publish)
 | 
			
		||||
		}),
 | 
			
		||||
 | 
			
		||||
		// 管理后台控制器
 | 
			
		||||
@@ -275,12 +262,12 @@ func main() {
 | 
			
		||||
			group.POST("login", h.Login)
 | 
			
		||||
			group.GET("logout", h.Logout)
 | 
			
		||||
			group.GET("session", h.Session)
 | 
			
		||||
			group.GET("migrate", h.Migrate)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.ApiKeyHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/admin/apikey/")
 | 
			
		||||
			group.POST("save", h.Save)
 | 
			
		||||
			group.GET("list", h.List)
 | 
			
		||||
			group.POST("set", h.Set)
 | 
			
		||||
			group.GET("remove", h.Remove)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.UserHandler) {
 | 
			
		||||
@@ -296,11 +283,13 @@ func main() {
 | 
			
		||||
			group.GET("list", h.List)
 | 
			
		||||
			group.POST("save", h.Save)
 | 
			
		||||
			group.POST("sort", h.Sort)
 | 
			
		||||
			group.POST("set", h.Set)
 | 
			
		||||
			group.GET("remove", h.Remove)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.RewardHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/admin/reward/")
 | 
			
		||||
			group.GET("list", h.List)
 | 
			
		||||
			group.GET("remove", h.Remove)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.DashboardHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/admin/dashboard/")
 | 
			
		||||
@@ -314,16 +303,19 @@ func main() {
 | 
			
		||||
			group := s.Engine.Group("/api/admin/model/")
 | 
			
		||||
			group.POST("save", h.Save)
 | 
			
		||||
			group.GET("list", h.List)
 | 
			
		||||
			group.POST("enable", h.Enable)
 | 
			
		||||
			group.POST("set", h.Set)
 | 
			
		||||
			group.POST("sort", h.Sort)
 | 
			
		||||
			group.GET("remove", h.Remove)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.PaymentHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/payment/")
 | 
			
		||||
			group.GET("alipay", h.Alipay)
 | 
			
		||||
			group.GET("doPay", h.DoPay)
 | 
			
		||||
			group.GET("payWays", h.GetPayWays)
 | 
			
		||||
			group.POST("query", h.OrderQuery)
 | 
			
		||||
			group.POST("alipay/qrcode", h.AlipayQrcode)
 | 
			
		||||
			group.POST("qrcode", h.PayQrcode)
 | 
			
		||||
			group.POST("alipay/notify", h.AlipayNotify)
 | 
			
		||||
			group.POST("hupipay/notify", h.HuPiPayNotify)
 | 
			
		||||
			group.POST("payjs/notify", h.PayJsNotify)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.ProductHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/admin/product/")
 | 
			
		||||
@@ -347,13 +339,53 @@ func main() {
 | 
			
		||||
			group.GET("list", h.List)
 | 
			
		||||
		}),
 | 
			
		||||
 | 
			
		||||
		fx.Provide(handler.NewInviteHandler),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.InviteHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/invite/")
 | 
			
		||||
			group.GET("code", h.Code)
 | 
			
		||||
			group.POST("list", h.List)
 | 
			
		||||
			group.GET("hits", h.Hits)
 | 
			
		||||
		}),
 | 
			
		||||
 | 
			
		||||
		fx.Provide(handler.NewPromptHandler),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.PromptHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/prompt/")
 | 
			
		||||
			group.POST("rewrite", h.Rewrite)
 | 
			
		||||
			group.POST("translate", h.Translate)
 | 
			
		||||
		}),
 | 
			
		||||
 | 
			
		||||
		fx.Provide(admin.NewFunctionHandler),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.FunctionHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/admin/function/")
 | 
			
		||||
			group.POST("save", h.Save)
 | 
			
		||||
			group.POST("set", h.Set)
 | 
			
		||||
			group.GET("list", h.List)
 | 
			
		||||
			group.GET("remove", h.Remove)
 | 
			
		||||
			group.GET("token", h.GenToken)
 | 
			
		||||
		}),
 | 
			
		||||
 | 
			
		||||
		fx.Provide(handler.NewFunctionHandler),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.FunctionHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/function/")
 | 
			
		||||
			group.POST("weibo", h.WeiBo)
 | 
			
		||||
			group.POST("zaobao", h.ZaoBao)
 | 
			
		||||
			group.POST("dalle3", h.Dall3)
 | 
			
		||||
		}),
 | 
			
		||||
 | 
			
		||||
		fx.Provide(handler.NewTestHandler),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.TestHandler) {
 | 
			
		||||
			s.Engine.GET("/api/test", h.Test)
 | 
			
		||||
			s.Engine.POST("/api/test/mj", h.Mj)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, db *gorm.DB) {
 | 
			
		||||
			err := s.Run(db)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
		}),
 | 
			
		||||
 | 
			
		||||
		fx.Invoke(func(h *chatimpl.ChatHandler) {
 | 
			
		||||
			h.Init()
 | 
			
		||||
		}),
 | 
			
		||||
		// 注册生命周期回调函数
 | 
			
		||||
		fx.Invoke(func(lifecycle fx.Lifecycle, lc *AppLifecycle) {
 | 
			
		||||
			lifecycle.Append(fx.Hook{
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										38
									
								
								api/res/certs/alipay/alipayPublicCert.crt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								api/res/certs/alipay/alipayPublicCert.crt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
			
		||||
-----BEGIN CERTIFICATE-----
 | 
			
		||||
MIIDszCCApugAwIBAgIQICMRB0rBU2/rZJbfJGMYIzANBgkqhkiG9w0BAQsFADCBkTELMAkGA1UE
 | 
			
		||||
BhMCQ04xGzAZBgNVBAoMEkFudCBGaW5hbmNpYWwgdGVzdDElMCMGA1UECwwcQ2VydGlmaWNhdGlv
 | 
			
		||||
biBBdXRob3JpdHkgdGVzdDE+MDwGA1UEAww1QW50IEZpbmFuY2lhbCBDZXJ0aWZpY2F0aW9uIEF1
 | 
			
		||||
dGhvcml0eSBDbGFzcyAyIFIxIHRlc3QwHhcNMjMxMTA3MDYzNTQxWhcNMjQxMTA2MDYzNTQxWjCB
 | 
			
		||||
hDELMAkGA1UEBhMCQ04xHzAdBgNVBAoMFm1ib25meTkwMTVAc2FuZGJveC5jb20xDzANBgNVBAsM
 | 
			
		||||
BkFsaXBheTFDMEEGA1UEAww65pSv5LuY5a6dKOS4reWbvSnnvZHnu5zmioDmnK/mnInpmZDlhazl
 | 
			
		||||
j7gtMjA4ODcyMTAyMDc1MDU4MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKsoKcw5
 | 
			
		||||
sxaiyV7mpWzDtnQ1K518eQLP0+dJlZAf06aBep/Aj9DIqrba/k7DHt8dKQvILMLAMpN1+2IRxbaO
 | 
			
		||||
yxMa/laj3lZ1eHrB6F077O3D62oHcE3noZtXL0N1zZAxpmkNmYIHeLZS2oLMS4ANu47O/wpDC7BV
 | 
			
		||||
HjdpZugtdPJ4mxdCpM9GDdLs7W4s5QI4PUPK4skFNMFoKI+0cYP/9ju87UP//IHC/K510GWNl+Gn
 | 
			
		||||
Cvgag3AmiIB0utJNsGhxm6zT1T9tUWjW9iz/BxBKiPatsCX9VpPQzGnW7ZonRQtiZSokIlP2IPvl
 | 
			
		||||
H5DcwpWUz3/LUY0SmKxnKOEYeOOqCW8CAwEAAaMSMBAwDgYDVR0PAQH/BAQDAgTwMA0GCSqGSIb3
 | 
			
		||||
DQEBCwUAA4IBAQAtgxF2EzjOndEFxBUD9tFwcSt6XKGggOp52oft1pvynPg4ALTLafOtfEPDrFBH
 | 
			
		||||
PwpYrSu9s9C8NJtaA2HrlCfBjIuwEFTXiN+HPvS0SwSPKt9AXEiTcOF8vDcGamEen8QI4fo5Jia7
 | 
			
		||||
2VRKkerkww5/+FzSaVO7ZUKuL80M1QJStmAZc8kPPwdYOTTW2bGf8BcmSDL6SPElBkt7tCCRd4sn
 | 
			
		||||
+jq4cZ0yb2i77rBZCwHcTvfTqIBblPwLv4uGvg3+83BxIB5w6Kqp06bKEAPmobFY5IVHa+ON0/qi
 | 
			
		||||
BXxXr+WQ3piKRVQEN64+PTAjSc67Ix1umvpLl3Ko6Ry7NJmpDcUn
 | 
			
		||||
-----END CERTIFICATE-----
 | 
			
		||||
-----BEGIN CERTIFICATE-----
 | 
			
		||||
MIIDszCCApugAwIBAgIQIBkIGbgVxq210KxLJ+YA/TANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UE
 | 
			
		||||
BhMCQ04xFjAUBgNVBAoMDUFudCBGaW5hbmNpYWwxJTAjBgNVBAsMHENlcnRpZmljYXRpb24gQXV0
 | 
			
		||||
aG9yaXR5IHRlc3QxNjA0BgNVBAMMLUFudCBGaW5hbmNpYWwgQ2VydGlmaWNhdGlvbiBBdXRob3Jp
 | 
			
		||||
dHkgUjEgdGVzdDAeFw0xOTA4MTkxMTE2MDBaFw0yNDA4MDExMTE2MDBaMIGRMQswCQYDVQQGEwJD
 | 
			
		||||
TjEbMBkGA1UECgwSQW50IEZpbmFuY2lhbCB0ZXN0MSUwIwYDVQQLDBxDZXJ0aWZpY2F0aW9uIEF1
 | 
			
		||||
dGhvcml0eSB0ZXN0MT4wPAYDVQQDDDVBbnQgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9y
 | 
			
		||||
aXR5IENsYXNzIDIgUjEgdGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMh4FKYO
 | 
			
		||||
ZyRQHD6eFbPKZeSAnrfjfU7xmS9Yoozuu+iuqZlb6Z0SPLUqqTZAFZejOcmr07ln/pwZxluqplxC
 | 
			
		||||
5+B48End4nclDMlT5HPrDr3W0frs6Xsa2ZNcyil/iKNB5MbGll8LRAxntsKvZZj6vUTMb705gYgm
 | 
			
		||||
VUMILwi/ZxKTQqBtkT/kQQ5y6nOZsj7XI5rYdz6qqOROrpvS/d7iypdHOMIM9Iz9DlL1mrCykbBi
 | 
			
		||||
t25y+gTeXmuisHUwqaRpwtCGK4BayCqxRGbNipe6W73EK9lBrrzNtTr9NaysesT/v+l25JHCL9tG
 | 
			
		||||
wpNr1oWFzk4IHVOg0ORiQ6SUgxZUTYcCAwEAAaMSMBAwDgYDVR0PAQH/BAQDAgTwMA0GCSqGSIb3
 | 
			
		||||
DQEBCwUAA4IBAQBWThEoIaQoBX2YeRY/I8gu6TYnFXtyuCljANnXnM38ft+ikhE5mMNgKmJYLHvT
 | 
			
		||||
yWWWgwHoSAWEuml7EGbE/2AK2h3k0MdfiWLzdmpPCRG/RJHk6UB1pMHPilI+c0MVu16OPpKbg5Vf
 | 
			
		||||
LTv7dsAB40AzKsvyYw88/Ezi1osTXo6QQwda7uefvudirtb8FcQM9R66cJxl3kt1FXbpYwheIm/p
 | 
			
		||||
j1mq64swCoIYu4NrsUYtn6CV542DTQMI5QdXkn+PzUUly8F6kDp+KpMNd0avfWNL5+O++z+F5Szy
 | 
			
		||||
1CPta1D7EQ/eYmMP+mOQ35oifWIoFCpN6qQVBS/Hob1J/UUyg7BW
 | 
			
		||||
-----END CERTIFICATE-----
 | 
			
		||||
							
								
								
									
										88
									
								
								api/res/certs/alipay/alipayRootCert.crt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								api/res/certs/alipay/alipayRootCert.crt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,88 @@
 | 
			
		||||
-----BEGIN CERTIFICATE-----
 | 
			
		||||
MIIBszCCAVegAwIBAgIIaeL+wBcKxnswDAYIKoEcz1UBg3UFADAuMQswCQYDVQQG
 | 
			
		||||
EwJDTjEOMAwGA1UECgwFTlJDQUMxDzANBgNVBAMMBlJPT1RDQTAeFw0xMjA3MTQw
 | 
			
		||||
MzExNTlaFw00MjA3MDcwMzExNTlaMC4xCzAJBgNVBAYTAkNOMQ4wDAYDVQQKDAVO
 | 
			
		||||
UkNBQzEPMA0GA1UEAwwGUk9PVENBMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAE
 | 
			
		||||
MPCca6pmgcchsTf2UnBeL9rtp4nw+itk1Kzrmbnqo05lUwkwlWK+4OIrtFdAqnRT
 | 
			
		||||
V7Q9v1htkv42TsIutzd126NdMFswHwYDVR0jBBgwFoAUTDKxl9kzG8SmBcHG5Yti
 | 
			
		||||
W/CXdlgwDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFEwysZfZ
 | 
			
		||||
MxvEpgXBxuWLYlvwl3ZYMAwGCCqBHM9VAYN1BQADSAAwRQIgG1bSLeOXp3oB8H7b
 | 
			
		||||
53W+CKOPl2PknmWEq/lMhtn25HkCIQDaHDgWxWFtnCrBjH16/W3Ezn7/U/Vjo5xI
 | 
			
		||||
pDoiVhsLwg==
 | 
			
		||||
-----END CERTIFICATE-----
 | 
			
		||||
 | 
			
		||||
-----BEGIN CERTIFICATE-----
 | 
			
		||||
MIIF0zCCA7ugAwIBAgIIH8+hjWpIDREwDQYJKoZIhvcNAQELBQAwejELMAkGA1UE
 | 
			
		||||
BhMCQ04xFjAUBgNVBAoMDUFudCBGaW5hbmNpYWwxIDAeBgNVBAsMF0NlcnRpZmlj
 | 
			
		||||
YXRpb24gQXV0aG9yaXR5MTEwLwYDVQQDDChBbnQgRmluYW5jaWFsIENlcnRpZmlj
 | 
			
		||||
YXRpb24gQXV0aG9yaXR5IFIxMB4XDTE4MDMyMTEzNDg0MFoXDTM4MDIyODEzNDg0
 | 
			
		||||
MFowejELMAkGA1UEBhMCQ04xFjAUBgNVBAoMDUFudCBGaW5hbmNpYWwxIDAeBgNV
 | 
			
		||||
BAsMF0NlcnRpZmljYXRpb24gQXV0aG9yaXR5MTEwLwYDVQQDDChBbnQgRmluYW5j
 | 
			
		||||
aWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFIxMIICIjANBgkqhkiG9w0BAQEF
 | 
			
		||||
AAOCAg8AMIICCgKCAgEAtytTRcBNuur5h8xuxnlKJetT65cHGemGi8oD+beHFPTk
 | 
			
		||||
rUTlFt9Xn7fAVGo6QSsPb9uGLpUFGEdGmbsQ2q9cV4P89qkH04VzIPwT7AywJdt2
 | 
			
		||||
xAvMs+MgHFJzOYfL1QkdOOVO7NwKxH8IvlQgFabWomWk2Ei9WfUyxFjVO1LVh0Bp
 | 
			
		||||
dRBeWLMkdudx0tl3+21t1apnReFNQ5nfX29xeSxIhesaMHDZFViO/DXDNW2BcTs6
 | 
			
		||||
vSWKyJ4YIIIzStumD8K1xMsoaZBMDxg4itjWFaKRgNuPiIn4kjDY3kC66Sl/6yTl
 | 
			
		||||
YUz8AybbEsICZzssdZh7jcNb1VRfk79lgAprm/Ktl+mgrU1gaMGP1OE25JCbqli1
 | 
			
		||||
Pbw/BpPynyP9+XulE+2mxFwTYhKAwpDIDKuYsFUXuo8t261pCovI1CXFzAQM2w7H
 | 
			
		||||
DtA2nOXSW6q0jGDJ5+WauH+K8ZSvA6x4sFo4u0KNCx0ROTBpLif6GTngqo3sj+98
 | 
			
		||||
SZiMNLFMQoQkjkdN5Q5g9N6CFZPVZ6QpO0JcIc7S1le/g9z5iBKnifrKxy0TQjtG
 | 
			
		||||
PsDwc8ubPnRm/F82RReCoyNyx63indpgFfhN7+KxUIQ9cOwwTvemmor0A+ZQamRe
 | 
			
		||||
9LMuiEfEaWUDK+6O0Gl8lO571uI5onYdN1VIgOmwFbe+D8TcuzVjIZ/zvHrAGUcC
 | 
			
		||||
AwEAAaNdMFswCwYDVR0PBAQDAgEGMAwGA1UdEwQFMAMBAf8wHQYDVR0OBBYEFF90
 | 
			
		||||
tATATwda6uWx2yKjh0GynOEBMB8GA1UdIwQYMBaAFF90tATATwda6uWx2yKjh0Gy
 | 
			
		||||
nOEBMA0GCSqGSIb3DQEBCwUAA4ICAQCVYaOtqOLIpsrEikE5lb+UARNSFJg6tpkf
 | 
			
		||||
tJ2U8QF/DejemEHx5IClQu6ajxjtu0Aie4/3UnIXop8nH/Q57l+Wyt9T7N2WPiNq
 | 
			
		||||
JSlYKYbJpPF8LXbuKYG3BTFTdOVFIeRe2NUyYh/xs6bXGr4WKTXb3qBmzR02FSy3
 | 
			
		||||
IODQw5Q6zpXj8prYqFHYsOvGCEc1CwJaSaYwRhTkFedJUxiyhyB5GQwoFfExCVHW
 | 
			
		||||
05ZFCAVYFldCJvUzfzrWubN6wX0DD2dwultgmldOn/W/n8at52mpPNvIdbZb2F41
 | 
			
		||||
T0YZeoWnCJrYXjq/32oc1cmifIHqySnyMnavi75DxPCdZsCOpSAT4j4lAQRGsfgI
 | 
			
		||||
kkLPGQieMfNNkMCKh7qjwdXAVtdqhf0RVtFILH3OyEodlk1HYXqX5iE5wlaKzDop
 | 
			
		||||
PKwf2Q3BErq1xChYGGVS+dEvyXc/2nIBlt7uLWKp4XFjqekKbaGaLJdjYP5b2s7N
 | 
			
		||||
1dM0MXQ/f8XoXKBkJNzEiM3hfsU6DOREgMc1DIsFKxfuMwX3EkVQM1If8ghb6x5Y
 | 
			
		||||
jXayv+NLbidOSzk4vl5QwngO/JYFMkoc6i9LNwEaEtR9PhnrdubxmrtM+RjfBm02
 | 
			
		||||
77q3dSWFESFQ4QxYWew4pHE0DpWbWy/iMIKQ6UZ5RLvB8GEcgt8ON7BBJeMc+Dyi
 | 
			
		||||
kT9qhqn+lw==
 | 
			
		||||
-----END CERTIFICATE-----
 | 
			
		||||
 | 
			
		||||
-----BEGIN CERTIFICATE-----
 | 
			
		||||
MIICiDCCAgygAwIBAgIIQX76UsB/30owDAYIKoZIzj0EAwMFADB6MQswCQYDVQQG
 | 
			
		||||
EwJDTjEWMBQGA1UECgwNQW50IEZpbmFuY2lhbDEgMB4GA1UECwwXQ2VydGlmaWNh
 | 
			
		||||
dGlvbiBBdXRob3JpdHkxMTAvBgNVBAMMKEFudCBGaW5hbmNpYWwgQ2VydGlmaWNh
 | 
			
		||||
dGlvbiBBdXRob3JpdHkgRTEwHhcNMTkwNDI4MTYyMDQ0WhcNNDkwNDIwMTYyMDQ0
 | 
			
		||||
WjB6MQswCQYDVQQGEwJDTjEWMBQGA1UECgwNQW50IEZpbmFuY2lhbDEgMB4GA1UE
 | 
			
		||||
CwwXQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxMTAvBgNVBAMMKEFudCBGaW5hbmNp
 | 
			
		||||
YWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgRTEwdjAQBgcqhkjOPQIBBgUrgQQA
 | 
			
		||||
IgNiAASCCRa94QI0vR5Up9Yr9HEupz6hSoyjySYqo7v837KnmjveUIUNiuC9pWAU
 | 
			
		||||
WP3jwLX3HkzeiNdeg22a0IZPoSUCpasufiLAnfXh6NInLiWBrjLJXDSGaY7vaokt
 | 
			
		||||
rpZvAdmjXTBbMAsGA1UdDwQEAwIBBjAMBgNVHRMEBTADAQH/MB0GA1UdDgQWBBRZ
 | 
			
		||||
4ZTgDpksHL2qcpkFkxD2zVd16TAfBgNVHSMEGDAWgBRZ4ZTgDpksHL2qcpkFkxD2
 | 
			
		||||
zVd16TAMBggqhkjOPQQDAwUAA2gAMGUCMQD4IoqT2hTUn0jt7oXLdMJ8q4vLp6sg
 | 
			
		||||
wHfPiOr9gxreb+e6Oidwd2LDnC4OUqCWiF8CMAzwKs4SnDJYcMLf2vpkbuVE4dTH
 | 
			
		||||
Rglz+HGcTLWsFs4KxLsq7MuU+vJTBUeDJeDjdA==
 | 
			
		||||
-----END CERTIFICATE-----
 | 
			
		||||
 | 
			
		||||
-----BEGIN CERTIFICATE-----
 | 
			
		||||
MIIDxTCCAq2gAwIBAgIUEMdk6dVgOEIS2cCP0Q43P90Ps5YwDQYJKoZIhvcNAQEF
 | 
			
		||||
BQAwajELMAkGA1UEBhMCQ04xEzARBgNVBAoMCmlUcnVzQ2hpbmExHDAaBgNVBAsM
 | 
			
		||||
E0NoaW5hIFRydXN0IE5ldHdvcmsxKDAmBgNVBAMMH2lUcnVzQ2hpbmEgQ2xhc3Mg
 | 
			
		||||
MiBSb290IENBIC0gRzMwHhcNMTMwNDE4MDkzNjU2WhcNMzMwNDE4MDkzNjU2WjBq
 | 
			
		||||
MQswCQYDVQQGEwJDTjETMBEGA1UECgwKaVRydXNDaGluYTEcMBoGA1UECwwTQ2hp
 | 
			
		||||
bmEgVHJ1c3QgTmV0d29yazEoMCYGA1UEAwwfaVRydXNDaGluYSBDbGFzcyAyIFJv
 | 
			
		||||
b3QgQ0EgLSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOPPShpV
 | 
			
		||||
nJbMqqCw6Bz1kehnoPst9pkr0V9idOwU2oyS47/HjJXk9Rd5a9xfwkPO88trUpz5
 | 
			
		||||
4GmmwspDXjVFu9L0eFaRuH3KMha1Ak01citbF7cQLJlS7XI+tpkTGHEY5pt3EsQg
 | 
			
		||||
wykfZl/A1jrnSkspMS997r2Gim54cwz+mTMgDRhZsKK/lbOeBPpWtcFizjXYCqhw
 | 
			
		||||
WktvQfZBYi6o4sHCshnOswi4yV1p+LuFcQ2ciYdWvULh1eZhLxHbGXyznYHi0dGN
 | 
			
		||||
z+I9H8aXxqAQfHVhbdHNzi77hCxFjOy+hHrGsyzjrd2swVQ2iUWP8BfEQqGLqM1g
 | 
			
		||||
KgWKYfcTGdbPB1MCAwEAAaNjMGEwHQYDVR0OBBYEFG/oAMxTVe7y0+408CTAK8hA
 | 
			
		||||
uTyRMB8GA1UdIwQYMBaAFG/oAMxTVe7y0+408CTAK8hAuTyRMA8GA1UdEwEB/wQF
 | 
			
		||||
MAMBAf8wDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQBLnUTfW7hp
 | 
			
		||||
emMbuUGCk7RBswzOT83bDM6824EkUnf+X0iKS95SUNGeeSWK2o/3ALJo5hi7GZr3
 | 
			
		||||
U8eLaWAcYizfO99UXMRBPw5PRR+gXGEronGUugLpxsjuynoLQu8GQAeysSXKbN1I
 | 
			
		||||
UugDo9u8igJORYA+5ms0s5sCUySqbQ2R5z/GoceyI9LdxIVa1RjVX8pYOj8JFwtn
 | 
			
		||||
DJN3ftSFvNMYwRuILKuqUYSHc2GPYiHVflDh5nDymCMOQFcFG3WsEuB+EYQPFgIU
 | 
			
		||||
1DHmdZcz7Llx8UOZXX2JupWCYzK1XhJb+r4hK5ncf/w8qGtYlmyJpxk3hr1TfUJX
 | 
			
		||||
Yf4Zr0fJsGuv
 | 
			
		||||
-----END CERTIFICATE-----
 | 
			
		||||
							
								
								
									
										19
									
								
								api/res/certs/alipay/appPublicCert.crt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								api/res/certs/alipay/appPublicCert.crt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
-----BEGIN CERTIFICATE-----
 | 
			
		||||
MIIDmTCCAoGgAwIBAgIQICMRB2LW76yahgdg3IFNPDANBgkqhkiG9w0BAQsFADCBkTELMAkGA1UE
 | 
			
		||||
BhMCQ04xGzAZBgNVBAoMEkFudCBGaW5hbmNpYWwgdGVzdDElMCMGA1UECwwcQ2VydGlmaWNhdGlv
 | 
			
		||||
biBBdXRob3JpdHkgdGVzdDE+MDwGA1UEAww1QW50IEZpbmFuY2lhbCBDZXJ0aWZpY2F0aW9uIEF1
 | 
			
		||||
dGhvcml0eSBDbGFzcyAyIFIxIHRlc3QwHhcNMjMxMTA3MDU0NjE5WhcNMjQxMTExMDU0NjE5WjBr
 | 
			
		||||
MQswCQYDVQQGEwJDTjEfMB0GA1UECgwWbWJvbmZ5OTAxNUBzYW5kYm94LmNvbTEPMA0GA1UECwwG
 | 
			
		||||
QWxpcGF5MSowKAYDVQQDDCEyMDg4NzIxMDIwNzUwNTgxLTkwMjEwMDAxMzE2NTgwMjMwggEiMA0G
 | 
			
		||||
CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCxihQPf1Q+g9ArgM46shVqL5sbRha/df95D1PsWyEq
 | 
			
		||||
ANmWmG4zZ+ksYDVQrc4KzhSRoi56sm/7TDFYTmM6bW99e/nKW58WxyZB4ie5qA3F4n17psPyDqb8
 | 
			
		||||
IokcQmCphSFDaXQD6AoXoLNtTM0vAI2cWxAgebZ/vsrdj5Ntjt+Rp3NYMCk1i5xovHcfILzLEGbX
 | 
			
		||||
QXoT9fo5AhHotTWa6xHVLPUGY9qwLzQxHzBmvy5ZMfnOfJkm/mDisTSqAUB59F3dzU/1ARVkEZ1w
 | 
			
		||||
Mgb4XohWBw6iurQfbMnH2mIomAAwwZVFv+sXDbL9yMbSMo/SjVsTQprn0Q0EnwLo7nmmOM6HAgMB
 | 
			
		||||
AAGjEjAQMA4GA1UdDwEB/wQEAwIE8DANBgkqhkiG9w0BAQsFAAOCAQEAn3Y4/C1h9R6ONsBqX3/q
 | 
			
		||||
XfHX7yX1FM0Y1x48X3/Yxk6HivAkTukhhhVYVKJsbrbzRqHDp9vhAP/FR6o6pAevaYMmLov0VMXU
 | 
			
		||||
7oAuetgkaYEYkDuNen5/Hpdhqi2vTtdT+q9w8zHJd6MDQ0aoHgIxpLKw5vof2R1N4fwSgNXMiXE5
 | 
			
		||||
kmllKQMem/+on2p+Sj80/2asxryHIGlH87qPzkffv+kIOkZthbTApTFLLjdVri2QHGe8/cc4xy01
 | 
			
		||||
/9iR3IUzNahotT41lJ4bMevBY7XMAS3n5ekyABN/9ZRJqhWdXgmFCRN/u56qd6lDgu7R2M2QUoyc
 | 
			
		||||
LuW5DfgRItKlmUB7sw==
 | 
			
		||||
-----END CERTIFICATE-----
 | 
			
		||||
							
								
								
									
										1
									
								
								api/res/certs/alipay/privateKey.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								api/res/certs/alipay/privateKey.txt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
MIIEpQIBAAKCAQEAsYoUD39UPoPQK4DOOrIVai+bG0YWv3X/eQ9T7FshKgDZlphuM2fpLGA1UK3OCs4UkaIuerJv+0wxWE5jOm1vfXv5ylufFscmQeInuagNxeJ9e6bD8g6m/CKJHEJgqYUhQ2l0A+gKF6CzbUzNLwCNnFsQIHm2f77K3Y+TbY7fkadzWDApNYucaLx3HyC8yxBm10F6E/X6OQIR6LU1musR1Sz1BmPasC80MR8wZr8uWTH5znyZJv5g4rE0qgFAefRd3c1P9QEVZBGdcDIG+F6IVgcOorq0H2zJx9piKJgAMMGVRb/rFw2y/cjG0jKP0o1bE0Ka59ENBJ8C6O55pjjOhwIDAQABAoIBAFetNfz1R7hbxjlFshMAkVzQR8wvT9qbvl+dtzdZRcaFhu89NecDIP7+QDYor0FcxoGpU0TazDyRQyk2BQD8vHt+9zv9BVLtZLJSqoWgPbUFBi1DjS8EF2ka8RVYnn35NhUhhd7L//ftL88Bh673mfembQ9srDjoEy1Z01feoABAnCMkNFl986DmEwnarvEufXSDIgeN4ioMxha4NvfIPuI0zpVdV1O9sv+SGC+VEWZBtN3GNsaf4zS/f8FVGvTiU/Abz0gSw/iwSPHclDWQDTN3yFHf/tfqlzh0mH0WfhnuOBFWXzK+R7fbnM+asI9ttvzRcfpzgRGXdPcNcOv/6cECgYEA3DVqpi1k8MYfJixju6SG5gfyhM4VFksFmCMaNPgtatDMBKLMTgV/Ej6LXREojcy29uZl83F09pVlpd41eG39ULIPktixA/BqErQ2UaWh6kOxifycpu22Jh0r09hax6UgVrcBrrnCJEjcFsuJlrZvXQSzc3PBxjWy5gjabS5h9iECgYEAzmVAIh2frF01Y95zsLueAhhZwCtPanm6kf7ivR4r1plIX3b2sNRhWGmEHFgaCE6Braa0ogQ73Hd26kw4ZW+D6QMGC/zjCBEzDLLf++SjdVUHiY5AR4WHqXzq1jdAlsVyo9R661oAOp3lhiJVGLNXkHyEfEVPHsaxJh4osYSbX6cCgYEAx32Qx0i6eDFTyLZQB46uMrgiaVN04QRH5iJuvGvUYT8UhGKjaU8rZfDJOh+wOH2rhxMEaz1uc3C2bERY9mfWI4Ob/jFWc7YZsiYWS3Mcsuhubw4tMECLUg39RWZsHw8ls8kIuixIh6yFzhTH6YQOcRswIrhMZG8DScfdcSmiz2ECgYEAkWP1t5KSpkLKl11etcKUXfl1T8+yk9jIOowIgRw92WAFAWq2AH67TCKYM7dEL1HOO9tRJ0hAOt/U3ttuZtYVYBEHM26jJ02mXm2rJrA7DS4mrxmL4lYH6LbcXqZxU0Qnq4zEQgIWYzRTORf6Rfof1uJAGaJhR9bDd4yLMfGt2cUCgYEAo216Y61xOHUTA4AF1eekk+r+uOcQgQDvLXfs9FkDdJLk0mPG48/+eIYpPFnANJ/riF/DWOp8WGEe2IzA9yUFexzDbNQK8ha9kGcxaSAyiCwzjZ/t9/+hScDSV8kNqWSRSisu/YOFleEHbokT6mbLZ+gdqES8mUUanaEBzRQYGxo=
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								api/res/img/wechat-pay.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								api/res/img/wechat-pay.jpg
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 5.7 KiB  | 
@@ -1,7 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
  "data": [
 | 
			
		||||
    "task(m1wpaa4v60zedj8)",
 | 
			
		||||
    "a cute cat",
 | 
			
		||||
    "task(cxvkpawy8onnfti)",
 | 
			
		||||
    "a  cute girl",
 | 
			
		||||
    "",
 | 
			
		||||
    [],
 | 
			
		||||
    20,
 | 
			
		||||
@@ -10,12 +10,12 @@
 | 
			
		||||
    1,
 | 
			
		||||
    7,
 | 
			
		||||
    512,
 | 
			
		||||
    384,
 | 
			
		||||
    true,
 | 
			
		||||
    512,
 | 
			
		||||
    false,
 | 
			
		||||
    0.7,
 | 
			
		||||
    2,
 | 
			
		||||
    "ESRGAN_4x",
 | 
			
		||||
    10,
 | 
			
		||||
    "Latent",
 | 
			
		||||
    0,
 | 
			
		||||
    0,
 | 
			
		||||
    0,
 | 
			
		||||
    "Use same checkpoint",
 | 
			
		||||
@@ -33,6 +33,10 @@
 | 
			
		||||
    0,
 | 
			
		||||
    0,
 | 
			
		||||
    0,
 | 
			
		||||
    null,
 | 
			
		||||
    null,
 | 
			
		||||
    null,
 | 
			
		||||
    null,
 | 
			
		||||
    false,
 | 
			
		||||
    false,
 | 
			
		||||
    "positive",
 | 
			
		||||
@@ -55,13 +59,22 @@
 | 
			
		||||
    false,
 | 
			
		||||
    false,
 | 
			
		||||
    0,
 | 
			
		||||
    null,
 | 
			
		||||
    null,
 | 
			
		||||
    false,
 | 
			
		||||
    null,
 | 
			
		||||
    null,
 | 
			
		||||
    false,
 | 
			
		||||
    null,
 | 
			
		||||
    null,
 | 
			
		||||
    false,
 | 
			
		||||
    50,
 | 
			
		||||
    [],
 | 
			
		||||
    "",
 | 
			
		||||
    "",
 | 
			
		||||
    ""
 | 
			
		||||
  ],
 | 
			
		||||
  "event_data": null,
 | 
			
		||||
  "fn_index": 96,
 | 
			
		||||
  "session_hash": "kmb0ojjfhdj"
 | 
			
		||||
  "fn_index": 446,
 | 
			
		||||
  "session_hash": "nk5noh1rz1o"
 | 
			
		||||
}
 | 
			
		||||
@@ -1,42 +0,0 @@
 | 
			
		||||
package fun
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/service/mj"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// AI 绘画函数
 | 
			
		||||
 | 
			
		||||
type FuncMidJourney struct {
 | 
			
		||||
	name    string
 | 
			
		||||
	service *mj.Service
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewMidJourneyFunc(mjService *mj.Service) 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(types.MjTask{
 | 
			
		||||
		SessionId: utils.InterfaceToString(params["session_id"]),
 | 
			
		||||
		Src:       types.TaskSrcChat,
 | 
			
		||||
		Type:      types.TaskImage,
 | 
			
		||||
		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{}
 | 
			
		||||
@@ -1,39 +0,0 @@
 | 
			
		||||
package fun
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	logger2 "chatplus/logger"
 | 
			
		||||
	"chatplus/service/mj"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Function interface {
 | 
			
		||||
	Invoke(map[string]interface{}) (string, error)
 | 
			
		||||
	Name() string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var logger = logger2.GetLogger()
 | 
			
		||||
 | 
			
		||||
type resVo struct {
 | 
			
		||||
	Code    types.BizCode `json:"code"`
 | 
			
		||||
	Message string        `json:"message"`
 | 
			
		||||
	Data    struct {
 | 
			
		||||
		Title     string     `json:"title"`
 | 
			
		||||
		UpdatedAt string     `json:"updated_at"`
 | 
			
		||||
		Items     []dataItem `json:"items"`
 | 
			
		||||
	} `json:"data"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type dataItem struct {
 | 
			
		||||
	Title  string `json:"title"`
 | 
			
		||||
	Url    string `json:"url"`
 | 
			
		||||
	Remark string `json:"remark"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewFunctions(config *types.AppConfig, mjService *mj.Service) map[string]Function {
 | 
			
		||||
	return map[string]Function{
 | 
			
		||||
		types.FuncZaoBao:     NewZaoBao(config.ApiConfig),
 | 
			
		||||
		types.FuncWeibo:      NewWeiboHot(config.ApiConfig),
 | 
			
		||||
		types.FuncHeadLine:   NewHeadLines(config.ApiConfig),
 | 
			
		||||
		types.FuncMidJourney: NewMidJourneyFunc(mjService),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,58 +0,0 @@
 | 
			
		||||
package fun
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/imroc/req/v3"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 今日头条函数实现
 | 
			
		||||
 | 
			
		||||
type FuncHeadlines struct {
 | 
			
		||||
	name   string
 | 
			
		||||
	config types.ChatPlusApiConfig
 | 
			
		||||
	client *req.Client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewHeadLines(config types.ChatPlusApiConfig) FuncHeadlines {
 | 
			
		||||
	return FuncHeadlines{
 | 
			
		||||
		name:   "今日头条",
 | 
			
		||||
		config: config,
 | 
			
		||||
		client: req.C().SetTimeout(10 * time.Second)}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f FuncHeadlines) Invoke(map[string]interface{}) (string, error) {
 | 
			
		||||
	if f.config.Token == "" {
 | 
			
		||||
		return "", errors.New("无效的 API Token")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	url := fmt.Sprintf("%s/api/headline/fetch", f.config.ApiURL)
 | 
			
		||||
	var res resVo
 | 
			
		||||
	r, err := f.client.R().
 | 
			
		||||
		SetHeader("AppId", f.config.AppId).
 | 
			
		||||
		SetHeader("Authorization", fmt.Sprintf("Bearer %s", f.config.Token)).
 | 
			
		||||
		SetSuccessResult(&res).Get(url)
 | 
			
		||||
	if err != nil || r.IsErrorState() {
 | 
			
		||||
		return "", fmt.Errorf("%v%v", err, r.Err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if res.Code != types.Success {
 | 
			
		||||
		return "", errors.New(res.Message)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	builder := make([]string, 0)
 | 
			
		||||
	builder = append(builder, fmt.Sprintf("**%s**,最新更新:%s", res.Data.Title, res.Data.UpdatedAt))
 | 
			
		||||
	for i, v := range res.Data.Items {
 | 
			
		||||
		builder = append(builder, fmt.Sprintf("%d、 [%s](%s) [%s]", i+1, v.Title, v.Url, v.Remark))
 | 
			
		||||
	}
 | 
			
		||||
	return strings.Join(builder, "\n\n"), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f FuncHeadlines) Name() string {
 | 
			
		||||
	return f.name
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ Function = &FuncHeadlines{}
 | 
			
		||||
@@ -1,58 +0,0 @@
 | 
			
		||||
package fun
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/imroc/req/v3"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 微博热搜函数实现
 | 
			
		||||
 | 
			
		||||
type FuncWeiboHot struct {
 | 
			
		||||
	name   string
 | 
			
		||||
	config types.ChatPlusApiConfig
 | 
			
		||||
	client *req.Client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewWeiboHot(config types.ChatPlusApiConfig) FuncWeiboHot {
 | 
			
		||||
	return FuncWeiboHot{
 | 
			
		||||
		name:   "微博热搜",
 | 
			
		||||
		config: config,
 | 
			
		||||
		client: req.C().SetTimeout(10 * time.Second)}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f FuncWeiboHot) Invoke(map[string]interface{}) (string, error) {
 | 
			
		||||
	if f.config.Token == "" {
 | 
			
		||||
		return "", errors.New("无效的 API Token")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	url := fmt.Sprintf("%s/api/weibo/fetch", f.config.ApiURL)
 | 
			
		||||
	var res resVo
 | 
			
		||||
	r, err := f.client.R().
 | 
			
		||||
		SetHeader("AppId", f.config.AppId).
 | 
			
		||||
		SetHeader("Authorization", fmt.Sprintf("Bearer %s", f.config.Token)).
 | 
			
		||||
		SetSuccessResult(&res).Get(url)
 | 
			
		||||
	if err != nil || r.IsErrorState() {
 | 
			
		||||
		return "", fmt.Errorf("%v%v", err, r.Err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if res.Code != types.Success {
 | 
			
		||||
		return "", errors.New(res.Message)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	builder := make([]string, 0)
 | 
			
		||||
	builder = append(builder, fmt.Sprintf("**%s**,最新更新:%s", res.Data.Title, res.Data.UpdatedAt))
 | 
			
		||||
	for i, v := range res.Data.Items {
 | 
			
		||||
		builder = append(builder, fmt.Sprintf("%d、 [%s](%s) [热度:%s]", i+1, v.Title, v.Url, v.Remark))
 | 
			
		||||
	}
 | 
			
		||||
	return strings.Join(builder, "\n\n"), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f FuncWeiboHot) Name() string {
 | 
			
		||||
	return f.name
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ Function = &FuncWeiboHot{}
 | 
			
		||||
@@ -1,59 +0,0 @@
 | 
			
		||||
package fun
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/imroc/req/v3"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 每日早报函数实现
 | 
			
		||||
 | 
			
		||||
type FuncZaoBao struct {
 | 
			
		||||
	name   string
 | 
			
		||||
	config types.ChatPlusApiConfig
 | 
			
		||||
	client *req.Client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewZaoBao(config types.ChatPlusApiConfig) FuncZaoBao {
 | 
			
		||||
	return FuncZaoBao{
 | 
			
		||||
		name:   "每日早报",
 | 
			
		||||
		config: config,
 | 
			
		||||
		client: req.C().SetTimeout(10 * time.Second)}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f FuncZaoBao) Invoke(map[string]interface{}) (string, error) {
 | 
			
		||||
	if f.config.Token == "" {
 | 
			
		||||
		return "", errors.New("无效的 API Token")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	url := fmt.Sprintf("%s/api/zaobao/fetch", f.config.ApiURL)
 | 
			
		||||
	var res resVo
 | 
			
		||||
	r, err := f.client.R().
 | 
			
		||||
		SetHeader("AppId", f.config.AppId).
 | 
			
		||||
		SetHeader("Authorization", fmt.Sprintf("Bearer %s", f.config.Token)).
 | 
			
		||||
		SetSuccessResult(&res).Get(url)
 | 
			
		||||
	if err != nil || r.IsErrorState() {
 | 
			
		||||
		return "", fmt.Errorf("%v%v", err, r.Err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if res.Code != types.Success {
 | 
			
		||||
		return "", errors.New(res.Message)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	builder := make([]string, 0)
 | 
			
		||||
	builder = append(builder, fmt.Sprintf("**%s 早报:**", res.Data.UpdatedAt))
 | 
			
		||||
	for _, v := range res.Data.Items {
 | 
			
		||||
		builder = append(builder, v.Title)
 | 
			
		||||
	}
 | 
			
		||||
	builder = append(builder, fmt.Sprintf("%s", res.Data.Title))
 | 
			
		||||
	return strings.Join(builder, "\n\n"), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f FuncZaoBao) Name() string {
 | 
			
		||||
	return f.name
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ Function = &FuncZaoBao{}
 | 
			
		||||
@@ -4,7 +4,7 @@ import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	logger2 "chatplus/logger"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"github.com/bwmarrin/discordgo"
 | 
			
		||||
	discordgo "github.com/bg5t/mydiscordgo"
 | 
			
		||||
	"github.com/gorilla/websocket"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
@@ -17,32 +17,49 @@ import (
 | 
			
		||||
var logger = logger2.GetLogger()
 | 
			
		||||
 | 
			
		||||
type Bot struct {
 | 
			
		||||
	config  *types.MidJourneyConfig
 | 
			
		||||
	config  types.MidJourneyConfig
 | 
			
		||||
	bot     *discordgo.Session
 | 
			
		||||
	name    string
 | 
			
		||||
	service *Service
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewBot(config *types.AppConfig, service *Service) (*Bot, error) {
 | 
			
		||||
	discord, err := discordgo.New("Bot " + config.MjConfig.BotToken)
 | 
			
		||||
func NewBot(name string, proxy string, config types.MidJourneyConfig, service *Service) (*Bot, error) {
 | 
			
		||||
	bot, err := discordgo.New("Bot " + config.BotToken)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Error(err)
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if config.ProxyURL != "" {
 | 
			
		||||
		proxy, _ := url.Parse(config.ProxyURL)
 | 
			
		||||
		discord.Client = &http.Client{
 | 
			
		||||
			Transport: &http.Transport{
 | 
			
		||||
	// use CDN reverse proxy
 | 
			
		||||
	if config.UseCDN {
 | 
			
		||||
		discordgo.SetEndpointDiscord(config.DiscordAPI)
 | 
			
		||||
		discordgo.SetEndpointCDN("https://cdn.discordapp.com")
 | 
			
		||||
		discordgo.SetEndpointStatus(config.DiscordAPI + "/api/v2/")
 | 
			
		||||
		bot.MjGateway = config.DiscordGateway + "/"
 | 
			
		||||
	} else { // use proxy
 | 
			
		||||
		discordgo.SetEndpointDiscord("https://discord.com")
 | 
			
		||||
		discordgo.SetEndpointCDN("https://cdn.discordapp.com")
 | 
			
		||||
		discordgo.SetEndpointStatus("https://discord.com/api/v2/")
 | 
			
		||||
		bot.MjGateway = "wss://gateway.discord.gg"
 | 
			
		||||
 | 
			
		||||
		if proxy != "" {
 | 
			
		||||
			proxy, _ := url.Parse(proxy)
 | 
			
		||||
			bot.Client = &http.Client{
 | 
			
		||||
				Transport: &http.Transport{
 | 
			
		||||
					Proxy: http.ProxyURL(proxy),
 | 
			
		||||
				},
 | 
			
		||||
			}
 | 
			
		||||
			bot.Dialer = &websocket.Dialer{
 | 
			
		||||
				Proxy: http.ProxyURL(proxy),
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
		discord.Dialer = &websocket.Dialer{
 | 
			
		||||
			Proxy: http.ProxyURL(proxy),
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &Bot{
 | 
			
		||||
		config:  &config.MjConfig,
 | 
			
		||||
		bot:     discord,
 | 
			
		||||
		config:  config,
 | 
			
		||||
		bot:     bot,
 | 
			
		||||
		name:    name,
 | 
			
		||||
		service: service,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
@@ -52,13 +69,13 @@ func (b *Bot) Run() error {
 | 
			
		||||
	b.bot.AddHandler(b.messageCreate)
 | 
			
		||||
	b.bot.AddHandler(b.messageUpdate)
 | 
			
		||||
 | 
			
		||||
	logger.Info("Starting MidJourney Bot...")
 | 
			
		||||
	logger.Infof("Starting MidJourney %s", b.name)
 | 
			
		||||
	err := b.bot.Open()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Error("Error opening Discord connection:", err)
 | 
			
		||||
		logger.Errorf("Error opening Discord connection for %s, error: %v", b.name, err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	logger.Info("Starting MidJourney Bot successfully!")
 | 
			
		||||
	logger.Infof("Starting MidJourney %s successfully!", b.name)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -87,7 +104,7 @@ func (b *Bot) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// ignore messages for self
 | 
			
		||||
	if m.Author.ID == s.State.User.ID {
 | 
			
		||||
	if m.Author == nil || m.Author.ID == s.State.User.ID {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -99,6 +116,7 @@ func (b *Bot) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
 | 
			
		||||
	if strings.Contains(m.Content, "(Waiting to start)") && !strings.Contains(m.Content, "Rerolling **") {
 | 
			
		||||
		// parse content
 | 
			
		||||
		req := CBReq{
 | 
			
		||||
			ChannelId:   m.ChannelID,
 | 
			
		||||
			MessageId:   m.ID,
 | 
			
		||||
			ReferenceId: referenceId,
 | 
			
		||||
			Prompt:      extractPrompt(m.Content),
 | 
			
		||||
@@ -109,7 +127,7 @@ func (b *Bot) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.addAttachment(m.ID, referenceId, m.Content, m.Attachments)
 | 
			
		||||
	b.addAttachment(m.ChannelID, m.ID, referenceId, m.Content, m.Attachments)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bot) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdate) {
 | 
			
		||||
@@ -118,7 +136,7 @@ func (b *Bot) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdate) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// ignore messages for self
 | 
			
		||||
	if m.Author.ID == s.State.User.ID {
 | 
			
		||||
	if m.Author == nil || m.Author.ID == s.State.User.ID {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -130,6 +148,7 @@ func (b *Bot) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdate) {
 | 
			
		||||
	}
 | 
			
		||||
	if strings.Contains(m.Content, "(Stopped)") {
 | 
			
		||||
		req := CBReq{
 | 
			
		||||
			ChannelId:   m.ChannelID,
 | 
			
		||||
			MessageId:   m.ID,
 | 
			
		||||
			ReferenceId: referenceId,
 | 
			
		||||
			Prompt:      extractPrompt(m.Content),
 | 
			
		||||
@@ -140,11 +159,11 @@ func (b *Bot) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdate) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b.addAttachment(m.ID, referenceId, m.Content, m.Attachments)
 | 
			
		||||
	b.addAttachment(m.ChannelID, m.ID, referenceId, m.Content, m.Attachments)
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (b *Bot) addAttachment(messageId string, referenceId string, content string, attachments []*discordgo.MessageAttachment) {
 | 
			
		||||
func (b *Bot) addAttachment(channelId string, messageId string, referenceId string, content string, attachments []*discordgo.MessageAttachment) {
 | 
			
		||||
	progress := extractProgress(content)
 | 
			
		||||
	var status TaskStatus
 | 
			
		||||
	if progress == 100 {
 | 
			
		||||
@@ -166,6 +185,7 @@ func (b *Bot) addAttachment(messageId string, referenceId string, content string
 | 
			
		||||
			Hash:     extractHashFromFilename(attachment.Filename),
 | 
			
		||||
		}
 | 
			
		||||
		req := CBReq{
 | 
			
		||||
			ChannelId:   channelId,
 | 
			
		||||
			MessageId:   messageId,
 | 
			
		||||
			ReferenceId: referenceId,
 | 
			
		||||
			Image:       image,
 | 
			
		||||
 
 | 
			
		||||
@@ -3,32 +3,42 @@ package mj
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/imroc/req/v3"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/imroc/req/v3"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// MidJourney client
 | 
			
		||||
 | 
			
		||||
type Client struct {
 | 
			
		||||
	client *req.Client
 | 
			
		||||
	config *types.MidJourneyConfig
 | 
			
		||||
	client    *req.Client
 | 
			
		||||
	Config    types.MidJourneyConfig
 | 
			
		||||
	imgCdnURL string
 | 
			
		||||
	apiURL    string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewClient(config *types.AppConfig) *Client {
 | 
			
		||||
func NewClient(config types.MidJourneyConfig, proxy string, imgCdnURL string) *Client {
 | 
			
		||||
	client := req.C().SetTimeout(10 * time.Second)
 | 
			
		||||
	var apiURL string
 | 
			
		||||
	// set proxy URL
 | 
			
		||||
	if config.ProxyURL != "" {
 | 
			
		||||
		client.SetProxyURL(config.ProxyURL)
 | 
			
		||||
	if config.UseCDN {
 | 
			
		||||
		apiURL = config.DiscordAPI + "/api/v9/interactions"
 | 
			
		||||
	} else {
 | 
			
		||||
		apiURL = "https://discord.com/api/v9/interactions"
 | 
			
		||||
		if proxy != "" {
 | 
			
		||||
			client.SetProxyURL(proxy)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return &Client{client: client, config: &config.MjConfig}
 | 
			
		||||
 | 
			
		||||
	return &Client{client: client, Config: config, apiURL: apiURL, imgCdnURL: imgCdnURL}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Client) Imagine(prompt string) error {
 | 
			
		||||
	interactionsReq := &InteractionsRequest{
 | 
			
		||||
		Type:          2,
 | 
			
		||||
		ApplicationID: ApplicationID,
 | 
			
		||||
		GuildID:       c.config.GuildId,
 | 
			
		||||
		ChannelID:     c.config.ChanelId,
 | 
			
		||||
		GuildID:       c.Config.GuildId,
 | 
			
		||||
		ChannelID:     c.Config.ChanelId,
 | 
			
		||||
		SessionID:     SessionID,
 | 
			
		||||
		Data: map[string]any{
 | 
			
		||||
			"version": "1166847114203123795",
 | 
			
		||||
@@ -66,11 +76,10 @@ func (c *Client) Imagine(prompt string) error {
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	url := "https://discord.com/api/v9/interactions"
 | 
			
		||||
	r, err := c.client.R().SetHeader("Authorization", c.config.UserToken).
 | 
			
		||||
	r, err := c.client.R().SetHeader("Authorization", c.Config.UserToken).
 | 
			
		||||
		SetHeader("Content-Type", "application/json").
 | 
			
		||||
		SetBody(interactionsReq).
 | 
			
		||||
		Post(url)
 | 
			
		||||
		Post(c.apiURL)
 | 
			
		||||
 | 
			
		||||
	if err != nil || r.IsErrorState() {
 | 
			
		||||
		return fmt.Errorf("error with http request: %w%v", err, r.Err)
 | 
			
		||||
@@ -85,8 +94,8 @@ func (c *Client) Upscale(index int, messageId string, hash string) error {
 | 
			
		||||
	interactionsReq := &InteractionsRequest{
 | 
			
		||||
		Type:          3,
 | 
			
		||||
		ApplicationID: ApplicationID,
 | 
			
		||||
		GuildID:       c.config.GuildId,
 | 
			
		||||
		ChannelID:     c.config.ChanelId,
 | 
			
		||||
		GuildID:       c.Config.GuildId,
 | 
			
		||||
		ChannelID:     c.Config.ChanelId,
 | 
			
		||||
		MessageFlags:  &flags,
 | 
			
		||||
		MessageID:     &messageId,
 | 
			
		||||
		SessionID:     SessionID,
 | 
			
		||||
@@ -97,13 +106,12 @@ func (c *Client) Upscale(index int, messageId string, hash string) error {
 | 
			
		||||
		Nonce: fmt.Sprintf("%d", time.Now().UnixNano()),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	url := "https://discord.com/api/v9/interactions"
 | 
			
		||||
	var res InteractionsResult
 | 
			
		||||
	r, err := c.client.R().SetHeader("Authorization", c.config.UserToken).
 | 
			
		||||
	r, err := c.client.R().SetHeader("Authorization", c.Config.UserToken).
 | 
			
		||||
		SetHeader("Content-Type", "application/json").
 | 
			
		||||
		SetBody(interactionsReq).
 | 
			
		||||
		SetErrorResult(&res).
 | 
			
		||||
		Post(url)
 | 
			
		||||
		Post(c.apiURL)
 | 
			
		||||
	if err != nil || r.IsErrorState() {
 | 
			
		||||
		return fmt.Errorf("error with http request: %v%v%v", err, r.Err, res.Message)
 | 
			
		||||
	}
 | 
			
		||||
@@ -117,8 +125,8 @@ func (c *Client) Variation(index int, messageId string, hash string) error {
 | 
			
		||||
	interactionsReq := &InteractionsRequest{
 | 
			
		||||
		Type:          3,
 | 
			
		||||
		ApplicationID: ApplicationID,
 | 
			
		||||
		GuildID:       c.config.GuildId,
 | 
			
		||||
		ChannelID:     c.config.ChanelId,
 | 
			
		||||
		GuildID:       c.Config.GuildId,
 | 
			
		||||
		ChannelID:     c.Config.ChanelId,
 | 
			
		||||
		MessageFlags:  &flags,
 | 
			
		||||
		MessageID:     &messageId,
 | 
			
		||||
		SessionID:     SessionID,
 | 
			
		||||
@@ -129,13 +137,12 @@ func (c *Client) Variation(index int, messageId string, hash string) error {
 | 
			
		||||
		Nonce: fmt.Sprintf("%d", time.Now().UnixNano()),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	url := "https://discord.com/api/v9/interactions"
 | 
			
		||||
	var res InteractionsResult
 | 
			
		||||
	r, err := c.client.R().SetHeader("Authorization", c.config.UserToken).
 | 
			
		||||
	r, err := c.client.R().SetHeader("Authorization", c.Config.UserToken).
 | 
			
		||||
		SetHeader("Content-Type", "application/json").
 | 
			
		||||
		SetBody(interactionsReq).
 | 
			
		||||
		SetErrorResult(&res).
 | 
			
		||||
		Post(url)
 | 
			
		||||
		Post(c.apiURL)
 | 
			
		||||
	if err != nil || r.IsErrorState() {
 | 
			
		||||
		return fmt.Errorf("error with http request: %v%v%v", err, r.Err, res.Message)
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										174
									
								
								api/service/mj/plus/client.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								api/service/mj/plus/client.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,174 @@
 | 
			
		||||
package plus
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	logger2 "chatplus/logger"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
 | 
			
		||||
	"github.com/imroc/req/v3"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var logger = logger2.GetLogger()
 | 
			
		||||
 | 
			
		||||
// Client MidJourney Plus Client
 | 
			
		||||
type Client struct {
 | 
			
		||||
	Config types.MidJourneyPlusConfig
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewClient(config types.MidJourneyPlusConfig) *Client {
 | 
			
		||||
	return &Client{Config: config}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ImageReq struct {
 | 
			
		||||
	BotType       string        `json:"botType"`
 | 
			
		||||
	Prompt        string        `json:"prompt"`
 | 
			
		||||
	Base64Array   []interface{} `json:"base64Array,omitempty"`
 | 
			
		||||
	AccountFilter struct {
 | 
			
		||||
		InstanceId          string        `json:"instanceId"`
 | 
			
		||||
		Modes               []interface{} `json:"modes"`
 | 
			
		||||
		Remix               bool          `json:"remix"`
 | 
			
		||||
		RemixAutoConsidered bool          `json:"remixAutoConsidered"`
 | 
			
		||||
	} `json:"accountFilter,omitempty"`
 | 
			
		||||
	NotifyHook string `json:"notifyHook"`
 | 
			
		||||
	State      string `json:"state,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ImageRes struct {
 | 
			
		||||
	Code        int    `json:"code"`
 | 
			
		||||
	Description string `json:"description"`
 | 
			
		||||
	Properties  struct {
 | 
			
		||||
	} `json:"properties"`
 | 
			
		||||
	Result string `json:"result"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ErrRes struct {
 | 
			
		||||
	Error struct {
 | 
			
		||||
		Message string `json:"message"`
 | 
			
		||||
	} `json:"error"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Client) Imagine(prompt string) (ImageRes, error) {
 | 
			
		||||
	apiURL := fmt.Sprintf("%s/mj-fast/mj/submit/imagine", c.Config.ApiURL)
 | 
			
		||||
	body := ImageReq{
 | 
			
		||||
		BotType:    "MID_JOURNEY",
 | 
			
		||||
		Prompt:     prompt,
 | 
			
		||||
		NotifyHook: c.Config.NotifyURL,
 | 
			
		||||
	}
 | 
			
		||||
	var res ImageRes
 | 
			
		||||
	var errRes ErrRes
 | 
			
		||||
	r, err := req.C().R().
 | 
			
		||||
		SetHeader("Authorization", "Bearer "+c.Config.ApiKey).
 | 
			
		||||
		SetBody(body).
 | 
			
		||||
		SetSuccessResult(&res).
 | 
			
		||||
		SetErrorResult(&errRes).
 | 
			
		||||
		Post(apiURL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		errStr, _ := io.ReadAll(r.Body)
 | 
			
		||||
		return ImageRes{}, fmt.Errorf("请求 API 出错:%v,%v", err, string(errStr))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if r.IsErrorState() {
 | 
			
		||||
		return ImageRes{}, fmt.Errorf("API 返回错误:%s", errRes.Error.Message)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return res, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Upscale 放大指定的图片
 | 
			
		||||
func (c *Client) Upscale(index int, messageId string, hash string) (ImageRes, error) {
 | 
			
		||||
	body := map[string]string{
 | 
			
		||||
		"customId":   fmt.Sprintf("MJ::JOB::upsample::%d::%s", index, hash),
 | 
			
		||||
		"taskId":     messageId,
 | 
			
		||||
		"notifyHook": c.Config.NotifyURL,
 | 
			
		||||
	}
 | 
			
		||||
	apiURL := fmt.Sprintf("%s/mj/submit/action", c.Config.ApiURL)
 | 
			
		||||
	var res ImageRes
 | 
			
		||||
	var errRes ErrRes
 | 
			
		||||
	r, err := req.C().R().
 | 
			
		||||
		SetHeader("Authorization", "Bearer "+c.Config.ApiKey).
 | 
			
		||||
		SetBody(body).
 | 
			
		||||
		SetSuccessResult(&res).
 | 
			
		||||
		SetErrorResult(&errRes).
 | 
			
		||||
		Post(apiURL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return ImageRes{}, fmt.Errorf("请求 API 出错:%v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if r.IsErrorState() {
 | 
			
		||||
		return ImageRes{}, fmt.Errorf("API 返回错误:%s", errRes.Error.Message)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return res, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Variation  以指定的图片的视角进行变换再创作,注意需要在对应的频道中关闭 Remix 变换,否则 Variation 指令将不会生效
 | 
			
		||||
func (c *Client) Variation(index int, messageId string, hash string) (ImageRes, error) {
 | 
			
		||||
	body := map[string]string{
 | 
			
		||||
		"customId":   fmt.Sprintf("MJ::JOB::variation::%d::%s", index, hash),
 | 
			
		||||
		"taskId":     messageId,
 | 
			
		||||
		"notifyHook": c.Config.NotifyURL,
 | 
			
		||||
	}
 | 
			
		||||
	apiURL := fmt.Sprintf("%s/mj/submit/action", c.Config.ApiURL)
 | 
			
		||||
	var res ImageRes
 | 
			
		||||
	var errRes ErrRes
 | 
			
		||||
	r, err := req.C().R().
 | 
			
		||||
		SetHeader("Authorization", "Bearer "+c.Config.ApiKey).
 | 
			
		||||
		SetBody(body).
 | 
			
		||||
		SetSuccessResult(&res).
 | 
			
		||||
		SetErrorResult(&errRes).
 | 
			
		||||
		Post(apiURL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return ImageRes{}, fmt.Errorf("请求 API 出错:%v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if r.IsErrorState() {
 | 
			
		||||
		return ImageRes{}, fmt.Errorf("API 返回错误:%s", errRes.Error.Message)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return res, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type QueryRes struct {
 | 
			
		||||
	Action  string `json:"action"`
 | 
			
		||||
	Buttons []struct {
 | 
			
		||||
		CustomId string `json:"customId"`
 | 
			
		||||
		Emoji    string `json:"emoji"`
 | 
			
		||||
		Label    string `json:"label"`
 | 
			
		||||
		Style    int    `json:"style"`
 | 
			
		||||
		Type     int    `json:"type"`
 | 
			
		||||
	} `json:"buttons"`
 | 
			
		||||
	Description string `json:"description"`
 | 
			
		||||
	FailReason  string `json:"failReason"`
 | 
			
		||||
	FinishTime  int    `json:"finishTime"`
 | 
			
		||||
	Id          string `json:"id"`
 | 
			
		||||
	ImageUrl    string `json:"imageUrl"`
 | 
			
		||||
	Progress    string `json:"progress"`
 | 
			
		||||
	Prompt      string `json:"prompt"`
 | 
			
		||||
	PromptEn    string `json:"promptEn"`
 | 
			
		||||
	Properties  struct {
 | 
			
		||||
	} `json:"properties"`
 | 
			
		||||
	StartTime  int    `json:"startTime"`
 | 
			
		||||
	State      string `json:"state"`
 | 
			
		||||
	Status     string `json:"status"`
 | 
			
		||||
	SubmitTime int    `json:"submitTime"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Client) QueryTask(taskId string) (QueryRes, error) {
 | 
			
		||||
	apiURL := fmt.Sprintf("%s/mj/task/%s/fetch", c.Config.ApiURL, taskId)
 | 
			
		||||
	var res QueryRes
 | 
			
		||||
	r, err := req.C().R().SetHeader("Authorization", "Bearer "+c.Config.ApiKey).
 | 
			
		||||
		SetSuccessResult(&res).
 | 
			
		||||
		Get(apiURL)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return QueryRes{}, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if r.IsErrorState() {
 | 
			
		||||
		return QueryRes{}, errors.New("error status:" + r.Status)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return res, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										167
									
								
								api/service/mj/plus/service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								api/service/mj/plus/service.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,167 @@
 | 
			
		||||
package plus
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/store"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync/atomic"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Service MJ 绘画服务
 | 
			
		||||
type Service struct {
 | 
			
		||||
	Name             string  // service Name
 | 
			
		||||
	Client           *Client // MJ Client
 | 
			
		||||
	taskQueue        *store.RedisQueue
 | 
			
		||||
	notifyQueue      *store.RedisQueue
 | 
			
		||||
	db               *gorm.DB
 | 
			
		||||
	maxHandleTaskNum int32             // max task number current service can handle
 | 
			
		||||
	HandledTaskNum   int32             // already handled task number
 | 
			
		||||
	taskStartTimes   map[int]time.Time // task start time, to check if the task is timeout
 | 
			
		||||
	taskTimeout      int64
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewService(name string, taskQueue *store.RedisQueue, notifyQueue *store.RedisQueue, maxTaskNum int32, timeout int64, db *gorm.DB, client *Client) *Service {
 | 
			
		||||
	return &Service{
 | 
			
		||||
		Name:             name,
 | 
			
		||||
		db:               db,
 | 
			
		||||
		taskQueue:        taskQueue,
 | 
			
		||||
		notifyQueue:      notifyQueue,
 | 
			
		||||
		Client:           client,
 | 
			
		||||
		taskTimeout:      timeout,
 | 
			
		||||
		maxHandleTaskNum: maxTaskNum,
 | 
			
		||||
		taskStartTimes:   make(map[int]time.Time, 0),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Service) Run() {
 | 
			
		||||
	logger.Infof("Starting MidJourney job consumer for %s", s.Name)
 | 
			
		||||
	for {
 | 
			
		||||
		s.checkTasks()
 | 
			
		||||
		if !s.canHandleTask() {
 | 
			
		||||
			// current service is full, can not handle more task
 | 
			
		||||
			// waiting for running task finish
 | 
			
		||||
			time.Sleep(time.Second * 3)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var task types.MjTask
 | 
			
		||||
		err := s.taskQueue.LPop(&task)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logger.Errorf("taking task with error: %v", err)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// if it's reference message, check if it's this channel's  message
 | 
			
		||||
		if task.ChannelId != "" && task.ChannelId != s.Name {
 | 
			
		||||
			logger.Debugf("handle other service task, name: %s, channel_id: %s, drop it.", s.Name, task.ChannelId)
 | 
			
		||||
			s.taskQueue.RPush(task)
 | 
			
		||||
			time.Sleep(time.Second)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		logger.Infof("%s handle a new MidJourney task: %+v", s.Name, task)
 | 
			
		||||
		var res ImageRes
 | 
			
		||||
		switch task.Type {
 | 
			
		||||
		case types.TaskImage:
 | 
			
		||||
			index := strings.Index(task.Prompt, " ")
 | 
			
		||||
			res, err = s.Client.Imagine(task.Prompt[index+1:])
 | 
			
		||||
			break
 | 
			
		||||
		case types.TaskUpscale:
 | 
			
		||||
			res, err = s.Client.Upscale(task.Index, task.MessageId, task.MessageHash)
 | 
			
		||||
			break
 | 
			
		||||
		case types.TaskVariation:
 | 
			
		||||
			res, err = s.Client.Variation(task.Index, task.MessageId, task.MessageHash)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err != nil || (res.Code != 1 && res.Code != 22) {
 | 
			
		||||
			logger.Error("绘画任务执行失败:", err)
 | 
			
		||||
			// update the task progress
 | 
			
		||||
			s.db.Model(&model.MidJourneyJob{Id: uint(task.Id)}).UpdateColumn("progress", -1)
 | 
			
		||||
			// 任务失败,通知前端
 | 
			
		||||
			s.notifyQueue.RPush(task.UserId)
 | 
			
		||||
			// restore img_call quota
 | 
			
		||||
			if task.Type.String() != types.TaskUpscale.String() {
 | 
			
		||||
				s.db.Model(&model.User{}).Where("id = ?", task.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls + ?", 1))
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// TODO: 任务提交失败,加入队列重试
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		logger.Infof("任务提交成功:%+v", res)
 | 
			
		||||
		// lock the task until the execute timeout
 | 
			
		||||
		s.taskStartTimes[task.Id] = time.Now()
 | 
			
		||||
		atomic.AddInt32(&s.HandledTaskNum, 1)
 | 
			
		||||
		// 更新任务 ID/频道
 | 
			
		||||
		s.db.Model(&model.MidJourneyJob{}).Where("id = ?", task.Id).UpdateColumns(map[string]interface{}{
 | 
			
		||||
			"task_id":    res.Result,
 | 
			
		||||
			"channel_id": s.Name,
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// check if current service instance can handle more task
 | 
			
		||||
func (s *Service) canHandleTask() bool {
 | 
			
		||||
	handledNum := atomic.LoadInt32(&s.HandledTaskNum)
 | 
			
		||||
	return handledNum < s.maxHandleTaskNum
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// remove the expired tasks
 | 
			
		||||
func (s *Service) checkTasks() {
 | 
			
		||||
	for k, t := range s.taskStartTimes {
 | 
			
		||||
		if time.Now().Unix()-t.Unix() > s.taskTimeout {
 | 
			
		||||
			delete(s.taskStartTimes, k)
 | 
			
		||||
			atomic.AddInt32(&s.HandledTaskNum, -1)
 | 
			
		||||
			// delete task from database
 | 
			
		||||
			s.db.Delete(&model.MidJourneyJob{Id: uint(k)}, "progress < 100")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type CBReq struct {
 | 
			
		||||
	Id          string      `json:"id"`
 | 
			
		||||
	Action      string      `json:"action"`
 | 
			
		||||
	Status      string      `json:"status"`
 | 
			
		||||
	Prompt      string      `json:"prompt"`
 | 
			
		||||
	PromptEn    string      `json:"promptEn"`
 | 
			
		||||
	Description string      `json:"description"`
 | 
			
		||||
	SubmitTime  int64       `json:"submitTime"`
 | 
			
		||||
	StartTime   int64       `json:"startTime"`
 | 
			
		||||
	FinishTime  int64       `json:"finishTime"`
 | 
			
		||||
	Progress    string      `json:"progress"`
 | 
			
		||||
	ImageUrl    string      `json:"imageUrl"`
 | 
			
		||||
	FailReason  interface{} `json:"failReason"`
 | 
			
		||||
	Properties  struct {
 | 
			
		||||
		FinalPrompt string `json:"finalPrompt"`
 | 
			
		||||
	} `json:"properties"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Service) Notify(data CBReq, job model.MidJourneyJob) error {
 | 
			
		||||
 | 
			
		||||
	job.Progress = utils.IntValue(strings.Replace(data.Progress, "%", "", 1), 0)
 | 
			
		||||
	job.Prompt = data.Properties.FinalPrompt
 | 
			
		||||
	if data.ImageUrl != "" {
 | 
			
		||||
		job.OrgURL = data.ImageUrl
 | 
			
		||||
	}
 | 
			
		||||
	job.UseProxy = true
 | 
			
		||||
	job.MessageId = data.Id
 | 
			
		||||
	logger.Debugf("JOB: %+v", job)
 | 
			
		||||
	res := s.db.Updates(&job)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		return fmt.Errorf("error with update job: %v", res.Error)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if data.Status == "SUCCESS" {
 | 
			
		||||
		// release lock task
 | 
			
		||||
		atomic.AddInt32(&s.HandledTaskNum, -1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	s.notifyQueue.RPush(job.UserId)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										265
									
								
								api/service/mj/pool.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										265
									
								
								api/service/mj/pool.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,265 @@
 | 
			
		||||
package mj
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/service/mj/plus"
 | 
			
		||||
	"chatplus/service/oss"
 | 
			
		||||
	"chatplus/store"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/go-redis/redis/v8"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync/atomic"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// ServicePool Mj service pool
 | 
			
		||||
type ServicePool struct {
 | 
			
		||||
	services        []interface{}
 | 
			
		||||
	taskQueue       *store.RedisQueue
 | 
			
		||||
	notifyQueue     *store.RedisQueue
 | 
			
		||||
	db              *gorm.DB
 | 
			
		||||
	uploaderManager *oss.UploaderManager
 | 
			
		||||
	Clients         *types.LMap[uint, *types.WsClient] // UserId => Client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewServicePool(db *gorm.DB, redisCli *redis.Client, manager *oss.UploaderManager, appConfig *types.AppConfig) *ServicePool {
 | 
			
		||||
	services := make([]interface{}, 0)
 | 
			
		||||
	taskQueue := store.NewRedisQueue("MidJourney_Task_Queue", redisCli)
 | 
			
		||||
	notifyQueue := store.NewRedisQueue("MidJourney_Notify_Queue", redisCli)
 | 
			
		||||
 | 
			
		||||
	for k, config := range appConfig.MjPlusConfigs {
 | 
			
		||||
		if config.Enabled == false {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		if config.ApiURL != "https://gpt.bemore.lol" && config.ApiURL != "https://api.chat-plus.net" {
 | 
			
		||||
			config.ApiURL = "https://api.chat-plus.net"
 | 
			
		||||
		}
 | 
			
		||||
		client := plus.NewClient(config)
 | 
			
		||||
		name := fmt.Sprintf("mj-service-plus-%d", k)
 | 
			
		||||
		servicePlus := plus.NewService(name, taskQueue, notifyQueue, 10, 600, db, client)
 | 
			
		||||
		go func() {
 | 
			
		||||
			servicePlus.Run()
 | 
			
		||||
		}()
 | 
			
		||||
		services = append(services, servicePlus)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(services) == 0 {
 | 
			
		||||
		// create mj client and service
 | 
			
		||||
		for k, config := range appConfig.MjConfigs {
 | 
			
		||||
			if config.Enabled == false {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			// create mj client
 | 
			
		||||
			client := NewClient(config, appConfig.ProxyURL, appConfig.ImgCdnURL)
 | 
			
		||||
 | 
			
		||||
			name := fmt.Sprintf("MjService-%d", k)
 | 
			
		||||
			// create mj service
 | 
			
		||||
			service := NewService(name, taskQueue, notifyQueue, 4, 600, db, client)
 | 
			
		||||
			botName := fmt.Sprintf("MjBot-%d", k)
 | 
			
		||||
			bot, err := NewBot(botName, appConfig.ProxyURL, config, service)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			err = bot.Run()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// run mj service
 | 
			
		||||
			go func() {
 | 
			
		||||
				service.Run()
 | 
			
		||||
			}()
 | 
			
		||||
 | 
			
		||||
			services = append(services, service)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &ServicePool{
 | 
			
		||||
		taskQueue:       taskQueue,
 | 
			
		||||
		notifyQueue:     notifyQueue,
 | 
			
		||||
		services:        services,
 | 
			
		||||
		uploaderManager: manager,
 | 
			
		||||
		db:              db,
 | 
			
		||||
		Clients:         types.NewLMap[uint, *types.WsClient](),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *ServicePool) CheckTaskNotify() {
 | 
			
		||||
	go func() {
 | 
			
		||||
		for {
 | 
			
		||||
			var userId uint
 | 
			
		||||
			err := p.notifyQueue.LPop(&userId)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			client := p.Clients.Get(userId)
 | 
			
		||||
			err = client.Send([]byte("Task Updated"))
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *ServicePool) DownloadImages() {
 | 
			
		||||
	go func() {
 | 
			
		||||
		var items []model.MidJourneyJob
 | 
			
		||||
		for {
 | 
			
		||||
			res := p.db.Where("img_url = ? AND progress = ?", "", 100).Find(&items)
 | 
			
		||||
			if res.Error != nil {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// download images
 | 
			
		||||
			for _, v := range items {
 | 
			
		||||
				if v.OrgURL == "" {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				var imgURL string
 | 
			
		||||
				var err error
 | 
			
		||||
				if v.UseProxy {
 | 
			
		||||
					if servicePlus := p.getServicePlus(v.ChannelId); servicePlus != nil {
 | 
			
		||||
						task, _ := servicePlus.Client.QueryTask(v.TaskId)
 | 
			
		||||
						if task.ImageUrl != "" {
 | 
			
		||||
							imgURL, err = p.uploaderManager.GetUploadHandler().PutImg(task.ImageUrl, false)
 | 
			
		||||
						}
 | 
			
		||||
						if len(task.Buttons) > 0 {
 | 
			
		||||
							v.Hash = getImageHash(task.Buttons[0].CustomId)
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				} else {
 | 
			
		||||
					imgURL, err = p.uploaderManager.GetUploadHandler().PutImg(v.OrgURL, true)
 | 
			
		||||
				}
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					logger.Error("error with download image: ", err)
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				v.ImgURL = imgURL
 | 
			
		||||
				p.db.Updates(&v)
 | 
			
		||||
 | 
			
		||||
				client := p.Clients.Get(uint(v.UserId))
 | 
			
		||||
				err = client.Send([]byte("Task Updated"))
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			time.Sleep(time.Second * 5)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PushTask push a new mj task in to task queue
 | 
			
		||||
func (p *ServicePool) PushTask(task types.MjTask) {
 | 
			
		||||
	logger.Debugf("add a new MidJourney task to the task list: %+v", task)
 | 
			
		||||
	p.taskQueue.RPush(task)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HasAvailableService check if it has available mj service in pool
 | 
			
		||||
func (p *ServicePool) HasAvailableService() bool {
 | 
			
		||||
	return len(p.services) > 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *ServicePool) Notify(data plus.CBReq) error {
 | 
			
		||||
	logger.Infof("收到任务回调:%+v", data)
 | 
			
		||||
	var job model.MidJourneyJob
 | 
			
		||||
	res := p.db.Where("task_id = ?", data.Id).First(&job)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		return fmt.Errorf("非法任务:%s", data.Id)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 任务已经拉取完成
 | 
			
		||||
	if job.Progress == 100 {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	if servicePlus := p.getServicePlus(job.ChannelId); servicePlus != nil {
 | 
			
		||||
		return servicePlus.Notify(data, job)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SyncTaskProgress 异步拉取任务
 | 
			
		||||
func (p *ServicePool) SyncTaskProgress() {
 | 
			
		||||
	go func() {
 | 
			
		||||
		var items []model.MidJourneyJob
 | 
			
		||||
		for {
 | 
			
		||||
			res := p.db.Where("progress < ?", 100).Find(&items)
 | 
			
		||||
			if res.Error != nil {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			for _, v := range items {
 | 
			
		||||
				// 30 分钟还没完成的任务直接删除
 | 
			
		||||
				if time.Now().Sub(v.CreatedAt) > time.Minute*30 {
 | 
			
		||||
					p.db.Delete(&v)
 | 
			
		||||
					// 非放大任务,退回绘图次数
 | 
			
		||||
					if v.Type != types.TaskUpscale.String() {
 | 
			
		||||
						p.db.Model(&model.User{}).Where("id = ?", v.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls + ?", 1))
 | 
			
		||||
					}
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if !strings.HasPrefix(v.ChannelId, "mj-service-plus") {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if servicePlus := p.getServicePlus(v.ChannelId); servicePlus != nil {
 | 
			
		||||
					task, err := servicePlus.Client.QueryTask(v.TaskId)
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						continue
 | 
			
		||||
					}
 | 
			
		||||
					if len(task.Buttons) > 0 {
 | 
			
		||||
						v.Hash = getImageHash(task.Buttons[0].CustomId)
 | 
			
		||||
					}
 | 
			
		||||
					oldProgress := v.Progress
 | 
			
		||||
					v.Progress = utils.IntValue(strings.Replace(task.Progress, "%", "", 1), 0)
 | 
			
		||||
					v.Prompt = task.PromptEn
 | 
			
		||||
					if task.ImageUrl != "" {
 | 
			
		||||
						v.OrgURL = task.ImageUrl
 | 
			
		||||
					}
 | 
			
		||||
					v.UseProxy = true
 | 
			
		||||
					v.MessageId = task.Id
 | 
			
		||||
 | 
			
		||||
					p.db.Updates(&v)
 | 
			
		||||
 | 
			
		||||
					if task.Status == "SUCCESS" {
 | 
			
		||||
						// release lock task
 | 
			
		||||
						atomic.AddInt32(&servicePlus.HandledTaskNum, -1)
 | 
			
		||||
					}
 | 
			
		||||
					// 通知前端更新任务进度
 | 
			
		||||
					if oldProgress != v.Progress {
 | 
			
		||||
						p.notifyQueue.RPush(v.UserId)
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			time.Sleep(time.Second)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *ServicePool) getServicePlus(name string) *plus.Service {
 | 
			
		||||
	for _, s := range p.services {
 | 
			
		||||
		if servicePlus, ok := s.(*plus.Service); ok {
 | 
			
		||||
			if servicePlus.Name == name {
 | 
			
		||||
				return servicePlus
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getImageHash(action string) string {
 | 
			
		||||
	split := strings.Split(action, "::")
 | 
			
		||||
	if len(split) > 5 {
 | 
			
		||||
		return split[4]
 | 
			
		||||
	}
 | 
			
		||||
	return split[len(split)-1]
 | 
			
		||||
}
 | 
			
		||||
@@ -2,63 +2,67 @@ package mj
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/service/oss"
 | 
			
		||||
	"chatplus/store"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/go-redis/redis/v8"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync/atomic"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// MJ 绘画服务
 | 
			
		||||
 | 
			
		||||
const RunningJobKey = "MidJourney_Running_Job"
 | 
			
		||||
 | 
			
		||||
// Service MJ 绘画服务
 | 
			
		||||
type Service struct {
 | 
			
		||||
	client        *Client // MJ 客户端
 | 
			
		||||
	taskQueue     *store.RedisQueue
 | 
			
		||||
	redis         *redis.Client
 | 
			
		||||
	db            *gorm.DB
 | 
			
		||||
	uploadManager *oss.UploaderManager
 | 
			
		||||
	Clients       *types.LMap[string, *types.WsClient] // MJ 绘画页面 websocket 连接池,用户推送绘画消息
 | 
			
		||||
	ChatClients   *types.LMap[string, *types.WsClient] // 聊天页面 websocket 连接池,用于推送绘画消息
 | 
			
		||||
	proxyURL      string
 | 
			
		||||
	name             string  // service name
 | 
			
		||||
	client           *Client // MJ client
 | 
			
		||||
	taskQueue        *store.RedisQueue
 | 
			
		||||
	notifyQueue      *store.RedisQueue
 | 
			
		||||
	db               *gorm.DB
 | 
			
		||||
	maxHandleTaskNum int32             // max task number current service can handle
 | 
			
		||||
	handledTaskNum   int32             // already handled task number
 | 
			
		||||
	taskStartTimes   map[int]time.Time // task start time, to check if the task is timeout
 | 
			
		||||
	taskTimeout      int64
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewService(redisCli *redis.Client, db *gorm.DB, client *Client, manager *oss.UploaderManager, config *types.AppConfig) *Service {
 | 
			
		||||
func NewService(name string, taskQueue *store.RedisQueue, notifyQueue *store.RedisQueue, maxTaskNum int32, timeout int64, db *gorm.DB, client *Client) *Service {
 | 
			
		||||
	return &Service{
 | 
			
		||||
		redis:         redisCli,
 | 
			
		||||
		db:            db,
 | 
			
		||||
		taskQueue:     store.NewRedisQueue("MidJourney_Task_Queue", redisCli),
 | 
			
		||||
		client:        client,
 | 
			
		||||
		uploadManager: manager,
 | 
			
		||||
		Clients:       types.NewLMap[string, *types.WsClient](),
 | 
			
		||||
		ChatClients:   types.NewLMap[string, *types.WsClient](),
 | 
			
		||||
		proxyURL:      config.ProxyURL,
 | 
			
		||||
		name:             name,
 | 
			
		||||
		db:               db,
 | 
			
		||||
		taskQueue:        taskQueue,
 | 
			
		||||
		notifyQueue:      notifyQueue,
 | 
			
		||||
		client:           client,
 | 
			
		||||
		taskTimeout:      timeout,
 | 
			
		||||
		maxHandleTaskNum: maxTaskNum,
 | 
			
		||||
		taskStartTimes:   make(map[int]time.Time, 0),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Service) Run() {
 | 
			
		||||
	logger.Info("Starting MidJourney job consumer.")
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	logger.Infof("Starting MidJourney job consumer for %s", s.name)
 | 
			
		||||
	for {
 | 
			
		||||
		_, err := s.redis.Get(ctx, RunningJobKey).Result()
 | 
			
		||||
		if err == nil { // 队列串行执行
 | 
			
		||||
		s.checkTasks()
 | 
			
		||||
		if !s.canHandleTask() {
 | 
			
		||||
			// current service is full, can not handle more task
 | 
			
		||||
			// waiting for running task finish
 | 
			
		||||
			time.Sleep(time.Second * 3)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var task types.MjTask
 | 
			
		||||
		err = s.taskQueue.LPop(&task)
 | 
			
		||||
		err := s.taskQueue.LPop(&task)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logger.Errorf("taking task with error: %v", err)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		logger.Infof("Consuming Task: %+v", task)
 | 
			
		||||
 | 
			
		||||
		// if it's reference message, check if it's this channel's  message
 | 
			
		||||
		if task.ChannelId != "" && task.ChannelId != s.client.Config.ChanelId {
 | 
			
		||||
			s.taskQueue.RPush(task)
 | 
			
		||||
			time.Sleep(time.Second)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		logger.Infof("%s handle a new MidJourney task: %+v", s.name, task)
 | 
			
		||||
		switch task.Type {
 | 
			
		||||
		case types.TaskImage:
 | 
			
		||||
			err = s.client.Imagine(task.Prompt)
 | 
			
		||||
@@ -70,42 +74,47 @@ func (s *Service) Run() {
 | 
			
		||||
		case types.TaskVariation:
 | 
			
		||||
			err = s.client.Variation(task.Index, task.MessageId, task.MessageHash)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logger.Error("绘画任务执行失败:", err)
 | 
			
		||||
			if task.RetryCount <= 5 {
 | 
			
		||||
				s.taskQueue.RPush(task)
 | 
			
		||||
			// update the task progress
 | 
			
		||||
			s.db.Model(&model.MidJourneyJob{Id: uint(task.Id)}).UpdateColumn("progress", -1)
 | 
			
		||||
			s.notifyQueue.RPush(task.UserId)
 | 
			
		||||
			// restore img_call quota
 | 
			
		||||
			if task.Type.String() != types.TaskUpscale.String() {
 | 
			
		||||
				s.db.Model(&model.User{}).Where("id = ?", task.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls + ?", 1))
 | 
			
		||||
			}
 | 
			
		||||
			task.RetryCount += 1
 | 
			
		||||
			time.Sleep(time.Second * 3)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 更新任务的执行状态
 | 
			
		||||
		s.db.Model(&model.MidJourneyJob{}).Where("id = ?", task.Id).UpdateColumn("started", true)
 | 
			
		||||
		// 锁定任务执行通道,直到任务超时(5分钟)
 | 
			
		||||
		s.redis.Set(ctx, RunningJobKey, utils.JsonEncode(task), time.Minute*5)
 | 
			
		||||
		// lock the task until the execute timeout
 | 
			
		||||
		s.taskStartTimes[task.Id] = time.Now()
 | 
			
		||||
		atomic.AddInt32(&s.handledTaskNum, 1)
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Service) PushTask(task types.MjTask) {
 | 
			
		||||
	logger.Infof("add a new MidJourney Task: %+v", task)
 | 
			
		||||
	s.taskQueue.RPush(task)
 | 
			
		||||
// check if current service instance can handle more task
 | 
			
		||||
func (s *Service) canHandleTask() bool {
 | 
			
		||||
	handledNum := atomic.LoadInt32(&s.handledTaskNum)
 | 
			
		||||
	return handledNum < s.maxHandleTaskNum
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// remove the expired tasks
 | 
			
		||||
func (s *Service) checkTasks() {
 | 
			
		||||
	for k, t := range s.taskStartTimes {
 | 
			
		||||
		if time.Now().Unix()-t.Unix() > s.taskTimeout {
 | 
			
		||||
			delete(s.taskStartTimes, k)
 | 
			
		||||
			atomic.AddInt32(&s.handledTaskNum, -1)
 | 
			
		||||
			// delete task from database
 | 
			
		||||
			s.db.Delete(&model.MidJourneyJob{Id: uint(k)}, "progress < 100")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Service) Notify(data CBReq) {
 | 
			
		||||
	taskString, err := s.redis.Get(context.Background(), RunningJobKey).Result()
 | 
			
		||||
	if err != nil { // 过期任务,丢弃
 | 
			
		||||
		logger.Warn("任务已过期:", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var task types.MjTask
 | 
			
		||||
	err = utils.JsonDecode(taskString, &task)
 | 
			
		||||
	if err != nil { // 非标准任务,丢弃
 | 
			
		||||
		logger.Warn("任务解析失败:", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// extract the task ID
 | 
			
		||||
	split := strings.Split(data.Prompt, " ")
 | 
			
		||||
	var job model.MidJourneyJob
 | 
			
		||||
	res := s.db.Where("message_id = ?", data.MessageId).First(&job)
 | 
			
		||||
	if res.Error == nil && data.Status == Finished {
 | 
			
		||||
@@ -113,137 +122,41 @@ func (s *Service) Notify(data CBReq) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if task.Src == types.TaskSrcImg { // 绘画任务
 | 
			
		||||
		var job model.MidJourneyJob
 | 
			
		||||
		res := s.db.Where("id = ?", task.Id).First(&job)
 | 
			
		||||
		if res.Error != nil {
 | 
			
		||||
			logger.Warn("非法任务:", res.Error)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		job.MessageId = data.MessageId
 | 
			
		||||
		job.ReferenceId = data.ReferenceId
 | 
			
		||||
		job.Progress = data.Progress
 | 
			
		||||
		job.Prompt = data.Prompt
 | 
			
		||||
		job.Hash = data.Image.Hash
 | 
			
		||||
 | 
			
		||||
		// 任务完成,将最终的图片下载下来
 | 
			
		||||
		if data.Progress == 100 {
 | 
			
		||||
			imgURL, err := s.uploadManager.GetUploadHandler().PutImg(data.Image.URL, true)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				logger.Error("error with download img: ", err.Error())
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			job.ImgURL = imgURL
 | 
			
		||||
		} else {
 | 
			
		||||
			// 临时图片直接保存,访问的时候使用代理进行转发
 | 
			
		||||
			job.ImgURL = data.Image.URL
 | 
			
		||||
		}
 | 
			
		||||
		res = s.db.Updates(&job)
 | 
			
		||||
		if res.Error != nil {
 | 
			
		||||
			logger.Error("error with update job: ", res.Error)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var jobVo vo.MidJourneyJob
 | 
			
		||||
		err := utils.CopyObject(job, &jobVo)
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			if data.Progress < 100 {
 | 
			
		||||
				image, err := utils.DownloadImage(jobVo.ImgURL, s.proxyURL)
 | 
			
		||||
				if err == nil {
 | 
			
		||||
					jobVo.ImgURL = "data:image/png;base64," + base64.StdEncoding.EncodeToString(image)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// 推送任务到前端
 | 
			
		||||
			client := s.Clients.Get(task.SessionId)
 | 
			
		||||
			if client != nil {
 | 
			
		||||
				utils.ReplyChunkMessage(client, jobVo)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	} else if task.Src == types.TaskSrcChat { // 聊天任务
 | 
			
		||||
		wsClient := s.ChatClients.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 := s.uploadManager.GetUploadHandler().PutImg(data.Image.URL, true)
 | 
			
		||||
			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
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			tx := s.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 {
 | 
			
		||||
				logger.Error("error with update database: ", err)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// 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 {
 | 
			
		||||
				logger.Error("error with update database: ", err)
 | 
			
		||||
				tx.Rollback()
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			tx.Commit()
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if wsClient == nil { // 客户端断线,则丢弃
 | 
			
		||||
			logger.Errorf("Client is offline: %+v", data)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if data.Status == Finished {
 | 
			
		||||
			utils.ReplyChunkMessage(wsClient, types.WsMessage{Type: types.WsMjImg, Content: data})
 | 
			
		||||
			utils.ReplyChunkMessage(wsClient, types.WsMessage{Type: types.WsEnd})
 | 
			
		||||
			// 本次绘画完毕,移除客户端
 | 
			
		||||
			s.ChatClients.Delete(task.SessionId)
 | 
			
		||||
		} else {
 | 
			
		||||
			// 使用代理临时转发图片
 | 
			
		||||
			if data.Image.URL != "" {
 | 
			
		||||
				image, err := utils.DownloadImage(data.Image.URL, s.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})
 | 
			
		||||
		}
 | 
			
		||||
	tx := s.db.Session(&gorm.Session{}).Where("progress < ?", 100).Order("id ASC")
 | 
			
		||||
	if data.ReferenceId != "" {
 | 
			
		||||
		tx = tx.Where("reference_id = ?", data.ReferenceId)
 | 
			
		||||
	} else {
 | 
			
		||||
		tx = tx.Where("task_id = ?", split[0])
 | 
			
		||||
	}
 | 
			
		||||
	res = tx.First(&job)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		logger.Warn("非法任务:", res.Error)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	job.ChannelId = data.ChannelId
 | 
			
		||||
	job.MessageId = data.MessageId
 | 
			
		||||
	job.ReferenceId = data.ReferenceId
 | 
			
		||||
	job.Progress = data.Progress
 | 
			
		||||
	job.Prompt = data.Prompt
 | 
			
		||||
	job.Hash = data.Image.Hash
 | 
			
		||||
	job.OrgURL = data.Image.URL
 | 
			
		||||
	if s.client.Config.UseCDN {
 | 
			
		||||
		job.UseProxy = true
 | 
			
		||||
		job.ImgURL = strings.ReplaceAll(data.Image.URL, "https://cdn.discordapp.com", s.client.imgCdnURL)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	res = s.db.Updates(&job)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		logger.Error("error with update job: ", res.Error)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 更新用户剩余绘图次数
 | 
			
		||||
	// TODO: 放大图片是否需要消耗绘图次数?
 | 
			
		||||
	if data.Status == Finished {
 | 
			
		||||
		s.db.Model(&model.User{}).Where("id = ?", task.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
 | 
			
		||||
		// 解除任务锁定
 | 
			
		||||
		s.redis.Del(context.Background(), RunningJobKey)
 | 
			
		||||
		// release lock task
 | 
			
		||||
		atomic.AddInt32(&s.handledTaskNum, -1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	s.notifyQueue.RPush(job.UserId)
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,7 @@ type InteractionsResult struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type CBReq struct {
 | 
			
		||||
	ChannelId   string     `json:"channel_id"`
 | 
			
		||||
	MessageId   string     `json:"message_id"`
 | 
			
		||||
	ReferenceId string     `json:"reference_id"`
 | 
			
		||||
	Image       Image      `json:"image"`
 | 
			
		||||
 
 | 
			
		||||
@@ -32,6 +32,10 @@ func NewAliYunOss(appConfig *types.AppConfig) (*AliYunOss, error) {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if config.SubDir == "" {
 | 
			
		||||
		config.SubDir = "gpt"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &AliYunOss{
 | 
			
		||||
		config:   config,
 | 
			
		||||
		bucket:   bucket,
 | 
			
		||||
@@ -40,28 +44,33 @@ func NewAliYunOss(appConfig *types.AppConfig) (*AliYunOss, error) {
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s AliYunOss) PutFile(ctx *gin.Context, name string) (string, error) {
 | 
			
		||||
func (s AliYunOss) PutFile(ctx *gin.Context, name string) (File, error) {
 | 
			
		||||
	// 解析表单
 | 
			
		||||
	file, err := ctx.FormFile(name)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
		return File{}, err
 | 
			
		||||
	}
 | 
			
		||||
	// 打开上传文件
 | 
			
		||||
	src, err := file.Open()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
		return File{}, err
 | 
			
		||||
	}
 | 
			
		||||
	defer src.Close()
 | 
			
		||||
 | 
			
		||||
	fileExt := filepath.Ext(file.Filename)
 | 
			
		||||
	objectKey := fmt.Sprintf("%d%s", time.Now().UnixMicro(), fileExt)
 | 
			
		||||
	objectKey := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), fileExt)
 | 
			
		||||
	// 上传文件
 | 
			
		||||
	err = s.bucket.PutObject(objectKey, src)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
		return File{}, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return fmt.Sprintf("https://%s.%s/%s", s.config.Bucket, s.config.Endpoint, objectKey), nil
 | 
			
		||||
	return File{
 | 
			
		||||
		Name: file.Filename,
 | 
			
		||||
		URL:  fmt.Sprintf("%s/%s", s.config.Domain, objectKey),
 | 
			
		||||
		Ext:  fileExt,
 | 
			
		||||
		Size: file.Size,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s AliYunOss) PutImg(imageURL string, useProxy bool) (string, error) {
 | 
			
		||||
@@ -80,18 +89,19 @@ func (s AliYunOss) PutImg(imageURL string, useProxy bool) (string, error) {
 | 
			
		||||
		return "", fmt.Errorf("error with parse image URL: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	fileExt := filepath.Ext(parse.Path)
 | 
			
		||||
	objectKey := fmt.Sprintf("%d%s", time.Now().UnixMicro(), fileExt)
 | 
			
		||||
	objectKey := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), fileExt)
 | 
			
		||||
	// 上传文件字节数据
 | 
			
		||||
	err = s.bucket.PutObject(objectKey, bytes.NewReader(imageData))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	return fmt.Sprintf("https://%s.%s/%s", s.config.Bucket, s.config.Endpoint, objectKey), nil
 | 
			
		||||
	return fmt.Sprintf("%s/%s", s.config.Domain, objectKey), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s AliYunOss) Delete(fileURL string) error {
 | 
			
		||||
	objectName := filepath.Base(fileURL)
 | 
			
		||||
	return s.bucket.DeleteObject(objectName)
 | 
			
		||||
	key := fmt.Sprintf("%s/%s", s.config.SubDir, objectName)
 | 
			
		||||
	return s.bucket.DeleteObject(key)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ Uploader = AliYunOss{}
 | 
			
		||||
 
 | 
			
		||||
@@ -23,23 +23,29 @@ func NewLocalStorage(config *types.AppConfig) LocalStorage {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s LocalStorage) PutFile(ctx *gin.Context, name string) (string, error) {
 | 
			
		||||
func (s LocalStorage) PutFile(ctx *gin.Context, name string) (File, error) {
 | 
			
		||||
	file, err := ctx.FormFile(name)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("error with get form: %v", err)
 | 
			
		||||
		return File{}, fmt.Errorf("error with get form: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	filePath, err := utils.GenUploadPath(s.config.BasePath, file.Filename)
 | 
			
		||||
	path, err := utils.GenUploadPath(s.config.BasePath, file.Filename)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("error with generate filename: %s", err.Error())
 | 
			
		||||
		return File{}, fmt.Errorf("error with generate filename: %s", err.Error())
 | 
			
		||||
	}
 | 
			
		||||
	// 将文件保存到指定路径
 | 
			
		||||
	err = ctx.SaveUploadedFile(file, filePath)
 | 
			
		||||
	err = ctx.SaveUploadedFile(file, path)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("error with save upload file: %s", err.Error())
 | 
			
		||||
		return File{}, fmt.Errorf("error with save upload file: %s", err.Error())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return utils.GenUploadUrl(s.config.BasePath, s.config.BaseURL, filePath), nil
 | 
			
		||||
	ext := filepath.Ext(file.Filename)
 | 
			
		||||
	return File{
 | 
			
		||||
		Name: file.Filename,
 | 
			
		||||
		URL:  utils.GenUploadUrl(s.config.BasePath, s.config.BaseURL, path),
 | 
			
		||||
		Ext:  ext,
 | 
			
		||||
		Size: file.Size,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s LocalStorage) PutImg(imageURL string, useProxy bool) (string, error) {
 | 
			
		||||
 
 | 
			
		||||
@@ -29,6 +29,9 @@ func NewMiniOss(appConfig *types.AppConfig) (MiniOss, error) {
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return MiniOss{}, err
 | 
			
		||||
	}
 | 
			
		||||
	if config.SubDir == "" {
 | 
			
		||||
		config.SubDir = "gpt"
 | 
			
		||||
	}
 | 
			
		||||
	return MiniOss{config: config, client: minioClient, proxyURL: appConfig.ProxyURL}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -48,7 +51,7 @@ func (s MiniOss) PutImg(imageURL string, useProxy bool) (string, error) {
 | 
			
		||||
		return "", fmt.Errorf("error with parse image URL: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	fileExt := filepath.Ext(parse.Path)
 | 
			
		||||
	filename := fmt.Sprintf("%d%s", time.Now().UnixMicro(), fileExt)
 | 
			
		||||
	filename := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), fileExt)
 | 
			
		||||
	info, err := s.client.PutObject(
 | 
			
		||||
		context.Background(),
 | 
			
		||||
		s.config.Bucket,
 | 
			
		||||
@@ -62,33 +65,39 @@ func (s MiniOss) PutImg(imageURL string, useProxy bool) (string, error) {
 | 
			
		||||
	return fmt.Sprintf("%s/%s/%s", s.config.Domain, s.config.Bucket, info.Key), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s MiniOss) PutFile(ctx *gin.Context, name string) (string, error) {
 | 
			
		||||
func (s MiniOss) PutFile(ctx *gin.Context, name string) (File, error) {
 | 
			
		||||
	file, err := ctx.FormFile(name)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("error with get form: %v", err)
 | 
			
		||||
		return File{}, fmt.Errorf("error with get form: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	// Open the uploaded file
 | 
			
		||||
	fileReader, err := file.Open()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("error opening file: %v", err)
 | 
			
		||||
		return File{}, fmt.Errorf("error opening file: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	defer fileReader.Close()
 | 
			
		||||
 | 
			
		||||
	fileExt := filepath.Ext(file.Filename)
 | 
			
		||||
	filename := fmt.Sprintf("%d%s", time.Now().UnixMicro(), fileExt)
 | 
			
		||||
	filename := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), fileExt)
 | 
			
		||||
	info, err := s.client.PutObject(ctx, s.config.Bucket, filename, fileReader, file.Size, minio.PutObjectOptions{
 | 
			
		||||
		ContentType: file.Header.Get("Content-Type"),
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("error uploading to MinIO: %v", err)
 | 
			
		||||
		return File{}, fmt.Errorf("error uploading to MinIO: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return fmt.Sprintf("%s/%s/%s", s.config.Domain, s.config.Bucket, info.Key), nil
 | 
			
		||||
	return File{
 | 
			
		||||
		Name: file.Filename,
 | 
			
		||||
		URL:  fmt.Sprintf("%s/%s/%s", s.config.Domain, s.config.Bucket, info.Key),
 | 
			
		||||
		Ext:  fileExt,
 | 
			
		||||
		Size: file.Size,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s MiniOss) Delete(fileURL string) error {
 | 
			
		||||
	objectName := filepath.Base(fileURL)
 | 
			
		||||
	return s.client.RemoveObject(context.Background(), s.config.Bucket, objectName, minio.RemoveObjectOptions{})
 | 
			
		||||
	key := fmt.Sprintf("%s/%s", s.config.SubDir, objectName)
 | 
			
		||||
	return s.client.RemoveObject(context.Background(), s.config.Bucket, key, minio.RemoveObjectOptions{})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ Uploader = MiniOss{}
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,6 @@ type QinNiuOss struct {
 | 
			
		||||
	uploader  *storage.FormUploader
 | 
			
		||||
	manager   *storage.BucketManager
 | 
			
		||||
	proxyURL  string
 | 
			
		||||
	dir       string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewQiNiuOss(appConfig *types.AppConfig) QinNiuOss {
 | 
			
		||||
@@ -38,6 +37,9 @@ func NewQiNiuOss(appConfig *types.AppConfig) QinNiuOss {
 | 
			
		||||
	putPolicy := storage.PutPolicy{
 | 
			
		||||
		Scope: config.Bucket,
 | 
			
		||||
	}
 | 
			
		||||
	if config.SubDir == "" {
 | 
			
		||||
		config.SubDir = "gpt"
 | 
			
		||||
	}
 | 
			
		||||
	return QinNiuOss{
 | 
			
		||||
		config:    config,
 | 
			
		||||
		mac:       mac,
 | 
			
		||||
@@ -45,34 +47,39 @@ func NewQiNiuOss(appConfig *types.AppConfig) QinNiuOss {
 | 
			
		||||
		uploader:  formUploader,
 | 
			
		||||
		manager:   storage.NewBucketManager(mac, &storeConfig),
 | 
			
		||||
		proxyURL:  appConfig.ProxyURL,
 | 
			
		||||
		dir:       "chatgpt-plus",
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s QinNiuOss) PutFile(ctx *gin.Context, name string) (string, error) {
 | 
			
		||||
func (s QinNiuOss) PutFile(ctx *gin.Context, name string) (File, error) {
 | 
			
		||||
	// 解析表单
 | 
			
		||||
	file, err := ctx.FormFile(name)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
		return File{}, err
 | 
			
		||||
	}
 | 
			
		||||
	// 打开上传文件
 | 
			
		||||
	src, err := file.Open()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
		return File{}, err
 | 
			
		||||
	}
 | 
			
		||||
	defer src.Close()
 | 
			
		||||
 | 
			
		||||
	fileExt := filepath.Ext(file.Filename)
 | 
			
		||||
	key := fmt.Sprintf("%s/%d%s", s.dir, time.Now().UnixMicro(), fileExt)
 | 
			
		||||
	key := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), fileExt)
 | 
			
		||||
	// 上传文件
 | 
			
		||||
	ret := storage.PutRet{}
 | 
			
		||||
	extra := storage.PutExtra{}
 | 
			
		||||
	err = s.uploader.Put(ctx, &ret, s.putPolicy.UploadToken(s.mac), key, src, file.Size, &extra)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
		return File{}, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return fmt.Sprintf("%s/%s", s.config.Domain, ret.Key), nil
 | 
			
		||||
	return File{
 | 
			
		||||
		Name: file.Filename,
 | 
			
		||||
		URL:  fmt.Sprintf("%s/%s", s.config.Domain, ret.Key),
 | 
			
		||||
		Ext:  fileExt,
 | 
			
		||||
		Size: file.Size,
 | 
			
		||||
	}, nil
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s QinNiuOss) PutImg(imageURL string, useProxy bool) (string, error) {
 | 
			
		||||
@@ -91,7 +98,7 @@ func (s QinNiuOss) PutImg(imageURL string, useProxy bool) (string, error) {
 | 
			
		||||
		return "", fmt.Errorf("error with parse image URL: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	fileExt := filepath.Ext(parse.Path)
 | 
			
		||||
	key := fmt.Sprintf("%s/%d%s", s.dir, time.Now().UnixMicro(), fileExt)
 | 
			
		||||
	key := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), fileExt)
 | 
			
		||||
	ret := storage.PutRet{}
 | 
			
		||||
	extra := storage.PutExtra{}
 | 
			
		||||
	// 上传文件字节数据
 | 
			
		||||
@@ -104,7 +111,7 @@ func (s QinNiuOss) PutImg(imageURL string, useProxy bool) (string, error) {
 | 
			
		||||
 | 
			
		||||
func (s QinNiuOss) Delete(fileURL string) error {
 | 
			
		||||
	objectName := filepath.Base(fileURL)
 | 
			
		||||
	key := fmt.Sprintf("%s/%s", s.dir, objectName)
 | 
			
		||||
	key := fmt.Sprintf("%s/%s", s.config.SubDir, objectName)
 | 
			
		||||
	return s.manager.Delete(s.config.Bucket, key)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,19 @@ package oss
 | 
			
		||||
 | 
			
		||||
import "github.com/gin-gonic/gin"
 | 
			
		||||
 | 
			
		||||
const Local = "LOCAL"
 | 
			
		||||
const Minio = "MINIO"
 | 
			
		||||
const QiNiu = "QINIU"
 | 
			
		||||
const AliYun = "ALIYUN"
 | 
			
		||||
 | 
			
		||||
type File struct {
 | 
			
		||||
	Name string `json:"name"`
 | 
			
		||||
	Size int64  `json:"size"`
 | 
			
		||||
	URL  string `json:"url"`
 | 
			
		||||
	Ext  string `json:"ext"`
 | 
			
		||||
}
 | 
			
		||||
type Uploader interface {
 | 
			
		||||
	PutFile(ctx *gin.Context, name string) (string, error)
 | 
			
		||||
	PutFile(ctx *gin.Context, name string) (File, error)
 | 
			
		||||
	PutImg(imageURL string, useProxy bool) (string, error)
 | 
			
		||||
	Delete(fileURL string) error
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,11 +9,6 @@ type UploaderManager struct {
 | 
			
		||||
	handler Uploader
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Local = "LOCAL"
 | 
			
		||||
const Minio = "MINIO"
 | 
			
		||||
const QiNiu = "QINIU"
 | 
			
		||||
const AliYun = "ALIYUN"
 | 
			
		||||
 | 
			
		||||
func NewUploaderManager(config *types.AppConfig) (*UploaderManager, error) {
 | 
			
		||||
	active := Local
 | 
			
		||||
	if config.OSS.Active != "" {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										162
									
								
								api/service/payment/hupipay_serive.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										162
									
								
								api/service/payment/hupipay_serive.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,162 @@
 | 
			
		||||
package payment
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"crypto/md5"
 | 
			
		||||
	"encoding/hex"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"sort"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type HuPiPayService struct {
 | 
			
		||||
	appId     string
 | 
			
		||||
	appSecret string
 | 
			
		||||
	apiURL    string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewHuPiPay(config *types.AppConfig) *HuPiPayService {
 | 
			
		||||
	return &HuPiPayService{
 | 
			
		||||
		appId:     config.HuPiPayConfig.AppId,
 | 
			
		||||
		appSecret: config.HuPiPayConfig.AppSecret,
 | 
			
		||||
		apiURL:    config.HuPiPayConfig.ApiURL,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type HuPiPayReq struct {
 | 
			
		||||
	AppId        string `json:"appid"`
 | 
			
		||||
	Version      string `json:"version"`
 | 
			
		||||
	TradeOrderId string `json:"trade_order_id"`
 | 
			
		||||
	TotalFee     string `json:"total_fee"`
 | 
			
		||||
	Title        string `json:"title"`
 | 
			
		||||
	NotifyURL    string `json:"notify_url"`
 | 
			
		||||
	ReturnURL    string `json:"return_url"`
 | 
			
		||||
	WapName      string `json:"wap_name"`
 | 
			
		||||
	CallbackURL  string `json:"callback_url"`
 | 
			
		||||
	Time         string `json:"time"`
 | 
			
		||||
	NonceStr     string `json:"nonce_str"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type HuPiResp struct {
 | 
			
		||||
	Openid    interface{} `json:"openid"`
 | 
			
		||||
	UrlQrcode string      `json:"url_qrcode"`
 | 
			
		||||
	URL       string      `json:"url"`
 | 
			
		||||
	ErrCode   int         `json:"errcode"`
 | 
			
		||||
	ErrMsg    string      `json:"errmsg,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Pay 执行支付请求操作
 | 
			
		||||
func (s *HuPiPayService) Pay(params HuPiPayReq) (HuPiResp, error) {
 | 
			
		||||
	data := url.Values{}
 | 
			
		||||
	simple := strconv.FormatInt(time.Now().Unix(), 10)
 | 
			
		||||
	params.AppId = s.appId
 | 
			
		||||
	params.Time = simple
 | 
			
		||||
	params.NonceStr = simple
 | 
			
		||||
	encode := utils.JsonEncode(params)
 | 
			
		||||
	m := make(map[string]string)
 | 
			
		||||
	_ = utils.JsonDecode(encode, &m)
 | 
			
		||||
	for k, v := range m {
 | 
			
		||||
		data.Add(k, fmt.Sprintf("%v", v))
 | 
			
		||||
	}
 | 
			
		||||
	// 生成签名
 | 
			
		||||
	data.Add("hash", s.Sign(data))
 | 
			
		||||
	// 发送支付请求
 | 
			
		||||
	apiURL := fmt.Sprintf("%s/payment/do.html", s.apiURL)
 | 
			
		||||
	resp, err := http.PostForm(apiURL, data)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return HuPiResp{}, fmt.Errorf("error with requst api: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
	all, err := io.ReadAll(resp.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return HuPiResp{}, fmt.Errorf("error with reading response: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var res HuPiResp
 | 
			
		||||
	err = utils.JsonDecode(string(all), &res)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return HuPiResp{}, fmt.Errorf("error with decode payment result: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if res.ErrCode != 0 {
 | 
			
		||||
		return HuPiResp{}, fmt.Errorf("error with generate pay url: %s", res.ErrMsg)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return res, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Sign 签名方法
 | 
			
		||||
func (s *HuPiPayService) Sign(params url.Values) string {
 | 
			
		||||
	params.Del(`Sign`)
 | 
			
		||||
	var keys = make([]string, 0, 0)
 | 
			
		||||
	for key := range params {
 | 
			
		||||
		if params.Get(key) != `` {
 | 
			
		||||
			keys = append(keys, key)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	sort.Strings(keys)
 | 
			
		||||
 | 
			
		||||
	var pList = make([]string, 0, 0)
 | 
			
		||||
	for _, key := range keys {
 | 
			
		||||
		var value = strings.TrimSpace(params.Get(key))
 | 
			
		||||
		if len(value) > 0 {
 | 
			
		||||
			pList = append(pList, key+"="+value)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	var src = strings.Join(pList, "&")
 | 
			
		||||
	src += s.appSecret
 | 
			
		||||
 | 
			
		||||
	md5bs := md5.Sum([]byte(src))
 | 
			
		||||
	return hex.EncodeToString(md5bs[:])
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Check 校验订单状态
 | 
			
		||||
func (s *HuPiPayService) Check(tradeNo string) error {
 | 
			
		||||
	data := url.Values{}
 | 
			
		||||
	data.Add("appid", s.appId)
 | 
			
		||||
	data.Add("open_order_id", tradeNo)
 | 
			
		||||
	stamp := strconv.FormatInt(time.Now().Unix(), 10)
 | 
			
		||||
	data.Add("time", stamp)
 | 
			
		||||
	data.Add("nonce_str", stamp)
 | 
			
		||||
	data.Add("hash", s.Sign(data))
 | 
			
		||||
 | 
			
		||||
	apiURL := fmt.Sprintf("%s/payment/query.html", s.apiURL)
 | 
			
		||||
	resp, err := http.PostForm(apiURL, data)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("error with http reqeust: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
	body, err := io.ReadAll(resp.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("error with reading response: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var r struct {
 | 
			
		||||
		ErrCode int `json:"errcode"`
 | 
			
		||||
		Data    struct {
 | 
			
		||||
			Status      string `json:"status"`
 | 
			
		||||
			OpenOrderId string `json:"open_order_id"`
 | 
			
		||||
		} `json:"data,omitempty"`
 | 
			
		||||
		ErrMsg string `json:"errmsg"`
 | 
			
		||||
		Hash   string `json:"hash"`
 | 
			
		||||
	}
 | 
			
		||||
	err = utils.JsonDecode(string(body), &r)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("error with decode response: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if r.ErrCode == 0 && r.Data.Status == "OD" {
 | 
			
		||||
		return nil
 | 
			
		||||
	} else {
 | 
			
		||||
		logger.Debugf("%+v", r)
 | 
			
		||||
		return errors.New("order not paid:" + r.ErrMsg)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										140
									
								
								api/service/payment/payjs_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								api/service/payment/payjs_service.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,140 @@
 | 
			
		||||
package payment
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"crypto/md5"
 | 
			
		||||
	"encoding/hex"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"sort"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type PayJS struct {
 | 
			
		||||
	config *types.JPayConfig
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewPayJS(appConfig *types.AppConfig) *PayJS {
 | 
			
		||||
	return &PayJS{
 | 
			
		||||
		config: &appConfig.JPayConfig,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type JPayReq struct {
 | 
			
		||||
	TotalFee   int    `json:"total_fee"`
 | 
			
		||||
	OutTradeNo string `json:"out_trade_no"`
 | 
			
		||||
	Subject    string `json:"body"`
 | 
			
		||||
	NotifyURL  string `json:"notify_url"`
 | 
			
		||||
}
 | 
			
		||||
type JPayReps struct {
 | 
			
		||||
	CodeUrl    string `json:"code_url"`
 | 
			
		||||
	OutTradeNo string `json:"out_trade_no"`
 | 
			
		||||
	OrderId    string `json:"payjs_order_id"`
 | 
			
		||||
	Qrcode     string `json:"qrcode"`
 | 
			
		||||
	ReturnCode int    `json:"return_code"`
 | 
			
		||||
	ReturnMsg  string `json:"return_msg"`
 | 
			
		||||
	Sign       string `json:"Sign"`
 | 
			
		||||
	TotalFee   string `json:"total_fee"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r JPayReps) IsOK() bool {
 | 
			
		||||
	return r.ReturnMsg == "SUCCESS"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (js *PayJS) Pay(param JPayReq) JPayReps {
 | 
			
		||||
	param.NotifyURL = js.config.NotifyURL
 | 
			
		||||
	var p = url.Values{}
 | 
			
		||||
	encode := utils.JsonEncode(param)
 | 
			
		||||
	m := make(map[string]interface{})
 | 
			
		||||
	_ = utils.JsonDecode(encode, &m)
 | 
			
		||||
	for k, v := range m {
 | 
			
		||||
		p.Add(k, fmt.Sprintf("%v", v))
 | 
			
		||||
	}
 | 
			
		||||
	p.Add("mchid", js.config.AppId)
 | 
			
		||||
 | 
			
		||||
	p.Add("Sign", js.sign(p))
 | 
			
		||||
 | 
			
		||||
	cli := http.Client{}
 | 
			
		||||
	apiURL := fmt.Sprintf("%s/api/native", js.config.ApiURL)
 | 
			
		||||
	r, err := cli.PostForm(apiURL, p)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return JPayReps{ReturnMsg: err.Error()}
 | 
			
		||||
	}
 | 
			
		||||
	defer r.Body.Close()
 | 
			
		||||
	bs, err := io.ReadAll(r.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return JPayReps{ReturnMsg: err.Error()}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var data JPayReps
 | 
			
		||||
	err = utils.JsonDecode(string(bs), &data)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return JPayReps{ReturnMsg: err.Error()}
 | 
			
		||||
	}
 | 
			
		||||
	return data
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (js *PayJS) sign(params url.Values) string {
 | 
			
		||||
	params.Del(`Sign`)
 | 
			
		||||
	var keys = make([]string, 0, 0)
 | 
			
		||||
	for key := range params {
 | 
			
		||||
		if params.Get(key) != `` {
 | 
			
		||||
			keys = append(keys, key)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	sort.Strings(keys)
 | 
			
		||||
 | 
			
		||||
	var pList = make([]string, 0, 0)
 | 
			
		||||
	for _, key := range keys {
 | 
			
		||||
		var value = strings.TrimSpace(params.Get(key))
 | 
			
		||||
		if len(value) > 0 {
 | 
			
		||||
			pList = append(pList, key+"="+value)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	var src = strings.Join(pList, "&")
 | 
			
		||||
	src += "&key=" + js.config.PrivateKey
 | 
			
		||||
 | 
			
		||||
	md5bs := md5.Sum([]byte(src))
 | 
			
		||||
	md5res := hex.EncodeToString(md5bs[:])
 | 
			
		||||
	return strings.ToUpper(md5res)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Check 查询订单支付状态
 | 
			
		||||
// @param tradeNo 支付平台交易 ID
 | 
			
		||||
func (js *PayJS) Check(tradeNo string) error {
 | 
			
		||||
	apiURL := fmt.Sprintf("%s/api/check", js.config.ApiURL)
 | 
			
		||||
	params := url.Values{}
 | 
			
		||||
	params.Add("payjs_order_id", tradeNo)
 | 
			
		||||
	params.Add("Sign", js.sign(params))
 | 
			
		||||
	data := strings.NewReader(params.Encode())
 | 
			
		||||
	resp, err := http.Post(apiURL, "application/x-www-form-urlencoded", data)
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("error with http reqeust: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
	body, err := io.ReadAll(resp.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("error with reading response: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var r struct {
 | 
			
		||||
		ReturnCode int `json:"return_code"`
 | 
			
		||||
		Status     int `json:"status"`
 | 
			
		||||
	}
 | 
			
		||||
	err = utils.JsonDecode(string(body), &r)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("error with decode response: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if r.ReturnCode == 1 && r.Status == 1 {
 | 
			
		||||
		return nil
 | 
			
		||||
	} else {
 | 
			
		||||
		return errors.New("order not paid")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										53
									
								
								api/service/sd/pool.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								api/service/sd/pool.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,53 @@
 | 
			
		||||
package sd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/service/oss"
 | 
			
		||||
	"chatplus/store"
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-redis/redis/v8"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ServicePool struct {
 | 
			
		||||
	services  []*Service
 | 
			
		||||
	taskQueue *store.RedisQueue
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewServicePool(db *gorm.DB, redisCli *redis.Client, manager *oss.UploaderManager, appConfig *types.AppConfig) *ServicePool {
 | 
			
		||||
	services := make([]*Service, 0)
 | 
			
		||||
	queue := store.NewRedisQueue("StableDiffusion_Task_Queue", redisCli)
 | 
			
		||||
	// create mj client and service
 | 
			
		||||
	for k, config := range appConfig.SdConfigs {
 | 
			
		||||
		if config.Enabled == false {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// create sd service
 | 
			
		||||
		name := fmt.Sprintf("StableDifffusion Service-%d", k)
 | 
			
		||||
		service := NewService(name, 1, 300, config, queue, db, manager)
 | 
			
		||||
		// run sd service
 | 
			
		||||
		go func() {
 | 
			
		||||
			service.Run()
 | 
			
		||||
		}()
 | 
			
		||||
 | 
			
		||||
		services = append(services, service)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &ServicePool{
 | 
			
		||||
		taskQueue: queue,
 | 
			
		||||
		services:  services,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PushTask push a new mj task in to task queue
 | 
			
		||||
func (p *ServicePool) PushTask(task types.SdTask) {
 | 
			
		||||
	logger.Debugf("add a new MidJourney task to the task list: %+v", task)
 | 
			
		||||
	p.taskQueue.RPush(task)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HasAvailableService check if it has available mj service in pool
 | 
			
		||||
func (p *ServicePool) HasAvailableService() bool {
 | 
			
		||||
	return len(p.services) > 0
 | 
			
		||||
}
 | 
			
		||||
@@ -5,84 +5,99 @@ import (
 | 
			
		||||
	"chatplus/service/oss"
 | 
			
		||||
	"chatplus/store"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/go-redis/redis/v8"
 | 
			
		||||
	"github.com/imroc/req/v3"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"io"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"sync/atomic"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/imroc/req/v3"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// SD 绘画服务
 | 
			
		||||
 | 
			
		||||
const RunningJobKey = "StableDiffusion_Running_Job"
 | 
			
		||||
 | 
			
		||||
type Service struct {
 | 
			
		||||
	httpClient    *req.Client
 | 
			
		||||
	config        *types.StableDiffusionConfig
 | 
			
		||||
	taskQueue     *store.RedisQueue
 | 
			
		||||
	redis         *redis.Client
 | 
			
		||||
	db            *gorm.DB
 | 
			
		||||
	uploadManager *oss.UploaderManager
 | 
			
		||||
	Clients       *types.LMap[string, *types.WsClient] // SD 绘画页面 websocket 连接池
 | 
			
		||||
	httpClient       *req.Client
 | 
			
		||||
	config           types.StableDiffusionConfig
 | 
			
		||||
	taskQueue        *store.RedisQueue
 | 
			
		||||
	db               *gorm.DB
 | 
			
		||||
	uploadManager    *oss.UploaderManager
 | 
			
		||||
	name             string            // service name
 | 
			
		||||
	maxHandleTaskNum int32             // max task number current service can handle
 | 
			
		||||
	handledTaskNum   int32             // already handled task number
 | 
			
		||||
	taskStartTimes   map[int]time.Time // task start time, to check if the task is timeout
 | 
			
		||||
	taskTimeout      int64
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewService(config *types.AppConfig, redisCli *redis.Client, db *gorm.DB, manager *oss.UploaderManager) *Service {
 | 
			
		||||
func NewService(name string, maxTaskNum int32, timeout int64, config types.StableDiffusionConfig, queue *store.RedisQueue, db *gorm.DB, manager *oss.UploaderManager) *Service {
 | 
			
		||||
	return &Service{
 | 
			
		||||
		config:        &config.SdConfig,
 | 
			
		||||
		httpClient:    req.C(),
 | 
			
		||||
		redis:         redisCli,
 | 
			
		||||
		db:            db,
 | 
			
		||||
		uploadManager: manager,
 | 
			
		||||
		Clients:       types.NewLMap[string, *types.WsClient](),
 | 
			
		||||
		taskQueue:     store.NewRedisQueue("stable_diffusion_task_queue", redisCli),
 | 
			
		||||
		name:             name,
 | 
			
		||||
		config:           config,
 | 
			
		||||
		httpClient:       req.C(),
 | 
			
		||||
		taskQueue:        queue,
 | 
			
		||||
		db:               db,
 | 
			
		||||
		uploadManager:    manager,
 | 
			
		||||
		taskTimeout:      timeout,
 | 
			
		||||
		maxHandleTaskNum: maxTaskNum,
 | 
			
		||||
		taskStartTimes:   make(map[int]time.Time),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Service) Run() {
 | 
			
		||||
	logger.Info("Starting StableDiffusion job consumer.")
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	for {
 | 
			
		||||
		_, err := s.redis.Get(ctx, RunningJobKey).Result()
 | 
			
		||||
		if err == nil { // 队列串行执行
 | 
			
		||||
		s.checkTasks()
 | 
			
		||||
		if !s.canHandleTask() {
 | 
			
		||||
			// current service is full, can not handle more task
 | 
			
		||||
			// waiting for running task finish
 | 
			
		||||
			time.Sleep(time.Second * 3)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var task types.SdTask
 | 
			
		||||
		err = s.taskQueue.LPop(&task)
 | 
			
		||||
		err := s.taskQueue.LPop(&task)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logger.Errorf("taking task with error: %v", err)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		logger.Infof("Consuming Task: %+v", task)
 | 
			
		||||
		logger.Infof("%s handle a new Stable-Diffusion task: %+v", s.name, task)
 | 
			
		||||
		err = s.Txt2Img(task)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logger.Error("绘画任务执行失败:", err)
 | 
			
		||||
			if task.RetryCount <= 5 {
 | 
			
		||||
				s.taskQueue.RPush(task)
 | 
			
		||||
			}
 | 
			
		||||
			task.RetryCount += 1
 | 
			
		||||
			time.Sleep(time.Second * 3)
 | 
			
		||||
			// update the task progress
 | 
			
		||||
			s.db.Model(&model.SdJob{Id: uint(task.Id)}).UpdateColumn("progress", -1)
 | 
			
		||||
			// restore img_call quota
 | 
			
		||||
			s.db.Model(&model.User{}).Where("id = ?", task.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls + ?", 1))
 | 
			
		||||
			// release task num
 | 
			
		||||
			atomic.AddInt32(&s.handledTaskNum, -1)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 更新任务的执行状态
 | 
			
		||||
		s.db.Model(&model.SdJob{}).Where("id = ?", task.Id).UpdateColumn("started", true)
 | 
			
		||||
		// 锁定任务执行通道,直到任务超时(5分钟)
 | 
			
		||||
		s.redis.Set(ctx, RunningJobKey, utils.JsonEncode(task), time.Minute*5)
 | 
			
		||||
		// lock the task until the execute timeout
 | 
			
		||||
		s.taskStartTimes[task.Id] = time.Now()
 | 
			
		||||
		atomic.AddInt32(&s.handledTaskNum, 1)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PushTask 推送任务到队列
 | 
			
		||||
func (s *Service) PushTask(task types.SdTask) {
 | 
			
		||||
	logger.Infof("add a new Stable Diffusion Task: %+v", task)
 | 
			
		||||
	s.taskQueue.RPush(task)
 | 
			
		||||
// check if current service instance can handle more task
 | 
			
		||||
func (s *Service) canHandleTask() bool {
 | 
			
		||||
	handledNum := atomic.LoadInt32(&s.handledTaskNum)
 | 
			
		||||
	return handledNum < s.maxHandleTaskNum
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// remove the expired tasks
 | 
			
		||||
func (s *Service) checkTasks() {
 | 
			
		||||
	for k, t := range s.taskStartTimes {
 | 
			
		||||
		if time.Now().Unix()-t.Unix() > s.taskTimeout {
 | 
			
		||||
			delete(s.taskStartTimes, k)
 | 
			
		||||
			atomic.AddInt32(&s.handledTaskNum, -1)
 | 
			
		||||
			// delete task from database
 | 
			
		||||
			s.db.Delete(&model.MidJourneyJob{Id: uint(k)}, "progress < 100")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Txt2Img 文生图 API
 | 
			
		||||
@@ -121,6 +136,7 @@ func (s *Service) Txt2Img(task types.SdTask) error {
 | 
			
		||||
	taskInfo.TaskId = params.TaskId
 | 
			
		||||
	taskInfo.Data = data
 | 
			
		||||
	taskInfo.JobId = task.Id
 | 
			
		||||
	taskInfo.UserId = uint(task.UserId)
 | 
			
		||||
	go func() {
 | 
			
		||||
		s.runTask(taskInfo, s.httpClient)
 | 
			
		||||
	}()
 | 
			
		||||
@@ -135,7 +151,6 @@ func (s *Service) runTask(taskInfo TaskInfo, client *req.Client) {
 | 
			
		||||
		"fn_index":     taskInfo.FnIndex,
 | 
			
		||||
		"session_hash": taskInfo.SessionHash,
 | 
			
		||||
	}
 | 
			
		||||
	logger.Debug(utils.JsonEncode(body))
 | 
			
		||||
	var result = make(chan CBReq)
 | 
			
		||||
	go func() {
 | 
			
		||||
		var res struct {
 | 
			
		||||
@@ -144,7 +159,7 @@ func (s *Service) runTask(taskInfo TaskInfo, client *req.Client) {
 | 
			
		||||
			Duration        float64       `json:"duration"`
 | 
			
		||||
			AverageDuration float64       `json:"average_duration"`
 | 
			
		||||
		}
 | 
			
		||||
		var cbReq = CBReq{TaskId: taskInfo.TaskId, JobId: taskInfo.JobId, SessionId: taskInfo.SessionId}
 | 
			
		||||
		var cbReq = CBReq{UserId: taskInfo.UserId, TaskId: taskInfo.TaskId, JobId: taskInfo.JobId, SessionId: taskInfo.SessionId}
 | 
			
		||||
		response, err := client.R().SetBody(body).SetSuccessResult(&res).Post(s.config.ApiURL + "/run/predict")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			cbReq.Message = "error with send request: " + err.Error()
 | 
			
		||||
@@ -217,7 +232,7 @@ func (s *Service) runTask(taskInfo TaskInfo, client *req.Client) {
 | 
			
		||||
				TextInfo      interface{} `json:"textinfo"`
 | 
			
		||||
			}
 | 
			
		||||
			response, err := client.R().SetBody(progressReq).SetSuccessResult(&progressRes).Post(s.config.ApiURL + "/internal/progress")
 | 
			
		||||
			var cbReq = CBReq{TaskId: taskInfo.TaskId, Success: true, JobId: taskInfo.JobId, SessionId: taskInfo.SessionId}
 | 
			
		||||
			var cbReq = CBReq{UserId: taskInfo.UserId, TaskId: taskInfo.TaskId, Success: true, JobId: taskInfo.JobId, SessionId: taskInfo.SessionId}
 | 
			
		||||
			if err != nil { // TODO: 这里可以考虑设置失败重试次数
 | 
			
		||||
				logger.Error(err)
 | 
			
		||||
				return
 | 
			
		||||
@@ -231,7 +246,6 @@ func (s *Service) runTask(taskInfo TaskInfo, client *req.Client) {
 | 
			
		||||
 | 
			
		||||
			cbReq.ImageData = progressRes.LivePreview
 | 
			
		||||
			cbReq.Progress = int(progressRes.Progress * 100)
 | 
			
		||||
			logger.Debug(cbReq)
 | 
			
		||||
			s.callback(cbReq)
 | 
			
		||||
			time.Sleep(time.Second)
 | 
			
		||||
		}
 | 
			
		||||
@@ -239,9 +253,8 @@ func (s *Service) runTask(taskInfo TaskInfo, client *req.Client) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Service) callback(data CBReq) {
 | 
			
		||||
	// 释放任务锁
 | 
			
		||||
	s.redis.Del(context.Background(), RunningJobKey)
 | 
			
		||||
	client := s.Clients.Get(data.SessionId)
 | 
			
		||||
	// release task num
 | 
			
		||||
	atomic.AddInt32(&s.handledTaskNum, -1)
 | 
			
		||||
	if data.Success { // 任务成功
 | 
			
		||||
		var job model.SdJob
 | 
			
		||||
		res := s.db.Where("id = ?", data.JobId).First(&job)
 | 
			
		||||
@@ -261,13 +274,15 @@ func (s *Service) callback(data CBReq) {
 | 
			
		||||
 | 
			
		||||
		params.Seed = data.Seed
 | 
			
		||||
		if data.ImageName != "" { // 下载图片
 | 
			
		||||
			imageURL := fmt.Sprintf("%s/file=%s", s.config.ApiURL, data.ImageName)
 | 
			
		||||
			imageURL, err := s.uploadManager.GetUploadHandler().PutImg(imageURL, false)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				logger.Error("error with download img: ", err.Error())
 | 
			
		||||
				return
 | 
			
		||||
			job.ImgURL = fmt.Sprintf("%s/file=%s", s.config.ApiURL, data.ImageName)
 | 
			
		||||
			if data.Progress == 100 {
 | 
			
		||||
				imageURL, err := s.uploadManager.GetUploadHandler().PutImg(job.ImgURL, false)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					logger.Error("error with download img: ", err.Error())
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
				job.ImgURL = imageURL
 | 
			
		||||
			}
 | 
			
		||||
			job.ImgURL = imageURL
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		job.Params = utils.JsonEncode(params)
 | 
			
		||||
@@ -277,32 +292,12 @@ func (s *Service) callback(data CBReq) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var jobVo vo.SdJob
 | 
			
		||||
		err = utils.CopyObject(job, &jobVo)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logger.Error("error with copy object: ", err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if data.Progress < 100 && data.ImageData != "" {
 | 
			
		||||
			jobVo.ImgURL = data.ImageData
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 推送任务到前端
 | 
			
		||||
		if client != nil {
 | 
			
		||||
			utils.ReplyChunkMessage(client, jobVo)
 | 
			
		||||
		}
 | 
			
		||||
		logger.Debugf("绘图进度:%d", data.Progress)
 | 
			
		||||
	} else { // 任务失败
 | 
			
		||||
		logger.Error("任务执行失败:", data.Message)
 | 
			
		||||
		// 删除任务
 | 
			
		||||
		s.db.Delete(&model.SdJob{Id: uint(data.JobId)})
 | 
			
		||||
		// 推送消息到前端
 | 
			
		||||
		if client != nil {
 | 
			
		||||
			utils.ReplyChunkMessage(client, vo.SdJob{
 | 
			
		||||
				Id:       uint(data.JobId),
 | 
			
		||||
				Progress: -1,
 | 
			
		||||
				TaskId:   data.TaskId,
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
		// update the task progress
 | 
			
		||||
		s.db.Model(&model.SdJob{Id: uint(data.JobId)}).UpdateColumn("progress", -1)
 | 
			
		||||
		// restore img_calls
 | 
			
		||||
		s.db.Model(&model.User{}).Where("id = ? AND img_calls > 0", data.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls + ?", 1))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ import logger2 "chatplus/logger"
 | 
			
		||||
var logger = logger2.GetLogger()
 | 
			
		||||
 | 
			
		||||
type TaskInfo struct {
 | 
			
		||||
	UserId      uint          `json:"user_id"`
 | 
			
		||||
	SessionId   string        `json:"session_id"`
 | 
			
		||||
	JobId       int           `json:"job_id"`
 | 
			
		||||
	TaskId      string        `json:"task_id"`
 | 
			
		||||
@@ -15,6 +16,7 @@ type TaskInfo struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type CBReq struct {
 | 
			
		||||
	UserId    uint
 | 
			
		||||
	SessionId string
 | 
			
		||||
	JobId     int
 | 
			
		||||
	TaskId    string
 | 
			
		||||
@@ -32,11 +34,11 @@ var ParamKeys = map[string]int{
 | 
			
		||||
	"negative_prompt": 2,
 | 
			
		||||
	"steps":           4,
 | 
			
		||||
	"sampler":         5,
 | 
			
		||||
	"face_fix":        6, // 面部修复
 | 
			
		||||
	"face_fix":        7, // 面部修复
 | 
			
		||||
	"cfg_scale":       8,
 | 
			
		||||
	"seed":            27,
 | 
			
		||||
	"height":          9,
 | 
			
		||||
	"width":           10,
 | 
			
		||||
	"height":          10,
 | 
			
		||||
	"width":           9,
 | 
			
		||||
	"hd_fix":          11,
 | 
			
		||||
	"hd_redraw_rate":  12, //高清修复重绘幅度
 | 
			
		||||
	"hd_scale":        13, // 高清修复放大倍数
 | 
			
		||||
 
 | 
			
		||||
@@ -1,31 +1,29 @@
 | 
			
		||||
package service
 | 
			
		||||
package sms
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/store"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/aliyun/alibaba-cloud-sdk-go/services/dysmsapi"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type AliYunSmsService struct {
 | 
			
		||||
	config *types.AliYunSmsConfig
 | 
			
		||||
	db     *store.LevelDB
 | 
			
		||||
	config *types.SmsConfigAli
 | 
			
		||||
	client *dysmsapi.Client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewAliYunSmsService(config *types.AppConfig, db *store.LevelDB) (*AliYunSmsService, error) {
 | 
			
		||||
func NewAliYunSmsService(appConfig *types.AppConfig) (*AliYunSmsService, error) {
 | 
			
		||||
	config := &appConfig.SMS.Ali
 | 
			
		||||
	// 创建阿里云短信客户端
 | 
			
		||||
	client, err := dysmsapi.NewClientWithAccessKey(
 | 
			
		||||
		"cn-hangzhou",
 | 
			
		||||
		config.SmsConfig.AccessKey,
 | 
			
		||||
		config.SmsConfig.AccessSecret)
 | 
			
		||||
		config.AccessKey,
 | 
			
		||||
		config.AccessSecret)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("failed to create client: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &AliYunSmsService{
 | 
			
		||||
		config: &config.SmsConfig,
 | 
			
		||||
		db:     db,
 | 
			
		||||
		config: config,
 | 
			
		||||
		client: client,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
@@ -49,6 +47,7 @@ func (s *AliYunSmsService) SendVerifyCode(mobile string, code int) error {
 | 
			
		||||
	if response.Code != "OK" {
 | 
			
		||||
		return fmt.Errorf("failed to send SMS:%v", response.Message)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ Service = &AliYunSmsService{}
 | 
			
		||||
							
								
								
									
										72
									
								
								api/service/sms/bao.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								api/service/sms/bao.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,72 @@
 | 
			
		||||
package sms
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type BaoSmsService struct {
 | 
			
		||||
	config *types.SmsConfigBao
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewSmsBaoSmsService(appConfig *types.AppConfig) *BaoSmsService {
 | 
			
		||||
	config := appConfig.SMS.Bao
 | 
			
		||||
	if config.Domain == "" { // use default domain
 | 
			
		||||
		config.Domain = "api.smsbao.com"
 | 
			
		||||
		logger.Infof("Using default domain for SMS-BAO: %s", config.Domain)
 | 
			
		||||
	}
 | 
			
		||||
	return &BaoSmsService{
 | 
			
		||||
		config: &config,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var errMsg = map[string]string{
 | 
			
		||||
	"0":  "短信发送成功",
 | 
			
		||||
	"-1": "参数不全",
 | 
			
		||||
	"-2": "服务器空间不支持,请确认支持curl或者fsocket,联系您的空间商解决或者更换空间",
 | 
			
		||||
	"30": "密码错误",
 | 
			
		||||
	"40": "账号不存在",
 | 
			
		||||
	"41": "余额不足",
 | 
			
		||||
	"42": "账户已过期",
 | 
			
		||||
	"43": "IP地址限制",
 | 
			
		||||
	"50": "内容含有敏感词",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *BaoSmsService) SendVerifyCode(mobile string, code int) error {
 | 
			
		||||
 | 
			
		||||
	content := fmt.Sprintf("%s%s", s.config.Sign, s.config.CodeTemplate)
 | 
			
		||||
	content = strings.ReplaceAll(content, "{code}", strconv.Itoa(code))
 | 
			
		||||
	password := utils.Md5(s.config.Password)
 | 
			
		||||
	params := url.Values{}
 | 
			
		||||
	params.Set("u", s.config.Username)
 | 
			
		||||
	params.Set("p", password)
 | 
			
		||||
	params.Set("m", mobile)
 | 
			
		||||
	params.Set("c", content)
 | 
			
		||||
 | 
			
		||||
	apiURL := fmt.Sprintf("https://%s/sms?%s", s.config.Domain, params.Encode())
 | 
			
		||||
	response, err := http.Get(apiURL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer response.Body.Close()
 | 
			
		||||
 | 
			
		||||
	body, err := io.ReadAll(response.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	result := string(body)
 | 
			
		||||
	logger.Debugf("send SmsBao result: %v", errMsg[result])
 | 
			
		||||
 | 
			
		||||
	if result != "0" {
 | 
			
		||||
		return fmt.Errorf("failed to send SMS:%v", errMsg[result])
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ Service = &BaoSmsService{}
 | 
			
		||||
							
								
								
									
										8
									
								
								api/service/sms/service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								api/service/sms/service.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
package sms
 | 
			
		||||
 | 
			
		||||
const Ali = "ALI"
 | 
			
		||||
const Bao = "BAO"
 | 
			
		||||
 | 
			
		||||
type Service interface {
 | 
			
		||||
	SendVerifyCode(mobile string, code int) error
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										39
									
								
								api/service/sms/service_manager.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								api/service/sms/service_manager.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
			
		||||
package sms
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	logger2 "chatplus/logger"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ServiceManager struct {
 | 
			
		||||
	handler Service
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var logger = logger2.GetLogger()
 | 
			
		||||
 | 
			
		||||
func NewSendServiceManager(config *types.AppConfig) (*ServiceManager, error) {
 | 
			
		||||
	active := Ali
 | 
			
		||||
	if config.OSS.Active != "" {
 | 
			
		||||
		active = strings.ToUpper(config.SMS.Active)
 | 
			
		||||
	}
 | 
			
		||||
	var handler Service
 | 
			
		||||
	switch active {
 | 
			
		||||
	case Ali:
 | 
			
		||||
		client, err := NewAliYunSmsService(config)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		handler = client
 | 
			
		||||
		break
 | 
			
		||||
	case Bao:
 | 
			
		||||
		handler = NewSmsBaoSmsService(config)
 | 
			
		||||
		break
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &ServiceManager{handler: handler}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *ServiceManager) GetService() Service {
 | 
			
		||||
	return m.handler
 | 
			
		||||
}
 | 
			
		||||
@@ -1,5 +0,0 @@
 | 
			
		||||
package service
 | 
			
		||||
 | 
			
		||||
type SmsService interface {
 | 
			
		||||
	SendVerifyCode(mobile string, code int) error
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										44
									
								
								api/service/smtp_sms_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								api/service/smtp_sms_service.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
			
		||||
package service
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"mime"
 | 
			
		||||
	"net/smtp"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type SmtpService struct {
 | 
			
		||||
	config *types.SmtpConfig
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewSmtpService(appConfig *types.AppConfig) *SmtpService {
 | 
			
		||||
	return &SmtpService{
 | 
			
		||||
		config: &appConfig.SmtpConfig,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *SmtpService) SendVerifyCode(to string, code int) error {
 | 
			
		||||
	subject := "ChatPlus注册验证码"
 | 
			
		||||
	body := fmt.Sprintf("您正在注册 ChatPlus AI 助手账户,注册验证码为 %d,请不要告诉他人。如非本人操作,请忽略此邮件。", code)
 | 
			
		||||
 | 
			
		||||
	// 设置SMTP客户端配置
 | 
			
		||||
	auth := smtp.PlainAuth("", s.config.From, s.config.Password, s.config.Host)
 | 
			
		||||
 | 
			
		||||
	// 对主题进行MIME编码
 | 
			
		||||
	encodedSubject := mime.QEncoding.Encode("UTF-8", subject)
 | 
			
		||||
	// 组装邮件
 | 
			
		||||
	message := bytes.NewBuffer(nil)
 | 
			
		||||
	message.WriteString(fmt.Sprintf("From: \"%s\" <%s>\r\n", s.config.AppName, s.config.From))
 | 
			
		||||
	message.WriteString(fmt.Sprintf("To: %s\r\n", to))
 | 
			
		||||
	message.WriteString(fmt.Sprintf("Subject: %s\r\n", encodedSubject))
 | 
			
		||||
	message.WriteString("\r\n" + body)
 | 
			
		||||
 | 
			
		||||
	// 发送邮件
 | 
			
		||||
	// 发送邮件
 | 
			
		||||
	err := smtp.SendMail(s.config.Host+":"+fmt.Sprint(s.config.Port), auth, s.config.From, []string{to}, message.Bytes())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("error sending email: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
@@ -23,7 +23,7 @@ func NewSnowflake() *Snowflake {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Next 生成一个新的唯一ID
 | 
			
		||||
func (s *Snowflake) Next() (string, error) {
 | 
			
		||||
func (s *Snowflake) Next(raw bool) (string, error) {
 | 
			
		||||
	s.mu.Lock()
 | 
			
		||||
	defer s.mu.Unlock()
 | 
			
		||||
 | 
			
		||||
@@ -43,6 +43,9 @@ func (s *Snowflake) Next() (string, error) {
 | 
			
		||||
 | 
			
		||||
	s.lastTimestamp = timestamp
 | 
			
		||||
	id := (timestamp << 22) | (int64(s.workerID) << 10) | int64(s.sequence)
 | 
			
		||||
	if raw {
 | 
			
		||||
		return fmt.Sprintf("%d", id), nil
 | 
			
		||||
	}
 | 
			
		||||
	now := time.Now()
 | 
			
		||||
	return fmt.Sprintf("%d%02d%02d%d", now.Year(), now.Month(), now.Day(), id), nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								api/service/wanx/types.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								api/service/wanx/types.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
package wanx
 | 
			
		||||
@@ -6,6 +6,8 @@ import (
 | 
			
		||||
	"github.com/eatmoreapple/openwechat"
 | 
			
		||||
	"github.com/skip2/go-qrcode"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strconv"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 微信收款机器人
 | 
			
		||||
@@ -34,8 +36,13 @@ func (b *Bot) Run() error {
 | 
			
		||||
	}
 | 
			
		||||
	// scan code login callback
 | 
			
		||||
	b.bot.UUIDCallback = b.qrCodeCallBack
 | 
			
		||||
 | 
			
		||||
	err := b.bot.Login()
 | 
			
		||||
	debug, err := strconv.ParseBool(os.Getenv("APP_DEBUG"))
 | 
			
		||||
	if debug {
 | 
			
		||||
		reloadStorage := openwechat.NewJsonFileHotReloadStorage("storage.json")
 | 
			
		||||
		err = b.bot.HotLogin(reloadStorage, true)
 | 
			
		||||
	} else {
 | 
			
		||||
		err = b.bot.Login()
 | 
			
		||||
	}
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
@@ -56,13 +63,13 @@ func (b *Bot) messageHandler(msg *openwechat.Message) {
 | 
			
		||||
		msg.MsgType == openwechat.MsgTypeApp ||
 | 
			
		||||
		msg.AppMsgType == openwechat.AppMsgTypeUrl {
 | 
			
		||||
		// 解析支付金额
 | 
			
		||||
		message, err := parseTransactionMessage(msg.Content)
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			transaction := extractTransaction(message)
 | 
			
		||||
			logger.Infof("解析到收款信息:%+v", transaction)
 | 
			
		||||
		message := parseTransactionMessage(msg.Content)
 | 
			
		||||
		transaction := extractTransaction(message)
 | 
			
		||||
		logger.Infof("解析到收款信息:%+v", transaction)
 | 
			
		||||
		if transaction.TransId != "" {
 | 
			
		||||
			var item model.Reward
 | 
			
		||||
			res := b.db.Where("tx_id = ?", transaction.TransId).First(&item)
 | 
			
		||||
			if res.Error == nil {
 | 
			
		||||
			if item.Id > 0 {
 | 
			
		||||
				logger.Error("当前交易 ID 己经存在!")
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,17 +2,15 @@ package wx
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/xml"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Message 转账消息
 | 
			
		||||
type Message struct {
 | 
			
		||||
	XMLName xml.Name `xml:"msg"`
 | 
			
		||||
	AppMsg  struct {
 | 
			
		||||
		Des string `xml:"des"`
 | 
			
		||||
		Url string `xml:"url"`
 | 
			
		||||
	} `xml:"appmsg"`
 | 
			
		||||
	Des string
 | 
			
		||||
	Url string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Transaction 解析后的交易信息
 | 
			
		||||
@@ -23,20 +21,56 @@ type Transaction struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 解析微信转账消息
 | 
			
		||||
func parseTransactionMessage(xmlData string) (*Message, error) {
 | 
			
		||||
	var msg Message
 | 
			
		||||
	if err := xml.Unmarshal([]byte(xmlData), &msg); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
func parseTransactionMessage(xmlData string) *Message {
 | 
			
		||||
	decoder := xml.NewDecoder(strings.NewReader(xmlData))
 | 
			
		||||
	message := Message{}
 | 
			
		||||
	for {
 | 
			
		||||
		token, err := decoder.Token()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		switch se := token.(type) {
 | 
			
		||||
		case xml.StartElement:
 | 
			
		||||
			var value string
 | 
			
		||||
			if se.Name.Local == "des" && message.Des == "" {
 | 
			
		||||
				if err := decoder.DecodeElement(&value, &se); err == nil {
 | 
			
		||||
					message.Des = strings.TrimSpace(value)
 | 
			
		||||
				}
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
			if se.Name.Local == "weapp_path" || se.Name.Local == "url" {
 | 
			
		||||
				if err := decoder.DecodeElement(&value, &se); err == nil {
 | 
			
		||||
					if strings.Contains(value, "trans_id=") {
 | 
			
		||||
						message.Url = value
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &msg, nil
 | 
			
		||||
	// 兼容旧版消息记录
 | 
			
		||||
	if message.Url == "" {
 | 
			
		||||
		var msg struct {
 | 
			
		||||
			XMLName xml.Name `xml:"msg"`
 | 
			
		||||
			AppMsg  struct {
 | 
			
		||||
				Des string `xml:"des"`
 | 
			
		||||
				Url string `xml:"url"`
 | 
			
		||||
			} `xml:"appmsg"`
 | 
			
		||||
		}
 | 
			
		||||
		if err := xml.Unmarshal([]byte(xmlData), &msg); err == nil {
 | 
			
		||||
			message.Url = msg.AppMsg.Url
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return &message
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 导出交易信息
 | 
			
		||||
func extractTransaction(message *Message) Transaction {
 | 
			
		||||
	var tx = Transaction{}
 | 
			
		||||
	// 导出交易金额和备注
 | 
			
		||||
	lines := strings.Split(message.AppMsg.Des, "\n")
 | 
			
		||||
	lines := strings.Split(message.Des, "\n")
 | 
			
		||||
	for _, line := range lines {
 | 
			
		||||
		line = strings.TrimSpace(line)
 | 
			
		||||
		if len(line) == 0 {
 | 
			
		||||
@@ -59,10 +93,13 @@ func extractTransaction(message *Message) Transaction {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 解析交易 ID
 | 
			
		||||
	index := strings.Index(message.AppMsg.Url, "trans_id=")
 | 
			
		||||
	if index != -1 {
 | 
			
		||||
		end := strings.LastIndex(message.AppMsg.Url, "&")
 | 
			
		||||
		tx.TransId = strings.TrimSpace(message.AppMsg.Url[index+9 : end])
 | 
			
		||||
	parse, err := url.Parse(message.Url)
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		tx.TransId = parse.Query().Get("id")
 | 
			
		||||
		if tx.TransId == "" {
 | 
			
		||||
			tx.TransId = parse.Query().Get("trans_id")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return tx
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -38,13 +38,13 @@ func NewXXLJobExecutor(config *types.AppConfig, db *gorm.DB) *XXLJobExecutor {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e *XXLJobExecutor) Run() error {
 | 
			
		||||
	e.executor.RegTask("ClearOrder", e.ClearOrder)
 | 
			
		||||
	e.executor.RegTask("ClearOrders", e.ClearOrders)
 | 
			
		||||
	e.executor.RegTask("ResetVipCalls", e.ResetVipCalls)
 | 
			
		||||
	return e.executor.Run()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ClearOrder 清理未支付的订单,如果没有抛出异常则表示执行成功
 | 
			
		||||
func (e *XXLJobExecutor) ClearOrder(cxt context.Context, param *xxl.RunReq) (msg string) {
 | 
			
		||||
// ClearOrders 清理未支付的订单,如果没有抛出异常则表示执行成功
 | 
			
		||||
func (e *XXLJobExecutor) ClearOrders(cxt context.Context, param *xxl.RunReq) (msg string) {
 | 
			
		||||
	logger.Debug("执行清理未支付订单...")
 | 
			
		||||
	var sysConfig model.Config
 | 
			
		||||
	res := e.db.Where("marker", "system").First(&sysConfig)
 | 
			
		||||
@@ -63,7 +63,8 @@ func (e *XXLJobExecutor) ClearOrder(cxt context.Context, param *xxl.RunReq) (msg
 | 
			
		||||
	}
 | 
			
		||||
	timeout := time.Now().Unix() - int64(config.OrderPayTimeout)
 | 
			
		||||
	start := utils.Stamp2str(timeout)
 | 
			
		||||
	res = e.db.Where("status != ? AND created_at < ?", types.OrderPaidSuccess, start).Delete(&model.Order{})
 | 
			
		||||
	// 这里不是用软删除,而是永久删除订单
 | 
			
		||||
	res = e.db.Unscoped().Where("status != ? AND created_at < ?", types.OrderPaidSuccess, start).Delete(&model.Order{})
 | 
			
		||||
	return fmt.Sprintf("Clear order successfully, affect rows: %d", res.RowsAffected)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -95,35 +96,44 @@ func (e *XXLJobExecutor) ResetVipCalls(cxt context.Context, param *xxl.RunReq) (
 | 
			
		||||
	for _, u := range users {
 | 
			
		||||
		// 账号到期,直接清零
 | 
			
		||||
		if u.ExpiredTime <= currentTime.Unix() {
 | 
			
		||||
			logger.Info("账号过期:", u.Mobile)
 | 
			
		||||
			logger.Info("账号过期:", u.Username)
 | 
			
		||||
			u.Calls = 0
 | 
			
		||||
			u.Vip = false
 | 
			
		||||
		} else {
 | 
			
		||||
			if u.Calls <= 0 {
 | 
			
		||||
				u.Calls = config.VipMonthCalls
 | 
			
		||||
			} else {
 | 
			
		||||
				// 如果该用户当月有充值点卡,则将点卡中未用完的点数结余到下个月
 | 
			
		||||
				var orders []model.Order
 | 
			
		||||
				e.db.Debug().Where("user_id = ? AND pay_time > ?", u.Id, firstOfMonth).Find(&orders)
 | 
			
		||||
				var calls = 0
 | 
			
		||||
				for _, o := range orders {
 | 
			
		||||
					var remark types.OrderRemark
 | 
			
		||||
					err = utils.JsonDecode(o.Remark, &remark)
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						continue
 | 
			
		||||
					}
 | 
			
		||||
					if remark.Days > 0 { // 会员续费
 | 
			
		||||
						continue
 | 
			
		||||
					}
 | 
			
		||||
					calls += remark.Calls
 | 
			
		||||
				}
 | 
			
		||||
				if u.Calls > calls { // 本月套餐没有用完
 | 
			
		||||
					u.Calls = calls + config.VipMonthCalls
 | 
			
		||||
				} else {
 | 
			
		||||
					u.Calls = u.Calls + config.VipMonthCalls
 | 
			
		||||
				}
 | 
			
		||||
				logger.Infof("%s 点卡结余:%d", u.Mobile, calls)
 | 
			
		||||
				u.Calls = 0
 | 
			
		||||
			}
 | 
			
		||||
			if u.ImgCalls <= 0 {
 | 
			
		||||
				u.ImgCalls = 0
 | 
			
		||||
			}
 | 
			
		||||
			// 如果该用户当月有充值点卡,则将点卡中未用完的点数结余到下个月
 | 
			
		||||
			var orders []model.Order
 | 
			
		||||
			e.db.Debug().Where("user_id = ? AND pay_time > ?", u.Id, firstOfMonth).Find(&orders)
 | 
			
		||||
			var calls = 0
 | 
			
		||||
			var imgCalls = 0
 | 
			
		||||
			for _, o := range orders {
 | 
			
		||||
				var remark types.OrderRemark
 | 
			
		||||
				err = utils.JsonDecode(o.Remark, &remark)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				if remark.Days > 0 { // 会员续费
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				calls += remark.Calls
 | 
			
		||||
				imgCalls += remark.ImgCalls
 | 
			
		||||
			}
 | 
			
		||||
			if u.Calls > calls { // 本月套餐没有用完
 | 
			
		||||
				u.Calls = calls + config.VipMonthCalls
 | 
			
		||||
			} else {
 | 
			
		||||
				u.Calls = u.Calls + config.VipMonthCalls
 | 
			
		||||
			}
 | 
			
		||||
			if u.ImgCalls > imgCalls { // 本月套餐没有用完
 | 
			
		||||
				u.ImgCalls = imgCalls + config.VipMonthImgCalls
 | 
			
		||||
			} else {
 | 
			
		||||
				u.ImgCalls = u.ImgCalls + config.VipMonthImgCalls
 | 
			
		||||
			}
 | 
			
		||||
			logger.Infof("%s 点卡结余:%d", u.Username, calls)
 | 
			
		||||
		}
 | 
			
		||||
		u.Tokens = 0
 | 
			
		||||
		// update user
 | 
			
		||||
 
 | 
			
		||||
@@ -1 +0,0 @@
 | 
			
		||||
hello, world!
 | 
			
		||||
@@ -1,96 +0,0 @@
 | 
			
		||||
package store
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"github.com/syndtr/goleveldb/leveldb"
 | 
			
		||||
	"github.com/syndtr/goleveldb/leveldb/util"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type LevelDB struct {
 | 
			
		||||
	driver *leveldb.DB
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewLevelDB() (*LevelDB, error) {
 | 
			
		||||
	db, err := leveldb.OpenFile("data/leveldb", nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return &LevelDB{
 | 
			
		||||
		driver: db,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (db *LevelDB) Put(key string, value interface{}) error {
 | 
			
		||||
	bytes, err := json.Marshal(value)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return db.driver.Put([]byte(key), bytes, nil)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (db *LevelDB) Get(key string, value interface{}) error {
 | 
			
		||||
	bytes, err := db.driver.Get([]byte(key), nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return json.Unmarshal(bytes, &value)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (db *LevelDB) Search(prefix string) []string {
 | 
			
		||||
	var items = make([]string, 0)
 | 
			
		||||
	iter := db.driver.NewIterator(util.BytesPrefix([]byte(prefix)), nil)
 | 
			
		||||
	defer iter.Release()
 | 
			
		||||
 | 
			
		||||
	for iter.Next() {
 | 
			
		||||
		items = append(items, string(iter.Value()))
 | 
			
		||||
	}
 | 
			
		||||
	return items
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (db *LevelDB) SearchPage(prefix string, page int, pageSize int) *vo.Page {
 | 
			
		||||
	var items = make([]string, 0)
 | 
			
		||||
	iter := db.driver.NewIterator(util.BytesPrefix([]byte(prefix)), nil)
 | 
			
		||||
	defer iter.Release()
 | 
			
		||||
 | 
			
		||||
	res := &vo.Page{Page: page, PageSize: pageSize}
 | 
			
		||||
	// 计算数据总数和总页数
 | 
			
		||||
	total := 0
 | 
			
		||||
	for iter.Next() {
 | 
			
		||||
		total++
 | 
			
		||||
	}
 | 
			
		||||
	res.TotalPage = (total + pageSize - 1) / pageSize
 | 
			
		||||
	res.Total = int64(total)
 | 
			
		||||
 | 
			
		||||
	// 计算目标页码的起始和结束位置
 | 
			
		||||
	start := (page - 1) * pageSize
 | 
			
		||||
	if start > total {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	end := start + pageSize
 | 
			
		||||
	if end > total {
 | 
			
		||||
		end = total
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 跳转到目标页码的起始位置
 | 
			
		||||
	count := 0
 | 
			
		||||
	for iter.Next() {
 | 
			
		||||
		if count >= start {
 | 
			
		||||
			items = append(items, string(iter.Value()))
 | 
			
		||||
		}
 | 
			
		||||
		count++
 | 
			
		||||
	}
 | 
			
		||||
	iter.Release()
 | 
			
		||||
	res.Items = items
 | 
			
		||||
	return res
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (db *LevelDB) Delete(key string) error {
 | 
			
		||||
	return db.driver.Delete([]byte(key), nil)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Close release resources
 | 
			
		||||
func (db *LevelDB) Close() error {
 | 
			
		||||
	return db.driver.Close()
 | 
			
		||||
}
 | 
			
		||||
@@ -4,6 +4,11 @@ package model
 | 
			
		||||
type ApiKey struct {
 | 
			
		||||
	BaseModel
 | 
			
		||||
	Platform   string
 | 
			
		||||
	Name       string
 | 
			
		||||
	Type       string // 用途 chat => 聊天,img => 绘图
 | 
			
		||||
	Value      string // API Key 的值
 | 
			
		||||
	ApiURL     string // 当前 KEY 的 API 地址
 | 
			
		||||
	Enabled    bool   // 是否启用
 | 
			
		||||
	UseProxy   bool   // 是否使用代理访问 API URL
 | 
			
		||||
	LastUsedAt int64  // 最后使用时间
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,5 +7,6 @@ type ChatModel struct {
 | 
			
		||||
	Value    string // API Key 的值
 | 
			
		||||
	SortNum  int
 | 
			
		||||
	Enabled  bool
 | 
			
		||||
	Weight   int // 对话权重,每次对话扣减多少次对话额度
 | 
			
		||||
	Weight   int  // 对话权重,每次对话扣减多少次对话额度
 | 
			
		||||
	Open     bool // 是否开放模型给所有人使用
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										13
									
								
								api/store/model/file.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								api/store/model/file.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
import "time"
 | 
			
		||||
 | 
			
		||||
type File struct {
 | 
			
		||||
	Id        uint `gorm:"primarykey;column:id"`
 | 
			
		||||
	UserId    uint
 | 
			
		||||
	Name      string
 | 
			
		||||
	URL       string
 | 
			
		||||
	Ext       string
 | 
			
		||||
	Size      int64
 | 
			
		||||
	CreatedAt time.Time
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										12
									
								
								api/store/model/function.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								api/store/model/function.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
type Function struct {
 | 
			
		||||
	Id          uint `gorm:"primarykey;column:id"`
 | 
			
		||||
	Name        string
 | 
			
		||||
	Label       string
 | 
			
		||||
	Description string
 | 
			
		||||
	Parameters  string
 | 
			
		||||
	Action      string
 | 
			
		||||
	Token       string
 | 
			
		||||
	Enabled     bool
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										12
									
								
								api/store/model/invite_code.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								api/store/model/invite_code.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
import "time"
 | 
			
		||||
 | 
			
		||||
type InviteCode struct {
 | 
			
		||||
	Id        uint `gorm:"primarykey;column:id"`
 | 
			
		||||
	UserId    uint
 | 
			
		||||
	Code      string
 | 
			
		||||
	Hits      int // 点击次数
 | 
			
		||||
	RegNum    int // 注册人数
 | 
			
		||||
	CreatedAt time.Time
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										15
									
								
								api/store/model/invite_log.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								api/store/model/invite_log.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type InviteLog struct {
 | 
			
		||||
	Id         uint `gorm:"primarykey;column:id"`
 | 
			
		||||
	InviterId  uint
 | 
			
		||||
	UserId     uint
 | 
			
		||||
	Username   string
 | 
			
		||||
	InviteCode string
 | 
			
		||||
	Reward     string `gorm:"column:reward_json"` // 邀请奖励
 | 
			
		||||
	CreatedAt  time.Time
 | 
			
		||||
}
 | 
			
		||||
@@ -6,13 +6,17 @@ type MidJourneyJob struct {
 | 
			
		||||
	Id          uint `gorm:"primarykey;column:id"`
 | 
			
		||||
	Type        string
 | 
			
		||||
	UserId      int
 | 
			
		||||
	TaskId      string
 | 
			
		||||
	ChannelId   string
 | 
			
		||||
	MessageId   string
 | 
			
		||||
	ReferenceId string
 | 
			
		||||
	ImgURL      string
 | 
			
		||||
	OrgURL      string // 原图地址
 | 
			
		||||
	Hash        string // message hash
 | 
			
		||||
	Progress    int
 | 
			
		||||
	Prompt      string
 | 
			
		||||
	Started     bool
 | 
			
		||||
	UseProxy    bool // 是否使用反代加载图片
 | 
			
		||||
	Publish     bool //是否发布图片到画廊
 | 
			
		||||
	CreatedAt   time.Time
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -10,8 +10,9 @@ type Order struct {
 | 
			
		||||
	BaseModel
 | 
			
		||||
	UserId    uint
 | 
			
		||||
	ProductId uint
 | 
			
		||||
	Mobile    string
 | 
			
		||||
	Username  string
 | 
			
		||||
	OrderNo   string
 | 
			
		||||
	TradeNo   string
 | 
			
		||||
	Subject   string
 | 
			
		||||
	Amount    float64
 | 
			
		||||
	Status    types.OrderStatus
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ type Product struct {
 | 
			
		||||
	Discount float64
 | 
			
		||||
	Days     int
 | 
			
		||||
	Calls    int
 | 
			
		||||
	ImgCalls int
 | 
			
		||||
	Enabled  bool
 | 
			
		||||
	Sales    int
 | 
			
		||||
	SortNum  int
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user