mirror of
https://github.com/yangjian102621/geekai.git
synced 2025-11-02 23:33:47 +08:00
Compare commits
147 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
825a1b1027 | ||
|
|
950e7d1b00 | ||
|
|
d8f9f48278 | ||
|
|
6a066f1b8e | ||
|
|
b7d137247a | ||
|
|
4373642ebd | ||
|
|
5bf90920f5 | ||
|
|
edcbb3e226 | ||
|
|
5213bdf08b | ||
|
|
cf817fd8ea | ||
|
|
a2481ff1cf | ||
|
|
bc7d06d3e5 | ||
|
|
8e81dfa12a | ||
|
|
0ff76f0f21 | ||
|
|
787caa84c8 | ||
|
|
c2503e663a | ||
|
|
405a88862b | ||
|
|
296eabe09a | ||
|
|
54b45ec2ff | ||
|
|
c434f85045 | ||
|
|
4d10279870 | ||
|
|
9de9489673 | ||
|
|
9814fec930 | ||
|
|
53ba731159 | ||
|
|
2081d3ce29 | ||
|
|
1fe1e40a43 | ||
|
|
ad6e2dd370 | ||
|
|
bb63f23414 | ||
|
|
43f6bf74f2 | ||
|
|
662d7b099e | ||
|
|
d5eeeea764 | ||
|
|
43c507c597 | ||
|
|
e356771049 | ||
|
|
48139290ed | ||
|
|
bd852c82b7 | ||
|
|
13564993d7 | ||
|
|
bfc1e1bc2c | ||
|
|
ba20717a09 | ||
|
|
52e40daf23 | ||
|
|
430a7b2297 | ||
|
|
c91a38a882 | ||
|
|
6e02bee4b7 | ||
|
|
b62218110e | ||
|
|
e2960b2607 | ||
|
|
88e7c39066 | ||
|
|
2a6dd636fa | ||
|
|
6bf38f78d5 | ||
|
|
5a04a935be | ||
|
|
8923e938d2 | ||
|
|
1a1734abf0 | ||
|
|
8093a3eeb2 | ||
|
|
9edb3d0a82 | ||
|
|
d95fab11be | ||
|
|
6ef09c8ad5 | ||
|
|
283a023a06 | ||
|
|
d315edef5f | ||
|
|
5fa17b300e | ||
|
|
32919de7a7 | ||
|
|
7d126aab41 | ||
|
|
16ac57ced3 | ||
|
|
4976b967e7 | ||
|
|
e874178782 | ||
|
|
8cb66ad01b | ||
|
|
f887a39912 | ||
|
|
2beffd3dd3 | ||
|
|
d8cb92d8d4 | ||
|
|
158db83965 | ||
|
|
603bfa7def | ||
|
|
829fb879a6 | ||
|
|
0385e60ce1 | ||
|
|
aaea23f785 | ||
|
|
131efd6ba5 | ||
|
|
866564370d | ||
|
|
dcdc0d8918 | ||
|
|
6c7fa17e50 | ||
|
|
38a0d00142 | ||
|
|
5c77e67b0f | ||
|
|
961cee5e41 | ||
|
|
c9cc93be8c | ||
|
|
49f2e1a71e | ||
|
|
97eff6085a | ||
|
|
8b2e2d61af | ||
|
|
c096efb416 | ||
|
|
cdaf6fb9dc | ||
|
|
78f443ed6d | ||
|
|
54e8d72b10 | ||
|
|
05161f48fd | ||
|
|
e971bf6b88 | ||
|
|
55b979784c | ||
|
|
97aa922b5f | ||
|
|
11c760a4e8 | ||
|
|
87b03332d9 | ||
|
|
8b14eeadf4 | ||
|
|
e0ead127e0 | ||
|
|
0887bcdee0 | ||
|
|
67d83041d7 | ||
|
|
1350f388f0 | ||
|
|
65dde9e69d | ||
|
|
2e5bd238b7 | ||
|
|
8fc8fd6cba | ||
|
|
dfc6c87250 | ||
|
|
b63e01225e | ||
|
|
561b82027a | ||
|
|
f6d8fbf570 | ||
|
|
568201ebbb | ||
|
|
ab421f2185 | ||
|
|
f71a2f5263 | ||
|
|
d000cc5a67 | ||
|
|
04d6ba0853 | ||
|
|
8d7c028ca8 | ||
|
|
3ae7ebfeaf | ||
|
|
aa42d38387 | ||
|
|
43843b92f2 | ||
|
|
5da879600a | ||
|
|
87ed2064e3 | ||
|
|
34e96e91d4 | ||
|
|
8c4c2b89ce | ||
|
|
373021c191 | ||
|
|
740c3c1b00 | ||
|
|
67c7132e6b | ||
|
|
c77843424b | ||
|
|
2d4959aa7d | ||
|
|
167c59a159 | ||
|
|
1d0006ce59 | ||
|
|
6a8b4ee2f1 | ||
|
|
72b1515b68 | ||
|
|
3f0252b498 | ||
|
|
1d9d487f0e | ||
|
|
96f1126d02 | ||
|
|
7f9b8d8246 | ||
|
|
5132d52a44 | ||
|
|
1bcbf74883 | ||
|
|
abdf5298fe | ||
|
|
2129f7a8b7 | ||
|
|
f6f8748521 | ||
|
|
59301df073 | ||
|
|
e17dcf4d5f | ||
|
|
09f44e6d9b | ||
|
|
59824bffc5 | ||
|
|
cb0dacd5e0 | ||
|
|
7463cfc66c | ||
|
|
b248560ba2 | ||
|
|
37368fe13f | ||
|
|
246b023624 | ||
|
|
a6b9f57a50 | ||
|
|
42bc23cacf | ||
|
|
282f55c7a3 |
65
CHANGELOG.md
65
CHANGELOG.md
@@ -1,4 +1,69 @@
|
||||
# 更新日志
|
||||
## v4.1.6
|
||||
* 功能新增:**支持OpenAI实时语音对话功能** :rocket: :rocket: :rocket:, Beta 版,目前没有做算力计费控制,目前只有 VIP 用户可以使用。
|
||||
* 功能优化:优化MysQL容器配置文档,解决MysQL容器资源占用过高问题
|
||||
* 功能新增:管理后台增加AI绘图任务管理,可在管理后台浏览和删除用户的绘图任务
|
||||
* 功能新增:管理后台增加Suno和Luma任务管理功能
|
||||
* Bug修复:修复管理后台删除兑换码报 404 错误
|
||||
* 功能优化:优化充值产品定价逻辑,可以设置原价和优惠价,**升级当前版本之后请务必要到管理后台去重新设置一下产品价格,以免造成损失!!!**,**升级当前版本之后请务必要到管理后台去重新设置一下产品价格,以免造成损失!!!**,**升级当前版本之后请务必要到管理后台去重新设置一下产品价格,以免造成损失!!!**。
|
||||
|
||||
## v4.1.5
|
||||
* 功能优化:重构 websocket 组件,减少 websocket 连接数,全站共享一个 websocket 连接
|
||||
* Bug修复:兼容手机端原生微信支付和支付宝支付渠道
|
||||
* Bug修复:修复删除绘图任务时候因为字段长度过短导致SQL执行失败问题
|
||||
* 功能优化:优化 Vue 组件通信代码,使用共享数据来替换之前的事件订阅模式,效率更高一些
|
||||
* 功能优化:优化思维导图生成功果页面,优化用户体验
|
||||
|
||||
## v4.1.4
|
||||
* 功能优化:用户文件列表组件增加分页功能支持
|
||||
* Bug修复:修复用户注册失败Bug,注册操作只弹出一次行为验证码
|
||||
* 功能优化:首次登录不需要验证码,直接登录,登录失败之后才弹出验证码
|
||||
* 功能新增:给 AI 应用(角色)增加分类,前端支持分类筛选
|
||||
* 功能优化:允许用户在聊天页面设置是否使用流式输出或者一次性输出,兼容 GPT-O1 模型。
|
||||
* 功能优化:移除PayJS支付渠道支持,PayJs已经关闭注册服务,请使用其他支付方式。
|
||||
* 功能新增:新增GeeK易支付支付渠道,支持支付宝,微信支付,QQ钱包,京东支付,抖音支付,Paypal支付等支付方式
|
||||
* Bug修复:修复注册页面 tab 组件没有自动选中问题 [#6](https://github.com/yangjian102621/geekai-plus/issues/6)
|
||||
* 功能优化:Luma生成视频任务增加自动翻译功能
|
||||
* Bug修复:Suno 和 Luma 任务没有判断用户算力
|
||||
* 功能新增:邮箱注册增加邮箱后缀白名单,防止使用某些垃圾邮箱注册薅羊毛
|
||||
* 功能优化:清空未支付订单时,只清空超过15分钟未支付的订单
|
||||
|
||||
## v4.1.3
|
||||
* 功能优化:重构用户登录模块,给所有的登录组件增加行为验证码功能,支持用户绑定手机,邮箱和微信
|
||||
* 功能优化:重构找回密码模块,支持通过手机或者邮箱找回密码
|
||||
* 功能优化:管理后台给可以拖动排序的组件添加拖动图标
|
||||
* 功能优化:Suno 支持合成完整歌曲,和上传自己的音乐作品进行二次创作
|
||||
* Bug修复:手机端角色和模型选择不生效
|
||||
* Bug修复:用户登录过期之后聊天页面出现大量报错,需要刷新页面才能正常
|
||||
* 功能优化:优化聊天页面 Websocket 断线重连代码,提高用户体验
|
||||
* 功能优化:给算力增减服务全部加上数据库事务和同步锁
|
||||
* 功能优化:支持用户在前端对话界面选择插件
|
||||
* 功能新增:支持 Luma 文生视频功能
|
||||
|
||||
## v4.1.2
|
||||
* Bug修复:修复思维导图页面获取模型失败的问题
|
||||
* 功能优化:优化MJ,SD,DALL-E 任务列表页面,显示失败任务的错误信息,删除失败任务可以恢复扣减算力
|
||||
* Bug修复:修复后台拖动排序组件 Bug
|
||||
* 功能优化:更新数据库失败时候显示具体的的报错信息
|
||||
* Bug修复:修复管理后台对话详情页内容显示异常问题
|
||||
* 功能优化:管理后台新增清空所有未支付订单的功能
|
||||
* 功能优化:给会话信息和系统配置数据加上缓存功能,减少 http 请求
|
||||
* 功能新增:移除微信机器人收款功能,增加卡密功能,支持用户使用卡密兑换算力
|
||||
|
||||
## v4.1.1
|
||||
* Bug修复:修复 GPT 模型 function call 调用后没有输出的问题
|
||||
* 功能新增:允许获取 License 授权用户可以自定义版权信息
|
||||
* 功能新增:聊天对话框支持粘贴剪切板内容来上传截图和文件
|
||||
* 功能优化:增加 session 和系统配置缓存,确保每个页面只进行一次 session 和 get system config 请求
|
||||
* 功能优化:在应用列表页面,无需先添加模型到用户工作区,可以直接使用
|
||||
* 功能新增:MJ 绘图失败的任务不会自动删除,而是会在列表页显示失败详细错误信息
|
||||
* 功能新增:允许在设置首页纯色背景,背景图片,随机背景图片三种背景模式
|
||||
* 功能新增:允许在管理后台设置首页显示的导航菜单
|
||||
* Bug修复:修复注册页面先显示关闭注册组件,然后再显示注册组件
|
||||
* 功能新增:增加 Suno 文生歌曲功能
|
||||
* 功能优化:移除多平台模型支持,统一使用 one-api 接口形式,其他平台的模型需要通过 one-api 接口添加
|
||||
* 功能优化:在所有列表页面增加返回顶部按钮
|
||||
|
||||
## v4.1.0
|
||||
* bug修复:修复移动端修改聊天标题不生效的问题
|
||||
* Bug修复:修复用户注册不显示用户名的问题
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
# GeekAI
|
||||
> 根据[《生成式人工智能服务管理暂行办法》](https://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区公众提供一切未经备案的生成式人工智能服务。
|
||||
|
||||
**GeekAI** 基于 AI 大语言模型 API 实现的 AI 助手全套开源解决方案,自带运营管理后台,开箱即用。集成了 OpenAI, Azure,
|
||||
ChatGLM,讯飞星火,文心一言等多个平台的大语言模型。集成了 MidJourney 和 Stable Diffusion AI绘画功能。
|
||||
**GeekAI** 基于 AI 大语言模型 API 实现的 AI 助手全套开源解决方案,自带运营管理后台,开箱即用。集成了 OpenAI, Claude, 通义千问,Kimi,DeepSeek,Gitee AI 等多个平台的大语言模型。集成了 MidJourney 和 Stable Diffusion AI绘画功能。
|
||||
|
||||
主要特性:
|
||||
|
||||
- 完整的开源系统,前端应用和后台管理系统皆可开箱即用。
|
||||
- 基于 Websocket 实现,完美的打字机体验。
|
||||
- 内置了各种预训练好的角色应用,比如小红书写手,英语翻译大师,苏格拉底,孔子,乔布斯,周报助手等。轻松满足你的各种聊天和应用需求。
|
||||
- 支持 OPenAI,Azure,文心一言,讯飞星火,清华 ChatGLM等多个大语言模型。
|
||||
- 支持 OpenAI, Claude, 通义千问,Kimi,DeepSeek等多个大语言模型,**支持 Gitee AI Serverless 大模型 API**。
|
||||
- 支持 Suno 文生音乐
|
||||
- 支持 MidJourney / Stable Diffusion AI 绘画集成,文生图,图生图,换脸,融图。开箱即用。
|
||||
- 支持使用个人微信二维码作为充值收费的支付渠道,无需企业支付通道。
|
||||
|
||||
@@ -3,8 +3,6 @@ ProxyURL = "" # 如 http://127.0.0.1:7777
|
||||
MysqlDns = "root:12345678@tcp(172.22.11.200:3307)/chatgpt_plus?charset=utf8mb4&collation=utf8mb4_unicode_ci&parseTime=True&loc=Local"
|
||||
StaticDir = "./static" # 静态资源的目录
|
||||
StaticUrl = "/static" # 静态资源访问 URL
|
||||
AesEncryptKey = ""
|
||||
WeChatBot = false
|
||||
TikaHost = "http://tika:9998"
|
||||
|
||||
[Session]
|
||||
@@ -18,7 +16,7 @@ TikaHost = "http://tika:9998"
|
||||
DB = 0
|
||||
|
||||
[ApiConfig] # 微博热搜,今日头条等函数服务 API 配置,此为第三方插件服务,如需使用请联系作者开通
|
||||
ApiURL = ""
|
||||
ApiURL = "https://sapi.geekai.me"
|
||||
AppId = ""
|
||||
Token = ""
|
||||
|
||||
@@ -65,23 +63,6 @@ TikaHost = "http://tika:9998"
|
||||
SubDir = ""
|
||||
Domain = ""
|
||||
|
||||
[[MjProxyConfigs]]
|
||||
Enabled = true
|
||||
ApiURL = "http://midjourney-proxy:8082"
|
||||
ApiKey = "sk-geekmaster"
|
||||
|
||||
[[MjPlusConfigs]]
|
||||
Enabled = false
|
||||
ApiURL = "https://api.chat-plus.net"
|
||||
Mode = "fast" # MJ 绘画模式,可选值 relax/fast/turbo
|
||||
ApiKey = "sk-xxx"
|
||||
|
||||
[[SdConfigs]]
|
||||
Enabled = false
|
||||
ApiURL = ""
|
||||
ApiKey = ""
|
||||
Txt2ImgJsonPath = "res/sd/text2img.json"
|
||||
|
||||
[XXLConfig] # xxl-job 配置,需要你部署 XXL-JOB 定时任务工具,用来定期清理未支付订单和清理过期 VIP,如果你没有启用支付服务,则该服务也无需启动
|
||||
Enabled = false # 是否启用 XXL JOB 服务
|
||||
ServerAddr = "http://172.22.11.47:8080/xxl-job-admin" # xxl-job-admin 管理地址
|
||||
@@ -90,6 +71,15 @@ TikaHost = "http://tika:9998"
|
||||
AccessToken = "xxl-job-api-token" # 执行器 API 通信 token
|
||||
RegistryKey = "chatgpt-plus" # 任务注册 key
|
||||
|
||||
[SmtpConfig] # 注意,阿里云服务器禁用了25号端口,请使用 465 端口,并开启 TLS 连接
|
||||
UseTls = false
|
||||
Host = "smtp.163.com"
|
||||
Port = 25
|
||||
AppName = "极客学长"
|
||||
From = "test@163.com" # 发件邮箱人地址
|
||||
Password = "" #邮箱 stmp 服务授权码
|
||||
|
||||
# 支付宝商户支付
|
||||
[AlipayConfig]
|
||||
Enabled = false # 启用支付宝支付通道
|
||||
SandBox = false # 是否启用沙盒模式
|
||||
@@ -99,31 +89,13 @@ TikaHost = "http://tika:9998"
|
||||
PublicKey = "certs/alipay/appPublicCert.crt" # 应用公钥证书
|
||||
AlipayPublicKey = "certs/alipay/alipayPublicCert.crt" # 支付宝公钥证书
|
||||
RootCert = "certs/alipay/alipayRootCert.crt" # 支付宝根证书
|
||||
NotifyURL = "https://ai.r9it.com/api/payment/alipay/notify" # 支付异步回调地址
|
||||
|
||||
# 虎皮椒支付
|
||||
[HuPiPayConfig]
|
||||
Enabled = false
|
||||
Name = "wechat"
|
||||
AppId = ""
|
||||
AppSecret = ""
|
||||
ApiURL = "https://api.xunhupay.com"
|
||||
NotifyURL = "https://ai.r9it.com/api/payment/hupipay/notify"
|
||||
|
||||
[SmtpConfig] # 注意,阿里云服务器禁用了25号端口,请使用 465 端口,并开启 TLS 连接
|
||||
UseTls = false
|
||||
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"
|
||||
NotifyURL = "https://ai.r9it.com/api/payment/payjs/notify" # 异步回调地址,域名改成你自己的
|
||||
|
||||
# 微信商户支付
|
||||
[WechatPayConfig]
|
||||
@@ -133,6 +105,11 @@ TikaHost = "http://tika:9998"
|
||||
SerialNo = "" # API 证书序列号
|
||||
PrivateKey = "certs/alipay/privateKey.txt" # API 证书私钥文件路径,跟支付宝一样,把私钥文件拷贝到对应的路径,证书路径要映射到容器内
|
||||
ApiV3Key = "" # APIV3 私钥,这个是你自己在微信支付平台设置的
|
||||
NotifyURL = "https://ai.r9it.com/api/payment/wechat/notify" # 支付成功异步回调地址,域名改成自己的
|
||||
ReturnURL = "" # 支付成功同步回调地址
|
||||
|
||||
# 易支付
|
||||
[GeekPayConfig]
|
||||
Enabled = true
|
||||
AppId = "" # 商户ID
|
||||
PrivateKey = "" # 商户私钥
|
||||
ApiURL = "https://pay.geekai.cn"
|
||||
Methods = ["alipay", "wxpay", "qqpay", "jdpay", "douyin", "paypal"] # 支持的支付方式
|
||||
|
||||
@@ -49,10 +49,7 @@ func NewServer(appConfig *types.AppConfig) *AppServer {
|
||||
}
|
||||
|
||||
func (s *AppServer) Init(debug bool, client *redis.Client) {
|
||||
if debug { // 调试模式允许跨域请求 API
|
||||
s.Debug = debug
|
||||
logger.Info("Enabled debug mode")
|
||||
}
|
||||
// 允许跨域请求 API
|
||||
s.Engine.Use(corsMiddleware())
|
||||
s.Engine.Use(staticResourceMiddleware())
|
||||
s.Engine.Use(authorizeMiddleware(s, client))
|
||||
@@ -65,13 +62,13 @@ func (s *AppServer) Init(debug bool, client *redis.Client) {
|
||||
func (s *AppServer) Run(db *gorm.DB) error {
|
||||
// load system configs
|
||||
var sysConfig model.Config
|
||||
res := db.Where("marker", "system").First(&sysConfig)
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
err := utils.JsonDecode(sysConfig.Config, &s.SysConfig)
|
||||
err := db.Where("marker", "system").First(&sysConfig).Error
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to load system config: %v", err)
|
||||
}
|
||||
err = utils.JsonDecode(sysConfig.Config, &s.SysConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode system config: %v", err)
|
||||
}
|
||||
logger.Infof("http://%s", s.Config.Listen)
|
||||
return s.Engine.Run(s.Config.Listen)
|
||||
@@ -83,7 +80,7 @@ func errorHandler(c *gin.Context) {
|
||||
if r := recover(); r != nil {
|
||||
logger.Errorf("Handler Panic: %v", r)
|
||||
debug.PrintStack()
|
||||
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: types.ErrorMsg})
|
||||
c.JSON(http.StatusBadRequest, types.BizVo{Code: types.Failed, Message: types.ErrorMsg})
|
||||
c.Abort()
|
||||
}
|
||||
}()
|
||||
@@ -101,9 +98,9 @@ func corsMiddleware() gin.HandlerFunc {
|
||||
c.Header("Access-Control-Allow-Origin", origin)
|
||||
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
|
||||
//允许跨域设置可以返回其他子段,可以自定义字段
|
||||
c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, Content-Type, Chat-Token, Admin-Authorization")
|
||||
c.Header("Access-Control-Allow-Headers", "Authorization, Body-Length, Body-Type, Admin-Authorization,content-type")
|
||||
// 允许浏览器(客户端)可以解析的头部 (重要)
|
||||
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers")
|
||||
c.Header("Access-Control-Expose-Headers", "Body-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers")
|
||||
//设置缓存时间
|
||||
c.Header("Access-Control-Max-Age", "172800")
|
||||
//允许客户端传递校验信息比如 cookie (重要)
|
||||
@@ -127,19 +124,26 @@ func corsMiddleware() gin.HandlerFunc {
|
||||
// 用户授权验证
|
||||
func authorizeMiddleware(s *AppServer, client *redis.Client) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
clientProtocols := c.GetHeader("Sec-WebSocket-Protocol")
|
||||
var tokenString string
|
||||
isAdminApi := strings.Contains(c.Request.URL.Path, "/api/admin/")
|
||||
if isAdminApi { // 后台管理 API
|
||||
tokenString = c.GetHeader(types.AdminAuthHeader)
|
||||
} else if c.Request.URL.Path == "/api/chat/new" {
|
||||
tokenString = c.Query("token")
|
||||
} else if clientProtocols != "" { // Websocket 连接
|
||||
// 解析子协议内容
|
||||
protocols := strings.Split(clientProtocols, ",")
|
||||
if protocols[0] == "realtime" {
|
||||
tokenString = strings.TrimSpace(protocols[1][25:])
|
||||
} else if protocols[0] == "token" {
|
||||
tokenString = strings.TrimSpace(protocols[1])
|
||||
}
|
||||
} else {
|
||||
tokenString = c.GetHeader(types.UserAuthHeader)
|
||||
}
|
||||
|
||||
if tokenString == "" {
|
||||
if needLogin(c) {
|
||||
resp.ERROR(c, "You should put Authorization in request headers")
|
||||
resp.NotAuth(c, "You should put Authorization in request headers")
|
||||
c.Abort()
|
||||
return
|
||||
} else { // 直接放行
|
||||
@@ -201,30 +205,28 @@ func needLogin(c *gin.Context) bool {
|
||||
c.Request.URL.Path == "/api/admin/logout" ||
|
||||
c.Request.URL.Path == "/api/admin/login/captcha" ||
|
||||
c.Request.URL.Path == "/api/user/register" ||
|
||||
c.Request.URL.Path == "/api/user/session" ||
|
||||
c.Request.URL.Path == "/api/chat/history" ||
|
||||
c.Request.URL.Path == "/api/chat/detail" ||
|
||||
c.Request.URL.Path == "/api/chat/list" ||
|
||||
c.Request.URL.Path == "/api/role/list" ||
|
||||
c.Request.URL.Path == "/api/app/list" ||
|
||||
c.Request.URL.Path == "/api/app/type/list" ||
|
||||
c.Request.URL.Path == "/api/app/list/user" ||
|
||||
c.Request.URL.Path == "/api/model/list" ||
|
||||
c.Request.URL.Path == "/api/mj/imgWall" ||
|
||||
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/imgWall" ||
|
||||
c.Request.URL.Path == "/api/sd/client" ||
|
||||
c.Request.URL.Path == "/api/dall/imgWall" ||
|
||||
c.Request.URL.Path == "/api/dall/client" ||
|
||||
c.Request.URL.Path == "/api/product/list" ||
|
||||
c.Request.URL.Path == "/api/menu/list" ||
|
||||
c.Request.URL.Path == "/api/markMap/client" ||
|
||||
c.Request.URL.Path == "/api/payment/alipay/notify" ||
|
||||
c.Request.URL.Path == "/api/payment/hupipay/notify" ||
|
||||
c.Request.URL.Path == "/api/payment/payjs/notify" ||
|
||||
c.Request.URL.Path == "/api/payment/wechat/notify" ||
|
||||
c.Request.URL.Path == "/api/payment/doPay" ||
|
||||
c.Request.URL.Path == "/api/payment/payWays" ||
|
||||
c.Request.URL.Path == "/api/suno/detail" ||
|
||||
c.Request.URL.Path == "/api/suno/play" ||
|
||||
c.Request.URL.Path == "/api/download" ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/test") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/payment/notify/") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/user/clogin") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/config/") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/function/") ||
|
||||
@@ -364,6 +366,7 @@ func staticResourceMiddleware() gin.HandlerFunc {
|
||||
// 直接输出图像数据流
|
||||
c.Data(http.StatusOK, "image/jpeg", buffer.Bytes())
|
||||
c.Abort() // 中断请求
|
||||
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
|
||||
@@ -38,7 +38,6 @@ func NewDefaultConfig() *types.AppConfig {
|
||||
BasePath: "./static/upload",
|
||||
},
|
||||
},
|
||||
WeChatBot: false,
|
||||
AlipayConfig: types.AlipayConfig{Enabled: false, SandBox: false},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,14 @@ package types
|
||||
|
||||
// ApiRequest API 请求实体
|
||||
type ApiRequest struct {
|
||||
Model string `json:"model,omitempty"` // 兼容百度文心一言
|
||||
Temperature float32 `json:"temperature"`
|
||||
MaxTokens int `json:"max_tokens,omitempty"` // 兼容百度文心一言
|
||||
Stream bool `json:"stream"`
|
||||
Messages []interface{} `json:"messages,omitempty"`
|
||||
Prompt []interface{} `json:"prompt,omitempty"` // 兼容 ChatGLM
|
||||
Tools []Tool `json:"tools,omitempty"`
|
||||
Functions []interface{} `json:"functions,omitempty"` // 兼容中转平台
|
||||
Model string `json:"model,omitempty"`
|
||||
Temperature float32 `json:"temperature"`
|
||||
MaxTokens int `json:"max_tokens,omitempty"`
|
||||
MaxCompletionTokens int `json:"max_completion_tokens,omitempty"` // 兼容GPT O1 模型
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
Messages []interface{} `json:"messages,omitempty"`
|
||||
Tools []Tool `json:"tools,omitempty"`
|
||||
Functions []interface{} `json:"functions,omitempty"` // 兼容中转平台
|
||||
|
||||
ToolChoice string `json:"tool_choice,omitempty"`
|
||||
|
||||
@@ -52,16 +52,17 @@ type Delta struct {
|
||||
|
||||
// ChatSession 聊天会话对象
|
||||
type ChatSession struct {
|
||||
SessionId string `json:"session_id"`
|
||||
UserId uint `json:"user_id"`
|
||||
ClientIP string `json:"client_ip"` // 客户端 IP
|
||||
ChatId string `json:"chat_id"` // 客户端聊天会话 ID, 多会话模式专用字段
|
||||
Model ChatModel `json:"model"` // GPT 模型
|
||||
UserId uint `json:"user_id"`
|
||||
ClientIP string `json:"client_ip"` // 客户端 IP
|
||||
ChatId string `json:"chat_id"` // 客户端聊天会话 ID, 多会话模式专用字段
|
||||
Model ChatModel `json:"model"` // GPT 模型
|
||||
Start int64 `json:"start"` // 开始请求时间戳
|
||||
Tools []int `json:"tools"` // 工具函数列表
|
||||
Stream bool `json:"stream"` // 是否采用流式输出
|
||||
}
|
||||
|
||||
type ChatModel struct {
|
||||
Id uint `json:"id"`
|
||||
Platform string `json:"platform"`
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
Power int `json:"power"`
|
||||
@@ -91,7 +92,7 @@ const (
|
||||
PowerConsume = PowerType(2) // 消费
|
||||
PowerRefund = PowerType(3) // 任务(SD,MJ)执行失败,退款
|
||||
PowerInvite = PowerType(4) // 邀请奖励
|
||||
PowerReward = PowerType(5) // 众筹
|
||||
PowerRedeem = PowerType(5) // 众筹
|
||||
PowerGift = PowerType(6) // 系统赠送
|
||||
)
|
||||
|
||||
@@ -103,8 +104,8 @@ func (t PowerType) String() string {
|
||||
return "消费"
|
||||
case PowerRefund:
|
||||
return "退款"
|
||||
case PowerReward:
|
||||
return "众筹"
|
||||
case PowerRedeem:
|
||||
return "兑换"
|
||||
|
||||
}
|
||||
return "其他"
|
||||
|
||||
@@ -17,15 +17,17 @@ var ErrConClosed = errors.New("connection Closed")
|
||||
|
||||
// WsClient websocket client
|
||||
type WsClient struct {
|
||||
Id string
|
||||
Conn *websocket.Conn
|
||||
lock sync.Mutex
|
||||
mt int
|
||||
Closed bool
|
||||
}
|
||||
|
||||
func NewWsClient(conn *websocket.Conn) *WsClient {
|
||||
func NewWsClient(conn *websocket.Conn, id string) *WsClient {
|
||||
return &WsClient{
|
||||
Conn: conn,
|
||||
Id: id,
|
||||
lock: sync.Mutex{},
|
||||
mt: 2, // fixed bug for 'Invalid UTF-8 in text frame'
|
||||
Closed: false,
|
||||
|
||||
@@ -12,28 +12,23 @@ import (
|
||||
)
|
||||
|
||||
type AppConfig struct {
|
||||
Path string `toml:"-"`
|
||||
Listen string
|
||||
Session Session
|
||||
AdminSession Session
|
||||
ProxyURL string
|
||||
MysqlDns string // mysql 连接地址
|
||||
StaticDir string // 静态资源目录
|
||||
StaticUrl string // 静态资源 URL
|
||||
Redis RedisConfig // redis 连接信息
|
||||
ApiConfig ApiConfig // ChatPlus API authorization configs
|
||||
SMS SMSConfig // send mobile message config
|
||||
OSS OSSConfig // OSS config
|
||||
MjProxyConfigs []MjProxyConfig // MJ proxy config
|
||||
MjPlusConfigs []MjPlusConfig // MJ plus config
|
||||
WeChatBot bool // 是否启用微信机器人
|
||||
SdConfigs []StableDiffusionConfig // sd AI draw service pool
|
||||
|
||||
Path string `toml:"-"`
|
||||
Listen string
|
||||
Session Session
|
||||
AdminSession Session
|
||||
ProxyURL string
|
||||
MysqlDns string // mysql 连接地址
|
||||
StaticDir string // 静态资源目录
|
||||
StaticUrl string // 静态资源 URL
|
||||
Redis RedisConfig // redis 连接信息
|
||||
ApiConfig ApiConfig // ChatPlus API authorization configs
|
||||
SMS SMSConfig // send mobile message config
|
||||
OSS OSSConfig // OSS config
|
||||
SmtpConfig SmtpConfig // 邮件发送配置
|
||||
XXLConfig XXLConfig
|
||||
AlipayConfig AlipayConfig // 支付宝支付渠道配置
|
||||
HuPiPayConfig HuPiPayConfig // 虎皮椒支付配置
|
||||
SmtpConfig SmtpConfig // 邮件发送配置
|
||||
JPayConfig JPayConfig // payjs 支付配置
|
||||
GeekPayConfig GeekPayConfig // GEEK 支付配置
|
||||
WechatPayConfig WechatPayConfig // 微信支付渠道配置
|
||||
TikaHost string // TiKa 服务器地址
|
||||
}
|
||||
@@ -53,27 +48,6 @@ type ApiConfig struct {
|
||||
Token string
|
||||
}
|
||||
|
||||
type MjProxyConfig struct {
|
||||
Enabled bool
|
||||
ApiURL string // api 地址
|
||||
Mode string // 绘画模式,可选值:fast/turbo/relax
|
||||
ApiKey string
|
||||
}
|
||||
|
||||
type StableDiffusionConfig struct {
|
||||
Enabled bool
|
||||
Model string // 模型名称
|
||||
ApiURL string
|
||||
ApiKey string
|
||||
}
|
||||
|
||||
type MjPlusConfig struct {
|
||||
Enabled bool // 如果启用了 MidJourney Plus,将会自动禁用原生的MidJourney服务
|
||||
ApiURL string // api 地址
|
||||
Mode string // 绘画模式,可选值:fast/turbo/relax
|
||||
ApiKey string
|
||||
}
|
||||
|
||||
type AlipayConfig struct {
|
||||
Enabled bool // 是否启用该支付通道
|
||||
SandBox bool // 是否沙盒环境
|
||||
@@ -83,8 +57,8 @@ type AlipayConfig struct {
|
||||
PublicKey string // 用户公钥文件路径
|
||||
AlipayPublicKey string // 支付宝公钥文件路径
|
||||
RootCert string // Root 秘钥路径
|
||||
NotifyURL string // 异步通知回调
|
||||
ReturnURL string // 支付成功返回地址
|
||||
NotifyURL string // 异步通知地址
|
||||
ReturnURL string // 同步通知地址
|
||||
}
|
||||
|
||||
type WechatPayConfig struct {
|
||||
@@ -94,29 +68,27 @@ type WechatPayConfig struct {
|
||||
SerialNo string // 商户证书的证书序列号
|
||||
PrivateKey string // 用户私钥文件路径
|
||||
ApiV3Key string // API V3 秘钥
|
||||
NotifyURL string // 异步通知回调
|
||||
ReturnURL string // 支付成功返回地址
|
||||
NotifyURL string // 异步通知地址
|
||||
}
|
||||
|
||||
type HuPiPayConfig struct { //虎皮椒第四方支付配置
|
||||
Enabled bool // 是否启用该支付通道
|
||||
Name string // 支付名称,如:wechat/alipay
|
||||
AppId string // App ID
|
||||
AppSecret string // app 密钥
|
||||
ApiURL string // 支付网关
|
||||
NotifyURL string // 异步通知回调
|
||||
ReturnURL string // 支付成功返回地址
|
||||
NotifyURL string // 异步通知地址
|
||||
ReturnURL string // 同步通知地址
|
||||
}
|
||||
|
||||
// JPayConfig PayJs 支付配置
|
||||
type JPayConfig struct {
|
||||
// GeekPayConfig GEEK支付配置
|
||||
type GeekPayConfig struct {
|
||||
Enabled bool
|
||||
Name string // 支付名称,默认 wechat
|
||||
AppId string // 商户 ID
|
||||
PrivateKey string // 私钥
|
||||
ApiURL string // API 网关
|
||||
NotifyURL string // 异步回调地址
|
||||
ReturnURL string // 支付成功返回地址
|
||||
AppId string // 商户 ID
|
||||
PrivateKey string // 私钥
|
||||
ApiURL string // API 网关
|
||||
NotifyURL string // 异步通知地址
|
||||
ReturnURL string // 同步通知地址
|
||||
Methods []string // 支付方式
|
||||
}
|
||||
|
||||
type XXLConfig struct { // XXL 任务调度配置
|
||||
@@ -155,50 +127,11 @@ func (c RedisConfig) Url() string {
|
||||
return fmt.Sprintf("%s:%d", c.Host, c.Port)
|
||||
}
|
||||
|
||||
type Platform struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
ChatURL string `json:"chat_url"`
|
||||
ImgURL string `json:"img_url"`
|
||||
}
|
||||
|
||||
var OpenAI = Platform{
|
||||
Name: "OpenAI - GPT",
|
||||
Value: "OpenAI",
|
||||
ChatURL: "https://api.chat-plus.net/v1/chat/completions",
|
||||
ImgURL: "https://api.chat-plus.net/v1/images/generations",
|
||||
}
|
||||
var Azure = Platform{
|
||||
Name: "微软 - Azure",
|
||||
Value: "Azure",
|
||||
ChatURL: "https://chat-bot-api.openai.azure.com/openai/deployments/{model}/chat/completions?api-version=2023-05-15",
|
||||
}
|
||||
var ChatGLM = Platform{
|
||||
Name: "智谱 - ChatGLM",
|
||||
Value: "ChatGLM",
|
||||
ChatURL: "https://open.bigmodel.cn/api/paas/v3/model-api/{model}/sse-invoke",
|
||||
}
|
||||
var Baidu = Platform{
|
||||
Name: "百度 - 文心大模型",
|
||||
Value: "Baidu",
|
||||
ChatURL: "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/{model}",
|
||||
}
|
||||
var XunFei = Platform{
|
||||
Name: "讯飞 - 星火大模型",
|
||||
Value: "XunFei",
|
||||
ChatURL: "wss://spark-api.xf-yun.com/{version}/chat",
|
||||
}
|
||||
var QWen = Platform{
|
||||
Name: "阿里 - 通义千问",
|
||||
Value: "QWen",
|
||||
ChatURL: "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation",
|
||||
}
|
||||
|
||||
type SystemConfig struct {
|
||||
Title string `json:"title,omitempty"` // 网站标题
|
||||
Slogan string `json:"slogan,omitempty"` // 网站 slogan
|
||||
AdminTitle string `json:"admin_title,omitempty"` // 管理后台标题
|
||||
Logo string `json:"logo,omitempty"`
|
||||
Title string `json:"title,omitempty"` // 网站标题
|
||||
Slogan string `json:"slogan,omitempty"` // 网站 slogan
|
||||
AdminTitle string `json:"admin_title,omitempty"` // 管理后台标题
|
||||
Logo string `json:"logo,omitempty"` // 方形 Logo
|
||||
InitPower int `json:"init_power,omitempty"` // 新用户注册赠送算力值
|
||||
DailyPower int `json:"daily_power,omitempty"` // 每日赠送算力
|
||||
InvitePower int `json:"invite_power,omitempty"` // 邀请新用户赠送算力值
|
||||
@@ -207,10 +140,6 @@ type SystemConfig struct {
|
||||
RegisterWays []string `json:"register_ways,omitempty"` // 注册方式:支持手机(mobile),邮箱注册(email),账号密码注册
|
||||
EnabledRegister bool `json:"enabled_register,omitempty"` // 是否开放注册
|
||||
|
||||
RewardImg string `json:"reward_img,omitempty"` // 众筹收款二维码地址
|
||||
EnabledReward bool `json:"enabled_reward,omitempty"` // 启用众筹功能
|
||||
PowerPrice float64 `json:"power_price,omitempty"` // 算力单价
|
||||
|
||||
OrderPayTimeout int `json:"order_pay_timeout,omitempty"` //订单支付超时时间
|
||||
VipInfoText string `json:"vip_info_text,omitempty"` // 会员页面充值说明
|
||||
DefaultModels []int `json:"default_models,omitempty"` // 默认开通的 AI 模型
|
||||
@@ -218,7 +147,9 @@ type SystemConfig struct {
|
||||
MjPower int `json:"mj_power,omitempty"` // MJ 绘画消耗算力
|
||||
MjActionPower int `json:"mj_action_power,omitempty"` // MJ 操作(放大,变换)消耗算力
|
||||
SdPower int `json:"sd_power,omitempty"` // SD 绘画消耗算力
|
||||
DallPower int `json:"dall_power,omitempty"` // DALLE3 绘图消耗算力
|
||||
DallPower int `json:"dall_power,omitempty"` // DALL-E-3 绘图消耗算力
|
||||
SunoPower int `json:"suno_power,omitempty"` // Suno 生成歌曲消耗算力
|
||||
LumaPower int `json:"luma_power,omitempty"` // Luma 生成视频消耗算力
|
||||
|
||||
WechatCardURL string `json:"wechat_card_url,omitempty"` // 微信客服地址
|
||||
|
||||
@@ -226,6 +157,13 @@ type SystemConfig struct {
|
||||
ContextDeep int `json:"context_deep,omitempty"`
|
||||
|
||||
SdNegPrompt string `json:"sd_neg_prompt"` // SD 默认反向提示词
|
||||
MjMode string `json:"mj_mode"` // midjourney 默认的API模式,relax, fast, turbo
|
||||
|
||||
IndexBgURL string `json:"index_bg_url"` // 前端首页背景图片
|
||||
IndexBgURL string `json:"index_bg_url"` // 前端首页背景图片
|
||||
IndexNavs []int `json:"index_navs"` // 首页显示的导航菜单
|
||||
Copyright string `json:"copyright"` // 版权信息
|
||||
MarkMapText string `json:"mark_map_text"` // 思维导入的默认文本
|
||||
|
||||
EnabledVerify bool `json:"enabled_verify"` // 是否启用验证码
|
||||
EmailWhiteList []string `json:"email_white_list"` // 邮箱白名单列表
|
||||
}
|
||||
|
||||
@@ -22,3 +22,18 @@ type OrderRemark struct {
|
||||
Price float64 `json:"price"`
|
||||
Discount float64 `json:"discount"`
|
||||
}
|
||||
|
||||
var PayMethods = map[string]string{
|
||||
"alipay": "支付宝商号",
|
||||
"wechat": "微信商号",
|
||||
"hupi": "虎皮椒",
|
||||
"geek": "易支付",
|
||||
}
|
||||
var PayNames = map[string]string{
|
||||
"alipay": "支付宝",
|
||||
"wxpay": "微信支付",
|
||||
"qqpay": "QQ钱包",
|
||||
"jdpay": "京东支付",
|
||||
"douyin": "抖音支付",
|
||||
"paypal": "PayPal支付",
|
||||
}
|
||||
|
||||
@@ -24,10 +24,10 @@ const (
|
||||
|
||||
// MjTask MidJourney 任务
|
||||
type MjTask struct {
|
||||
Id uint `json:"id"`
|
||||
TaskId string `json:"task_id"`
|
||||
Id uint `json:"id"` // 任务ID
|
||||
TaskId string `json:"task_id"` // 中转任务ID
|
||||
ClientId string `json:"client_id"`
|
||||
ImgArr []string `json:"img_arr"`
|
||||
ChannelId string `json:"channel_id"`
|
||||
Type TaskType `json:"type"`
|
||||
UserId int `json:"user_id"`
|
||||
Prompt string `json:"prompt,omitempty"`
|
||||
@@ -37,17 +37,21 @@ type MjTask struct {
|
||||
MessageId string `json:"message_id,omitempty"`
|
||||
MessageHash string `json:"message_hash,omitempty"`
|
||||
RetryCount int `json:"retry_count"`
|
||||
ChannelId string `json:"channel_id"` // 渠道ID,用来区分是哪个渠道创建的任务,一个任务的 create 和 action 操作必须要再同一个渠道
|
||||
Mode string `json:"mode"` // 绘画模式,relax, fast, turbo
|
||||
}
|
||||
|
||||
type SdTask struct {
|
||||
Id int `json:"id"` // job 数据库ID
|
||||
Type TaskType `json:"type"`
|
||||
ClientId string `json:"client_id"`
|
||||
UserId int `json:"user_id"`
|
||||
Params SdTaskParams `json:"params"`
|
||||
RetryCount int `json:"retry_count"`
|
||||
}
|
||||
|
||||
type SdTaskParams struct {
|
||||
ClientId string `json:"client_id"` // 客户端ID
|
||||
TaskId string `json:"task_id"`
|
||||
Prompt string `json:"prompt"` // 提示词
|
||||
NegPrompt string `json:"neg_prompt"` // 反向提示词
|
||||
@@ -68,13 +72,60 @@ type SdTaskParams struct {
|
||||
|
||||
// DallTask DALL-E task
|
||||
type DallTask struct {
|
||||
JobId uint `json:"job_id"`
|
||||
UserId uint `json:"user_id"`
|
||||
Prompt string `json:"prompt"`
|
||||
N int `json:"n"`
|
||||
Quality string `json:"quality"`
|
||||
Size string `json:"size"`
|
||||
Style string `json:"style"`
|
||||
ClientId string `json:"client_id"`
|
||||
JobId uint `json:"job_id"`
|
||||
UserId uint `json:"user_id"`
|
||||
Prompt string `json:"prompt"`
|
||||
N int `json:"n"`
|
||||
Quality string `json:"quality"`
|
||||
Size string `json:"size"`
|
||||
Style string `json:"style"`
|
||||
|
||||
Power int `json:"power"`
|
||||
}
|
||||
|
||||
type SunoTask struct {
|
||||
ClientId string `json:"client_id"`
|
||||
Id uint `json:"id"`
|
||||
Channel string `json:"channel"`
|
||||
UserId int `json:"user_id"`
|
||||
Type int `json:"type"`
|
||||
Title string `json:"title"`
|
||||
RefTaskId string `json:"ref_task_id,omitempty"`
|
||||
RefSongId string `json:"ref_song_id,omitempty"`
|
||||
Prompt string `json:"prompt"` // 提示词/歌词
|
||||
Tags string `json:"tags"`
|
||||
Model string `json:"model"`
|
||||
Instrumental bool `json:"instrumental"` // 是否纯音乐
|
||||
ExtendSecs int `json:"extend_secs,omitempty"` // 延长秒杀
|
||||
SongId string `json:"song_id,omitempty"` // 合并歌曲ID
|
||||
AudioURL string `json:"audio_url"` // 用户上传音频地址
|
||||
}
|
||||
|
||||
const (
|
||||
VideoLuma = "luma"
|
||||
VideoRunway = "runway"
|
||||
VideoCog = "cog"
|
||||
)
|
||||
|
||||
type VideoTask struct {
|
||||
ClientId string `json:"client_id"`
|
||||
Id uint `json:"id"`
|
||||
Channel string `json:"channel"`
|
||||
UserId int `json:"user_id"`
|
||||
Type string `json:"type"`
|
||||
TaskId string `json:"task_id"`
|
||||
Prompt string `json:"prompt"` // 提示词
|
||||
Params VideoParams `json:"params"`
|
||||
}
|
||||
|
||||
type VideoParams struct {
|
||||
PromptOptimize bool `json:"prompt_optimize"` // 是否优化提示词
|
||||
Loop bool `json:"loop"` // 是否循环参考图
|
||||
StartImgURL string `json:"start_img_url"` // 第一帧参考图地址
|
||||
EndImgURL string `json:"end_img_url"` // 最后一帧参考图地址
|
||||
Model string `json:"model"` // 使用哪个模型生成视频
|
||||
Radio string `json:"radio"` // 视频尺寸
|
||||
Style string `json:"style"` // 风格
|
||||
Duration int `json:"duration"` // 视频时长(秒)
|
||||
}
|
||||
|
||||
@@ -17,21 +17,48 @@ type BizVo struct {
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// WsMessage Websocket message
|
||||
type WsMessage struct {
|
||||
Type WsMsgType `json:"type"` // 消息类别,start, end, img
|
||||
Content interface{} `json:"content"`
|
||||
// ReplyMessage 对话回复消息结构
|
||||
type ReplyMessage struct {
|
||||
Channel WsChannel `json:"channel"` // 消息频道,目前只有 chat
|
||||
ClientId string `json:"clientId"` // 客户端ID
|
||||
Type WsMsgType `json:"type"` // 消息类别
|
||||
Body interface{} `json:"body"`
|
||||
}
|
||||
|
||||
type WsMsgType string
|
||||
type WsChannel string
|
||||
|
||||
const (
|
||||
WsStart = WsMsgType("start")
|
||||
WsMiddle = WsMsgType("middle")
|
||||
WsEnd = WsMsgType("end")
|
||||
WsErr = WsMsgType("error")
|
||||
MsgTypeText = WsMsgType("text") // 输出内容
|
||||
MsgTypeEnd = WsMsgType("end")
|
||||
MsgTypeErr = WsMsgType("error")
|
||||
MsgTypePing = WsMsgType("ping") // 心跳消息
|
||||
|
||||
ChPing = WsChannel("ping")
|
||||
ChChat = WsChannel("chat")
|
||||
ChMj = WsChannel("mj")
|
||||
ChSd = WsChannel("sd")
|
||||
ChDall = WsChannel("dall")
|
||||
ChSuno = WsChannel("suno")
|
||||
ChLuma = WsChannel("luma")
|
||||
)
|
||||
|
||||
// InputMessage 对话输入消息结构
|
||||
type InputMessage struct {
|
||||
Channel WsChannel `json:"channel"` // 消息频道
|
||||
Type WsMsgType `json:"type"` // 消息类别
|
||||
Body interface{} `json:"body"`
|
||||
}
|
||||
|
||||
type ChatMessage struct {
|
||||
Tools []int `json:"tools,omitempty"` // 允许调用工具列表
|
||||
Stream bool `json:"stream,omitempty"` // 是否采用流式输出
|
||||
RoleId int `json:"role_id"`
|
||||
ModelId int `json:"model_id"`
|
||||
ChatId string `json:"chat_id"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type BizCode int
|
||||
|
||||
const (
|
||||
|
||||
@@ -8,7 +8,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/eatmoreapple/openwechat v1.2.1
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/go-redis/redis/v8 v8.11.5
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0
|
||||
@@ -30,8 +29,6 @@ require (
|
||||
github.com/go-pay/gopay v1.5.101
|
||||
github.com/google/go-tika v0.3.1
|
||||
github.com/microcosm-cc/bluemonday v1.0.26
|
||||
github.com/mojocn/base64Captcha v1.3.6
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible
|
||||
github.com/shopspring/decimal v1.3.1
|
||||
github.com/syndtr/goleveldb v1.0.0
|
||||
golang.org/x/image v0.15.0
|
||||
@@ -45,9 +42,9 @@ require (
|
||||
github.com/go-pay/util v0.0.2 // indirect
|
||||
github.com/go-pay/xlog v0.0.2 // indirect
|
||||
github.com/go-pay/xtime v0.0.2 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.13 // indirect
|
||||
github.com/tklauser/numcpus v0.7.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
|
||||
14
api/go.sum
14
api/go.sum
@@ -28,8 +28,6 @@ github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0
|
||||
github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/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/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
@@ -84,8 +82,6 @@ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG
|
||||
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
@@ -157,8 +153,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mojocn/base64Captcha v1.3.6 h1:gZEKu1nsKpttuIAQgWHO+4Mhhls8cAKyiV2Ew03H+Tw=
|
||||
github.com/mojocn/base64Captcha v1.3.6/go.mod h1:i5CtHvm+oMbj1UzEPXaA8IH/xHFZ3DGY3Wh3dBpZ28E=
|
||||
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=
|
||||
@@ -220,12 +214,10 @@ github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gt
|
||||
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/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4=
|
||||
github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0=
|
||||
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
|
||||
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
|
||||
github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4=
|
||||
github.com/tklauser/numcpus v0.7.0/go.mod h1:bb6dMVcj8A42tSE7i32fsIUCbQNllK5iDguyOZRUzAY=
|
||||
github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
|
||||
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
|
||||
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=
|
||||
@@ -267,7 +259,6 @@ golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
golang.org/x/image v0.13.0/go.mod h1:6mmbMOeV28HuMTgA6OSRkdXKYw/t5W9Uwn2Yv1r3Yxk=
|
||||
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
|
||||
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
@@ -323,7 +314,6 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
logger2 "geekai/logger"
|
||||
"geekai/service"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
@@ -28,38 +29,48 @@ import (
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
// Manager 管理员
|
||||
type Manager struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Captcha string `json:"captcha"` // 验证码
|
||||
CaptchaId string `json:"captcha_id"` // 验证码id
|
||||
}
|
||||
|
||||
const SuperManagerID = 1
|
||||
|
||||
type ManagerHandler struct {
|
||||
handler.BaseHandler
|
||||
redis *redis.Client
|
||||
redis *redis.Client
|
||||
captcha *service.CaptchaService
|
||||
}
|
||||
|
||||
func NewAdminHandler(app *core.AppServer, db *gorm.DB, client *redis.Client) *ManagerHandler {
|
||||
return &ManagerHandler{BaseHandler: handler.BaseHandler{DB: db, App: app}, redis: client}
|
||||
func NewAdminHandler(app *core.AppServer, db *gorm.DB, client *redis.Client, captcha *service.CaptchaService) *ManagerHandler {
|
||||
return &ManagerHandler{
|
||||
BaseHandler: handler.BaseHandler{DB: db, App: app},
|
||||
redis: client,
|
||||
captcha: captcha,
|
||||
}
|
||||
}
|
||||
|
||||
// Login 登录
|
||||
func (h *ManagerHandler) Login(c *gin.Context) {
|
||||
var data Manager
|
||||
var data struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Key string `json:"key,omitempty"`
|
||||
Dots string `json:"dots,omitempty"`
|
||||
X int `json:"x,omitempty"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
//// add captcha
|
||||
//if !base64Captcha.DefaultMemStore.Verify(data.CaptchaId, data.Captcha, true) {
|
||||
// resp.ERROR(c, "验证码错误!")
|
||||
// return
|
||||
//}
|
||||
if h.App.SysConfig.EnabledVerify {
|
||||
var check bool
|
||||
if data.X != 0 {
|
||||
check = h.captcha.SlideCheck(data)
|
||||
} else {
|
||||
check = h.captcha.Check(data)
|
||||
}
|
||||
if !check {
|
||||
resp.ERROR(c, "请先完人机验证")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var manager model.AdminUser
|
||||
res := h.DB.Model(&model.AdminUser{}).Where("username = ?", data.Username).First(&manager)
|
||||
|
||||
@@ -8,6 +8,7 @@ package admin
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
@@ -31,7 +32,6 @@ func NewApiKeyHandler(app *core.AppServer, db *gorm.DB) *ApiKeyHandler {
|
||||
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"`
|
||||
@@ -48,24 +48,22 @@ func (h *ApiKeyHandler) Save(c *gin.Context) {
|
||||
if data.Id > 0 {
|
||||
h.DB.Find(&apiKey, data.Id)
|
||||
}
|
||||
apiKey.Platform = data.Platform
|
||||
apiKey.Value = data.Value
|
||||
apiKey.Type = data.Type
|
||||
apiKey.ApiURL = data.ApiURL
|
||||
apiKey.Enabled = data.Enabled
|
||||
apiKey.ProxyURL = data.ProxyURL
|
||||
apiKey.Name = data.Name
|
||||
res := h.DB.Save(&apiKey)
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
err := h.DB.Save(&apiKey).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var keyVo vo.ApiKey
|
||||
err := utils.CopyObject(apiKey, &keyVo)
|
||||
err = utils.CopyObject(apiKey, &keyVo)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "数据拷贝失败!")
|
||||
resp.ERROR(c, fmt.Sprintf("拷贝数据失败:%v", err))
|
||||
return
|
||||
}
|
||||
keyVo.Id = apiKey.Id
|
||||
@@ -76,7 +74,6 @@ func (h *ApiKeyHandler) Save(c *gin.Context) {
|
||||
func (h *ApiKeyHandler) List(c *gin.Context) {
|
||||
status := h.GetBool(c, "status")
|
||||
t := h.GetTrim(c, "type")
|
||||
platform := h.GetTrim(c, "platform")
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if status {
|
||||
@@ -85,9 +82,6 @@ func (h *ApiKeyHandler) List(c *gin.Context) {
|
||||
if t != "" {
|
||||
session = session.Where("type", t)
|
||||
}
|
||||
if platform != "" {
|
||||
session = session.Where("platform", platform)
|
||||
}
|
||||
|
||||
var items []model.ApiKey
|
||||
var keys = make([]vo.ApiKey, 0)
|
||||
@@ -121,10 +115,9 @@ func (h *ApiKeyHandler) Set(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
res := h.DB.Model(&model.ApiKey{}).Where("id = ?", data.Id).Update(data.Filed, data.Value)
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
err := h.DB.Model(&model.ApiKey{}).Where("id = ?", data.Id).Update(data.Filed, data.Value).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
@@ -137,10 +130,9 @@ func (h *ApiKeyHandler) Remove(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
res := h.DB.Where("id", id).Delete(&model.ApiKey{})
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
err := h.DB.Where("id", id).Delete(&model.ApiKey{}).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
|
||||
@@ -8,6 +8,7 @@ package admin
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
@@ -21,16 +22,16 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ChatRoleHandler struct {
|
||||
type ChatAppHandler struct {
|
||||
handler.BaseHandler
|
||||
}
|
||||
|
||||
func NewChatRoleHandler(app *core.AppServer, db *gorm.DB) *ChatRoleHandler {
|
||||
return &ChatRoleHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
func NewChatAppHandler(app *core.AppServer, db *gorm.DB) *ChatAppHandler {
|
||||
return &ChatAppHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// Save 创建或者更新某个角色
|
||||
func (h *ChatRoleHandler) Save(c *gin.Context) {
|
||||
func (h *ChatAppHandler) Save(c *gin.Context) {
|
||||
var data vo.ChatRole
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
@@ -45,11 +46,16 @@ func (h *ChatRoleHandler) Save(c *gin.Context) {
|
||||
role.Id = data.Id
|
||||
if data.CreatedAt > 0 {
|
||||
role.CreatedAt = time.Unix(data.CreatedAt, 0)
|
||||
} else {
|
||||
err = h.DB.Where("marker", data.Key).First(&role).Error
|
||||
if err == nil {
|
||||
resp.ERROR(c, fmt.Sprintf("角色 %s 已存在", data.Key))
|
||||
return
|
||||
}
|
||||
}
|
||||
res := h.DB.Save(&role)
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
err = h.DB.Save(&role).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
// 填充 ID 数据
|
||||
@@ -58,7 +64,7 @@ func (h *ChatRoleHandler) Save(c *gin.Context) {
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
func (h *ChatRoleHandler) List(c *gin.Context) {
|
||||
func (h *ChatAppHandler) List(c *gin.Context) {
|
||||
var items []model.ChatRole
|
||||
var roles = make([]vo.ChatRole, 0)
|
||||
res := h.DB.Order("sort_num ASC").Find(&items)
|
||||
@@ -69,13 +75,18 @@ func (h *ChatRoleHandler) List(c *gin.Context) {
|
||||
|
||||
// initialize model mane for role
|
||||
modelIds := make([]int, 0)
|
||||
typeIds := make([]int, 0)
|
||||
for _, v := range items {
|
||||
if v.ModelId > 0 {
|
||||
modelIds = append(modelIds, v.ModelId)
|
||||
}
|
||||
if v.Tid > 0 {
|
||||
typeIds = append(typeIds, v.Tid)
|
||||
}
|
||||
}
|
||||
|
||||
modelNameMap := make(map[int]string)
|
||||
typeNameMap := make(map[int]string)
|
||||
if len(modelIds) > 0 {
|
||||
var models []model.ChatModel
|
||||
tx := h.DB.Where("id IN ?", modelIds).Find(&models)
|
||||
@@ -85,6 +96,15 @@ func (h *ChatRoleHandler) List(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(typeIds) > 0 {
|
||||
var appTypes []model.AppType
|
||||
tx := h.DB.Where("id IN ?", typeIds).Find(&appTypes)
|
||||
if tx.Error == nil {
|
||||
for _, m := range appTypes {
|
||||
typeNameMap[int(m.Id)] = m.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range items {
|
||||
var role vo.ChatRole
|
||||
@@ -94,6 +114,7 @@ func (h *ChatRoleHandler) List(c *gin.Context) {
|
||||
role.CreatedAt = v.CreatedAt.Unix()
|
||||
role.UpdatedAt = v.UpdatedAt.Unix()
|
||||
role.ModelName = modelNameMap[role.ModelId]
|
||||
role.TypeName = typeNameMap[role.Tid]
|
||||
roles = append(roles, role)
|
||||
}
|
||||
}
|
||||
@@ -102,7 +123,7 @@ func (h *ChatRoleHandler) List(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Sort 更新角色排序
|
||||
func (h *ChatRoleHandler) Sort(c *gin.Context) {
|
||||
func (h *ChatAppHandler) Sort(c *gin.Context) {
|
||||
var data struct {
|
||||
Ids []uint `json:"ids"`
|
||||
Sorts []int `json:"sorts"`
|
||||
@@ -114,10 +135,9 @@ func (h *ChatRoleHandler) Sort(c *gin.Context) {
|
||||
}
|
||||
|
||||
for index, id := range data.Ids {
|
||||
res := h.DB.Model(&model.ChatRole{}).Where("id = ?", id).Update("sort_num", data.Sorts[index])
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
err := h.DB.Model(&model.ChatRole{}).Where("id = ?", id).Update("sort_num", data.Sorts[index]).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -125,7 +145,7 @@ func (h *ChatRoleHandler) Sort(c *gin.Context) {
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *ChatRoleHandler) Set(c *gin.Context) {
|
||||
func (h *ChatAppHandler) Set(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
Filed string `json:"filed"`
|
||||
@@ -137,16 +157,15 @@ func (h *ChatRoleHandler) Set(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
res := h.DB.Model(&model.ChatRole{}).Where("id = ?", data.Id).Update(data.Filed, data.Value)
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
err := h.DB.Model(&model.ChatRole{}).Where("id = ?", data.Id).Update(data.Filed, data.Value).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *ChatRoleHandler) Remove(c *gin.Context) {
|
||||
func (h *ChatAppHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
|
||||
if id <= 0 {
|
||||
148
api/handler/admin/chat_app_type_handler.go
Normal file
148
api/handler/admin/chat_app_type_handler.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ChatAppTypeHandler struct {
|
||||
handler.BaseHandler
|
||||
}
|
||||
|
||||
func NewChatAppTypeHandler(app *core.AppServer, db *gorm.DB) *ChatAppTypeHandler {
|
||||
return &ChatAppTypeHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// Save 创建或更新App类型
|
||||
func (h *ChatAppTypeHandler) Save(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Icon string `json:"icon"`
|
||||
SortNum int `json:"sort_num"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
if data.Id == 0 { // for add
|
||||
err := h.DB.Where("name", data.Name).First(&model.AppType{}).Error
|
||||
if err == nil {
|
||||
resp.ERROR(c, "当前分类已经存在")
|
||||
return
|
||||
}
|
||||
err = h.DB.Create(&model.AppType{
|
||||
Name: data.Name,
|
||||
Icon: data.Icon,
|
||||
Enabled: data.Enabled,
|
||||
SortNum: data.SortNum,
|
||||
}).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
} else { // for update
|
||||
err := h.DB.Model(&model.AppType{}).Where("id", data.Id).Updates(map[string]interface{}{
|
||||
"name": data.Name,
|
||||
"icon": data.Icon,
|
||||
"enabled": data.Enabled,
|
||||
}).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// List 获取App类型列表
|
||||
func (h *ChatAppTypeHandler) List(c *gin.Context) {
|
||||
var items []model.AppType
|
||||
var appTypes = make([]vo.AppType, 0)
|
||||
err := h.DB.Order("sort_num ASC").Find(&items).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, v := range items {
|
||||
var appType vo.AppType
|
||||
err = utils.CopyObject(v, &appType)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
appType.Id = v.Id
|
||||
appType.CreatedAt = v.CreatedAt.Unix()
|
||||
appTypes = append(appTypes, appType)
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, appTypes)
|
||||
}
|
||||
|
||||
// Remove 删除App类型
|
||||
func (h *ChatAppTypeHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
|
||||
if id <= 0 {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
err := h.DB.Where("id", id).Delete(&model.AppType{}).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// Enable 启用|禁用
|
||||
func (h *ChatAppTypeHandler) Enable(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.DB.Model(&model.AppType{}).Where("id", data.Id).UpdateColumn("enabled", data.Enabled).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// Sort 更新排序
|
||||
func (h *ChatAppTypeHandler) Sort(c *gin.Context) {
|
||||
var data struct {
|
||||
Ids []uint `json:"ids"`
|
||||
Sorts []int `json:"sorts"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
for index, id := range data.Ids {
|
||||
err := h.DB.Model(&model.AppType{}).Where("id", id).Update("sort_num", data.Sorts[index]).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
@@ -259,10 +259,9 @@ func (h *ChatHandler) RemoveChat(c *gin.Context) {
|
||||
// RemoveMessage 删除聊天记录
|
||||
func (h *ChatHandler) RemoveMessage(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
tx := h.DB.Unscoped().Where("id = ?", id).Delete(&model.ChatMessage{})
|
||||
if tx.Error != nil {
|
||||
logger.Error("error with update database:", tx.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
err := h.DB.Unscoped().Where("id = ?", id).Delete(&model.ChatMessage{}).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
|
||||
@@ -49,28 +49,32 @@ func (h *ChatModelHandler) Save(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
item := model.ChatModel{
|
||||
Platform: data.Platform,
|
||||
Name: data.Name,
|
||||
Value: data.Value,
|
||||
Enabled: data.Enabled,
|
||||
Open: data.Open,
|
||||
MaxTokens: data.MaxTokens,
|
||||
MaxContext: data.MaxContext,
|
||||
Temperature: data.Temperature,
|
||||
KeyId: data.KeyId,
|
||||
Power: data.Power}
|
||||
item := model.ChatModel{}
|
||||
// 更新
|
||||
if data.Id > 0 {
|
||||
h.DB.Where("id", data.Id).First(&item)
|
||||
}
|
||||
|
||||
item.Name = data.Name
|
||||
item.Value = data.Value
|
||||
item.Enabled = data.Enabled
|
||||
item.SortNum = data.SortNum
|
||||
item.Open = data.Open
|
||||
item.Power = data.Power
|
||||
item.MaxTokens = data.MaxTokens
|
||||
item.MaxContext = data.MaxContext
|
||||
item.Temperature = data.Temperature
|
||||
item.KeyId = data.KeyId
|
||||
|
||||
var res *gorm.DB
|
||||
if data.Id > 0 {
|
||||
item.Id = data.Id
|
||||
item.SortNum = data.SortNum
|
||||
res = h.DB.Select("*").Omit("created_at").Updates(&item)
|
||||
res = h.DB.Save(&item)
|
||||
} else {
|
||||
res = h.DB.Create(&item)
|
||||
}
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -89,12 +93,12 @@ func (h *ChatModelHandler) Save(c *gin.Context) {
|
||||
func (h *ChatModelHandler) List(c *gin.Context) {
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
enable := h.GetBool(c, "enable")
|
||||
platform := h.GetTrim(c, "platform")
|
||||
name := h.GetTrim(c, "name")
|
||||
if enable {
|
||||
session = session.Where("enabled", enable)
|
||||
}
|
||||
if platform != "" {
|
||||
session = session.Where("platform", platform)
|
||||
if name != "" {
|
||||
session = session.Where("name LIKE ?", name+"%")
|
||||
}
|
||||
var items []model.ChatModel
|
||||
var cms = make([]vo.ChatModel, 0)
|
||||
@@ -143,10 +147,9 @@ func (h *ChatModelHandler) Set(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
res := h.DB.Model(&model.ChatModel{}).Where("id = ?", data.Id).Update(data.Filed, data.Value)
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
err := h.DB.Model(&model.ChatModel{}).Where("id = ?", data.Id).Update(data.Filed, data.Value).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
@@ -164,10 +167,9 @@ func (h *ChatModelHandler) Sort(c *gin.Context) {
|
||||
}
|
||||
|
||||
for index, id := range data.Ids {
|
||||
res := h.DB.Model(&model.ChatModel{}).Where("id = ?", id).Update("sort_num", data.Sorts[index])
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
err := h.DB.Model(&model.ChatModel{}).Where("id = ?", id).Update("sort_num", data.Sorts[index]).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -182,10 +184,9 @@ func (h *ChatModelHandler) Remove(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
res := h.DB.Where("id = ?", id).Delete(&model.ChatModel{})
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
err := h.DB.Where("id = ?", id).Delete(&model.ChatModel{}).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
|
||||
@@ -12,8 +12,6 @@ import (
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/service"
|
||||
"geekai/service/mj"
|
||||
"geekai/service/sd"
|
||||
"geekai/store"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
@@ -28,16 +26,12 @@ type ConfigHandler struct {
|
||||
handler.BaseHandler
|
||||
levelDB *store.LevelDB
|
||||
licenseService *service.LicenseService
|
||||
mjServicePool *mj.ServicePool
|
||||
sdServicePool *sd.ServicePool
|
||||
}
|
||||
|
||||
func NewConfigHandler(app *core.AppServer, db *gorm.DB, levelDB *store.LevelDB, licenseService *service.LicenseService, mjPool *mj.ServicePool, sdPool *sd.ServicePool) *ConfigHandler {
|
||||
func NewConfigHandler(app *core.AppServer, db *gorm.DB, levelDB *store.LevelDB, licenseService *service.LicenseService) *ConfigHandler {
|
||||
return &ConfigHandler{
|
||||
BaseHandler: handler.BaseHandler{App: app, DB: db},
|
||||
levelDB: levelDB,
|
||||
mjServicePool: mjPool,
|
||||
sdServicePool: sdPool,
|
||||
licenseService: licenseService,
|
||||
}
|
||||
}
|
||||
@@ -50,6 +44,7 @@ func (h *ConfigHandler) Update(c *gin.Context) {
|
||||
Content string `json:"content,omitempty"`
|
||||
Updated bool `json:"updated,omitempty"`
|
||||
} `json:"config"`
|
||||
ConfigBak types.SystemConfig `json:"config_bak,omitempty"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
@@ -57,6 +52,12 @@ func (h *ConfigHandler) Update(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// ONLY authorized user can change the copyright
|
||||
if (data.Key == "system" && data.Config.Copyright != data.ConfigBak.Copyright) && !h.licenseService.GetLicense().Configs.DeCopy {
|
||||
resp.ERROR(c, "您无权修改版权信息,请先联系作者获取授权")
|
||||
return
|
||||
}
|
||||
|
||||
value := utils.JsonEncode(&data.Config)
|
||||
config := model.Config{Key: data.Key, Config: value}
|
||||
res := h.DB.FirstOrCreate(&config, model.Config{Key: data.Key})
|
||||
@@ -140,58 +141,69 @@ func (h *ConfigHandler) GetLicense(c *gin.Context) {
|
||||
resp.SUCCESS(c, license)
|
||||
}
|
||||
|
||||
// GetAppConfig 获取内置配置
|
||||
func (h *ConfigHandler) GetAppConfig(c *gin.Context) {
|
||||
resp.SUCCESS(c, gin.H{
|
||||
"mj_plus": h.App.Config.MjPlusConfigs,
|
||||
"mj_proxy": h.App.Config.MjProxyConfigs,
|
||||
"sd": h.App.Config.SdConfigs,
|
||||
"platforms": Platforms,
|
||||
})
|
||||
}
|
||||
|
||||
// SaveDrawingConfig 保存AI绘画配置
|
||||
func (h *ConfigHandler) SaveDrawingConfig(c *gin.Context) {
|
||||
var data struct {
|
||||
Sd []types.StableDiffusionConfig `json:"sd"`
|
||||
MjPlus []types.MjPlusConfig `json:"mj_plus"`
|
||||
MjProxy []types.MjProxyConfig `json:"mj_proxy"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
changed := false
|
||||
if configChanged(data.Sd, h.App.Config.SdConfigs) {
|
||||
logger.Debugf("SD 配置变动了")
|
||||
h.App.Config.SdConfigs = data.Sd
|
||||
h.sdServicePool.InitServices(data.Sd)
|
||||
changed = true
|
||||
}
|
||||
|
||||
if configChanged(data.MjPlus, h.App.Config.MjPlusConfigs) || configChanged(data.MjProxy, h.App.Config.MjProxyConfigs) {
|
||||
logger.Debugf("MidJourney 配置变动了")
|
||||
h.App.Config.MjPlusConfigs = data.MjPlus
|
||||
h.App.Config.MjProxyConfigs = data.MjProxy
|
||||
h.mjServicePool.InitServices(data.MjPlus, data.MjProxy)
|
||||
changed = true
|
||||
}
|
||||
|
||||
if changed {
|
||||
err := core.SaveConfig(h.App.Config)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "更新配置文档失败!")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
|
||||
}
|
||||
|
||||
func configChanged(c1 interface{}, c2 interface{}) bool {
|
||||
encode1 := utils.JsonEncode(c1)
|
||||
encode2 := utils.JsonEncode(c2)
|
||||
return utils.Md5(encode1) != utils.Md5(encode2)
|
||||
// FixData 修复数据
|
||||
func (h *ConfigHandler) FixData(c *gin.Context) {
|
||||
resp.ERROR(c, "当前升级版本没有数据需要修正!")
|
||||
return
|
||||
//var fixed bool
|
||||
//version := "data_fix_4.1.4"
|
||||
//err := h.levelDB.Get(version, &fixed)
|
||||
//if err == nil || fixed {
|
||||
// resp.ERROR(c, "当前版本数据修复已完成,请不要重复执行操作")
|
||||
// return
|
||||
//}
|
||||
//tx := h.DB.Begin()
|
||||
//var users []model.User
|
||||
//err = tx.Find(&users).Error
|
||||
//if err != nil {
|
||||
// resp.ERROR(c, err.Error())
|
||||
// return
|
||||
//}
|
||||
//for _, user := range users {
|
||||
// if user.Email != "" || user.Mobile != "" {
|
||||
// continue
|
||||
// }
|
||||
// if utils.IsValidEmail(user.Username) {
|
||||
// user.Email = user.Username
|
||||
// } else if utils.IsValidMobile(user.Username) {
|
||||
// user.Mobile = user.Username
|
||||
// }
|
||||
// err = tx.Save(&user).Error
|
||||
// if err != nil {
|
||||
// resp.ERROR(c, err.Error())
|
||||
// tx.Rollback()
|
||||
// return
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//var orders []model.Order
|
||||
//err = h.DB.Find(&orders).Error
|
||||
//if err != nil {
|
||||
// resp.ERROR(c, err.Error())
|
||||
// return
|
||||
//}
|
||||
//for _, order := range orders {
|
||||
// if order.PayWay == "支付宝" {
|
||||
// order.PayWay = "alipay"
|
||||
// order.PayType = "alipay"
|
||||
// } else if order.PayWay == "微信支付" {
|
||||
// order.PayWay = "wechat"
|
||||
// order.PayType = "wxpay"
|
||||
// } else if order.PayWay == "hupi" {
|
||||
// order.PayType = "wxpay"
|
||||
// }
|
||||
// err = tx.Save(&order).Error
|
||||
// if err != nil {
|
||||
// resp.ERROR(c, err.Error())
|
||||
// tx.Rollback()
|
||||
// return
|
||||
// }
|
||||
//}
|
||||
//tx.Commit()
|
||||
//err = h.levelDB.Put(version, true)
|
||||
//if err != nil {
|
||||
// resp.ERROR(c, err.Error())
|
||||
// return
|
||||
//}
|
||||
//resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
@@ -60,13 +60,6 @@ func (h *DashboardHandler) Stats(c *gin.Context) {
|
||||
stats.Tokens += item.Tokens
|
||||
}
|
||||
|
||||
// 众筹收入
|
||||
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)
|
||||
@@ -101,13 +94,6 @@ func (h *DashboardHandler) Stats(c *gin.Context) {
|
||||
historyMessagesStatistic[item.CreatedAt.Format("2006-01-02")] += float64(item.Tokens)
|
||||
}
|
||||
|
||||
// 浮点数相加?
|
||||
// 统计最近7天的众筹
|
||||
res = h.DB.Where("created_at > ?", startDate).Find(&rewards)
|
||||
for _, item := range rewards {
|
||||
incomeStatistic[item.CreatedAt.Format("2006-01-02")], _ = decimal.NewFromFloat(incomeStatistic[item.CreatedAt.Format("2006-01-02")]).Add(decimal.NewFromFloat(item.Amount)).Float64()
|
||||
}
|
||||
|
||||
// 统计最近7天的订单
|
||||
res = h.DB.Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", startDate).Find(&orders)
|
||||
for _, item := range orders {
|
||||
|
||||
@@ -69,10 +69,9 @@ func (h *FunctionHandler) Set(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
res := h.DB.Model(&model.Function{}).Where("id = ?", data.Id).Update(data.Filed, data.Value)
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
err := h.DB.Model(&model.Function{}).Where("id = ?", data.Id).Update(data.Filed, data.Value).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
@@ -102,10 +101,9 @@ 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 {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
err := h.DB.Delete(&model.Function{Id: uint(id)}).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
254
api/handler/admin/image_handler.go
Normal file
254
api/handler/admin/image_handler.go
Normal file
@@ -0,0 +1,254 @@
|
||||
package admin
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/service"
|
||||
"geekai/service/oss"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ImageHandler struct {
|
||||
handler.BaseHandler
|
||||
userService *service.UserService
|
||||
uploader *oss.UploaderManager
|
||||
}
|
||||
|
||||
func NewImageHandler(app *core.AppServer, db *gorm.DB, userService *service.UserService, manager *oss.UploaderManager) *ImageHandler {
|
||||
return &ImageHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}, userService: userService, uploader: manager}
|
||||
}
|
||||
|
||||
type imageQuery struct {
|
||||
Prompt string `json:"prompt"`
|
||||
Username string `json:"username"`
|
||||
CreatedAt []string `json:"created_at"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// MjList Midjourney 任务列表
|
||||
func (h *ImageHandler) MjList(c *gin.Context) {
|
||||
var data imageQuery
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if data.Username != "" {
|
||||
var user model.User
|
||||
err := h.DB.Where("username", data.Username).First(&user).Error
|
||||
if err == nil {
|
||||
session = session.Where("user_id", user.Id)
|
||||
}
|
||||
}
|
||||
if data.Prompt != "" {
|
||||
session = session.Where("prompt LIKE ?", "%"+data.Prompt+"%")
|
||||
}
|
||||
if len(data.CreatedAt) == 2 {
|
||||
session = session.Where("created_at >= ? AND created_at <= ?", data.CreatedAt[0], data.CreatedAt[1])
|
||||
}
|
||||
var total int64
|
||||
session.Model(&model.MidJourneyJob{}).Count(&total)
|
||||
var list []model.MidJourneyJob
|
||||
var items = make([]vo.MidJourneyJob, 0)
|
||||
offset := (data.Page - 1) * data.PageSize
|
||||
err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error
|
||||
if err == nil {
|
||||
// 填充数据
|
||||
for _, item := range list {
|
||||
var job vo.MidJourneyJob
|
||||
err = utils.CopyObject(item, &job)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
job.CreatedAt = item.CreatedAt.Unix()
|
||||
items = append(items, job)
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items))
|
||||
}
|
||||
|
||||
// SdList Stable Diffusion 任务列表
|
||||
func (h *ImageHandler) SdList(c *gin.Context) {
|
||||
var data imageQuery
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if data.Username != "" {
|
||||
var user model.User
|
||||
err := h.DB.Where("username", data.Username).First(&user).Error
|
||||
if err == nil {
|
||||
session = session.Where("user_id", user.Id)
|
||||
}
|
||||
}
|
||||
if data.Prompt != "" {
|
||||
session = session.Where("prompt LIKE ?", "%"+data.Prompt+"%")
|
||||
}
|
||||
if len(data.CreatedAt) == 2 {
|
||||
session = session.Where("created_at >= ? AND created_at <= ?", data.CreatedAt[0], data.CreatedAt[1])
|
||||
}
|
||||
var total int64
|
||||
session.Model(&model.SdJob{}).Count(&total)
|
||||
var list []model.SdJob
|
||||
var items = make([]vo.SdJob, 0)
|
||||
offset := (data.Page - 1) * data.PageSize
|
||||
err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error
|
||||
if err == nil {
|
||||
// 填充数据
|
||||
for _, item := range list {
|
||||
var job vo.SdJob
|
||||
err = utils.CopyObject(item, &job)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
job.CreatedAt = item.CreatedAt.Unix()
|
||||
items = append(items, job)
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items))
|
||||
}
|
||||
|
||||
// DallList DALL-E 任务列表
|
||||
func (h *ImageHandler) DallList(c *gin.Context) {
|
||||
var data imageQuery
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if data.Username != "" {
|
||||
var user model.User
|
||||
err := h.DB.Where("username", data.Username).First(&user).Error
|
||||
if err == nil {
|
||||
session = session.Where("user_id", user.Id)
|
||||
}
|
||||
}
|
||||
if data.Prompt != "" {
|
||||
session = session.Where("prompt LIKE ?", "%"+data.Prompt+"%")
|
||||
}
|
||||
if len(data.CreatedAt) == 2 {
|
||||
session = session.Where("created_at >= ? AND created_at <= ?", data.CreatedAt[0], data.CreatedAt[1])
|
||||
}
|
||||
var total int64
|
||||
session.Model(&model.DallJob{}).Count(&total)
|
||||
var list []model.DallJob
|
||||
var items = make([]vo.DallJob, 0)
|
||||
offset := (data.Page - 1) * data.PageSize
|
||||
err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error
|
||||
if err == nil {
|
||||
// 填充数据
|
||||
for _, item := range list {
|
||||
var job vo.DallJob
|
||||
err = utils.CopyObject(item, &job)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
job.CreatedAt = item.CreatedAt.Unix()
|
||||
items = append(items, job)
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items))
|
||||
}
|
||||
|
||||
func (h *ImageHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
tab := c.Query("tab")
|
||||
|
||||
tx := h.DB.Begin()
|
||||
var md, remark, imgURL string
|
||||
var power, userId, progress int
|
||||
switch tab {
|
||||
case "mj":
|
||||
var job model.MidJourneyJob
|
||||
if err := h.DB.Where("id", id).First(&job).Error; err != nil {
|
||||
resp.ERROR(c, "记录不存在")
|
||||
return
|
||||
}
|
||||
tx.Delete(&job)
|
||||
md = "mid-journey"
|
||||
power = job.Power
|
||||
userId = job.UserId
|
||||
remark = fmt.Sprintf("任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg)
|
||||
progress = job.Progress
|
||||
imgURL = job.ImgURL
|
||||
break
|
||||
case "sd":
|
||||
var job model.SdJob
|
||||
if res := h.DB.Where("id", id).First(&job); res.Error != nil {
|
||||
resp.ERROR(c, "记录不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
tx.Delete(&job)
|
||||
md = "stable-diffusion"
|
||||
power = job.Power
|
||||
userId = job.UserId
|
||||
remark = fmt.Sprintf("任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg)
|
||||
progress = job.Progress
|
||||
imgURL = job.ImgURL
|
||||
break
|
||||
case "dall":
|
||||
var job model.DallJob
|
||||
if res := h.DB.Where("id", id).First(&job); res.Error != nil {
|
||||
resp.ERROR(c, "记录不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
tx.Delete(&job)
|
||||
md = "dall-e-3"
|
||||
power = job.Power
|
||||
userId = int(job.UserId)
|
||||
remark = fmt.Sprintf("任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg)
|
||||
progress = job.Progress
|
||||
imgURL = job.ImgURL
|
||||
break
|
||||
default:
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
if progress != 100 {
|
||||
err := h.userService.IncreasePower(userId, power, model.PowerLog{
|
||||
Type: types.PowerRefund,
|
||||
Model: md,
|
||||
Remark: remark,
|
||||
})
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
tx.Commit()
|
||||
// remove image
|
||||
err := h.uploader.GetUploadHandler().Delete(imgURL)
|
||||
if err != nil {
|
||||
logger.Error("remove image failed: ", err)
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
200
api/handler/admin/media_handler.go
Normal file
200
api/handler/admin/media_handler.go
Normal file
@@ -0,0 +1,200 @@
|
||||
package admin
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/service"
|
||||
"geekai/service/oss"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type MediaHandler struct {
|
||||
handler.BaseHandler
|
||||
userService *service.UserService
|
||||
uploader *oss.UploaderManager
|
||||
}
|
||||
|
||||
func NewMediaHandler(app *core.AppServer, db *gorm.DB, userService *service.UserService, manager *oss.UploaderManager) *MediaHandler {
|
||||
return &MediaHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}, userService: userService, uploader: manager}
|
||||
}
|
||||
|
||||
type mediaQuery struct {
|
||||
Prompt string `json:"prompt"`
|
||||
Username string `json:"username"`
|
||||
CreatedAt []string `json:"created_at"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// SunoList Suno 任务列表
|
||||
func (h *MediaHandler) SunoList(c *gin.Context) {
|
||||
var data mediaQuery
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if data.Username != "" {
|
||||
var user model.User
|
||||
err := h.DB.Where("username", data.Username).First(&user).Error
|
||||
if err == nil {
|
||||
session = session.Where("user_id", user.Id)
|
||||
}
|
||||
}
|
||||
if data.Prompt != "" {
|
||||
session = session.Where("prompt LIKE ?", "%"+data.Prompt+"%")
|
||||
}
|
||||
if len(data.CreatedAt) == 2 {
|
||||
session = session.Where("created_at >= ? AND created_at <= ?", data.CreatedAt[0], data.CreatedAt[1])
|
||||
}
|
||||
var total int64
|
||||
session.Model(&model.SunoJob{}).Count(&total)
|
||||
var list []model.SunoJob
|
||||
var items = make([]vo.SunoJob, 0)
|
||||
offset := (data.Page - 1) * data.PageSize
|
||||
err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error
|
||||
if err == nil {
|
||||
// 填充数据
|
||||
for _, item := range list {
|
||||
var job vo.SunoJob
|
||||
err = utils.CopyObject(item, &job)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
job.CreatedAt = item.CreatedAt.Unix()
|
||||
items = append(items, job)
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items))
|
||||
}
|
||||
|
||||
// LumaList Luma 视频任务列表
|
||||
func (h *MediaHandler) LumaList(c *gin.Context) {
|
||||
var data mediaQuery
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if data.Username != "" {
|
||||
var user model.User
|
||||
err := h.DB.Where("username", data.Username).First(&user).Error
|
||||
if err == nil {
|
||||
session = session.Where("user_id", user.Id)
|
||||
}
|
||||
}
|
||||
if data.Prompt != "" {
|
||||
session = session.Where("prompt LIKE ?", "%"+data.Prompt+"%")
|
||||
}
|
||||
if len(data.CreatedAt) == 2 {
|
||||
session = session.Where("created_at >= ? AND created_at <= ?", data.CreatedAt[0], data.CreatedAt[1])
|
||||
}
|
||||
var total int64
|
||||
session.Model(&model.VideoJob{}).Count(&total)
|
||||
var list []model.VideoJob
|
||||
var items = make([]vo.VideoJob, 0)
|
||||
offset := (data.Page - 1) * data.PageSize
|
||||
err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error
|
||||
if err == nil {
|
||||
// 填充数据
|
||||
for _, item := range list {
|
||||
var job vo.VideoJob
|
||||
err = utils.CopyObject(item, &job)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
job.CreatedAt = item.CreatedAt.Unix()
|
||||
if job.VideoURL == "" {
|
||||
job.VideoURL = job.WaterURL
|
||||
}
|
||||
items = append(items, job)
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items))
|
||||
}
|
||||
|
||||
func (h *MediaHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
tab := c.Query("tab")
|
||||
|
||||
tx := h.DB.Begin()
|
||||
var md, remark, fileURL string
|
||||
var power, userId, progress int
|
||||
switch tab {
|
||||
case "suno":
|
||||
var job model.SunoJob
|
||||
if err := h.DB.Where("id", id).First(&job).Error; err != nil {
|
||||
resp.ERROR(c, "记录不存在")
|
||||
return
|
||||
}
|
||||
tx.Delete(&job)
|
||||
md = "suno"
|
||||
power = job.Power
|
||||
userId = job.UserId
|
||||
remark = fmt.Sprintf("SUNO 任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg)
|
||||
progress = job.Progress
|
||||
fileURL = job.AudioURL
|
||||
break
|
||||
case "luma":
|
||||
var job model.VideoJob
|
||||
if res := h.DB.Where("id", id).First(&job); res.Error != nil {
|
||||
resp.ERROR(c, "记录不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
tx.Delete(&job)
|
||||
md = job.Type
|
||||
power = job.Power
|
||||
userId = job.UserId
|
||||
remark = fmt.Sprintf("LUMA 任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg)
|
||||
progress = job.Progress
|
||||
fileURL = job.VideoURL
|
||||
if fileURL == "" {
|
||||
fileURL = job.WaterURL
|
||||
}
|
||||
break
|
||||
default:
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
if progress != 100 {
|
||||
err := h.userService.IncreasePower(userId, power, model.PowerLog{
|
||||
Type: types.PowerRefund,
|
||||
Model: md,
|
||||
Remark: remark,
|
||||
})
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
tx.Commit()
|
||||
// remove image
|
||||
err := h.uploader.GetUploadHandler().Delete(fileURL)
|
||||
if err != nil {
|
||||
logger.Error("remove image failed: ", err)
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
@@ -41,17 +41,16 @@ func (h *MenuHandler) Save(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
res := h.DB.Save(&model.Menu{
|
||||
err := h.DB.Save(&model.Menu{
|
||||
Id: data.Id,
|
||||
Name: data.Name,
|
||||
Icon: data.Icon,
|
||||
URL: data.URL,
|
||||
SortNum: data.SortNum,
|
||||
Enabled: data.Enabled,
|
||||
})
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
}).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
@@ -85,10 +84,9 @@ func (h *MenuHandler) Enable(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
res := h.DB.Model(&model.Menu{}).Where("id", data.Id).UpdateColumn("enabled", data.Enabled)
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
err := h.DB.Model(&model.Menu{}).Where("id", data.Id).UpdateColumn("enabled", data.Enabled).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
@@ -106,10 +104,9 @@ func (h *MenuHandler) Sort(c *gin.Context) {
|
||||
}
|
||||
|
||||
for index, id := range data.Ids {
|
||||
res := h.DB.Model(&model.Menu{}).Where("id", id).Update("sort_num", data.Sorts[index])
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
err := h.DB.Model(&model.Menu{}).Where("id", id).Update("sort_num", data.Sorts[index]).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -121,10 +118,9 @@ func (h *MenuHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
|
||||
if id > 0 {
|
||||
res := h.DB.Where("id", id).Delete(&model.Menu{})
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
err := h.DB.Where("id", id).Delete(&model.Menu{}).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -67,6 +68,16 @@ func (h *OrderHandler) List(c *gin.Context) {
|
||||
order.Id = item.Id
|
||||
order.CreatedAt = item.CreatedAt.Unix()
|
||||
order.UpdatedAt = item.UpdatedAt.Unix()
|
||||
payMethod, ok := types.PayMethods[item.PayWay]
|
||||
if !ok {
|
||||
payMethod = item.PayWay
|
||||
}
|
||||
payName, ok := types.PayNames[item.PayType]
|
||||
if !ok {
|
||||
payName = item.PayWay
|
||||
}
|
||||
order.PayMethod = payMethod
|
||||
order.PayName = payName
|
||||
list = append(list, order)
|
||||
} else {
|
||||
logger.Error(err)
|
||||
@@ -92,12 +103,33 @@ func (h *OrderHandler) Remove(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
res = h.DB.Unscoped().Where("id = ?", id).Delete(&model.Order{})
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
err := h.DB.Where("id = ?", id).Delete(&model.Order{}).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *OrderHandler) Clear(c *gin.Context) {
|
||||
var orders []model.Order
|
||||
err := h.DB.Where("status <> ?", 2).Where("pay_time", 0).Find(&orders).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
deleteIds := make([]uint, 0)
|
||||
for _, order := range orders {
|
||||
// 只删除 15 分钟内的未支付订单
|
||||
if time.Now().After(order.CreatedAt.Add(time.Minute * 15)) {
|
||||
deleteIds = append(deleteIds, order.Id)
|
||||
}
|
||||
}
|
||||
err = h.DB.Where("id IN ?", deleteIds).Delete(&model.Order{}).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
@@ -55,17 +55,16 @@ func (h *ProductHandler) Save(c *gin.Context) {
|
||||
if item.Id > 0 {
|
||||
item.CreatedAt = time.Unix(data.CreatedAt, 0)
|
||||
}
|
||||
res := h.DB.Save(&item)
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
err := h.DB.Save(&item).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var itemVo vo.Product
|
||||
err := utils.CopyObject(item, &itemVo)
|
||||
err = utils.CopyObject(item, &itemVo)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "数据拷贝失败!")
|
||||
resp.ERROR(c, "数据拷贝失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
itemVo.Id = item.Id
|
||||
@@ -106,10 +105,9 @@ func (h *ProductHandler) Enable(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
res := h.DB.Model(&model.Product{}).Where("id", data.Id).UpdateColumn("enabled", data.Enabled)
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
err := h.DB.Model(&model.Product{}).Where("id", data.Id).UpdateColumn("enabled", data.Enabled).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
@@ -127,10 +125,9 @@ func (h *ProductHandler) Sort(c *gin.Context) {
|
||||
}
|
||||
|
||||
for index, id := range data.Ids {
|
||||
res := h.DB.Model(&model.Product{}).Where("id", id).Update("sort_num", data.Sorts[index])
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
err := h.DB.Model(&model.Product{}).Where("id", id).Update("sort_num", data.Sorts[index]).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -142,10 +139,9 @@ func (h *ProductHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
|
||||
if id > 0 {
|
||||
res := h.DB.Where("id", id).Delete(&model.Product{})
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
err := h.DB.Where("id", id).Delete(&model.Product{}).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
160
api/handler/admin/redeem_handler.go
Normal file
160
api/handler/admin/redeem_handler.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package admin
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type RedeemHandler struct {
|
||||
handler.BaseHandler
|
||||
}
|
||||
|
||||
func NewRedeemHandler(app *core.AppServer, db *gorm.DB) *RedeemHandler {
|
||||
return &RedeemHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
func (h *RedeemHandler) List(c *gin.Context) {
|
||||
page := h.GetInt(c, "page", 1)
|
||||
pageSize := h.GetInt(c, "page_size", 20)
|
||||
code := c.Query("code")
|
||||
status := h.GetInt(c, "status", -1)
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if code != "" {
|
||||
session.Where("code LIKE ?", "%"+code+"%")
|
||||
}
|
||||
if status == 0 {
|
||||
session.Where("redeem_at = ?", 0)
|
||||
} else if status == 1 {
|
||||
session.Where("redeem_at > ?", 0)
|
||||
}
|
||||
|
||||
var total int64
|
||||
session.Model(&model.Redeem{}).Count(&total)
|
||||
var redeems []model.Redeem
|
||||
offset := (page - 1) * pageSize
|
||||
err := session.Order("id DESC").Offset(offset).Limit(pageSize).Find(&redeems).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
var items = make([]vo.Redeem, 0)
|
||||
userIds := make([]uint, 0)
|
||||
for _, v := range redeems {
|
||||
userIds = append(userIds, v.UserId)
|
||||
}
|
||||
var users []model.User
|
||||
h.DB.Where("id IN ?", userIds).Find(&users)
|
||||
var userMap = make(map[uint]model.User)
|
||||
for _, u := range users {
|
||||
userMap[u.Id] = u
|
||||
}
|
||||
|
||||
for _, v := range redeems {
|
||||
var r vo.Redeem
|
||||
err = utils.CopyObject(v, &r)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
r.Id = v.Id
|
||||
r.Username = userMap[v.UserId].Username
|
||||
r.CreatedAt = v.CreatedAt.Unix()
|
||||
items = append(items, r)
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, vo.NewPage(total, page, pageSize, items))
|
||||
}
|
||||
|
||||
func (h *RedeemHandler) Create(c *gin.Context) {
|
||||
var data struct {
|
||||
Name string `json:"name"`
|
||||
Power int `json:"power"`
|
||||
Num int `json:"num"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
counter := 0
|
||||
codes := make([]string, 0)
|
||||
var errMsg = ""
|
||||
if data.Num > 0 {
|
||||
for i := 0; i < data.Num; i++ {
|
||||
code, err := utils.GenRedeemCode(32)
|
||||
if err != nil {
|
||||
errMsg = err.Error()
|
||||
continue
|
||||
}
|
||||
err = h.DB.Create(&model.Redeem{
|
||||
Code: code,
|
||||
Name: data.Name,
|
||||
Power: data.Power,
|
||||
Enabled: true,
|
||||
}).Error
|
||||
if err != nil {
|
||||
errMsg = err.Error()
|
||||
continue
|
||||
}
|
||||
codes = append(codes, code)
|
||||
counter++
|
||||
}
|
||||
}
|
||||
if counter == 0 {
|
||||
resp.ERROR(c, errMsg)
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, gin.H{
|
||||
"counter": counter,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *RedeemHandler) 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
|
||||
}
|
||||
|
||||
err := h.DB.Model(&model.Redeem{}).Where("id = ?", data.Id).Update(data.Filed, data.Value).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *RedeemHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
if id <= 0 {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
err := h.DB.Where("id", id).Delete(&model.Redeem{}).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package admin
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type RewardHandler struct {
|
||||
handler.BaseHandler
|
||||
}
|
||||
|
||||
func NewRewardHandler(app *core.AppServer, db *gorm.DB) *RewardHandler {
|
||||
return &RewardHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
func (h *RewardHandler) List(c *gin.Context) {
|
||||
var items []model.Reward
|
||||
res := h.DB.Order("id DESC").Find(&items)
|
||||
var rewards = make([]vo.Reward, 0)
|
||||
if res.Error == nil {
|
||||
userIds := make([]uint, 0)
|
||||
for _, v := range items {
|
||||
userIds = append(userIds, v.UserId)
|
||||
}
|
||||
var users []model.User
|
||||
h.DB.Where("id IN ?", userIds).Find(&users)
|
||||
var userMap = make(map[uint]model.User)
|
||||
for _, u := range users {
|
||||
userMap[u.Id] = u
|
||||
}
|
||||
|
||||
for _, v := range items {
|
||||
var r vo.Reward
|
||||
err := utils.CopyObject(v, &r)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
r.Id = v.Id
|
||||
r.Username = userMap[v.UserId].Username
|
||||
r.CreatedAt = v.CreatedAt.Unix()
|
||||
r.UpdatedAt = v.UpdatedAt.Unix()
|
||||
rewards = append(rewards, r)
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, rewards)
|
||||
}
|
||||
|
||||
func (h *RewardHandler) Remove(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
if data.Id > 0 {
|
||||
res := h.DB.Where("id = ?", data.Id).Delete(&model.Reward{})
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package admin
|
||||
|
||||
import "geekai/core/types"
|
||||
|
||||
var Platforms = []types.Platform{
|
||||
types.OpenAI,
|
||||
types.QWen,
|
||||
types.XunFei,
|
||||
types.ChatGLM,
|
||||
types.Baidu,
|
||||
types.Azure,
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -26,10 +27,11 @@ import (
|
||||
type UserHandler struct {
|
||||
handler.BaseHandler
|
||||
licenseService *service.LicenseService
|
||||
redis *redis.Client
|
||||
}
|
||||
|
||||
func NewUserHandler(app *core.AppServer, db *gorm.DB, licenseService *service.LicenseService) *UserHandler {
|
||||
return &UserHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}, licenseService: licenseService}
|
||||
func NewUserHandler(app *core.AppServer, db *gorm.DB, licenseService *service.LicenseService, redisCli *redis.Client) *UserHandler {
|
||||
return &UserHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}, licenseService: licenseService, redis: redisCli}
|
||||
}
|
||||
|
||||
// List 用户列表
|
||||
@@ -49,7 +51,7 @@ func (h *UserHandler) List(c *gin.Context) {
|
||||
}
|
||||
|
||||
session.Model(&model.User{}).Count(&total)
|
||||
res := session.Offset(offset).Limit(pageSize).Find(&items)
|
||||
res := session.Offset(offset).Limit(pageSize).Order("id DESC").Find(&items)
|
||||
if res.Error == nil {
|
||||
for _, item := range items {
|
||||
var user vo.User
|
||||
@@ -73,6 +75,8 @@ func (h *UserHandler) Save(c *gin.Context) {
|
||||
Id uint `json:"id"`
|
||||
Password string `json:"password"`
|
||||
Username string `json:"username"`
|
||||
Mobile string `json:"mobile"`
|
||||
Email string `json:"email"`
|
||||
ChatRoles []string `json:"chat_roles"`
|
||||
ChatModels []int `json:"chat_models"`
|
||||
ExpiredTime string `json:"expired_time"`
|
||||
@@ -102,6 +106,8 @@ func (h *UserHandler) Save(c *gin.Context) {
|
||||
}
|
||||
var oldPower = user.Power
|
||||
user.Username = data.Username
|
||||
user.Email = data.Email
|
||||
user.Mobile = data.Mobile
|
||||
user.Status = data.Status
|
||||
user.Vip = data.Vip
|
||||
user.Power = data.Power
|
||||
@@ -109,10 +115,11 @@ func (h *UserHandler) Save(c *gin.Context) {
|
||||
user.ChatModels = utils.JsonEncode(data.ChatModels)
|
||||
user.ExpiredTime = utils.Str2stamp(data.ExpiredTime)
|
||||
|
||||
res = h.DB.Select("username", "status", "vip", "power", "chat_roles_json", "chat_models_json", "expired_time").Updates(&user)
|
||||
res = h.DB.Select("username", "mobile", "email", "status", "vip", "power", "chat_roles_json", "chat_models_json", "expired_time").Updates(&user)
|
||||
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
// 记录算力日志
|
||||
@@ -135,11 +142,27 @@ func (h *UserHandler) Save(c *gin.Context) {
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
// 如果禁用了用户,则将用户踢下线
|
||||
if user.Status == false {
|
||||
key := fmt.Sprintf("users/%v", user.Id)
|
||||
if _, err := h.redis.Del(c, key).Result(); err != nil {
|
||||
logger.Error("error with delete session: ", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 检查用户是否已经存在
|
||||
h.DB.Where("username", data.Username).First(&user)
|
||||
if user.Id > 0 {
|
||||
resp.ERROR(c, "用户名已存在")
|
||||
return
|
||||
}
|
||||
|
||||
salt := utils.RandString(8)
|
||||
u := model.User{
|
||||
Username: data.Username,
|
||||
Password: utils.GenPassword(data.Password, salt),
|
||||
Mobile: data.Mobile,
|
||||
Email: data.Email,
|
||||
Avatar: "/images/avatar/user.png",
|
||||
Salt: salt,
|
||||
Power: data.Power,
|
||||
@@ -161,8 +184,7 @@ func (h *UserHandler) Save(c *gin.Context) {
|
||||
}
|
||||
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败")
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -198,33 +220,69 @@ func (h *UserHandler) ResetPass(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *UserHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
if id <= 0 {
|
||||
id := c.Query("id")
|
||||
ids := c.QueryArray("ids[]")
|
||||
if id != "" {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
// 删除用户
|
||||
res := h.DB.Where("id = ?", id).Delete(&model.User{})
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "删除失败")
|
||||
|
||||
tx := h.DB.Begin()
|
||||
var err error
|
||||
for _, id = range ids {
|
||||
// 删除用户
|
||||
if err = tx.Where("id", id).Delete(&model.User{}).Error; err != nil {
|
||||
break
|
||||
}
|
||||
// 删除聊天记录
|
||||
if err = tx.Unscoped().Where("user_id = ?", id).Delete(&model.ChatItem{}).Error; err != nil {
|
||||
break
|
||||
}
|
||||
// 删除聊天历史记录
|
||||
if err = tx.Unscoped().Where("user_id = ?", id).Delete(&model.ChatMessage{}).Error; err != nil {
|
||||
break
|
||||
}
|
||||
// 删除登录日志
|
||||
if err = tx.Where("user_id = ?", id).Delete(&model.UserLoginLog{}).Error; err != nil {
|
||||
break
|
||||
}
|
||||
// 删除算力日志
|
||||
if err = tx.Where("user_id = ?", id).Delete(&model.PowerLog{}).Error; err != nil {
|
||||
break
|
||||
}
|
||||
if err = tx.Where("user_id = ?", id).Delete(&model.InviteLog{}).Error; err != nil {
|
||||
break
|
||||
}
|
||||
// 删除众筹日志
|
||||
if err = tx.Where("user_id = ?", id).Delete(&model.Redeem{}).Error; err != nil {
|
||||
break
|
||||
}
|
||||
// 删除绘图任务
|
||||
if err = tx.Where("user_id = ?", id).Delete(&model.MidJourneyJob{}).Error; err != nil {
|
||||
break
|
||||
}
|
||||
if err = tx.Where("user_id = ?", id).Delete(&model.SdJob{}).Error; err != nil {
|
||||
break
|
||||
}
|
||||
if err = tx.Where("user_id = ?", id).Delete(&model.DallJob{}).Error; err != nil {
|
||||
break
|
||||
}
|
||||
if err = tx.Where("user_id = ?", id).Delete(&model.SunoJob{}).Error; err != nil {
|
||||
break
|
||||
}
|
||||
if err = tx.Where("user_id = ?", id).Delete(&model.VideoJob{}).Error; err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
tx.Rollback()
|
||||
return
|
||||
}
|
||||
|
||||
// 删除聊天记录
|
||||
h.DB.Where("user_id = ?", id).Delete(&model.ChatItem{})
|
||||
// 删除聊天历史记录
|
||||
h.DB.Where("user_id = ?", id).Delete(&model.ChatMessage{})
|
||||
// 删除登录日志
|
||||
h.DB.Where("user_id = ?", id).Delete(&model.UserLoginLog{})
|
||||
// 删除算力日志
|
||||
h.DB.Where("user_id = ?", id).Delete(&model.PowerLog{})
|
||||
// 删除众筹日志
|
||||
h.DB.Where("user_id = ?", id).Delete(&model.Reward{})
|
||||
// 删除绘图任务
|
||||
h.DB.Where("user_id = ?", id).Delete(&model.MidJourneyJob{})
|
||||
h.DB.Where("user_id = ?", id).Delete(&model.SdJob{})
|
||||
// 删除订单
|
||||
h.DB.Where("user_id = ?", id).Delete(&model.Order{})
|
||||
tx.Commit()
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,13 +8,13 @@ package handler
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
logger2 "geekai/logger"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"errors"
|
||||
"fmt"
|
||||
"gorm.io/gorm"
|
||||
"strings"
|
||||
|
||||
@@ -85,7 +85,7 @@ func (h *BaseHandler) GetLoginUser(c *gin.Context) (model.User, error) {
|
||||
}
|
||||
|
||||
var user model.User
|
||||
res := h.DB.First(&user, userId)
|
||||
res := h.DB.Where("id", userId).First(&user)
|
||||
// 更新缓存
|
||||
if res.Error == nil {
|
||||
c.Set(types.LoginUserCache, user)
|
||||
|
||||
44
api/handler/chat_app_type_handler.go
Normal file
44
api/handler/chat_app_type_handler.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ChatAppTypeHandler struct {
|
||||
BaseHandler
|
||||
}
|
||||
|
||||
func NewChatAppTypeHandler(app *core.AppServer, db *gorm.DB) *ChatAppTypeHandler {
|
||||
return &ChatAppTypeHandler{BaseHandler: BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// List 获取App类型列表
|
||||
func (h *ChatAppTypeHandler) List(c *gin.Context) {
|
||||
var items []model.AppType
|
||||
var appTypes = make([]vo.AppType, 0)
|
||||
err := h.DB.Where("enabled", true).Order("sort_num ASC").Find(&items).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, v := range items {
|
||||
var appType vo.AppType
|
||||
err = utils.CopyObject(v, &appType)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
appType.Id = v.Id
|
||||
appType.CreatedAt = v.CreatedAt.Unix()
|
||||
appTypes = append(appTypes, appType)
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, appTypes)
|
||||
}
|
||||
522
api/handler/chat_handler.go
Normal file
522
api/handler/chat_handler.go
Normal file
@@ -0,0 +1,522 @@
|
||||
package handler
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/service/oss"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ChatHandler struct {
|
||||
BaseHandler
|
||||
redis *redis.Client
|
||||
uploadManager *oss.UploaderManager
|
||||
licenseService *service.LicenseService
|
||||
ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function
|
||||
ChatContexts *types.LMap[string, []types.Message] // 聊天上下文 Map [chatId] => []Message
|
||||
userService *service.UserService
|
||||
}
|
||||
|
||||
func NewChatHandler(app *core.AppServer, db *gorm.DB, redis *redis.Client, manager *oss.UploaderManager, licenseService *service.LicenseService, userService *service.UserService) *ChatHandler {
|
||||
return &ChatHandler{
|
||||
BaseHandler: BaseHandler{App: app, DB: db},
|
||||
redis: redis,
|
||||
uploadManager: manager,
|
||||
licenseService: licenseService,
|
||||
ReqCancelFunc: types.NewLMap[string, context.CancelFunc](),
|
||||
ChatContexts: types.NewLMap[string, []types.Message](),
|
||||
userService: userService,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSession, role model.ChatRole, prompt string, ws *types.WsClient) error {
|
||||
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)
|
||||
if res.Error != nil {
|
||||
return errors.New("未授权用户,您正在进行非法操作!")
|
||||
}
|
||||
var userVo vo.User
|
||||
err := utils.CopyObject(user, &userVo)
|
||||
userVo.Id = user.Id
|
||||
if err != nil {
|
||||
return errors.New("User 对象转换失败," + err.Error())
|
||||
}
|
||||
|
||||
if userVo.Status == false {
|
||||
return errors.New("您的账号已经被禁用,如果疑问,请联系管理员!")
|
||||
}
|
||||
|
||||
if userVo.Power < session.Model.Power {
|
||||
return fmt.Errorf("您当前剩余算力 %d 已不足以支付当前模型的单次对话需要消耗的算力 %d,[立即购买](/member)。", userVo.Power, session.Model.Power)
|
||||
}
|
||||
|
||||
if userVo.ExpiredTime > 0 && userVo.ExpiredTime <= time.Now().Unix() {
|
||||
return errors.New("您的账号已经过期,请联系管理员!")
|
||||
}
|
||||
|
||||
// 检查 prompt 长度是否超过了当前模型允许的最大上下文长度
|
||||
promptTokens, err := utils.CalcTokens(prompt, session.Model.Value)
|
||||
if promptTokens > session.Model.MaxContext {
|
||||
|
||||
return errors.New("对话内容超出了当前模型允许的最大上下文长度!")
|
||||
}
|
||||
|
||||
var req = types.ApiRequest{
|
||||
Model: session.Model.Value,
|
||||
}
|
||||
// 兼容 GPT-O1 模型
|
||||
if strings.HasPrefix(session.Model.Value, "o1-") {
|
||||
utils.SendChunkMsg(ws, "AI 正在思考...\n")
|
||||
req.Stream = false
|
||||
session.Start = time.Now().Unix()
|
||||
} else {
|
||||
req.MaxTokens = session.Model.MaxTokens
|
||||
req.Temperature = session.Model.Temperature
|
||||
req.Stream = session.Stream
|
||||
}
|
||||
|
||||
if len(session.Tools) > 0 && !strings.HasPrefix(session.Model.Value, "o1-") {
|
||||
var items []model.Function
|
||||
res = h.DB.Where("enabled", true).Where("id IN ?", session.Tools).Find(&items)
|
||||
if res.Error == nil {
|
||||
var tools = make([]types.Tool, 0)
|
||||
for _, v := range items {
|
||||
var parameters map[string]interface{}
|
||||
err = utils.JsonDecode(v.Parameters, ¶meters)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
tool := types.Tool{
|
||||
Type: "function",
|
||||
Function: types.Function{
|
||||
Name: v.Name,
|
||||
Description: v.Description,
|
||||
Parameters: parameters,
|
||||
},
|
||||
}
|
||||
if v, ok := parameters["required"]; v == nil || !ok {
|
||||
tool.Function.Parameters["required"] = []string{}
|
||||
}
|
||||
tools = append(tools, tool)
|
||||
}
|
||||
|
||||
if len(tools) > 0 {
|
||||
req.Tools = tools
|
||||
req.ToolChoice = "auto"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载聊天上下文
|
||||
chatCtx := make([]types.Message, 0)
|
||||
messages := make([]types.Message, 0)
|
||||
if h.App.SysConfig.EnableContext {
|
||||
if h.ChatContexts.Has(session.ChatId) {
|
||||
messages = h.ChatContexts.Get(session.ChatId)
|
||||
} else {
|
||||
_ = utils.JsonDecode(role.Context, &messages)
|
||||
if h.App.SysConfig.ContextDeep > 0 {
|
||||
var historyMessages []model.ChatMessage
|
||||
res := h.DB.Where("chat_id = ? and use_context = 1", session.ChatId).Limit(h.App.SysConfig.ContextDeep).Order("id DESC").Find(&historyMessages)
|
||||
if res.Error == nil {
|
||||
for i := len(historyMessages) - 1; i >= 0; i-- {
|
||||
msg := historyMessages[i]
|
||||
ms := types.Message{Role: "user", Content: msg.Content}
|
||||
if msg.Type == types.ReplyMsg {
|
||||
ms.Role = "assistant"
|
||||
}
|
||||
chatCtx = append(chatCtx, ms)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 计算当前请求的 token 总长度,确保不会超出最大上下文长度
|
||||
// MaxContextLength = Response + Tool + Prompt + Context
|
||||
tokens := req.MaxTokens // 最大响应长度
|
||||
tks, _ := utils.CalcTokens(utils.JsonEncode(req.Tools), req.Model)
|
||||
tokens += tks + promptTokens
|
||||
|
||||
for i := len(messages) - 1; i >= 0; i-- {
|
||||
v := messages[i]
|
||||
tks, _ = utils.CalcTokens(v.Content, req.Model)
|
||||
// 上下文 token 超出了模型的最大上下文长度
|
||||
if tokens+tks >= session.Model.MaxContext {
|
||||
break
|
||||
}
|
||||
|
||||
// 上下文的深度超出了模型的最大上下文深度
|
||||
if len(chatCtx) >= h.App.SysConfig.ContextDeep {
|
||||
break
|
||||
}
|
||||
|
||||
tokens += tks
|
||||
chatCtx = append(chatCtx, v)
|
||||
}
|
||||
|
||||
logger.Debugf("聊天上下文:%+v", chatCtx)
|
||||
}
|
||||
reqMgs := make([]interface{}, 0)
|
||||
for _, m := range chatCtx {
|
||||
reqMgs = append(reqMgs, m)
|
||||
}
|
||||
|
||||
fullPrompt := prompt
|
||||
text := prompt
|
||||
// extract files in prompt
|
||||
files := utils.ExtractFileURLs(prompt)
|
||||
logger.Debugf("detected FILES: %+v", files)
|
||||
// 如果不是逆向模型,则提取文件内容
|
||||
if len(files) > 0 && !(session.Model.Value == "gpt-4-all" ||
|
||||
strings.HasPrefix(session.Model.Value, "gpt-4-gizmo") ||
|
||||
strings.HasSuffix(session.Model.Value, "claude-3")) {
|
||||
contents := make([]string, 0)
|
||||
var file model.File
|
||||
for _, v := range files {
|
||||
h.DB.Where("url = ?", v).First(&file)
|
||||
content, err := utils.ReadFileContent(v, h.App.Config.TikaHost)
|
||||
if err != nil {
|
||||
logger.Error("error with read file: ", err)
|
||||
} else {
|
||||
contents = append(contents, fmt.Sprintf("%s 文件内容:%s", file.Name, content))
|
||||
}
|
||||
text = strings.Replace(text, v, "", 1)
|
||||
}
|
||||
if len(contents) > 0 {
|
||||
fullPrompt = fmt.Sprintf("请根据提供的文件内容信息回答问题(其中Excel 已转成 HTML):\n\n %s\n\n 问题:%s", strings.Join(contents, "\n"), text)
|
||||
}
|
||||
|
||||
tokens, _ := utils.CalcTokens(fullPrompt, req.Model)
|
||||
if tokens > session.Model.MaxContext {
|
||||
return fmt.Errorf("文件的长度超出模型允许的最大上下文长度,请减少文件内容数量或文件大小。")
|
||||
}
|
||||
}
|
||||
logger.Debug("最终Prompt:", fullPrompt)
|
||||
|
||||
// extract images from prompt
|
||||
imgURLs := utils.ExtractImgURLs(prompt)
|
||||
logger.Debugf("detected IMG: %+v", imgURLs)
|
||||
var content interface{}
|
||||
if len(imgURLs) > 0 {
|
||||
data := make([]interface{}, 0)
|
||||
for _, v := range imgURLs {
|
||||
text = strings.Replace(text, v, "", 1)
|
||||
data = append(data, gin.H{
|
||||
"type": "image_url",
|
||||
"image_url": gin.H{
|
||||
"url": v,
|
||||
},
|
||||
})
|
||||
}
|
||||
data = append(data, gin.H{
|
||||
"type": "text",
|
||||
"text": strings.TrimSpace(text),
|
||||
})
|
||||
content = data
|
||||
} else {
|
||||
content = fullPrompt
|
||||
}
|
||||
req.Messages = append(reqMgs, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": content,
|
||||
})
|
||||
|
||||
logger.Debugf("%+v", req.Messages)
|
||||
|
||||
return h.sendOpenAiMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
|
||||
}
|
||||
|
||||
// Tokens 统计 token 数量
|
||||
func (h *ChatHandler) Tokens(c *gin.Context) {
|
||||
var data struct {
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
// 如果没有传入 text 字段,则说明是获取当前 reply 总的 token 消耗(带上下文)
|
||||
//if data.Text == "" && data.ChatId != "" {
|
||||
// var item model.ChatMessage
|
||||
// userId, _ := c.Get(types.LoginUserID)
|
||||
// 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
|
||||
// }
|
||||
// resp.SUCCESS(c, item.Tokens)
|
||||
// return
|
||||
//}
|
||||
|
||||
tokens, err := utils.CalcTokens(data.Text, data.Model)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, tokens)
|
||||
}
|
||||
|
||||
func getTotalTokens(req types.ApiRequest) int {
|
||||
encode := utils.JsonEncode(req.Messages)
|
||||
var items []map[string]interface{}
|
||||
err := utils.JsonDecode(encode, &items)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
tokens := 0
|
||||
for _, item := range items {
|
||||
content, ok := item["content"]
|
||||
if ok && !utils.IsEmptyValue(content) {
|
||||
t, err := utils.CalcTokens(utils.InterfaceToString(content), req.Model)
|
||||
if err == nil {
|
||||
tokens += t
|
||||
}
|
||||
}
|
||||
}
|
||||
return tokens
|
||||
}
|
||||
|
||||
// StopGenerate 停止生成
|
||||
func (h *ChatHandler) StopGenerate(c *gin.Context) {
|
||||
sessionId := c.Query("session_id")
|
||||
if h.ReqCancelFunc.Has(sessionId) {
|
||||
h.ReqCancelFunc.Get(sessionId)()
|
||||
h.ReqCancelFunc.Delete(sessionId)
|
||||
}
|
||||
resp.SUCCESS(c, types.OkMsg)
|
||||
}
|
||||
|
||||
// 发送请求到 OpenAI 服务器
|
||||
// useOwnApiKey: 是否使用了用户自己的 API KEY
|
||||
func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, session *types.ChatSession, apiKey *model.ApiKey) (*http.Response, error) {
|
||||
// if the chat model bind a KEY, use it directly
|
||||
if session.Model.KeyId > 0 {
|
||||
h.DB.Where("id", session.Model.KeyId).Find(apiKey)
|
||||
}
|
||||
// use the last unused key
|
||||
if apiKey.Id == 0 {
|
||||
h.DB.Where("type", "chat").Where("enabled", true).Order("last_used_at ASC").First(apiKey)
|
||||
}
|
||||
if apiKey.Id == 0 {
|
||||
return nil, errors.New("no available key, please import key")
|
||||
}
|
||||
|
||||
// ONLY allow apiURL in blank list
|
||||
err := h.licenseService.IsValidApiURL(apiKey.ApiURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logger.Debugf("对话请求消息体:%+v", req)
|
||||
|
||||
apiURL := fmt.Sprintf("%s/v1/chat/completions", apiKey.ApiURL)
|
||||
// 创建 HttpClient 请求对象
|
||||
var client *http.Client
|
||||
requestBody, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
request, err := http.NewRequest(http.MethodPost, apiURL, bytes.NewBuffer(requestBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request = request.WithContext(ctx)
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
if len(apiKey.ProxyURL) > 5 { // 使用代理
|
||||
proxy, _ := url.Parse(apiKey.ProxyURL)
|
||||
client = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyURL(proxy),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
logger.Debugf("Sending %s request, API KEY:%s, PROXY: %s, Model: %s", apiKey.ApiURL, apiURL, apiKey.ProxyURL, req.Model)
|
||||
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey.Value))
|
||||
// 更新API KEY 最后使用时间
|
||||
h.DB.Model(&model.ApiKey{}).Where("id", apiKey.Id).UpdateColumn("last_used_at", time.Now().Unix())
|
||||
return client.Do(request)
|
||||
}
|
||||
|
||||
// 扣减用户算力
|
||||
func (h *ChatHandler) subUserPower(userVo vo.User, session *types.ChatSession, promptTokens int, replyTokens int) {
|
||||
power := 1
|
||||
if session.Model.Power > 0 {
|
||||
power = session.Model.Power
|
||||
}
|
||||
|
||||
err := h.userService.DecreasePower(int(userVo.Id), power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: session.Model.Value,
|
||||
Remark: fmt.Sprintf("模型名称:%s, 提问长度:%d,回复长度:%d", session.Model.Name, promptTokens, replyTokens),
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ChatHandler) saveChatHistory(
|
||||
req types.ApiRequest,
|
||||
usage Usage,
|
||||
message types.Message,
|
||||
chatCtx []types.Message,
|
||||
session *types.ChatSession,
|
||||
role model.ChatRole,
|
||||
userVo vo.User,
|
||||
promptCreatedAt time.Time,
|
||||
replyCreatedAt time.Time) {
|
||||
|
||||
useMsg := types.Message{Role: "user", Content: usage.Prompt}
|
||||
// 更新上下文消息,如果是调用函数则不需要更新上下文
|
||||
if h.App.SysConfig.EnableContext {
|
||||
chatCtx = append(chatCtx, useMsg) // 提问消息
|
||||
chatCtx = append(chatCtx, message) // 回复消息
|
||||
h.ChatContexts.Put(session.ChatId, chatCtx)
|
||||
}
|
||||
|
||||
// 追加聊天记录
|
||||
// for prompt
|
||||
var promptTokens, replyTokens, totalTokens int
|
||||
if usage.PromptTokens > 0 {
|
||||
promptTokens = usage.PromptTokens
|
||||
} else {
|
||||
promptTokens, _ = utils.CalcTokens(usage.Content, req.Model)
|
||||
}
|
||||
|
||||
historyUserMsg := model.ChatMessage{
|
||||
UserId: userVo.Id,
|
||||
ChatId: session.ChatId,
|
||||
RoleId: role.Id,
|
||||
Type: types.PromptMsg,
|
||||
Icon: userVo.Avatar,
|
||||
Content: template.HTMLEscapeString(usage.Prompt),
|
||||
Tokens: promptTokens,
|
||||
TotalTokens: promptTokens,
|
||||
UseContext: true,
|
||||
Model: req.Model,
|
||||
}
|
||||
historyUserMsg.CreatedAt = promptCreatedAt
|
||||
historyUserMsg.UpdatedAt = promptCreatedAt
|
||||
err := h.DB.Save(&historyUserMsg).Error
|
||||
if err != nil {
|
||||
logger.Error("failed to save prompt history message: ", err)
|
||||
}
|
||||
|
||||
// for reply
|
||||
// 计算本次对话消耗的总 token 数量
|
||||
if usage.CompletionTokens > 0 {
|
||||
replyTokens = usage.CompletionTokens
|
||||
totalTokens = usage.TotalTokens
|
||||
} else {
|
||||
replyTokens, _ = utils.CalcTokens(message.Content, req.Model)
|
||||
totalTokens = replyTokens + getTotalTokens(req)
|
||||
}
|
||||
historyReplyMsg := model.ChatMessage{
|
||||
UserId: userVo.Id,
|
||||
ChatId: session.ChatId,
|
||||
RoleId: role.Id,
|
||||
Type: types.ReplyMsg,
|
||||
Icon: role.Icon,
|
||||
Content: usage.Content,
|
||||
Tokens: replyTokens,
|
||||
TotalTokens: totalTokens,
|
||||
UseContext: true,
|
||||
Model: req.Model,
|
||||
}
|
||||
historyReplyMsg.CreatedAt = replyCreatedAt
|
||||
historyReplyMsg.UpdatedAt = replyCreatedAt
|
||||
err = h.DB.Create(&historyReplyMsg).Error
|
||||
if err != nil {
|
||||
logger.Error("failed to save reply history message: ", err)
|
||||
}
|
||||
|
||||
// 更新用户算力
|
||||
if session.Model.Power > 0 {
|
||||
h.subUserPower(userVo, session, promptTokens, replyTokens)
|
||||
}
|
||||
// 保存当前会话
|
||||
var chatItem model.ChatItem
|
||||
err = h.DB.Where("chat_id = ?", session.ChatId).First(&chatItem).Error
|
||||
if err != nil {
|
||||
chatItem.ChatId = session.ChatId
|
||||
chatItem.UserId = userVo.Id
|
||||
chatItem.RoleId = role.Id
|
||||
chatItem.ModelId = session.Model.Id
|
||||
if utf8.RuneCountInString(usage.Prompt) > 30 {
|
||||
chatItem.Title = string([]rune(usage.Prompt)[:30]) + "..."
|
||||
} else {
|
||||
chatItem.Title = usage.Prompt
|
||||
}
|
||||
chatItem.Model = req.Model
|
||||
err = h.DB.Create(&chatItem).Error
|
||||
if err != nil {
|
||||
logger.Error("failed to save chat item: ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 将AI回复消息中生成的图片链接下载到本地
|
||||
func (h *ChatHandler) extractImgUrl(text string) string {
|
||||
pattern := `!\[([^\]]*)]\(([^)]+)\)`
|
||||
re := regexp.MustCompile(pattern)
|
||||
matches := re.FindAllStringSubmatch(text, -1)
|
||||
|
||||
// 下载图片并替换链接地址
|
||||
for _, match := range matches {
|
||||
imageURL := match[2]
|
||||
logger.Debug(imageURL)
|
||||
// 对于相同地址的图片,已经被替换了,就不再重复下载了
|
||||
if !strings.Contains(text, imageURL) {
|
||||
continue
|
||||
}
|
||||
|
||||
newImgURL, err := h.uploadManager.GetUploadHandler().PutUrlFile(imageURL, false)
|
||||
if err != nil {
|
||||
logger.Error("error with download image: ", err)
|
||||
continue
|
||||
}
|
||||
|
||||
text = strings.ReplaceAll(text, imageURL, newImgURL)
|
||||
}
|
||||
return text
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package chatimpl
|
||||
package handler
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
@@ -28,31 +28,40 @@ func (h *ChatHandler) List(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
var items = make([]vo.ChatItem, 0)
|
||||
var chats []model.ChatItem
|
||||
res := h.DB.Where("user_id = ?", userId).Order("id DESC").Find(&chats)
|
||||
if res.Error == nil {
|
||||
var roleIds = make([]uint, 0)
|
||||
for _, chat := range chats {
|
||||
roleIds = append(roleIds, chat.RoleId)
|
||||
}
|
||||
var roles []model.ChatRole
|
||||
res = h.DB.Find(&roles, roleIds)
|
||||
if res.Error == nil {
|
||||
roleMap := make(map[uint]model.ChatRole)
|
||||
for _, role := range roles {
|
||||
roleMap[role.Id] = role
|
||||
}
|
||||
h.DB.Where("user_id", userId).Order("id DESC").Find(&chats)
|
||||
if len(chats) == 0 {
|
||||
resp.SUCCESS(c, items)
|
||||
return
|
||||
}
|
||||
|
||||
for _, chat := range chats {
|
||||
var item vo.ChatItem
|
||||
err := utils.CopyObject(chat, &item)
|
||||
if err == nil {
|
||||
item.Id = chat.Id
|
||||
item.Icon = roleMap[chat.RoleId].Icon
|
||||
items = append(items, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
var roleIds = make([]uint, 0)
|
||||
var modelValues = make([]string, 0)
|
||||
for _, chat := range chats {
|
||||
roleIds = append(roleIds, chat.RoleId)
|
||||
modelValues = append(modelValues, chat.Model)
|
||||
}
|
||||
|
||||
var roles []model.ChatRole
|
||||
var models []model.ChatModel
|
||||
roleMap := make(map[uint]model.ChatRole)
|
||||
modelMap := make(map[string]model.ChatModel)
|
||||
h.DB.Where("id IN ?", roleIds).Find(&roles)
|
||||
h.DB.Where("value IN ?", modelValues).Find(&models)
|
||||
for _, role := range roles {
|
||||
roleMap[role.Id] = role
|
||||
}
|
||||
for _, m := range models {
|
||||
modelMap[m.Value] = m
|
||||
}
|
||||
for _, chat := range chats {
|
||||
var item vo.ChatItem
|
||||
err := utils.CopyObject(chat, &item)
|
||||
if err == nil {
|
||||
item.Id = chat.Id
|
||||
item.Icon = roleMap[chat.RoleId].Icon
|
||||
item.ModelId = modelMap[chat.Model].Id
|
||||
items = append(items, item)
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, items)
|
||||
}
|
||||
@@ -31,9 +31,14 @@ func (h *ChatModelHandler) List(c *gin.Context) {
|
||||
var items []model.ChatModel
|
||||
var chatModels = make([]vo.ChatModel, 0)
|
||||
var res *gorm.DB
|
||||
session := h.DB.Session(&gorm.Session{}).Where("enabled", true)
|
||||
t := c.Query("type")
|
||||
if t != "" {
|
||||
session = session.Where("type", t)
|
||||
}
|
||||
// 如果用户没有登录,则加载所有开放模型
|
||||
if !h.IsLogin(c) {
|
||||
res = h.DB.Where("enabled", true).Where("open", true).Order("sort_num ASC").Find(&items)
|
||||
res = session.Where("open", true).Order("sort_num ASC").Find(&items)
|
||||
} else {
|
||||
user, _ := h.GetLoginUser(c)
|
||||
var models []int
|
||||
|
||||
@@ -29,45 +29,63 @@ func NewChatRoleHandler(app *core.AppServer, db *gorm.DB) *ChatRoleHandler {
|
||||
|
||||
// List 获取用户聊天应用列表
|
||||
func (h *ChatRoleHandler) List(c *gin.Context) {
|
||||
all := h.GetBool(c, "all")
|
||||
tid := h.GetInt(c, "tid", 0)
|
||||
var roles []model.ChatRole
|
||||
session := h.DB.Where("enable", true)
|
||||
if tid > 0 {
|
||||
session = session.Where("tid", tid)
|
||||
}
|
||||
err := session.Order("sort_num ASC").Find(&roles).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var roleVos = make([]vo.ChatRole, 0)
|
||||
for _, r := range roles {
|
||||
var v vo.ChatRole
|
||||
err := utils.CopyObject(r, &v)
|
||||
if err == nil {
|
||||
v.Id = r.Id
|
||||
roleVos = append(roleVos, v)
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, roleVos)
|
||||
}
|
||||
|
||||
// ListByUser 获取用户添加的角色列表
|
||||
func (h *ChatRoleHandler) ListByUser(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
userId := h.GetLoginUserId(c)
|
||||
var roles []model.ChatRole
|
||||
var roleVos = make([]vo.ChatRole, 0)
|
||||
res := h.DB.Where("enable", true).Order("sort_num ASC").Find(&roles)
|
||||
session := h.DB.Where("enable", true)
|
||||
// 如果用户没登录,则获取所有角色
|
||||
if userId > 0 {
|
||||
var user model.User
|
||||
h.DB.First(&user, userId)
|
||||
var roleKeys []string
|
||||
err := utils.JsonDecode(user.ChatRoles, &roleKeys)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "角色解析失败!")
|
||||
return
|
||||
}
|
||||
// 保证用户至少有一个角色可用
|
||||
if len(roleKeys) > 0 {
|
||||
session = session.Where("marker IN ?", roleKeys)
|
||||
}
|
||||
}
|
||||
|
||||
if id > 0 {
|
||||
session = session.Or("id", id)
|
||||
}
|
||||
res := session.Order("sort_num ASC").Find(&roles)
|
||||
if res.Error != nil {
|
||||
resp.SUCCESS(c, roleVos)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取所有角色
|
||||
if userId == 0 || all {
|
||||
// 转成 vo
|
||||
var roleVos = make([]vo.ChatRole, 0)
|
||||
for _, r := range roles {
|
||||
var v vo.ChatRole
|
||||
err := utils.CopyObject(r, &v)
|
||||
if err == nil {
|
||||
v.Id = r.Id
|
||||
roleVos = append(roleVos, v)
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, roleVos)
|
||||
return
|
||||
}
|
||||
|
||||
var user model.User
|
||||
h.DB.First(&user, userId)
|
||||
var roleKeys []string
|
||||
err := utils.JsonDecode(user.ChatRoles, &roleKeys)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "角色解析失败!")
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var roleVos = make([]vo.ChatRole, 0)
|
||||
for _, r := range roles {
|
||||
if !utils.Contains(roleKeys, r.Key) {
|
||||
continue
|
||||
}
|
||||
var v vo.ChatRole
|
||||
err := utils.CopyObject(r, &v)
|
||||
if err == nil {
|
||||
@@ -94,10 +112,9 @@ func (h *ChatRoleHandler) UpdateRole(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
res := h.DB.Model(&model.User{}).Where("id = ?", user.Id).UpdateColumn("chat_roles_json", utils.JsonEncode(data.Keys))
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
err = h.DB.Model(&model.User{}).Where("id = ?", user.Id).UpdateColumn("chat_roles_json", utils.JsonEncode(data.Keys)).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
package chatimpl
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 微软 Azure 模型消息发送实现
|
||||
|
||||
func (h *ChatHandler) sendAzureMessage(
|
||||
chatCtx []types.Message,
|
||||
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, &apiKey)
|
||||
logger.Info("HTTP请求完成,耗时:", time.Now().Sub(start))
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "context canceled") {
|
||||
return fmt.Errorf("用户取消了请求:%s", prompt)
|
||||
} else if strings.Contains(err.Error(), "no available key") {
|
||||
return errors.New("抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
|
||||
}
|
||||
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)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if !strings.Contains(line, "data:") || len(line) < 30 {
|
||||
continue
|
||||
}
|
||||
|
||||
var responseBody = types.ApiResponse{}
|
||||
err = json.Unmarshal([]byte(line[6:]), &responseBody)
|
||||
if err != nil { // 数据解析出错
|
||||
return errors.New(line)
|
||||
}
|
||||
|
||||
if len(responseBody.Choices) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// 初始化 role
|
||||
if responseBody.Choices[0].Delta.Role != "" && message.Role == "" {
|
||||
message.Role = responseBody.Choices[0].Delta.Role
|
||||
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
|
||||
continue
|
||||
} else if responseBody.Choices[0].FinishReason != "" {
|
||||
break // 输出完成或者输出中断了
|
||||
} else {
|
||||
content := responseBody.Choices[0].Delta.Content
|
||||
contents = append(contents, utils.InterfaceToString(content))
|
||||
utils.ReplyChunkMessage(ws, types.WsMessage{
|
||||
Type: types.WsMiddle,
|
||||
Content: utils.InterfaceToString(responseBody.Choices[0].Delta.Content),
|
||||
})
|
||||
}
|
||||
} // end for
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
if strings.Contains(err.Error(), "context canceled") {
|
||||
logger.Info("用户取消了请求:", prompt)
|
||||
} else {
|
||||
logger.Error("信息读取出错:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 消息发送成功
|
||||
if len(contents) > 0 {
|
||||
h.saveChatHistory(req, prompt, contents, message, chatCtx, session, role, userVo, promptCreatedAt, replyCreatedAt)
|
||||
}
|
||||
|
||||
} else {
|
||||
body, _ := io.ReadAll(response.Body)
|
||||
return fmt.Errorf("请求大模型 API 失败:%s", body)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
package chatimpl
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type baiduResp struct {
|
||||
Id string `json:"id"`
|
||||
Object string `json:"object"`
|
||||
Created int `json:"created"`
|
||||
SentenceId int `json:"sentence_id"`
|
||||
IsEnd bool `json:"is_end"`
|
||||
IsTruncated bool `json:"is_truncated"`
|
||||
Result string `json:"result"`
|
||||
NeedClearHistory bool `json:"need_clear_history"`
|
||||
Usage struct {
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
} `json:"usage"`
|
||||
}
|
||||
|
||||
// 百度文心一言消息发送实现
|
||||
|
||||
func (h *ChatHandler) sendBaiduMessage(
|
||||
chatCtx []types.Message,
|
||||
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, &apiKey)
|
||||
logger.Info("HTTP请求完成,耗时:", time.Now().Sub(start))
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
if strings.Contains(err.Error(), "context canceled") {
|
||||
return fmt.Errorf("用户取消了请求:%s", prompt)
|
||||
} else if strings.Contains(err.Error(), "no available key") {
|
||||
return errors.New("抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
|
||||
}
|
||||
return err
|
||||
} else {
|
||||
defer response.Body.Close()
|
||||
}
|
||||
|
||||
contentType := response.Header.Get("Content-Type")
|
||||
if strings.Contains(contentType, "text/event-stream") {
|
||||
replyCreatedAt := time.Now() // 记录回复时间
|
||||
// 循环读取 Chunk 消息
|
||||
var message = types.Message{}
|
||||
var contents = make([]string, 0)
|
||||
var content string
|
||||
scanner := bufio.NewScanner(response.Body)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if len(line) < 5 || strings.HasPrefix(line, "id:") {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "data:") {
|
||||
content = line[5:]
|
||||
}
|
||||
|
||||
// 处理代码换行
|
||||
if len(content) == 0 {
|
||||
content = "\n"
|
||||
}
|
||||
|
||||
var resp baiduResp
|
||||
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})
|
||||
}
|
||||
utils.ReplyChunkMessage(ws, types.WsMessage{
|
||||
Type: types.WsMiddle,
|
||||
Content: utils.InterfaceToString(resp.Result),
|
||||
})
|
||||
contents = append(contents, resp.Result)
|
||||
|
||||
if resp.IsTruncated {
|
||||
utils.ReplyMessage(ws, "AI 输出异常中断")
|
||||
break
|
||||
}
|
||||
|
||||
if resp.IsEnd {
|
||||
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.saveChatHistory(req, prompt, contents, message, chatCtx, session, role, userVo, promptCreatedAt, replyCreatedAt)
|
||||
}
|
||||
} else {
|
||||
body, _ := io.ReadAll(response.Body)
|
||||
return fmt.Errorf("请求大模型 API 失败:%s", body)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ChatHandler) getBaiduToken(apiKey string) (string, error) {
|
||||
ctx := context.Background()
|
||||
tokenString, err := h.redis.Get(ctx, apiKey).Result()
|
||||
if err == nil {
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
expr := time.Hour * 24 * 20 // access_token 有效期
|
||||
key := strings.Split(apiKey, "|")
|
||||
if len(key) != 2 {
|
||||
return "", fmt.Errorf("invalid api key: %s", apiKey)
|
||||
}
|
||||
url := fmt.Sprintf("https://aip.baidubce.com/oauth/2.0/token?client_id=%s&client_secret=%s&grant_type=client_credentials", key[0], key[1])
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("POST", url, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
req.Header.Add("Accept", "application/json")
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with send request: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with read response: %w", err)
|
||||
}
|
||||
var r map[string]interface{}
|
||||
err = json.Unmarshal(body, &r)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with parse response: %w", err)
|
||||
}
|
||||
|
||||
if r["error"] != nil {
|
||||
return "", fmt.Errorf("error with api response: %s", r["error_description"])
|
||||
}
|
||||
|
||||
tokenString = fmt.Sprintf("%s", r["access_token"])
|
||||
h.redis.Set(ctx, apiKey, tokenString, expr)
|
||||
return tokenString, nil
|
||||
}
|
||||
@@ -1,720 +0,0 @@
|
||||
package chatimpl
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
logger2 "geekai/logger"
|
||||
"geekai/service"
|
||||
"geekai/service/oss"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/gorilla/websocket"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
type ChatHandler struct {
|
||||
handler.BaseHandler
|
||||
redis *redis.Client
|
||||
uploadManager *oss.UploaderManager
|
||||
licenseService *service.LicenseService
|
||||
ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function
|
||||
ChatContexts *types.LMap[string, []types.Message] // 聊天上下文 Map [chatId] => []Message
|
||||
}
|
||||
|
||||
func NewChatHandler(app *core.AppServer, db *gorm.DB, redis *redis.Client, manager *oss.UploaderManager, licenseService *service.LicenseService) *ChatHandler {
|
||||
return &ChatHandler{
|
||||
BaseHandler: handler.BaseHandler{App: app, DB: db},
|
||||
redis: redis,
|
||||
uploadManager: manager,
|
||||
licenseService: licenseService,
|
||||
ReqCancelFunc: types.NewLMap[string, context.CancelFunc](),
|
||||
ChatContexts: types.NewLMap[string, []types.Message](),
|
||||
}
|
||||
}
|
||||
|
||||
// ChatHandle 处理聊天 WebSocket 请求
|
||||
func (h *ChatHandler) ChatHandle(c *gin.Context) {
|
||||
ws, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}).Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
sessionId := c.Query("session_id")
|
||||
roleId := h.GetInt(c, "role_id", 0)
|
||||
chatId := c.Query("chat_id")
|
||||
modelId := h.GetInt(c, "model_id", 0)
|
||||
|
||||
client := types.NewWsClient(ws)
|
||||
var chatRole model.ChatRole
|
||||
res := h.DB.First(&chatRole, roleId)
|
||||
if res.Error != nil || !chatRole.Enable {
|
||||
utils.ReplyMessage(client, "当前聊天角色不存在或者未启用,连接已关闭!!!")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
// if the role bind a model_id, use role's bind model_id
|
||||
if chatRole.ModelId > 0 {
|
||||
modelId = chatRole.ModelId
|
||||
}
|
||||
// get model info
|
||||
var chatModel model.ChatModel
|
||||
res = h.DB.First(&chatModel, modelId)
|
||||
if res.Error != nil || chatModel.Enabled == false {
|
||||
utils.ReplyMessage(client, "当前AI模型暂未启用,连接已关闭!!!")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
session := &types.ChatSession{
|
||||
SessionId: sessionId,
|
||||
ClientIP: c.ClientIP(),
|
||||
UserId: h.GetLoginUserId(c),
|
||||
}
|
||||
|
||||
// use old chat data override the chat model and role ID
|
||||
var chat model.ChatItem
|
||||
res = h.DB.Where("chat_id = ?", chatId).First(&chat)
|
||||
if res.Error == nil {
|
||||
chatModel.Id = chat.ModelId
|
||||
roleId = int(chat.RoleId)
|
||||
}
|
||||
|
||||
session.ChatId = chatId
|
||||
session.Model = types.ChatModel{
|
||||
Id: chatModel.Id,
|
||||
Name: chatModel.Name,
|
||||
Value: chatModel.Value,
|
||||
Power: chatModel.Power,
|
||||
MaxTokens: chatModel.MaxTokens,
|
||||
MaxContext: chatModel.MaxContext,
|
||||
Temperature: chatModel.Temperature,
|
||||
KeyId: chatModel.KeyId,
|
||||
Platform: chatModel.Platform}
|
||||
logger.Infof("New websocket connected, IP: %s", c.ClientIP())
|
||||
|
||||
go func() {
|
||||
for {
|
||||
_, msg, err := client.Receive()
|
||||
if err != nil {
|
||||
logger.Debugf("close connection: %s", client.Conn.RemoteAddr())
|
||||
client.Close()
|
||||
cancelFunc := h.ReqCancelFunc.Get(sessionId)
|
||||
if cancelFunc != nil {
|
||||
cancelFunc()
|
||||
h.ReqCancelFunc.Delete(sessionId)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
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.ReqCancelFunc.Put(sessionId, cancel)
|
||||
// 回复消息
|
||||
err = h.sendMessage(ctx, session, chatRole, utils.InterfaceToString(message.Content), client)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
utils.ReplyMessage(client, err.Error())
|
||||
} else {
|
||||
utils.ReplyChunkMessage(client, types.WsMessage{Type: types.WsEnd})
|
||||
logger.Infof("回答完毕: %v", message.Content)
|
||||
}
|
||||
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSession, role model.ChatRole, prompt string, ws *types.WsClient) error {
|
||||
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)
|
||||
if res.Error != nil {
|
||||
return errors.New("未授权用户,您正在进行非法操作!")
|
||||
}
|
||||
var userVo vo.User
|
||||
err := utils.CopyObject(user, &userVo)
|
||||
userVo.Id = user.Id
|
||||
if err != nil {
|
||||
return errors.New("User 对象转换失败," + err.Error())
|
||||
}
|
||||
|
||||
if userVo.Status == false {
|
||||
return errors.New("您的账号已经被禁用,如果疑问,请联系管理员!")
|
||||
}
|
||||
|
||||
if userVo.Power < session.Model.Power {
|
||||
return fmt.Errorf("您当前剩余算力 %d 已不足以支付当前模型的单次对话需要消耗的算力 %d,[立即购买](/member)。", userVo.Power, session.Model.Power)
|
||||
}
|
||||
|
||||
if userVo.ExpiredTime > 0 && userVo.ExpiredTime <= time.Now().Unix() {
|
||||
return errors.New("您的账号已经过期,请联系管理员!")
|
||||
}
|
||||
|
||||
// 检查 prompt 长度是否超过了当前模型允许的最大上下文长度
|
||||
promptTokens, err := utils.CalcTokens(prompt, session.Model.Value)
|
||||
if promptTokens > session.Model.MaxContext {
|
||||
|
||||
return errors.New("对话内容超出了当前模型允许的最大上下文长度!")
|
||||
}
|
||||
|
||||
var req = types.ApiRequest{
|
||||
Model: session.Model.Value,
|
||||
Stream: true,
|
||||
}
|
||||
switch session.Model.Platform {
|
||||
case types.Azure.Value, types.ChatGLM.Value, types.Baidu.Value, types.XunFei.Value:
|
||||
req.Temperature = session.Model.Temperature
|
||||
req.MaxTokens = session.Model.MaxTokens
|
||||
break
|
||||
case types.OpenAI.Value:
|
||||
req.Temperature = session.Model.Temperature
|
||||
req.MaxTokens = session.Model.MaxTokens
|
||||
// OpenAI 支持函数功能
|
||||
var items []model.Function
|
||||
res := h.DB.Where("enabled", true).Find(&items)
|
||||
if res.Error != nil {
|
||||
break
|
||||
}
|
||||
|
||||
var tools = make([]types.Tool, 0)
|
||||
for _, v := range items {
|
||||
var parameters map[string]interface{}
|
||||
err = utils.JsonDecode(v.Parameters, ¶meters)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
tool := types.Tool{
|
||||
Type: "function",
|
||||
Function: types.Function{
|
||||
Name: v.Name,
|
||||
Description: v.Description,
|
||||
Parameters: parameters,
|
||||
},
|
||||
}
|
||||
if v, ok := parameters["required"]; v == nil || !ok {
|
||||
tool.Function.Parameters["required"] = []string{}
|
||||
}
|
||||
tools = append(tools, tool)
|
||||
}
|
||||
|
||||
if len(tools) > 0 {
|
||||
req.Tools = tools
|
||||
req.ToolChoice = "auto"
|
||||
}
|
||||
case types.QWen.Value:
|
||||
req.Parameters = map[string]interface{}{
|
||||
"max_tokens": session.Model.MaxTokens,
|
||||
"temperature": session.Model.Temperature,
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
return fmt.Errorf("不支持的平台:%s", session.Model.Platform)
|
||||
}
|
||||
|
||||
// 加载聊天上下文
|
||||
chatCtx := make([]types.Message, 0)
|
||||
messages := make([]types.Message, 0)
|
||||
if h.App.SysConfig.EnableContext {
|
||||
if h.ChatContexts.Has(session.ChatId) {
|
||||
messages = h.ChatContexts.Get(session.ChatId)
|
||||
} else {
|
||||
_ = utils.JsonDecode(role.Context, &messages)
|
||||
if h.App.SysConfig.ContextDeep > 0 {
|
||||
var historyMessages []model.ChatMessage
|
||||
res := h.DB.Where("chat_id = ? and use_context = 1", session.ChatId).Limit(h.App.SysConfig.ContextDeep).Order("id DESC").Find(&historyMessages)
|
||||
if res.Error == nil {
|
||||
for i := len(historyMessages) - 1; i >= 0; i-- {
|
||||
msg := historyMessages[i]
|
||||
ms := types.Message{Role: "user", Content: msg.Content}
|
||||
if msg.Type == types.ReplyMsg {
|
||||
ms.Role = "assistant"
|
||||
}
|
||||
chatCtx = append(chatCtx, ms)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 计算当前请求的 token 总长度,确保不会超出最大上下文长度
|
||||
// MaxContextLength = Response + Tool + Prompt + Context
|
||||
tokens := req.MaxTokens // 最大响应长度
|
||||
tks, _ := utils.CalcTokens(utils.JsonEncode(req.Tools), req.Model)
|
||||
tokens += tks + promptTokens
|
||||
|
||||
for _, v := range messages {
|
||||
tks, _ := utils.CalcTokens(v.Content, req.Model)
|
||||
// 上下文 token 超出了模型的最大上下文长度
|
||||
if tokens+tks >= session.Model.MaxContext {
|
||||
break
|
||||
}
|
||||
|
||||
// 上下文的深度超出了模型的最大上下文深度
|
||||
if len(chatCtx) >= h.App.SysConfig.ContextDeep {
|
||||
break
|
||||
}
|
||||
|
||||
tokens += tks
|
||||
chatCtx = append(chatCtx, v)
|
||||
}
|
||||
|
||||
logger.Debugf("聊天上下文:%+v", chatCtx)
|
||||
}
|
||||
reqMgs := make([]interface{}, 0)
|
||||
for _, m := range chatCtx {
|
||||
reqMgs = append(reqMgs, m)
|
||||
}
|
||||
|
||||
fullPrompt := prompt
|
||||
text := prompt
|
||||
// extract files in prompt
|
||||
files := utils.ExtractFileURLs(prompt)
|
||||
logger.Debugf("detected FILES: %+v", files)
|
||||
// 如果不是逆向模型,则提取文件内容
|
||||
if len(files) > 0 && !(session.Model.Value == "gpt-4-all" ||
|
||||
strings.HasPrefix(session.Model.Value, "gpt-4-gizmo") ||
|
||||
strings.HasSuffix(session.Model.Value, "claude-3")) {
|
||||
contents := make([]string, 0)
|
||||
var file model.File
|
||||
for _, v := range files {
|
||||
h.DB.Where("url = ?", v).First(&file)
|
||||
content, err := utils.ReadFileContent(v, h.App.Config.TikaHost)
|
||||
if err != nil {
|
||||
logger.Error("error with read file: ", err)
|
||||
} else {
|
||||
contents = append(contents, fmt.Sprintf("%s 文件内容:%s", file.Name, content))
|
||||
}
|
||||
text = strings.Replace(text, v, "", 1)
|
||||
}
|
||||
if len(contents) > 0 {
|
||||
fullPrompt = fmt.Sprintf("请根据提供的文件内容信息回答问题(其中Excel 已转成 HTML):\n\n %s\n\n 问题:%s", strings.Join(contents, "\n"), text)
|
||||
}
|
||||
|
||||
tokens, _ := utils.CalcTokens(fullPrompt, req.Model)
|
||||
if tokens > session.Model.MaxContext {
|
||||
return fmt.Errorf("文件的长度超出模型允许的最大上下文长度,请减少文件内容数量或文件大小。")
|
||||
}
|
||||
}
|
||||
logger.Debug("最终Prompt:", fullPrompt)
|
||||
|
||||
if session.Model.Platform == types.QWen.Value {
|
||||
req.Input = make(map[string]interface{})
|
||||
reqMgs = append(reqMgs, types.Message{
|
||||
Role: "user",
|
||||
Content: fullPrompt,
|
||||
})
|
||||
req.Input["messages"] = reqMgs
|
||||
} else if session.Model.Platform == types.OpenAI.Value || session.Model.Platform == types.Azure.Value { // extract image for gpt-vision model
|
||||
imgURLs := utils.ExtractImgURLs(prompt)
|
||||
logger.Debugf("detected IMG: %+v", imgURLs)
|
||||
var content interface{}
|
||||
if len(imgURLs) > 0 {
|
||||
data := make([]interface{}, 0)
|
||||
for _, v := range imgURLs {
|
||||
text = strings.Replace(text, v, "", 1)
|
||||
data = append(data, gin.H{
|
||||
"type": "image_url",
|
||||
"image_url": gin.H{
|
||||
"url": v,
|
||||
},
|
||||
})
|
||||
}
|
||||
data = append(data, gin.H{
|
||||
"type": "text",
|
||||
"text": strings.TrimSpace(text),
|
||||
})
|
||||
content = data
|
||||
} else {
|
||||
content = fullPrompt
|
||||
}
|
||||
req.Messages = append(reqMgs, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": content,
|
||||
})
|
||||
} else {
|
||||
req.Messages = append(reqMgs, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": fullPrompt,
|
||||
})
|
||||
}
|
||||
|
||||
logger.Debugf("%+v", req.Messages)
|
||||
|
||||
switch session.Model.Platform {
|
||||
case types.Azure.Value:
|
||||
return h.sendAzureMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
|
||||
case types.OpenAI.Value:
|
||||
return h.sendOpenAiMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
|
||||
case types.ChatGLM.Value:
|
||||
return h.sendChatGLMMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
|
||||
case types.Baidu.Value:
|
||||
return h.sendBaiduMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
|
||||
case types.XunFei.Value:
|
||||
return h.sendXunFeiMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
|
||||
case types.QWen.Value:
|
||||
return h.sendQWenMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Tokens 统计 token 数量
|
||||
func (h *ChatHandler) Tokens(c *gin.Context) {
|
||||
var data struct {
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
// 如果没有传入 text 字段,则说明是获取当前 reply 总的 token 消耗(带上下文)
|
||||
if data.Text == "" && data.ChatId != "" {
|
||||
var item model.ChatMessage
|
||||
userId, _ := c.Get(types.LoginUserID)
|
||||
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
|
||||
}
|
||||
resp.SUCCESS(c, item.Tokens)
|
||||
return
|
||||
}
|
||||
|
||||
tokens, err := utils.CalcTokens(data.Text, data.Model)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, tokens)
|
||||
}
|
||||
|
||||
func getTotalTokens(req types.ApiRequest) int {
|
||||
encode := utils.JsonEncode(req.Messages)
|
||||
var items []map[string]interface{}
|
||||
err := utils.JsonDecode(encode, &items)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
tokens := 0
|
||||
for _, item := range items {
|
||||
content, ok := item["content"]
|
||||
if ok && !utils.IsEmptyValue(content) {
|
||||
t, err := utils.CalcTokens(utils.InterfaceToString(content), req.Model)
|
||||
if err == nil {
|
||||
tokens += t
|
||||
}
|
||||
}
|
||||
}
|
||||
return tokens
|
||||
}
|
||||
|
||||
// StopGenerate 停止生成
|
||||
func (h *ChatHandler) StopGenerate(c *gin.Context) {
|
||||
sessionId := c.Query("session_id")
|
||||
if h.ReqCancelFunc.Has(sessionId) {
|
||||
h.ReqCancelFunc.Get(sessionId)()
|
||||
h.ReqCancelFunc.Delete(sessionId)
|
||||
}
|
||||
resp.SUCCESS(c, types.OkMsg)
|
||||
}
|
||||
|
||||
// 发送请求到 OpenAI 服务器
|
||||
// useOwnApiKey: 是否使用了用户自己的 API KEY
|
||||
func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, session *types.ChatSession, apiKey *model.ApiKey) (*http.Response, error) {
|
||||
// if the chat model bind a KEY, use it directly
|
||||
if session.Model.KeyId > 0 {
|
||||
h.DB.Where("id", session.Model.KeyId).Find(apiKey)
|
||||
}
|
||||
// use the last unused key
|
||||
if apiKey.Id == 0 {
|
||||
h.DB.Where("platform", session.Model.Platform).Where("type", "chat").Where("enabled", true).Order("last_used_at ASC").First(apiKey)
|
||||
}
|
||||
if apiKey.Id == 0 {
|
||||
return nil, errors.New("no available key, please import key")
|
||||
}
|
||||
|
||||
// ONLY allow apiURL in blank list
|
||||
if session.Model.Platform == types.OpenAI.Value {
|
||||
err := h.licenseService.IsValidApiURL(apiKey.ApiURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var apiURL string
|
||||
switch session.Model.Platform {
|
||||
case types.Azure.Value:
|
||||
md := strings.Replace(req.Model, ".", "", 1)
|
||||
apiURL = strings.Replace(apiKey.ApiURL, "{model}", md, 1)
|
||||
break
|
||||
case types.ChatGLM.Value:
|
||||
apiURL = strings.Replace(apiKey.ApiURL, "{model}", req.Model, 1)
|
||||
req.Prompt = req.Messages // 使用 prompt 字段替代 message 字段
|
||||
req.Messages = nil
|
||||
break
|
||||
case types.Baidu.Value:
|
||||
apiURL = strings.Replace(apiKey.ApiURL, "{model}", req.Model, 1)
|
||||
break
|
||||
case types.QWen.Value:
|
||||
apiURL = apiKey.ApiURL
|
||||
req.Messages = nil
|
||||
break
|
||||
default:
|
||||
apiURL = apiKey.ApiURL
|
||||
}
|
||||
// 更新 API KEY 的最后使用时间
|
||||
h.DB.Model(apiKey).UpdateColumn("last_used_at", time.Now().Unix())
|
||||
// 百度文心,需要串接 access_token
|
||||
if session.Model.Platform == types.Baidu.Value {
|
||||
token, err := h.getBaiduToken(apiKey.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logger.Info("百度文心 Access_Token:", token)
|
||||
apiURL = fmt.Sprintf("%s?access_token=%s", apiURL, token)
|
||||
}
|
||||
|
||||
logger.Debugf(utils.JsonEncode(req))
|
||||
|
||||
// 创建 HttpClient 请求对象
|
||||
var client *http.Client
|
||||
requestBody, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
request, err := http.NewRequest(http.MethodPost, apiURL, bytes.NewBuffer(requestBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request = request.WithContext(ctx)
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
if len(apiKey.ProxyURL) > 5 { // 使用代理
|
||||
proxy, _ := url.Parse(apiKey.ProxyURL)
|
||||
client = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyURL(proxy),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
logger.Debugf("Sending %s request, ApiURL:%s, API KEY:%s, PROXY: %s, Model: %s", session.Model.Platform, apiURL, apiKey.Value, apiKey.ProxyURL, req.Model)
|
||||
switch session.Model.Platform {
|
||||
case types.Azure.Value:
|
||||
request.Header.Set("api-key", apiKey.Value)
|
||||
break
|
||||
case types.ChatGLM.Value:
|
||||
token, err := h.getChatGLMToken(apiKey.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
break
|
||||
case types.Baidu.Value:
|
||||
request.RequestURI = ""
|
||||
case types.OpenAI.Value:
|
||||
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey.Value))
|
||||
break
|
||||
case types.QWen.Value:
|
||||
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey.Value))
|
||||
request.Header.Set("X-DashScope-SSE", "enable")
|
||||
break
|
||||
}
|
||||
return client.Do(request)
|
||||
}
|
||||
|
||||
// 扣减用户算力
|
||||
func (h *ChatHandler) subUserPower(userVo vo.User, session *types.ChatSession, promptTokens int, replyTokens int) {
|
||||
power := 1
|
||||
if session.Model.Power > 0 {
|
||||
power = session.Model.Power
|
||||
}
|
||||
res := h.DB.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("power", gorm.Expr("power - ?", power))
|
||||
if res.Error == nil {
|
||||
// 记录算力消费日志
|
||||
var u model.User
|
||||
h.DB.Where("id", userVo.Id).First(&u)
|
||||
h.DB.Create(&model.PowerLog{
|
||||
UserId: userVo.Id,
|
||||
Username: userVo.Username,
|
||||
Type: types.PowerConsume,
|
||||
Amount: power,
|
||||
Mark: types.PowerSub,
|
||||
Balance: u.Power,
|
||||
Model: session.Model.Value,
|
||||
Remark: fmt.Sprintf("模型名称:%s, 提问长度:%d,回复长度:%d", session.Model.Name, promptTokens, replyTokens),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (h *ChatHandler) saveChatHistory(
|
||||
req types.ApiRequest,
|
||||
prompt string,
|
||||
contents []string,
|
||||
message types.Message,
|
||||
chatCtx []types.Message,
|
||||
session *types.ChatSession,
|
||||
role model.ChatRole,
|
||||
userVo vo.User,
|
||||
promptCreatedAt time.Time,
|
||||
replyCreatedAt time.Time) {
|
||||
if message.Role == "" {
|
||||
message.Role = "assistant"
|
||||
}
|
||||
message.Content = strings.Join(contents, "")
|
||||
useMsg := types.Message{Role: "user", Content: prompt}
|
||||
|
||||
// 更新上下文消息,如果是调用函数则不需要更新上下文
|
||||
if h.App.SysConfig.EnableContext {
|
||||
chatCtx = append(chatCtx, useMsg) // 提问消息
|
||||
chatCtx = append(chatCtx, message) // 回复消息
|
||||
h.ChatContexts.Put(session.ChatId, chatCtx)
|
||||
}
|
||||
|
||||
// 追加聊天记录
|
||||
// for prompt
|
||||
promptToken, err := utils.CalcTokens(prompt, req.Model)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
historyUserMsg := model.ChatMessage{
|
||||
UserId: userVo.Id,
|
||||
ChatId: session.ChatId,
|
||||
RoleId: role.Id,
|
||||
Type: types.PromptMsg,
|
||||
Icon: userVo.Avatar,
|
||||
Content: template.HTMLEscapeString(prompt),
|
||||
Tokens: promptToken,
|
||||
UseContext: true,
|
||||
Model: req.Model,
|
||||
}
|
||||
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 数量
|
||||
replyTokens, _ := utils.CalcTokens(message.Content, req.Model)
|
||||
totalTokens := replyTokens + getTotalTokens(req)
|
||||
historyReplyMsg := model.ChatMessage{
|
||||
UserId: userVo.Id,
|
||||
ChatId: session.ChatId,
|
||||
RoleId: role.Id,
|
||||
Type: types.ReplyMsg,
|
||||
Icon: role.Icon,
|
||||
Content: message.Content,
|
||||
Tokens: totalTokens,
|
||||
UseContext: true,
|
||||
Model: req.Model,
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
// 更新用户算力
|
||||
if session.Model.Power > 0 {
|
||||
h.subUserPower(userVo, session, promptToken, replyTokens)
|
||||
}
|
||||
// 保存当前会话
|
||||
var chatItem model.ChatItem
|
||||
res = h.DB.Where("chat_id = ?", session.ChatId).First(&chatItem)
|
||||
if res.Error != nil {
|
||||
chatItem.ChatId = session.ChatId
|
||||
chatItem.UserId = userVo.Id
|
||||
chatItem.RoleId = role.Id
|
||||
chatItem.ModelId = session.Model.Id
|
||||
if utf8.RuneCountInString(prompt) > 30 {
|
||||
chatItem.Title = string([]rune(prompt)[:30]) + "..."
|
||||
} else {
|
||||
chatItem.Title = prompt
|
||||
}
|
||||
chatItem.Model = req.Model
|
||||
h.DB.Create(&chatItem)
|
||||
}
|
||||
}
|
||||
|
||||
// 将AI回复消息中生成的图片链接下载到本地
|
||||
func (h *ChatHandler) extractImgUrl(text string) string {
|
||||
pattern := `!\[([^\]]*)]\(([^)]+)\)`
|
||||
re := regexp.MustCompile(pattern)
|
||||
matches := re.FindAllStringSubmatch(text, -1)
|
||||
|
||||
// 下载图片并替换链接地址
|
||||
for _, match := range matches {
|
||||
imageURL := match[2]
|
||||
logger.Debug(imageURL)
|
||||
// 对于相同地址的图片,已经被替换了,就不再重复下载了
|
||||
if !strings.Contains(text, imageURL) {
|
||||
continue
|
||||
}
|
||||
|
||||
newImgURL, err := h.uploadManager.GetUploadHandler().PutImg(imageURL, false)
|
||||
if err != nil {
|
||||
logger.Error("error with download image: ", err)
|
||||
continue
|
||||
}
|
||||
|
||||
text = strings.ReplaceAll(text, imageURL, newImgURL)
|
||||
}
|
||||
return text
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
package chatimpl
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 清华大学 ChatGML 消息发送实现
|
||||
|
||||
func (h *ChatHandler) sendChatGLMMessage(
|
||||
chatCtx []types.Message,
|
||||
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, &apiKey)
|
||||
logger.Info("HTTP请求完成,耗时:", time.Now().Sub(start))
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "context canceled") {
|
||||
return fmt.Errorf("用户取消了请求:%s", prompt)
|
||||
} else if strings.Contains(err.Error(), "no available key") {
|
||||
return errors.New("抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
|
||||
}
|
||||
return err
|
||||
} else {
|
||||
defer response.Body.Close()
|
||||
}
|
||||
|
||||
contentType := response.Header.Get("Content-Type")
|
||||
if strings.Contains(contentType, "text/event-stream") {
|
||||
replyCreatedAt := time.Now() // 记录回复时间
|
||||
// 循环读取 Chunk 消息
|
||||
var message = types.Message{}
|
||||
var contents = make([]string, 0)
|
||||
var event, content string
|
||||
scanner := bufio.NewScanner(response.Body)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if len(line) < 5 || strings.HasPrefix(line, "id:") {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(line, "event:") {
|
||||
event = line[6:]
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "data:") {
|
||||
content = line[5:]
|
||||
}
|
||||
// 处理代码换行
|
||||
if len(content) == 0 {
|
||||
content = "\n"
|
||||
}
|
||||
switch event {
|
||||
case "add":
|
||||
if len(contents) == 0 {
|
||||
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
|
||||
}
|
||||
utils.ReplyChunkMessage(ws, types.WsMessage{
|
||||
Type: types.WsMiddle,
|
||||
Content: utils.InterfaceToString(content),
|
||||
})
|
||||
contents = append(contents, content)
|
||||
case "finish":
|
||||
break
|
||||
case "error":
|
||||
utils.ReplyMessage(ws, fmt.Sprintf("**调用 ChatGLM API 出错:%s**", content))
|
||||
break
|
||||
case "interrupted":
|
||||
utils.ReplyMessage(ws, "**调用 ChatGLM API 出错,当前输出被中断!**")
|
||||
}
|
||||
|
||||
} // end for
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
if strings.Contains(err.Error(), "context canceled") {
|
||||
logger.Info("用户取消了请求:", prompt)
|
||||
} else {
|
||||
logger.Error("信息读取出错:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 消息发送成功
|
||||
if len(contents) > 0 {
|
||||
h.saveChatHistory(req, prompt, contents, message, chatCtx, session, role, userVo, promptCreatedAt, replyCreatedAt)
|
||||
}
|
||||
} else {
|
||||
body, _ := io.ReadAll(response.Body)
|
||||
return fmt.Errorf("请求大模型 API 失败:%s", body)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ChatHandler) getChatGLMToken(apiKey string) (string, error) {
|
||||
ctx := context.Background()
|
||||
tokenString, err := h.redis.Get(ctx, apiKey).Result()
|
||||
if err == nil {
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
expr := time.Hour * 2
|
||||
key := strings.Split(apiKey, ".")
|
||||
if len(key) != 2 {
|
||||
return "", fmt.Errorf("invalid api key: %s", apiKey)
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"api_key": key[0],
|
||||
"timestamp": time.Now().Unix(),
|
||||
"exp": time.Now().Add(expr).Add(time.Second * 10).Unix(),
|
||||
})
|
||||
token.Header["alg"] = "HS256"
|
||||
token.Header["sign_type"] = "SIGN"
|
||||
delete(token.Header, "typ")
|
||||
// Sign and get the complete encoded token as a string using the secret
|
||||
tokenString, err = token.SignedString([]byte(key[1]))
|
||||
h.redis.Set(ctx, apiKey, tokenString, expr)
|
||||
return tokenString, err
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
package chatimpl
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"github.com/syndtr/goleveldb/leveldb/errors"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type qWenResp struct {
|
||||
Output struct {
|
||||
FinishReason string `json:"finish_reason"`
|
||||
Text string `json:"text"`
|
||||
} `json:"output,omitempty"`
|
||||
Usage struct {
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
InputTokens int `json:"input_tokens"`
|
||||
OutputTokens int `json:"output_tokens"`
|
||||
} `json:"usage,omitempty"`
|
||||
RequestID string `json:"request_id"`
|
||||
|
||||
Code string `json:"code,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// 通义千问消息发送实现
|
||||
func (h *ChatHandler) sendQWenMessage(
|
||||
chatCtx []types.Message,
|
||||
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, &apiKey)
|
||||
logger.Info("HTTP请求完成,耗时:", time.Now().Sub(start))
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "context canceled") {
|
||||
return fmt.Errorf("用户取消了请求:%s", prompt)
|
||||
} else if strings.Contains(err.Error(), "no available key") {
|
||||
return errors.New("抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
|
||||
}
|
||||
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
|
||||
var outPutStart = false
|
||||
|
||||
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:") {
|
||||
continue
|
||||
}
|
||||
|
||||
content = line[5:]
|
||||
var resp qWenResp
|
||||
if len(contents) == 0 { // 发送消息头
|
||||
if !outPutStart {
|
||||
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
|
||||
outPutStart = true
|
||||
continue
|
||||
} else {
|
||||
// 处理代码换行
|
||||
content = "\n"
|
||||
}
|
||||
} else {
|
||||
err := utils.JsonDecode(content, &resp)
|
||||
if err != nil {
|
||||
logger.Error("error with parse data line: ", content)
|
||||
utils.ReplyMessage(ws, fmt.Sprintf("**解析数据行失败:%s**", err))
|
||||
break
|
||||
}
|
||||
if resp.Message != "" {
|
||||
utils.ReplyMessage(ws, fmt.Sprintf("**API 返回错误:%s**", resp.Message))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
//通过比较 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.saveChatHistory(req, prompt, contents, message, chatCtx, session, role, userVo, promptCreatedAt, replyCreatedAt)
|
||||
}
|
||||
} else {
|
||||
body, _ := io.ReadAll(response.Body)
|
||||
return fmt.Errorf("请求大模型 API 失败:%s", body)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,255 +0,0 @@
|
||||
package chatimpl
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"github.com/gorilla/websocket"
|
||||
"gorm.io/gorm"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type xunFeiResp struct {
|
||||
Header struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Sid string `json:"sid"`
|
||||
Status int `json:"status"`
|
||||
} `json:"header"`
|
||||
Payload struct {
|
||||
Choices struct {
|
||||
Status int `json:"status"`
|
||||
Seq int `json:"seq"`
|
||||
Text []struct {
|
||||
Content string `json:"content"`
|
||||
Role string `json:"role"`
|
||||
Index int `json:"index"`
|
||||
} `json:"text"`
|
||||
} `json:"choices"`
|
||||
Usage struct {
|
||||
Text struct {
|
||||
QuestionTokens int `json:"question_tokens"`
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
} `json:"text"`
|
||||
} `json:"usage"`
|
||||
} `json:"payload"`
|
||||
}
|
||||
|
||||
var Model2URL = map[string]string{
|
||||
"general": "v1.1",
|
||||
"generalv2": "v2.1",
|
||||
"generalv3": "v3.1",
|
||||
"generalv3.5": "v3.5",
|
||||
}
|
||||
|
||||
// 科大讯飞消息发送实现
|
||||
|
||||
func (h *ChatHandler) sendXunFeiMessage(
|
||||
chatCtx []types.Message,
|
||||
req types.ApiRequest,
|
||||
userVo vo.User,
|
||||
ctx context.Context,
|
||||
session *types.ChatSession,
|
||||
role model.ChatRole,
|
||||
prompt string,
|
||||
ws *types.WsClient) error {
|
||||
promptCreatedAt := time.Now() // 记录提问时间
|
||||
var apiKey model.ApiKey
|
||||
var res *gorm.DB
|
||||
// use the bind key
|
||||
if session.Model.KeyId > 0 {
|
||||
res = h.DB.Where("id", session.Model.KeyId).Find(&apiKey)
|
||||
}
|
||||
// use the last unused key
|
||||
if apiKey.Id == 0 {
|
||||
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 {
|
||||
return errors.New("抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
|
||||
}
|
||||
// 更新 API KEY 的最后使用时间
|
||||
h.DB.Model(&apiKey).UpdateColumn("last_used_at", time.Now().Unix())
|
||||
|
||||
d := websocket.Dialer{
|
||||
HandshakeTimeout: 5 * time.Second,
|
||||
}
|
||||
key := strings.Split(apiKey.Value, "|")
|
||||
if len(key) != 3 {
|
||||
utils.ReplyMessage(ws, "非法的 API KEY!")
|
||||
return nil
|
||||
}
|
||||
|
||||
apiURL := strings.Replace(apiKey.ApiURL, "{version}", Model2URL[req.Model], 1)
|
||||
logger.Debugf("Sending %s request, ApiURL:%s, API KEY:%s, PROXY: %s, Model: %s", session.Model.Platform, apiURL, apiKey.Value, apiKey.ProxyURL, req.Model)
|
||||
wsURL, err := assembleAuthUrl(apiURL, key[1], key[2])
|
||||
//握手并建立websocket 连接
|
||||
conn, resp, err := d.Dial(wsURL, nil)
|
||||
if err != nil {
|
||||
logger.Error(readResp(resp) + err.Error())
|
||||
utils.ReplyMessage(ws, "请求讯飞星火模型 API 失败:"+readResp(resp)+err.Error())
|
||||
return nil
|
||||
} else if resp.StatusCode != 101 {
|
||||
utils.ReplyMessage(ws, "请求讯飞星火模型 API 失败:"+readResp(resp)+err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
data := buildRequest(key[0], req)
|
||||
fmt.Printf("%+v", data)
|
||||
fmt.Println(apiURL)
|
||||
err = conn.WriteJSON(data)
|
||||
if err != nil {
|
||||
utils.ReplyMessage(ws, "发送消息失败:"+err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
replyCreatedAt := time.Now() // 记录回复时间
|
||||
// 循环读取 Chunk 消息
|
||||
var message = types.Message{}
|
||||
var contents = make([]string, 0)
|
||||
var content string
|
||||
for {
|
||||
_, msg, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
logger.Error("error with read message:", err)
|
||||
utils.ReplyMessage(ws, fmt.Sprintf("**数据读取失败:%s**", err))
|
||||
break
|
||||
}
|
||||
|
||||
// 解析数据
|
||||
var result xunFeiResp
|
||||
err = json.Unmarshal(msg, &result)
|
||||
if err != nil {
|
||||
logger.Error("error with parsing JSON:", err)
|
||||
utils.ReplyMessage(ws, fmt.Sprintf("**解析数据行失败:%s**", err))
|
||||
return nil
|
||||
}
|
||||
|
||||
if result.Header.Code != 0 {
|
||||
utils.ReplyMessage(ws, fmt.Sprintf("**请求 API 返回错误:%s**", result.Header.Message))
|
||||
return nil
|
||||
}
|
||||
|
||||
content = result.Payload.Choices.Text[0].Content
|
||||
// 处理代码换行
|
||||
if len(content) == 0 {
|
||||
content = "\n"
|
||||
}
|
||||
contents = append(contents, content)
|
||||
// 第一个结果
|
||||
if result.Payload.Choices.Status == 0 {
|
||||
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
|
||||
}
|
||||
utils.ReplyChunkMessage(ws, types.WsMessage{
|
||||
Type: types.WsMiddle,
|
||||
Content: utils.InterfaceToString(content),
|
||||
})
|
||||
|
||||
if result.Payload.Choices.Status == 2 { // 最终结果
|
||||
_ = conn.Close() // 关闭连接
|
||||
break
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
utils.ReplyMessage(ws, "**用户取消了生成指令!**")
|
||||
return nil
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
}
|
||||
// 消息发送成功
|
||||
if len(contents) > 0 {
|
||||
h.saveChatHistory(req, prompt, contents, message, chatCtx, session, role, userVo, promptCreatedAt, replyCreatedAt)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 构建 websocket 请求实体
|
||||
func buildRequest(appid string, req types.ApiRequest) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"header": map[string]interface{}{
|
||||
"app_id": appid,
|
||||
},
|
||||
"parameter": map[string]interface{}{
|
||||
"chat": map[string]interface{}{
|
||||
"domain": req.Model,
|
||||
"temperature": req.Temperature,
|
||||
"top_k": int64(6),
|
||||
"max_tokens": int64(req.MaxTokens),
|
||||
"auditing": "default",
|
||||
},
|
||||
},
|
||||
"payload": map[string]interface{}{
|
||||
"message": map[string]interface{}{
|
||||
"text": req.Messages,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 创建鉴权 URL
|
||||
func assembleAuthUrl(hostURL string, apiKey, apiSecret string) (string, error) {
|
||||
ul, err := url.Parse(hostURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
date := time.Now().UTC().Format(time.RFC1123)
|
||||
signString := []string{"host: " + ul.Host, "date: " + date, "GET " + ul.Path + " HTTP/1.1"}
|
||||
//拼接签名字符串
|
||||
signStr := strings.Join(signString, "\n")
|
||||
sha := hmacWithSha256(signStr, apiSecret)
|
||||
|
||||
authUrl := fmt.Sprintf("hmac username=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"", apiKey,
|
||||
"hmac-sha256", "host date request-line", sha)
|
||||
//将请求参数使用base64编码
|
||||
authorization := base64.StdEncoding.EncodeToString([]byte(authUrl))
|
||||
v := url.Values{}
|
||||
v.Add("host", ul.Host)
|
||||
v.Add("date", date)
|
||||
v.Add("authorization", authorization)
|
||||
//将编码后的字符串url encode后添加到url后面
|
||||
return hostURL + "?" + v.Encode(), nil
|
||||
}
|
||||
|
||||
// 使用 sha256 签名
|
||||
func hmacWithSha256(data, key string) string {
|
||||
mac := hmac.New(sha256.New, []byte(key))
|
||||
mac.Write([]byte(data))
|
||||
encodeData := mac.Sum(nil)
|
||||
return base64.StdEncoding.EncodeToString(encodeData)
|
||||
}
|
||||
|
||||
// 读取响应
|
||||
func readResp(resp *http.Response) string {
|
||||
if resp == nil {
|
||||
return ""
|
||||
}
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return fmt.Sprintf("code=%d,body=%s", resp.StatusCode, string(b))
|
||||
}
|
||||
@@ -8,18 +8,16 @@ package handler
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/service/dalle"
|
||||
"geekai/service/oss"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"gorm.io/gorm"
|
||||
@@ -27,15 +25,17 @@ import (
|
||||
|
||||
type DallJobHandler struct {
|
||||
BaseHandler
|
||||
redis *redis.Client
|
||||
service *dalle.Service
|
||||
uploader *oss.UploaderManager
|
||||
redis *redis.Client
|
||||
dallService *dalle.Service
|
||||
uploader *oss.UploaderManager
|
||||
userService *service.UserService
|
||||
}
|
||||
|
||||
func NewDallJobHandler(app *core.AppServer, db *gorm.DB, service *dalle.Service, manager *oss.UploaderManager) *DallJobHandler {
|
||||
func NewDallJobHandler(app *core.AppServer, db *gorm.DB, service *dalle.Service, manager *oss.UploaderManager, userService *service.UserService) *DallJobHandler {
|
||||
return &DallJobHandler{
|
||||
service: service,
|
||||
uploader: manager,
|
||||
dallService: service,
|
||||
uploader: manager,
|
||||
userService: userService,
|
||||
BaseHandler: BaseHandler{
|
||||
App: app,
|
||||
DB: db,
|
||||
@@ -43,49 +43,6 @@ func NewDallJobHandler(app *core.AppServer, db *gorm.DB, service *dalle.Service,
|
||||
}
|
||||
}
|
||||
|
||||
// Client WebSocket 客户端,用于通知任务状态变更
|
||||
func (h *DallJobHandler) 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.service.Clients.Put(uint(userId), client)
|
||||
logger.Infof("New websocket connected, IP: %s", c.RemoteIP())
|
||||
go func() {
|
||||
for {
|
||||
_, msg, err := client.Receive()
|
||||
if err != nil {
|
||||
client.Close()
|
||||
h.service.Clients.Delete(uint(userId))
|
||||
return
|
||||
}
|
||||
|
||||
var message types.WsMessage
|
||||
err = utils.JsonDecode(string(msg), &message)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 心跳消息
|
||||
if message.Type == "heartbeat" {
|
||||
logger.Debug("收到 DallE 心跳消息:", message.Content)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (h *DallJobHandler) preCheck(c *gin.Context) bool {
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
@@ -126,20 +83,16 @@ func (h *DallJobHandler) Image(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
h.service.PushTask(types.DallTask{
|
||||
JobId: job.Id,
|
||||
UserId: uint(userId),
|
||||
Prompt: data.Prompt,
|
||||
Quality: data.Quality,
|
||||
Size: data.Size,
|
||||
Style: data.Style,
|
||||
Power: job.Power,
|
||||
h.dallService.PushTask(types.DallTask{
|
||||
ClientId: data.ClientId,
|
||||
JobId: job.Id,
|
||||
UserId: uint(userId),
|
||||
Prompt: data.Prompt,
|
||||
Quality: data.Quality,
|
||||
Size: data.Size,
|
||||
Style: data.Style,
|
||||
Power: job.Power,
|
||||
})
|
||||
|
||||
client := h.service.Clients.Get(job.UserId)
|
||||
if client != nil {
|
||||
_ = client.Send([]byte("Task Updated"))
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
@@ -174,11 +127,11 @@ func (h *DallJobHandler) JobList(c *gin.Context) {
|
||||
}
|
||||
|
||||
// JobList 获取任务列表
|
||||
func (h *DallJobHandler) getData(finish bool, userId uint, page int, pageSize int, publish bool) (error, []vo.DallJob) {
|
||||
func (h *DallJobHandler) getData(finish bool, userId uint, page int, pageSize int, publish bool) (error, vo.Page) {
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if finish {
|
||||
session = session.Where("progress = ?", 100).Order("id DESC")
|
||||
session = session.Where("progress >= ?", 100).Order("id DESC")
|
||||
} else {
|
||||
session = session.Where("progress < ?", 100).Order("id ASC")
|
||||
}
|
||||
@@ -192,11 +145,14 @@ func (h *DallJobHandler) getData(finish bool, userId uint, page int, pageSize in
|
||||
offset := (page - 1) * pageSize
|
||||
session = session.Offset(offset).Limit(pageSize)
|
||||
}
|
||||
// 统计总数
|
||||
var total int64
|
||||
session.Model(&model.DallJob{}).Count(&total)
|
||||
|
||||
var items []model.DallJob
|
||||
res := session.Find(&items)
|
||||
if res.Error != nil {
|
||||
return res.Error, nil
|
||||
return res.Error, vo.Page{}
|
||||
}
|
||||
|
||||
var jobs = make([]vo.DallJob, 0)
|
||||
@@ -209,25 +165,36 @@ func (h *DallJobHandler) getData(finish bool, userId uint, page int, pageSize in
|
||||
jobs = append(jobs, job)
|
||||
}
|
||||
|
||||
return nil, jobs
|
||||
return nil, vo.NewPage(total, page, pageSize, jobs)
|
||||
}
|
||||
|
||||
// Remove remove task image
|
||||
func (h *DallJobHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
userId := h.GetInt(c, "user_id", 0)
|
||||
userId := h.GetLoginUserId(c)
|
||||
var job model.DallJob
|
||||
if res := h.DB.Where("id = ? AND user_id = ?", id, userId).First(&job); res.Error != nil {
|
||||
resp.ERROR(c, "记录不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// remove job recode
|
||||
res := h.DB.Delete(&model.DallJob{Id: job.Id})
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
// 删除任务
|
||||
tx := h.DB.Begin()
|
||||
tx.Delete(&job)
|
||||
// 如果任务未完成,或者任务失败,则恢复用户算力
|
||||
if job.Progress != 100 {
|
||||
err := h.userService.IncreasePower(int(job.UserId), job.Power, model.PowerLog{
|
||||
Type: types.PowerRefund,
|
||||
Model: "dall-e-3",
|
||||
Remark: fmt.Sprintf("任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg),
|
||||
})
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
// remove image
|
||||
err := h.uploader.GetUploadHandler().Delete(job.ImgURL)
|
||||
@@ -241,13 +208,12 @@ func (h *DallJobHandler) Remove(c *gin.Context) {
|
||||
// Publish 发布/取消发布图片到画廊显示
|
||||
func (h *DallJobHandler) Publish(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
userId := h.GetInt(c, "user_id", 0)
|
||||
userId := h.GetLoginUserId(c)
|
||||
action := h.GetBool(c, "action") // 发布动作,true => 发布,false => 取消分享
|
||||
|
||||
res := h.DB.Model(&model.DallJob{Id: uint(id), UserId: uint(userId)}).UpdateColumn("publish", action)
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败")
|
||||
err := h.DB.Model(&model.DallJob{Id: uint(id), UserId: userId}).UpdateColumn("publish", action).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -8,15 +8,16 @@ package handler
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/service/dalle"
|
||||
"geekai/service/oss"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -224,3 +225,27 @@ func (h *FunctionHandler) Dall3(c *gin.Context) {
|
||||
|
||||
resp.SUCCESS(c, content)
|
||||
}
|
||||
|
||||
// List 获取所有的工具函数列表
|
||||
func (h *FunctionHandler) List(c *gin.Context) {
|
||||
var items []model.Function
|
||||
err := h.DB.Where("enabled", true).Find(&items).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
tools := make([]vo.Function, 0)
|
||||
for _, v := range items {
|
||||
var f vo.Function
|
||||
err = utils.CopyObject(v, &f)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
f.Action = ""
|
||||
f.Token = ""
|
||||
tools = append(tools, f)
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, tools)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ package handler
|
||||
|
||||
import (
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
@@ -59,23 +58,16 @@ func (h *InviteHandler) Code(c *gin.Context) {
|
||||
|
||||
// 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
|
||||
}
|
||||
page := h.GetInt(c, "page", 1)
|
||||
pageSize := h.GetInt(c, "page_size", 20)
|
||||
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)
|
||||
offset := (page - 1) * pageSize
|
||||
res := session.Order("id DESC").Offset(offset).Limit(pageSize).Find(&items)
|
||||
if res.Error == nil {
|
||||
for _, item := range items {
|
||||
var v vo.InviteLog
|
||||
@@ -89,7 +81,7 @@ func (h *InviteHandler) List(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, list))
|
||||
resp.SUCCESS(c, vo.NewPage(total, page, pageSize, list))
|
||||
}
|
||||
|
||||
// Hits 访问邀请码
|
||||
|
||||
@@ -8,110 +8,66 @@ package handler
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"gorm.io/gorm"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MarkMapHandler 生成思维导图
|
||||
type MarkMapHandler struct {
|
||||
BaseHandler
|
||||
clients *types.LMap[int, *types.WsClient]
|
||||
clients *types.LMap[int, *types.WsClient]
|
||||
userService *service.UserService
|
||||
}
|
||||
|
||||
func NewMarkMapHandler(app *core.AppServer, db *gorm.DB) *MarkMapHandler {
|
||||
func NewMarkMapHandler(app *core.AppServer, db *gorm.DB, userService *service.UserService) *MarkMapHandler {
|
||||
return &MarkMapHandler{
|
||||
BaseHandler: BaseHandler{App: app, DB: db},
|
||||
clients: types.NewLMap[int, *types.WsClient](),
|
||||
userService: userService,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *MarkMapHandler) 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)
|
||||
// Generate 生成思维导图
|
||||
func (h *MarkMapHandler) Generate(c *gin.Context) {
|
||||
var data struct {
|
||||
Prompt string `json:"prompt"`
|
||||
ModelId int `json:"model_id"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
modelId := h.GetInt(c, "model_id", 0)
|
||||
userId := h.GetInt(c, "user_id", 0)
|
||||
|
||||
client := types.NewWsClient(ws)
|
||||
h.clients.Put(userId, client)
|
||||
go func() {
|
||||
for {
|
||||
_, msg, err := client.Receive()
|
||||
if err != nil {
|
||||
client.Close()
|
||||
h.clients.Delete(userId)
|
||||
return
|
||||
}
|
||||
|
||||
var message types.WsMessage
|
||||
err = utils.JsonDecode(string(msg), &message)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 心跳消息
|
||||
if message.Type == "heartbeat" {
|
||||
logger.Debug("收到 MarkMap 心跳消息:", message.Content)
|
||||
continue
|
||||
}
|
||||
// change model
|
||||
if message.Type == "model_id" {
|
||||
modelId = utils.IntValue(utils.InterfaceToString(message.Content), 0)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Info("Receive a message: ", message.Content)
|
||||
err = h.sendMessage(client, utils.InterfaceToString(message.Content), modelId, userId)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
utils.ReplyChunkMessage(client, types.WsMessage{Type: types.WsErr, Content: err.Error()})
|
||||
}
|
||||
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (h *MarkMapHandler) sendMessage(client *types.WsClient, prompt string, modelId int, userId int) error {
|
||||
userId := h.GetLoginUserId(c)
|
||||
var user model.User
|
||||
res := h.DB.Model(&model.User{}).First(&user, userId)
|
||||
if res.Error != nil {
|
||||
return fmt.Errorf("error with query user info: %v", res.Error)
|
||||
err := h.DB.Where("id", userId).First(&user, userId).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with query user info")
|
||||
return
|
||||
}
|
||||
var chatModel model.ChatModel
|
||||
res = h.DB.Where("id", modelId).First(&chatModel)
|
||||
if res.Error != nil {
|
||||
return fmt.Errorf("error with query chat model: %v", res.Error)
|
||||
}
|
||||
|
||||
if user.Status == false {
|
||||
return errors.New("当前用户被禁用")
|
||||
err = h.DB.Where("id", data.ModelId).First(&chatModel).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with query chat model")
|
||||
return
|
||||
}
|
||||
|
||||
if user.Power < chatModel.Power {
|
||||
return fmt.Errorf("您当前剩余算力(%d)已不足以支付当前模型算力(%d)!", user.Power, chatModel.Power)
|
||||
resp.ERROR(c, fmt.Sprintf("您当前剩余算力(%d)已不足以支付当前模型算力(%d)!", user.Power, chatModel.Power))
|
||||
return
|
||||
}
|
||||
|
||||
messages := make([]interface{}, 0)
|
||||
messages = append(messages, types.Message{Role: "system", Content: `
|
||||
你是一位非常优秀的思维导图助手,你会把用户的所有提问都总结成思维导图,然后以 Markdown 格式输出。markdown 只需要输出一级标题,二级标题,三级标题,四级标题,最多输出四级,除此之外不要输出任何其他 markdown 标记。下面是一个合格的例子:
|
||||
你是一位非常优秀的思维导图助手, 你能帮助用户整理思路,根据用户提供的主题或内容,快速生成结构清晰,有条理的思维导图,然后以 Markdown 格式输出。markdown 只需要输出一级标题,二级标题,三级标题,四级标题,最多输出四级,除此之外不要输出任何其他 markdown 标记。下面是一个合格的例子:
|
||||
# Geek-AI 助手
|
||||
|
||||
## 完整的开源系统
|
||||
@@ -128,130 +84,27 @@ func (h *MarkMapHandler) sendMessage(client *types.WsClient, prompt string, mode
|
||||
### 支付宝
|
||||
### 微信
|
||||
|
||||
另外,除此之外不要任何解释性语句。
|
||||
请直接生成结果,不要任何解释性语句。
|
||||
`})
|
||||
messages = append(messages, types.Message{Role: "user", Content: prompt})
|
||||
var req = types.ApiRequest{
|
||||
Model: chatModel.Value,
|
||||
Stream: true,
|
||||
Messages: messages,
|
||||
}
|
||||
|
||||
var apiKey model.ApiKey
|
||||
response, err := h.doRequest(req, chatModel, &apiKey)
|
||||
messages = append(messages, types.Message{Role: "user", Content: fmt.Sprintf("请生成一份有关【%s】一份思维导图,要求结构清晰,有条理", data.Prompt)})
|
||||
content, err := utils.SendOpenAIMessage(h.DB, messages, chatModel.Value, chatModel.KeyId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("请求 OpenAI API 失败: %s", err)
|
||||
}
|
||||
|
||||
defer response.Body.Close()
|
||||
|
||||
contentType := response.Header.Get("Content-Type")
|
||||
if strings.Contains(contentType, "text/event-stream") {
|
||||
// 循环读取 Chunk 消息
|
||||
scanner := bufio.NewScanner(response.Body)
|
||||
var isNew = true
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if !strings.Contains(line, "data:") || len(line) < 30 {
|
||||
continue
|
||||
}
|
||||
|
||||
var responseBody = types.ApiResponse{}
|
||||
err = json.Unmarshal([]byte(line[6:]), &responseBody)
|
||||
if err != nil { // 数据解析出错
|
||||
return fmt.Errorf("error with decode data: %v", line)
|
||||
}
|
||||
|
||||
if len(responseBody.Choices) == 0 { // Fixed: 兼容 Azure API 第一个输出空行
|
||||
continue
|
||||
}
|
||||
|
||||
if responseBody.Choices[0].FinishReason == "stop" {
|
||||
break
|
||||
}
|
||||
|
||||
if isNew {
|
||||
utils.ReplyChunkMessage(client, types.WsMessage{Type: types.WsStart})
|
||||
isNew = false
|
||||
}
|
||||
utils.ReplyChunkMessage(client, types.WsMessage{
|
||||
Type: types.WsMiddle,
|
||||
Content: utils.InterfaceToString(responseBody.Choices[0].Delta.Content),
|
||||
})
|
||||
} // end for
|
||||
|
||||
utils.ReplyChunkMessage(client, types.WsMessage{Type: types.WsEnd})
|
||||
|
||||
} else {
|
||||
body, _ := io.ReadAll(response.Body)
|
||||
return fmt.Errorf("请求 OpenAI API 失败:%s", string(body))
|
||||
resp.ERROR(c, fmt.Sprintf("请求 OpenAI API 失败: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 扣减算力
|
||||
if chatModel.Power > 0 {
|
||||
res = h.DB.Model(&model.User{}).Where("id", userId).UpdateColumn("power", gorm.Expr("power - ?", chatModel.Power))
|
||||
if res.Error == nil {
|
||||
// 记录算力消费日志
|
||||
var u model.User
|
||||
h.DB.Where("id", userId).First(&u)
|
||||
h.DB.Create(&model.PowerLog{
|
||||
UserId: u.Id,
|
||||
Username: u.Username,
|
||||
Type: types.PowerConsume,
|
||||
Amount: chatModel.Power,
|
||||
Mark: types.PowerSub,
|
||||
Balance: u.Power,
|
||||
Model: chatModel.Value,
|
||||
Remark: fmt.Sprintf("AI绘制思维导图,模型名称:%s, ", chatModel.Value),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
err = h.userService.DecreasePower(int(userId), chatModel.Power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: chatModel.Value,
|
||||
Remark: fmt.Sprintf("AI绘制思维导图,模型名称:%s, ", chatModel.Value),
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with save power log, "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MarkMapHandler) doRequest(req types.ApiRequest, chatModel model.ChatModel, apiKey *model.ApiKey) (*http.Response, error) {
|
||||
// if the chat model bind a KEY, use it directly
|
||||
var res *gorm.DB
|
||||
if chatModel.KeyId > 0 {
|
||||
res = h.DB.Where("id", chatModel.KeyId).Find(apiKey)
|
||||
}
|
||||
// use the last unused key
|
||||
if apiKey.Id == 0 {
|
||||
res = h.DB.Where("platform", types.OpenAI.Value).
|
||||
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")
|
||||
}
|
||||
apiURL := apiKey.ApiURL
|
||||
// 更新 API KEY 的最后使用时间
|
||||
h.DB.Model(apiKey).UpdateColumn("last_used_at", time.Now().Unix())
|
||||
|
||||
// 创建 HttpClient 请求对象
|
||||
var client *http.Client
|
||||
requestBody, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
request, err := http.NewRequest(http.MethodPost, apiURL, bytes.NewBuffer(requestBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
if len(apiKey.ProxyURL) > 5 { // 使用代理
|
||||
proxy, _ := url.Parse(apiKey.ProxyURL)
|
||||
client = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyURL(proxy),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey.Value))
|
||||
return client.Do(request)
|
||||
resp.SUCCESS(c, content)
|
||||
}
|
||||
|
||||
@@ -27,9 +27,15 @@ func NewMenuHandler(app *core.AppServer, db *gorm.DB) *MenuHandler {
|
||||
|
||||
// List 数据列表
|
||||
func (h *MenuHandler) List(c *gin.Context) {
|
||||
index := h.GetBool(c, "index")
|
||||
var items []model.Menu
|
||||
var list = make([]vo.Menu, 0)
|
||||
res := h.DB.Where("enabled", true).Order("sort_num ASC").Find(&items)
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
session = session.Where("enabled", true)
|
||||
if index {
|
||||
session = session.Where("id IN ?", h.App.SysConfig.IndexNavs)
|
||||
}
|
||||
res := session.Order("sort_num ASC").Find(&items)
|
||||
if res.Error == nil {
|
||||
for _, item := range items {
|
||||
var product vo.Menu
|
||||
|
||||
@@ -8,7 +8,6 @@ package handler
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
@@ -19,27 +18,27 @@ import (
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type MidJourneyHandler struct {
|
||||
BaseHandler
|
||||
pool *mj.ServicePool
|
||||
snowflake *service.Snowflake
|
||||
uploader *oss.UploaderManager
|
||||
mjService *mj.Service
|
||||
snowflake *service.Snowflake
|
||||
uploader *oss.UploaderManager
|
||||
userService *service.UserService
|
||||
}
|
||||
|
||||
func NewMidJourneyHandler(app *core.AppServer, db *gorm.DB, snowflake *service.Snowflake, pool *mj.ServicePool, manager *oss.UploaderManager) *MidJourneyHandler {
|
||||
func NewMidJourneyHandler(app *core.AppServer, db *gorm.DB, snowflake *service.Snowflake, service *mj.Service, manager *oss.UploaderManager, userService *service.UserService) *MidJourneyHandler {
|
||||
return &MidJourneyHandler{
|
||||
snowflake: snowflake,
|
||||
pool: pool,
|
||||
uploader: manager,
|
||||
snowflake: snowflake,
|
||||
mjService: service,
|
||||
uploader: manager,
|
||||
userService: userService,
|
||||
BaseHandler: BaseHandler{
|
||||
App: app,
|
||||
DB: db,
|
||||
@@ -59,40 +58,15 @@ func (h *MidJourneyHandler) preCheck(c *gin.Context) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
if !h.pool.HasAvailableService() {
|
||||
resp.ERROR(c, "MidJourney 池子中没有没有可用的服务!")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
// 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 {
|
||||
TaskType string `json:"task_type"`
|
||||
ClientId string `json:"client_id"`
|
||||
Prompt string `json:"prompt"`
|
||||
NegPrompt string `json:"neg_prompt"`
|
||||
Rate string `json:"rate"`
|
||||
@@ -201,8 +175,9 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
h.pool.PushTask(types.MjTask{
|
||||
h.mjService.PushTask(types.MjTask{
|
||||
Id: job.Id,
|
||||
ClientId: data.ClientId,
|
||||
TaskId: taskId,
|
||||
Type: types.TaskType(data.TaskType),
|
||||
Prompt: data.Prompt,
|
||||
@@ -210,35 +185,26 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
|
||||
Params: params,
|
||||
UserId: userId,
|
||||
ImgArr: data.ImgArr,
|
||||
Mode: h.App.SysConfig.MjMode,
|
||||
})
|
||||
|
||||
client := h.pool.Clients.Get(uint(job.UserId))
|
||||
if client != nil {
|
||||
_ = client.Send([]byte("Task Updated"))
|
||||
// update user's power
|
||||
err = h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: "mid-journey",
|
||||
Remark: fmt.Sprintf("%s操作,任务ID:%s", opt, job.TaskId),
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// update user's power
|
||||
tx := h.DB.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power - ?", job.Power))
|
||||
// 记录算力变化日志
|
||||
if tx.Error == nil && tx.RowsAffected > 0 {
|
||||
user, _ := h.GetLoginUser(c)
|
||||
h.DB.Create(&model.PowerLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
Type: types.PowerConsume,
|
||||
Amount: job.Power,
|
||||
Balance: user.Power - job.Power,
|
||||
Mark: types.PowerSub,
|
||||
Model: "mid-journey",
|
||||
Remark: fmt.Sprintf("%s操作,任务ID:%s", opt, job.TaskId),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
type reqVo struct {
|
||||
Index int `json:"index"`
|
||||
ClientId string `json:"client_id"`
|
||||
ChannelId string `json:"channel_id"`
|
||||
MessageId string `json:"message_id"`
|
||||
MessageHash string `json:"message_hash"`
|
||||
@@ -273,37 +239,29 @@ func (h *MidJourneyHandler) Upscale(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
h.pool.PushTask(types.MjTask{
|
||||
h.mjService.PushTask(types.MjTask{
|
||||
Id: job.Id,
|
||||
ClientId: data.ClientId,
|
||||
Type: types.TaskUpscale,
|
||||
UserId: userId,
|
||||
ChannelId: data.ChannelId,
|
||||
Index: data.Index,
|
||||
MessageId: data.MessageId,
|
||||
MessageHash: data.MessageHash,
|
||||
Mode: h.App.SysConfig.MjMode,
|
||||
})
|
||||
|
||||
client := h.pool.Clients.Get(uint(job.UserId))
|
||||
if client != nil {
|
||||
_ = client.Send([]byte("Task Updated"))
|
||||
}
|
||||
// update user's power
|
||||
tx := h.DB.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power - ?", job.Power))
|
||||
// 记录算力变化日志
|
||||
if tx.Error == nil && tx.RowsAffected > 0 {
|
||||
user, _ := h.GetLoginUser(c)
|
||||
h.DB.Create(&model.PowerLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
Type: types.PowerConsume,
|
||||
Amount: job.Power,
|
||||
Balance: user.Power - job.Power,
|
||||
Mark: types.PowerSub,
|
||||
Model: "mid-journey",
|
||||
Remark: fmt.Sprintf("Upscale 操作,任务ID:%s", job.TaskId),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
err := h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: "mid-journey",
|
||||
Remark: fmt.Sprintf("Upscale 操作,任务ID:%s", job.TaskId),
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
@@ -337,38 +295,28 @@ func (h *MidJourneyHandler) Variation(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
h.pool.PushTask(types.MjTask{
|
||||
h.mjService.PushTask(types.MjTask{
|
||||
Id: job.Id,
|
||||
Type: types.TaskVariation,
|
||||
ClientId: data.ClientId,
|
||||
UserId: userId,
|
||||
Index: data.Index,
|
||||
ChannelId: data.ChannelId,
|
||||
MessageId: data.MessageId,
|
||||
MessageHash: data.MessageHash,
|
||||
Mode: h.App.SysConfig.MjMode,
|
||||
})
|
||||
|
||||
client := h.pool.Clients.Get(uint(job.UserId))
|
||||
if client != nil {
|
||||
_ = client.Send([]byte("Task Updated"))
|
||||
err := h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: "mid-journey",
|
||||
Remark: fmt.Sprintf("Variation 操作,任务ID:%s", job.TaskId),
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// update user's power
|
||||
tx := h.DB.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power - ?", job.Power))
|
||||
// 记录算力变化日志
|
||||
if tx.Error == nil && tx.RowsAffected > 0 {
|
||||
user, _ := h.GetLoginUser(c)
|
||||
h.DB.Create(&model.PowerLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
Type: types.PowerConsume,
|
||||
Amount: job.Power,
|
||||
Balance: user.Power - job.Power,
|
||||
Mark: types.PowerSub,
|
||||
Model: "mid-journey",
|
||||
Remark: fmt.Sprintf("Variation 操作,任务ID:%s", job.TaskId),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
@@ -403,10 +351,10 @@ func (h *MidJourneyHandler) JobList(c *gin.Context) {
|
||||
}
|
||||
|
||||
// JobList 获取 MJ 任务列表
|
||||
func (h *MidJourneyHandler) getData(finish bool, userId uint, page int, pageSize int, publish bool) (error, []vo.MidJourneyJob) {
|
||||
func (h *MidJourneyHandler) getData(finish bool, userId uint, page int, pageSize int, publish bool) (error, vo.Page) {
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if finish {
|
||||
session = session.Where("progress = ?", 100).Order("id DESC")
|
||||
session = session.Where("progress >= ?", 100).Order("id DESC")
|
||||
} else {
|
||||
session = session.Where("progress < ?", 100).Order("id ASC")
|
||||
}
|
||||
@@ -421,10 +369,14 @@ func (h *MidJourneyHandler) getData(finish bool, userId uint, page int, pageSize
|
||||
session = session.Offset(offset).Limit(pageSize)
|
||||
}
|
||||
|
||||
// 统计总数
|
||||
var total int64
|
||||
session.Model(&model.MidJourneyJob{}).Count(&total)
|
||||
|
||||
var items []model.MidJourneyJob
|
||||
res := session.Find(&items)
|
||||
if res.Error != nil {
|
||||
return res.Error, nil
|
||||
return res.Error, vo.Page{}
|
||||
}
|
||||
|
||||
var jobs = make([]vo.MidJourneyJob, 0)
|
||||
@@ -434,17 +386,9 @@ func (h *MidJourneyHandler) getData(finish bool, userId uint, page int, pageSize
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if item.Progress < 100 && item.ImgURL == "" && item.OrgURL != "" {
|
||||
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)
|
||||
}
|
||||
return nil, jobs
|
||||
return nil, vo.NewPage(total, page, pageSize, jobs)
|
||||
}
|
||||
|
||||
// Remove remove task image
|
||||
@@ -456,12 +400,24 @@ func (h *MidJourneyHandler) Remove(c *gin.Context) {
|
||||
resp.ERROR(c, "记录不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// remove job recode
|
||||
res := h.DB.Delete(&job)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
tx := h.DB.Begin()
|
||||
tx.Delete(&job)
|
||||
// 如果任务未完成,或者任务失败,则恢复用户算力
|
||||
if job.Progress != 100 {
|
||||
err := h.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{
|
||||
Type: types.PowerRefund,
|
||||
Model: "mid-journey",
|
||||
Remark: fmt.Sprintf("任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg),
|
||||
})
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
// remove image
|
||||
err := h.uploader.GetUploadHandler().Delete(job.ImgURL)
|
||||
@@ -469,11 +425,6 @@ func (h *MidJourneyHandler) Remove(c *gin.Context) {
|
||||
logger.Error("remove image failed: ", err)
|
||||
}
|
||||
|
||||
client := h.pool.Clients.Get(uint(job.UserId))
|
||||
if client != nil {
|
||||
_ = client.Send([]byte("Task Updated"))
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
@@ -482,10 +433,9 @@ func (h *MidJourneyHandler) Publish(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
userId := h.GetInt(c, "user_id", 0)
|
||||
action := h.GetBool(c, "action") // 发布动作,true => 发布,false => 取消分享
|
||||
res := h.DB.Model(&model.MidJourneyJob{Id: uint(id), UserId: userId}).UpdateColumn("publish", action)
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败")
|
||||
err := h.DB.Model(&model.MidJourneyJob{Id: uint(id), UserId: userId}).UpdateColumn("publish", action).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -17,19 +17,21 @@ import (
|
||||
"geekai/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UploadHandler struct {
|
||||
type NetHandler struct {
|
||||
BaseHandler
|
||||
uploaderManager *oss.UploaderManager
|
||||
}
|
||||
|
||||
func NewUploadHandler(app *core.AppServer, db *gorm.DB, manager *oss.UploaderManager) *UploadHandler {
|
||||
return &UploadHandler{BaseHandler: BaseHandler{App: app, DB: db}, uploaderManager: manager}
|
||||
func NewNetHandler(app *core.AppServer, db *gorm.DB, manager *oss.UploaderManager) *NetHandler {
|
||||
return &NetHandler{BaseHandler: BaseHandler{App: app, DB: db}, uploaderManager: manager}
|
||||
}
|
||||
|
||||
func (h *UploadHandler) Upload(c *gin.Context) {
|
||||
func (h *NetHandler) Upload(c *gin.Context) {
|
||||
file, err := h.uploaderManager.GetUploadHandler().PutFile(c, "file")
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
@@ -60,9 +62,11 @@ func (h *UploadHandler) Upload(c *gin.Context) {
|
||||
resp.SUCCESS(c, file)
|
||||
}
|
||||
|
||||
func (h *UploadHandler) List(c *gin.Context) {
|
||||
func (h *NetHandler) List(c *gin.Context) {
|
||||
var data struct {
|
||||
Urls []string `json:"urls,omitempty"`
|
||||
Urls []string `json:"urls,omitempty"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
@@ -77,25 +81,36 @@ func (h *UploadHandler) List(c *gin.Context) {
|
||||
if len(data.Urls) > 0 {
|
||||
session = session.Where("url IN ?", data.Urls)
|
||||
}
|
||||
session.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)
|
||||
}
|
||||
// 统计总数
|
||||
var total int64
|
||||
session.Model(&model.File{}).Count(&total)
|
||||
|
||||
if data.Page > 0 && data.PageSize > 0 {
|
||||
offset := (data.Page - 1) * data.PageSize
|
||||
session = session.Offset(offset).Limit(data.PageSize)
|
||||
}
|
||||
err := session.Order("id desc").Find(&items).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, files)
|
||||
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, vo.NewPage(total, data.Page, data.PageSize, files))
|
||||
}
|
||||
|
||||
// Remove remove files
|
||||
func (h *UploadHandler) Remove(c *gin.Context) {
|
||||
func (h *NetHandler) Remove(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
id := h.GetInt(c, "id", 0)
|
||||
var file model.File
|
||||
@@ -119,3 +134,28 @@ func (h *UploadHandler) Remove(c *gin.Context) {
|
||||
_ = h.uploaderManager.GetUploadHandler().Delete(objectKey)
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *NetHandler) Download(c *gin.Context) {
|
||||
fileUrl := c.Query("url")
|
||||
// 使用http工具下载文件
|
||||
if fileUrl == "" {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
// 使用http.Get下载文件
|
||||
r, err := http.Get(fileUrl)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
if r.StatusCode != http.StatusOK {
|
||||
resp.ERROR(c, "error status:"+r.Status)
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusOK)
|
||||
// 将下载的文件内容写入响应
|
||||
_, _ = io.Copy(c.Writer, r.Body)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package chatimpl
|
||||
package handler
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
@@ -23,6 +23,32 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Usage struct {
|
||||
Prompt string `json:"prompt,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
}
|
||||
|
||||
type OpenAIResVo struct {
|
||||
Id string `json:"id"`
|
||||
Object string `json:"object"`
|
||||
Created int `json:"created"`
|
||||
Model string `json:"model"`
|
||||
SystemFingerprint string `json:"system_fingerprint"`
|
||||
Choices []struct {
|
||||
Index int `json:"index"`
|
||||
Message struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
} `json:"message"`
|
||||
Logprobs interface{} `json:"logprobs"`
|
||||
FinishReason string `json:"finish_reason"`
|
||||
} `json:"choices"`
|
||||
Usage Usage `json:"usage"`
|
||||
}
|
||||
|
||||
// OPenAI 消息发送实现
|
||||
func (h *ChatHandler) sendOpenAiMessage(
|
||||
chatCtx []types.Message,
|
||||
@@ -49,23 +75,26 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
defer response.Body.Close()
|
||||
}
|
||||
|
||||
if response.StatusCode != 200 {
|
||||
body, _ := io.ReadAll(response.Body)
|
||||
return fmt.Errorf("请求 OpenAI API 失败:%d, %v", response.StatusCode, string(body))
|
||||
}
|
||||
|
||||
contentType := response.Header.Get("Content-Type")
|
||||
if strings.Contains(contentType, "text/event-stream") {
|
||||
replyCreatedAt := time.Now() // 记录回复时间
|
||||
// 循环读取 Chunk 消息
|
||||
var message = types.Message{}
|
||||
var message = types.Message{Role: "assistant"}
|
||||
var contents = make([]string, 0)
|
||||
var function model.Function
|
||||
var toolCall = false
|
||||
var arguments = make([]string, 0)
|
||||
scanner := bufio.NewScanner(response.Body)
|
||||
var isNew = true
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if !strings.Contains(line, "data:") || len(line) < 30 {
|
||||
continue
|
||||
}
|
||||
|
||||
var responseBody = types.ApiResponse{}
|
||||
err = json.Unmarshal([]byte(line[6:]), &responseBody)
|
||||
if err != nil { // 数据解析出错
|
||||
@@ -74,12 +103,12 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
if len(responseBody.Choices) == 0 { // Fixed: 兼容 Azure API 第一个输出空行
|
||||
continue
|
||||
}
|
||||
if responseBody.Choices[0].Delta.Content == nil {
|
||||
if responseBody.Choices[0].Delta.Content == nil && responseBody.Choices[0].Delta.ToolCalls == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if responseBody.Choices[0].FinishReason == "stop" && len(contents) == 0 {
|
||||
utils.ReplyMessage(ws, "抱歉😔😔😔,AI助手由于未知原因已经停止输出内容。")
|
||||
utils.SendChunkMsg(ws, "抱歉😔😔😔,AI助手由于未知原因已经停止输出内容。")
|
||||
break
|
||||
}
|
||||
|
||||
@@ -107,8 +136,7 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
if res.Error == nil {
|
||||
toolCall = true
|
||||
callMsg := fmt.Sprintf("正在调用工具 `%s` 作答 ...\n\n", function.Label)
|
||||
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
|
||||
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: callMsg})
|
||||
utils.SendChunkMsg(ws, callMsg)
|
||||
contents = append(contents, callMsg)
|
||||
}
|
||||
continue
|
||||
@@ -125,14 +153,7 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
} else {
|
||||
content := responseBody.Choices[0].Delta.Content
|
||||
contents = append(contents, utils.InterfaceToString(content))
|
||||
if isNew {
|
||||
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
|
||||
isNew = false
|
||||
}
|
||||
utils.ReplyChunkMessage(ws, types.WsMessage{
|
||||
Type: types.WsMiddle,
|
||||
Content: utils.InterfaceToString(responseBody.Choices[0].Delta.Content),
|
||||
})
|
||||
utils.SendChunkMsg(ws, responseBody.Choices[0].Delta.Content)
|
||||
}
|
||||
} // end for
|
||||
|
||||
@@ -150,7 +171,7 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
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").
|
||||
r, err := req2.C().R().SetHeader("Body-Type", "application/json").
|
||||
SetHeader("Authorization", function.Token).
|
||||
SetBody(params).
|
||||
SetSuccessResult(&apiRes).Post(function.Action)
|
||||
@@ -161,28 +182,45 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
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)
|
||||
errMsg = "调用函数工具出错:" + apiRes.Message + errMsg
|
||||
contents = append(contents, errMsg)
|
||||
} else {
|
||||
utils.ReplyChunkMessage(ws, types.WsMessage{
|
||||
Type: types.WsMiddle,
|
||||
Content: apiRes.Data,
|
||||
})
|
||||
contents = append(contents, utils.InterfaceToString(apiRes.Data))
|
||||
errMsg = utils.InterfaceToString(apiRes.Data)
|
||||
contents = append(contents, errMsg)
|
||||
}
|
||||
utils.SendChunkMsg(ws, errMsg)
|
||||
}
|
||||
|
||||
// 消息发送成功
|
||||
if len(contents) > 0 {
|
||||
h.saveChatHistory(req, prompt, contents, message, chatCtx, session, role, userVo, promptCreatedAt, replyCreatedAt)
|
||||
usage := Usage{
|
||||
Prompt: prompt,
|
||||
Content: strings.Join(contents, ""),
|
||||
PromptTokens: 0,
|
||||
CompletionTokens: 0,
|
||||
TotalTokens: 0,
|
||||
}
|
||||
message.Content = usage.Content
|
||||
h.saveChatHistory(req, usage, message, chatCtx, session, role, userVo, promptCreatedAt, replyCreatedAt)
|
||||
}
|
||||
} else {
|
||||
body, _ := io.ReadAll(response.Body)
|
||||
return fmt.Errorf("请求 OpenAI API 失败:%s", body)
|
||||
} else { // 非流式输出
|
||||
var respVo OpenAIResVo
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取响应失败:%v", body)
|
||||
}
|
||||
err = json.Unmarshal(body, &respVo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("解析响应失败:%v", body)
|
||||
}
|
||||
content := respVo.Choices[0].Message.Content
|
||||
if strings.HasPrefix(req.Model, "o1-") {
|
||||
content = fmt.Sprintf("AI思考结束,耗时:%d 秒。\n%s", time.Now().Unix()-session.Start, respVo.Choices[0].Message.Content)
|
||||
}
|
||||
utils.SendChunkMsg(ws, content)
|
||||
respVo.Usage.Prompt = prompt
|
||||
respVo.Usage.Content = content
|
||||
h.saveChatHistory(req, respVo.Usage, respVo.Choices[0].Message, chatCtx, session, role, userVo, promptCreatedAt, time.Now())
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -48,6 +48,16 @@ func (h *OrderHandler) List(c *gin.Context) {
|
||||
order.Id = item.Id
|
||||
order.CreatedAt = item.CreatedAt.Unix()
|
||||
order.UpdatedAt = item.UpdatedAt.Unix()
|
||||
payMethod, ok := types.PayMethods[item.PayWay]
|
||||
if !ok {
|
||||
payMethod = item.PayWay
|
||||
}
|
||||
payName, ok := types.PayNames[item.PayType]
|
||||
if !ok {
|
||||
payName = item.PayWay
|
||||
}
|
||||
order.PayMethod = payMethod
|
||||
order.PayName = payName
|
||||
list = append(list, order)
|
||||
} else {
|
||||
logger.Error(err)
|
||||
|
||||
@@ -9,7 +9,6 @@ package handler
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
@@ -18,10 +17,7 @@ import (
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"github.com/shopspring/decimal"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -34,21 +30,15 @@ type PayWay struct {
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
var (
|
||||
PayWayAlipay = PayWay{Name: "支付宝", Value: "alipay"}
|
||||
PayWayXunHu = PayWay{Name: "虎皮椒", Value: "hupi"}
|
||||
PayWayJs = PayWay{Name: "PayJS", Value: "payjs"}
|
||||
PayWayWechat = PayWay{Name: "微信支付", Value: "wechat"}
|
||||
)
|
||||
|
||||
// PaymentHandler 支付服务回调 handler
|
||||
type PaymentHandler struct {
|
||||
BaseHandler
|
||||
alipayService *payment.AlipayService
|
||||
huPiPayService *payment.HuPiPayService
|
||||
jsPayService *payment.JPayService
|
||||
geekPayService *payment.GeekPayService
|
||||
wechatPayService *payment.WechatPayService
|
||||
snowflake *service.Snowflake
|
||||
userService *service.UserService
|
||||
fs embed.FS
|
||||
lock sync.Mutex
|
||||
signKey string // 用来签名的随机秘钥
|
||||
@@ -58,17 +48,19 @@ func NewPaymentHandler(
|
||||
server *core.AppServer,
|
||||
alipayService *payment.AlipayService,
|
||||
huPiPayService *payment.HuPiPayService,
|
||||
jsPayService *payment.JPayService,
|
||||
geekPayService *payment.GeekPayService,
|
||||
wechatPayService *payment.WechatPayService,
|
||||
db *gorm.DB,
|
||||
userService *service.UserService,
|
||||
snowflake *service.Snowflake,
|
||||
fs embed.FS) *PaymentHandler {
|
||||
return &PaymentHandler{
|
||||
alipayService: alipayService,
|
||||
huPiPayService: huPiPayService,
|
||||
jsPayService: jsPayService,
|
||||
geekPayService: geekPayService,
|
||||
wechatPayService: wechatPayService,
|
||||
snowflake: snowflake,
|
||||
userService: userService,
|
||||
fs: fs,
|
||||
lock: sync.Mutex{},
|
||||
BaseHandler: BaseHandler{
|
||||
@@ -79,309 +71,167 @@ func NewPaymentHandler(
|
||||
}
|
||||
}
|
||||
|
||||
func (h *PaymentHandler) DoPay(c *gin.Context) {
|
||||
orderNo := h.GetTrim(c, "order_no")
|
||||
payWay := h.GetTrim(c, "pay_way")
|
||||
t := h.GetInt(c, "t", 0)
|
||||
sign := h.GetTrim(c, "sign")
|
||||
signStr := fmt.Sprintf("%s-%s-%d-%s", orderNo, payWay, t, h.signKey)
|
||||
newSign := utils.Sha256(signStr)
|
||||
if newSign != sign {
|
||||
resp.ERROR(c, "订单签名错误!")
|
||||
return
|
||||
func (h *PaymentHandler) Pay(c *gin.Context) {
|
||||
var data struct {
|
||||
PayWay string `json:"pay_way"`
|
||||
PayType string `json:"pay_type"`
|
||||
ProductId int `json:"product_id"`
|
||||
UserId int `json:"user_id"`
|
||||
Device string `json:"device"`
|
||||
Host string `json:"host"`
|
||||
}
|
||||
|
||||
// 检查二维码是否过期
|
||||
if time.Now().Unix()-int64(t) > int64(h.App.SysConfig.OrderPayTimeout) {
|
||||
resp.ERROR(c, "支付二维码已过期,请重新生成!")
|
||||
return
|
||||
}
|
||||
|
||||
if orderNo == "" {
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
var order model.Order
|
||||
res := h.DB.Where("order_no = ?", orderNo).First(&order)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "Order not found")
|
||||
var product model.Product
|
||||
err := h.DB.Where("id", data.ProductId).First(&product).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "Product not found")
|
||||
return
|
||||
}
|
||||
|
||||
// fix: 这里先检查一下订单状态,如果已经支付了,就直接返回
|
||||
if order.Status == types.OrderPaidSuccess {
|
||||
resp.ERROR(c, "订单已支付成功,无需重复支付!")
|
||||
orderNo, err := h.snowflake.Next(false)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with generate trade no: "+err.Error())
|
||||
return
|
||||
}
|
||||
var user model.User
|
||||
err = h.DB.Where("id", data.UserId).First(&user).Error
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新扫码状态
|
||||
h.DB.Model(&order).UpdateColumn("status", types.OrderScanned)
|
||||
amount := product.Discount
|
||||
var payURL, returnURL, notifyURL string
|
||||
switch data.PayWay {
|
||||
case "alipay":
|
||||
if h.App.Config.AlipayConfig.NotifyURL != "" { // 用于本地调试支付
|
||||
notifyURL = h.App.Config.AlipayConfig.NotifyURL
|
||||
} else {
|
||||
notifyURL = fmt.Sprintf("%s/api/payment/notify/alipay", data.Host)
|
||||
}
|
||||
if h.App.Config.AlipayConfig.ReturnURL != "" { // 用于本地调试支付
|
||||
returnURL = h.App.Config.AlipayConfig.ReturnURL
|
||||
} else {
|
||||
returnURL = fmt.Sprintf("%s/payReturn", data.Host)
|
||||
}
|
||||
money := fmt.Sprintf("%.2f", amount)
|
||||
if data.Device == "wechat" {
|
||||
payURL, err = h.alipayService.PayMobile(payment.AlipayParams{
|
||||
OutTradeNo: orderNo,
|
||||
Subject: product.Name,
|
||||
TotalFee: money,
|
||||
ReturnURL: returnURL,
|
||||
NotifyURL: notifyURL,
|
||||
})
|
||||
} else {
|
||||
payURL, err = h.alipayService.PayPC(payment.AlipayParams{
|
||||
OutTradeNo: orderNo,
|
||||
Subject: product.Name,
|
||||
TotalFee: money,
|
||||
ReturnURL: returnURL,
|
||||
NotifyURL: notifyURL,
|
||||
})
|
||||
}
|
||||
|
||||
if payWay == "alipay" { // 支付宝
|
||||
amount := fmt.Sprintf("%.2f", order.Amount)
|
||||
uri, err := h.alipayService.PayUrlMobile(order.OrderNo, 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: "极客学长",
|
||||
break
|
||||
case "wechat":
|
||||
if h.App.Config.WechatPayConfig.NotifyURL != "" {
|
||||
notifyURL = h.App.Config.WechatPayConfig.NotifyURL
|
||||
} else {
|
||||
notifyURL = fmt.Sprintf("%s/api/payment/notify/wechat", data.Host)
|
||||
}
|
||||
if data.Device == "wechat" {
|
||||
payURL, err = h.wechatPayService.PayUrlH5(payment.WechatPayParams{
|
||||
OutTradeNo: orderNo,
|
||||
TotalFee: int(amount * 100),
|
||||
Subject: product.Name,
|
||||
NotifyURL: notifyURL,
|
||||
ClientIP: c.ClientIP(),
|
||||
})
|
||||
} else {
|
||||
payURL, err = h.wechatPayService.PayUrlNative(payment.WechatPayParams{
|
||||
OutTradeNo: orderNo,
|
||||
TotalFee: int(amount * 100),
|
||||
Subject: product.Name,
|
||||
NotifyURL: notifyURL,
|
||||
})
|
||||
}
|
||||
r, err := h.huPiPayService.Pay(params)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(302, r.URL)
|
||||
}
|
||||
resp.ERROR(c, "Invalid operations")
|
||||
}
|
||||
|
||||
// PayQrcode 生成支付 URL 二维码
|
||||
func (h *PaymentHandler) PayQrcode(c *gin.Context) {
|
||||
var data struct {
|
||||
PayWay string `json:"pay_way"` // 支付方式
|
||||
ProductId uint `json:"product_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
var product model.Product
|
||||
res := h.DB.First(&product, data.ProductId)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "Product not found")
|
||||
return
|
||||
}
|
||||
|
||||
orderNo, err := h.snowflake.Next(false)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with generate trade no: "+err.Error())
|
||||
return
|
||||
}
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
var payWay string
|
||||
var notifyURL string
|
||||
switch data.PayWay {
|
||||
break
|
||||
case "hupi":
|
||||
payWay = PayWayXunHu.Value
|
||||
notifyURL = h.App.Config.HuPiPayConfig.NotifyURL
|
||||
break
|
||||
case "payjs":
|
||||
payWay = PayWayJs.Value
|
||||
notifyURL = h.App.Config.JPayConfig.NotifyURL
|
||||
break
|
||||
case "alipay":
|
||||
payWay = PayWayAlipay.Value
|
||||
notifyURL = h.App.Config.AlipayConfig.NotifyURL
|
||||
break
|
||||
default:
|
||||
payWay = PayWayWechat.Value
|
||||
notifyURL = h.App.Config.WechatPayConfig.NotifyURL
|
||||
|
||||
}
|
||||
// 创建订单
|
||||
remark := types.OrderRemark{
|
||||
Days: product.Days,
|
||||
Power: product.Power,
|
||||
Name: product.Name,
|
||||
Price: product.Price,
|
||||
Discount: product.Discount,
|
||||
}
|
||||
|
||||
amount, _ := decimal.NewFromFloat(product.Price).Sub(decimal.NewFromFloat(product.Discount)).Float64()
|
||||
order := model.Order{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
ProductId: product.Id,
|
||||
OrderNo: orderNo,
|
||||
Subject: product.Name,
|
||||
Amount: amount,
|
||||
Status: types.OrderNotPaid,
|
||||
PayWay: payWay,
|
||||
Remark: utils.JsonEncode(remark),
|
||||
}
|
||||
res = h.DB.Create(&order)
|
||||
if res.Error != nil || res.RowsAffected == 0 {
|
||||
resp.ERROR(c, "error with create order: "+res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// PayJs 单独处理,只能用官方生成的二维码
|
||||
if data.PayWay == "payjs" {
|
||||
params := payment.JPayReq{
|
||||
TotalFee: int(math.Ceil(order.Amount * 100)),
|
||||
OutTradeNo: order.OrderNo,
|
||||
Subject: product.Name,
|
||||
}
|
||||
r := h.jsPayService.Pay(params)
|
||||
if r.IsOK() {
|
||||
resp.SUCCESS(c, gin.H{"order_no": order.OrderNo, "image": r.Qrcode})
|
||||
return
|
||||
if h.App.Config.HuPiPayConfig.NotifyURL != "" {
|
||||
notifyURL = h.App.Config.HuPiPayConfig.NotifyURL
|
||||
} else {
|
||||
resp.ERROR(c, "error with generating payment qrcode: "+r.ReturnMsg)
|
||||
return
|
||||
notifyURL = fmt.Sprintf("%s/api/payment/notify/hupi", data.Host)
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
if h.App.Config.HuPiPayConfig.ReturnURL != "" {
|
||||
returnURL = h.App.Config.HuPiPayConfig.ReturnURL
|
||||
} else {
|
||||
logo = "res/img/alipay.jpg"
|
||||
returnURL = fmt.Sprintf("%s/payReturn", data.Host)
|
||||
}
|
||||
} else if data.PayWay == "wechat" {
|
||||
logo = "res/img/wechat-pay.jpg"
|
||||
}
|
||||
|
||||
file, err := h.fs.Open(logo)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with open qrcode log file: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
parse, err := url.Parse(notifyURL)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
timestamp := time.Now().Unix()
|
||||
signStr := fmt.Sprintf("%s-%s-%d-%s", orderNo, data.PayWay, timestamp, h.signKey)
|
||||
sign := utils.Sha256(signStr)
|
||||
var imageURL string
|
||||
if data.PayWay == "wechat" {
|
||||
payUrl, err := h.wechatPayService.PayUrlNative(order.OrderNo, int(math.Floor(order.Amount*100)), product.Name)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with generating wechat payment qrcode: "+err.Error())
|
||||
return
|
||||
} else {
|
||||
imageURL = payUrl
|
||||
}
|
||||
} else {
|
||||
imageURL = fmt.Sprintf("%s://%s/api/payment/doPay?order_no=%s&pay_way=%s&t=%d&sign=%s", parse.Scheme, parse.Host, orderNo, data.PayWay, timestamp, sign)
|
||||
}
|
||||
imgData, err := utils.GenQrcode(imageURL, 400, file)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
imgDataBase64 := base64.StdEncoding.EncodeToString(imgData)
|
||||
resp.SUCCESS(c, gin.H{"order_no": orderNo, "image": fmt.Sprintf("data:image/jpg;base64, %s", imgDataBase64), "url": imageURL})
|
||||
}
|
||||
|
||||
// Mobile 移动端支付
|
||||
func (h *PaymentHandler) Mobile(c *gin.Context) {
|
||||
var data struct {
|
||||
PayWay string `json:"pay_way"` // 支付方式
|
||||
ProductId uint `json:"product_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
var product model.Product
|
||||
res := h.DB.First(&product, data.ProductId)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "Product not found")
|
||||
return
|
||||
}
|
||||
|
||||
orderNo, err := h.snowflake.Next(false)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with generate trade no: "+err.Error())
|
||||
return
|
||||
}
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
amount, _ := decimal.NewFromFloat(product.Price).Sub(decimal.NewFromFloat(product.Discount)).Float64()
|
||||
var payWay string
|
||||
var notifyURL, returnURL string
|
||||
var payURL string
|
||||
switch data.PayWay {
|
||||
case "hupi":
|
||||
payWay = PayWayXunHu.Name
|
||||
notifyURL = h.App.Config.HuPiPayConfig.NotifyURL
|
||||
returnURL = h.App.Config.HuPiPayConfig.ReturnURL
|
||||
parse, _ := url.Parse(h.App.Config.HuPiPayConfig.ReturnURL)
|
||||
baseURL := fmt.Sprintf("%s://%s", parse.Scheme, parse.Host)
|
||||
params := payment.HuPiPayReq{
|
||||
r, err := h.huPiPayService.Pay(payment.HuPiPayParams{
|
||||
Version: "1.1",
|
||||
TradeOrderId: orderNo,
|
||||
TotalFee: fmt.Sprintf("%f", amount),
|
||||
Title: product.Name,
|
||||
NotifyURL: notifyURL,
|
||||
ReturnURL: returnURL,
|
||||
CallbackURL: returnURL,
|
||||
WapName: "极客学长",
|
||||
WapUrl: baseURL,
|
||||
Type: "WAP",
|
||||
}
|
||||
r, err := h.huPiPayService.Pay(params)
|
||||
WapName: "GeekAI助手",
|
||||
})
|
||||
if err != nil {
|
||||
errMsg := "error with generating Pay Hupi URL: " + err.Error()
|
||||
logger.Error(errMsg)
|
||||
resp.ERROR(c, errMsg)
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
payURL = r.URL
|
||||
case "payjs":
|
||||
payWay = PayWayJs.Name
|
||||
notifyURL = h.App.Config.JPayConfig.NotifyURL
|
||||
returnURL = h.App.Config.JPayConfig.ReturnURL
|
||||
totalFee := decimal.NewFromFloat(product.Price).Sub(decimal.NewFromFloat(product.Discount)).Mul(decimal.NewFromInt(100)).IntPart()
|
||||
params := url.Values{}
|
||||
params.Add("total_fee", fmt.Sprintf("%d", totalFee))
|
||||
params.Add("out_trade_no", orderNo)
|
||||
params.Add("body", product.Name)
|
||||
params.Add("notify_url", notifyURL)
|
||||
params.Add("auto", "0")
|
||||
payURL = h.jsPayService.PayH5(params)
|
||||
case "alipay":
|
||||
payWay = PayWayAlipay.Name
|
||||
payURL, err = h.alipayService.PayUrlMobile(orderNo, fmt.Sprintf("%.2f", amount), product.Name)
|
||||
break
|
||||
case "geek":
|
||||
if h.App.Config.GeekPayConfig.NotifyURL != "" {
|
||||
notifyURL = h.App.Config.GeekPayConfig.NotifyURL
|
||||
} else {
|
||||
notifyURL = fmt.Sprintf("%s/api/payment/notify/geek", data.Host)
|
||||
}
|
||||
if h.App.Config.GeekPayConfig.ReturnURL != "" {
|
||||
data.Host = utils.GetBaseURL(h.App.Config.GeekPayConfig.ReturnURL)
|
||||
}
|
||||
if data.Device == "wechat" { // 微信客户端打开,调回手机端用户中心页面
|
||||
returnURL = fmt.Sprintf("%s/mobile/profile", data.Host)
|
||||
} else {
|
||||
returnURL = fmt.Sprintf("%s/payReturn", data.Host)
|
||||
}
|
||||
params := payment.GeekPayParams{
|
||||
OutTradeNo: orderNo,
|
||||
Method: "web",
|
||||
Name: product.Name,
|
||||
Money: fmt.Sprintf("%f", amount),
|
||||
ClientIP: c.ClientIP(),
|
||||
Device: data.Device,
|
||||
Type: data.PayType,
|
||||
ReturnURL: returnURL,
|
||||
NotifyURL: notifyURL,
|
||||
}
|
||||
|
||||
res, err := h.geekPayService.Pay(params)
|
||||
if err != nil {
|
||||
errMsg := "error with generating Alipay URL: " + err.Error()
|
||||
resp.ERROR(c, errMsg)
|
||||
return
|
||||
}
|
||||
case "wechat":
|
||||
payWay = PayWayWechat.Name
|
||||
payURL, err = h.wechatPayService.PayUrlH5(orderNo, int(amount*100), product.Name, c.ClientIP())
|
||||
if err != nil {
|
||||
errMsg := "error with generating Wechat URL: " + err.Error()
|
||||
logger.Error(errMsg)
|
||||
resp.ERROR(c, errMsg)
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
payURL = res.PayURL
|
||||
default:
|
||||
resp.ERROR(c, "Unsupported pay way: "+data.PayWay)
|
||||
resp.ERROR(c, "不支持的支付渠道")
|
||||
return
|
||||
}
|
||||
|
||||
// 创建订单
|
||||
remark := types.OrderRemark{
|
||||
Days: product.Days,
|
||||
@@ -390,7 +240,6 @@ func (h *PaymentHandler) Mobile(c *gin.Context) {
|
||||
Price: product.Price,
|
||||
Discount: product.Discount,
|
||||
}
|
||||
|
||||
order := model.Order{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
@@ -399,26 +248,24 @@ func (h *PaymentHandler) Mobile(c *gin.Context) {
|
||||
Subject: product.Name,
|
||||
Amount: amount,
|
||||
Status: types.OrderNotPaid,
|
||||
PayWay: payWay,
|
||||
PayWay: data.PayWay,
|
||||
PayType: data.PayType,
|
||||
Remark: utils.JsonEncode(remark),
|
||||
}
|
||||
res = h.DB.Create(&order)
|
||||
if res.Error != nil || res.RowsAffected == 0 {
|
||||
resp.ERROR(c, "error with create order: "+res.Error.Error())
|
||||
err = h.DB.Create(&order).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with create order: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, gin.H{"url": payURL, "order_no": orderNo})
|
||||
resp.SUCCESS(c, payURL)
|
||||
}
|
||||
|
||||
// 异步通知回调公共逻辑
|
||||
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
|
||||
err := h.DB.Where("order_no = ?", orderNo).First(&order).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("error with fetch order: %v", err)
|
||||
}
|
||||
|
||||
h.lock.Lock()
|
||||
@@ -430,45 +277,24 @@ func (h *PaymentHandler) notify(orderNo string, tradeNo string) error {
|
||||
}
|
||||
|
||||
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
|
||||
err = h.DB.First(&user, order.UserId).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("error with fetch user info: %v", err)
|
||||
}
|
||||
|
||||
var remark types.OrderRemark
|
||||
err := utils.JsonDecode(order.Remark, &remark)
|
||||
err = utils.JsonDecode(order.Remark, &remark)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("error with decode order remark: %v", err)
|
||||
logger.Error(err)
|
||||
return err
|
||||
return fmt.Errorf("error with decode order remark: %v", err)
|
||||
}
|
||||
|
||||
var opt string
|
||||
var power int
|
||||
if remark.Days > 0 { // VIP 充值
|
||||
if user.ExpiredTime >= time.Now().Unix() {
|
||||
user.ExpiredTime = time.Unix(user.ExpiredTime, 0).AddDate(0, 0, remark.Days).Unix()
|
||||
opt = "VIP充值,VIP 没到期,只延期不增加算力"
|
||||
} else {
|
||||
user.ExpiredTime = time.Now().AddDate(0, 0, remark.Days).Unix()
|
||||
user.Power += h.App.SysConfig.VipMonthPower
|
||||
power = h.App.SysConfig.VipMonthPower
|
||||
opt = "VIP充值"
|
||||
}
|
||||
user.Vip = true
|
||||
} else { // 充值点卡,直接增加次数即可
|
||||
user.Power += remark.Power
|
||||
opt = "点卡充值"
|
||||
power = remark.Power
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
res = h.DB.Updates(&user)
|
||||
if res.Error != nil {
|
||||
err := fmt.Errorf("error with update user info: %v", res.Error)
|
||||
logger.Error(err)
|
||||
// 增加用户算力
|
||||
err = h.userService.IncreasePower(int(order.UserId), remark.Power, model.PowerLog{
|
||||
Type: types.PowerRecharge,
|
||||
Model: order.PayWay,
|
||||
Remark: fmt.Sprintf("充值算力,金额:%f,订单号:%s", order.Amount, order.OrderNo),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -476,29 +302,16 @@ func (h *PaymentHandler) notify(orderNo string, tradeNo string) error {
|
||||
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
|
||||
err = h.DB.Updates(&order).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("error with update order info: %v", err)
|
||||
}
|
||||
|
||||
// 更新产品销量
|
||||
h.DB.Model(&model.Product{}).Where("id = ?", order.ProductId).UpdateColumn("sales", gorm.Expr("sales + ?", 1))
|
||||
|
||||
// 记录算力充值日志
|
||||
if power > 0 {
|
||||
h.DB.Create(&model.PowerLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
Type: types.PowerRecharge,
|
||||
Amount: power,
|
||||
Balance: user.Power,
|
||||
Mark: types.PowerAdd,
|
||||
Model: order.PayWay,
|
||||
Remark: fmt.Sprintf("%s,金额:%f,订单号:%s", opt, order.Amount, order.OrderNo),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
err = h.DB.Model(&model.Product{}).Where("id = ?", order.ProductId).
|
||||
UpdateColumn("sales", gorm.Expr("sales + ?", 1)).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("error with update product sales: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -506,20 +319,22 @@ func (h *PaymentHandler) notify(orderNo string, tradeNo string) error {
|
||||
|
||||
// GetPayWays 获取支付方式
|
||||
func (h *PaymentHandler) GetPayWays(c *gin.Context) {
|
||||
data := gin.H{}
|
||||
payWays := make([]gin.H, 0)
|
||||
if h.App.Config.AlipayConfig.Enabled {
|
||||
data["alipay"] = gin.H{"name": "alipay"}
|
||||
payWays = append(payWays, gin.H{"pay_way": "alipay", "pay_type": "alipay"})
|
||||
}
|
||||
if h.App.Config.HuPiPayConfig.Enabled {
|
||||
data["hupi"] = gin.H{"name": h.App.Config.HuPiPayConfig.Name}
|
||||
payWays = append(payWays, gin.H{"pay_way": "hupi", "pay_type": "wxpay"})
|
||||
}
|
||||
if h.App.Config.JPayConfig.Enabled {
|
||||
data["payjs"] = gin.H{"name": h.App.Config.JPayConfig.Name}
|
||||
if h.App.Config.GeekPayConfig.Enabled {
|
||||
for _, v := range h.App.Config.GeekPayConfig.Methods {
|
||||
payWays = append(payWays, gin.H{"pay_way": "geek", "pay_type": v})
|
||||
}
|
||||
}
|
||||
if h.App.Config.WechatPayConfig.Enabled {
|
||||
data["wechat"] = gin.H{"name": "wechat"}
|
||||
payWays = append(payWays, gin.H{"pay_way": "wechat", "pay_type": "wxpay"})
|
||||
}
|
||||
resp.SUCCESS(c, data)
|
||||
resp.SUCCESS(c, payWays)
|
||||
}
|
||||
|
||||
// HuPiPayNotify 虎皮椒支付异步回调
|
||||
@@ -532,15 +347,17 @@ func (h *PaymentHandler) HuPiPayNotify(c *gin.Context) {
|
||||
|
||||
orderNo := c.Request.Form.Get("trade_order_id")
|
||||
tradeNo := c.Request.Form.Get("open_order_id")
|
||||
logger.Infof("收到虎皮椒订单支付回调,订单 NO:%s,交易流水号:%s", orderNo, tradeNo)
|
||||
logger.Infof("收到虎皮椒订单支付回调,%+v", c.Request.Form)
|
||||
|
||||
if err = h.huPiPayService.Check(tradeNo); err != nil {
|
||||
if err = h.huPiPayService.Check(orderNo); err != nil {
|
||||
logger.Error("订单校验失败:", err)
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.notify(orderNo, tradeNo)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
@@ -556,18 +373,18 @@ func (h *PaymentHandler) AlipayNotify(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO:验证交易签名
|
||||
res := h.alipayService.TradeVerify(c.Request)
|
||||
logger.Infof("验证支付结果:%+v", res)
|
||||
if !res.Success() {
|
||||
logger.Error("订单校验失败:", res.Message)
|
||||
result := h.alipayService.TradeVerify(c.Request)
|
||||
logger.Infof("收到支付宝商号订单支付回调:%+v", result)
|
||||
if !result.Success() {
|
||||
logger.Error("订单校验失败:", result.Message)
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
tradeNo := c.Request.Form.Get("trade_no")
|
||||
err = h.notify(res.OutTradeNo, tradeNo)
|
||||
err = h.notify(result.OutTradeNo, tradeNo)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
@@ -575,33 +392,30 @@ func (h *PaymentHandler) AlipayNotify(c *gin.Context) {
|
||||
c.String(http.StatusOK, "success")
|
||||
}
|
||||
|
||||
// PayJsNotify PayJs 支付异步回调
|
||||
func (h *PaymentHandler) PayJsNotify(c *gin.Context) {
|
||||
err := c.Request.ParseForm()
|
||||
if err != nil {
|
||||
// GeekPayNotify 支付异步回调
|
||||
func (h *PaymentHandler) GeekPayNotify(c *gin.Context) {
|
||||
var params = make(map[string]string)
|
||||
for k := range c.Request.URL.Query() {
|
||||
params[k] = c.Query(k)
|
||||
}
|
||||
|
||||
logger.Infof("收到GeekPay订单支付回调:%+v", params)
|
||||
// 检查支付状态
|
||||
if params["trade_status"] != "TRADE_SUCCESS" {
|
||||
c.String(http.StatusOK, "success")
|
||||
return
|
||||
}
|
||||
|
||||
sign := h.geekPayService.Sign(params)
|
||||
if sign != c.Query("sign") {
|
||||
logger.Errorf("签名验证失败, %s, %s", sign, c.Query("sign"))
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
orderNo := c.Request.Form.Get("out_trade_no")
|
||||
returnCode := c.Request.Form.Get("return_code")
|
||||
logger.Infof("收到PayJs订单支付回调,订单 NO:%s,支付结果代码:%v", orderNo, returnCode)
|
||||
// 支付失败
|
||||
if returnCode != "1" {
|
||||
return
|
||||
}
|
||||
|
||||
// 校验订单支付状态
|
||||
tradeNo := c.Request.Form.Get("payjs_order_id")
|
||||
err = h.jsPayService.TradeVerify(tradeNo)
|
||||
if err != nil {
|
||||
logger.Error("订单校验失败:", err)
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.notify(orderNo, tradeNo)
|
||||
err := h.notify(params["out_trade_no"], params["trade_no"])
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
@@ -618,6 +432,7 @@ func (h *PaymentHandler) WechatPayNotify(c *gin.Context) {
|
||||
}
|
||||
|
||||
result := h.wechatPayService.TradeVerify(c.Request)
|
||||
logger.Infof("收到微信商号订单支付回调:%+v", result)
|
||||
if !result.Success() {
|
||||
logger.Error("订单校验失败:", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
@@ -629,6 +444,7 @@ func (h *PaymentHandler) WechatPayNotify(c *gin.Context) {
|
||||
|
||||
err = h.notify(result.OutTradeNo, result.TradeId)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
128
api/handler/realtime_handler.go
Normal file
128
api/handler/realtime_handler.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/store/model"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"gorm.io/gorm"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
// OpenAI Realtime API Relay Server
|
||||
|
||||
type RealtimeHandler struct {
|
||||
BaseHandler
|
||||
}
|
||||
|
||||
func NewRealtimeHandler(server *core.AppServer, db *gorm.DB) *RealtimeHandler {
|
||||
return &RealtimeHandler{BaseHandler{App: server, DB: db}}
|
||||
}
|
||||
|
||||
func (h *RealtimeHandler) Connection(c *gin.Context) {
|
||||
// 获取客户端请求中指定的子协议
|
||||
clientProtocols := c.GetHeader("Sec-WebSocket-Protocol")
|
||||
md := c.Query("model")
|
||||
|
||||
userId := h.GetLoginUserId(c)
|
||||
var user model.User
|
||||
if err := h.DB.Where("id", userId).First(&user).Error; err != nil {
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 将 HTTP 协议升级为 Websocket 协议
|
||||
subProtocols := strings.Split(clientProtocols, ",")
|
||||
ws, err := (&websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
Subprotocols: subProtocols,
|
||||
}).Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
defer ws.Close()
|
||||
|
||||
// 目前只针对 VIP 用户可以访问
|
||||
if !user.Vip {
|
||||
sendError(ws, "当前功能只针对 VIP 用户开放")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
var apiKey model.ApiKey
|
||||
h.DB.Where("type", "realtime").Where("enabled", true).Order("last_used_at ASC").First(&apiKey)
|
||||
if apiKey.Id == 0 {
|
||||
sendError(ws, "管理员未配置 Realtime API KEY")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
apiURL := fmt.Sprintf("%s/v1/realtime?model=%s", apiKey.ApiURL, md)
|
||||
// 连接到真实的后端服务器,传入相同的子协议
|
||||
headers := http.Header{}
|
||||
// 修正子协议内容
|
||||
subProtocols[1] = "openai-insecure-api-key." + apiKey.Value
|
||||
if clientProtocols != "" {
|
||||
headers.Set("Sec-WebSocket-Protocol", strings.Join(subProtocols, ","))
|
||||
}
|
||||
backendConn, _, err := websocket.DefaultDialer.Dial(apiURL, headers)
|
||||
if err != nil {
|
||||
sendError(ws, "桥接后端 API 失败:"+err.Error())
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
defer backendConn.Close()
|
||||
|
||||
// 确保协议一致性,如果失败返回
|
||||
if ws.Subprotocol() != backendConn.Subprotocol() {
|
||||
sendError(ws, "Websocket 子协议不匹配")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 更新API KEY 最后使用时间
|
||||
h.DB.Model(&model.ApiKey{}).Where("id", apiKey.Id).UpdateColumn("last_used_at", time.Now().Unix())
|
||||
|
||||
// 开始双向转发
|
||||
errorChan := make(chan error, 2)
|
||||
go relay(ws, backendConn, errorChan)
|
||||
go relay(backendConn, ws, errorChan)
|
||||
|
||||
// 等待其中一个连接关闭
|
||||
err = <-errorChan
|
||||
logger.Infof("Relay ended: %v", err)
|
||||
}
|
||||
|
||||
func relay(src, dst *websocket.Conn, errorChan chan error) {
|
||||
for {
|
||||
messageType, message, err := src.ReadMessage()
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
return
|
||||
}
|
||||
err = dst.WriteMessage(messageType, message)
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sendError(ws *websocket.Conn, message string) {
|
||||
err := ws.WriteJSON(map[string]string{"event_id": "event_01", "type": "error", "error": message})
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
88
api/handler/redeem_handler.go
Normal file
88
api/handler/redeem_handler.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package handler
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/store/model"
|
||||
"geekai/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RedeemHandler struct {
|
||||
BaseHandler
|
||||
lock sync.Mutex
|
||||
userService *service.UserService
|
||||
}
|
||||
|
||||
func NewRedeemHandler(app *core.AppServer, db *gorm.DB, userService *service.UserService) *RedeemHandler {
|
||||
return &RedeemHandler{BaseHandler: BaseHandler{App: app, DB: db}, userService: userService}
|
||||
}
|
||||
|
||||
func (h *RedeemHandler) Verify(c *gin.Context) {
|
||||
var data struct {
|
||||
Code string `json:"code"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
userId := h.GetLoginUserId(c)
|
||||
|
||||
h.lock.Lock()
|
||||
defer h.lock.Unlock()
|
||||
|
||||
var item model.Redeem
|
||||
res := h.DB.Where("code", data.Code).First(&item)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "无效的兑换码!")
|
||||
return
|
||||
}
|
||||
|
||||
if !item.Enabled {
|
||||
resp.ERROR(c, "当前兑换码已被禁用!")
|
||||
return
|
||||
}
|
||||
|
||||
if item.RedeemedAt > 0 {
|
||||
resp.ERROR(c, "当前兑换码已使用,请勿重复使用!")
|
||||
return
|
||||
}
|
||||
|
||||
tx := h.DB.Begin()
|
||||
err := h.userService.IncreasePower(int(userId), item.Power, model.PowerLog{
|
||||
Type: types.PowerRedeem,
|
||||
Model: "兑换码",
|
||||
Remark: fmt.Sprintf("兑换码核销,算力:%d,兑换码:%s...", item.Power, item.Code[:10]),
|
||||
})
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 更新核销状态
|
||||
item.RedeemedAt = time.Now().Unix()
|
||||
item.UserId = userId
|
||||
err = tx.Updates(&item).Error
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
resp.SUCCESS(c)
|
||||
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
package handler
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"math"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RewardHandler struct {
|
||||
BaseHandler
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func NewRewardHandler(app *core.AppServer, db *gorm.DB) *RewardHandler {
|
||||
return &RewardHandler{BaseHandler: BaseHandler{App: app, DB: db}}
|
||||
}
|
||||
|
||||
// Verify 打赏码核销
|
||||
func (h *RewardHandler) Verify(c *gin.Context) {
|
||||
var data struct {
|
||||
TxId string `json:"tx_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.GetLoginUser(c)
|
||||
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 {
|
||||
resp.ERROR(c, "无效的众筹交易流水号!")
|
||||
return
|
||||
}
|
||||
|
||||
if item.Status {
|
||||
resp.ERROR(c, "当前众筹交易流水号已经被核销,请不要重复核销!")
|
||||
return
|
||||
}
|
||||
|
||||
tx := h.DB.Begin()
|
||||
exchange := vo.RewardExchange{}
|
||||
power := math.Ceil(item.Amount / h.App.SysConfig.PowerPrice)
|
||||
exchange.Power = int(power)
|
||||
res = tx.Model(&user).UpdateColumn("power", gorm.Expr("power + ?", exchange.Power))
|
||||
if res.Error != nil {
|
||||
tx.Rollback()
|
||||
logger.Error("添加应用失败:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
}
|
||||
|
||||
// 更新核销状态
|
||||
item.Status = true
|
||||
item.UserId = user.Id
|
||||
item.Exchange = utils.JsonEncode(exchange)
|
||||
res = tx.Updates(&item)
|
||||
if res.Error != nil {
|
||||
tx.Rollback()
|
||||
logger.Error("添加应用失败:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
}
|
||||
|
||||
// 记录算力充值日志
|
||||
h.DB.Create(&model.PowerLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
Type: types.PowerReward,
|
||||
Amount: exchange.Power,
|
||||
Balance: user.Power + exchange.Power,
|
||||
Mark: types.PowerAdd,
|
||||
Model: "众筹支付",
|
||||
Remark: fmt.Sprintf("众筹充值算力,金额:%f,价格:%f", item.Amount, h.App.SysConfig.PowerPrice),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
tx.Commit()
|
||||
resp.SUCCESS(c)
|
||||
|
||||
}
|
||||
@@ -19,11 +19,8 @@ import (
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"gorm.io/gorm"
|
||||
@@ -31,19 +28,27 @@ import (
|
||||
|
||||
type SdJobHandler struct {
|
||||
BaseHandler
|
||||
redis *redis.Client
|
||||
pool *sd.ServicePool
|
||||
uploader *oss.UploaderManager
|
||||
snowflake *service.Snowflake
|
||||
leveldb *store.LevelDB
|
||||
redis *redis.Client
|
||||
sdService *sd.Service
|
||||
uploader *oss.UploaderManager
|
||||
snowflake *service.Snowflake
|
||||
leveldb *store.LevelDB
|
||||
userService *service.UserService
|
||||
}
|
||||
|
||||
func NewSdJobHandler(app *core.AppServer, db *gorm.DB, pool *sd.ServicePool, manager *oss.UploaderManager, snowflake *service.Snowflake, levelDB *store.LevelDB) *SdJobHandler {
|
||||
func NewSdJobHandler(app *core.AppServer,
|
||||
db *gorm.DB,
|
||||
service *sd.Service,
|
||||
manager *oss.UploaderManager,
|
||||
snowflake *service.Snowflake,
|
||||
userService *service.UserService,
|
||||
levelDB *store.LevelDB) *SdJobHandler {
|
||||
return &SdJobHandler{
|
||||
pool: pool,
|
||||
uploader: manager,
|
||||
snowflake: snowflake,
|
||||
leveldb: levelDB,
|
||||
sdService: service,
|
||||
uploader: manager,
|
||||
snowflake: snowflake,
|
||||
leveldb: levelDB,
|
||||
userService: userService,
|
||||
BaseHandler: BaseHandler{
|
||||
App: app,
|
||||
DB: db,
|
||||
@@ -51,27 +56,6 @@ func NewSdJobHandler(app *core.AppServer, db *gorm.DB, pool *sd.ServicePool, man
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
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())
|
||||
}
|
||||
|
||||
func (h *SdJobHandler) preCheck(c *gin.Context) bool {
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
@@ -79,11 +63,6 @@ func (h *SdJobHandler) preCheck(c *gin.Context) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
if !h.pool.HasAvailableService() {
|
||||
resp.ERROR(c, "Stable-Diffusion 池子中没有没有可用的服务!")
|
||||
return false
|
||||
}
|
||||
|
||||
if user.Power < h.App.SysConfig.SdPower {
|
||||
resp.ERROR(c, "当前用户剩余算力不足以完成本次绘画!")
|
||||
return false
|
||||
@@ -164,34 +143,23 @@ func (h *SdJobHandler) Image(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
h.pool.PushTask(types.SdTask{
|
||||
Id: int(job.Id),
|
||||
Type: types.TaskImage,
|
||||
Params: params,
|
||||
UserId: userId,
|
||||
h.sdService.PushTask(types.SdTask{
|
||||
Id: int(job.Id),
|
||||
ClientId: data.ClientId,
|
||||
Type: types.TaskImage,
|
||||
Params: params,
|
||||
UserId: userId,
|
||||
})
|
||||
|
||||
client := h.pool.Clients.Get(uint(job.UserId))
|
||||
if client != nil {
|
||||
_ = client.Send([]byte("Task Updated"))
|
||||
}
|
||||
|
||||
// update user's power
|
||||
tx := h.DB.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power - ?", job.Power))
|
||||
// 记录算力变化日志
|
||||
if tx.Error == nil && tx.RowsAffected > 0 {
|
||||
user, _ := h.GetLoginUser(c)
|
||||
h.DB.Create(&model.PowerLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
Type: types.PowerConsume,
|
||||
Amount: job.Power,
|
||||
Balance: user.Power - job.Power,
|
||||
Mark: types.PowerSub,
|
||||
Model: "stable-diffusion",
|
||||
Remark: fmt.Sprintf("绘图操作,任务ID:%s", job.TaskId),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
err = h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: "stable-diffusion",
|
||||
Remark: fmt.Sprintf("绘图操作,任务ID:%s", job.TaskId),
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
@@ -228,11 +196,11 @@ func (h *SdJobHandler) JobList(c *gin.Context) {
|
||||
}
|
||||
|
||||
// JobList 获取 MJ 任务列表
|
||||
func (h *SdJobHandler) getData(finish bool, userId uint, page int, pageSize int, publish bool) (error, []vo.SdJob) {
|
||||
func (h *SdJobHandler) getData(finish bool, userId uint, page int, pageSize int, publish bool) (error, vo.Page) {
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if finish {
|
||||
session = session.Where("progress = ?", 100).Order("id DESC")
|
||||
session = session.Where("progress >= ?", 100).Order("id DESC")
|
||||
} else {
|
||||
session = session.Where("progress < ?", 100).Order("id ASC")
|
||||
}
|
||||
@@ -247,10 +215,14 @@ func (h *SdJobHandler) getData(finish bool, userId uint, page int, pageSize int,
|
||||
session = session.Offset(offset).Limit(pageSize)
|
||||
}
|
||||
|
||||
// 统计总数
|
||||
var total int64
|
||||
session.Model(&model.SdJob{}).Count(&total)
|
||||
|
||||
var items []model.SdJob
|
||||
res := session.Find(&items)
|
||||
if res.Error != nil {
|
||||
return res.Error, nil
|
||||
return res.Error, vo.Page{}
|
||||
}
|
||||
|
||||
var jobs = make([]vo.SdJob, 0)
|
||||
@@ -260,37 +232,39 @@ func (h *SdJobHandler) getData(finish bool, userId uint, page int, pageSize int,
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if item.Progress < 100 {
|
||||
// 从 leveldb 中获取图片预览数据
|
||||
var imageData string
|
||||
err = h.leveldb.Get(item.TaskId, &imageData)
|
||||
if err == nil {
|
||||
job.ImgURL = "data:image/png;base64," + imageData
|
||||
}
|
||||
}
|
||||
jobs = append(jobs, job)
|
||||
}
|
||||
|
||||
return nil, jobs
|
||||
return nil, vo.NewPage(total, page, pageSize, jobs)
|
||||
}
|
||||
|
||||
// Remove remove task image
|
||||
func (h *SdJobHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
userId := h.GetInt(c, "user_id", 0)
|
||||
userId := h.GetLoginUserId(c)
|
||||
var job model.SdJob
|
||||
if res := h.DB.Where("id = ? AND user_id = ?", id, userId).First(&job); res.Error != nil {
|
||||
resp.ERROR(c, "记录不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// remove job recode
|
||||
res := h.DB.Delete(&model.SdJob{Id: job.Id})
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
// 删除任务
|
||||
tx := h.DB.Begin()
|
||||
tx.Delete(&job)
|
||||
// 如果任务未完成,或者任务失败,则恢复用户算力
|
||||
if job.Progress != 100 {
|
||||
err := h.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{
|
||||
Type: types.PowerRefund,
|
||||
Model: "stable-diffusion",
|
||||
Remark: fmt.Sprintf("任务失败,退回算力。任务ID:%d, Err: %s", job.Id, job.ErrMsg),
|
||||
})
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
// remove image
|
||||
err := h.uploader.GetUploadHandler().Delete(job.ImgURL)
|
||||
@@ -298,24 +272,18 @@ func (h *SdJobHandler) Remove(c *gin.Context) {
|
||||
logger.Error("remove image failed: ", err)
|
||||
}
|
||||
|
||||
client := h.pool.Clients.Get(uint(job.UserId))
|
||||
if client != nil {
|
||||
_ = client.Send([]byte(sd.Finished))
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// Publish 发布/取消发布图片到画廊显示
|
||||
func (h *SdJobHandler) Publish(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
userId := h.GetInt(c, "user_id", 0)
|
||||
userId := h.GetLoginUserId(c)
|
||||
action := h.GetBool(c, "action") // 发布动作,true => 发布,false => 取消分享
|
||||
|
||||
res := h.DB.Model(&model.SdJob{Id: uint(id), UserId: userId}).UpdateColumn("publish", action)
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败")
|
||||
err := h.DB.Model(&model.SdJob{Id: uint(id), UserId: int(userId)}).UpdateColumn("publish", action).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -56,15 +56,17 @@ func (h *SmsHandler) SendCode(c *gin.Context) {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
var check bool
|
||||
if data.X != 0 {
|
||||
check = h.captcha.SlideCheck(data)
|
||||
} else {
|
||||
check = h.captcha.Check(data)
|
||||
}
|
||||
if !check {
|
||||
resp.ERROR(c, "验证码错误,请先完人机验证")
|
||||
return
|
||||
if h.App.SysConfig.EnabledVerify {
|
||||
var check bool
|
||||
if data.X != 0 {
|
||||
check = h.captcha.SlideCheck(data)
|
||||
} else {
|
||||
check = h.captcha.Check(data)
|
||||
}
|
||||
if !check {
|
||||
resp.ERROR(c, "请先完人机验证")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
code := utils.RandomNumber(6)
|
||||
@@ -74,6 +76,20 @@ func (h *SmsHandler) SendCode(c *gin.Context) {
|
||||
resp.ERROR(c, "系统已禁用邮箱注册!")
|
||||
return
|
||||
}
|
||||
// 检查邮箱后缀是否在白名单
|
||||
if len(h.App.SysConfig.EmailWhiteList) > 0 {
|
||||
inWhiteList := false
|
||||
for _, suffix := range h.App.SysConfig.EmailWhiteList {
|
||||
if strings.HasSuffix(data.Receiver, suffix) {
|
||||
inWhiteList = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !inWhiteList {
|
||||
resp.ERROR(c, "邮箱后缀不在白名单中")
|
||||
return
|
||||
}
|
||||
}
|
||||
err = h.smtp.SendVerifyCode(data.Receiver, code)
|
||||
} else {
|
||||
if !utils.Contains(h.App.SysConfig.RegisterWays, "mobile") {
|
||||
|
||||
373
api/handler/suno_handler.go
Normal file
373
api/handler/suno_handler.go
Normal file
@@ -0,0 +1,373 @@
|
||||
package handler
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/service/oss"
|
||||
"geekai/service/suno"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SunoHandler struct {
|
||||
BaseHandler
|
||||
sunoService *suno.Service
|
||||
uploader *oss.UploaderManager
|
||||
userService *service.UserService
|
||||
}
|
||||
|
||||
func NewSunoHandler(app *core.AppServer, db *gorm.DB, service *suno.Service, uploader *oss.UploaderManager, userService *service.UserService) *SunoHandler {
|
||||
return &SunoHandler{
|
||||
BaseHandler: BaseHandler{
|
||||
App: app,
|
||||
DB: db,
|
||||
},
|
||||
sunoService: service,
|
||||
uploader: uploader,
|
||||
userService: userService,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *SunoHandler) Create(c *gin.Context) {
|
||||
|
||||
var data struct {
|
||||
ClientId string `json:"client_id"`
|
||||
Prompt string `json:"prompt"`
|
||||
Instrumental bool `json:"instrumental"`
|
||||
Lyrics string `json:"lyrics"`
|
||||
Model string `json:"model"`
|
||||
Tags string `json:"tags"`
|
||||
Title string `json:"title"`
|
||||
Type int `json:"type"`
|
||||
RefTaskId string `json:"ref_task_id"` // 续写的任务id
|
||||
ExtendSecs int `json:"extend_secs"` // 续写秒数
|
||||
RefSongId string `json:"ref_song_id"` // 续写的歌曲id
|
||||
SongId string `json:"song_id,omitempty"` // 要拼接的歌曲id
|
||||
AudioURL string `json:"audio_url,omitempty"` // 上传自己创作的歌曲
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
if user.Power < h.App.SysConfig.SunoPower {
|
||||
resp.ERROR(c, "您的算力不足,请充值后再试!")
|
||||
return
|
||||
}
|
||||
|
||||
// 歌曲拼接
|
||||
if data.SongId != "" && data.Type == 3 {
|
||||
var song model.SunoJob
|
||||
if err := h.DB.Where("song_id = ?", data.SongId).First(&song).Error; err == nil {
|
||||
data.Instrumental = song.Instrumental
|
||||
data.Model = song.ModelName
|
||||
data.Tags = song.Tags
|
||||
}
|
||||
// 拼接歌词
|
||||
var refSong model.SunoJob
|
||||
if err := h.DB.Where("song_id = ?", data.RefSongId).First(&refSong).Error; err == nil {
|
||||
data.Prompt = fmt.Sprintf("%s\n%s", song.Prompt, refSong.Prompt)
|
||||
}
|
||||
}
|
||||
|
||||
// 插入数据库
|
||||
job := model.SunoJob{
|
||||
UserId: int(h.GetLoginUserId(c)),
|
||||
Prompt: data.Prompt,
|
||||
Instrumental: data.Instrumental,
|
||||
ModelName: data.Model,
|
||||
Tags: data.Tags,
|
||||
Title: data.Title,
|
||||
Type: data.Type,
|
||||
RefSongId: data.RefSongId,
|
||||
RefTaskId: data.RefTaskId,
|
||||
ExtendSecs: data.ExtendSecs,
|
||||
Power: h.App.SysConfig.SunoPower,
|
||||
SongId: utils.RandString(32),
|
||||
}
|
||||
if data.Lyrics != "" {
|
||||
job.Prompt = data.Lyrics
|
||||
}
|
||||
tx := h.DB.Create(&job)
|
||||
if tx.Error != nil {
|
||||
resp.ERROR(c, tx.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 创建任务
|
||||
h.sunoService.PushTask(types.SunoTask{
|
||||
ClientId: data.ClientId,
|
||||
Id: job.Id,
|
||||
UserId: job.UserId,
|
||||
Type: job.Type,
|
||||
Title: job.Title,
|
||||
RefTaskId: data.RefTaskId,
|
||||
RefSongId: data.RefSongId,
|
||||
ExtendSecs: data.ExtendSecs,
|
||||
Prompt: job.Prompt,
|
||||
Tags: data.Tags,
|
||||
Model: data.Model,
|
||||
Instrumental: data.Instrumental,
|
||||
SongId: data.SongId,
|
||||
AudioURL: data.AudioURL,
|
||||
})
|
||||
|
||||
// update user's power
|
||||
err = h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Remark: fmt.Sprintf("Suno 文生歌曲,%s", job.ModelName),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *SunoHandler) List(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
page := h.GetInt(c, "page", 1)
|
||||
pageSize := h.GetInt(c, "page_size", 20)
|
||||
session := h.DB.Session(&gorm.Session{}).Where("user_id", userId)
|
||||
|
||||
// 统计总数
|
||||
var total int64
|
||||
session.Model(&model.SunoJob{}).Count(&total)
|
||||
|
||||
if page > 0 && pageSize > 0 {
|
||||
offset := (page - 1) * pageSize
|
||||
session = session.Offset(offset).Limit(pageSize)
|
||||
}
|
||||
var list []model.SunoJob
|
||||
err := session.Order("id desc").Find(&list).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
// 初始化续写关系
|
||||
songIds := make([]string, 0)
|
||||
for _, v := range list {
|
||||
if v.RefTaskId != "" {
|
||||
songIds = append(songIds, v.RefSongId)
|
||||
}
|
||||
}
|
||||
var tasks []model.SunoJob
|
||||
h.DB.Where("song_id IN ?", songIds).Find(&tasks)
|
||||
songMap := make(map[string]model.SunoJob)
|
||||
for _, t := range tasks {
|
||||
songMap[t.SongId] = t
|
||||
}
|
||||
// 转换为 VO
|
||||
items := make([]vo.SunoJob, 0)
|
||||
for _, v := range list {
|
||||
var item vo.SunoJob
|
||||
err = utils.CopyObject(v, &item)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
item.CreatedAt = v.CreatedAt.Unix()
|
||||
if s, ok := songMap[v.RefSongId]; ok {
|
||||
item.RefSong = map[string]interface{}{
|
||||
"id": s.Id,
|
||||
"title": s.Title,
|
||||
"cover": s.CoverURL,
|
||||
"audio": s.AudioURL,
|
||||
}
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, vo.NewPage(total, page, pageSize, items))
|
||||
}
|
||||
|
||||
func (h *SunoHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
userId := h.GetLoginUserId(c)
|
||||
var job model.SunoJob
|
||||
err := h.DB.Where("id = ?", id).Where("user_id", userId).First(&job).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 只有失败,或者超时的任务才能删除
|
||||
if job.Progress != service.FailTaskProgress || time.Now().Before(job.CreatedAt.Add(time.Minute*10)) {
|
||||
resp.ERROR(c, "只有失败和超时(10分钟)的任务才能删除!")
|
||||
return
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
tx := h.DB.Begin()
|
||||
if err := tx.Delete(&job).Error; err != nil {
|
||||
tx.Rollback()
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 恢复用户算力
|
||||
err = h.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{
|
||||
Type: types.PowerRefund,
|
||||
Model: job.ModelName,
|
||||
Remark: fmt.Sprintf("Suno 任务失败,退回算力。任务ID:%s,Err:%s", job.TaskId, job.ErrMsg),
|
||||
})
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
// 删除文件
|
||||
_ = h.uploader.GetUploadHandler().Delete(job.CoverURL)
|
||||
_ = h.uploader.GetUploadHandler().Delete(job.AudioURL)
|
||||
}
|
||||
|
||||
func (h *SunoHandler) Publish(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
userId := h.GetLoginUserId(c)
|
||||
publish := h.GetBool(c, "publish")
|
||||
err := h.DB.Model(&model.SunoJob{}).Where("id", id).Where("user_id", userId).UpdateColumn("publish", publish).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *SunoHandler) Update(c *gin.Context) {
|
||||
var data struct {
|
||||
Id int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Cover string `json:"cover"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
if data.Id == 0 || data.Title == "" || data.Cover == "" {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
userId := h.GetLoginUserId(c)
|
||||
var item model.SunoJob
|
||||
if err := h.DB.Where("id", data.Id).Where("user_id", userId).First(&item).Error; err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
item.Title = data.Title
|
||||
item.CoverURL = data.Cover
|
||||
|
||||
if err := h.DB.Updates(&item).Error; err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// Detail 歌曲详情
|
||||
func (h *SunoHandler) Detail(c *gin.Context) {
|
||||
songId := c.Query("song_id")
|
||||
if songId == "" {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
var item model.SunoJob
|
||||
if err := h.DB.Where("song_id", songId).First(&item).Error; err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 读取用户信息
|
||||
var user model.User
|
||||
if err := h.DB.Where("id", item.UserId).First(&user).Error; err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var itemVo vo.SunoJob
|
||||
if err := utils.CopyObject(item, &itemVo); err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
itemVo.CreatedAt = item.CreatedAt.Unix()
|
||||
itemVo.User = map[string]interface{}{
|
||||
"nickname": user.Nickname,
|
||||
"avatar": user.Avatar,
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, itemVo)
|
||||
}
|
||||
|
||||
// Play 增加歌曲播放次数
|
||||
func (h *SunoHandler) Play(c *gin.Context) {
|
||||
songId := c.Query("song_id")
|
||||
if songId == "" {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
h.DB.Model(&model.SunoJob{}).Where("song_id", songId).UpdateColumn("play_times", gorm.Expr("play_times + ?", 1))
|
||||
}
|
||||
|
||||
const genLyricTemplate = `
|
||||
你是一位才华横溢的作曲家,拥有丰富的情感和细腻的笔触,你对文字有着独特的感悟力,能将各种情感和意境巧妙地融入歌词中。
|
||||
请以【%s】为主题创作一首歌曲,歌曲时间不要太短,3分钟左右,不要输出任何解释性的内容。
|
||||
输出格式如下:
|
||||
歌曲名称
|
||||
第一节:
|
||||
{{歌词内容}}
|
||||
副歌:
|
||||
{{歌词内容}}
|
||||
|
||||
第二节:
|
||||
{{歌词内容}}
|
||||
副歌:
|
||||
{{歌词内容}}
|
||||
|
||||
尾声:
|
||||
{{歌词内容}}
|
||||
`
|
||||
|
||||
// Lyric 生成歌词
|
||||
func (h *SunoHandler) Lyric(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(genLyricTemplate, data.Prompt), "gpt-4o-mini", 0)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, content)
|
||||
}
|
||||
@@ -3,15 +3,52 @@ package handler
|
||||
import (
|
||||
"geekai/service"
|
||||
"geekai/service/payment"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type TestHandler struct {
|
||||
db *gorm.DB
|
||||
snowflake *service.Snowflake
|
||||
js *payment.JPayService
|
||||
js *payment.GeekPayService
|
||||
}
|
||||
|
||||
func NewTestHandler(db *gorm.DB, snowflake *service.Snowflake, js *payment.JPayService) *TestHandler {
|
||||
func NewTestHandler(db *gorm.DB, snowflake *service.Snowflake, js *payment.GeekPayService) *TestHandler {
|
||||
return &TestHandler{db: db, snowflake: snowflake, js: js}
|
||||
}
|
||||
|
||||
func (h *TestHandler) SseTest(c *gin.Context) {
|
||||
//c.Header("Body-Type", "text/event-stream")
|
||||
//c.Header("Cache-Control", "no-cache")
|
||||
//c.Header("Connection", "keep-alive")
|
||||
//
|
||||
//
|
||||
//// 模拟实时数据更新
|
||||
//for i := 0; i < 10; i++ {
|
||||
// // 发送 SSE 数据
|
||||
// _, err := fmt.Fprintf(c.Writer, "data: %v\n\n", data)
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
// c.Writer.Flush() // 确保立即发送数据
|
||||
// time.Sleep(1 * time.Second) // 每秒发送一次数据
|
||||
//}
|
||||
//c.Abort()
|
||||
}
|
||||
|
||||
func (h *TestHandler) PostTest(c *gin.Context) {
|
||||
var data struct {
|
||||
Message string `json:"message"`
|
||||
UserId uint `json:"user_id"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 将参数存储在上下文中
|
||||
c.Set("data", data)
|
||||
c.Next()
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ type UserHandler struct {
|
||||
searcher *xdb.Searcher
|
||||
redis *redis.Client
|
||||
licenseService *service.LicenseService
|
||||
captcha *service.CaptchaService
|
||||
userService *service.UserService
|
||||
}
|
||||
|
||||
func NewUserHandler(
|
||||
@@ -40,12 +42,16 @@ func NewUserHandler(
|
||||
db *gorm.DB,
|
||||
searcher *xdb.Searcher,
|
||||
client *redis.Client,
|
||||
captcha *service.CaptchaService,
|
||||
userService *service.UserService,
|
||||
licenseService *service.LicenseService) *UserHandler {
|
||||
return &UserHandler{
|
||||
BaseHandler: BaseHandler{DB: db, App: app},
|
||||
searcher: searcher,
|
||||
redis: client,
|
||||
captcha: captcha,
|
||||
licenseService: licenseService,
|
||||
userService: userService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,14 +61,33 @@ func (h *UserHandler) Register(c *gin.Context) {
|
||||
var data struct {
|
||||
RegWay string `json:"reg_way"`
|
||||
Username string `json:"username"`
|
||||
Mobile string `json:"mobile"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
Code string `json:"code"`
|
||||
InviteCode string `json:"invite_code"`
|
||||
Key string `json:"key,omitempty"`
|
||||
Dots string `json:"dots,omitempty"`
|
||||
X int `json:"x,omitempty"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
if h.App.SysConfig.EnabledVerify && data.RegWay == "username" {
|
||||
var check bool
|
||||
if data.X != 0 {
|
||||
check = h.captcha.SlideCheck(data)
|
||||
} else {
|
||||
check = h.captcha.Check(data)
|
||||
}
|
||||
if !check {
|
||||
resp.ERROR(c, "请先完人机验证")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
data.Password = strings.TrimSpace(data.Password)
|
||||
if len(data.Password) < 8 {
|
||||
resp.ERROR(c, "密码长度不能少于8个字符")
|
||||
@@ -79,8 +104,15 @@ func (h *UserHandler) Register(c *gin.Context) {
|
||||
|
||||
// 检查验证码
|
||||
var key string
|
||||
if data.RegWay == "email" || data.RegWay == "mobile" {
|
||||
key = CodeStorePrefix + data.Username
|
||||
if data.RegWay == "email" {
|
||||
key = CodeStorePrefix + data.Email
|
||||
code, err := h.redis.Get(c, key).Result()
|
||||
if err != nil || code != data.Code {
|
||||
resp.ERROR(c, "验证码错误")
|
||||
return
|
||||
}
|
||||
} else if data.RegWay == "mobile" {
|
||||
key = CodeStorePrefix + data.Mobile
|
||||
code, err := h.redis.Get(c, key).Result()
|
||||
if err != nil || code != data.Code {
|
||||
resp.ERROR(c, "验证码错误")
|
||||
@@ -100,7 +132,17 @@ func (h *UserHandler) Register(c *gin.Context) {
|
||||
|
||||
// check if the username is existing
|
||||
var item model.User
|
||||
res := h.DB.Where("username = ?", data.Username).First(&item)
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if data.Mobile != "" {
|
||||
session = session.Where("mobile = ?", data.Mobile)
|
||||
data.Username = data.Mobile
|
||||
} else if data.Email != "" {
|
||||
session = session.Where("email = ?", data.Email)
|
||||
data.Username = data.Email
|
||||
} else if data.Username != "" {
|
||||
session = session.Where("username = ?", data.Username)
|
||||
}
|
||||
session.First(&item)
|
||||
if item.Id > 0 {
|
||||
resp.ERROR(c, "该用户名已经被注册")
|
||||
return
|
||||
@@ -109,6 +151,8 @@ func (h *UserHandler) Register(c *gin.Context) {
|
||||
salt := utils.RandString(8)
|
||||
user := model.User{
|
||||
Username: data.Username,
|
||||
Mobile: data.Mobile,
|
||||
Email: data.Email,
|
||||
Password: utils.GenPassword(data.Password, salt),
|
||||
Avatar: "/images/avatar/user.png",
|
||||
Salt: salt,
|
||||
@@ -128,10 +172,9 @@ func (h *UserHandler) Register(c *gin.Context) {
|
||||
user.Nickname = fmt.Sprintf("极客学长@%d", utils.RandomNumber(6))
|
||||
}
|
||||
|
||||
res = h.DB.Create(&user)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "保存数据失败")
|
||||
logger.Error(res.Error)
|
||||
tx := h.DB.Begin()
|
||||
if err := tx.Create(&user).Error; err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -140,35 +183,35 @@ func (h *UserHandler) Register(c *gin.Context) {
|
||||
// 增加邀请数量
|
||||
h.DB.Model(&model.InviteCode{}).Where("code = ?", data.InviteCode).UpdateColumn("reg_num", gorm.Expr("reg_num + ?", 1))
|
||||
if h.App.SysConfig.InvitePower > 0 {
|
||||
h.DB.Model(&model.User{}).Where("id = ?", inviteCode.UserId).UpdateColumn("power", gorm.Expr("power + ?", h.App.SysConfig.InvitePower))
|
||||
// 记录邀请算力充值日志
|
||||
var inviter model.User
|
||||
h.DB.Where("id", inviteCode.UserId).First(&inviter)
|
||||
h.DB.Create(&model.PowerLog{
|
||||
UserId: inviter.Id,
|
||||
Username: inviter.Username,
|
||||
Type: types.PowerInvite,
|
||||
Amount: h.App.SysConfig.InvitePower,
|
||||
Balance: inviter.Power,
|
||||
Mark: types.PowerAdd,
|
||||
Model: "",
|
||||
Remark: fmt.Sprintf("邀请用户注册奖励,金额:%d,邀请码:%s,新用户:%s", h.App.SysConfig.InvitePower, inviteCode.Code, user.Username),
|
||||
CreatedAt: time.Now(),
|
||||
err := h.userService.IncreasePower(int(inviteCode.UserId), h.App.SysConfig.InvitePower, model.PowerLog{
|
||||
Type: types.PowerInvite,
|
||||
Model: "",
|
||||
Remark: fmt.Sprintf("邀请用户注册奖励,金额:%d,邀请码:%s,新用户:%s", h.App.SysConfig.InvitePower, inviteCode.Code, user.Username),
|
||||
})
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 添加邀请记录
|
||||
h.DB.Create(&model.InviteLog{
|
||||
err := tx.Create(&model.InviteLog{
|
||||
InviterId: inviteCode.UserId,
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
InviteCode: inviteCode.Code,
|
||||
Remark: fmt.Sprintf("奖励 %d 算力", h.App.SysConfig.InvitePower),
|
||||
})
|
||||
}).Error
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
_ = h.redis.Del(c, key) // 注册成功,删除短信验证码
|
||||
|
||||
// 自动登录创建 token
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"user_id": user.Id,
|
||||
@@ -193,20 +236,41 @@ func (h *UserHandler) Login(c *gin.Context) {
|
||||
var data struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Key string `json:"key,omitempty"`
|
||||
Dots string `json:"dots,omitempty"`
|
||||
X int `json:"x,omitempty"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
verifyKey := fmt.Sprintf("users/verify/%s", data.Username)
|
||||
needVerify, err := h.redis.Get(c, verifyKey).Bool()
|
||||
|
||||
if h.App.SysConfig.EnabledVerify && needVerify {
|
||||
var check bool
|
||||
if data.X != 0 {
|
||||
check = h.captcha.SlideCheck(data)
|
||||
} else {
|
||||
check = h.captcha.Check(data)
|
||||
}
|
||||
if !check {
|
||||
resp.ERROR(c, "请先完人机验证")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var user model.User
|
||||
res := h.DB.Where("username = ?", data.Username).First(&user)
|
||||
if res.Error != nil {
|
||||
h.redis.Set(c, verifyKey, true, 0)
|
||||
resp.ERROR(c, "用户名不存在")
|
||||
return
|
||||
}
|
||||
|
||||
password := utils.GenPassword(data.Password, user.Salt)
|
||||
if password != user.Password {
|
||||
h.redis.Set(c, verifyKey, true, 0)
|
||||
resp.ERROR(c, "用户名或密码错误")
|
||||
return
|
||||
}
|
||||
@@ -239,11 +303,13 @@ func (h *UserHandler) Login(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
// 保存到 redis
|
||||
key := fmt.Sprintf("users/%d", user.Id)
|
||||
if _, err := h.redis.Set(c, key, tokenString, 0).Result(); err != nil {
|
||||
sessionKey := fmt.Sprintf("users/%d", user.Id)
|
||||
if _, err = h.redis.Set(c, sessionKey, tokenString, 0).Result(); err != nil {
|
||||
resp.ERROR(c, "error with save token: "+err.Error())
|
||||
return
|
||||
}
|
||||
// 移除登录行为验证码
|
||||
h.redis.Del(c, verifyKey)
|
||||
resp.SUCCESS(c, gin.H{"token": tokenString, "user_id": user.Id, "username": user.Username})
|
||||
}
|
||||
|
||||
@@ -285,8 +351,10 @@ func (h *UserHandler) CLogin(c *gin.Context) {
|
||||
|
||||
// CLoginCallback 第三方登录回调
|
||||
func (h *UserHandler) CLoginCallback(c *gin.Context) {
|
||||
loginType := h.GetTrim(c, "login_type")
|
||||
code := h.GetTrim(c, "code")
|
||||
loginType := c.Query("login_type")
|
||||
code := c.Query("code")
|
||||
userId := h.GetInt(c, "user_id", 0)
|
||||
action := c.Query("action")
|
||||
|
||||
var res types.BizVo
|
||||
apiURL := fmt.Sprintf("%s/api/clogin/info", h.App.Config.ApiConfig.ApiURL)
|
||||
@@ -311,11 +379,34 @@ func (h *UserHandler) CLoginCallback(c *gin.Context) {
|
||||
|
||||
// login successfully
|
||||
data := res.Data.(map[string]interface{})
|
||||
session := gin.H{}
|
||||
var user model.User
|
||||
tx := h.DB.Debug().Where("openid", data["openid"]).First(&user)
|
||||
if tx.Error != nil { // user not exist, create new user
|
||||
// 检测最大注册人数
|
||||
if action == "bind" && userId > 0 {
|
||||
err = h.DB.Where("openid", data["openid"]).First(&user).Error
|
||||
if err == nil {
|
||||
resp.ERROR(c, "该微信已经绑定其他账号,请先解绑")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.DB.Where("id", userId).First(&user).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "绑定用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.DB.Model(&user).UpdateColumn("openid", data["openid"]).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "更新用户信息失败,"+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, gin.H{"token": ""})
|
||||
return
|
||||
}
|
||||
|
||||
session := gin.H{}
|
||||
tx := h.DB.Where("openid", data["openid"]).First(&user)
|
||||
if tx.Error != nil {
|
||||
// create new user
|
||||
var totalUser int64
|
||||
h.DB.Model(&model.User{}).Count(&totalUser)
|
||||
if h.licenseService.GetLicense().Configs.UserNum > 0 && int(totalUser) >= h.licenseService.GetLicense().Configs.UserNum {
|
||||
@@ -383,18 +474,24 @@ func (h *UserHandler) CLoginCallback(c *gin.Context) {
|
||||
// Session 获取/验证会话
|
||||
func (h *UserHandler) Session(c *gin.Context) {
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err == nil {
|
||||
var userVo vo.User
|
||||
err := utils.CopyObject(user, &userVo)
|
||||
if err != nil {
|
||||
resp.ERROR(c)
|
||||
}
|
||||
userVo.Id = user.Id
|
||||
resp.SUCCESS(c, userVo)
|
||||
} else {
|
||||
resp.NotAuth(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var userVo vo.User
|
||||
err = utils.CopyObject(user, &userVo)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
// 用户 VIP 到期
|
||||
if user.ExpiredTime > 0 && user.ExpiredTime < time.Now().Unix() {
|
||||
h.DB.Model(&user).UpdateColumn("vip", false)
|
||||
}
|
||||
userVo.Id = user.Id
|
||||
resp.SUCCESS(c, userVo)
|
||||
|
||||
}
|
||||
|
||||
type userProfile struct {
|
||||
@@ -481,20 +578,21 @@ func (h *UserHandler) UpdatePass(c *gin.Context) {
|
||||
}
|
||||
|
||||
newPass := utils.GenPassword(data.Password, user.Salt)
|
||||
res := h.DB.Model(&user).UpdateColumn("password", newPass)
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database:", res.Error)
|
||||
resp.ERROR(c, "更新数据库失败")
|
||||
err = h.DB.Model(&user).UpdateColumn("password", newPass).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// ResetPass 重置密码
|
||||
// ResetPass 找回密码
|
||||
func (h *UserHandler) ResetPass(c *gin.Context) {
|
||||
var data struct {
|
||||
Username string `json:"username"`
|
||||
Type string `json:"type"` // 验证类别:mobile, email
|
||||
Mobile string `json:"mobile"` // 手机号
|
||||
Email string `json:"email"` // 邮箱地址
|
||||
Code string `json:"code"` // 验证码
|
||||
Password string `json:"password"` // 新密码
|
||||
}
|
||||
@@ -503,37 +601,47 @@ func (h *UserHandler) ResetPass(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
var key string
|
||||
if data.Type == "email" {
|
||||
session = session.Where("email", data.Email)
|
||||
key = CodeStorePrefix + data.Email
|
||||
} else if data.Type == "mobile" {
|
||||
session = session.Where("mobile", data.Mobile)
|
||||
key = CodeStorePrefix + data.Mobile
|
||||
} else {
|
||||
resp.ERROR(c, "验证类别错误")
|
||||
return
|
||||
}
|
||||
var user model.User
|
||||
res := h.DB.Where("username", data.Username).First(&user)
|
||||
if res.Error != nil {
|
||||
err := session.First(&user).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "用户不存在!")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查验证码
|
||||
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
|
||||
}
|
||||
|
||||
password := utils.GenPassword(data.Password, user.Salt)
|
||||
user.Password = password
|
||||
res = h.DB.Updates(&user)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c)
|
||||
err = h.DB.Model(&user).UpdateColumn("password", password).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
} else {
|
||||
h.redis.Del(c, key)
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
}
|
||||
|
||||
// BindUsername 重置账号
|
||||
func (h *UserHandler) BindUsername(c *gin.Context) {
|
||||
// BindMobile 绑定手机号
|
||||
func (h *UserHandler) BindMobile(c *gin.Context) {
|
||||
var data struct {
|
||||
Username string `json:"username"`
|
||||
Code string `json:"code"`
|
||||
Mobile string `json:"mobile"`
|
||||
Code string `json:"code"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
@@ -541,7 +649,7 @@ func (h *UserHandler) BindUsername(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 检查验证码
|
||||
key := CodeStorePrefix + data.Username
|
||||
key := CodeStorePrefix + data.Mobile
|
||||
code, err := h.redis.Get(c, key).Result()
|
||||
if err != nil || code != data.Code {
|
||||
resp.ERROR(c, "验证码错误")
|
||||
@@ -550,22 +658,56 @@ func (h *UserHandler) BindUsername(c *gin.Context) {
|
||||
|
||||
// 检查手机号是否被其他账号绑定
|
||||
var item model.User
|
||||
res := h.DB.Where("username = ?", data.Username).First(&item)
|
||||
res := h.DB.Where("mobile", data.Mobile).First(&item)
|
||||
if res.Error == nil {
|
||||
resp.ERROR(c, "该账号已经被其他账号绑定")
|
||||
resp.ERROR(c, "该手机号已经绑定了其他账号,请更换手机号")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.GetLoginUser(c)
|
||||
userId := h.GetLoginUserId(c)
|
||||
|
||||
err = h.DB.Model(&item).Where("id", userId).UpdateColumn("mobile", data.Mobile).Error
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
res = h.DB.Model(&user).UpdateColumn("username", data.Username)
|
||||
if res.Error != nil {
|
||||
logger.Error(res.Error)
|
||||
resp.ERROR(c, "更新数据库失败")
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
_ = h.redis.Del(c, key) // 删除短信验证码
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// BindEmail 绑定邮箱
|
||||
func (h *UserHandler) BindEmail(c *gin.Context) {
|
||||
var data struct {
|
||||
Email string `json:"email"`
|
||||
Code string `json:"code"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查验证码
|
||||
key := CodeStorePrefix + data.Email
|
||||
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("email", data.Email).First(&item)
|
||||
if res.Error == nil {
|
||||
resp.ERROR(c, "该邮箱地址已经绑定了其他账号,请更邮箱地址")
|
||||
return
|
||||
}
|
||||
|
||||
userId := h.GetLoginUserId(c)
|
||||
|
||||
err = h.DB.Model(&item).Where("id", userId).UpdateColumn("email", data.Email).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
227
api/handler/video_handler.go
Normal file
227
api/handler/video_handler.go
Normal file
@@ -0,0 +1,227 @@
|
||||
package handler
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/service/oss"
|
||||
"geekai/service/video"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type VideoHandler struct {
|
||||
BaseHandler
|
||||
videoService *video.Service
|
||||
uploader *oss.UploaderManager
|
||||
userService *service.UserService
|
||||
}
|
||||
|
||||
func NewVideoHandler(app *core.AppServer, db *gorm.DB, service *video.Service, uploader *oss.UploaderManager, userService *service.UserService) *VideoHandler {
|
||||
return &VideoHandler{
|
||||
BaseHandler: BaseHandler{
|
||||
App: app,
|
||||
DB: db,
|
||||
},
|
||||
videoService: service,
|
||||
uploader: uploader,
|
||||
userService: userService,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *VideoHandler) LumaCreate(c *gin.Context) {
|
||||
|
||||
var data struct {
|
||||
ClientId string `json:"client_id"`
|
||||
Prompt string `json:"prompt"`
|
||||
FirstFrameImg string `json:"first_frame_img,omitempty"`
|
||||
EndFrameImg string `json:"end_frame_img,omitempty"`
|
||||
ExpandPrompt bool `json:"expand_prompt,omitempty"`
|
||||
Loop bool `json:"loop,omitempty"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
if user.Power < h.App.SysConfig.LumaPower {
|
||||
resp.ERROR(c, "您的算力不足,请充值后再试!")
|
||||
return
|
||||
}
|
||||
|
||||
if data.Prompt == "" {
|
||||
resp.ERROR(c, "prompt is needed")
|
||||
return
|
||||
}
|
||||
|
||||
userId := int(h.GetLoginUserId(c))
|
||||
params := types.VideoParams{
|
||||
PromptOptimize: data.ExpandPrompt,
|
||||
Loop: data.Loop,
|
||||
StartImgURL: data.FirstFrameImg,
|
||||
EndImgURL: data.EndFrameImg,
|
||||
}
|
||||
// 插入数据库
|
||||
job := model.VideoJob{
|
||||
UserId: userId,
|
||||
Type: types.VideoLuma,
|
||||
Prompt: data.Prompt,
|
||||
Power: h.App.SysConfig.LumaPower,
|
||||
Params: utils.JsonEncode(params),
|
||||
}
|
||||
tx := h.DB.Create(&job)
|
||||
if tx.Error != nil {
|
||||
resp.ERROR(c, tx.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 创建任务
|
||||
h.videoService.PushTask(types.VideoTask{
|
||||
ClientId: data.ClientId,
|
||||
Id: job.Id,
|
||||
UserId: userId,
|
||||
Type: types.VideoLuma,
|
||||
Prompt: data.Prompt,
|
||||
Params: params,
|
||||
})
|
||||
|
||||
// update user's power
|
||||
err = h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: "luma",
|
||||
Remark: fmt.Sprintf("Luma 文生视频,任务ID:%d", job.Id),
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *VideoHandler) List(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
t := c.Query("type")
|
||||
page := h.GetInt(c, "page", 1)
|
||||
pageSize := h.GetInt(c, "page_size", 20)
|
||||
all := h.GetBool(c, "all")
|
||||
session := h.DB.Session(&gorm.Session{}).Where("user_id", userId)
|
||||
if t != "" {
|
||||
session = session.Where("type", t)
|
||||
}
|
||||
if all {
|
||||
session = session.Where("publish", 0).Where("progress", 100)
|
||||
} else {
|
||||
session = session.Where("user_id", h.GetLoginUserId(c))
|
||||
}
|
||||
// 统计总数
|
||||
var total int64
|
||||
session.Model(&model.VideoJob{}).Count(&total)
|
||||
|
||||
if page > 0 && pageSize > 0 {
|
||||
offset := (page - 1) * pageSize
|
||||
session = session.Offset(offset).Limit(pageSize)
|
||||
}
|
||||
var list []model.VideoJob
|
||||
err := session.Order("id desc").Find(&list).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为 VO
|
||||
items := make([]vo.VideoJob, 0)
|
||||
for _, v := range list {
|
||||
var item vo.VideoJob
|
||||
err = utils.CopyObject(v, &item)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
item.CreatedAt = v.CreatedAt.Unix()
|
||||
if item.VideoURL == "" {
|
||||
item.VideoURL = v.WaterURL
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, vo.NewPage(total, page, pageSize, items))
|
||||
}
|
||||
|
||||
func (h *VideoHandler) Remove(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
userId := h.GetLoginUserId(c)
|
||||
var job model.VideoJob
|
||||
err := h.DB.Where("id = ?", id).Where("user_id", userId).First(&job).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
// 只有失败或者超时的任务才能删除
|
||||
if !(job.Progress == service.FailTaskProgress || time.Now().After(job.CreatedAt.Add(time.Minute*30))) {
|
||||
resp.ERROR(c, "只有失败和超时(30分钟)的任务才能删除!")
|
||||
return
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
tx := h.DB.Begin()
|
||||
if err := tx.Delete(&job).Error; err != nil {
|
||||
tx.Rollback()
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 恢复算力
|
||||
err = h.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{
|
||||
Type: types.PowerRefund,
|
||||
Model: "luma",
|
||||
Remark: fmt.Sprintf("Luma 任务失败,退回算力。任务ID:%s,Err:%s", job.TaskId, job.ErrMsg),
|
||||
})
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
// 删除文件
|
||||
_ = h.uploader.GetUploadHandler().Delete(job.CoverURL)
|
||||
_ = h.uploader.GetUploadHandler().Delete(job.VideoURL)
|
||||
}
|
||||
|
||||
func (h *VideoHandler) Publish(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
userId := h.GetLoginUserId(c)
|
||||
publish := h.GetBool(c, "publish")
|
||||
var job model.VideoJob
|
||||
err := h.DB.Where("id = ?", id).Where("user_id", userId).First(&job).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = h.DB.Model(&job).UpdateColumn("publish", publish).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
150
api/handler/ws_handler.go
Normal file
150
api/handler/ws_handler.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package handler
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"context"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"gorm.io/gorm"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Websocket 连接处理 handler
|
||||
|
||||
type WebsocketHandler struct {
|
||||
BaseHandler
|
||||
wsService *service.WebsocketService
|
||||
chatHandler *ChatHandler
|
||||
}
|
||||
|
||||
func NewWebsocketHandler(app *core.AppServer, s *service.WebsocketService, db *gorm.DB, chatHandler *ChatHandler) *WebsocketHandler {
|
||||
return &WebsocketHandler{
|
||||
BaseHandler: BaseHandler{App: app, DB: db},
|
||||
chatHandler: chatHandler,
|
||||
wsService: s,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *WebsocketHandler) Client(c *gin.Context) {
|
||||
clientProtocols := c.GetHeader("Sec-WebSocket-Protocol")
|
||||
ws, err := (&websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
Subprotocols: strings.Split(clientProtocols, ","),
|
||||
}).Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
clientId := c.Query("client_id")
|
||||
client := types.NewWsClient(ws, clientId)
|
||||
userId := h.GetLoginUserId(c)
|
||||
if userId == 0 {
|
||||
_ = client.Send([]byte("Invalid user_id"))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
var user model.User
|
||||
if err := h.DB.Where("id", userId).First(&user).Error; err != nil {
|
||||
_ = client.Send([]byte("Invalid user_id"))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
h.wsService.Clients.Put(clientId, client)
|
||||
logger.Infof("New websocket connected, IP: %s", c.RemoteIP())
|
||||
go func() {
|
||||
for {
|
||||
_, msg, err := client.Receive()
|
||||
if err != nil {
|
||||
logger.Debugf("close connection: %s", client.Conn.RemoteAddr())
|
||||
client.Close()
|
||||
h.wsService.Clients.Delete(clientId)
|
||||
break
|
||||
}
|
||||
|
||||
var message types.InputMessage
|
||||
err = utils.JsonDecode(string(msg), &message)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Debugf("Receive a message:%+v", message)
|
||||
if message.Type == types.MsgTypePing {
|
||||
utils.SendChannelMsg(client, types.ChPing, "pong")
|
||||
continue
|
||||
}
|
||||
|
||||
// 当前只处理聊天消息,其他消息全部丢弃
|
||||
var chatMessage types.ChatMessage
|
||||
err = utils.JsonDecode(utils.JsonEncode(message.Body), &chatMessage)
|
||||
if err != nil || message.Channel != types.ChChat {
|
||||
logger.Warnf("invalid message body:%+v", message.Body)
|
||||
continue
|
||||
}
|
||||
var chatRole model.ChatRole
|
||||
err = h.DB.First(&chatRole, chatMessage.RoleId).Error
|
||||
if err != nil || !chatRole.Enable {
|
||||
utils.SendAndFlush(client, "当前聊天角色不存在或者未启用,请更换角色之后再发起对话!!!")
|
||||
continue
|
||||
}
|
||||
// if the role bind a model_id, use role's bind model_id
|
||||
if chatRole.ModelId > 0 {
|
||||
chatMessage.RoleId = chatRole.ModelId
|
||||
}
|
||||
// get model info
|
||||
var chatModel model.ChatModel
|
||||
err = h.DB.Where("id", chatMessage.ModelId).First(&chatModel).Error
|
||||
if err != nil || chatModel.Enabled == false {
|
||||
utils.SendAndFlush(client, "当前AI模型暂未启用,请更换模型后再发起对话!!!")
|
||||
continue
|
||||
}
|
||||
|
||||
session := &types.ChatSession{
|
||||
ClientIP: c.ClientIP(),
|
||||
UserId: userId,
|
||||
}
|
||||
|
||||
// use old chat data override the chat model and role ID
|
||||
var chat model.ChatItem
|
||||
h.DB.Where("chat_id", chatMessage.ChatId).First(&chat)
|
||||
if chat.Id > 0 {
|
||||
chatModel.Id = chat.ModelId
|
||||
chatMessage.RoleId = int(chat.RoleId)
|
||||
}
|
||||
|
||||
session.ChatId = chatMessage.ChatId
|
||||
session.Tools = chatMessage.Tools
|
||||
session.Stream = chatMessage.Stream
|
||||
// 复制模型数据
|
||||
err = utils.CopyObject(chatModel, &session.Model)
|
||||
if err != nil {
|
||||
logger.Error(err, chatModel)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
h.chatHandler.ReqCancelFunc.Put(clientId, cancel)
|
||||
err = h.chatHandler.sendMessage(ctx, session, chatRole, chatMessage.Content, client)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
utils.SendAndFlush(client, err.Error())
|
||||
} else {
|
||||
utils.SendMsg(client, types.ReplyMessage{Channel: types.ChChat, Type: types.MsgTypeEnd})
|
||||
logger.Infof("回答完毕: %v", message.Body)
|
||||
}
|
||||
|
||||
}
|
||||
}()
|
||||
}
|
||||
203
api/main.go
203
api/main.go
@@ -14,7 +14,6 @@ import (
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/handler/admin"
|
||||
"geekai/handler/chatimpl"
|
||||
logger2 "geekai/logger"
|
||||
"geekai/service"
|
||||
"geekai/service/dalle"
|
||||
@@ -23,7 +22,8 @@ import (
|
||||
"geekai/service/payment"
|
||||
"geekai/service/sd"
|
||||
"geekai/service/sms"
|
||||
"geekai/service/wx"
|
||||
"geekai/service/suno"
|
||||
"geekai/service/video"
|
||||
"geekai/store"
|
||||
"io"
|
||||
"log"
|
||||
@@ -127,10 +127,10 @@ func main() {
|
||||
// 创建控制器
|
||||
fx.Provide(handler.NewChatRoleHandler),
|
||||
fx.Provide(handler.NewUserHandler),
|
||||
fx.Provide(chatimpl.NewChatHandler),
|
||||
fx.Provide(handler.NewUploadHandler),
|
||||
fx.Provide(handler.NewChatHandler),
|
||||
fx.Provide(handler.NewNetHandler),
|
||||
fx.Provide(handler.NewSmsHandler),
|
||||
fx.Provide(handler.NewRewardHandler),
|
||||
fx.Provide(handler.NewRedeemHandler),
|
||||
fx.Provide(handler.NewCaptchaHandler),
|
||||
fx.Provide(handler.NewMidJourneyHandler),
|
||||
fx.Provide(handler.NewChatModelHandler),
|
||||
@@ -145,8 +145,8 @@ func main() {
|
||||
fx.Provide(admin.NewAdminHandler),
|
||||
fx.Provide(admin.NewApiKeyHandler),
|
||||
fx.Provide(admin.NewUserHandler),
|
||||
fx.Provide(admin.NewChatRoleHandler),
|
||||
fx.Provide(admin.NewRewardHandler),
|
||||
fx.Provide(admin.NewChatAppHandler),
|
||||
fx.Provide(admin.NewRedeemHandler),
|
||||
fx.Provide(admin.NewDashboardHandler),
|
||||
fx.Provide(admin.NewChatModelHandler),
|
||||
fx.Provide(admin.NewProductHandler),
|
||||
@@ -160,13 +160,12 @@ func main() {
|
||||
return service.NewCaptchaService(config.ApiConfig)
|
||||
}),
|
||||
fx.Provide(oss.NewUploaderManager),
|
||||
fx.Provide(mj.NewService),
|
||||
fx.Provide(dalle.NewService),
|
||||
fx.Invoke(func(service *dalle.Service) {
|
||||
service.Run()
|
||||
service.CheckTaskNotify()
|
||||
service.DownloadImages()
|
||||
service.CheckTaskStatus()
|
||||
fx.Invoke(func(s *dalle.Service) {
|
||||
s.Run()
|
||||
s.CheckTaskNotify()
|
||||
s.DownloadImages()
|
||||
s.CheckTaskStatus()
|
||||
}),
|
||||
|
||||
// 邮件服务
|
||||
@@ -177,38 +176,39 @@ func main() {
|
||||
licenseService.SyncLicense()
|
||||
}),
|
||||
|
||||
// 微信机器人服务
|
||||
fx.Provide(wx.NewWeChatBot),
|
||||
fx.Invoke(func(config *types.AppConfig, bot *wx.Bot) {
|
||||
if config.WeChatBot {
|
||||
err := bot.Run()
|
||||
if err != nil {
|
||||
logger.Error("微信登录失败:", err)
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
// MidJourney service pool
|
||||
fx.Provide(mj.NewServicePool),
|
||||
fx.Invoke(func(pool *mj.ServicePool, config *types.AppConfig) {
|
||||
pool.InitServices(config.MjPlusConfigs, config.MjProxyConfigs)
|
||||
if pool.HasAvailableService() {
|
||||
pool.DownloadImages()
|
||||
pool.CheckTaskNotify()
|
||||
pool.SyncTaskProgress()
|
||||
}
|
||||
fx.Provide(mj.NewService),
|
||||
fx.Provide(mj.NewClient),
|
||||
fx.Invoke(func(s *mj.Service) {
|
||||
s.Run()
|
||||
s.SyncTaskProgress()
|
||||
s.CheckTaskNotify()
|
||||
s.DownloadImages()
|
||||
}),
|
||||
|
||||
// Stable Diffusion 机器人
|
||||
fx.Provide(sd.NewServicePool),
|
||||
fx.Invoke(func(pool *sd.ServicePool, config *types.AppConfig) {
|
||||
pool.InitServices(config.SdConfigs)
|
||||
if pool.HasAvailableService() {
|
||||
pool.CheckTaskNotify()
|
||||
pool.CheckTaskStatus()
|
||||
}
|
||||
fx.Provide(sd.NewService),
|
||||
fx.Invoke(func(s *sd.Service, config *types.AppConfig) {
|
||||
s.Run()
|
||||
s.CheckTaskStatus()
|
||||
s.CheckTaskNotify()
|
||||
}),
|
||||
|
||||
fx.Provide(suno.NewService),
|
||||
fx.Invoke(func(s *suno.Service) {
|
||||
s.Run()
|
||||
s.SyncTaskProgress()
|
||||
s.CheckTaskNotify()
|
||||
s.DownloadFiles()
|
||||
}),
|
||||
fx.Provide(video.NewService),
|
||||
fx.Invoke(func(s *video.Service) {
|
||||
s.Run()
|
||||
s.SyncTaskProgress()
|
||||
s.CheckTaskNotify()
|
||||
s.DownloadFiles()
|
||||
}),
|
||||
fx.Provide(service.NewUserService),
|
||||
fx.Provide(payment.NewAlipayService),
|
||||
fx.Provide(payment.NewHuPiPay),
|
||||
fx.Provide(payment.NewJPayService),
|
||||
@@ -225,8 +225,9 @@ func main() {
|
||||
|
||||
// 注册路由
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.ChatRoleHandler) {
|
||||
group := s.Engine.Group("/api/role/")
|
||||
group := s.Engine.Group("/api/app/")
|
||||
group.GET("list", h.List)
|
||||
group.GET("list/user", h.ListByUser)
|
||||
group.POST("update", h.UpdateRole)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.UserHandler) {
|
||||
@@ -238,14 +239,14 @@ func main() {
|
||||
group.GET("profile", h.Profile)
|
||||
group.POST("profile/update", h.ProfileUpdate)
|
||||
group.POST("password", h.UpdatePass)
|
||||
group.POST("bind/username", h.BindUsername)
|
||||
group.POST("bind/mobile", h.BindMobile)
|
||||
group.POST("bind/email", h.BindEmail)
|
||||
group.POST("resetPass", h.ResetPass)
|
||||
group.GET("clogin", h.CLogin)
|
||||
group.GET("clogin/callback", h.CLoginCallback)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *chatimpl.ChatHandler) {
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.ChatHandler) {
|
||||
group := s.Engine.Group("/api/chat/")
|
||||
group.Any("new", h.ChatHandle)
|
||||
group.GET("list", h.List)
|
||||
group.GET("detail", h.Detail)
|
||||
group.POST("update", h.Update)
|
||||
@@ -255,10 +256,11 @@ func main() {
|
||||
group.POST("tokens", h.Tokens)
|
||||
group.GET("stop", h.StopGenerate)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.UploadHandler) {
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.NetHandler) {
|
||||
s.Engine.POST("/api/upload", h.Upload)
|
||||
s.Engine.POST("/api/upload/list", h.List)
|
||||
s.Engine.GET("/api/upload/remove", h.Remove)
|
||||
s.Engine.GET("/api/download", h.Download)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.SmsHandler) {
|
||||
group := s.Engine.Group("/api/sms/")
|
||||
@@ -271,13 +273,12 @@ func main() {
|
||||
group.GET("slide/get", h.SlideGet)
|
||||
group.POST("slide/check", h.SlideCheck)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.RewardHandler) {
|
||||
group := s.Engine.Group("/api/reward/")
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.RedeemHandler) {
|
||||
group := s.Engine.Group("/api/redeem/")
|
||||
group.POST("verify", h.Verify)
|
||||
}),
|
||||
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)
|
||||
@@ -288,7 +289,6 @@ func main() {
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.SdJobHandler) {
|
||||
group := s.Engine.Group("/api/sd")
|
||||
group.Any("client", h.Client)
|
||||
group.POST("image", h.Image)
|
||||
group.GET("jobs", h.JobList)
|
||||
group.GET("imgWall", h.ImgWall)
|
||||
@@ -303,13 +303,12 @@ func main() {
|
||||
|
||||
// 管理后台控制器
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ConfigHandler) {
|
||||
group := s.Engine.Group("/api/admin/")
|
||||
group.POST("config/update", h.Update)
|
||||
group.GET("config/get", h.Get)
|
||||
group := s.Engine.Group("/api/admin/config")
|
||||
group.POST("update", h.Update)
|
||||
group.GET("get", h.Get)
|
||||
group.POST("active", h.Active)
|
||||
group.GET("config/get/license", h.GetLicense)
|
||||
group.GET("config/get/app", h.GetAppConfig)
|
||||
group.POST("config/update/draw", h.SaveDrawingConfig)
|
||||
group.GET("fixData", h.FixData)
|
||||
group.GET("license", h.GetLicense)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ManagerHandler) {
|
||||
group := s.Engine.Group("/api/admin/")
|
||||
@@ -337,7 +336,7 @@ func main() {
|
||||
group.GET("loginLog", h.LoginLog)
|
||||
group.POST("resetPass", h.ResetPass)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ChatRoleHandler) {
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ChatAppHandler) {
|
||||
group := s.Engine.Group("/api/admin/role/")
|
||||
group.GET("list", h.List)
|
||||
group.POST("save", h.Save)
|
||||
@@ -345,10 +344,12 @@ func main() {
|
||||
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/")
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.RedeemHandler) {
|
||||
group := s.Engine.Group("/api/admin/redeem/")
|
||||
group.GET("list", h.List)
|
||||
group.POST("remove", h.Remove)
|
||||
group.POST("create", h.Create)
|
||||
group.POST("set", h.Set)
|
||||
group.GET("remove", h.Remove)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.DashboardHandler) {
|
||||
group := s.Engine.Group("/api/admin/dashboard/")
|
||||
@@ -368,14 +369,12 @@ func main() {
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.PaymentHandler) {
|
||||
group := s.Engine.Group("/api/payment/")
|
||||
group.GET("doPay", h.DoPay)
|
||||
group.POST("doPay", h.Pay)
|
||||
group.GET("payWays", h.GetPayWays)
|
||||
group.POST("qrcode", h.PayQrcode)
|
||||
group.POST("mobile", h.Mobile)
|
||||
group.POST("alipay/notify", h.AlipayNotify)
|
||||
group.POST("hupipay/notify", h.HuPiPayNotify)
|
||||
group.POST("payjs/notify", h.PayJsNotify)
|
||||
group.POST("wechat/notify", h.WechatPayNotify)
|
||||
group.POST("notify/alipay", h.AlipayNotify)
|
||||
group.GET("notify/geek", h.GeekPayNotify)
|
||||
group.POST("notify/wechat", h.WechatPayNotify)
|
||||
group.POST("notify/hupi", h.HuPiPayNotify)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ProductHandler) {
|
||||
group := s.Engine.Group("/api/admin/product/")
|
||||
@@ -389,6 +388,7 @@ func main() {
|
||||
group := s.Engine.Group("/api/admin/order/")
|
||||
group.POST("list", h.List)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("clear", h.Clear)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.OrderHandler) {
|
||||
group := s.Engine.Group("/api/order/")
|
||||
@@ -404,7 +404,7 @@ func main() {
|
||||
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("list", h.List)
|
||||
group.GET("hits", h.Hits)
|
||||
}),
|
||||
|
||||
@@ -429,6 +429,7 @@ func main() {
|
||||
group.POST("weibo", h.WeiBo)
|
||||
group.POST("zaobao", h.ZaoBao)
|
||||
group.POST("dalle3", h.Dall3)
|
||||
group.GET("list", h.List)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ChatHandler) {
|
||||
group := s.Engine.Group("/api/admin/chat/")
|
||||
@@ -462,24 +463,67 @@ func main() {
|
||||
}),
|
||||
fx.Provide(handler.NewMarkMapHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.MarkMapHandler) {
|
||||
group := s.Engine.Group("/api/markMap/")
|
||||
group.Any("client", h.Client)
|
||||
s.Engine.POST("/api/markMap/gen", h.Generate)
|
||||
}),
|
||||
fx.Provide(handler.NewDallJobHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.DallJobHandler) {
|
||||
group := s.Engine.Group("/api/dall")
|
||||
group.Any("client", h.Client)
|
||||
group.POST("image", h.Image)
|
||||
group.GET("jobs", h.JobList)
|
||||
group.GET("imgWall", h.ImgWall)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("publish", h.Publish)
|
||||
}),
|
||||
fx.Provide(handler.NewSunoHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.SunoHandler) {
|
||||
group := s.Engine.Group("/api/suno")
|
||||
group.POST("create", h.Create)
|
||||
group.GET("list", h.List)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("publish", h.Publish)
|
||||
group.POST("update", h.Update)
|
||||
group.GET("detail", h.Detail)
|
||||
group.GET("play", h.Play)
|
||||
group.POST("lyric", h.Lyric)
|
||||
}),
|
||||
fx.Provide(handler.NewVideoHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.VideoHandler) {
|
||||
group := s.Engine.Group("/api/video")
|
||||
group.POST("luma/create", h.LumaCreate)
|
||||
group.GET("list", h.List)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("publish", h.Publish)
|
||||
}),
|
||||
fx.Provide(admin.NewChatAppTypeHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ChatAppTypeHandler) {
|
||||
group := s.Engine.Group("/api/admin/app/type")
|
||||
group.POST("save", h.Save)
|
||||
group.GET("list", h.List)
|
||||
group.GET("remove", h.Remove)
|
||||
group.POST("enable", h.Enable)
|
||||
group.POST("sort", h.Sort)
|
||||
}),
|
||||
fx.Provide(handler.NewChatAppTypeHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.ChatAppTypeHandler) {
|
||||
group := s.Engine.Group("/api/app/type")
|
||||
group.GET("list", h.List)
|
||||
}),
|
||||
fx.Provide(handler.NewTestHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.TestHandler) {
|
||||
group := s.Engine.Group("/api/test")
|
||||
group.Any("sse", h.PostTest, h.SseTest)
|
||||
}),
|
||||
fx.Provide(service.NewWebsocketService),
|
||||
fx.Provide(handler.NewWebsocketHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.WebsocketHandler) {
|
||||
s.Engine.Any("/api/ws", h.Client)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, db *gorm.DB) {
|
||||
go func() {
|
||||
err := s.Run(db)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
logger.Error(err)
|
||||
os.Exit(0)
|
||||
}
|
||||
}()
|
||||
}),
|
||||
@@ -495,6 +539,25 @@ func main() {
|
||||
},
|
||||
})
|
||||
}),
|
||||
fx.Provide(admin.NewImageHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ImageHandler) {
|
||||
group := s.Engine.Group("/api/admin/image")
|
||||
group.POST("/list/mj", h.MjList)
|
||||
group.POST("/list/sd", h.SdList)
|
||||
group.POST("/list/dall", h.DallList)
|
||||
group.GET("/remove", h.Remove)
|
||||
}),
|
||||
fx.Provide(admin.NewMediaHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.MediaHandler) {
|
||||
group := s.Engine.Group("/api/admin/media")
|
||||
group.POST("/list/suno", h.SunoList)
|
||||
group.POST("/list/luma", h.LumaList)
|
||||
group.GET("/remove", h.Remove)
|
||||
}),
|
||||
fx.Provide(handler.NewRealtimeHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.RealtimeHandler) {
|
||||
s.Engine.Any("/api/realtime", h.Connection)
|
||||
}),
|
||||
)
|
||||
// 启动应用程序
|
||||
go func() {
|
||||
|
||||
BIN
api/res/img/geek-pay.jpg
Normal file
BIN
api/res/img/geek-pay.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
BIN
api/res/img/qq-pay.jpg
Normal file
BIN
api/res/img/qq-pay.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -14,7 +14,6 @@ import (
|
||||
logger2 "geekai/logger"
|
||||
"geekai/service"
|
||||
"geekai/service/oss"
|
||||
"geekai/service/sd"
|
||||
"geekai/store"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
@@ -35,17 +34,21 @@ type Service struct {
|
||||
uploadManager *oss.UploaderManager
|
||||
taskQueue *store.RedisQueue
|
||||
notifyQueue *store.RedisQueue
|
||||
Clients *types.LMap[uint, *types.WsClient] // UserId => Client
|
||||
userService *service.UserService
|
||||
wsService *service.WebsocketService
|
||||
clientIds map[uint]string
|
||||
}
|
||||
|
||||
func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Client) *Service {
|
||||
func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Client, userService *service.UserService, wsService *service.WebsocketService) *Service {
|
||||
return &Service{
|
||||
httpClient: req.C().SetTimeout(time.Minute * 3),
|
||||
db: db,
|
||||
taskQueue: store.NewRedisQueue("DallE_Task_Queue", redisCli),
|
||||
notifyQueue: store.NewRedisQueue("DallE_Notify_Queue", redisCli),
|
||||
Clients: types.NewLMap[uint, *types.WsClient](),
|
||||
wsService: wsService,
|
||||
uploadManager: manager,
|
||||
userService: userService,
|
||||
clientIds: map[uint]string{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,14 +69,15 @@ func (s *Service) Run() {
|
||||
continue
|
||||
}
|
||||
logger.Infof("handle a new DALL-E task: %+v", task)
|
||||
s.clientIds[task.JobId] = task.ClientId
|
||||
_, err = s.Image(task, false)
|
||||
if err != nil {
|
||||
logger.Errorf("error with image task: %v", err)
|
||||
s.db.Model(&model.DallJob{Id: task.JobId}).UpdateColumns(map[string]interface{}{
|
||||
"progress": -1,
|
||||
"progress": service.FailTaskProgress,
|
||||
"err_msg": err.Error(),
|
||||
})
|
||||
s.notifyQueue.RPush(sd.NotifyMessage{UserId: int(task.UserId), JobId: int(task.JobId), Message: sd.Failed})
|
||||
s.notifyQueue.RPush(service.NotifyMessage{ClientId: task.ClientId, UserId: int(task.UserId), JobId: int(task.JobId), Message: service.TaskStatusFailed})
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -110,12 +114,11 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
|
||||
prompt := task.Prompt
|
||||
// translate prompt
|
||||
if utils.HasChinese(prompt) {
|
||||
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, prompt))
|
||||
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, prompt), "gpt-4o-mini", 0)
|
||||
if err == nil {
|
||||
prompt = content
|
||||
logger.Debugf("重写后提示词:%s", prompt)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var user model.User
|
||||
@@ -124,32 +127,23 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
|
||||
return "", errors.New("insufficient of power")
|
||||
}
|
||||
|
||||
// 更新用户算力
|
||||
tx := s.db.Model(&model.User{}).Where("id", user.Id).UpdateColumn("power", gorm.Expr("power - ?", task.Power))
|
||||
// 记录算力变化日志
|
||||
if tx.Error == nil && tx.RowsAffected > 0 {
|
||||
var u model.User
|
||||
s.db.Where("id", user.Id).First(&u)
|
||||
s.db.Create(&model.PowerLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
Type: types.PowerConsume,
|
||||
Amount: task.Power,
|
||||
Balance: u.Power,
|
||||
Mark: types.PowerSub,
|
||||
Model: "dall-e-3",
|
||||
Remark: fmt.Sprintf("绘画提示词:%s", utils.CutWords(task.Prompt, 10)),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
// 扣减算力
|
||||
err := s.userService.DecreasePower(int(user.Id), task.Power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: "dall-e-3",
|
||||
Remark: fmt.Sprintf("绘画提示词:%s", utils.CutWords(task.Prompt, 10)),
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with decrease power: %v", err)
|
||||
}
|
||||
|
||||
// get image generation API KEY
|
||||
var apiKey model.ApiKey
|
||||
tx = s.db.Where("type", "img").
|
||||
err = s.db.Where("type", "dalle").
|
||||
Where("enabled", true).
|
||||
Order("last_used_at ASC").First(&apiKey)
|
||||
if tx.Error != nil {
|
||||
return "", fmt.Errorf("no available IMG api key: %v", tx.Error)
|
||||
Order("last_used_at ASC").First(&apiKey).Error
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("no available DALL-E api key: %v", err)
|
||||
}
|
||||
|
||||
var res imgRes
|
||||
@@ -157,6 +151,7 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
|
||||
if len(apiKey.ProxyURL) > 5 {
|
||||
s.httpClient.SetProxyURL(apiKey.ProxyURL).R()
|
||||
}
|
||||
apiURL := fmt.Sprintf("%s/v1/images/generations", apiKey.ApiURL)
|
||||
reqBody := imgReq{
|
||||
Model: "dall-e-3",
|
||||
Prompt: prompt,
|
||||
@@ -165,14 +160,13 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
|
||||
Style: task.Style,
|
||||
Quality: task.Quality,
|
||||
}
|
||||
logger.Infof("Sending %s request, ApiURL:%s, API KEY:%s, BODY: %+v", apiKey.Platform, apiKey.ApiURL, apiKey.Value, reqBody)
|
||||
request := s.httpClient.R().SetHeader("Content-Type", "application/json")
|
||||
if apiKey.Platform == types.Azure.Value {
|
||||
request = request.SetHeader("api-key", apiKey.Value)
|
||||
} else {
|
||||
request = request.SetHeader("Authorization", "Bearer "+apiKey.Value)
|
||||
}
|
||||
r, err := request.SetBody(reqBody).SetErrorResult(&errRes).SetSuccessResult(&res).Post(apiKey.ApiURL)
|
||||
logger.Infof("Channel:%s, API KEY:%s, BODY: %+v", apiURL, apiKey.Value, reqBody)
|
||||
r, err := s.httpClient.R().SetHeader("Body-Type", "application/json").
|
||||
SetHeader("Authorization", "Bearer "+apiKey.Value).
|
||||
SetBody(reqBody).
|
||||
SetErrorResult(&errRes).
|
||||
SetSuccessResult(&res).
|
||||
Post(apiURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with send request: %v", err)
|
||||
}
|
||||
@@ -183,16 +177,16 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
|
||||
// update the api key last use time
|
||||
s.db.Model(&apiKey).UpdateColumn("last_used_at", time.Now().Unix())
|
||||
// update task progress
|
||||
tx = s.db.Model(&model.DallJob{Id: task.JobId}).UpdateColumns(map[string]interface{}{
|
||||
err = s.db.Model(&model.DallJob{Id: task.JobId}).UpdateColumns(map[string]interface{}{
|
||||
"progress": 100,
|
||||
"org_url": res.Data[0].Url,
|
||||
"prompt": prompt,
|
||||
})
|
||||
if tx.Error != nil {
|
||||
return "", fmt.Errorf("err with update database: %v", tx.Error)
|
||||
}).Error
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("err with update database: %v", err)
|
||||
}
|
||||
|
||||
s.notifyQueue.RPush(sd.NotifyMessage{UserId: int(task.UserId), JobId: int(task.JobId), Message: sd.Finished})
|
||||
s.notifyQueue.RPush(service.NotifyMessage{ClientId: task.ClientId, UserId: int(task.UserId), JobId: int(task.JobId), Message: service.TaskStatusFailed})
|
||||
var content string
|
||||
if sync {
|
||||
imgURL, err := s.downloadImage(task.JobId, int(task.UserId), res.Data[0].Url)
|
||||
@@ -209,19 +203,42 @@ func (s *Service) CheckTaskNotify() {
|
||||
go func() {
|
||||
logger.Info("Running DALL-E task notify checking ...")
|
||||
for {
|
||||
var message sd.NotifyMessage
|
||||
var message service.NotifyMessage
|
||||
err := s.notifyQueue.LPop(&message)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
client := s.Clients.Get(uint(message.UserId))
|
||||
|
||||
logger.Debugf("notify message: %+v", message)
|
||||
client := s.wsService.Clients.Get(message.ClientId)
|
||||
if client == nil {
|
||||
continue
|
||||
}
|
||||
err = client.Send([]byte(message.Message))
|
||||
if err != nil {
|
||||
utils.SendChannelMsg(client, types.ChDall, message.Message)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Service) CheckTaskStatus() {
|
||||
go func() {
|
||||
logger.Info("Running DALL-E task status checking ...")
|
||||
for {
|
||||
var jobs []model.DallJob
|
||||
res := s.db.Where("progress < ?", 100).Find(&jobs)
|
||||
if res.Error != nil {
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, job := range jobs {
|
||||
// 超时的任务标记为失败
|
||||
if time.Now().Sub(job.CreatedAt) > time.Minute*10 {
|
||||
job.Progress = service.FailTaskProgress
|
||||
job.ErrMsg = "任务超时"
|
||||
s.db.Updates(&job)
|
||||
}
|
||||
}
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -259,7 +276,7 @@ func (s *Service) DownloadImages() {
|
||||
|
||||
func (s *Service) downloadImage(jobId uint, userId int, orgURL string) (string, error) {
|
||||
// sava image
|
||||
imgURL, err := s.uploadManager.GetUploadHandler().PutImg(orgURL, false)
|
||||
imgURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(orgURL, false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -269,47 +286,6 @@ func (s *Service) downloadImage(jobId uint, userId int, orgURL string) (string,
|
||||
if res.Error != nil {
|
||||
return "", err
|
||||
}
|
||||
s.notifyQueue.RPush(sd.NotifyMessage{UserId: userId, JobId: int(jobId), Message: sd.Finished})
|
||||
s.notifyQueue.RPush(service.NotifyMessage{ClientId: s.clientIds[jobId], UserId: userId, JobId: int(jobId), Message: service.TaskStatusFinished})
|
||||
return imgURL, nil
|
||||
}
|
||||
|
||||
// CheckTaskStatus 检查任务状态,自动删除过期或者失败的任务
|
||||
func (s *Service) CheckTaskStatus() {
|
||||
go func() {
|
||||
logger.Info("Running Stable-Diffusion task status checking ...")
|
||||
for {
|
||||
var jobs []model.DallJob
|
||||
res := s.db.Where("progress < ?", 100).Find(&jobs)
|
||||
if res.Error != nil {
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, job := range jobs {
|
||||
// 5 分钟还没完成的任务直接删除
|
||||
if time.Now().Sub(job.CreatedAt) > time.Minute*5 || job.Progress == -1 {
|
||||
s.db.Delete(&job)
|
||||
var user model.User
|
||||
s.db.Where("id = ?", job.UserId).First(&user)
|
||||
// 退回绘图次数
|
||||
res = s.db.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power + ?", job.Power))
|
||||
if res.Error == nil && res.RowsAffected > 0 {
|
||||
s.db.Create(&model.PowerLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
Type: types.PowerConsume,
|
||||
Amount: job.Power,
|
||||
Balance: user.Power + job.Power,
|
||||
Mark: types.PowerAdd,
|
||||
Model: "dall-e-3",
|
||||
Remark: fmt.Sprintf("任务失败,退回算力。任务ID:%d", job.Id),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -7,15 +7,28 @@ package mj
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import "geekai/core/types"
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
logger2 "geekai/logger"
|
||||
"geekai/service"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"github.com/imroc/req/v3"
|
||||
"gorm.io/gorm"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
type Client interface {
|
||||
Imagine(task types.MjTask) (ImageRes, error)
|
||||
Blend(task types.MjTask) (ImageRes, error)
|
||||
SwapFace(task types.MjTask) (ImageRes, error)
|
||||
Upscale(task types.MjTask) (ImageRes, error)
|
||||
Variation(task types.MjTask) (ImageRes, error)
|
||||
QueryTask(taskId string) (QueryRes, error)
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Client MidJourney client
|
||||
type Client struct {
|
||||
client *req.Client
|
||||
licenseService *service.LicenseService
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
type ImageReq struct {
|
||||
@@ -33,13 +46,8 @@ type ImageRes struct {
|
||||
Description string `json:"description"`
|
||||
Properties struct {
|
||||
} `json:"properties"`
|
||||
Result string `json:"result"`
|
||||
}
|
||||
|
||||
type ErrRes struct {
|
||||
Error struct {
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
Result string `json:"result"`
|
||||
Channel string `json:"channel,omitempty"`
|
||||
}
|
||||
|
||||
type QueryRes struct {
|
||||
@@ -66,3 +74,177 @@ type QueryRes struct {
|
||||
Status string `json:"status"`
|
||||
SubmitTime int `json:"submitTime"`
|
||||
}
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
func NewClient(licenseService *service.LicenseService, db *gorm.DB) *Client {
|
||||
return &Client{
|
||||
client: req.C().SetTimeout(time.Minute).SetUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"),
|
||||
licenseService: licenseService,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Imagine(task types.MjTask) (ImageRes, error) {
|
||||
apiPath := fmt.Sprintf("mj-%s/mj/submit/imagine", task.Mode)
|
||||
prompt := fmt.Sprintf("%s %s", task.Prompt, task.Params)
|
||||
if task.NegPrompt != "" {
|
||||
prompt += fmt.Sprintf(" --no %s", task.NegPrompt)
|
||||
}
|
||||
body := ImageReq{
|
||||
BotType: "MID_JOURNEY",
|
||||
Prompt: prompt,
|
||||
Base64Array: make([]string, 0),
|
||||
}
|
||||
// 生成图片 Base64 编码
|
||||
if len(task.ImgArr) > 0 {
|
||||
imageData, err := utils.DownloadImage(task.ImgArr[0], "")
|
||||
if err != nil {
|
||||
logger.Error("error with download image: ", err)
|
||||
} else {
|
||||
body.Base64Array = append(body.Base64Array, "data:image/png;base64,"+base64.StdEncoding.EncodeToString(imageData))
|
||||
}
|
||||
|
||||
}
|
||||
return c.doRequest(body, apiPath, task.ChannelId)
|
||||
}
|
||||
|
||||
// Blend 融图
|
||||
func (c *Client) Blend(task types.MjTask) (ImageRes, error) {
|
||||
apiPath := fmt.Sprintf("mj-%s/mj/submit/blend", task.Mode)
|
||||
body := ImageReq{
|
||||
BotType: "MID_JOURNEY",
|
||||
Dimensions: "SQUARE",
|
||||
Base64Array: make([]string, 0),
|
||||
}
|
||||
// 生成图片 Base64 编码
|
||||
if len(task.ImgArr) > 0 {
|
||||
for _, imgURL := range task.ImgArr {
|
||||
imageData, err := utils.DownloadImage(imgURL, "")
|
||||
if err != nil {
|
||||
logger.Error("error with download image: ", err)
|
||||
} else {
|
||||
body.Base64Array = append(body.Base64Array, "data:image/png;base64,"+base64.StdEncoding.EncodeToString(imageData))
|
||||
}
|
||||
}
|
||||
}
|
||||
return c.doRequest(body, apiPath, task.ChannelId)
|
||||
}
|
||||
|
||||
// SwapFace 换脸
|
||||
func (c *Client) SwapFace(task types.MjTask) (ImageRes, error) {
|
||||
apiPath := fmt.Sprintf("mj-%s/mj/insight-face/swap", task.Mode)
|
||||
// 生成图片 Base64 编码
|
||||
if len(task.ImgArr) != 2 {
|
||||
return ImageRes{}, errors.New("参数错误,必须上传2张图片")
|
||||
}
|
||||
var sourceBase64 string
|
||||
var targetBase64 string
|
||||
imageData, err := utils.DownloadImage(task.ImgArr[0], "")
|
||||
if err != nil {
|
||||
logger.Error("error with download image: ", err)
|
||||
} else {
|
||||
sourceBase64 = "data:image/png;base64," + base64.StdEncoding.EncodeToString(imageData)
|
||||
}
|
||||
imageData, err = utils.DownloadImage(task.ImgArr[1], "")
|
||||
if err != nil {
|
||||
logger.Error("error with download image: ", err)
|
||||
} else {
|
||||
targetBase64 = "data:image/png;base64," + base64.StdEncoding.EncodeToString(imageData)
|
||||
}
|
||||
|
||||
body := gin.H{
|
||||
"sourceBase64": sourceBase64,
|
||||
"targetBase64": targetBase64,
|
||||
"accountFilter": gin.H{
|
||||
"instanceId": "",
|
||||
},
|
||||
"state": "",
|
||||
}
|
||||
return c.doRequest(body, apiPath, task.ChannelId)
|
||||
}
|
||||
|
||||
// Upscale 放大指定的图片
|
||||
func (c *Client) Upscale(task types.MjTask) (ImageRes, error) {
|
||||
body := map[string]string{
|
||||
"customId": fmt.Sprintf("MJ::JOB::upsample::%d::%s", task.Index, task.MessageHash),
|
||||
"taskId": task.MessageId,
|
||||
}
|
||||
apiPath := fmt.Sprintf("mj-%s/mj/submit/action", task.Mode)
|
||||
return c.doRequest(body, apiPath, task.ChannelId)
|
||||
}
|
||||
|
||||
// Variation 以指定的图片的视角进行变换再创作,注意需要在对应的频道中关闭 Remix 变换,否则 Variation 指令将不会生效
|
||||
func (c *Client) Variation(task types.MjTask) (ImageRes, error) {
|
||||
body := map[string]string{
|
||||
"customId": fmt.Sprintf("MJ::JOB::variation::%d::%s", task.Index, task.MessageHash),
|
||||
"taskId": task.MessageId,
|
||||
}
|
||||
apiPath := fmt.Sprintf("mj-%s/mj/submit/action", task.Mode)
|
||||
|
||||
return c.doRequest(body, apiPath, task.ChannelId)
|
||||
}
|
||||
|
||||
func (c *Client) doRequest(body interface{}, apiPath string, channel string) (ImageRes, error) {
|
||||
var res ImageRes
|
||||
session := c.db.Session(&gorm.Session{}).Where("type", "mj").Where("enabled", true)
|
||||
if channel != "" {
|
||||
session = session.Where("api_url", channel)
|
||||
}
|
||||
|
||||
var apiKey model.ApiKey
|
||||
err := session.Order("last_used_at ASC").First(&apiKey).Error
|
||||
if err != nil {
|
||||
return ImageRes{}, fmt.Errorf("no available MidJourney api key: %v", err)
|
||||
}
|
||||
|
||||
if err = c.licenseService.IsValidApiURL(apiKey.ApiURL); err != nil {
|
||||
return ImageRes{}, err
|
||||
}
|
||||
|
||||
apiURL := fmt.Sprintf("%s/%s", apiKey.ApiURL, apiPath)
|
||||
logger.Info("API URL: ", apiURL)
|
||||
r, err := req.C().R().
|
||||
SetHeader("Authorization", "Bearer "+apiKey.Value).
|
||||
SetBody(body).
|
||||
SetSuccessResult(&res).
|
||||
Post(apiURL)
|
||||
if err != nil {
|
||||
return ImageRes{}, fmt.Errorf("请求 API 出错:%v", err)
|
||||
}
|
||||
|
||||
if r.IsErrorState() {
|
||||
errMsg, _ := io.ReadAll(r.Body)
|
||||
return ImageRes{}, fmt.Errorf("API 返回错误:%s", string(errMsg))
|
||||
}
|
||||
|
||||
// update the api key last used time
|
||||
if err = c.db.Model(&apiKey).Update("last_used_at", time.Now().Unix()).Error; err != nil {
|
||||
logger.Error("update api key last used time error: ", err)
|
||||
}
|
||||
res.Channel = apiKey.ApiURL
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *Client) QueryTask(taskId string, channel string) (QueryRes, error) {
|
||||
var apiKey model.ApiKey
|
||||
err := c.db.Where("type", "mj").Where("enabled", true).Where("api_url", channel).First(&apiKey).Error
|
||||
if err != nil {
|
||||
return QueryRes{}, fmt.Errorf("no available MidJourney api key: %v", err)
|
||||
}
|
||||
apiURL := fmt.Sprintf("%s/mj/task/%s/fetch", apiKey.ApiURL, taskId)
|
||||
var res QueryRes
|
||||
r, err := c.client.R().SetHeader("Authorization", "Bearer "+apiKey.Value).
|
||||
SetSuccessResult(&res).
|
||||
Get(apiURL)
|
||||
|
||||
if err != nil {
|
||||
return QueryRes{}, err
|
||||
}
|
||||
|
||||
if r.IsErrorState() {
|
||||
return QueryRes{}, errors.New("error status:" + r.Status)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@@ -1,267 +0,0 @@
|
||||
package mj
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/utils"
|
||||
"github.com/imroc/req/v3"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// PlusClient MidJourney Plus ProxyClient
|
||||
type PlusClient struct {
|
||||
Config types.MjPlusConfig
|
||||
apiURL string
|
||||
client *req.Client
|
||||
licenseService *service.LicenseService
|
||||
}
|
||||
|
||||
func NewPlusClient(config types.MjPlusConfig, licenseService *service.LicenseService) *PlusClient {
|
||||
return &PlusClient{
|
||||
Config: config,
|
||||
apiURL: config.ApiURL,
|
||||
client: req.C().SetTimeout(time.Minute).SetUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"),
|
||||
licenseService: licenseService,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *PlusClient) preCheck() error {
|
||||
return c.licenseService.IsValidApiURL(c.Config.ApiURL)
|
||||
}
|
||||
|
||||
func (c *PlusClient) Imagine(task types.MjTask) (ImageRes, error) {
|
||||
if err := c.preCheck(); err != nil {
|
||||
return ImageRes{}, err
|
||||
}
|
||||
|
||||
apiURL := fmt.Sprintf("%s/mj-%s/mj/submit/imagine", c.apiURL, c.Config.Mode)
|
||||
prompt := fmt.Sprintf("%s %s", task.Prompt, task.Params)
|
||||
if task.NegPrompt != "" {
|
||||
prompt += fmt.Sprintf(" --no %s", task.NegPrompt)
|
||||
}
|
||||
body := ImageReq{
|
||||
BotType: "MID_JOURNEY",
|
||||
Prompt: prompt,
|
||||
Base64Array: make([]string, 0),
|
||||
}
|
||||
// 生成图片 Base64 编码
|
||||
if len(task.ImgArr) > 0 {
|
||||
imageData, err := utils.DownloadImage(task.ImgArr[0], "")
|
||||
if err != nil {
|
||||
logger.Error("error with download image: ", err)
|
||||
} else {
|
||||
body.Base64Array = append(body.Base64Array, "data:image/png;base64,"+base64.StdEncoding.EncodeToString(imageData))
|
||||
}
|
||||
|
||||
}
|
||||
logger.Info("API URL: ", apiURL)
|
||||
var res ImageRes
|
||||
var errRes ErrRes
|
||||
r, err := c.client.R().
|
||||
SetHeader("Authorization", "Bearer "+c.Config.ApiKey).
|
||||
SetBody(body).
|
||||
SetSuccessResult(&res).
|
||||
SetErrorResult(&errRes).
|
||||
Post(apiURL)
|
||||
if err != nil {
|
||||
return ImageRes{}, fmt.Errorf("请求 API %s 出错:%v", apiURL, err)
|
||||
}
|
||||
|
||||
if r.IsErrorState() {
|
||||
errStr, _ := io.ReadAll(r.Body)
|
||||
return ImageRes{}, fmt.Errorf("API 返回错误:%s,%v", errRes.Error.Message, string(errStr))
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Blend 融图
|
||||
func (c *PlusClient) Blend(task types.MjTask) (ImageRes, error) {
|
||||
if err := c.preCheck(); err != nil {
|
||||
return ImageRes{}, err
|
||||
}
|
||||
|
||||
apiURL := fmt.Sprintf("%s/mj-%s/mj/submit/blend", c.apiURL, c.Config.Mode)
|
||||
logger.Info("API URL: ", apiURL)
|
||||
body := ImageReq{
|
||||
BotType: "MID_JOURNEY",
|
||||
Dimensions: "SQUARE",
|
||||
Base64Array: make([]string, 0),
|
||||
}
|
||||
// 生成图片 Base64 编码
|
||||
if len(task.ImgArr) > 0 {
|
||||
for _, imgURL := range task.ImgArr {
|
||||
imageData, err := utils.DownloadImage(imgURL, "")
|
||||
if err != nil {
|
||||
logger.Error("error with download image: ", err)
|
||||
} else {
|
||||
body.Base64Array = append(body.Base64Array, "data:image/png;base64,"+base64.StdEncoding.EncodeToString(imageData))
|
||||
}
|
||||
}
|
||||
}
|
||||
var res ImageRes
|
||||
var errRes ErrRes
|
||||
r, err := c.client.R().
|
||||
SetHeader("Authorization", "Bearer "+c.Config.ApiKey).
|
||||
SetBody(body).
|
||||
SetSuccessResult(&res).
|
||||
SetErrorResult(&errRes).
|
||||
Post(apiURL)
|
||||
if err != nil {
|
||||
return ImageRes{}, fmt.Errorf("请求 API %s 出错:%v", apiURL, err)
|
||||
}
|
||||
|
||||
if r.IsErrorState() {
|
||||
return ImageRes{}, fmt.Errorf("API 返回错误:%s", errRes.Error.Message)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// SwapFace 换脸
|
||||
func (c *PlusClient) SwapFace(task types.MjTask) (ImageRes, error) {
|
||||
if err := c.preCheck(); err != nil {
|
||||
return ImageRes{}, err
|
||||
}
|
||||
|
||||
apiURL := fmt.Sprintf("%s/mj-%s/mj/insight-face/swap", c.apiURL, c.Config.Mode)
|
||||
// 生成图片 Base64 编码
|
||||
if len(task.ImgArr) != 2 {
|
||||
return ImageRes{}, errors.New("参数错误,必须上传2张图片")
|
||||
}
|
||||
var sourceBase64 string
|
||||
var targetBase64 string
|
||||
imageData, err := utils.DownloadImage(task.ImgArr[0], "")
|
||||
if err != nil {
|
||||
logger.Error("error with download image: ", err)
|
||||
} else {
|
||||
sourceBase64 = "data:image/png;base64," + base64.StdEncoding.EncodeToString(imageData)
|
||||
}
|
||||
imageData, err = utils.DownloadImage(task.ImgArr[1], "")
|
||||
if err != nil {
|
||||
logger.Error("error with download image: ", err)
|
||||
} else {
|
||||
targetBase64 = "data:image/png;base64," + base64.StdEncoding.EncodeToString(imageData)
|
||||
}
|
||||
|
||||
body := gin.H{
|
||||
"sourceBase64": sourceBase64,
|
||||
"targetBase64": targetBase64,
|
||||
"accountFilter": gin.H{
|
||||
"instanceId": "",
|
||||
},
|
||||
"state": "",
|
||||
}
|
||||
var res ImageRes
|
||||
var errRes ErrRes
|
||||
r, err := c.client.SetTimeout(time.Minute).R().
|
||||
SetHeader("Authorization", "Bearer "+c.Config.ApiKey).
|
||||
SetBody(body).
|
||||
SetSuccessResult(&res).
|
||||
SetErrorResult(&errRes).
|
||||
Post(apiURL)
|
||||
if err != nil {
|
||||
return ImageRes{}, fmt.Errorf("请求 API %s 出错:%v", apiURL, err)
|
||||
}
|
||||
|
||||
if r.IsErrorState() {
|
||||
return ImageRes{}, fmt.Errorf("API 返回错误:%s", errRes.Error.Message)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Upscale 放大指定的图片
|
||||
func (c *PlusClient) Upscale(task types.MjTask) (ImageRes, error) {
|
||||
if err := c.preCheck(); err != nil {
|
||||
return ImageRes{}, err
|
||||
}
|
||||
|
||||
body := map[string]string{
|
||||
"customId": fmt.Sprintf("MJ::JOB::upsample::%d::%s", task.Index, task.MessageHash),
|
||||
"taskId": task.MessageId,
|
||||
}
|
||||
apiURL := fmt.Sprintf("%s/mj-%s/mj/submit/action", c.apiURL, c.Config.Mode)
|
||||
logger.Info("API URL: ", apiURL)
|
||||
var res ImageRes
|
||||
var errRes ErrRes
|
||||
r, err := c.client.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 *PlusClient) Variation(task types.MjTask) (ImageRes, error) {
|
||||
if err := c.preCheck(); err != nil {
|
||||
return ImageRes{}, err
|
||||
}
|
||||
|
||||
body := map[string]string{
|
||||
"customId": fmt.Sprintf("MJ::JOB::variation::%d::%s", task.Index, task.MessageHash),
|
||||
"taskId": task.MessageId,
|
||||
}
|
||||
apiURL := fmt.Sprintf("%s/mj-%s/mj/submit/action", c.apiURL, c.Config.Mode)
|
||||
logger.Info("API URL: ", 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
|
||||
}
|
||||
|
||||
func (c *PlusClient) QueryTask(taskId string) (QueryRes, error) {
|
||||
apiURL := fmt.Sprintf("%s/mj/task/%s/fetch", c.apiURL, taskId)
|
||||
var res QueryRes
|
||||
r, err := c.client.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
|
||||
}
|
||||
|
||||
var _ Client = &PlusClient{}
|
||||
@@ -1,230 +0,0 @@
|
||||
package mj
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
logger2 "geekai/logger"
|
||||
"geekai/service"
|
||||
"geekai/service/oss"
|
||||
"geekai/service/sd"
|
||||
"geekai/store"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ServicePool Mj service pool
|
||||
type ServicePool struct {
|
||||
services []*Service
|
||||
taskQueue *store.RedisQueue
|
||||
notifyQueue *store.RedisQueue
|
||||
db *gorm.DB
|
||||
uploaderManager *oss.UploaderManager
|
||||
Clients *types.LMap[uint, *types.WsClient] // UserId => Client
|
||||
licenseService *service.LicenseService
|
||||
}
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
func NewServicePool(db *gorm.DB, redisCli *redis.Client, manager *oss.UploaderManager, licenseService *service.LicenseService) *ServicePool {
|
||||
services := make([]*Service, 0)
|
||||
taskQueue := store.NewRedisQueue("MidJourney_Task_Queue", redisCli)
|
||||
notifyQueue := store.NewRedisQueue("MidJourney_Notify_Queue", redisCli)
|
||||
return &ServicePool{
|
||||
taskQueue: taskQueue,
|
||||
notifyQueue: notifyQueue,
|
||||
services: services,
|
||||
uploaderManager: manager,
|
||||
db: db,
|
||||
Clients: types.NewLMap[uint, *types.WsClient](),
|
||||
licenseService: licenseService,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ServicePool) InitServices(plusConfigs []types.MjPlusConfig, proxyConfigs []types.MjProxyConfig) {
|
||||
// stop old service
|
||||
for _, s := range p.services {
|
||||
s.Stop()
|
||||
}
|
||||
p.services = make([]*Service, 0)
|
||||
|
||||
for _, config := range plusConfigs {
|
||||
if config.Enabled == false {
|
||||
continue
|
||||
}
|
||||
|
||||
cli := NewPlusClient(config, p.licenseService)
|
||||
name := utils.Md5(config.ApiURL)
|
||||
plusService := NewService(name, p.taskQueue, p.notifyQueue, p.db, cli)
|
||||
go func() {
|
||||
plusService.Run()
|
||||
}()
|
||||
p.services = append(p.services, plusService)
|
||||
}
|
||||
|
||||
// for mid-journey proxy
|
||||
for _, config := range proxyConfigs {
|
||||
if config.Enabled == false {
|
||||
continue
|
||||
}
|
||||
cli := NewProxyClient(config)
|
||||
name := utils.Md5(config.ApiURL)
|
||||
proxyService := NewService(name, p.taskQueue, p.notifyQueue, p.db, cli)
|
||||
go func() {
|
||||
proxyService.Run()
|
||||
}()
|
||||
p.services = append(p.services, proxyService)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ServicePool) CheckTaskNotify() {
|
||||
go func() {
|
||||
for {
|
||||
var message sd.NotifyMessage
|
||||
err := p.notifyQueue.LPop(&message)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
cli := p.Clients.Get(uint(message.UserId))
|
||||
if cli == nil {
|
||||
continue
|
||||
}
|
||||
err = cli.Send([]byte(message.Message))
|
||||
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
|
||||
}
|
||||
|
||||
logger.Infof("try to download image: %s", v.OrgURL)
|
||||
mjService := p.getService(v.ChannelId)
|
||||
if mjService == nil {
|
||||
logger.Errorf("Invalid task: %+v", v)
|
||||
continue
|
||||
}
|
||||
|
||||
task, _ := mjService.Client.QueryTask(v.TaskId)
|
||||
if len(task.Buttons) > 0 {
|
||||
v.Hash = GetImageHash(task.Buttons[0].CustomId)
|
||||
}
|
||||
// 如果是返回的是 discord 图片地址,则使用代理下载
|
||||
proxy := false
|
||||
if strings.HasPrefix(v.OrgURL, "https://cdn.discordapp.com") {
|
||||
proxy = true
|
||||
}
|
||||
imgURL, err := p.uploaderManager.GetUploadHandler().PutImg(v.OrgURL, proxy)
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("error with download image %s, %v", v.OrgURL, err)
|
||||
continue
|
||||
} else {
|
||||
logger.Infof("download image %s successfully.", v.OrgURL)
|
||||
}
|
||||
|
||||
v.ImgURL = imgURL
|
||||
p.db.Updates(&v)
|
||||
|
||||
cli := p.Clients.Get(uint(v.UserId))
|
||||
if cli == nil {
|
||||
continue
|
||||
}
|
||||
err = cli.Send([]byte(sd.Finished))
|
||||
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
|
||||
}
|
||||
|
||||
// SyncTaskProgress 异步拉取任务
|
||||
func (p *ServicePool) SyncTaskProgress() {
|
||||
go func() {
|
||||
var jobs []model.MidJourneyJob
|
||||
for {
|
||||
res := p.db.Where("progress < ?", 100).Find(&jobs)
|
||||
if res.Error != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, job := range jobs {
|
||||
// 失败或者 30 分钟还没完成的任务删除并退回算力
|
||||
if time.Now().Sub(job.CreatedAt) > time.Minute*30 || job.Progress == -1 {
|
||||
p.db.Delete(&job)
|
||||
// 退回算力
|
||||
tx := p.db.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power + ?", job.Power))
|
||||
if tx.Error == nil && tx.RowsAffected > 0 {
|
||||
var user model.User
|
||||
p.db.Where("id = ?", job.UserId).First(&user)
|
||||
p.db.Create(&model.PowerLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
Type: types.PowerConsume,
|
||||
Amount: job.Power,
|
||||
Balance: user.Power + job.Power,
|
||||
Mark: types.PowerAdd,
|
||||
Model: "mid-journey",
|
||||
Remark: fmt.Sprintf("绘画任务失败,退回算力。任务ID:%s", job.TaskId),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
if servicePlus := p.getService(job.ChannelId); servicePlus != nil {
|
||||
_ = servicePlus.Notify(job)
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (p *ServicePool) getService(name string) *Service {
|
||||
for _, s := range p.services {
|
||||
if s.Name == name {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
package mj
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"geekai/utils"
|
||||
"github.com/imroc/req/v3"
|
||||
"io"
|
||||
)
|
||||
|
||||
// ProxyClient MidJourney Proxy Client
|
||||
type ProxyClient struct {
|
||||
Config types.MjProxyConfig
|
||||
apiURL string
|
||||
}
|
||||
|
||||
func NewProxyClient(config types.MjProxyConfig) *ProxyClient {
|
||||
return &ProxyClient{Config: config, apiURL: config.ApiURL}
|
||||
}
|
||||
|
||||
func (c *ProxyClient) Imagine(task types.MjTask) (ImageRes, error) {
|
||||
apiURL := fmt.Sprintf("%s/mj/submit/imagine", c.apiURL)
|
||||
prompt := fmt.Sprintf("%s %s", task.Prompt, task.Params)
|
||||
if task.NegPrompt != "" {
|
||||
prompt += fmt.Sprintf(" --no %s", task.NegPrompt)
|
||||
}
|
||||
body := ImageReq{
|
||||
Prompt: prompt,
|
||||
Base64Array: make([]string, 0),
|
||||
}
|
||||
// 生成图片 Base64 编码
|
||||
if len(task.ImgArr) > 0 {
|
||||
imageData, err := utils.DownloadImage(task.ImgArr[0], "")
|
||||
if err != nil {
|
||||
logger.Error("error with download image: ", err)
|
||||
} else {
|
||||
body.Base64Array = append(body.Base64Array, "data:image/png;base64,"+base64.StdEncoding.EncodeToString(imageData))
|
||||
}
|
||||
|
||||
}
|
||||
logger.Info("API URL: ", apiURL)
|
||||
var res ImageRes
|
||||
var errRes ErrRes
|
||||
r, err := req.C().R().
|
||||
SetHeader("mj-api-secret", c.Config.ApiKey).
|
||||
SetBody(body).
|
||||
SetSuccessResult(&res).
|
||||
SetErrorResult(&errRes).
|
||||
Post(apiURL)
|
||||
if err != nil {
|
||||
return ImageRes{}, fmt.Errorf("请求 API %s 出错:%v", apiURL, err)
|
||||
}
|
||||
|
||||
if r.IsErrorState() {
|
||||
errStr, _ := io.ReadAll(r.Body)
|
||||
return ImageRes{}, fmt.Errorf("API 返回错误:%s,%v", errRes.Error.Message, string(errStr))
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Blend 融图
|
||||
func (c *ProxyClient) Blend(task types.MjTask) (ImageRes, error) {
|
||||
apiURL := fmt.Sprintf("%s/mj/submit/blend", c.apiURL)
|
||||
body := ImageReq{
|
||||
Dimensions: "SQUARE",
|
||||
Base64Array: make([]string, 0),
|
||||
}
|
||||
// 生成图片 Base64 编码
|
||||
if len(task.ImgArr) > 0 {
|
||||
for _, imgURL := range task.ImgArr {
|
||||
imageData, err := utils.DownloadImage(imgURL, "")
|
||||
if err != nil {
|
||||
logger.Error("error with download image: ", err)
|
||||
} else {
|
||||
body.Base64Array = append(body.Base64Array, "data:image/png;base64,"+base64.StdEncoding.EncodeToString(imageData))
|
||||
}
|
||||
}
|
||||
}
|
||||
var res ImageRes
|
||||
var errRes ErrRes
|
||||
r, err := req.C().R().
|
||||
SetHeader("mj-api-secret", c.Config.ApiKey).
|
||||
SetBody(body).
|
||||
SetSuccessResult(&res).
|
||||
SetErrorResult(&errRes).
|
||||
Post(apiURL)
|
||||
if err != nil {
|
||||
return ImageRes{}, fmt.Errorf("请求 API %s 出错:%v", apiURL, err)
|
||||
}
|
||||
|
||||
if r.IsErrorState() {
|
||||
return ImageRes{}, fmt.Errorf("API 返回错误:%s", errRes.Error.Message)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// SwapFace 换脸
|
||||
func (c *ProxyClient) SwapFace(_ types.MjTask) (ImageRes, error) {
|
||||
return ImageRes{}, errors.New("MidJourney-Proxy暂未实现该功能,请使用 MidJourney-Plus")
|
||||
}
|
||||
|
||||
// Upscale 放大指定的图片
|
||||
func (c *ProxyClient) Upscale(task types.MjTask) (ImageRes, error) {
|
||||
body := map[string]interface{}{
|
||||
"action": "UPSCALE",
|
||||
"index": task.Index,
|
||||
"taskId": task.MessageId,
|
||||
}
|
||||
apiURL := fmt.Sprintf("%s/mj/submit/change", c.apiURL)
|
||||
var res ImageRes
|
||||
var errRes ErrRes
|
||||
r, err := req.C().R().
|
||||
SetHeader("mj-api-secret", 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 *ProxyClient) Variation(task types.MjTask) (ImageRes, error) {
|
||||
body := map[string]interface{}{
|
||||
"action": "VARIATION",
|
||||
"index": task.Index,
|
||||
"taskId": task.MessageId,
|
||||
}
|
||||
apiURL := fmt.Sprintf("%s/mj/submit/change", c.apiURL)
|
||||
var res ImageRes
|
||||
var errRes ErrRes
|
||||
r, err := req.C().R().
|
||||
SetHeader("mj-api-secret", 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
|
||||
}
|
||||
|
||||
func (c *ProxyClient) QueryTask(taskId string) (QueryRes, error) {
|
||||
apiURL := fmt.Sprintf("%s/mj/task/%s/fetch", c.apiURL, taskId)
|
||||
var res QueryRes
|
||||
r, err := req.C().R().SetHeader("mj-api-secret", 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
|
||||
}
|
||||
|
||||
var _ Client = &ProxyClient{}
|
||||
@@ -11,10 +11,11 @@ import (
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/service/sd"
|
||||
"geekai/service/oss"
|
||||
"geekai/store"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -23,118 +24,115 @@ import (
|
||||
|
||||
// Service MJ 绘画服务
|
||||
type Service struct {
|
||||
Name string // service Name
|
||||
Client Client // MJ Client
|
||||
taskQueue *store.RedisQueue
|
||||
notifyQueue *store.RedisQueue
|
||||
db *gorm.DB
|
||||
running bool
|
||||
client *Client // MJ Client
|
||||
taskQueue *store.RedisQueue
|
||||
notifyQueue *store.RedisQueue
|
||||
db *gorm.DB
|
||||
wsService *service.WebsocketService
|
||||
uploaderManager *oss.UploaderManager
|
||||
clientIds map[uint]string
|
||||
}
|
||||
|
||||
func NewService(name string, taskQueue *store.RedisQueue, notifyQueue *store.RedisQueue, db *gorm.DB, cli Client) *Service {
|
||||
func NewService(redisCli *redis.Client, db *gorm.DB, client *Client, manager *oss.UploaderManager, wsService *service.WebsocketService) *Service {
|
||||
return &Service{
|
||||
Name: name,
|
||||
db: db,
|
||||
taskQueue: taskQueue,
|
||||
notifyQueue: notifyQueue,
|
||||
Client: cli,
|
||||
running: true,
|
||||
db: db,
|
||||
taskQueue: store.NewRedisQueue("MidJourney_Task_Queue", redisCli),
|
||||
notifyQueue: store.NewRedisQueue("MidJourney_Notify_Queue", redisCli),
|
||||
client: client,
|
||||
wsService: wsService,
|
||||
uploaderManager: manager,
|
||||
clientIds: map[uint]string{},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Run() {
|
||||
logger.Infof("Starting MidJourney job consumer for %s", s.Name)
|
||||
for s.running {
|
||||
var task types.MjTask
|
||||
err := s.taskQueue.LPop(&task)
|
||||
if err != nil {
|
||||
logger.Errorf("taking task with error: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 如果配置了多个中转平台的 API KEY
|
||||
// U,V 操作必须和 Image 操作属于同一个平台,否则找不到关联任务,需重新放回任务列表
|
||||
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
|
||||
}
|
||||
|
||||
// translate prompt
|
||||
if utils.HasChinese(task.Prompt) {
|
||||
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, task.Prompt))
|
||||
if err == nil {
|
||||
task.Prompt = content
|
||||
} else {
|
||||
logger.Warnf("error with translate prompt: %v", err)
|
||||
}
|
||||
}
|
||||
// translate negative prompt
|
||||
if task.NegPrompt != "" && utils.HasChinese(task.NegPrompt) {
|
||||
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, task.NegPrompt))
|
||||
if err == nil {
|
||||
task.NegPrompt = content
|
||||
} else {
|
||||
logger.Warnf("error with translate prompt: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
var job model.MidJourneyJob
|
||||
tx := s.db.Where("id = ?", task.Id).First(&job)
|
||||
if tx.Error != nil {
|
||||
logger.Error("任务不存在,任务ID:", task.TaskId)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Infof("%s handle a new MidJourney task: %+v", s.Name, task)
|
||||
var res ImageRes
|
||||
switch task.Type {
|
||||
case types.TaskImage:
|
||||
res, err = s.Client.Imagine(task)
|
||||
break
|
||||
case types.TaskUpscale:
|
||||
res, err = s.Client.Upscale(task)
|
||||
break
|
||||
case types.TaskVariation:
|
||||
res, err = s.Client.Variation(task)
|
||||
break
|
||||
case types.TaskBlend:
|
||||
res, err = s.Client.Blend(task)
|
||||
break
|
||||
case types.TaskSwapFace:
|
||||
res, err = s.Client.SwapFace(task)
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil || (res.Code != 1 && res.Code != 22) {
|
||||
var errMsg string
|
||||
logger.Info("Starting MidJourney job consumer for service")
|
||||
go func() {
|
||||
for {
|
||||
var task types.MjTask
|
||||
err := s.taskQueue.LPop(&task)
|
||||
if err != nil {
|
||||
errMsg = err.Error()
|
||||
} else {
|
||||
errMsg = fmt.Sprintf("%v,%s", err, res.Description)
|
||||
logger.Errorf("taking task with error: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Error("绘画任务执行失败:", errMsg)
|
||||
job.Progress = -1
|
||||
job.ErrMsg = errMsg
|
||||
// update the task progress
|
||||
s.db.Updates(&job)
|
||||
// 任务失败,通知前端
|
||||
s.notifyQueue.RPush(sd.NotifyMessage{UserId: task.UserId, JobId: int(job.Id), Message: sd.Failed})
|
||||
continue
|
||||
}
|
||||
logger.Infof("任务提交成功:%+v", res)
|
||||
// 更新任务 ID/频道
|
||||
job.TaskId = res.Result
|
||||
job.MessageId = res.Result
|
||||
job.ChannelId = s.Name
|
||||
s.db.Updates(&job)
|
||||
}
|
||||
}
|
||||
// translate prompt
|
||||
if utils.HasChinese(task.Prompt) {
|
||||
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.Prompt), "gpt-4o-mini", 0)
|
||||
if err == nil {
|
||||
task.Prompt = content
|
||||
} else {
|
||||
logger.Warnf("error with translate prompt: %v", err)
|
||||
}
|
||||
}
|
||||
// translate negative prompt
|
||||
if task.NegPrompt != "" && utils.HasChinese(task.NegPrompt) {
|
||||
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.NegPrompt), "gpt-4o-mini", 0)
|
||||
if err == nil {
|
||||
task.NegPrompt = content
|
||||
} else {
|
||||
logger.Warnf("error with translate prompt: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Stop() {
|
||||
s.running = false
|
||||
// use fast mode as default
|
||||
if task.Mode == "" {
|
||||
task.Mode = "fast"
|
||||
}
|
||||
s.clientIds[task.Id] = task.ClientId
|
||||
|
||||
var job model.MidJourneyJob
|
||||
tx := s.db.Where("id = ?", task.Id).First(&job)
|
||||
if tx.Error != nil {
|
||||
logger.Error("任务不存在,任务ID:", task.TaskId)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Infof("handle a new MidJourney task: %+v", task)
|
||||
var res ImageRes
|
||||
switch task.Type {
|
||||
case types.TaskImage:
|
||||
res, err = s.client.Imagine(task)
|
||||
break
|
||||
case types.TaskUpscale:
|
||||
res, err = s.client.Upscale(task)
|
||||
break
|
||||
case types.TaskVariation:
|
||||
res, err = s.client.Variation(task)
|
||||
break
|
||||
case types.TaskBlend:
|
||||
res, err = s.client.Blend(task)
|
||||
break
|
||||
case types.TaskSwapFace:
|
||||
res, err = s.client.SwapFace(task)
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil || (res.Code != 1 && res.Code != 22) {
|
||||
var errMsg string
|
||||
if err != nil {
|
||||
errMsg = err.Error()
|
||||
} else {
|
||||
errMsg = fmt.Sprintf("%v,%s", err, res.Description)
|
||||
}
|
||||
|
||||
logger.Error("绘画任务执行失败:", errMsg)
|
||||
job.Progress = service.FailTaskProgress
|
||||
job.ErrMsg = errMsg
|
||||
// update the task progress
|
||||
s.db.Updates(&job)
|
||||
// 任务失败,通知前端
|
||||
s.notifyQueue.RPush(service.NotifyMessage{ClientId: task.ClientId, UserId: task.UserId, JobId: int(job.Id), Message: service.TaskStatusFailed})
|
||||
continue
|
||||
}
|
||||
logger.Infof("任务提交成功:%+v", res)
|
||||
// 更新任务 ID/频道
|
||||
job.TaskId = res.Result
|
||||
job.MessageId = res.Result
|
||||
job.ChannelId = res.Channel
|
||||
s.db.Updates(&job)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
type CBReq struct {
|
||||
@@ -155,46 +153,6 @@ type CBReq struct {
|
||||
} `json:"properties"`
|
||||
}
|
||||
|
||||
func (s *Service) Notify(job model.MidJourneyJob) error {
|
||||
task, err := s.Client.QueryTask(job.TaskId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 任务执行失败了
|
||||
if task.FailReason != "" {
|
||||
s.db.Model(&model.MidJourneyJob{Id: job.Id}).UpdateColumns(map[string]interface{}{
|
||||
"progress": -1,
|
||||
"err_msg": task.FailReason,
|
||||
})
|
||||
s.notifyQueue.RPush(sd.NotifyMessage{UserId: job.UserId, JobId: int(job.Id), Message: sd.Failed})
|
||||
return fmt.Errorf("task failed: %v", task.FailReason)
|
||||
}
|
||||
|
||||
if len(task.Buttons) > 0 {
|
||||
job.Hash = GetImageHash(task.Buttons[0].CustomId)
|
||||
}
|
||||
oldProgress := job.Progress
|
||||
job.Progress = utils.IntValue(strings.Replace(task.Progress, "%", "", 1), 0)
|
||||
job.Prompt = task.PromptEn
|
||||
if task.ImageUrl != "" {
|
||||
job.OrgURL = task.ImageUrl
|
||||
}
|
||||
tx := s.db.Updates(&job)
|
||||
if tx.Error != nil {
|
||||
return fmt.Errorf("error with update database: %v", tx.Error)
|
||||
}
|
||||
// 通知前端更新任务进度
|
||||
if oldProgress != job.Progress {
|
||||
message := sd.Running
|
||||
if job.Progress == 100 {
|
||||
message = sd.Finished
|
||||
}
|
||||
s.notifyQueue.RPush(sd.NotifyMessage{UserId: job.UserId, JobId: int(job.Id), Message: message})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetImageHash(action string) string {
|
||||
split := strings.Split(action, "::")
|
||||
if len(split) > 5 {
|
||||
@@ -202,3 +160,150 @@ func GetImageHash(action string) string {
|
||||
}
|
||||
return split[len(split)-1]
|
||||
}
|
||||
|
||||
func (s *Service) CheckTaskNotify() {
|
||||
go func() {
|
||||
for {
|
||||
var message service.NotifyMessage
|
||||
err := s.notifyQueue.LPop(&message)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
logger.Debugf("receive a new mj notify message: %+v", message)
|
||||
client := s.wsService.Clients.Get(message.ClientId)
|
||||
if client == nil {
|
||||
continue
|
||||
}
|
||||
utils.SendChannelMsg(client, types.ChMj, message.Message)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Service) DownloadImages() {
|
||||
go func() {
|
||||
var items []model.MidJourneyJob
|
||||
for {
|
||||
res := s.db.Where("img_url = ? AND progress = ?", "", 100).Find(&items)
|
||||
if res.Error != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// download images
|
||||
for _, v := range items {
|
||||
if v.OrgURL == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Infof("try to download image: %s", v.OrgURL)
|
||||
// 如果是返回的是 discord 图片地址,则使用代理下载
|
||||
proxy := false
|
||||
if strings.HasPrefix(v.OrgURL, "https://cdn.discordapp.com") {
|
||||
proxy = true
|
||||
}
|
||||
imgURL, err := s.uploaderManager.GetUploadHandler().PutUrlFile(v.OrgURL, proxy)
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("error with download image %s, %v", v.OrgURL, err)
|
||||
continue
|
||||
} else {
|
||||
logger.Infof("download image %s successfully.", v.OrgURL)
|
||||
}
|
||||
|
||||
v.ImgURL = imgURL
|
||||
s.db.Updates(&v)
|
||||
|
||||
s.notifyQueue.RPush(service.NotifyMessage{
|
||||
ClientId: s.clientIds[v.Id],
|
||||
UserId: v.UserId,
|
||||
JobId: int(v.Id),
|
||||
Message: service.TaskStatusFinished})
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 5)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// PushTask push a new mj task in to task queue
|
||||
func (s *Service) PushTask(task types.MjTask) {
|
||||
logger.Debugf("add a new MidJourney task to the task list: %+v", task)
|
||||
s.taskQueue.RPush(task)
|
||||
}
|
||||
|
||||
// SyncTaskProgress 异步拉取任务
|
||||
func (s *Service) SyncTaskProgress() {
|
||||
go func() {
|
||||
var jobs []model.MidJourneyJob
|
||||
for {
|
||||
err := s.db.Where("progress < ?", 100).Find(&jobs).Error
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, job := range jobs {
|
||||
// 10 分钟还没完成的任务标记为失败
|
||||
if time.Now().Sub(job.CreatedAt) > time.Minute*10 {
|
||||
job.Progress = service.FailTaskProgress
|
||||
job.ErrMsg = "任务超时"
|
||||
s.db.Updates(&job)
|
||||
continue
|
||||
}
|
||||
|
||||
if job.ChannelId == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
task, err := s.client.QueryTask(job.TaskId, job.ChannelId)
|
||||
if err != nil {
|
||||
logger.Errorf("error with query task: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 任务执行失败了
|
||||
if task.FailReason != "" {
|
||||
s.db.Model(&model.MidJourneyJob{Id: job.Id}).UpdateColumns(map[string]interface{}{
|
||||
"progress": service.FailTaskProgress,
|
||||
"err_msg": task.FailReason,
|
||||
})
|
||||
logger.Errorf("task failed: %v", task.FailReason)
|
||||
s.notifyQueue.RPush(service.NotifyMessage{
|
||||
ClientId: s.clientIds[job.Id],
|
||||
UserId: job.UserId,
|
||||
JobId: int(job.Id),
|
||||
Message: service.TaskStatusFailed})
|
||||
continue
|
||||
}
|
||||
|
||||
if len(task.Buttons) > 0 {
|
||||
job.Hash = GetImageHash(task.Buttons[0].CustomId)
|
||||
}
|
||||
oldProgress := job.Progress
|
||||
job.Progress = utils.IntValue(strings.Replace(task.Progress, "%", "", 1), 0)
|
||||
job.Prompt = task.PromptEn
|
||||
if task.ImageUrl != "" {
|
||||
job.OrgURL = task.ImageUrl
|
||||
}
|
||||
err = s.db.Updates(&job).Error
|
||||
if err != nil {
|
||||
logger.Errorf("error with update database: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 通知前端更新任务进度
|
||||
if oldProgress != job.Progress {
|
||||
message := service.TaskStatusRunning
|
||||
if job.Progress == 100 {
|
||||
message = service.TaskStatusFinished
|
||||
}
|
||||
s.notifyQueue.RPush(service.NotifyMessage{
|
||||
ClientId: s.clientIds[job.Id],
|
||||
UserId: job.UserId,
|
||||
JobId: int(job.Id),
|
||||
Message: message})
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 5)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -84,25 +84,25 @@ func (s AliYunOss) PutFile(ctx *gin.Context, name string) (File, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s AliYunOss) PutImg(imageURL string, useProxy bool) (string, error) {
|
||||
var imageData []byte
|
||||
func (s AliYunOss) PutUrlFile(fileURL string, useProxy bool) (string, error) {
|
||||
var fileData []byte
|
||||
var err error
|
||||
if useProxy {
|
||||
imageData, err = utils.DownloadImage(imageURL, s.proxyURL)
|
||||
fileData, err = utils.DownloadImage(fileURL, s.proxyURL)
|
||||
} else {
|
||||
imageData, err = utils.DownloadImage(imageURL, "")
|
||||
fileData, err = utils.DownloadImage(fileURL, "")
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with download image: %v", err)
|
||||
}
|
||||
parse, err := url.Parse(imageURL)
|
||||
parse, err := url.Parse(fileURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with parse image URL: %v", err)
|
||||
}
|
||||
fileExt := utils.GetImgExt(parse.Path)
|
||||
objectKey := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), fileExt)
|
||||
// 上传文件字节数据
|
||||
err = s.bucket.PutObject(objectKey, bytes.NewReader(imageData))
|
||||
err = s.bucket.PutObject(objectKey, bytes.NewReader(fileData))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -57,8 +57,8 @@ func (s LocalStorage) PutFile(ctx *gin.Context, name string) (File, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s LocalStorage) PutImg(imageURL string, useProxy bool) (string, error) {
|
||||
parse, err := url.Parse(imageURL)
|
||||
func (s LocalStorage) PutUrlFile(fileURL string, useProxy bool) (string, error) {
|
||||
parse, err := url.Parse(fileURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with parse image URL: %v", err)
|
||||
}
|
||||
@@ -69,9 +69,9 @@ func (s LocalStorage) PutImg(imageURL string, useProxy bool) (string, error) {
|
||||
}
|
||||
|
||||
if useProxy {
|
||||
err = utils.DownloadFile(imageURL, filePath, s.proxyURL)
|
||||
err = utils.DownloadFile(fileURL, filePath, s.proxyURL)
|
||||
} else {
|
||||
err = utils.DownloadFile(imageURL, filePath, "")
|
||||
err = utils.DownloadFile(fileURL, filePath, "")
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with download image: %v", err)
|
||||
|
||||
@@ -44,18 +44,18 @@ func NewMiniOss(appConfig *types.AppConfig) (MiniOss, error) {
|
||||
return MiniOss{config: config, client: minioClient, proxyURL: appConfig.ProxyURL}, nil
|
||||
}
|
||||
|
||||
func (s MiniOss) PutImg(imageURL string, useProxy bool) (string, error) {
|
||||
var imageData []byte
|
||||
func (s MiniOss) PutUrlFile(fileURL string, useProxy bool) (string, error) {
|
||||
var fileData []byte
|
||||
var err error
|
||||
if useProxy {
|
||||
imageData, err = utils.DownloadImage(imageURL, s.proxyURL)
|
||||
fileData, err = utils.DownloadImage(fileURL, s.proxyURL)
|
||||
} else {
|
||||
imageData, err = utils.DownloadImage(imageURL, "")
|
||||
fileData, err = utils.DownloadImage(fileURL, "")
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with download image: %v", err)
|
||||
}
|
||||
parse, err := url.Parse(imageURL)
|
||||
parse, err := url.Parse(fileURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with parse image URL: %v", err)
|
||||
}
|
||||
@@ -65,8 +65,8 @@ func (s MiniOss) PutImg(imageURL string, useProxy bool) (string, error) {
|
||||
context.Background(),
|
||||
s.config.Bucket,
|
||||
filename,
|
||||
strings.NewReader(string(imageData)),
|
||||
int64(len(imageData)),
|
||||
strings.NewReader(string(fileData)),
|
||||
int64(len(fileData)),
|
||||
minio.PutObjectOptions{ContentType: "image/png"})
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -89,7 +89,7 @@ func (s MiniOss) PutFile(ctx *gin.Context, name string) (File, error) {
|
||||
fileExt := utils.GetImgExt(file.Filename)
|
||||
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"),
|
||||
ContentType: file.Header.Get("Body-Type"),
|
||||
})
|
||||
if err != nil {
|
||||
return File{}, fmt.Errorf("error uploading to MinIO: %v", err)
|
||||
|
||||
@@ -93,18 +93,18 @@ func (s QinNiuOss) PutFile(ctx *gin.Context, name string) (File, error) {
|
||||
|
||||
}
|
||||
|
||||
func (s QinNiuOss) PutImg(imageURL string, useProxy bool) (string, error) {
|
||||
var imageData []byte
|
||||
func (s QinNiuOss) PutUrlFile(fileURL string, useProxy bool) (string, error) {
|
||||
var fileData []byte
|
||||
var err error
|
||||
if useProxy {
|
||||
imageData, err = utils.DownloadImage(imageURL, s.proxyURL)
|
||||
fileData, err = utils.DownloadImage(fileURL, s.proxyURL)
|
||||
} else {
|
||||
imageData, err = utils.DownloadImage(imageURL, "")
|
||||
fileData, err = utils.DownloadImage(fileURL, "")
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with download image: %v", err)
|
||||
}
|
||||
parse, err := url.Parse(imageURL)
|
||||
parse, err := url.Parse(fileURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with parse image URL: %v", err)
|
||||
}
|
||||
@@ -113,7 +113,7 @@ func (s QinNiuOss) PutImg(imageURL string, useProxy bool) (string, error) {
|
||||
ret := storage.PutRet{}
|
||||
extra := storage.PutExtra{}
|
||||
// 上传文件字节数据
|
||||
err = s.uploader.Put(context.Background(), &ret, s.putPolicy.UploadToken(s.mac), key, bytes.NewReader(imageData), int64(len(imageData)), &extra)
|
||||
err = s.uploader.Put(context.Background(), &ret, s.putPolicy.UploadToken(s.mac), key, bytes.NewReader(fileData), int64(len(fileData)), &extra)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ type File struct {
|
||||
}
|
||||
type Uploader interface {
|
||||
PutFile(ctx *gin.Context, name string) (File, error)
|
||||
PutImg(imageURL string, useProxy bool) (string, error)
|
||||
PutUrlFile(url string, useProxy bool) (string, error)
|
||||
PutBase64(imageData string) (string, error)
|
||||
Delete(fileURL string) error
|
||||
}
|
||||
|
||||
@@ -43,10 +43,8 @@ func NewAlipayService(appConfig *types.AppConfig) (*AlipayService, error) {
|
||||
|
||||
//client.DebugSwitch = gopay.DebugOn // 开启调试模式
|
||||
client.SetLocation(alipay.LocationShanghai). // 设置时区,不设置或出错均为默认服务器时间
|
||||
SetCharset(alipay.UTF8). // 设置字符编码,不设置默认 utf-8
|
||||
SetSignType(alipay.RSA2). // 设置签名类型,不设置默认 RSA2
|
||||
SetReturnUrl(config.ReturnURL). // 设置返回URL
|
||||
SetNotifyUrl(config.NotifyURL)
|
||||
SetCharset(alipay.UTF8). // 设置字符编码,不设置默认 utf-8
|
||||
SetSignType(alipay.RSA2) // 设置签名类型,不设置默认 RSA2
|
||||
|
||||
if err = client.SetCertSnByPath(config.PublicKey, config.RootCert, config.AlipayPublicKey); err != nil {
|
||||
return nil, fmt.Errorf("error with load payment public key: %v", err)
|
||||
@@ -55,23 +53,31 @@ func NewAlipayService(appConfig *types.AppConfig) (*AlipayService, error) {
|
||||
return &AlipayService{config: &config, client: client}, nil
|
||||
}
|
||||
|
||||
func (s *AlipayService) PayUrlMobile(outTradeNo string, amount string, subject string) (string, error) {
|
||||
bm := make(gopay.BodyMap)
|
||||
bm.Set("subject", subject)
|
||||
bm.Set("out_trade_no", outTradeNo)
|
||||
bm.Set("quit_url", s.config.ReturnURL)
|
||||
bm.Set("total_amount", amount)
|
||||
bm.Set("product_code", "QUICK_WAP_WAY")
|
||||
return s.client.TradeWapPay(context.Background(), bm)
|
||||
type AlipayParams struct {
|
||||
OutTradeNo string `json:"out_trade_no"`
|
||||
Subject string `json:"subject"`
|
||||
TotalFee string `json:"total_fee"`
|
||||
ReturnURL string `json:"return_url"`
|
||||
NotifyURL string `json:"notify_url"`
|
||||
}
|
||||
|
||||
func (s *AlipayService) PayUrlPc(outTradeNo string, amount string, subject string) (string, error) {
|
||||
func (s *AlipayService) PayMobile(params AlipayParams) (string, error) {
|
||||
bm := make(gopay.BodyMap)
|
||||
bm.Set("subject", subject)
|
||||
bm.Set("out_trade_no", outTradeNo)
|
||||
bm.Set("total_amount", amount)
|
||||
bm.Set("subject", params.Subject)
|
||||
bm.Set("out_trade_no", params.OutTradeNo)
|
||||
bm.Set("quit_url", params.ReturnURL)
|
||||
bm.Set("total_amount", params.TotalFee)
|
||||
bm.Set("product_code", "QUICK_WAP_WAY")
|
||||
return s.client.SetNotifyUrl(params.NotifyURL).SetReturnUrl(params.ReturnURL).TradeWapPay(context.Background(), bm)
|
||||
}
|
||||
|
||||
func (s *AlipayService) PayPC(params AlipayParams) (string, error) {
|
||||
bm := make(gopay.BodyMap)
|
||||
bm.Set("subject", params.Subject)
|
||||
bm.Set("out_trade_no", params.OutTradeNo)
|
||||
bm.Set("total_amount", params.TotalFee)
|
||||
bm.Set("product_code", "FAST_INSTANT_TRADE_PAY")
|
||||
return s.client.TradePagePay(context.Background(), bm)
|
||||
return s.client.SetNotifyUrl(params.NotifyURL).SetReturnUrl(params.ReturnURL).TradePagePay(context.Background(), bm)
|
||||
}
|
||||
|
||||
// TradeVerify 交易验证
|
||||
|
||||
139
api/service/payment/geekpay_service.go
Normal file
139
api/service/payment/geekpay_service.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package payment
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"geekai/utils"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GeekPayService Geek 支付服务
|
||||
type GeekPayService struct {
|
||||
config *types.GeekPayConfig
|
||||
}
|
||||
|
||||
func NewJPayService(appConfig *types.AppConfig) *GeekPayService {
|
||||
return &GeekPayService{
|
||||
config: &appConfig.GeekPayConfig,
|
||||
}
|
||||
}
|
||||
|
||||
type GeekPayParams struct {
|
||||
Method string `json:"method"` // 接口类型
|
||||
Device string `json:"device"` // 设备类型
|
||||
Type string `json:"type"` // 支付方式
|
||||
OutTradeNo string `json:"out_trade_no"` // 商户订单号
|
||||
Name string `json:"name"` // 商品名称
|
||||
Money string `json:"money"` // 商品金额
|
||||
ClientIP string `json:"clientip"` //用户IP地址
|
||||
SubOpenId string `json:"sub_openid"` // 微信用户 openid,仅小程序支付需要
|
||||
SubAppId string `json:"sub_appid"` // 小程序 AppId,仅小程序支付需要
|
||||
NotifyURL string `json:"notify_url"`
|
||||
ReturnURL string `json:"return_url"`
|
||||
}
|
||||
|
||||
// Pay 支付订单
|
||||
func (s *GeekPayService) Pay(params GeekPayParams) (*GeekPayResp, error) {
|
||||
p := map[string]string{
|
||||
"pid": s.config.AppId,
|
||||
//"method": params.Method,
|
||||
"device": params.Device,
|
||||
"type": params.Type,
|
||||
"out_trade_no": params.OutTradeNo,
|
||||
"name": params.Name,
|
||||
"money": params.Money,
|
||||
"clientip": params.ClientIP,
|
||||
"notify_url": params.NotifyURL,
|
||||
"return_url": params.ReturnURL,
|
||||
"timestamp": fmt.Sprintf("%d", time.Now().Unix()),
|
||||
}
|
||||
p["sign"] = s.Sign(p)
|
||||
p["sign_type"] = "MD5"
|
||||
return s.sendRequest(s.config.ApiURL, p)
|
||||
}
|
||||
|
||||
func (s *GeekPayService) Sign(params map[string]string) string {
|
||||
// 按字母顺序排序参数
|
||||
var keys []string
|
||||
for k := range params {
|
||||
if params[k] == "" || k == "sign" || k == "sign_type" {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
// 构建待签名字符串
|
||||
var signStr strings.Builder
|
||||
for _, k := range keys {
|
||||
signStr.WriteString(k)
|
||||
signStr.WriteString("=")
|
||||
signStr.WriteString(params[k])
|
||||
signStr.WriteString("&")
|
||||
}
|
||||
signString := strings.TrimSuffix(signStr.String(), "&") + s.config.PrivateKey
|
||||
|
||||
return utils.Md5(signString)
|
||||
}
|
||||
|
||||
type GeekPayResp struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
TradeNo string `json:"trade_no"`
|
||||
PayURL string `json:"payurl"`
|
||||
QrCode string `json:"qrcode"`
|
||||
UrlScheme string `json:"urlscheme"` // 小程序跳转支付链接
|
||||
}
|
||||
|
||||
func (s *GeekPayService) sendRequest(endpoint string, params map[string]string) (*GeekPayResp, error) {
|
||||
form := url.Values{}
|
||||
for k, v := range params {
|
||||
form.Add(k, v)
|
||||
}
|
||||
|
||||
apiURL := fmt.Sprintf("%s/mapi.php", endpoint)
|
||||
logger.Infof(apiURL)
|
||||
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true, // 取消 SSL 证书验证
|
||||
},
|
||||
}
|
||||
client := &http.Client{Transport: tr}
|
||||
resp, err := client.PostForm(apiURL, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
logger.Debugf(string(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var r GeekPayResp
|
||||
err = json.Unmarshal(body, &r)
|
||||
if err != nil {
|
||||
return nil, errors.New("当前支付渠道暂不支持")
|
||||
}
|
||||
if r.Code != 1 {
|
||||
return nil, errors.New(r.Msg)
|
||||
}
|
||||
return &r, nil
|
||||
}
|
||||
@@ -37,7 +37,7 @@ func NewHuPiPay(config *types.AppConfig) *HuPiPayService {
|
||||
}
|
||||
}
|
||||
|
||||
type HuPiPayReq struct {
|
||||
type HuPiPayParams struct {
|
||||
AppId string `json:"appid"`
|
||||
Version string `json:"version"`
|
||||
TradeOrderId string `json:"trade_order_id"`
|
||||
@@ -53,7 +53,7 @@ type HuPiPayReq struct {
|
||||
WapUrl string `json:"wap_url"`
|
||||
}
|
||||
|
||||
type HuPiResp struct {
|
||||
type HuPiPayResp struct {
|
||||
Openid interface{} `json:"openid"`
|
||||
UrlQrcode string `json:"url_qrcode"`
|
||||
URL string `json:"url"`
|
||||
@@ -62,7 +62,7 @@ type HuPiResp struct {
|
||||
}
|
||||
|
||||
// Pay 执行支付请求操作
|
||||
func (s *HuPiPayService) Pay(params HuPiPayReq) (HuPiResp, error) {
|
||||
func (s *HuPiPayService) Pay(params HuPiPayParams) (HuPiPayResp, error) {
|
||||
data := url.Values{}
|
||||
simple := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
params.AppId = s.appId
|
||||
@@ -80,22 +80,22 @@ func (s *HuPiPayService) Pay(params HuPiPayReq) (HuPiResp, error) {
|
||||
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)
|
||||
return HuPiPayResp{}, 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)
|
||||
return HuPiPayResp{}, fmt.Errorf("error with reading response: %v", err)
|
||||
}
|
||||
|
||||
var res HuPiResp
|
||||
var res HuPiPayResp
|
||||
err = utils.JsonDecode(string(all), &res)
|
||||
if err != nil {
|
||||
return HuPiResp{}, fmt.Errorf("error with decode payment result: %v", err)
|
||||
return HuPiPayResp{}, 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 HuPiPayResp{}, fmt.Errorf("error with generate pay url: %s", res.ErrMsg)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
@@ -127,10 +127,10 @@ func (s *HuPiPayService) Sign(params url.Values) string {
|
||||
}
|
||||
|
||||
// Check 校验订单状态
|
||||
func (s *HuPiPayService) Check(tradeNo string) error {
|
||||
func (s *HuPiPayService) Check(outTradeNo string) error {
|
||||
data := url.Values{}
|
||||
data.Add("appid", s.appId)
|
||||
data.Add("open_order_id", tradeNo)
|
||||
data.Add("out_trade_order", outTradeNo)
|
||||
stamp := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
data.Add("time", stamp)
|
||||
data.Add("nonce_str", stamp)
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
package payment
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"geekai/utils"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type JPayService struct {
|
||||
config *types.JPayConfig
|
||||
}
|
||||
|
||||
func NewJPayService(appConfig *types.AppConfig) *JPayService {
|
||||
return &JPayService{
|
||||
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"`
|
||||
ReturnURL string `json:"callback_url"`
|
||||
}
|
||||
type JPayReps struct {
|
||||
OutTradeNo string `json:"out_trade_no"`
|
||||
OrderId string `json:"payjs_order_id"`
|
||||
ReturnCode int `json:"return_code"`
|
||||
ReturnMsg string `json:"return_msg"`
|
||||
Sign string `json:"Sign"`
|
||||
TotalFee string `json:"total_fee"`
|
||||
CodeUrl string `json:"code_url,omitempty"`
|
||||
Qrcode string `json:"qrcode,omitempty"`
|
||||
}
|
||||
|
||||
func (r JPayReps) IsOK() bool {
|
||||
return r.ReturnMsg == "SUCCESS"
|
||||
}
|
||||
|
||||
func (js *JPayService) 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 *JPayService) PayH5(p url.Values) string {
|
||||
p.Add("mchid", js.config.AppId)
|
||||
p.Add("sign", js.sign(p))
|
||||
return fmt.Sprintf("%s/api/cashier?%s", js.config.ApiURL, p.Encode())
|
||||
}
|
||||
|
||||
func (js *JPayService) 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)
|
||||
}
|
||||
|
||||
// TradeVerify 查询订单支付状态
|
||||
// @param tradeNo 支付平台交易 ID
|
||||
func (js *JPayService) TradeVerify(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)
|
||||
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 {
|
||||
logger.Errorf("PayJs 支付验证响应:%s", string(body))
|
||||
return errors.New("order not paid")
|
||||
}
|
||||
}
|
||||
@@ -46,18 +46,27 @@ func NewWechatService(appConfig *types.AppConfig) (*WechatPayService, error) {
|
||||
return &WechatPayService{config: &config, client: client}, nil
|
||||
}
|
||||
|
||||
func (s *WechatPayService) PayUrlNative(outTradeNo string, amount int, subject string) (string, error) {
|
||||
type WechatPayParams struct {
|
||||
OutTradeNo string `json:"out_trade_no"`
|
||||
TotalFee int `json:"total_fee"`
|
||||
Subject string `json:"subject"`
|
||||
ClientIP string `json:"client_ip"`
|
||||
ReturnURL string `json:"return_url"`
|
||||
NotifyURL string `json:"notify_url"`
|
||||
}
|
||||
|
||||
func (s *WechatPayService) PayUrlNative(params WechatPayParams) (string, error) {
|
||||
expire := time.Now().Add(10 * time.Minute).Format(time.RFC3339)
|
||||
// 初始化 BodyMap
|
||||
bm := make(gopay.BodyMap)
|
||||
bm.Set("appid", s.config.AppId).
|
||||
Set("mchid", s.config.MchId).
|
||||
Set("description", subject).
|
||||
Set("out_trade_no", outTradeNo).
|
||||
Set("description", params.Subject).
|
||||
Set("out_trade_no", params.OutTradeNo).
|
||||
Set("time_expire", expire).
|
||||
Set("notify_url", s.config.NotifyURL).
|
||||
Set("notify_url", params.NotifyURL).
|
||||
SetBodyMap("amount", func(bm gopay.BodyMap) {
|
||||
bm.Set("total", amount).
|
||||
bm.Set("total", params.TotalFee).
|
||||
Set("currency", "CNY")
|
||||
})
|
||||
|
||||
@@ -71,22 +80,22 @@ func (s *WechatPayService) PayUrlNative(outTradeNo string, amount int, subject s
|
||||
return wxRsp.Response.CodeUrl, nil
|
||||
}
|
||||
|
||||
func (s *WechatPayService) PayUrlH5(outTradeNo string, amount int, subject string, ip string) (string, error) {
|
||||
func (s *WechatPayService) PayUrlH5(params WechatPayParams) (string, error) {
|
||||
expire := time.Now().Add(10 * time.Minute).Format(time.RFC3339)
|
||||
// 初始化 BodyMap
|
||||
bm := make(gopay.BodyMap)
|
||||
bm.Set("appid", s.config.AppId).
|
||||
Set("mchid", s.config.MchId).
|
||||
Set("description", subject).
|
||||
Set("out_trade_no", outTradeNo).
|
||||
Set("description", params.Subject).
|
||||
Set("out_trade_no", params.OutTradeNo).
|
||||
Set("time_expire", expire).
|
||||
Set("notify_url", s.config.NotifyURL).
|
||||
Set("notify_url", params.NotifyURL).
|
||||
SetBodyMap("amount", func(bm gopay.BodyMap) {
|
||||
bm.Set("total", amount).
|
||||
bm.Set("total", params.TotalFee).
|
||||
Set("currency", "CNY")
|
||||
}).
|
||||
SetBodyMap("scene_info", func(bm gopay.BodyMap) {
|
||||
bm.Set("payer_client_ip", ip).
|
||||
bm.Set("payer_client_ip", params.ClientIP).
|
||||
SetBodyMap("h5_info", func(bm gopay.BodyMap) {
|
||||
bm.Set("type", "Wap")
|
||||
})
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
package sd
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"geekai/service/oss"
|
||||
"geekai/store"
|
||||
"geekai/store/model"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ServicePool struct {
|
||||
services []*Service
|
||||
taskQueue *store.RedisQueue
|
||||
notifyQueue *store.RedisQueue
|
||||
db *gorm.DB
|
||||
Clients *types.LMap[uint, *types.WsClient] // UserId => Client
|
||||
uploader *oss.UploaderManager
|
||||
levelDB *store.LevelDB
|
||||
}
|
||||
|
||||
func NewServicePool(db *gorm.DB, redisCli *redis.Client, manager *oss.UploaderManager, levelDB *store.LevelDB) *ServicePool {
|
||||
services := make([]*Service, 0)
|
||||
taskQueue := store.NewRedisQueue("StableDiffusion_Task_Queue", redisCli)
|
||||
notifyQueue := store.NewRedisQueue("StableDiffusion_Queue", redisCli)
|
||||
|
||||
return &ServicePool{
|
||||
taskQueue: taskQueue,
|
||||
notifyQueue: notifyQueue,
|
||||
services: services,
|
||||
db: db,
|
||||
Clients: types.NewLMap[uint, *types.WsClient](),
|
||||
uploader: manager,
|
||||
levelDB: levelDB,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ServicePool) InitServices(configs []types.StableDiffusionConfig) {
|
||||
// stop old service
|
||||
for _, s := range p.services {
|
||||
s.Stop()
|
||||
}
|
||||
p.services = make([]*Service, 0)
|
||||
|
||||
for k, config := range configs {
|
||||
if config.Enabled == false {
|
||||
continue
|
||||
}
|
||||
|
||||
// create sd service
|
||||
name := fmt.Sprintf(" sd-service-%d", k)
|
||||
service := NewService(name, config, p.taskQueue, p.notifyQueue, p.db, p.uploader, p.levelDB)
|
||||
// run sd service
|
||||
go func() {
|
||||
service.Run()
|
||||
}()
|
||||
|
||||
p.services = append(p.services, service)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
func (p *ServicePool) CheckTaskNotify() {
|
||||
go func() {
|
||||
logger.Info("Running Stable-Diffusion task notify checking ...")
|
||||
for {
|
||||
var message NotifyMessage
|
||||
err := p.notifyQueue.LPop(&message)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
client := p.Clients.Get(uint(message.UserId))
|
||||
if client == nil {
|
||||
continue
|
||||
}
|
||||
err = client.Send([]byte(message.Message))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// CheckTaskStatus 检查任务状态,自动删除过期或者失败的任务
|
||||
func (p *ServicePool) CheckTaskStatus() {
|
||||
go func() {
|
||||
logger.Info("Running Stable-Diffusion task status checking ...")
|
||||
for {
|
||||
var jobs []model.SdJob
|
||||
res := p.db.Where("progress < ?", 100).Find(&jobs)
|
||||
if res.Error != nil {
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, job := range jobs {
|
||||
// 5 分钟还没完成的任务直接删除
|
||||
if time.Now().Sub(job.CreatedAt) > time.Minute*5 || job.Progress == -1 {
|
||||
p.db.Delete(&job)
|
||||
var user model.User
|
||||
p.db.Where("id = ?", job.UserId).First(&user)
|
||||
// 退回绘图次数
|
||||
res = p.db.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power + ?", job.Power))
|
||||
if res.Error == nil && res.RowsAffected > 0 {
|
||||
p.db.Create(&model.PowerLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
Type: types.PowerConsume,
|
||||
Amount: job.Power,
|
||||
Balance: user.Power + job.Power,
|
||||
Mark: types.PowerAdd,
|
||||
Model: "stable-diffusion",
|
||||
Remark: fmt.Sprintf("任务失败,退回算力。任务ID:%s", job.TaskId),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
time.Sleep(time.Second * 5)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// HasAvailableService check if it has available mj service in pool
|
||||
func (p *ServicePool) HasAvailableService() bool {
|
||||
return len(p.services) > 0
|
||||
}
|
||||
@@ -10,95 +10,89 @@ package sd
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
logger2 "geekai/logger"
|
||||
"geekai/service"
|
||||
"geekai/service/oss"
|
||||
"geekai/store"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"strings"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"time"
|
||||
|
||||
"github.com/imroc/req/v3"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
// SD 绘画服务
|
||||
|
||||
type Service struct {
|
||||
httpClient *req.Client
|
||||
config types.StableDiffusionConfig
|
||||
taskQueue *store.RedisQueue
|
||||
notifyQueue *store.RedisQueue
|
||||
db *gorm.DB
|
||||
uploadManager *oss.UploaderManager
|
||||
name string // service name
|
||||
leveldb *store.LevelDB
|
||||
running bool // 运行状态
|
||||
wsService *service.WebsocketService
|
||||
}
|
||||
|
||||
func NewService(name string, config types.StableDiffusionConfig, taskQueue *store.RedisQueue, notifyQueue *store.RedisQueue, db *gorm.DB, manager *oss.UploaderManager, levelDB *store.LevelDB) *Service {
|
||||
config.ApiURL = strings.TrimRight(config.ApiURL, "/")
|
||||
func NewService(db *gorm.DB, manager *oss.UploaderManager, levelDB *store.LevelDB, redisCli *redis.Client, wsService *service.WebsocketService) *Service {
|
||||
return &Service{
|
||||
name: name,
|
||||
config: config,
|
||||
httpClient: req.C(),
|
||||
taskQueue: taskQueue,
|
||||
notifyQueue: notifyQueue,
|
||||
taskQueue: store.NewRedisQueue("StableDiffusion_Task_Queue", redisCli),
|
||||
notifyQueue: store.NewRedisQueue("StableDiffusion_Queue", redisCli),
|
||||
db: db,
|
||||
leveldb: levelDB,
|
||||
wsService: wsService,
|
||||
uploadManager: manager,
|
||||
running: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Run() {
|
||||
logger.Infof("Starting Stable-Diffusion job consumer for %s", s.name)
|
||||
for s.running {
|
||||
var task types.SdTask
|
||||
err := s.taskQueue.LPop(&task)
|
||||
if err != nil {
|
||||
logger.Errorf("taking task with error: %v", err)
|
||||
continue
|
||||
}
|
||||
logger.Infof("Starting Stable-Diffusion job consumer")
|
||||
go func() {
|
||||
for {
|
||||
var task types.SdTask
|
||||
err := s.taskQueue.LPop(&task)
|
||||
if err != nil {
|
||||
logger.Errorf("taking task with error: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// translate prompt
|
||||
if utils.HasChinese(task.Params.Prompt) {
|
||||
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, task.Params.Prompt))
|
||||
if err == nil {
|
||||
task.Params.Prompt = content
|
||||
} else {
|
||||
logger.Warnf("error with translate prompt: %v", err)
|
||||
// translate prompt
|
||||
if utils.HasChinese(task.Params.Prompt) {
|
||||
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, task.Params.Prompt), "gpt-4o-mini", 0)
|
||||
if err == nil {
|
||||
task.Params.Prompt = content
|
||||
} else {
|
||||
logger.Warnf("error with translate prompt: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// translate negative prompt
|
||||
if task.Params.NegPrompt != "" && utils.HasChinese(task.Params.NegPrompt) {
|
||||
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.Params.NegPrompt), "gpt-4o-mini", 0)
|
||||
if err == nil {
|
||||
task.Params.NegPrompt = content
|
||||
} else {
|
||||
logger.Warnf("error with translate prompt: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
logger.Infof("handle a new Stable-Diffusion task: %+v", task)
|
||||
err = s.Txt2Img(task)
|
||||
if err != nil {
|
||||
logger.Error("绘画任务执行失败:", err.Error())
|
||||
// update the task progress
|
||||
s.db.Model(&model.SdJob{Id: uint(task.Id)}).UpdateColumns(map[string]interface{}{
|
||||
"progress": service.FailTaskProgress,
|
||||
"err_msg": err.Error(),
|
||||
})
|
||||
// 通知前端,任务失败
|
||||
s.notifyQueue.RPush(service.NotifyMessage{ClientId: task.ClientId, UserId: task.UserId, JobId: task.Id, Message: service.TaskStatusFailed})
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// translate negative prompt
|
||||
if task.Params.NegPrompt != "" && utils.HasChinese(task.Params.NegPrompt) {
|
||||
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.Params.NegPrompt))
|
||||
if err == nil {
|
||||
task.Params.NegPrompt = content
|
||||
} else {
|
||||
logger.Warnf("error with translate prompt: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
logger.Infof("%s handle a new Stable-Diffusion task: %+v", s.name, task)
|
||||
err = s.Txt2Img(task)
|
||||
if err != nil {
|
||||
logger.Error("绘画任务执行失败:", err.Error())
|
||||
// update the task progress
|
||||
s.db.Model(&model.SdJob{Id: uint(task.Id)}).UpdateColumns(map[string]interface{}{
|
||||
"progress": -1,
|
||||
"err_msg": err.Error(),
|
||||
})
|
||||
// 通知前端,任务失败
|
||||
s.notifyQueue.RPush(NotifyMessage{UserId: task.UserId, JobId: task.Id, Message: Failed})
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Stop() {
|
||||
s.running = false
|
||||
}()
|
||||
}
|
||||
|
||||
// Txt2ImgReq 文生图请求实体
|
||||
@@ -130,9 +124,8 @@ type Txt2ImgResp struct {
|
||||
|
||||
// TaskProgressResp 任务进度响应实体
|
||||
type TaskProgressResp struct {
|
||||
Progress float64 `json:"progress"`
|
||||
EtaRelative float64 `json:"eta_relative"`
|
||||
CurrentImage string `json:"current_image"`
|
||||
Progress float64 `json:"progress"`
|
||||
EtaRelative float64 `json:"eta_relative"`
|
||||
}
|
||||
|
||||
// Txt2Img 文生图 API
|
||||
@@ -160,12 +153,19 @@ func (s *Service) Txt2Img(task types.SdTask) error {
|
||||
}
|
||||
var res Txt2ImgResp
|
||||
var errChan = make(chan error)
|
||||
apiURL := fmt.Sprintf("%s/sdapi/v1/txt2img", s.config.ApiURL)
|
||||
|
||||
var apiKey model.ApiKey
|
||||
err := s.db.Where("type", "sd").Where("enabled", true).Order("last_used_at ASC").First(&apiKey).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("no available Stable-Diffusion api key: %v", err)
|
||||
}
|
||||
|
||||
apiURL := fmt.Sprintf("%s/sdapi/v1/txt2img", apiKey.ApiURL)
|
||||
logger.Debugf("send image request to %s", apiURL)
|
||||
// send a request to sd api endpoint
|
||||
go func() {
|
||||
response, err := s.httpClient.R().
|
||||
SetHeader("Authorization", s.config.ApiKey).
|
||||
SetHeader("Authorization", apiKey.Value).
|
||||
SetBody(body).
|
||||
SetSuccessResult(&res).
|
||||
Post(apiURL)
|
||||
@@ -178,6 +178,10 @@ func (s *Service) Txt2Img(task types.SdTask) error {
|
||||
return
|
||||
}
|
||||
|
||||
// update the last used time
|
||||
apiKey.LastUsedAt = time.Now().Unix()
|
||||
s.db.Updates(&apiKey)
|
||||
|
||||
// 保存 Base64 图片
|
||||
imgURL, err := s.uploadManager.GetUploadHandler().PutBase64(res.Images[0])
|
||||
if err != nil {
|
||||
@@ -206,21 +210,15 @@ func (s *Service) Txt2Img(task types.SdTask) error {
|
||||
|
||||
// task finished
|
||||
s.db.Model(&model.SdJob{Id: uint(task.Id)}).UpdateColumn("progress", 100)
|
||||
s.notifyQueue.RPush(NotifyMessage{UserId: task.UserId, JobId: task.Id, Message: Finished})
|
||||
// 从 leveldb 中删除预览图片数据
|
||||
_ = s.leveldb.Delete(task.Params.TaskId)
|
||||
s.notifyQueue.RPush(service.NotifyMessage{ClientId: task.ClientId, UserId: task.UserId, JobId: task.Id, Message: service.TaskStatusFinished})
|
||||
return nil
|
||||
default:
|
||||
err, resp := s.checkTaskProgress()
|
||||
err, resp := s.checkTaskProgress(apiKey)
|
||||
// 更新任务进度
|
||||
if err == nil && resp.Progress > 0 {
|
||||
s.db.Model(&model.SdJob{Id: uint(task.Id)}).UpdateColumn("progress", int(resp.Progress*100))
|
||||
// 发送更新状态信号
|
||||
s.notifyQueue.RPush(NotifyMessage{UserId: task.UserId, JobId: task.Id, Message: Running})
|
||||
// 保存预览图片数据
|
||||
if resp.CurrentImage != "" {
|
||||
_ = s.leveldb.Put(task.Params.TaskId, resp.CurrentImage)
|
||||
}
|
||||
s.notifyQueue.RPush(service.NotifyMessage{ClientId: task.ClientId, UserId: task.UserId, JobId: task.Id, Message: service.TaskStatusRunning})
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
@@ -229,11 +227,11 @@ func (s *Service) Txt2Img(task types.SdTask) error {
|
||||
}
|
||||
|
||||
// 执行任务
|
||||
func (s *Service) checkTaskProgress() (error, *TaskProgressResp) {
|
||||
apiURL := fmt.Sprintf("%s/sdapi/v1/progress?skip_current_image=false", s.config.ApiURL)
|
||||
func (s *Service) checkTaskProgress(apiKey model.ApiKey) (error, *TaskProgressResp) {
|
||||
apiURL := fmt.Sprintf("%s/sdapi/v1/progress?skip_current_image=false", apiKey.ApiURL)
|
||||
var res TaskProgressResp
|
||||
response, err := s.httpClient.R().
|
||||
SetHeader("Authorization", s.config.ApiKey).
|
||||
SetHeader("Authorization", apiKey.Value).
|
||||
SetSuccessResult(&res).
|
||||
Get(apiURL)
|
||||
if err != nil {
|
||||
@@ -245,3 +243,52 @@ func (s *Service) checkTaskProgress() (error, *TaskProgressResp) {
|
||||
|
||||
return nil, &res
|
||||
}
|
||||
|
||||
func (s *Service) PushTask(task types.SdTask) {
|
||||
logger.Debugf("add a new MidJourney task to the task list: %+v", task)
|
||||
s.taskQueue.RPush(task)
|
||||
}
|
||||
|
||||
func (s *Service) CheckTaskNotify() {
|
||||
go func() {
|
||||
logger.Info("Running Stable-Diffusion task notify checking ...")
|
||||
for {
|
||||
var message service.NotifyMessage
|
||||
err := s.notifyQueue.LPop(&message)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
logger.Debugf("notify message: %+v", message)
|
||||
client := s.wsService.Clients.Get(message.ClientId)
|
||||
if client == nil {
|
||||
continue
|
||||
}
|
||||
utils.SendChannelMsg(client, types.ChSd, message.Message)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// CheckTaskStatus 检查任务状态,自动删除过期或者失败的任务
|
||||
func (s *Service) CheckTaskStatus() {
|
||||
go func() {
|
||||
logger.Info("Running Stable-Diffusion task status checking ...")
|
||||
for {
|
||||
var jobs []model.SdJob
|
||||
res := s.db.Where("progress < ?", 100).Find(&jobs)
|
||||
if res.Error != nil {
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, job := range jobs {
|
||||
// 5 分钟还没完成的任务标记为失败
|
||||
if time.Now().Sub(job.CreatedAt) > time.Minute*5 {
|
||||
job.Progress = service.FailTaskProgress
|
||||
job.ErrMsg = "任务超时"
|
||||
s.db.Updates(&job)
|
||||
}
|
||||
}
|
||||
time.Sleep(time.Second * 5)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package sd
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import logger2 "geekai/logger"
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
type NotifyMessage struct {
|
||||
UserId int `json:"user_id"`
|
||||
JobId int `json:"job_id"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
const (
|
||||
Running = "RUNNING"
|
||||
Finished = "FINISH"
|
||||
Failed = "FAIL"
|
||||
)
|
||||
@@ -29,7 +29,7 @@ func NewSmtpService(appConfig *types.AppConfig) *SmtpService {
|
||||
|
||||
func (s *SmtpService) SendVerifyCode(to string, code int) error {
|
||||
subject := fmt.Sprintf("%s 注册验证码", s.config.AppName)
|
||||
body := fmt.Sprintf("您正在注册 %s 账户,注册验证码为 %d,请不要告诉他人。如非本人操作,请忽略此邮件。", s.config.AppName, code)
|
||||
body := fmt.Sprintf("【%s】:您的验证码为 %d,请不要告诉他人。如非本人操作,请忽略此邮件。", s.config.AppName, code)
|
||||
|
||||
auth := smtp.PlainAuth("", s.config.From, s.config.Password, s.config.Host)
|
||||
if s.config.UseTls {
|
||||
|
||||
459
api/service/suno/service.go
Normal file
459
api/service/suno/service.go
Normal file
@@ -0,0 +1,459 @@
|
||||
package suno
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
logger2 "geekai/logger"
|
||||
"geekai/service"
|
||||
"geekai/service/oss"
|
||||
"geekai/store"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/imroc/req/v3"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
type Service struct {
|
||||
httpClient *req.Client
|
||||
db *gorm.DB
|
||||
uploadManager *oss.UploaderManager
|
||||
taskQueue *store.RedisQueue
|
||||
notifyQueue *store.RedisQueue
|
||||
wsService *service.WebsocketService
|
||||
clientIds map[string]string
|
||||
}
|
||||
|
||||
func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Client, wsService *service.WebsocketService) *Service {
|
||||
return &Service{
|
||||
httpClient: req.C().SetTimeout(time.Minute * 3),
|
||||
db: db,
|
||||
taskQueue: store.NewRedisQueue("Suno_Task_Queue", redisCli),
|
||||
notifyQueue: store.NewRedisQueue("Suno_Notify_Queue", redisCli),
|
||||
uploadManager: manager,
|
||||
wsService: wsService,
|
||||
clientIds: map[string]string{},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) PushTask(task types.SunoTask) {
|
||||
logger.Infof("add a new Suno task to the task list: %+v", task)
|
||||
s.taskQueue.RPush(task)
|
||||
}
|
||||
|
||||
func (s *Service) Run() {
|
||||
// 将数据库中未提交的人物加载到队列
|
||||
var jobs []model.SunoJob
|
||||
s.db.Where("task_id", "").Find(&jobs)
|
||||
for _, v := range jobs {
|
||||
s.PushTask(types.SunoTask{
|
||||
Id: v.Id,
|
||||
Channel: v.Channel,
|
||||
UserId: v.UserId,
|
||||
Type: v.Type,
|
||||
Title: v.Title,
|
||||
RefTaskId: v.RefTaskId,
|
||||
RefSongId: v.RefSongId,
|
||||
Prompt: v.Prompt,
|
||||
Tags: v.Tags,
|
||||
Model: v.ModelName,
|
||||
Instrumental: v.Instrumental,
|
||||
ExtendSecs: v.ExtendSecs,
|
||||
})
|
||||
}
|
||||
logger.Info("Starting Suno job consumer...")
|
||||
go func() {
|
||||
for {
|
||||
var task types.SunoTask
|
||||
err := s.taskQueue.LPop(&task)
|
||||
if err != nil {
|
||||
logger.Errorf("taking task with error: %v", err)
|
||||
continue
|
||||
}
|
||||
var r RespVo
|
||||
if task.Type == 3 && task.SongId != "" { // 歌曲拼接
|
||||
r, err = s.Merge(task)
|
||||
} else if task.Type == 4 && task.AudioURL != "" { // 上传歌曲
|
||||
r, err = s.Upload(task)
|
||||
} else { // 歌曲创作
|
||||
r, err = s.Create(task)
|
||||
}
|
||||
if err != nil {
|
||||
logger.Errorf("create task with error: %v", err)
|
||||
s.db.Model(&model.SunoJob{Id: task.Id}).UpdateColumns(map[string]interface{}{
|
||||
"err_msg": err.Error(),
|
||||
"progress": service.FailTaskProgress,
|
||||
})
|
||||
s.notifyQueue.RPush(service.NotifyMessage{ClientId: task.ClientId, UserId: task.UserId, JobId: int(task.Id), Message: service.TaskStatusFailed})
|
||||
continue
|
||||
}
|
||||
|
||||
// 更新任务信息
|
||||
s.db.Model(&model.SunoJob{Id: task.Id}).UpdateColumns(map[string]interface{}{
|
||||
"task_id": r.Data,
|
||||
"channel": r.Channel,
|
||||
})
|
||||
s.clientIds[r.Data] = task.ClientId
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
type RespVo struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data string `json:"data"`
|
||||
Channel string `json:"channel,omitempty"`
|
||||
}
|
||||
|
||||
func (s *Service) Create(task types.SunoTask) (RespVo, error) {
|
||||
// 读取 API KEY
|
||||
var apiKey model.ApiKey
|
||||
session := s.db.Session(&gorm.Session{}).Where("type", "suno").Where("enabled", true)
|
||||
if task.Channel != "" {
|
||||
session = session.Where("api_url", task.Channel)
|
||||
}
|
||||
tx := session.Order("last_used_at DESC").First(&apiKey)
|
||||
if tx.Error != nil {
|
||||
return RespVo{}, errors.New("no available API KEY for Suno")
|
||||
}
|
||||
|
||||
reqBody := map[string]interface{}{
|
||||
"task_id": task.RefTaskId,
|
||||
"continue_clip_id": task.RefSongId,
|
||||
"continue_at": task.ExtendSecs,
|
||||
"make_instrumental": task.Instrumental,
|
||||
}
|
||||
// 灵感模式
|
||||
if task.Type == 1 {
|
||||
reqBody["gpt_description_prompt"] = task.Prompt
|
||||
} else { // 自定义模式
|
||||
reqBody["prompt"] = task.Prompt
|
||||
reqBody["tags"] = task.Tags
|
||||
reqBody["mv"] = task.Model
|
||||
reqBody["title"] = task.Title
|
||||
}
|
||||
|
||||
var res RespVo
|
||||
apiURL := fmt.Sprintf("%s/suno/submit/music", apiKey.ApiURL)
|
||||
logger.Debugf("API URL: %s, request body: %+v", apiURL, reqBody)
|
||||
r, err := req.C().R().
|
||||
SetHeader("Authorization", "Bearer "+apiKey.Value).
|
||||
SetBody(reqBody).
|
||||
Post(apiURL)
|
||||
if err != nil {
|
||||
return RespVo{}, fmt.Errorf("请求 API 出错:%v", err)
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
err = json.Unmarshal(body, &res)
|
||||
if err != nil {
|
||||
return RespVo{}, fmt.Errorf("解析API数据失败:%v, %s", err, string(body))
|
||||
}
|
||||
|
||||
if res.Code != "success" {
|
||||
return RespVo{}, fmt.Errorf("API 返回失败:%s", res.Message)
|
||||
}
|
||||
// update the last_use_at for api key
|
||||
apiKey.LastUsedAt = time.Now().Unix()
|
||||
session.Updates(&apiKey)
|
||||
res.Channel = apiKey.ApiURL
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *Service) Merge(task types.SunoTask) (RespVo, error) {
|
||||
// 读取 API KEY
|
||||
var apiKey model.ApiKey
|
||||
session := s.db.Session(&gorm.Session{}).Where("type", "suno").Where("enabled", true)
|
||||
if task.Channel != "" {
|
||||
session = session.Where("api_url", task.Channel)
|
||||
}
|
||||
tx := session.Order("last_used_at DESC").First(&apiKey)
|
||||
if tx.Error != nil {
|
||||
return RespVo{}, errors.New("no available API KEY for Suno")
|
||||
}
|
||||
|
||||
reqBody := map[string]interface{}{
|
||||
"clip_id": task.SongId,
|
||||
"is_infill": false,
|
||||
}
|
||||
|
||||
var res RespVo
|
||||
apiURL := fmt.Sprintf("%s/suno/submit/concat", apiKey.ApiURL)
|
||||
logger.Debugf("API URL: %s, request body: %+v", apiURL, reqBody)
|
||||
r, err := req.C().R().
|
||||
SetHeader("Authorization", "Bearer "+apiKey.Value).
|
||||
SetBody(reqBody).
|
||||
Post(apiURL)
|
||||
if err != nil {
|
||||
return RespVo{}, fmt.Errorf("请求 API 出错:%v", err)
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
err = json.Unmarshal(body, &res)
|
||||
if err != nil {
|
||||
return RespVo{}, fmt.Errorf("解析API数据失败:%v, %s", err, string(body))
|
||||
}
|
||||
|
||||
if res.Code != "success" {
|
||||
return RespVo{}, fmt.Errorf("API 返回失败:%s", res.Message)
|
||||
}
|
||||
// update the last_use_at for api key
|
||||
apiKey.LastUsedAt = time.Now().Unix()
|
||||
session.Updates(&apiKey)
|
||||
res.Channel = apiKey.ApiURL
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *Service) Upload(task types.SunoTask) (RespVo, error) {
|
||||
// 读取 API KEY
|
||||
var apiKey model.ApiKey
|
||||
session := s.db.Session(&gorm.Session{}).Where("type", "suno").Where("enabled", true)
|
||||
if task.Channel != "" {
|
||||
session = session.Where("api_url", task.Channel)
|
||||
}
|
||||
tx := session.Order("last_used_at DESC").First(&apiKey)
|
||||
if tx.Error != nil {
|
||||
return RespVo{}, errors.New("no available API KEY for Suno")
|
||||
}
|
||||
|
||||
reqBody := map[string]interface{}{
|
||||
"url": task.AudioURL,
|
||||
}
|
||||
|
||||
var res RespVo
|
||||
apiURL := fmt.Sprintf("%s/suno/uploads/audio-url", apiKey.ApiURL)
|
||||
logger.Debugf("API URL: %s, request body: %+v", apiURL, reqBody)
|
||||
r, err := req.C().R().
|
||||
SetHeader("Authorization", "Bearer "+apiKey.Value).
|
||||
SetBody(reqBody).
|
||||
Post(apiURL)
|
||||
if err != nil {
|
||||
return RespVo{}, fmt.Errorf("请求 API 出错:%v", err)
|
||||
}
|
||||
|
||||
if r.StatusCode != 200 {
|
||||
return RespVo{}, fmt.Errorf("请求 API 出错:%d, %s", r.StatusCode, r.String())
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
err = json.Unmarshal(body, &res)
|
||||
if err != nil {
|
||||
return RespVo{}, fmt.Errorf("解析API数据失败:%v, %s", err, string(body))
|
||||
}
|
||||
|
||||
if res.Code != "success" {
|
||||
return RespVo{}, fmt.Errorf("API 返回失败:%s", res.Message)
|
||||
}
|
||||
// update the last_use_at for api key
|
||||
apiKey.LastUsedAt = time.Now().Unix()
|
||||
session.Updates(&apiKey)
|
||||
res.Channel = apiKey.ApiURL
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *Service) CheckTaskNotify() {
|
||||
go func() {
|
||||
logger.Info("Running Suno task notify checking ...")
|
||||
for {
|
||||
var message service.NotifyMessage
|
||||
err := s.notifyQueue.LPop(&message)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
logger.Debugf("notify message: %+v", message)
|
||||
logger.Debugf("client id: %+v", s.wsService.Clients)
|
||||
client := s.wsService.Clients.Get(message.ClientId)
|
||||
logger.Debugf("%+v", client)
|
||||
if client == nil {
|
||||
continue
|
||||
}
|
||||
utils.SendChannelMsg(client, types.ChSuno, message.Message)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Service) DownloadFiles() {
|
||||
go func() {
|
||||
var items []model.SunoJob
|
||||
for {
|
||||
res := s.db.Where("progress", 102).Find(&items)
|
||||
if res.Error != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, v := range items {
|
||||
// 下载图片和音频
|
||||
logger.Infof("try download cover image: %s", v.CoverURL)
|
||||
coverURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(v.CoverURL, true)
|
||||
if err != nil {
|
||||
logger.Errorf("download image with error: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Infof("try download audio: %s", v.AudioURL)
|
||||
audioURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(v.AudioURL, true)
|
||||
if err != nil {
|
||||
logger.Errorf("download audio with error: %v", err)
|
||||
continue
|
||||
}
|
||||
v.CoverURL = coverURL
|
||||
v.AudioURL = audioURL
|
||||
v.Progress = 100
|
||||
s.db.Updates(&v)
|
||||
s.notifyQueue.RPush(service.NotifyMessage{ClientId: s.clientIds[v.TaskId], UserId: v.UserId, JobId: int(v.Id), Message: service.TaskStatusFinished})
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// SyncTaskProgress 异步拉取任务
|
||||
func (s *Service) SyncTaskProgress() {
|
||||
go func() {
|
||||
var jobs []model.SunoJob
|
||||
for {
|
||||
res := s.db.Where("progress < ?", 100).Where("task_id <> ?", "").Find(&jobs)
|
||||
if res.Error != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, job := range jobs {
|
||||
task, err := s.QueryTask(job.TaskId, job.Channel)
|
||||
if err != nil {
|
||||
logger.Errorf("query task with error: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if task.Code != "success" {
|
||||
logger.Errorf("query task with error: %v", task.Message)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Debugf("task: %+v", task.Data.Status)
|
||||
// 任务完成,删除旧任务插入两条新任务
|
||||
if task.Data.Status == "SUCCESS" {
|
||||
var jobId = job.Id
|
||||
var flag = false
|
||||
tx := s.db.Begin()
|
||||
for _, v := range task.Data.Data {
|
||||
job.Id = 0
|
||||
job.Progress = 102 // 102 表示资源未下载完成
|
||||
job.Title = v.Title
|
||||
job.SongId = v.Id
|
||||
job.Duration = int(v.Metadata.Duration)
|
||||
job.Prompt = v.Metadata.Prompt
|
||||
job.Tags = v.Metadata.Tags
|
||||
job.ModelName = v.ModelName
|
||||
job.RawData = utils.JsonEncode(v)
|
||||
job.CoverURL = v.ImageLargeUrl
|
||||
job.AudioURL = v.AudioUrl
|
||||
|
||||
if err = tx.Create(&job).Error; err != nil {
|
||||
logger.Error("create job with error: %v", err)
|
||||
tx.Rollback()
|
||||
break
|
||||
}
|
||||
flag = true
|
||||
}
|
||||
|
||||
// 删除旧任务
|
||||
if flag {
|
||||
if err = tx.Delete(&model.SunoJob{}, "id = ?", jobId).Error; err != nil {
|
||||
logger.Error("create job with error: %v", err)
|
||||
tx.Rollback()
|
||||
continue
|
||||
}
|
||||
}
|
||||
tx.Commit()
|
||||
s.notifyQueue.RPush(service.NotifyMessage{ClientId: s.clientIds[job.TaskId], UserId: job.UserId, JobId: int(job.Id), Message: service.TaskStatusFinished})
|
||||
} else if task.Data.FailReason != "" {
|
||||
job.Progress = service.FailTaskProgress
|
||||
job.ErrMsg = task.Data.FailReason
|
||||
s.db.Updates(&job)
|
||||
s.notifyQueue.RPush(service.NotifyMessage{ClientId: s.clientIds[job.TaskId], UserId: job.UserId, JobId: int(job.Id), Message: service.TaskStatusFailed})
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
type QueryRespVo struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
TaskId string `json:"task_id"`
|
||||
Action string `json:"action"`
|
||||
Status string `json:"status"`
|
||||
FailReason string `json:"fail_reason"`
|
||||
SubmitTime int `json:"submit_time"`
|
||||
StartTime int `json:"start_time"`
|
||||
FinishTime int `json:"finish_time"`
|
||||
Progress string `json:"progress"`
|
||||
Data []struct {
|
||||
Id string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Status string `json:"status"`
|
||||
Metadata struct {
|
||||
Tags string `json:"tags"`
|
||||
Type string `json:"type"`
|
||||
Prompt string `json:"prompt"`
|
||||
Stream bool `json:"stream"`
|
||||
Duration float64 `json:"duration"`
|
||||
ErrorMessage interface{} `json:"error_message"`
|
||||
} `json:"metadata"`
|
||||
AudioUrl string `json:"audio_url"`
|
||||
ImageUrl string `json:"image_url"`
|
||||
VideoUrl string `json:"video_url"`
|
||||
ModelName string `json:"model_name"`
|
||||
DisplayName string `json:"display_name"`
|
||||
ImageLargeUrl string `json:"image_large_url"`
|
||||
MajorModelVersion string `json:"major_model_version"`
|
||||
} `json:"data"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
func (s *Service) QueryTask(taskId string, channel string) (QueryRespVo, error) {
|
||||
// 读取 API KEY
|
||||
var apiKey model.ApiKey
|
||||
err := s.db.Session(&gorm.Session{}).Where("type", "suno").
|
||||
Where("api_url", channel).
|
||||
Where("enabled", true).
|
||||
Order("last_used_at DESC").First(&apiKey).Error
|
||||
if err != nil {
|
||||
return QueryRespVo{}, errors.New("no available API KEY for Suno")
|
||||
}
|
||||
|
||||
apiURL := fmt.Sprintf("%s/suno/fetch/%s", apiKey.ApiURL, taskId)
|
||||
var res QueryRespVo
|
||||
r, err := req.C().R().SetHeader("Authorization", "Bearer "+apiKey.Value).Get(apiURL)
|
||||
|
||||
if err != nil {
|
||||
return QueryRespVo{}, fmt.Errorf("请求 API 失败:%v", err)
|
||||
}
|
||||
|
||||
defer r.Body.Close()
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
err = json.Unmarshal(body, &res)
|
||||
if err != nil {
|
||||
return QueryRespVo{}, fmt.Errorf("解析API数据失败:%v, %s", err, string(body))
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
@@ -1,4 +1,18 @@
|
||||
package service
|
||||
|
||||
const FailTaskProgress = 101
|
||||
const (
|
||||
TaskStatusRunning = "RUNNING"
|
||||
TaskStatusFinished = "FINISH"
|
||||
TaskStatusFailed = "FAIL"
|
||||
)
|
||||
|
||||
type NotifyMessage struct {
|
||||
UserId int `json:"user_id"`
|
||||
ClientId string `json:"client_id"`
|
||||
JobId int `json:"job_id"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
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 creative elements. Just output the final prompt word directly. Do not output any explanation lines. 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]"
|
||||
|
||||
83
api/service/user_service.go
Normal file
83
api/service/user_service.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"geekai/store/model"
|
||||
"gorm.io/gorm"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UserService struct {
|
||||
db *gorm.DB
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func NewUserService(db *gorm.DB) *UserService {
|
||||
return &UserService{db: db, lock: sync.Mutex{}}
|
||||
}
|
||||
|
||||
// IncreasePower 增加用户算力
|
||||
func (s *UserService) IncreasePower(userId int, power int, log model.PowerLog) error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
tx := s.db.Begin()
|
||||
err := tx.Model(&model.User{}).Where("id", userId).UpdateColumn("power", gorm.Expr("power + ?", power)).Error
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
var user model.User
|
||||
tx.Where("id", userId).First(&user)
|
||||
err = tx.Create(&model.PowerLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
Type: log.Type,
|
||||
Amount: power,
|
||||
Balance: user.Power,
|
||||
Mark: types.PowerAdd,
|
||||
Model: log.Model,
|
||||
Remark: log.Remark,
|
||||
CreatedAt: time.Now(),
|
||||
}).Error
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
tx.Commit()
|
||||
return nil
|
||||
}
|
||||
|
||||
// DecreasePower 减少用户算力
|
||||
func (s *UserService) DecreasePower(userId int, power int, log model.PowerLog) error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
tx := s.db.Begin()
|
||||
err := tx.Model(&model.User{}).Where("id", userId).UpdateColumn("power", gorm.Expr("power - ?", power)).Error
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("扣减算力失败:%v", err)
|
||||
}
|
||||
var user model.User
|
||||
tx.Where("id", userId).First(&user)
|
||||
err = tx.Create(&model.PowerLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
Type: log.Type,
|
||||
Amount: power,
|
||||
Balance: user.Power,
|
||||
Mark: types.PowerSub,
|
||||
Model: log.Model,
|
||||
Remark: log.Remark,
|
||||
CreatedAt: time.Now(),
|
||||
}).Error
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("记录算力日志失败:%v", err)
|
||||
}
|
||||
tx.Commit()
|
||||
return nil
|
||||
}
|
||||
345
api/service/video/luma.go
Normal file
345
api/service/video/luma.go
Normal file
@@ -0,0 +1,345 @@
|
||||
package video
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
logger2 "geekai/logger"
|
||||
"geekai/service"
|
||||
"geekai/service/oss"
|
||||
"geekai/store"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/imroc/req/v3"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
type Service struct {
|
||||
httpClient *req.Client
|
||||
db *gorm.DB
|
||||
uploadManager *oss.UploaderManager
|
||||
taskQueue *store.RedisQueue
|
||||
notifyQueue *store.RedisQueue
|
||||
wsService *service.WebsocketService
|
||||
clientIds map[uint]string
|
||||
}
|
||||
|
||||
func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Client, wsService *service.WebsocketService) *Service {
|
||||
return &Service{
|
||||
httpClient: req.C().SetTimeout(time.Minute * 3),
|
||||
db: db,
|
||||
taskQueue: store.NewRedisQueue("Video_Task_Queue", redisCli),
|
||||
notifyQueue: store.NewRedisQueue("Video_Notify_Queue", redisCli),
|
||||
wsService: wsService,
|
||||
uploadManager: manager,
|
||||
clientIds: map[uint]string{},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) PushTask(task types.VideoTask) {
|
||||
logger.Infof("add a new Video task to the task list: %+v", task)
|
||||
s.taskQueue.RPush(task)
|
||||
}
|
||||
|
||||
func (s *Service) Run() {
|
||||
// 将数据库中未提交的人物加载到队列
|
||||
var jobs []model.VideoJob
|
||||
s.db.Where("task_id", "").Where("progress", 0).Find(&jobs)
|
||||
for _, v := range jobs {
|
||||
var params types.VideoParams
|
||||
if err := utils.JsonDecode(v.Params, ¶ms); err != nil {
|
||||
logger.Errorf("unmarshal params failed: %v", err)
|
||||
continue
|
||||
}
|
||||
s.PushTask(types.VideoTask{
|
||||
Id: v.Id,
|
||||
Channel: v.Channel,
|
||||
UserId: v.UserId,
|
||||
Type: v.Type,
|
||||
TaskId: v.TaskId,
|
||||
Prompt: v.Prompt,
|
||||
Params: params,
|
||||
})
|
||||
}
|
||||
logger.Info("Starting Video job consumer...")
|
||||
go func() {
|
||||
for {
|
||||
var task types.VideoTask
|
||||
err := s.taskQueue.LPop(&task)
|
||||
if err != nil {
|
||||
logger.Errorf("taking task with error: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// translate prompt
|
||||
if utils.HasChinese(task.Prompt) {
|
||||
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.Prompt), "gpt-4o-mini", 0)
|
||||
if err == nil {
|
||||
task.Prompt = content
|
||||
} else {
|
||||
logger.Warnf("error with translate prompt: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if task.ClientId != "" {
|
||||
s.clientIds[task.Id] = task.ClientId
|
||||
}
|
||||
|
||||
var r LumaRespVo
|
||||
r, err = s.LumaCreate(task)
|
||||
if err != nil {
|
||||
logger.Errorf("create task with error: %v", err)
|
||||
err = s.db.Model(&model.VideoJob{Id: task.Id}).UpdateColumns(map[string]interface{}{
|
||||
"err_msg": err.Error(),
|
||||
"progress": service.FailTaskProgress,
|
||||
"cover_url": "/images/failed.jpg",
|
||||
}).Error
|
||||
if err != nil {
|
||||
logger.Errorf("update task with error: %v", err)
|
||||
}
|
||||
s.notifyQueue.RPush(service.NotifyMessage{ClientId: task.ClientId, UserId: task.UserId, JobId: int(task.Id), Message: service.TaskStatusFailed})
|
||||
continue
|
||||
}
|
||||
|
||||
// 更新任务信息
|
||||
err = s.db.Model(&model.VideoJob{Id: task.Id}).UpdateColumns(map[string]interface{}{
|
||||
"task_id": r.Id,
|
||||
"channel": r.Channel,
|
||||
"prompt_ext": r.Prompt,
|
||||
}).Error
|
||||
if err != nil {
|
||||
logger.Errorf("update task with error: %v", err)
|
||||
s.PushTask(task)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
type LumaRespVo struct {
|
||||
Id string `json:"id"`
|
||||
Prompt string `json:"prompt"`
|
||||
State string `json:"state"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Video interface{} `json:"video"`
|
||||
Liked interface{} `json:"liked"`
|
||||
EstimateWaitSeconds interface{} `json:"estimate_wait_seconds"`
|
||||
Channel string `json:"channel,omitempty"`
|
||||
}
|
||||
|
||||
func (s *Service) LumaCreate(task types.VideoTask) (LumaRespVo, error) {
|
||||
// 读取 API KEY
|
||||
var apiKey model.ApiKey
|
||||
session := s.db.Session(&gorm.Session{}).Where("type", "luma").Where("enabled", true)
|
||||
if task.Channel != "" {
|
||||
session = session.Where("api_url", task.Channel)
|
||||
}
|
||||
tx := session.Order("last_used_at DESC").First(&apiKey)
|
||||
if tx.Error != nil {
|
||||
return LumaRespVo{}, errors.New("no available API KEY for Luma")
|
||||
}
|
||||
|
||||
reqBody := map[string]interface{}{
|
||||
"user_prompt": task.Prompt,
|
||||
"expand_prompt": task.Params.PromptOptimize,
|
||||
"loop": task.Params.Loop,
|
||||
"image_url": task.Params.StartImgURL,
|
||||
"image_end_url": task.Params.EndImgURL,
|
||||
}
|
||||
var res LumaRespVo
|
||||
apiURL := fmt.Sprintf("%s/luma/generations", apiKey.ApiURL)
|
||||
logger.Debugf("API URL: %s, request body: %+v", apiURL, reqBody)
|
||||
r, err := req.C().R().
|
||||
SetHeader("Authorization", "Bearer "+apiKey.Value).
|
||||
SetBody(reqBody).
|
||||
Post(apiURL)
|
||||
if err != nil {
|
||||
return LumaRespVo{}, fmt.Errorf("请求 API 出错:%v", err)
|
||||
}
|
||||
|
||||
if r.StatusCode != 200 && r.StatusCode != 201 {
|
||||
return LumaRespVo{}, fmt.Errorf("请求 API 出错:%d, %s", r.StatusCode, r.String())
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
err = json.Unmarshal(body, &res)
|
||||
if err != nil {
|
||||
return LumaRespVo{}, fmt.Errorf("解析API数据失败:%v, %s", err, string(body))
|
||||
}
|
||||
|
||||
// update the last_use_at for api key
|
||||
apiKey.LastUsedAt = time.Now().Unix()
|
||||
session.Updates(&apiKey)
|
||||
res.Channel = apiKey.ApiURL
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *Service) CheckTaskNotify() {
|
||||
go func() {
|
||||
logger.Info("Running Suno task notify checking ...")
|
||||
for {
|
||||
var message service.NotifyMessage
|
||||
err := s.notifyQueue.LPop(&message)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
logger.Debugf("Receive notify message: %+v", message)
|
||||
client := s.wsService.Clients.Get(message.ClientId)
|
||||
if client == nil {
|
||||
continue
|
||||
}
|
||||
utils.SendChannelMsg(client, types.ChLuma, message.Message)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Service) DownloadFiles() {
|
||||
go func() {
|
||||
var items []model.VideoJob
|
||||
for {
|
||||
res := s.db.Where("progress", 102).Find(&items)
|
||||
if res.Error != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, v := range items {
|
||||
if v.WaterURL == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Infof("try download video: %s", v.WaterURL)
|
||||
videoURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(v.WaterURL, true)
|
||||
if err != nil {
|
||||
logger.Errorf("download video with error: %v", err)
|
||||
continue
|
||||
}
|
||||
logger.Infof("download video success: %s", videoURL)
|
||||
v.WaterURL = videoURL
|
||||
|
||||
if v.VideoURL != "" {
|
||||
logger.Infof("try download no water video: %s", v.VideoURL)
|
||||
videoURL, err = s.uploadManager.GetUploadHandler().PutUrlFile(v.VideoURL, true)
|
||||
if err != nil {
|
||||
logger.Errorf("download video with error: %v", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
logger.Info("download no water video success: %s", videoURL)
|
||||
v.VideoURL = videoURL
|
||||
v.Progress = 100
|
||||
s.db.Updates(&v)
|
||||
s.notifyQueue.RPush(service.NotifyMessage{ClientId: s.clientIds[v.Id], UserId: v.UserId, JobId: int(v.Id), Message: service.TaskStatusFinished})
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// SyncTaskProgress 异步拉取任务
|
||||
func (s *Service) SyncTaskProgress() {
|
||||
go func() {
|
||||
var jobs []model.VideoJob
|
||||
for {
|
||||
res := s.db.Where("progress < ?", 100).Where("task_id <> ?", "").Find(&jobs)
|
||||
if res.Error != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, job := range jobs {
|
||||
task, err := s.QueryLumaTask(job.TaskId, job.Channel)
|
||||
if err != nil {
|
||||
logger.Errorf("query task with error: %v", err)
|
||||
// 更新任务信息
|
||||
s.db.Model(&model.VideoJob{Id: job.Id}).UpdateColumns(map[string]interface{}{
|
||||
"progress": service.FailTaskProgress, // 102 表示资源未下载完成,
|
||||
"err_msg": err.Error(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Debugf("task: %+v", task)
|
||||
if task.State == "completed" { // 更新任务信息
|
||||
data := map[string]interface{}{
|
||||
"progress": 102, // 102 表示资源未下载完成,
|
||||
"water_url": task.Video.Url,
|
||||
"raw_data": utils.JsonEncode(task),
|
||||
"prompt_ext": task.Prompt,
|
||||
}
|
||||
if task.Video.DownloadUrl != "" {
|
||||
data["video_url"] = task.Video.DownloadUrl
|
||||
}
|
||||
err = s.db.Model(&model.VideoJob{Id: job.Id}).UpdateColumns(data).Error
|
||||
if err != nil {
|
||||
logger.Errorf("更新数据库失败:%v", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
type LumaTaskVo struct {
|
||||
Id string `json:"id"`
|
||||
Liked interface{} `json:"liked"`
|
||||
State string `json:"state"`
|
||||
Video struct {
|
||||
Url string `json:"url"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
DownloadUrl string `json:"download_url"`
|
||||
} `json:"video"`
|
||||
Prompt string `json:"prompt"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
EstimateWaitSeconds interface{} `json:"estimate_wait_seconds"`
|
||||
}
|
||||
|
||||
func (s *Service) QueryLumaTask(taskId string, channel string) (LumaTaskVo, error) {
|
||||
// 读取 API KEY
|
||||
var apiKey model.ApiKey
|
||||
err := s.db.Session(&gorm.Session{}).Where("type", "luma").
|
||||
Where("api_url", channel).
|
||||
Where("enabled", true).
|
||||
Order("last_used_at DESC").First(&apiKey).Error
|
||||
if err != nil {
|
||||
return LumaTaskVo{}, errors.New("no available API KEY for Luma")
|
||||
}
|
||||
|
||||
apiURL := fmt.Sprintf("%s/luma/generations/%s", apiKey.ApiURL, taskId)
|
||||
var res LumaTaskVo
|
||||
r, err := req.C().R().SetHeader("Authorization", "Bearer "+apiKey.Value).Get(apiURL)
|
||||
|
||||
if err != nil {
|
||||
return LumaTaskVo{}, fmt.Errorf("请求 API 失败:%v", err)
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
if r.StatusCode != 200 {
|
||||
return LumaTaskVo{}, fmt.Errorf("API 返回失败:%v", r.String())
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
err = json.Unmarshal(body, &res)
|
||||
if err != nil {
|
||||
return LumaTaskVo{}, fmt.Errorf("解析API数据失败:%v, %s", err, string(body))
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
13
api/service/ws_service.go
Normal file
13
api/service/ws_service.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package service
|
||||
|
||||
import "geekai/core/types"
|
||||
|
||||
type WebsocketService struct {
|
||||
Clients *types.LMap[string, *types.WsClient] // clientId => Client
|
||||
}
|
||||
|
||||
func NewWebsocketService() *WebsocketService {
|
||||
return &WebsocketService{
|
||||
Clients: types.NewLMap[string, *types.WsClient](),
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
package wx
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
logger2 "geekai/logger"
|
||||
"geekai/store/model"
|
||||
"github.com/eatmoreapple/openwechat"
|
||||
"github.com/skip2/go-qrcode"
|
||||
"gorm.io/gorm"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// 微信收款机器人
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
type Bot struct {
|
||||
bot *openwechat.Bot
|
||||
token string
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewWeChatBot(db *gorm.DB) *Bot {
|
||||
bot := openwechat.DefaultBot(openwechat.Desktop)
|
||||
return &Bot{
|
||||
bot: bot,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bot) Run() error {
|
||||
logger.Info("Starting WeChat Bot...")
|
||||
|
||||
// set message handler
|
||||
b.bot.MessageHandler = func(msg *openwechat.Message) {
|
||||
b.messageHandler(msg)
|
||||
}
|
||||
// scan code login callback
|
||||
b.bot.UUIDCallback = b.qrCodeCallBack
|
||||
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
|
||||
}
|
||||
|
||||
logger.Info("微信登录成功!")
|
||||
return nil
|
||||
}
|
||||
|
||||
// message handler
|
||||
func (b *Bot) messageHandler(msg *openwechat.Message) {
|
||||
sender, err := msg.Sender()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 只处理微信支付的推送消息
|
||||
if sender.NickName == "微信支付" ||
|
||||
msg.MsgType == openwechat.MsgTypeApp ||
|
||||
msg.AppMsgType == openwechat.AppMsgTypeUrl {
|
||||
// 解析支付金额
|
||||
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 item.Id > 0 {
|
||||
logger.Error("当前交易 ID 己经存在!")
|
||||
return
|
||||
}
|
||||
|
||||
res = b.db.Create(&model.Reward{
|
||||
TxId: transaction.TransId,
|
||||
Amount: transaction.Amount,
|
||||
Remark: transaction.Remark,
|
||||
Status: false,
|
||||
})
|
||||
if res.Error != nil {
|
||||
logger.Errorf("交易保存失败: %v", res.Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bot) qrCodeCallBack(uuid string) {
|
||||
logger.Info("请使用微信扫描下面二维码登录")
|
||||
q, _ := qrcode.New("https://login.weixin.qq.com/l/"+uuid, qrcode.Medium)
|
||||
logger.Info(q.ToString(true))
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
package wx
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Message 转账消息
|
||||
type Message struct {
|
||||
Des string
|
||||
Url string
|
||||
}
|
||||
|
||||
// Transaction 解析后的交易信息
|
||||
type Transaction struct {
|
||||
TransId string `json:"trans_id"` // 微信转账交易 ID
|
||||
Amount float64 `json:"amount"` // 微信转账交易金额
|
||||
Remark string `json:"remark"` // 转账备注
|
||||
}
|
||||
|
||||
// 解析微信转账消息
|
||||
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=") || strings.Contains(value, "?id=") {
|
||||
message.Url = value
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 兼容旧版消息记录
|
||||
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.Des, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
// 解析收款金额
|
||||
prefix := "收款金额¥"
|
||||
if strings.HasPrefix(line, prefix) {
|
||||
if value, err := strconv.ParseFloat(line[len(prefix):], 64); err == nil {
|
||||
tx.Amount = value
|
||||
continue
|
||||
}
|
||||
}
|
||||
// 解析收款备注
|
||||
prefix = "付款方备注"
|
||||
if strings.HasPrefix(line, prefix) {
|
||||
tx.Remark = line[len(prefix):]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 解析交易 ID
|
||||
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
|
||||
}
|
||||
@@ -81,54 +81,6 @@ func (e *XXLJobExecutor) ClearOrders(cxt context.Context, param *xxl.RunReq) (ms
|
||||
// 自动将 VIP 会员的算力补充到每月赠送的最大值
|
||||
func (e *XXLJobExecutor) ResetVipPower(cxt context.Context, param *xxl.RunReq) (msg string) {
|
||||
logger.Info("开始进行月底账号盘点...")
|
||||
var users []model.User
|
||||
res := e.db.Where("vip", 1).Where("status", 1).Find(&users)
|
||||
if res.Error != nil {
|
||||
return "No vip users found"
|
||||
}
|
||||
|
||||
var sysConfig model.Config
|
||||
res = e.db.Where("marker", "system").First(&sysConfig)
|
||||
if res.Error != nil {
|
||||
return "error with get system config: " + res.Error.Error()
|
||||
}
|
||||
|
||||
var config types.SystemConfig
|
||||
err := utils.JsonDecode(sysConfig.Config, &config)
|
||||
if err != nil {
|
||||
return "error with decode system config: " + err.Error()
|
||||
}
|
||||
|
||||
for _, u := range users {
|
||||
// 处理过期的 VIP
|
||||
if u.ExpiredTime > 0 && u.ExpiredTime <= time.Now().Unix() {
|
||||
u.Vip = false
|
||||
e.db.Model(&model.User{}).Where("id", u.Id).UpdateColumn("vip", false)
|
||||
continue
|
||||
}
|
||||
if u.Power < config.VipMonthPower {
|
||||
power := config.VipMonthPower - u.Power
|
||||
// update user
|
||||
tx := e.db.Model(&model.User{}).Where("id", u.Id).UpdateColumn("power", gorm.Expr("power + ?", power))
|
||||
// 记录算力变动日志
|
||||
if tx.Error == nil {
|
||||
var user model.User
|
||||
e.db.Where("id", u.Id).First(&user)
|
||||
e.db.Create(&model.PowerLog{
|
||||
UserId: u.Id,
|
||||
Username: u.Username,
|
||||
Type: types.PowerRecharge,
|
||||
Amount: power,
|
||||
Mark: types.PowerAdd,
|
||||
Balance: user.Power,
|
||||
Model: "系统盘点",
|
||||
Remark: fmt.Sprintf("VIP会员每月算力派发,:%d", config.VipMonthPower),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.Info("月底盘点完成!")
|
||||
return "success"
|
||||
}
|
||||
|
||||
|
||||
@@ -29,15 +29,9 @@ func NewLevelDB() (*LevelDB, error) {
|
||||
}
|
||||
|
||||
func (db *LevelDB) Put(key string, value interface{}) error {
|
||||
var byteData []byte
|
||||
if v, ok := value.(string); ok {
|
||||
byteData = []byte(v)
|
||||
} else {
|
||||
b, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
byteData = b
|
||||
byteData, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.driver.Put([]byte(key), byteData, nil)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package model
|
||||
// ApiKey OpenAI API 模型
|
||||
type ApiKey struct {
|
||||
BaseModel
|
||||
Platform string
|
||||
Name string
|
||||
Type string // 用途 chat => 聊天,img => 绘图
|
||||
Value string // API Key 的值
|
||||
|
||||
12
api/store/model/app_type.go
Normal file
12
api/store/model/app_type.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type AppType struct {
|
||||
Id uint `gorm:"primarykey"`
|
||||
Name string
|
||||
Icon string
|
||||
Enabled bool
|
||||
SortNum int
|
||||
CreatedAt time.Time
|
||||
}
|
||||
@@ -4,16 +4,17 @@ import "gorm.io/gorm"
|
||||
|
||||
type ChatMessage struct {
|
||||
BaseModel
|
||||
ChatId string // 会话 ID
|
||||
UserId uint // 用户 ID
|
||||
RoleId uint // 角色 ID
|
||||
Model string // AI模型
|
||||
Type string
|
||||
Icon string
|
||||
Tokens int
|
||||
Content string
|
||||
UseContext bool // 是否可以作为聊天上下文
|
||||
DeletedAt gorm.DeletedAt
|
||||
ChatId string // 会话 ID
|
||||
UserId uint // 用户 ID
|
||||
RoleId uint // 角色 ID
|
||||
Model string // AI模型
|
||||
Type string
|
||||
Icon string
|
||||
Tokens int
|
||||
TotalTokens int // 总 token 消耗
|
||||
Content string
|
||||
UseContext bool // 是否可以作为聊天上下文
|
||||
DeletedAt gorm.DeletedAt
|
||||
}
|
||||
|
||||
func (ChatMessage) TableName() string {
|
||||
|
||||
@@ -2,7 +2,6 @@ package model
|
||||
|
||||
type ChatModel struct {
|
||||
BaseModel
|
||||
Platform string
|
||||
Name string
|
||||
Value string // API Key 的值
|
||||
SortNum int
|
||||
|
||||
@@ -2,6 +2,7 @@ package model
|
||||
|
||||
type ChatRole struct {
|
||||
BaseModel
|
||||
Tid int
|
||||
Key string `gorm:"column:marker;unique"` // 角色唯一标识
|
||||
Name string // 角色名称
|
||||
Context string `gorm:"column:context_json"` // 角色语料信息 json
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user