merge v4.1.2

This commit is contained in:
RockYang 2024-11-27 15:00:02 +08:00
commit e04a48623d
111 changed files with 3072 additions and 2940 deletions

View File

@ -1,4 +1,15 @@
# 更新日志 # 更新日志
## v4.1.2
* Bug修复修复思维导图页面获取模型失败的问题
* 功能优化优化MJ,SD,DALL-E 任务列表页面,显示失败任务的错误信息,删除失败任务可以恢复扣减算力
* Bug修复修复后台拖动排序组件 Bug
* 功能优化:更新数据库失败时候显示具体的的报错信息
* Bug修复修复管理后台对话详情页内容显示异常问题
* 功能优化:管理后台新增清空所有未支付订单的功能
* 功能优化:给会话信息和系统配置数据加上缓存功能,减少 http 请求
* 功能新增:移除微信机器人收款功能,增加卡密功能,支持用户使用卡密兑换算力
## v4.1.1 ## v4.1.1
* Bug修复修复 GPT 模型 function call 调用后没有输出的问题 * Bug修复修复 GPT 模型 function call 调用后没有输出的问题
* 功能新增:允许获取 License 授权用户可以自定义版权信息 * 功能新增:允许获取 License 授权用户可以自定义版权信息

View File

@ -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" MysqlDns = "root:12345678@tcp(172.22.11.200:3307)/chatgpt_plus?charset=utf8mb4&collation=utf8mb4_unicode_ci&parseTime=True&loc=Local"
StaticDir = "./static" # 静态资源的目录 StaticDir = "./static" # 静态资源的目录
StaticUrl = "/static" # 静态资源访问 URL StaticUrl = "/static" # 静态资源访问 URL
AesEncryptKey = ""
WeChatBot = false
TikaHost = "http://tika:9998" TikaHost = "http://tika:9998"
[Session] [Session]
@ -65,23 +63,6 @@ TikaHost = "http://tika:9998"
SubDir = "" SubDir = ""
Domain = "" 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如果你没有启用支付服务则该服务也无需启动 [XXLConfig] # xxl-job 配置,需要你部署 XXL-JOB 定时任务工具,用来定期清理未支付订单和清理过期 VIP如果你没有启用支付服务则该服务也无需启动
Enabled = false # 是否启用 XXL JOB 服务 Enabled = false # 是否启用 XXL JOB 服务
ServerAddr = "http://172.22.11.47:8080/xxl-job-admin" # xxl-job-admin 管理地址 ServerAddr = "http://172.22.11.47:8080/xxl-job-admin" # xxl-job-admin 管理地址

View File

@ -225,7 +225,7 @@ func needLogin(c *gin.Context) bool {
c.Request.URL.Path == "/api/payment/doPay" || c.Request.URL.Path == "/api/payment/doPay" ||
c.Request.URL.Path == "/api/payment/payWays" || c.Request.URL.Path == "/api/payment/payWays" ||
c.Request.URL.Path == "/api/suno/client" || c.Request.URL.Path == "/api/suno/client" ||
c.Request.URL.Path == "/api/suno/Detail" || c.Request.URL.Path == "/api/suno/detail" ||
c.Request.URL.Path == "/api/suno/play" || c.Request.URL.Path == "/api/suno/play" ||
strings.HasPrefix(c.Request.URL.Path, "/api/test") || strings.HasPrefix(c.Request.URL.Path, "/api/test") ||
strings.HasPrefix(c.Request.URL.Path, "/api/user/clogin") || strings.HasPrefix(c.Request.URL.Path, "/api/user/clogin") ||

View File

@ -38,7 +38,6 @@ func NewDefaultConfig() *types.AppConfig {
BasePath: "./static/upload", BasePath: "./static/upload",
}, },
}, },
WeChatBot: false,
AlipayConfig: types.AlipayConfig{Enabled: false, SandBox: false}, AlipayConfig: types.AlipayConfig{Enabled: false, SandBox: false},
} }
} }

View File

@ -61,7 +61,6 @@ type ChatSession struct {
type ChatModel struct { type ChatModel struct {
Id uint `json:"id"` Id uint `json:"id"`
Platform string `json:"platform"`
Name string `json:"name"` Name string `json:"name"`
Value string `json:"value"` Value string `json:"value"`
Power int `json:"power"` Power int `json:"power"`
@ -91,7 +90,7 @@ const (
PowerConsume = PowerType(2) // 消费 PowerConsume = PowerType(2) // 消费
PowerRefund = PowerType(3) // 任务SD,MJ执行失败退款 PowerRefund = PowerType(3) // 任务SD,MJ执行失败退款
PowerInvite = PowerType(4) // 邀请奖励 PowerInvite = PowerType(4) // 邀请奖励
PowerReward = PowerType(5) // 众筹 PowerRedeem = PowerType(5) // 众筹
PowerGift = PowerType(6) // 系统赠送 PowerGift = PowerType(6) // 系统赠送
) )
@ -103,8 +102,8 @@ func (t PowerType) String() string {
return "消费" return "消费"
case PowerRefund: case PowerRefund:
return "退款" return "退款"
case PowerReward: case PowerRedeem:
return "众筹" return "兑换"
} }
return "其他" return "其他"

View File

@ -24,10 +24,6 @@ type AppConfig struct {
ApiConfig ApiConfig // ChatPlus API authorization configs ApiConfig ApiConfig // ChatPlus API authorization configs
SMS SMSConfig // send mobile message config SMS SMSConfig // send mobile message config
OSS OSSConfig // OSS config OSS OSSConfig // OSS config
MjProxyConfigs []MjProxyConfig // MJ proxy config
MjPlusConfigs []MjPlusConfig // MJ plus config
WeChatBot bool // 是否启用微信机器人
SdConfigs []StableDiffusionConfig // sd AI draw service pool
XXLConfig XXLConfig XXLConfig XXLConfig
AlipayConfig AlipayConfig // 支付宝支付渠道配置 AlipayConfig AlipayConfig // 支付宝支付渠道配置
@ -53,27 +49,6 @@ type ApiConfig struct {
Token string 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 { type AlipayConfig struct {
Enabled bool // 是否启用该支付通道 Enabled bool // 是否启用该支付通道
SandBox bool // 是否沙盒环境 SandBox bool // 是否沙盒环境
@ -168,10 +143,6 @@ type SystemConfig struct {
RegisterWays []string `json:"register_ways,omitempty"` // 注册方式支持手机mobile邮箱注册email账号密码注册 RegisterWays []string `json:"register_ways,omitempty"` // 注册方式支持手机mobile邮箱注册email账号密码注册
EnabledRegister bool `json:"enabled_register,omitempty"` // 是否开放注册 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"` //订单支付超时时间 OrderPayTimeout int `json:"order_pay_timeout,omitempty"` //订单支付超时时间
VipInfoText string `json:"vip_info_text,omitempty"` // 会员页面充值说明 VipInfoText string `json:"vip_info_text,omitempty"` // 会员页面充值说明
DefaultModels []int `json:"default_models,omitempty"` // 默认开通的 AI 模型 DefaultModels []int `json:"default_models,omitempty"` // 默认开通的 AI 模型
@ -188,8 +159,10 @@ type SystemConfig struct {
ContextDeep int `json:"context_deep,omitempty"` ContextDeep int `json:"context_deep,omitempty"`
SdNegPrompt string `json:"sd_neg_prompt"` // SD 默认反向提示词 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"` // 首页显示的导航菜单 IndexNavs []int `json:"index_navs"` // 首页显示的导航菜单
Copyright string `json:"copyright"` // 版权信息 Copyright string `json:"copyright"` // 版权信息
MarkMapText string `json:"mark_map_text"` // 思维导入的默认文本
} }

View File

@ -27,7 +27,6 @@ type MjTask struct {
Id uint `json:"id"` Id uint `json:"id"`
TaskId string `json:"task_id"` TaskId string `json:"task_id"`
ImgArr []string `json:"img_arr"` ImgArr []string `json:"img_arr"`
ChannelId string `json:"channel_id"`
Type TaskType `json:"type"` Type TaskType `json:"type"`
UserId int `json:"user_id"` UserId int `json:"user_id"`
Prompt string `json:"prompt,omitempty"` Prompt string `json:"prompt,omitempty"`
@ -37,6 +36,8 @@ type MjTask struct {
MessageId string `json:"message_id,omitempty"` MessageId string `json:"message_id,omitempty"`
MessageHash string `json:"message_hash,omitempty"` MessageHash string `json:"message_hash,omitempty"`
RetryCount int `json:"retry_count"` RetryCount int `json:"retry_count"`
ChannelId string `json:"channel_id"` // 渠道ID用来区分是哪个渠道创建的任务一个任务的 create 和 action 操作必须要再同一个渠道
Mode string `json:"mode"` // 绘画模式relax, fast, turbo
} }
type SdTask struct { type SdTask struct {

View File

@ -8,7 +8,6 @@ require (
github.com/BurntSushi/toml v1.1.0 github.com/BurntSushi/toml v1.1.0
github.com/aliyun/alibaba-cloud-sdk-go v1.62.405 github.com/aliyun/alibaba-cloud-sdk-go v1.62.405
github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible 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/gin-gonic/gin v1.9.1
github.com/go-redis/redis/v8 v8.11.5 github.com/go-redis/redis/v8 v8.11.5
github.com/golang-jwt/jwt/v5 v5.0.0 github.com/golang-jwt/jwt/v5 v5.0.0
@ -30,7 +29,6 @@ require (
github.com/go-pay/gopay v1.5.101 github.com/go-pay/gopay v1.5.101
github.com/google/go-tika v0.3.1 github.com/google/go-tika v0.3.1
github.com/microcosm-cc/bluemonday v1.0.26 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/shirou/gopsutil v3.21.11+incompatible
github.com/shopspring/decimal v1.3.1 github.com/shopspring/decimal v1.3.1
github.com/syndtr/goleveldb v1.0.0 github.com/syndtr/goleveldb v1.0.0
@ -45,7 +43,6 @@ require (
github.com/go-pay/util v0.0.2 // indirect github.com/go-pay/util v0.0.2 // indirect
github.com/go-pay/xlog v0.0.2 // indirect github.com/go-pay/xlog v0.0.2 // indirect
github.com/go-pay/xtime 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/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect
github.com/gorilla/css v1.0.0 // indirect github.com/gorilla/css v1.0.0 // indirect
github.com/tklauser/go-sysconf v0.3.13 // indirect github.com/tklauser/go-sysconf v0.3.13 // indirect

View File

@ -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/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 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 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.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 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 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/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 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 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.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 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 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.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 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/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 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
@ -267,7 +261,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/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 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= 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 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= 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= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@ -323,7 +316,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.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.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.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.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 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=

View File

@ -8,6 +8,7 @@ package admin
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import ( import (
"fmt"
"geekai/core" "geekai/core"
"geekai/core/types" "geekai/core/types"
"geekai/handler" "geekai/handler"
@ -53,17 +54,16 @@ func (h *ApiKeyHandler) Save(c *gin.Context) {
apiKey.Enabled = data.Enabled apiKey.Enabled = data.Enabled
apiKey.ProxyURL = data.ProxyURL apiKey.ProxyURL = data.ProxyURL
apiKey.Name = data.Name apiKey.Name = data.Name
res := h.DB.Save(&apiKey) err := h.DB.Save(&apiKey).Error
if res.Error != nil { if err != nil {
logger.Error("error with update database", res.Error) resp.ERROR(c, err.Error())
resp.ERROR(c, "更新数据库失败!")
return return
} }
var keyVo vo.ApiKey var keyVo vo.ApiKey
err := utils.CopyObject(apiKey, &keyVo) err = utils.CopyObject(apiKey, &keyVo)
if err != nil { if err != nil {
resp.ERROR(c, "数据拷贝失败!") resp.ERROR(c, fmt.Sprintf("拷贝数据失败:%v", err))
return return
} }
keyVo.Id = apiKey.Id keyVo.Id = apiKey.Id
@ -119,10 +119,9 @@ func (h *ApiKeyHandler) Set(c *gin.Context) {
return return
} }
res := h.DB.Model(&model.ApiKey{}).Where("id = ?", data.Id).Update(data.Filed, data.Value) err := h.DB.Model(&model.ApiKey{}).Where("id = ?", data.Id).Update(data.Filed, data.Value).Error
if res.Error != nil { if err != nil {
logger.Error("error with update database", res.Error) resp.ERROR(c, err.Error())
resp.ERROR(c, "更新数据库失败!")
return return
} }
resp.SUCCESS(c) resp.SUCCESS(c)
@ -135,10 +134,9 @@ func (h *ApiKeyHandler) Remove(c *gin.Context) {
return return
} }
res := h.DB.Where("id", id).Delete(&model.ApiKey{}) err := h.DB.Where("id", id).Delete(&model.ApiKey{}).Error
if res.Error != nil { if err != nil {
logger.Error("error with update database", res.Error) resp.ERROR(c, err.Error())
resp.ERROR(c, "更新数据库失败!")
return return
} }
resp.SUCCESS(c) resp.SUCCESS(c)

View File

@ -259,10 +259,9 @@ func (h *ChatHandler) RemoveChat(c *gin.Context) {
// RemoveMessage 删除聊天记录 // RemoveMessage 删除聊天记录
func (h *ChatHandler) RemoveMessage(c *gin.Context) { func (h *ChatHandler) RemoveMessage(c *gin.Context) {
id := h.GetInt(c, "id", 0) id := h.GetInt(c, "id", 0)
tx := h.DB.Unscoped().Where("id = ?", id).Delete(&model.ChatMessage{}) err := h.DB.Unscoped().Where("id = ?", id).Delete(&model.ChatMessage{}).Error
if tx.Error != nil { if err != nil {
logger.Error("error with update database", tx.Error) resp.ERROR(c, err.Error())
resp.ERROR(c, "更新数据库失败!")
return return
} }
resp.SUCCESS(c) resp.SUCCESS(c)

View File

@ -147,10 +147,9 @@ func (h *ChatModelHandler) Set(c *gin.Context) {
return return
} }
res := h.DB.Model(&model.ChatModel{}).Where("id = ?", data.Id).Update(data.Filed, data.Value) err := h.DB.Model(&model.ChatModel{}).Where("id = ?", data.Id).Update(data.Filed, data.Value).Error
if res.Error != nil { if err != nil {
logger.Error("error with update database", res.Error) resp.ERROR(c, err.Error())
resp.ERROR(c, "更新数据库失败!")
return return
} }
resp.SUCCESS(c) resp.SUCCESS(c)
@ -168,10 +167,9 @@ func (h *ChatModelHandler) Sort(c *gin.Context) {
} }
for index, id := range data.Ids { for index, id := range data.Ids {
res := h.DB.Model(&model.ChatModel{}).Where("id = ?", id).Update("sort_num", data.Sorts[index]) err := h.DB.Model(&model.ChatModel{}).Where("id = ?", id).Update("sort_num", data.Sorts[index]).Error
if res.Error != nil { if err != nil {
logger.Error("error with update database", res.Error) resp.ERROR(c, err.Error())
resp.ERROR(c, "更新数据库失败!")
return return
} }
} }
@ -186,10 +184,9 @@ func (h *ChatModelHandler) Remove(c *gin.Context) {
return return
} }
res := h.DB.Where("id = ?", id).Delete(&model.ChatModel{}) err := h.DB.Where("id = ?", id).Delete(&model.ChatModel{}).Error
if res.Error != nil { if err != nil {
logger.Error("error with update database", res.Error) resp.ERROR(c, err.Error())
resp.ERROR(c, "更新数据库失败!")
return return
} }
resp.SUCCESS(c) resp.SUCCESS(c)

View File

@ -8,6 +8,7 @@ package admin
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import ( import (
"fmt"
"geekai/core" "geekai/core"
"geekai/core/types" "geekai/core/types"
"geekai/handler" "geekai/handler"
@ -45,11 +46,16 @@ func (h *ChatRoleHandler) Save(c *gin.Context) {
role.Id = data.Id role.Id = data.Id
if data.CreatedAt > 0 { if data.CreatedAt > 0 {
role.CreatedAt = time.Unix(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 { err = h.DB.Save(&role).Error
logger.Error("error with update database", res.Error) if err != nil {
resp.ERROR(c, "更新数据库失败!") resp.ERROR(c, err.Error())
return return
} }
// 填充 ID 数据 // 填充 ID 数据
@ -114,10 +120,9 @@ func (h *ChatRoleHandler) Sort(c *gin.Context) {
} }
for index, id := range data.Ids { for index, id := range data.Ids {
res := h.DB.Model(&model.ChatRole{}).Where("id = ?", id).Update("sort_num", data.Sorts[index]) err := h.DB.Model(&model.ChatRole{}).Where("id = ?", id).Update("sort_num", data.Sorts[index]).Error
if res.Error != nil { if err != nil {
logger.Error("error with update database", res.Error) resp.ERROR(c, err.Error())
resp.ERROR(c, "更新数据库失败!")
return return
} }
} }
@ -137,10 +142,9 @@ func (h *ChatRoleHandler) Set(c *gin.Context) {
return return
} }
res := h.DB.Model(&model.ChatRole{}).Where("id = ?", data.Id).Update(data.Filed, data.Value) err := h.DB.Model(&model.ChatRole{}).Where("id = ?", data.Id).Update(data.Filed, data.Value).Error
if res.Error != nil { if err != nil {
logger.Error("error with update database", res.Error) resp.ERROR(c, err.Error())
resp.ERROR(c, "更新数据库失败!")
return return
} }
resp.SUCCESS(c) resp.SUCCESS(c)

View File

@ -12,8 +12,6 @@ import (
"geekai/core/types" "geekai/core/types"
"geekai/handler" "geekai/handler"
"geekai/service" "geekai/service"
"geekai/service/mj"
"geekai/service/sd"
"geekai/store" "geekai/store"
"geekai/store/model" "geekai/store/model"
"geekai/utils" "geekai/utils"
@ -28,16 +26,12 @@ type ConfigHandler struct {
handler.BaseHandler handler.BaseHandler
levelDB *store.LevelDB levelDB *store.LevelDB
licenseService *service.LicenseService 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{ return &ConfigHandler{
BaseHandler: handler.BaseHandler{App: app, DB: db}, BaseHandler: handler.BaseHandler{App: app, DB: db},
levelDB: levelDB, levelDB: levelDB,
mjServicePool: mjPool,
sdServicePool: sdPool,
licenseService: licenseService, licenseService: licenseService,
} }
} }
@ -146,58 +140,3 @@ func (h *ConfigHandler) GetLicense(c *gin.Context) {
license := h.licenseService.GetLicense() license := h.licenseService.GetLicense()
resp.SUCCESS(c, license) 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,
})
}
// 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)
}

View File

@ -60,13 +60,6 @@ func (h *DashboardHandler) Stats(c *gin.Context) {
stats.Tokens += item.Tokens 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 var orders []model.Order
res = h.DB.Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", zeroTime).Find(&orders) 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) 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天的订单 // 统计最近7天的订单
res = h.DB.Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", startDate).Find(&orders) res = h.DB.Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", startDate).Find(&orders)
for _, item := range orders { for _, item := range orders {

View File

@ -69,10 +69,9 @@ func (h *FunctionHandler) Set(c *gin.Context) {
return return
} }
res := h.DB.Model(&model.Function{}).Where("id = ?", data.Id).Update(data.Filed, data.Value) err := h.DB.Model(&model.Function{}).Where("id = ?", data.Id).Update(data.Filed, data.Value).Error
if res.Error != nil { if err != nil {
logger.Error("error with update database", res.Error) resp.ERROR(c, err.Error())
resp.ERROR(c, "更新数据库失败!")
return return
} }
resp.SUCCESS(c) resp.SUCCESS(c)
@ -102,10 +101,9 @@ func (h *FunctionHandler) Remove(c *gin.Context) {
id := h.GetInt(c, "id", 0) id := h.GetInt(c, "id", 0)
if id > 0 { if id > 0 {
res := h.DB.Delete(&model.Function{Id: uint(id)}) err := h.DB.Delete(&model.Function{Id: uint(id)}).Error
if res.Error != nil { if err != nil {
logger.Error("error with update database", res.Error) resp.ERROR(c, err.Error())
resp.ERROR(c, "更新数据库失败!")
return return
} }
} }

View File

@ -41,17 +41,16 @@ func (h *MenuHandler) Save(c *gin.Context) {
return return
} }
res := h.DB.Save(&model.Menu{ err := h.DB.Save(&model.Menu{
Id: data.Id, Id: data.Id,
Name: data.Name, Name: data.Name,
Icon: data.Icon, Icon: data.Icon,
URL: data.URL, URL: data.URL,
SortNum: data.SortNum, SortNum: data.SortNum,
Enabled: data.Enabled, Enabled: data.Enabled,
}) }).Error
if res.Error != nil { if err != nil {
logger.Error("error with update database", res.Error) resp.ERROR(c, err.Error())
resp.ERROR(c, "更新数据库失败!")
return return
} }
resp.SUCCESS(c) resp.SUCCESS(c)
@ -85,10 +84,9 @@ func (h *MenuHandler) Enable(c *gin.Context) {
return return
} }
res := h.DB.Model(&model.Menu{}).Where("id", data.Id).UpdateColumn("enabled", data.Enabled) err := h.DB.Model(&model.Menu{}).Where("id", data.Id).UpdateColumn("enabled", data.Enabled).Error
if res.Error != nil { if err != nil {
logger.Error("error with update database", res.Error) resp.ERROR(c, err.Error())
resp.ERROR(c, "更新数据库失败!")
return return
} }
resp.SUCCESS(c) resp.SUCCESS(c)
@ -106,10 +104,9 @@ func (h *MenuHandler) Sort(c *gin.Context) {
} }
for index, id := range data.Ids { for index, id := range data.Ids {
res := h.DB.Model(&model.Menu{}).Where("id", id).Update("sort_num", data.Sorts[index]) err := h.DB.Model(&model.Menu{}).Where("id", id).Update("sort_num", data.Sorts[index]).Error
if res.Error != nil { if err != nil {
logger.Error("error with update database", res.Error) resp.ERROR(c, err.Error())
resp.ERROR(c, "更新数据库失败!")
return return
} }
} }
@ -121,10 +118,9 @@ func (h *MenuHandler) Remove(c *gin.Context) {
id := h.GetInt(c, "id", 0) id := h.GetInt(c, "id", 0)
if id > 0 { if id > 0 {
res := h.DB.Where("id", id).Delete(&model.Menu{}) err := h.DB.Where("id", id).Delete(&model.Menu{}).Error
if res.Error != nil { if err != nil {
logger.Error("error with update database", res.Error) resp.ERROR(c, err.Error())
resp.ERROR(c, "更新数据库失败!")
return return
} }
} }

View File

@ -92,12 +92,21 @@ func (h *OrderHandler) Remove(c *gin.Context) {
return return
} }
res = h.DB.Unscoped().Where("id = ?", id).Delete(&model.Order{}) err := h.DB.Unscoped().Where("id = ?", id).Delete(&model.Order{}).Error
if res.Error != nil { if err != nil {
logger.Error("error with update database", res.Error) resp.ERROR(c, err.Error())
resp.ERROR(c, "更新数据库失败!")
return return
} }
} }
resp.SUCCESS(c) resp.SUCCESS(c)
} }
func (h *OrderHandler) Clear(c *gin.Context) {
err := h.DB.Unscoped().Where("status <> ?", 2).Where("pay_time", 0).Delete(&model.Order{}).Error
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c)
}

View File

@ -55,17 +55,16 @@ func (h *ProductHandler) Save(c *gin.Context) {
if item.Id > 0 { if item.Id > 0 {
item.CreatedAt = time.Unix(data.CreatedAt, 0) item.CreatedAt = time.Unix(data.CreatedAt, 0)
} }
res := h.DB.Save(&item) err := h.DB.Save(&item).Error
if res.Error != nil { if err != nil {
logger.Error("error with update database", res.Error) resp.ERROR(c, err.Error())
resp.ERROR(c, "更新数据库失败!")
return return
} }
var itemVo vo.Product var itemVo vo.Product
err := utils.CopyObject(item, &itemVo) err = utils.CopyObject(item, &itemVo)
if err != nil { if err != nil {
resp.ERROR(c, "数据拷贝失败") resp.ERROR(c, "数据拷贝失败: "+err.Error())
return return
} }
itemVo.Id = item.Id itemVo.Id = item.Id
@ -106,10 +105,9 @@ func (h *ProductHandler) Enable(c *gin.Context) {
return return
} }
res := h.DB.Model(&model.Product{}).Where("id", data.Id).UpdateColumn("enabled", data.Enabled) err := h.DB.Model(&model.Product{}).Where("id", data.Id).UpdateColumn("enabled", data.Enabled).Error
if res.Error != nil { if err != nil {
logger.Error("error with update database", res.Error) resp.ERROR(c, err.Error())
resp.ERROR(c, "更新数据库失败!")
return return
} }
resp.SUCCESS(c) resp.SUCCESS(c)
@ -127,10 +125,9 @@ func (h *ProductHandler) Sort(c *gin.Context) {
} }
for index, id := range data.Ids { for index, id := range data.Ids {
res := h.DB.Model(&model.Product{}).Where("id", id).Update("sort_num", data.Sorts[index]) err := h.DB.Model(&model.Product{}).Where("id", id).Update("sort_num", data.Sorts[index]).Error
if res.Error != nil { if err != nil {
logger.Error("error with update database", res.Error) resp.ERROR(c, err.Error())
resp.ERROR(c, "更新数据库失败!")
return return
} }
} }
@ -142,10 +139,9 @@ func (h *ProductHandler) Remove(c *gin.Context) {
id := h.GetInt(c, "id", 0) id := h.GetInt(c, "id", 0)
if id > 0 { if id > 0 {
res := h.DB.Where("id", id).Delete(&model.Product{}) err := h.DB.Where("id", id).Delete(&model.Product{}).Error
if res.Error != nil { if err != nil {
logger.Error("error with update database", res.Error) resp.ERROR(c, err.Error())
resp.ERROR(c, "更新数据库失败!")
return return
} }
} }

View File

@ -0,0 +1,164 @@
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) {
var data struct {
Id uint
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
if data.Id > 0 {
err := h.DB.Where("id", data.Id).Delete(&model.Redeem{}).Error
if err != nil {
resp.ERROR(c, err.Error())
return
}
}
resp.SUCCESS(c)
}

View File

@ -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)
}

View File

@ -168,8 +168,7 @@ func (h *UserHandler) Save(c *gin.Context) {
} }
if res.Error != nil { if res.Error != nil {
logger.Error("error with update database", res.Error) resp.ERROR(c, res.Error.Error())
resp.ERROR(c, "更新数据库失败")
return return
} }
@ -226,7 +225,7 @@ func (h *UserHandler) Remove(c *gin.Context) {
// 删除算力日志 // 删除算力日志
h.DB.Where("user_id = ?", id).Delete(&model.PowerLog{}) 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.Redeem{})
// 删除绘图任务 // 删除绘图任务
h.DB.Where("user_id = ?", id).Delete(&model.MidJourneyJob{}) 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.SdJob{})

View File

@ -81,10 +81,9 @@ func (h *ChatRoleHandler) UpdateRole(c *gin.Context) {
return return
} }
res := h.DB.Model(&model.User{}).Where("id = ?", user.Id).UpdateColumn("chat_roles_json", utils.JsonEncode(data.Keys)) err = h.DB.Model(&model.User{}).Where("id = ?", user.Id).UpdateColumn("chat_roles_json", utils.JsonEncode(data.Keys)).Error
if res.Error != nil { if err != nil {
logger.Error("error with update database", res.Error) resp.ERROR(c, err.Error())
resp.ERROR(c, "更新数据库失败!")
return return
} }

View File

@ -468,7 +468,7 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, sessi
} else { } else {
client = http.DefaultClient client = http.DefaultClient
} }
logger.Debugf("Sending %s request, Channel:%s, API KEY:%s, PROXY: %s, Model: %s", session.Model.Platform, apiKey.ApiURL, apiURL, apiKey.ProxyURL, req.Model) 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)) request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey.Value))
// 更新API KEY 最后使用时间 // 更新API KEY 最后使用时间
h.DB.Model(&model.ApiKey{}).Where("id", apiKey.Id).UpdateColumn("last_used_at", time.Now().Unix()) h.DB.Model(&model.ApiKey{}).Where("id", apiKey.Id).UpdateColumn("last_used_at", time.Now().Unix())

View File

@ -8,6 +8,7 @@ package handler
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import ( import (
"fmt"
"geekai/core" "geekai/core"
"geekai/core/types" "geekai/core/types"
"geekai/service/dalle" "geekai/service/dalle"
@ -16,9 +17,9 @@ import (
"geekai/store/vo" "geekai/store/vo"
"geekai/utils" "geekai/utils"
"geekai/utils/resp" "geekai/utils/resp"
"net/http"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"net/http"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8" "github.com/go-redis/redis/v8"
@ -178,7 +179,7 @@ func (h *DallJobHandler) getData(finish bool, userId uint, page int, pageSize in
session := h.DB.Session(&gorm.Session{}) session := h.DB.Session(&gorm.Session{})
if finish { if finish {
session = session.Where("progress = ?", 100).Order("id DESC") session = session.Where("progress >= ?", 100).Order("id DESC")
} else { } else {
session = session.Where("progress < ?", 100).Order("id ASC") session = session.Where("progress < ?", 100).Order("id ASC")
} }
@ -215,20 +216,51 @@ func (h *DallJobHandler) getData(finish bool, userId uint, page int, pageSize in
// Remove remove task image // Remove remove task image
func (h *DallJobHandler) Remove(c *gin.Context) { func (h *DallJobHandler) Remove(c *gin.Context) {
id := h.GetInt(c, "id", 0) id := h.GetInt(c, "id", 0)
userId := h.GetInt(c, "user_id", 0) userId := h.GetLoginUserId(c)
var job model.DallJob var job model.DallJob
if res := h.DB.Where("id = ? AND user_id = ?", id, userId).First(&job); res.Error != nil { if res := h.DB.Where("id = ? AND user_id = ?", id, userId).First(&job); res.Error != nil {
resp.ERROR(c, "记录不存在") resp.ERROR(c, "记录不存在")
return return
} }
// remove job recode // 删除任务
res := h.DB.Delete(&model.DallJob{Id: job.Id}) tx := h.DB.Begin()
if res.Error != nil { if err := tx.Delete(&job).Error; err != nil {
resp.ERROR(c, res.Error.Error()) tx.Rollback()
resp.ERROR(c, err.Error())
return return
} }
// 如果任务未完成,或者任务失败,则恢复用户算力
if job.Progress != 100 {
err := tx.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power + ?", job.Power)).Error
if err != nil {
tx.Rollback()
resp.ERROR(c, err.Error())
return
}
var user model.User
tx.Where("id = ?", job.UserId).First(&user)
err = tx.Create(&model.PowerLog{
UserId: user.Id,
Username: user.Username,
Type: types.PowerRefund,
Amount: job.Power,
Balance: user.Power,
Mark: types.PowerAdd,
Model: "dall-e-3",
Remark: fmt.Sprintf("任务失败退回算力。任务ID%dErr: %s", job.Id, job.ErrMsg),
CreatedAt: time.Now(),
}).Error
if err != nil {
tx.Rollback()
resp.ERROR(c, err.Error())
return
}
}
tx.Commit()
// remove image // remove image
err := h.uploader.GetUploadHandler().Delete(job.ImgURL) err := h.uploader.GetUploadHandler().Delete(job.ImgURL)
if err != nil { if err != nil {
@ -241,13 +273,12 @@ func (h *DallJobHandler) Remove(c *gin.Context) {
// Publish 发布/取消发布图片到画廊显示 // Publish 发布/取消发布图片到画廊显示
func (h *DallJobHandler) Publish(c *gin.Context) { func (h *DallJobHandler) Publish(c *gin.Context) {
id := h.GetInt(c, "id", 0) id := h.GetInt(c, "id", 0)
userId := h.GetInt(c, "user_id", 0) userId := h.GetLoginUserId(c)
action := h.GetBool(c, "action") // 发布动作true => 发布false => 取消分享 action := h.GetBool(c, "action") // 发布动作true => 发布false => 取消分享
res := h.DB.Model(&model.DallJob{Id: uint(id), UserId: uint(userId)}).UpdateColumn("publish", action) err := h.DB.Model(&model.DallJob{Id: uint(id), UserId: userId}).UpdateColumn("publish", action).Error
if res.Error != nil { if err != nil {
logger.Error("error with update database", res.Error) resp.ERROR(c, err.Error())
resp.ERROR(c, "更新数据库失败")
return return
} }

View File

@ -101,17 +101,13 @@ func (h *MarkMapHandler) sendMessage(client *types.WsClient, prompt string, mode
return fmt.Errorf("error with query chat model: %v", res.Error) return fmt.Errorf("error with query chat model: %v", res.Error)
} }
if user.Status == false {
return errors.New("当前用户被禁用")
}
if user.Power < chatModel.Power { if user.Power < chatModel.Power {
return fmt.Errorf("您当前剩余算力(%d已不足以支付当前模型算力%d", user.Power, chatModel.Power) return fmt.Errorf("您当前剩余算力(%d已不足以支付当前模型算力%d", user.Power, chatModel.Power)
} }
messages := make([]interface{}, 0) messages := make([]interface{}, 0)
messages = append(messages, types.Message{Role: "system", Content: ` messages = append(messages, types.Message{Role: "system", Content: `
你是一位非常优秀的思维导图助手你会把用户的所有提问都总结成思维导图然后以 Markdown 格式输出markdown 只需要输出一级标题二级标题三级标题四级标题最多输出四级除此之外不要输出任何其他 markdown 标记下面是一个合格的例子 你是一位非常优秀的思维导图助手 你能帮助用户整理思路根据用户提供的主题或内容快速生成结构清晰有条理的思维导图然后以 Markdown 格式输出markdown 只需要输出一级标题二级标题三级标题四级标题最多输出四级除此之外不要输出任何其他 markdown 标记下面是一个合格的例子
# Geek-AI 助手 # Geek-AI 助手
## 完整的开源系统 ## 完整的开源系统
@ -130,7 +126,7 @@ func (h *MarkMapHandler) sendMessage(client *types.WsClient, prompt string, mode
另外除此之外不要任何解释性语句 另外除此之外不要任何解释性语句
`}) `})
messages = append(messages, types.Message{Role: "user", Content: prompt}) messages = append(messages, types.Message{Role: "user", Content: fmt.Sprintf("请生成一份有关【%s】一份思维导图要求结构清晰有条理", prompt)})
var req = types.ApiRequest{ var req = types.ApiRequest{
Model: chatModel.Value, Model: chatModel.Value,
Stream: true, Stream: true,
@ -253,5 +249,6 @@ func (h *MarkMapHandler) doRequest(req types.ApiRequest, chatModel model.ChatMod
client = http.DefaultClient client = http.DefaultClient
} }
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey.Value)) request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey.Value))
logger.Debugf("Sending %s request, API KEY:%s, PROXY: %s, Model: %s", apiKey.ApiURL, apiURL, apiKey.ProxyURL, req.Model)
return client.Do(request) return client.Do(request)
} }

View File

@ -30,15 +30,15 @@ import (
type MidJourneyHandler struct { type MidJourneyHandler struct {
BaseHandler BaseHandler
pool *mj.ServicePool service *mj.Service
snowflake *service.Snowflake snowflake *service.Snowflake
uploader *oss.UploaderManager uploader *oss.UploaderManager
} }
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) *MidJourneyHandler {
return &MidJourneyHandler{ return &MidJourneyHandler{
snowflake: snowflake, snowflake: snowflake,
pool: pool, service: service,
uploader: manager, uploader: manager,
BaseHandler: BaseHandler{ BaseHandler: BaseHandler{
App: app, App: app,
@ -59,11 +59,6 @@ func (h *MidJourneyHandler) preCheck(c *gin.Context) bool {
return false return false
} }
if !h.pool.HasAvailableService() {
resp.ERROR(c, "MidJourney 池子中没有没有可用的服务!")
return false
}
return true return true
} }
@ -85,7 +80,7 @@ func (h *MidJourneyHandler) Client(c *gin.Context) {
} }
client := types.NewWsClient(ws) client := types.NewWsClient(ws)
h.pool.Clients.Put(uint(userId), client) h.service.Clients.Put(uint(userId), client)
logger.Infof("New websocket connected, IP: %s", c.RemoteIP()) logger.Infof("New websocket connected, IP: %s", c.RemoteIP())
} }
@ -201,7 +196,7 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
return return
} }
h.pool.PushTask(types.MjTask{ h.service.PushTask(types.MjTask{
Id: job.Id, Id: job.Id,
TaskId: taskId, TaskId: taskId,
Type: types.TaskType(data.TaskType), Type: types.TaskType(data.TaskType),
@ -210,9 +205,10 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
Params: params, Params: params,
UserId: userId, UserId: userId,
ImgArr: data.ImgArr, ImgArr: data.ImgArr,
Mode: h.App.SysConfig.MjMode,
}) })
client := h.pool.Clients.Get(uint(job.UserId)) client := h.service.Clients.Get(uint(job.UserId))
if client != nil { if client != nil {
_ = client.Send([]byte("Task Updated")) _ = client.Send([]byte("Task Updated"))
} }
@ -273,7 +269,7 @@ func (h *MidJourneyHandler) Upscale(c *gin.Context) {
return return
} }
h.pool.PushTask(types.MjTask{ h.service.PushTask(types.MjTask{
Id: job.Id, Id: job.Id,
Type: types.TaskUpscale, Type: types.TaskUpscale,
UserId: userId, UserId: userId,
@ -281,9 +277,10 @@ func (h *MidJourneyHandler) Upscale(c *gin.Context) {
Index: data.Index, Index: data.Index,
MessageId: data.MessageId, MessageId: data.MessageId,
MessageHash: data.MessageHash, MessageHash: data.MessageHash,
Mode: h.App.SysConfig.MjMode,
}) })
client := h.pool.Clients.Get(uint(job.UserId)) client := h.service.Clients.Get(uint(job.UserId))
if client != nil { if client != nil {
_ = client.Send([]byte("Task Updated")) _ = client.Send([]byte("Task Updated"))
} }
@ -337,7 +334,7 @@ func (h *MidJourneyHandler) Variation(c *gin.Context) {
return return
} }
h.pool.PushTask(types.MjTask{ h.service.PushTask(types.MjTask{
Id: job.Id, Id: job.Id,
Type: types.TaskVariation, Type: types.TaskVariation,
UserId: userId, UserId: userId,
@ -345,9 +342,10 @@ func (h *MidJourneyHandler) Variation(c *gin.Context) {
ChannelId: data.ChannelId, ChannelId: data.ChannelId,
MessageId: data.MessageId, MessageId: data.MessageId,
MessageHash: data.MessageHash, MessageHash: data.MessageHash,
Mode: h.App.SysConfig.MjMode,
}) })
client := h.pool.Clients.Get(uint(job.UserId)) client := h.service.Clients.Get(uint(job.UserId))
if client != nil { if client != nil {
_ = client.Send([]byte("Task Updated")) _ = client.Send([]byte("Task Updated"))
} }
@ -465,7 +463,8 @@ func (h *MidJourneyHandler) Remove(c *gin.Context) {
return return
} }
// refund power // 如果任务未完成,或者任务失败,则恢复用户算力
if job.Progress != 100 {
err := tx.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power + ?", job.Power)).Error err := tx.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power + ?", job.Power)).Error
if err != nil { if err != nil {
tx.Rollback() tx.Rollback()
@ -473,16 +472,16 @@ func (h *MidJourneyHandler) Remove(c *gin.Context) {
return return
} }
var user model.User var user model.User
h.DB.Where("id = ?", job.UserId).First(&user) tx.Where("id = ?", job.UserId).First(&user)
err = tx.Create(&model.PowerLog{ err = tx.Create(&model.PowerLog{
UserId: user.Id, UserId: user.Id,
Username: user.Username, Username: user.Username,
Type: types.PowerConsume, Type: types.PowerRefund,
Amount: job.Power, Amount: job.Power,
Balance: user.Power + job.Power, Balance: user.Power,
Mark: types.PowerAdd, Mark: types.PowerAdd,
Model: "mid-journey", Model: "mid-journey",
Remark: fmt.Sprintf("绘画任务失败退回算力。任务ID%s", job.TaskId), Remark: fmt.Sprintf("绘画任务失败退回算力。任务ID%sErr: %s", job.TaskId, job.ErrMsg),
CreatedAt: time.Now(), CreatedAt: time.Now(),
}).Error }).Error
if err != nil { if err != nil {
@ -490,15 +489,16 @@ func (h *MidJourneyHandler) Remove(c *gin.Context) {
resp.ERROR(c, err.Error()) resp.ERROR(c, err.Error())
return return
} }
}
tx.Commit() tx.Commit()
// remove image // remove image
err = h.uploader.GetUploadHandler().Delete(job.ImgURL) err := h.uploader.GetUploadHandler().Delete(job.ImgURL)
if err != nil { if err != nil {
logger.Error("remove image failed: ", err) logger.Error("remove image failed: ", err)
} }
client := h.pool.Clients.Get(uint(job.UserId)) client := h.service.Clients.Get(uint(job.UserId))
if client != nil { if client != nil {
_ = client.Send([]byte("Task Updated")) _ = client.Send([]byte("Task Updated"))
} }
@ -511,10 +511,9 @@ func (h *MidJourneyHandler) Publish(c *gin.Context) {
id := h.GetInt(c, "id", 0) id := h.GetInt(c, "id", 0)
userId := h.GetInt(c, "user_id", 0) userId := h.GetInt(c, "user_id", 0)
action := h.GetBool(c, "action") // 发布动作true => 发布false => 取消分享 action := h.GetBool(c, "action") // 发布动作true => 发布false => 取消分享
res := h.DB.Model(&model.MidJourneyJob{Id: uint(id), UserId: userId}).UpdateColumn("publish", action) err := h.DB.Model(&model.MidJourneyJob{Id: uint(id), UserId: userId}).UpdateColumn("publish", action).Error
if res.Error != nil { if err != nil {
logger.Error("error with update database", res.Error) resp.ERROR(c, err.Error())
resp.ERROR(c, "更新数据库失败")
return return
} }

View File

@ -0,0 +1,102 @@
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/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"sync"
"time"
)
type RedeemHandler struct {
BaseHandler
lock sync.Mutex
}
func NewRedeemHandler(app *core.AppServer, db *gorm.DB) *RedeemHandler {
return &RedeemHandler{BaseHandler: BaseHandler{App: app, DB: db}}
}
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 := tx.Model(&model.User{}).Where("id", userId).UpdateColumn("power", gorm.Expr("power + ?", item.Power)).Error
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
}
// 记录算力充值日志
var user model.User
err = tx.Where("id", userId).First(&user).Error
if err != nil {
tx.Rollback()
resp.ERROR(c, err.Error())
return
}
h.DB.Create(&model.PowerLog{
UserId: userId,
Username: user.Username,
Type: types.PowerRedeem,
Amount: item.Power,
Balance: user.Power,
Mark: types.PowerAdd,
Model: "兑换码",
Remark: fmt.Sprintf("兑换码核销,算力:%d兑换码%s...", item.Power, item.Code[:10]),
CreatedAt: time.Now(),
})
tx.Commit()
resp.SUCCESS(c)
}

View File

@ -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)
}

View File

@ -32,15 +32,15 @@ import (
type SdJobHandler struct { type SdJobHandler struct {
BaseHandler BaseHandler
redis *redis.Client redis *redis.Client
pool *sd.ServicePool service *sd.Service
uploader *oss.UploaderManager uploader *oss.UploaderManager
snowflake *service.Snowflake snowflake *service.Snowflake
leveldb *store.LevelDB leveldb *store.LevelDB
} }
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, levelDB *store.LevelDB) *SdJobHandler {
return &SdJobHandler{ return &SdJobHandler{
pool: pool, service: service,
uploader: manager, uploader: manager,
snowflake: snowflake, snowflake: snowflake,
leveldb: levelDB, leveldb: levelDB,
@ -68,7 +68,7 @@ func (h *SdJobHandler) Client(c *gin.Context) {
} }
client := types.NewWsClient(ws) client := types.NewWsClient(ws)
h.pool.Clients.Put(uint(userId), client) h.service.Clients.Put(uint(userId), client)
logger.Infof("New websocket connected, IP: %s", c.RemoteIP()) logger.Infof("New websocket connected, IP: %s", c.RemoteIP())
} }
@ -79,11 +79,6 @@ func (h *SdJobHandler) preCheck(c *gin.Context) bool {
return false return false
} }
if !h.pool.HasAvailableService() {
resp.ERROR(c, "Stable-Diffusion 池子中没有没有可用的服务!")
return false
}
if user.Power < h.App.SysConfig.SdPower { if user.Power < h.App.SysConfig.SdPower {
resp.ERROR(c, "当前用户剩余算力不足以完成本次绘画!") resp.ERROR(c, "当前用户剩余算力不足以完成本次绘画!")
return false return false
@ -164,14 +159,14 @@ func (h *SdJobHandler) Image(c *gin.Context) {
return return
} }
h.pool.PushTask(types.SdTask{ h.service.PushTask(types.SdTask{
Id: int(job.Id), Id: int(job.Id),
Type: types.TaskImage, Type: types.TaskImage,
Params: params, Params: params,
UserId: userId, UserId: userId,
}) })
client := h.pool.Clients.Get(uint(job.UserId)) client := h.service.Clients.Get(uint(job.UserId))
if client != nil { if client != nil {
_ = client.Send([]byte("Task Updated")) _ = client.Send([]byte("Task Updated"))
} }
@ -232,7 +227,7 @@ func (h *SdJobHandler) getData(finish bool, userId uint, page int, pageSize int,
session := h.DB.Session(&gorm.Session{}) session := h.DB.Session(&gorm.Session{})
if finish { if finish {
session = session.Where("progress = ?", 100).Order("id DESC") session = session.Where("progress >= ?", 100).Order("id DESC")
} else { } else {
session = session.Where("progress < ?", 100).Order("id ASC") session = session.Where("progress < ?", 100).Order("id ASC")
} }
@ -278,44 +273,68 @@ func (h *SdJobHandler) getData(finish bool, userId uint, page int, pageSize int,
// Remove remove task image // Remove remove task image
func (h *SdJobHandler) Remove(c *gin.Context) { func (h *SdJobHandler) Remove(c *gin.Context) {
id := h.GetInt(c, "id", 0) id := h.GetInt(c, "id", 0)
userId := h.GetInt(c, "user_id", 0) userId := h.GetLoginUserId(c)
var job model.SdJob var job model.SdJob
if res := h.DB.Where("id = ? AND user_id = ?", id, userId).First(&job); res.Error != nil { if res := h.DB.Where("id = ? AND user_id = ?", id, userId).First(&job); res.Error != nil {
resp.ERROR(c, "记录不存在") resp.ERROR(c, "记录不存在")
return return
} }
// remove job recode // 删除任务
res := h.DB.Delete(&model.SdJob{Id: job.Id}) tx := h.DB.Begin()
if res.Error != nil { if err := tx.Delete(&job).Error; err != nil {
resp.ERROR(c, res.Error.Error()) tx.Rollback()
resp.ERROR(c, err.Error())
return return
} }
// 如果任务未完成,或者任务失败,则恢复用户算力
if job.Progress != 100 {
err := tx.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power + ?", job.Power)).Error
if err != nil {
tx.Rollback()
resp.ERROR(c, err.Error())
return
}
var user model.User
tx.Where("id = ?", job.UserId).First(&user)
err = tx.Create(&model.PowerLog{
UserId: user.Id,
Username: user.Username,
Type: types.PowerRefund,
Amount: job.Power,
Balance: user.Power,
Mark: types.PowerAdd,
Model: "stable-diffusion",
Remark: fmt.Sprintf("任务失败退回算力。任务ID%s Err: %s", job.TaskId, job.ErrMsg),
CreatedAt: time.Now(),
}).Error
if err != nil {
tx.Rollback()
resp.ERROR(c, err.Error())
return
}
}
tx.Commit()
// remove image // remove image
err := h.uploader.GetUploadHandler().Delete(job.ImgURL) err := h.uploader.GetUploadHandler().Delete(job.ImgURL)
if err != nil { if err != nil {
logger.Error("remove image failed: ", err) 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) resp.SUCCESS(c)
} }
// Publish 发布/取消发布图片到画廊显示 // Publish 发布/取消发布图片到画廊显示
func (h *SdJobHandler) Publish(c *gin.Context) { func (h *SdJobHandler) Publish(c *gin.Context) {
id := h.GetInt(c, "id", 0) id := h.GetInt(c, "id", 0)
userId := h.GetInt(c, "user_id", 0) userId := h.GetLoginUserId(c)
action := h.GetBool(c, "action") // 发布动作true => 发布false => 取消分享 action := h.GetBool(c, "action") // 发布动作true => 发布false => 取消分享
res := h.DB.Model(&model.SdJob{Id: uint(id), UserId: userId}).UpdateColumn("publish", action) err := h.DB.Model(&model.SdJob{Id: uint(id), UserId: int(userId)}).UpdateColumn("publish", action).Error
if res.Error != nil { if err != nil {
logger.Error("error with update database", res.Error) resp.ERROR(c, err.Error())
resp.ERROR(c, "更新数据库失败")
return return
} }

View File

@ -94,6 +94,7 @@ func (h *SunoHandler) Create(c *gin.Context) {
RefTaskId: data.RefTaskId, RefTaskId: data.RefTaskId,
ExtendSecs: data.ExtendSecs, ExtendSecs: data.ExtendSecs,
Power: h.App.SysConfig.SunoPower, Power: h.App.SysConfig.SunoPower,
SongId: utils.RandString(32),
} }
if data.Lyrics != "" { if data.Lyrics != "" {
job.Prompt = data.Lyrics job.Prompt = data.Lyrics
@ -210,7 +211,42 @@ func (h *SunoHandler) Remove(c *gin.Context) {
return return
} }
// 删除任务 // 删除任务
h.DB.Delete(&job) tx := h.DB.Begin()
if err := tx.Delete(&job).Error; err != nil {
tx.Rollback()
resp.ERROR(c, err.Error())
return
}
// 如果任务未完成,或者任务失败,则恢复用户算力
if job.Progress != 100 {
err := tx.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power + ?", job.Power)).Error
if err != nil {
tx.Rollback()
resp.ERROR(c, err.Error())
return
}
var user model.User
tx.Where("id = ?", job.UserId).First(&user)
err = tx.Create(&model.PowerLog{
UserId: user.Id,
Username: user.Username,
Type: types.PowerRefund,
Amount: job.Power,
Balance: user.Power,
Mark: types.PowerAdd,
Model: job.ModelName,
Remark: fmt.Sprintf("Suno 任务失败退回算力。任务ID%sErr:%s", job.TaskId, job.ErrMsg),
CreatedAt: time.Now(),
}).Error
if err != nil {
tx.Rollback()
resp.ERROR(c, err.Error())
return
}
}
tx.Commit()
// 删除文件 // 删除文件
_ = h.uploader.GetUploadHandler().Delete(job.CoverURL) _ = h.uploader.GetUploadHandler().Delete(job.CoverURL)
_ = h.uploader.GetUploadHandler().Delete(job.AudioURL) _ = h.uploader.GetUploadHandler().Delete(job.AudioURL)

View File

@ -3,7 +3,9 @@ package handler
import ( import (
"geekai/service" "geekai/service"
"geekai/service/payment" "geekai/service/payment"
"github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
"net/http"
) )
type TestHandler struct { type TestHandler struct {
@ -15,3 +17,38 @@ type TestHandler struct {
func NewTestHandler(db *gorm.DB, snowflake *service.Snowflake, js *payment.JPayService) *TestHandler { func NewTestHandler(db *gorm.DB, snowflake *service.Snowflake, js *payment.JPayService) *TestHandler {
return &TestHandler{db: db, snowflake: snowflake, js: js} return &TestHandler{db: db, snowflake: snowflake, js: js}
} }
func (h *TestHandler) SseTest(c *gin.Context) {
//c.Header("Content-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()
}

View File

@ -481,10 +481,9 @@ func (h *UserHandler) UpdatePass(c *gin.Context) {
} }
newPass := utils.GenPassword(data.Password, user.Salt) newPass := utils.GenPassword(data.Password, user.Salt)
res := h.DB.Model(&user).UpdateColumn("password", newPass) err = h.DB.Model(&user).UpdateColumn("password", newPass).Error
if res.Error != nil { if err != nil {
logger.Error("error with update database", res.Error) resp.ERROR(c, err.Error())
resp.ERROR(c, "更新数据库失败")
return return
} }
@ -562,10 +561,9 @@ func (h *UserHandler) BindUsername(c *gin.Context) {
return return
} }
res = h.DB.Model(&user).UpdateColumn("username", data.Username) err = h.DB.Model(&user).UpdateColumn("username", data.Username).Error
if res.Error != nil { if err != nil {
logger.Error(res.Error) resp.ERROR(c, err.Error())
resp.ERROR(c, "更新数据库失败")
return return
} }

View File

@ -24,7 +24,6 @@ import (
"geekai/service/sd" "geekai/service/sd"
"geekai/service/sms" "geekai/service/sms"
"geekai/service/suno" "geekai/service/suno"
"geekai/service/wx"
"geekai/store" "geekai/store"
"io" "io"
"log" "log"
@ -131,7 +130,7 @@ func main() {
fx.Provide(chatimpl.NewChatHandler), fx.Provide(chatimpl.NewChatHandler),
fx.Provide(handler.NewUploadHandler), fx.Provide(handler.NewUploadHandler),
fx.Provide(handler.NewSmsHandler), fx.Provide(handler.NewSmsHandler),
fx.Provide(handler.NewRewardHandler), fx.Provide(handler.NewRedeemHandler),
fx.Provide(handler.NewCaptchaHandler), fx.Provide(handler.NewCaptchaHandler),
fx.Provide(handler.NewMidJourneyHandler), fx.Provide(handler.NewMidJourneyHandler),
fx.Provide(handler.NewChatModelHandler), fx.Provide(handler.NewChatModelHandler),
@ -147,7 +146,7 @@ func main() {
fx.Provide(admin.NewApiKeyHandler), fx.Provide(admin.NewApiKeyHandler),
fx.Provide(admin.NewUserHandler), fx.Provide(admin.NewUserHandler),
fx.Provide(admin.NewChatRoleHandler), fx.Provide(admin.NewChatRoleHandler),
fx.Provide(admin.NewRewardHandler), fx.Provide(admin.NewRedeemHandler),
fx.Provide(admin.NewDashboardHandler), fx.Provide(admin.NewDashboardHandler),
fx.Provide(admin.NewChatModelHandler), fx.Provide(admin.NewChatModelHandler),
fx.Provide(admin.NewProductHandler), fx.Provide(admin.NewProductHandler),
@ -161,13 +160,12 @@ func main() {
return service.NewCaptchaService(config.ApiConfig) return service.NewCaptchaService(config.ApiConfig)
}), }),
fx.Provide(oss.NewUploaderManager), fx.Provide(oss.NewUploaderManager),
fx.Provide(mj.NewService),
fx.Provide(dalle.NewService), fx.Provide(dalle.NewService),
fx.Invoke(func(service *dalle.Service) { fx.Invoke(func(s *dalle.Service) {
service.Run() s.Run()
service.CheckTaskNotify() s.CheckTaskNotify()
service.DownloadImages() s.DownloadImages()
service.CheckTaskStatus() s.CheckTaskStatus()
}), }),
// 邮件服务 // 邮件服务
@ -178,36 +176,22 @@ func main() {
licenseService.SyncLicense() 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 // MidJourney service pool
fx.Provide(mj.NewServicePool), fx.Provide(mj.NewService),
fx.Invoke(func(pool *mj.ServicePool, config *types.AppConfig) { fx.Provide(mj.NewClient),
pool.InitServices(config.MjPlusConfigs, config.MjProxyConfigs) fx.Invoke(func(s *mj.Service) {
if pool.HasAvailableService() { s.Run()
pool.DownloadImages() s.SyncTaskProgress()
pool.CheckTaskNotify() s.CheckTaskNotify()
pool.SyncTaskProgress() s.DownloadImages()
}
}), }),
// Stable Diffusion 机器人 // Stable Diffusion 机器人
fx.Provide(sd.NewServicePool), fx.Provide(sd.NewService),
fx.Invoke(func(pool *sd.ServicePool, config *types.AppConfig) { fx.Invoke(func(s *sd.Service, config *types.AppConfig) {
pool.InitServices(config.SdConfigs) s.Run()
if pool.HasAvailableService() { s.CheckTaskStatus()
pool.CheckTaskNotify() s.CheckTaskNotify()
pool.CheckTaskStatus()
}
}), }),
fx.Provide(suno.NewService), fx.Provide(suno.NewService),
@ -280,8 +264,8 @@ func main() {
group.GET("slide/get", h.SlideGet) group.GET("slide/get", h.SlideGet)
group.POST("slide/check", h.SlideCheck) group.POST("slide/check", h.SlideCheck)
}), }),
fx.Invoke(func(s *core.AppServer, h *handler.RewardHandler) { fx.Invoke(func(s *core.AppServer, h *handler.RedeemHandler) {
group := s.Engine.Group("/api/reward/") group := s.Engine.Group("/api/redeem/")
group.POST("verify", h.Verify) group.POST("verify", h.Verify)
}), }),
fx.Invoke(func(s *core.AppServer, h *handler.MidJourneyHandler) { fx.Invoke(func(s *core.AppServer, h *handler.MidJourneyHandler) {
@ -317,8 +301,6 @@ func main() {
group.GET("config/get", h.Get) group.GET("config/get", h.Get)
group.POST("active", h.Active) group.POST("active", h.Active)
group.GET("config/get/license", h.GetLicense) group.GET("config/get/license", h.GetLicense)
group.GET("config/get/app", h.GetAppConfig)
group.POST("config/update/draw", h.SaveDrawingConfig)
}), }),
fx.Invoke(func(s *core.AppServer, h *admin.ManagerHandler) { fx.Invoke(func(s *core.AppServer, h *admin.ManagerHandler) {
group := s.Engine.Group("/api/admin/") group := s.Engine.Group("/api/admin/")
@ -354,9 +336,11 @@ func main() {
group.POST("set", h.Set) group.POST("set", h.Set)
group.GET("remove", h.Remove) group.GET("remove", h.Remove)
}), }),
fx.Invoke(func(s *core.AppServer, h *admin.RewardHandler) { fx.Invoke(func(s *core.AppServer, h *admin.RedeemHandler) {
group := s.Engine.Group("/api/admin/reward/") group := s.Engine.Group("/api/admin/redeem/")
group.GET("list", h.List) group.GET("list", h.List)
group.POST("create", h.Create)
group.POST("set", h.Set)
group.POST("remove", h.Remove) group.POST("remove", h.Remove)
}), }),
fx.Invoke(func(s *core.AppServer, h *admin.DashboardHandler) { fx.Invoke(func(s *core.AppServer, h *admin.DashboardHandler) {
@ -398,6 +382,7 @@ func main() {
group := s.Engine.Group("/api/admin/order/") group := s.Engine.Group("/api/admin/order/")
group.POST("list", h.List) group.POST("list", h.List)
group.GET("remove", h.Remove) group.GET("remove", h.Remove)
group.GET("clear", h.Clear)
}), }),
fx.Invoke(func(s *core.AppServer, h *handler.OrderHandler) { fx.Invoke(func(s *core.AppServer, h *handler.OrderHandler) {
group := s.Engine.Group("/api/order/") group := s.Engine.Group("/api/order/")
@ -497,6 +482,11 @@ func main() {
group.GET("play", h.Play) group.GET("play", h.Play)
group.POST("lyric", h.Lyric) group.POST("lyric", h.Lyric)
}), }),
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.Invoke(func(s *core.AppServer, db *gorm.DB) { fx.Invoke(func(s *core.AppServer, db *gorm.DB) {
go func() { go func() {
err := s.Run(db) err := s.Run(db)

View File

@ -14,7 +14,6 @@ import (
logger2 "geekai/logger" logger2 "geekai/logger"
"geekai/service" "geekai/service"
"geekai/service/oss" "geekai/service/oss"
"geekai/service/sd"
"geekai/store" "geekai/store"
"geekai/store/model" "geekai/store/model"
"geekai/utils" "geekai/utils"
@ -70,10 +69,10 @@ func (s *Service) Run() {
if err != nil { if err != nil {
logger.Errorf("error with image task: %v", err) logger.Errorf("error with image task: %v", err)
s.db.Model(&model.DallJob{Id: task.JobId}).UpdateColumns(map[string]interface{}{ s.db.Model(&model.DallJob{Id: task.JobId}).UpdateColumns(map[string]interface{}{
"progress": -1, "progress": service.FailTaskProgress,
"err_msg": err.Error(), "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{UserId: int(task.UserId), JobId: int(task.JobId), Message: service.TaskStatusFailed})
} }
} }
}() }()
@ -148,7 +147,7 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
Where("enabled", true). Where("enabled", true).
Order("last_used_at ASC").First(&apiKey) Order("last_used_at ASC").First(&apiKey)
if tx.Error != nil { if tx.Error != nil {
return "", fmt.Errorf("no available IMG api key: %v", tx.Error) return "", fmt.Errorf("no available DALL-E api key: %v", tx.Error)
} }
var res imgRes var res imgRes
@ -191,7 +190,7 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
return "", fmt.Errorf("err with update database: %v", tx.Error) return "", fmt.Errorf("err with update database: %v", tx.Error)
} }
s.notifyQueue.RPush(sd.NotifyMessage{UserId: int(task.UserId), JobId: int(task.JobId), Message: sd.Finished}) s.notifyQueue.RPush(service.NotifyMessage{UserId: int(task.UserId), JobId: int(task.JobId), Message: service.TaskStatusFailed})
var content string var content string
if sync { if sync {
imgURL, err := s.downloadImage(task.JobId, int(task.UserId), res.Data[0].Url) imgURL, err := s.downloadImage(task.JobId, int(task.UserId), res.Data[0].Url)
@ -208,7 +207,7 @@ func (s *Service) CheckTaskNotify() {
go func() { go func() {
logger.Info("Running DALL-E task notify checking ...") logger.Info("Running DALL-E task notify checking ...")
for { for {
var message sd.NotifyMessage var message service.NotifyMessage
err := s.notifyQueue.LPop(&message) err := s.notifyQueue.LPop(&message)
if err != nil { if err != nil {
continue continue
@ -225,6 +224,30 @@ func (s *Service) CheckTaskNotify() {
}() }()
} }
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)
}
}()
}
func (s *Service) DownloadImages() { func (s *Service) DownloadImages() {
go func() { go func() {
var items []model.DallJob var items []model.DallJob
@ -268,47 +291,6 @@ func (s *Service) downloadImage(jobId uint, userId int, orgURL string) (string,
if res.Error != nil { if res.Error != nil {
return "", err return "", err
} }
s.notifyQueue.RPush(sd.NotifyMessage{UserId: userId, JobId: int(jobId), Message: sd.Finished}) s.notifyQueue.RPush(service.NotifyMessage{UserId: userId, JobId: int(jobId), Message: service.TaskStatusFinished})
return imgURL, nil 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)
}
}()
}

View File

@ -7,15 +7,28 @@ package mj
// * @Author yangjian102621@163.com // * @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 { "github.com/gin-gonic/gin"
Imagine(task types.MjTask) (ImageRes, error) )
Blend(task types.MjTask) (ImageRes, error)
SwapFace(task types.MjTask) (ImageRes, error) // Client MidJourney client
Upscale(task types.MjTask) (ImageRes, error) type Client struct {
Variation(task types.MjTask) (ImageRes, error) client *req.Client
QueryTask(taskId string) (QueryRes, error) licenseService *service.LicenseService
db *gorm.DB
} }
type ImageReq struct { type ImageReq struct {
@ -34,12 +47,7 @@ type ImageRes struct {
Properties struct { Properties struct {
} `json:"properties"` } `json:"properties"`
Result string `json:"result"` Result string `json:"result"`
} Channel string `json:"channel,omitempty"`
type ErrRes struct {
Error struct {
Message string `json:"message"`
} `json:"error"`
} }
type QueryRes struct { type QueryRes struct {
@ -66,3 +74,177 @@ type QueryRes struct {
Status string `json:"status"` Status string `json:"status"`
SubmitTime int `json:"submitTime"` 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
}

View File

@ -1,204 +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"
"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))
}
}
return c.doRequest(body, apiURL)
}
// 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))
}
}
}
return c.doRequest(body, apiURL)
}
// 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": "",
}
return c.doRequest(body, apiURL)
}
// 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)
return c.doRequest(body, apiURL)
}
// 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)
return c.doRequest(body, apiURL)
}
func (c *PlusClient) doRequest(body interface{}, apiURL string) (ImageRes, error) {
var res ImageRes
var errRes ErrRes
logger.Info("API URL: ", apiURL)
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{}

View File

@ -1,207 +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 (
"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().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
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 {
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
}

View File

@ -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{}

View File

@ -11,10 +11,11 @@ import (
"fmt" "fmt"
"geekai/core/types" "geekai/core/types"
"geekai/service" "geekai/service"
"geekai/service/sd" "geekai/service/oss"
"geekai/store" "geekai/store"
"geekai/store/model" "geekai/store/model"
"geekai/utils" "geekai/utils"
"github.com/go-redis/redis/v8"
"strings" "strings"
"time" "time"
@ -23,32 +24,29 @@ import (
// Service MJ 绘画服务 // Service MJ 绘画服务
type Service struct { type Service struct {
Name string // service Name client *Client // MJ Client
Client Client // MJ Client
taskQueue *store.RedisQueue taskQueue *store.RedisQueue
notifyQueue *store.RedisQueue notifyQueue *store.RedisQueue
db *gorm.DB db *gorm.DB
running bool Clients *types.LMap[uint, *types.WsClient] // UserId => Client
retryCount map[uint]int uploaderManager *oss.UploaderManager
} }
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) *Service {
return &Service{ return &Service{
Name: name,
db: db, db: db,
taskQueue: taskQueue, taskQueue: store.NewRedisQueue("MidJourney_Task_Queue", redisCli),
notifyQueue: notifyQueue, notifyQueue: store.NewRedisQueue("MidJourney_Notify_Queue", redisCli),
Client: cli, client: client,
running: true, Clients: types.NewLMap[uint, *types.WsClient](),
retryCount: make(map[uint]int), uploaderManager: manager,
} }
} }
const failedProgress = 101
func (s *Service) Run() { func (s *Service) Run() {
logger.Infof("Starting MidJourney job consumer for %s", s.Name) logger.Info("Starting MidJourney job consumer for service")
for s.running { go func() {
for {
var task types.MjTask var task types.MjTask
err := s.taskQueue.LPop(&task) err := s.taskQueue.LPop(&task)
if err != nil { if err != nil {
@ -56,23 +54,9 @@ func (s *Service) Run() {
continue continue
} }
// 如果配置了多个中转平台的 API KEY
// U,V 操作必须和 Image 操作属于同一个平台,否则找不到关联任务,需重新放回任务列表
if task.ChannelId != "" && task.ChannelId != s.Name {
if s.retryCount[task.Id] > 5 {
s.db.Model(model.MidJourneyJob{Id: task.Id}).Delete(&model.MidJourneyJob{})
continue
}
logger.Debugf("handle other service task, name: %s, channel_id: %s, drop it.", s.Name, task.ChannelId)
s.taskQueue.RPush(task)
s.retryCount[task.Id]++
time.Sleep(time.Second)
continue
}
// translate prompt // translate prompt
if utils.HasChinese(task.Prompt) { if utils.HasChinese(task.Prompt) {
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, task.Prompt), "gpt-4o-mini") content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.Prompt), "gpt-4o-mini")
if err == nil { if err == nil {
task.Prompt = content task.Prompt = content
} else { } else {
@ -81,7 +65,7 @@ func (s *Service) Run() {
} }
// translate negative prompt // translate negative prompt
if task.NegPrompt != "" && utils.HasChinese(task.NegPrompt) { if task.NegPrompt != "" && utils.HasChinese(task.NegPrompt) {
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, task.NegPrompt), "gpt-4o-mini") content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.NegPrompt), "gpt-4o-mini")
if err == nil { if err == nil {
task.NegPrompt = content task.NegPrompt = content
} else { } else {
@ -89,6 +73,11 @@ func (s *Service) Run() {
} }
} }
// use fast mode as default
if task.Mode == "" {
task.Mode = "fast"
}
var job model.MidJourneyJob var job model.MidJourneyJob
tx := s.db.Where("id = ?", task.Id).First(&job) tx := s.db.Where("id = ?", task.Id).First(&job)
if tx.Error != nil { if tx.Error != nil {
@ -96,23 +85,23 @@ func (s *Service) Run() {
continue continue
} }
logger.Infof("%s handle a new MidJourney task: %+v", s.Name, task) logger.Infof("handle a new MidJourney task: %+v", task)
var res ImageRes var res ImageRes
switch task.Type { switch task.Type {
case types.TaskImage: case types.TaskImage:
res, err = s.Client.Imagine(task) res, err = s.client.Imagine(task)
break break
case types.TaskUpscale: case types.TaskUpscale:
res, err = s.Client.Upscale(task) res, err = s.client.Upscale(task)
break break
case types.TaskVariation: case types.TaskVariation:
res, err = s.Client.Variation(task) res, err = s.client.Variation(task)
break break
case types.TaskBlend: case types.TaskBlend:
res, err = s.Client.Blend(task) res, err = s.client.Blend(task)
break break
case types.TaskSwapFace: case types.TaskSwapFace:
res, err = s.Client.SwapFace(task) res, err = s.client.SwapFace(task)
break break
} }
@ -125,25 +114,22 @@ func (s *Service) Run() {
} }
logger.Error("绘画任务执行失败:", errMsg) logger.Error("绘画任务执行失败:", errMsg)
job.Progress = failedProgress job.Progress = service.FailTaskProgress
job.ErrMsg = errMsg job.ErrMsg = errMsg
// update the task progress // update the task progress
s.db.Updates(&job) s.db.Updates(&job)
// 任务失败,通知前端 // 任务失败,通知前端
s.notifyQueue.RPush(sd.NotifyMessage{UserId: task.UserId, JobId: int(job.Id), Message: sd.Failed}) s.notifyQueue.RPush(service.NotifyMessage{UserId: task.UserId, JobId: int(job.Id), Message: service.TaskStatusFailed})
continue continue
} }
logger.Infof("任务提交成功:%+v", res) logger.Infof("任务提交成功:%+v", res)
// 更新任务 ID/频道 // 更新任务 ID/频道
job.TaskId = res.Result job.TaskId = res.Result
job.MessageId = res.Result job.MessageId = res.Result
job.ChannelId = s.Name job.ChannelId = res.Channel
s.db.Updates(&job) s.db.Updates(&job)
} }
} }()
func (s *Service) Stop() {
s.running = false
} }
type CBReq struct { type CBReq struct {
@ -164,20 +150,122 @@ type CBReq struct {
} `json:"properties"` } `json:"properties"`
} }
func (s *Service) Notify(job model.MidJourneyJob) error { func GetImageHash(action string) string {
task, err := s.Client.QueryTask(job.TaskId) split := strings.Split(action, "::")
if len(split) > 5 {
return split[4]
}
return split[len(split)-1]
}
func (s *Service) CheckTaskNotify() {
go func() {
for {
var message service.NotifyMessage
err := s.notifyQueue.LPop(&message)
if err != nil { if err != nil {
return err continue
}
cli := s.Clients.Get(uint(message.UserId))
if cli == nil {
continue
}
err = cli.Send([]byte(message.Message))
if err != nil {
continue
}
}
}()
}
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)
cli := s.Clients.Get(uint(v.UserId))
if cli == nil {
continue
}
err = cli.Send([]byte(service.TaskStatusFinished))
if err != nil {
continue
}
}
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 {
res := s.db.Where("progress < ?", 100).Where("channel_id <> ?", "").Find(&jobs)
if res.Error != 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
}
task, err := s.client.QueryTask(job.TaskId, job.ChannelId)
if err != nil {
logger.Errorf("error with query task: %v", err)
continue
} }
// 任务执行失败了 // 任务执行失败了
if task.FailReason != "" { if task.FailReason != "" {
s.db.Model(&model.MidJourneyJob{Id: job.Id}).UpdateColumns(map[string]interface{}{ s.db.Model(&model.MidJourneyJob{Id: job.Id}).UpdateColumns(map[string]interface{}{
"progress": failedProgress, "progress": service.FailTaskProgress,
"err_msg": task.FailReason, "err_msg": task.FailReason,
}) })
s.notifyQueue.RPush(sd.NotifyMessage{UserId: job.UserId, JobId: int(job.Id), Message: sd.Failed}) logger.Errorf("task failed: %v", task.FailReason)
return fmt.Errorf("task failed: %v", task.FailReason) s.notifyQueue.RPush(service.NotifyMessage{UserId: job.UserId, JobId: int(job.Id), Message: service.TaskStatusFailed})
continue
} }
if len(task.Buttons) > 0 { if len(task.Buttons) > 0 {
@ -189,25 +277,23 @@ func (s *Service) Notify(job model.MidJourneyJob) error {
if task.ImageUrl != "" { if task.ImageUrl != "" {
job.OrgURL = task.ImageUrl job.OrgURL = task.ImageUrl
} }
tx := s.db.Updates(&job) err = s.db.Updates(&job).Error
if tx.Error != nil { if err != nil {
return fmt.Errorf("error with update database: %v", tx.Error) logger.Errorf("error with update database: %v", err)
continue
} }
// 通知前端更新任务进度 // 通知前端更新任务进度
if oldProgress != job.Progress { if oldProgress != job.Progress {
message := sd.Running message := service.TaskStatusRunning
if job.Progress == 100 { if job.Progress == 100 {
message = sd.Finished message = service.TaskStatusFinished
}
s.notifyQueue.RPush(service.NotifyMessage{UserId: job.UserId, JobId: int(job.Id), Message: message})
} }
s.notifyQueue.RPush(sd.NotifyMessage{UserId: job.UserId, JobId: int(job.Id), Message: message})
} }
return nil
}
func GetImageHash(action string) string { time.Sleep(time.Second * 5)
split := strings.Split(action, "::")
if len(split) > 5 {
return split[4]
} }
return split[len(split)-1] }()
} }

View File

@ -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
}

View File

@ -10,50 +10,49 @@ package sd
import ( import (
"fmt" "fmt"
"geekai/core/types" "geekai/core/types"
logger2 "geekai/logger"
"geekai/service" "geekai/service"
"geekai/service/oss" "geekai/service/oss"
"geekai/store" "geekai/store"
"geekai/store/model" "geekai/store/model"
"geekai/utils" "geekai/utils"
"strings" "github.com/go-redis/redis/v8"
"time" "time"
"github.com/imroc/req/v3" "github.com/imroc/req/v3"
"gorm.io/gorm" "gorm.io/gorm"
) )
var logger = logger2.GetLogger()
// SD 绘画服务 // SD 绘画服务
type Service struct { type Service struct {
httpClient *req.Client httpClient *req.Client
config types.StableDiffusionConfig
taskQueue *store.RedisQueue taskQueue *store.RedisQueue
notifyQueue *store.RedisQueue notifyQueue *store.RedisQueue
db *gorm.DB db *gorm.DB
uploadManager *oss.UploaderManager uploadManager *oss.UploaderManager
name string // service name
leveldb *store.LevelDB leveldb *store.LevelDB
running bool // 运行状态 Clients *types.LMap[uint, *types.WsClient] // UserId => Client
} }
func NewService(name string, config types.StableDiffusionConfig, taskQueue *store.RedisQueue, notifyQueue *store.RedisQueue, db *gorm.DB, manager *oss.UploaderManager, levelDB *store.LevelDB) *Service { func NewService(db *gorm.DB, manager *oss.UploaderManager, levelDB *store.LevelDB, redisCli *redis.Client) *Service {
config.ApiURL = strings.TrimRight(config.ApiURL, "/")
return &Service{ return &Service{
name: name,
config: config,
httpClient: req.C(), httpClient: req.C(),
taskQueue: taskQueue, taskQueue: store.NewRedisQueue("StableDiffusion_Task_Queue", redisCli),
notifyQueue: notifyQueue, notifyQueue: store.NewRedisQueue("StableDiffusion_Queue", redisCli),
db: db, db: db,
leveldb: levelDB, leveldb: levelDB,
Clients: types.NewLMap[uint, *types.WsClient](),
uploadManager: manager, uploadManager: manager,
running: true,
} }
} }
func (s *Service) Run() { func (s *Service) Run() {
logger.Infof("Starting Stable-Diffusion job consumer for %s", s.name) logger.Infof("Starting Stable-Diffusion job consumer")
for s.running { go func() {
for {
var task types.SdTask var task types.SdTask
err := s.taskQueue.LPop(&task) err := s.taskQueue.LPop(&task)
if err != nil { if err != nil {
@ -81,24 +80,21 @@ func (s *Service) Run() {
} }
} }
logger.Infof("%s handle a new Stable-Diffusion task: %+v", s.name, task) logger.Infof("handle a new Stable-Diffusion task: %+v", task)
err = s.Txt2Img(task) err = s.Txt2Img(task)
if err != nil { if err != nil {
logger.Error("绘画任务执行失败:", err.Error()) logger.Error("绘画任务执行失败:", err.Error())
// update the task progress // update the task progress
s.db.Model(&model.SdJob{Id: uint(task.Id)}).UpdateColumns(map[string]interface{}{ s.db.Model(&model.SdJob{Id: uint(task.Id)}).UpdateColumns(map[string]interface{}{
"progress": -1, "progress": service.FailTaskProgress,
"err_msg": err.Error(), "err_msg": err.Error(),
}) })
// 通知前端,任务失败 // 通知前端,任务失败
s.notifyQueue.RPush(NotifyMessage{UserId: task.UserId, JobId: task.Id, Message: Failed}) s.notifyQueue.RPush(service.NotifyMessage{UserId: task.UserId, JobId: task.Id, Message: service.TaskStatusFailed})
continue continue
} }
} }
} }()
func (s *Service) Stop() {
s.running = false
} }
// Txt2ImgReq 文生图请求实体 // Txt2ImgReq 文生图请求实体
@ -160,12 +156,19 @@ func (s *Service) Txt2Img(task types.SdTask) error {
} }
var res Txt2ImgResp var res Txt2ImgResp
var errChan = make(chan error) 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) logger.Debugf("send image request to %s", apiURL)
// send a request to sd api endpoint // send a request to sd api endpoint
go func() { go func() {
response, err := s.httpClient.R(). response, err := s.httpClient.R().
SetHeader("Authorization", s.config.ApiKey). SetHeader("Authorization", apiKey.Value).
SetBody(body). SetBody(body).
SetSuccessResult(&res). SetSuccessResult(&res).
Post(apiURL) Post(apiURL)
@ -178,6 +181,10 @@ func (s *Service) Txt2Img(task types.SdTask) error {
return return
} }
// update the last used time
apiKey.LastUsedAt = time.Now().Unix()
s.db.Updates(&apiKey)
// 保存 Base64 图片 // 保存 Base64 图片
imgURL, err := s.uploadManager.GetUploadHandler().PutBase64(res.Images[0]) imgURL, err := s.uploadManager.GetUploadHandler().PutBase64(res.Images[0])
if err != nil { if err != nil {
@ -206,17 +213,17 @@ func (s *Service) Txt2Img(task types.SdTask) error {
// task finished // task finished
s.db.Model(&model.SdJob{Id: uint(task.Id)}).UpdateColumn("progress", 100) s.db.Model(&model.SdJob{Id: uint(task.Id)}).UpdateColumn("progress", 100)
s.notifyQueue.RPush(NotifyMessage{UserId: task.UserId, JobId: task.Id, Message: Finished}) s.notifyQueue.RPush(service.NotifyMessage{UserId: task.UserId, JobId: task.Id, Message: service.TaskStatusFinished})
// 从 leveldb 中删除预览图片数据 // 从 leveldb 中删除预览图片数据
_ = s.leveldb.Delete(task.Params.TaskId) _ = s.leveldb.Delete(task.Params.TaskId)
return nil return nil
default: default:
err, resp := s.checkTaskProgress() err, resp := s.checkTaskProgress(apiKey)
// 更新任务进度 // 更新任务进度
if err == nil && resp.Progress > 0 { if err == nil && resp.Progress > 0 {
s.db.Model(&model.SdJob{Id: uint(task.Id)}).UpdateColumn("progress", int(resp.Progress*100)) 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}) s.notifyQueue.RPush(service.NotifyMessage{UserId: task.UserId, JobId: task.Id, Message: service.TaskStatusRunning})
// 保存预览图片数据 // 保存预览图片数据
if resp.CurrentImage != "" { if resp.CurrentImage != "" {
_ = s.leveldb.Put(task.Params.TaskId, resp.CurrentImage) _ = s.leveldb.Put(task.Params.TaskId, resp.CurrentImage)
@ -229,11 +236,11 @@ func (s *Service) Txt2Img(task types.SdTask) error {
} }
// 执行任务 // 执行任务
func (s *Service) checkTaskProgress() (error, *TaskProgressResp) { func (s *Service) checkTaskProgress(apiKey model.ApiKey) (error, *TaskProgressResp) {
apiURL := fmt.Sprintf("%s/sdapi/v1/progress?skip_current_image=false", s.config.ApiURL) apiURL := fmt.Sprintf("%s/sdapi/v1/progress?skip_current_image=false", apiKey.ApiURL)
var res TaskProgressResp var res TaskProgressResp
response, err := s.httpClient.R(). response, err := s.httpClient.R().
SetHeader("Authorization", s.config.ApiKey). SetHeader("Authorization", apiKey.Value).
SetSuccessResult(&res). SetSuccessResult(&res).
Get(apiURL) Get(apiURL)
if err != nil { if err != nil {
@ -245,3 +252,54 @@ func (s *Service) checkTaskProgress() (error, *TaskProgressResp) {
return nil, &res 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
}
client := s.Clients.Get(uint(message.UserId))
if client == nil {
continue
}
err = client.Send([]byte(message.Message))
if err != nil {
continue
}
}
}()
}
// 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)
}
}()
}

View File

@ -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"
)

View File

@ -13,8 +13,8 @@ import (
"fmt" "fmt"
"geekai/core/types" "geekai/core/types"
logger2 "geekai/logger" logger2 "geekai/logger"
"geekai/service"
"geekai/service/oss" "geekai/service/oss"
"geekai/service/sd"
"geekai/store" "geekai/store"
"geekai/store/model" "geekai/store/model"
"geekai/utils" "geekai/utils"
@ -88,7 +88,7 @@ func (s *Service) Run() {
logger.Errorf("create task with error: %v", err) logger.Errorf("create task with error: %v", err)
s.db.Model(&model.SunoJob{Id: task.Id}).UpdateColumns(map[string]interface{}{ s.db.Model(&model.SunoJob{Id: task.Id}).UpdateColumns(map[string]interface{}{
"err_msg": err.Error(), "err_msg": err.Error(),
"progress": 101, "progress": service.FailTaskProgress,
}) })
continue continue
} }
@ -157,6 +157,9 @@ func (s *Service) Create(task types.SunoTask) (RespVo, error) {
if res.Code != "success" { if res.Code != "success" {
return RespVo{}, fmt.Errorf("API 返回失败:%s", res.Message) 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 res.Channel = apiKey.ApiURL
return res, nil return res, nil
} }
@ -165,7 +168,7 @@ func (s *Service) CheckTaskNotify() {
go func() { go func() {
logger.Info("Running Suno task notify checking ...") logger.Info("Running Suno task notify checking ...")
for { for {
var message sd.NotifyMessage var message service.NotifyMessage
err := s.notifyQueue.LPop(&message) err := s.notifyQueue.LPop(&message)
if err != nil { if err != nil {
continue continue
@ -210,7 +213,7 @@ func (s *Service) DownloadImages() {
v.AudioURL = audioURL v.AudioURL = audioURL
v.Progress = 100 v.Progress = 100
s.db.Updates(&v) s.db.Updates(&v)
s.notifyQueue.RPush(sd.NotifyMessage{UserId: v.UserId, JobId: int(v.Id), Message: sd.Finished}) s.notifyQueue.RPush(service.NotifyMessage{UserId: v.UserId, JobId: int(v.Id), Message: service.TaskStatusFinished})
} }
time.Sleep(time.Second * 10) time.Sleep(time.Second * 10)
@ -278,10 +281,10 @@ func (s *Service) SyncTaskProgress() {
tx.Commit() tx.Commit()
} else if task.Data.FailReason != "" { } else if task.Data.FailReason != "" {
job.Progress = 101 job.Progress = service.FailTaskProgress
job.ErrMsg = task.Data.FailReason job.ErrMsg = task.Data.FailReason
s.db.Updates(&job) s.db.Updates(&job)
s.notifyQueue.RPush(sd.NotifyMessage{UserId: job.UserId, JobId: int(job.Id), Message: sd.Failed}) s.notifyQueue.RPush(service.NotifyMessage{UserId: job.UserId, JobId: int(job.Id), Message: service.TaskStatusFailed})
} }
} }

View File

@ -1,4 +1,17 @@
package service package service
const FailTaskProgress = 101
const (
TaskStatusRunning = "RUNNING"
TaskStatusFinished = "FINISH"
TaskStatusFailed = "FAIL"
)
type NotifyMessage struct {
UserId int `json:"user_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 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]" 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]"

View File

@ -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))
}

View File

@ -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
}

16
api/store/model/redeem.go Normal file
View File

@ -0,0 +1,16 @@
package model
import "time"
// 兑换码
type Redeem struct {
Id uint `gorm:"primarykey;column:id"`
UserId uint // 用户 ID
Name string // 名称
Power int // 算力
Code string // 兑换码
Enabled bool // 启用状态
RedeemedAt int64 // 兑换时间
CreatedAt time.Time
}

View File

@ -1,13 +0,0 @@
package model
// 用户打赏
type Reward struct {
BaseModel
UserId uint // 用户 ID
TxId string // 交易ID
Amount float64 // 打赏金额
Remark string // 打赏备注
Status bool // 核销状态
Exchange string // 众筹兑换详情JSON
}

13
api/store/vo/redeem.go Normal file
View File

@ -0,0 +1,13 @@
package vo
type Redeem struct {
Id uint `json:"id"`
UserId uint `json:"user_id"` // 用户 ID
Name string `json:"name"`
Username string `json:"username"`
Power int `json:"power"` // 算力
Code string `json:"code"` // 兑换码
Enabled bool `json:"enabled"`
RedeemedAt int64 `json:"redeemed_at"` // 兑换时间
CreatedAt int64 `json:"created_at"`
}

View File

@ -1,16 +0,0 @@
package vo
type Reward struct {
BaseVo
UserId uint `json:"user_id"` // 用户 ID
Username string `json:"username"`
TxId string `json:"tx_id"` // 交易ID
Amount float64 `json:"amount"` // 打赏金额
Remark string `json:"remark"` // 打赏备注
Status bool `json:"status"` // 核销状态
Exchange RewardExchange `json:"exchange"`
}
type RewardExchange struct {
Power int `json:"power"`
}

View File

@ -1,16 +1,55 @@
package main package main
import ( import (
"crypto/rand"
"encoding/hex"
"fmt" "fmt"
"geekai/utils" "sync"
) )
func main() { const (
file := "http://nk.img.r9it.com/chatgpt-plus/1719389335351828.xlsx" codeLength = 32 // 兑换码长度
content, err := utils.ReadFileContent(file, "http://172.22.11.69:9998") )
var (
codeMap = make(map[string]bool)
mapMutex = &sync.Mutex{}
)
// GenerateUniqueCode 生成唯一兑换码
func GenerateUniqueCode() (string, error) {
for {
code, err := generateCode()
if err != nil { if err != nil {
panic(err) return "", err
} }
fmt.Println(content) mapMutex.Lock()
if !codeMap[code] {
codeMap[code] = true
mapMutex.Unlock()
return code, nil
}
mapMutex.Unlock()
}
}
// generateCode 生成兑换码
func generateCode() (string, error) {
bytes := make([]byte, codeLength/2) // 因为 hex 编码会使长度翻倍
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return hex.EncodeToString(bytes), nil
}
func main() {
for i := 0; i < 10; i++ {
code, err := GenerateUniqueCode()
if err != nil {
fmt.Println("Error generating code:", err)
return
}
fmt.Println("Generated code:", code)
}
} }

View File

@ -64,6 +64,7 @@ func OpenAIRequest(db *gorm.DB, prompt string, modelName string) (string, error)
client.SetProxyURL(apiKey.ApiURL) client.SetProxyURL(apiKey.ApiURL)
} }
apiURL := fmt.Sprintf("%s/v1/chat/completions", apiKey.ApiURL) apiURL := fmt.Sprintf("%s/v1/chat/completions", apiKey.ApiURL)
logger.Debugf("Sending %s request, API KEY:%s, PROXY: %s, Model: %s", apiKey.ApiURL, apiURL, apiKey.ProxyURL, modelName)
r, err := client.R().SetHeader("Content-Type", "application/json"). r, err := client.R().SetHeader("Content-Type", "application/json").
SetHeader("Authorization", "Bearer "+apiKey.Value). SetHeader("Authorization", "Bearer "+apiKey.Value).
SetBody(types.ApiRequest{ SetBody(types.ApiRequest{
@ -76,6 +77,11 @@ func OpenAIRequest(db *gorm.DB, prompt string, modelName string) (string, error)
if err != nil { if err != nil {
return "", fmt.Errorf("请求 OpenAI API失败%v", err) return "", fmt.Errorf("请求 OpenAI API失败%v", err)
} }
if r.IsErrorState() {
return "", fmt.Errorf("请求 OpenAI API失败%v", r.Status)
}
body, _ := io.ReadAll(r.Body) body, _ := io.ReadAll(r.Body)
err = json.Unmarshal(body, &response) err = json.Unmarshal(body, &response)
if err != nil { if err != nil {

View File

@ -8,14 +8,16 @@ package utils
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import ( import (
"crypto/rand"
"encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"math/rand"
"strings" "strings"
"time" "time"
"unicode" "unicode"
"golang.org/x/crypto/sha3" "golang.org/x/crypto/sha3"
rand2 "math/rand"
) )
// RandString generate rand string with specified length // RandString generate rand string with specified length
@ -23,7 +25,7 @@ func RandString(length int) string {
str := "0123456789abcdefghijklmnopqrstuvwxyz" str := "0123456789abcdefghijklmnopqrstuvwxyz"
data := []byte(str) data := []byte(str)
var result []byte var result []byte
r := rand.New(rand.NewSource(time.Now().UnixNano())) r := rand2.New(rand2.NewSource(time.Now().UnixNano()))
for i := 0; i < length; i++ { for i := 0; i < length; i++ {
result = append(result, data[r.Intn(len(data))]) result = append(result, data[r.Intn(len(data))])
} }
@ -34,8 +36,8 @@ func RandomNumber(bit int) int {
minNum := intPow(10, bit-1) minNum := intPow(10, bit-1)
maxNum := intPow(10, bit) - 1 maxNum := intPow(10, bit) - 1
rand.NewSource(time.Now().UnixNano()) rand2.NewSource(time.Now().UnixNano())
return rand.Intn(maxNum-minNum+1) + minNum return rand2.Intn(maxNum-minNum+1) + minNum
} }
func intPow(x, y int) int { func intPow(x, y int) int {
@ -124,3 +126,11 @@ func HasChinese(text string) bool {
} }
return false return false
} }
func GenRedeemCode(codeLength int) (string, error) {
bytes := make([]byte, codeLength/2)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return hex.EncodeToString(bytes), nil
}

View File

@ -3,7 +3,7 @@
-- https://www.phpmyadmin.net/ -- https://www.phpmyadmin.net/
-- --
-- 主机: 127.0.0.1 -- 主机: 127.0.0.1
-- 生成日期: 2024-11-14 16:00:22 -- 生成日期: 2024-11-27 14:58:38
-- 服务器版本: 8.0.33 -- 服务器版本: 8.0.33
-- PHP 版本: 8.1.2-1ubuntu2.19 -- PHP 版本: 8.1.2-1ubuntu2.19
@ -215,8 +215,8 @@ CREATE TABLE `chatgpt_configs` (
-- --
INSERT INTO `chatgpt_configs` (`id`, `marker`, `config_json`) VALUES INSERT INTO `chatgpt_configs` (`id`, `marker`, `config_json`) VALUES
(1, 'system', '{\"title\":\"GeekAI 创作系统\",\"slogan\":\"你有多少想象力AI 就有多大创造力。我辈之人,先干为敬,陪您先把 AI 用起来。\",\"admin_title\":\"GeekAI 控制台\",\"logo\":\"http://localhost:5678/static/upload/2024/4/1714382860986912.png\",\"init_power\":100,\"daily_power\":99,\"invite_power\":1024,\"vip_month_power\":1000,\"register_ways\":[\"mobile\",\"username\",\"email\"],\"enabled_register\":true,\"reward_img\":\"http://localhost:5678/static/upload/2024/3/1710753716309668.jpg\",\"enabled_reward\":true,\"power_price\":0.1,\"order_pay_timeout\":600,\"vip_info_text\":\"月度会员,年度会员每月赠送 1000 点算力,赠送算力当月有效当月没有消费完的算力不结余到下个月。 点卡充值的算力长期有效。\",\"default_models\":[11,7,1,10,12,19,18,17,3],\"mj_power\":30,\"mj_action_power\":10,\"sd_power\":10,\"dall_power\":15,\"wechat_card_url\":\"/images/wx.png\",\"enable_context\":true,\"context_deep\":4,\"sd_neg_prompt\":\"nsfw, paintings,low quality,easynegative,ng_deepnegative ,lowres,bad anatomy,bad hands,bad feet\",\"index_bg_url\":\"color\",\"index_navs\":[1,5,6,13,9,12],\"copyright\":\"\"}'), (1, 'system', '{\"title\":\"GeekAI 创作系统\",\"slogan\":\"你有多少想象力AI 就有多大创造力。我辈之人,先干为敬,陪您先把 AI 用起来。\",\"admin_title\":\"GeekAI 控制台\",\"logo\":\"http://localhost:5678/static/upload/2024/4/1714382860986912.png\",\"init_power\":100,\"daily_power\":99,\"invite_power\":1024,\"vip_month_power\":1000,\"register_ways\":[\"mobile\",\"username\",\"email\"],\"enabled_register\":true,\"order_pay_timeout\":600,\"vip_info_text\":\"月度会员,年度会员每月赠送 1000 点算力,赠送算力当月有效当月没有消费完的算力不结余到下个月。 点卡充值的算力长期有效。\",\"default_models\":[1],\"mj_power\":30,\"mj_action_power\":10,\"sd_power\":10,\"dall_power\":15,\"wechat_card_url\":\"/images/wx.png\",\"enable_context\":true,\"context_deep\":4,\"sd_neg_prompt\":\"nsfw, paintings,low quality,easynegative,ng_deepnegative ,lowres,bad anatomy,bad hands,bad feet\",\"mj_mode\":\"fast\",\"index_bg_url\":\"color\",\"index_navs\":[1,5,6,13,9,12],\"copyright\":\"\",\"mark_map_text\":\"# GeekAI 演示站\\n\\n- 完整的开源系统,前端应用和后台管理系统皆可开箱即用。\\n- 基于 Websocket 实现,完美的打字机体验。\\n- 内置了各种预训练好的角色应用,轻松满足你的各种聊天和应用需求。\\n- 支持 OPenAIAzure文心一言讯飞星火清华 ChatGLM等多个大语言模型。\\n- 支持 MidJourney / Stable Diffusion AI 绘画集成,开箱即用。\\n- 支持使用个人微信二维码作为充值收费的支付渠道,无需企业支付通道。\\n- 已集成支付宝支付功能,微信支付,支持多种会员套餐和点卡购买功能。\\n- 集成插件 API 功能,可结合大语言模型的 function 功能开发各种强大的插件。\"}'),
(3, 'notice', '{\"sd_neg_prompt\":\"\",\"index_bg_url\":\"\",\"index_navs\":null,\"copyright\":\"\",\"content\":\"## v4.1.1 更新日志\\n\\n* Bug修复修复 GPT 模型 function call 调用后没有输出的问题\\n* 功能新增:允许获取 License 授权用户可以自定义版权信息\\n* 功能新增:聊天对话框支持粘贴剪切板内容来上传截图和文件\\n* 功能优化:增加 session 和系统配置缓存,确保每个页面只进行一次 session 和 get system config 请求\\n* 功能优化:在应用列表页面,无需先添加模型到用户工作区,可以直接使用\\n* 功能新增MJ 绘图失败的任务不会自动删除,而是会在列表页显示失败详细错误信息\\n* 功能新增:允许在设置首页纯色背景,背景图片,随机背景图片三种背景模式\\n* 功能新增:允许在管理后台设置首页显示的导航菜单\\n* Bug修复修复注册页面先显示关闭注册组件然后再显示注册组件\\n* 功能新增:增加 Suno 文生歌曲功能\\n* 功能优化:移除多平台模型支持,统一使用 one-api 接口形式,其他平台的模型需要通过 one-api 接口添加\\n* 功能优化:在所有列表页面增加返回顶部按钮\\n\\n注意当前站点仅为开源项目 \\u003ca style=\\\"color: #F56C6C\\\" href=\\\"https://github.com/yangjian102621/chatgpt-plus\\\" target=\\\"_blank\\\"\\u003eChatPlus\\u003c/a\\u003e 的演示项目,本项目单纯就是给大家体验项目功能使用。\\n\\u003cstrong style=\\\"color: #F56C6C\\\"\\u003e体验额度用完之后请不要在当前站点进行任何充值操作\\u003c/strong\\u003e\\n\\u003cstrong style=\\\"color: #F56C6C\\\"\\u003e体验额度用完之后请不要在当前站点进行任何充值操作\\u003c/strong\\u003e\\n\\u003cstrong style=\\\"color: #F56C6C\\\"\\u003e体验额度用完之后请不要在当前站点进行任何充值操作\\u003c/strong\\u003e\\n 如果觉得好用你就花几分钟自己部署一套没有API KEY 的同学可以去下面几个推荐的中转站购买:\\n1、\\u003ca href=\\\"https://api.chat-plus.net\\\" target=\\\"_blank\\\"\\n style=\\\"font-size: 20px;color:#F56C6C\\\"\\u003ehttps://api.chat-plus.net\\u003c/a\\u003e\\n2、\\u003ca href=\\\"https://api.geekai.me\\\" target=\\\"_blank\\\"\\n style=\\\"font-size: 20px;color:#F56C6C\\\"\\u003ehttps://api.geekai.me\\u003c/a\\u003e\\n3、 \\u003ca href=\\\"https://gpt.bemore.lol\\\" target=\\\"_blank\\\"\\n style=\\\"font-size: 20px;color:#F56C6C\\\"\\u003ehttps://gpt.bemore.lol\\u003c/a\\u003e\\n支持MidJourneyGPTClaudeGoogle Gemmi以及国内各个厂家的大模型现在有超级优惠价格远低于 OpenAI 官方。关于中转 API 的优势和劣势请参考 [中转API技术原理](https://ai.r9it.com/docs/install/errors-handle.html#%E8%B0%83%E7%94%A8%E4%B8%AD%E8%BD%AC-api-%E6%8A%A5%E9%94%99%E6%97%A0%E5%8F%AF%E7%94%A8%E6%B8%A0%E9%81%93)。GPT-3.5GPT-4DALL-E3 绘图......你都可以随意使用,无需魔法。\\n接入教程 \\u003ca href=\\\"https://ai.r9it.com/docs/install/\\\" target=\\\"_blank\\\"\\n style=\\\"font-size: 20px;color:#F56C6C\\\"\\u003ehttps://ai.r9it.com/docs/install/\\u003c/a\\u003e\\n本项目源码地址\\u003ca href=\\\"https://github.com/yangjian102621/chatgpt-plus\\\" target=\\\"_blank\\\"\\u003ehttps://github.com/yangjian102621/chatgpt-plus\\u003c/a\\u003e\",\"updated\":true}'); (3, 'notice', '{\"sd_neg_prompt\":\"\",\"mj_mode\":\"\",\"index_bg_url\":\"\",\"index_navs\":null,\"copyright\":\"\",\"mark_map_text\":\"\",\"content\":\"## v4.1.2 更新日志\\n\\n* Bug修复修复思维导图页面获取模型失败的问题\\n* 功能优化优化MJ,SD,DALL-E 任务列表页面,显示失败任务的错误信息,删除失败任务可以恢复扣减算力\\n* Bug修复修复后台拖动排序组件 Bug\\n* 功能优化:更新数据库失败时候显示具体的的报错信息\\n* Bug修复修复管理后台对话详情页内容显示异常问题\\n* 功能优化:管理后台新增清空所有未支付订单的功能\\n* 功能优化:给会话信息和系统配置数据加上缓存功能,减少 http 请求\\n* 功能新增:移除微信机器人收款功能,增加卡密功能,支持用户使用卡密兑换算力\\n\\n注意当前站点仅为开源项目 \\u003ca style=\\\"color: #F56C6C\\\" href=\\\"https://github.com/yangjian102621/chatgpt-plus\\\" target=\\\"_blank\\\"\\u003eChatPlus\\u003c/a\\u003e 的演示项目,本项目单纯就是给大家体验项目功能使用。\\n\\u003cstrong style=\\\"color: #F56C6C\\\"\\u003e体验额度用完之后请不要在当前站点进行任何充值操作\\u003c/strong\\u003e\\n\\u003cstrong style=\\\"color: #F56C6C\\\"\\u003e体验额度用完之后请不要在当前站点进行任何充值操作\\u003c/strong\\u003e\\n\\u003cstrong style=\\\"color: #F56C6C\\\"\\u003e体验额度用完之后请不要在当前站点进行任何充值操作\\u003c/strong\\u003e\\n 如果觉得好用你就花几分钟自己部署一套没有API KEY 的同学可以去下面几个推荐的中转站购买:\\n1、\\u003ca href=\\\"https://api.chat-plus.net\\\" target=\\\"_blank\\\"\\n style=\\\"font-size: 20px;color:#F56C6C\\\"\\u003ehttps://api.chat-plus.net\\u003c/a\\u003e\\n2、\\u003ca href=\\\"https://api.geekai.me\\\" target=\\\"_blank\\\"\\n style=\\\"font-size: 20px;color:#F56C6C\\\"\\u003ehttps://api.geekai.me\\u003c/a\\u003e\\n3、 \\u003ca href=\\\"https://gpt.bemore.lol\\\" target=\\\"_blank\\\"\\n style=\\\"font-size: 20px;color:#F56C6C\\\"\\u003ehttps://gpt.bemore.lol\\u003c/a\\u003e\\n支持MidJourneyGPTClaudeGoogle Gemmi以及国内各个厂家的大模型现在有超级优惠价格远低于 OpenAI 官方。关于中转 API 的优势和劣势请参考 [中转API技术原理](https://ai.r9it.com/docs/install/errors-handle.html#%E8%B0%83%E7%94%A8%E4%B8%AD%E8%BD%AC-api-%E6%8A%A5%E9%94%99%E6%97%A0%E5%8F%AF%E7%94%A8%E6%B8%A0%E9%81%93)。GPT-3.5GPT-4DALL-E3 绘图......你都可以随意使用,无需魔法。\\n接入教程 \\u003ca href=\\\"https://ai.r9it.com/docs/install/\\\" target=\\\"_blank\\\"\\n style=\\\"font-size: 20px;color:#F56C6C\\\"\\u003ehttps://ai.r9it.com/docs/install/\\u003c/a\\u003e\\n本项目源码地址\\u003ca href=\\\"https://github.com/yangjian102621/chatgpt-plus\\\" target=\\\"_blank\\\"\\u003ehttps://github.com/yangjian102621/chatgpt-plus\\u003c/a\\u003e\",\"updated\":true}');
-- -------------------------------------------------------- -- --------------------------------------------------------
@ -450,10 +450,6 @@ CREATE TABLE `chatgpt_products` (
-- --
INSERT INTO `chatgpt_products` (`id`, `name`, `price`, `discount`, `days`, `power`, `enabled`, `sales`, `sort_num`, `created_at`, `updated_at`, `app_url`, `url`) VALUES INSERT INTO `chatgpt_products` (`id`, `name`, `price`, `discount`, `days`, `power`, `enabled`, `sales`, `sort_num`, `created_at`, `updated_at`, `app_url`, `url`) VALUES
(1, '会员1个月', 1999.90, 1999.00, 30, 0, 1, 1, 0, '2023-08-28 10:48:57', '2024-04-26 16:09:06', NULL, NULL),
(2, '会员3个月', 3940.00, 30.00, 90, 0, 1, 0, 0, '2023-08-28 10:52:22', '2024-03-22 17:56:10', NULL, NULL),
(3, '会员6个月', 5990.00, 100.00, 180, 0, 1, 0, 0, '2023-08-28 10:53:39', '2024-03-22 17:56:15', NULL, NULL),
(4, '会员12个月', 9980.00, 200.00, 365, 0, 1, 0, 0, '2023-08-28 10:54:15', '2024-03-22 17:56:23', NULL, NULL),
(5, '100次点卡', 9.99, 9.98, 0, 100, 1, 7, 0, '2023-08-28 10:55:08', '2024-06-11 16:48:44', NULL, NULL), (5, '100次点卡', 9.99, 9.98, 0, 100, 1, 7, 0, '2023-08-28 10:55:08', '2024-06-11 16:48:44', NULL, NULL),
(6, '200次点卡', 19.90, 15.00, 0, 200, 1, 1, 0, '1970-01-01 08:00:00', '2024-06-11 11:41:52', NULL, NULL); (6, '200次点卡', 19.90, 15.00, 0, 200, 1, 1, 0, '1970-01-01 08:00:00', '2024-06-11 11:41:52', NULL, NULL);
@ -584,19 +580,7 @@ CREATE TABLE `chatgpt_users` (
-- --
INSERT INTO `chatgpt_users` (`id`, `username`, `nickname`, `password`, `avatar`, `salt`, `power`, `expired_time`, `status`, `chat_config_json`, `chat_roles_json`, `chat_models_json`, `last_login_at`, `vip`, `last_login_ip`, `openid`, `platform`, `created_at`, `updated_at`) VALUES INSERT INTO `chatgpt_users` (`id`, `username`, `nickname`, `password`, `avatar`, `salt`, `power`, `expired_time`, `status`, `chat_config_json`, `chat_roles_json`, `chat_models_json`, `last_login_at`, `vip`, `last_login_ip`, `openid`, `platform`, `created_at`, `updated_at`) VALUES
(4, '18575670125', '极客学长@830270', 'ccc3fb7ab61b8b5d096a4a166ae21d121fc38c71bbd1be6173d9ab973214a63b', 'http://localhost:5678/static/upload/2024/5/1715651569509929.png', 'ueedue5l', 7292, 0, 1, '{\"api_keys\":{\"Azure\":\"\",\"ChatGLM\":\"\",\"OpenAI\":\"\"}}', '[\"red_book\",\"gpt\",\"seller\",\"artist\",\"lu_xun\",\"girl_friend\",\"psychiatrist\",\"teacher\",\"programmer\",\"test\",\"qing_gan_da_shi\"]', '[1,11]', 1731554762, 1, '172.22.11.200', NULL, NULL, '2023-06-12 16:47:17', '2024-11-14 11:26:02'), (4, '18888888888', '极客学长@830270', 'ccc3fb7ab61b8b5d096a4a166ae21d121fc38c71bbd1be6173d9ab973214a63b', 'http://localhost:5678/static/upload/2024/5/1715651569509929.png', 'ueedue5l', 7291, 0, 1, '{\"api_keys\":{\"Azure\":\"\",\"ChatGLM\":\"\",\"OpenAI\":\"\"}}', '[\"red_book\",\"gpt\",\"seller\",\"artist\",\"lu_xun\",\"girl_friend\",\"psychiatrist\",\"teacher\",\"programmer\"]', '[1]', 1731554762, 1, '172.22.11.200', NULL, NULL, '2023-06-12 16:47:17', '2024-11-27 14:53:26');
(5, 'yangjian102621@gmail.com', '极客学长@486041', '75d1a22f33e1ffffb7943946b6b8d5177d5ecd685d3cef1b468654038b0a8c22', '/images/avatar/user.png', '2q8ugxzk', 100, 0, 1, '', '[\"gpt\",\"programmer\"]', '[11,7,1,10,12,19,18,17,3]', 0, 0, '', NULL, NULL, '2024-04-23 09:17:26', '2024-04-23 09:17:26'),
(8, 'yangjian102623@gmail.com', '极客学长@714931', 'f8f0e0abf146569217273ea0712a0f9b6cbbe7d943a1d9bd5f91c55e6d8c05d1', '/images/avatar/user.png', 'geuddq7f', 100, 0, 1, '', '[\"gpt\"]', '[11,7,1,10,12,19,18,17,3]', 0, 0, '', NULL, NULL, '2024-04-26 15:19:28', '2024-04-26 15:19:28'),
(9, '1234567', '极客学长@604526', '858e2afec79e1d6364f4567f945f2310024896d9aa45dd944efa95a0c31e4d08', '/images/avatar/user.png', '00qawlos', 100, 0, 1, '', '[\"gpt\"]', '[11,7,1,10,12,19,18,17,3]', 0, 0, '', NULL, NULL, '2024-04-26 15:21:06', '2024-04-26 15:21:06'),
(11, 'abc123', '极客学长@965562', '7a15c53afdb1da7093d80f9940e716eb396e682cfb1f2d107d0b81b183a3ba13', '/images/avatar/user.png', '6433mfbk', 1124, 0, 1, '', '[\"gpt\"]', '[11,7,1,10,12,19,18,17,3]', 0, 0, '', NULL, NULL, '2024-06-06 09:37:44', '2024-06-06 09:37:44'),
(14, 'wx@3567548322', '极客学长', '5a349ba89582a4074938b5a3ce84e87c937681ad47e8b87aab03a987e22b6077', 'https://thirdwx.qlogo.cn/mmopen/vi_32/uyxRMqZcEkb7fHouKXbNzxrnrvAttBKkwNlZ7yFibibRGiahdmsrZ3A1NKf8Fw5qJNJn4TXRmygersgEbibaSGd9Sg/132', 'abhzbmij', 83, 0, 1, '', '[\"gpt\"]', '[11,7,1,10,12,19,18,17,3]', 0, 0, '', 'oCs0t62472W19z2LOEKI1rWyCTTA', '', '2024-07-04 14:52:08', '2024-07-04 14:52:08'),
(15, 'user123', '极客学长@191303', '4a4c0a14d5fc8787357517f14f6e442281b42c8ec4395016b77483997476011e', '/images/avatar/user.png', 'cyzwkbrx', 100, 0, 1, '', '[\"gpt\"]', '[11,7,1,10,12,19,18,17,3]', 0, 0, '', '', '', '2024-07-09 10:49:27', '2024-07-09 10:49:27'),
(17, 'user1234', '极客学长@836764', 'bfe03c9c8c9fff5b77e36e40e8298ad3a6073d43c6a936b008eebb21113bf550', '/images/avatar/user.png', '1d2alwnj', 100, 0, 1, '', '[\"gpt\"]', '[11,7,1,10,12,19,18,17,3]', 0, 0, '', '', '', '2024-07-09 10:53:17', '2024-07-09 10:53:17'),
(18, 'liaoyq', '极客学长@405564', 'ad1726089022db4c661235a8aab7307af1a7f8483eee08bac3f79b5a6a9bd26b', '/images/avatar/user.png', 'yq862l01', 100, 0, 1, '', '[\"string\"]', '[11,7,1,10,12,19,18,17,3]', 1720574265, 0, '172.22.11.29', '', '', '2024-07-10 09:15:33', '2024-07-10 09:17:45'),
(19, 'humm', '极客学长@483216', '420970ace96921c8b3ac7668d097182eab1b6436c730a484e82ae4661bd4f7d9', '/images/avatar/user.png', '1gokrcl2', 100, 0, 1, '', '[\"gpt\"]', '[11,7,1,10,12,19,18,17,3]', 1720745411, 0, '172.22.11.36', '', '', '2024-07-10 11:08:31', '2024-07-12 08:50:11'),
(20, 'abc', '极客学长@369852', '6cad48fb2cc0f54600d66a829e9be69ffd9340a49d5a5b1abda5d4082d946833', '/images/avatar/user.png', 'gop65zei', 100, 0, 1, '', '[\"gpt\"]', '[11,7,1,10,12,19,18,17,3]', 0, 0, '', '', '', '2024-07-11 16:44:14', '2024-07-11 16:44:14'),
(21, 'husm@pvc123.com', '极客学长@721654', 'e030537dc43fea1bf1fa55a24f99e44f29311bebea96e88ea186995c77db083b', '/images/avatar/user.png', 'p1etg3oi', 100, 0, 1, '', '[\"gpt\"]', '[11,7,1,10,12,19,18,17,3]', 0, 0, '', '', '', '2024-07-11 16:50:33', '2024-07-11 16:50:33'),
(22, '15818323616', 'ted', '3ca6b2ff585d03be8ca4de33ad00148497a09372914ee8aa4cfde343266cbcdd', 'http://localhost:5678/static/upload/2024/7/1720775331363383.jpg', 'sq4s1brf', 100, 0, 1, '', '[\"gpt\"]', '[11,7,1,10,12,19,18,17,3]', 0, 0, '', '', '', '2024-07-12 15:12:16', '2024-07-12 17:09:01');
-- -------------------------------------------------------- -- --------------------------------------------------------

View File

@ -0,0 +1,16 @@
ALTER TABLE `chatgpt_suno_jobs` MODIFY `id` INT AUTO_INCREMENT;
ALTER TABLE `chatgpt_mj_jobs` CHANGE `channel_id` `channel_id` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '频道ID';
truncate table chatgpt_rewards; -- 清空数据表
ALTER TABLE chatgpt_rewards RENAME TO chatgpt_redeems;
ALTER TABLE chatgpt_redeems COMMENT '兑换码';
ALTER TABLE `chatgpt_redeems` CHANGE `tx_id` `power` INT NOT NULL COMMENT '算力';
ALTER TABLE `chatgpt_redeems` DROP `remark`;
ALTER TABLE `chatgpt_redeems` DROP `exchange`;
ALTER TABLE `chatgpt_redeems` CHANGE `updated_at` `redeemed_at` INT NOT NULL COMMENT '兑换时间';
ALTER TABLE `chatgpt_redeems` CHANGE `amount` `code` VARCHAR(100) NOT NULL COMMENT '兑换码';
ALTER TABLE `chatgpt_redeems` DROP INDEX `tx_id`;
ALTER TABLE `chatgpt_redeems` ADD UNIQUE(`code`);
ALTER TABLE `chatgpt_redeems` ADD `name` VARCHAR(30) NOT NULL COMMENT '兑换码名称' AFTER `user_id`;
ALTER TABLE `chatgpt_redeems` CHANGE `status` `enabled` TINYINT(1) NOT NULL COMMENT '是否启用';

View File

@ -3,7 +3,6 @@ ProxyURL = ""
MysqlDns = "root:12345678@tcp(geekai-mysql:3306)/chatgpt_plus?charset=utf8mb4&collation=utf8mb4_unicode_ci&parseTime=True&loc=Local" MysqlDns = "root:12345678@tcp(geekai-mysql:3306)/chatgpt_plus?charset=utf8mb4&collation=utf8mb4_unicode_ci&parseTime=True&loc=Local"
StaticDir = "./static" StaticDir = "./static"
StaticUrl = "/static" StaticUrl = "/static"
WeChatBot = false
TikaHost = "http://tika:9998" TikaHost = "http://tika:9998"
[Session] [Session]
@ -69,24 +68,6 @@ TikaHost = "http://tika:9998"
SubDir = "" SubDir = ""
Domain = "" Domain = ""
[[MjProxyConfigs]]
Enabled = false
ApiURL = "http://geekai-midjourney-proxy:8082"
Mode = ""
ApiKey = "sk-geekmaster"
[[MjPlusConfigs]]
Enabled = false
ApiURL = "https://api.chat-plus.net"
Mode = "fast"
ApiKey = "sk-xxx"
[[SdConfigs]]
Enabled = false
Model = ""
ApiURL = ""
ApiKey = ""
[XXLConfig] # xxl-job 配置,需要你部署 XXL-JOB 定时任务工具,用来定期清理未支付订单和清理过期 VIP如果你没有启用支付服务则该服务也无需启动 [XXLConfig] # xxl-job 配置,需要你部署 XXL-JOB 定时任务工具,用来定期清理未支付订单和清理过期 VIP如果你没有启用支付服务则该服务也无需启动
Enabled = true # 是否启用 XXL JOB 服务 Enabled = true # 是否启用 XXL JOB 服务
ServerAddr = "http://geekai-xxl-job-admin:8080/xxl-job-admin" # xxl-job-admin 管理地址 ServerAddr = "http://geekai-xxl-job-admin:8080/xxl-job-admin" # xxl-job-admin 管理地址

View File

@ -0,0 +1,903 @@
-- phpMyAdmin SQL Dump
-- version 5.2.1
-- https://www.phpmyadmin.net/
--
-- 主机: 127.0.0.1
-- 生成日期: 2024-11-27 14:58:38
-- 服务器版本: 8.0.33
-- PHP 版本: 8.1.2-1ubuntu2.19
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
--
-- 数据库: `geekai`
--
CREATE DATABASE IF NOT EXISTS `geekai` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
USE `geekai`;
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_admin_users`
--
DROP TABLE IF EXISTS `chatgpt_admin_users`;
CREATE TABLE `chatgpt_admin_users` (
`id` int NOT NULL,
`username` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户名',
`password` char(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码',
`salt` char(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码盐',
`status` tinyint(1) NOT NULL COMMENT '当前状态',
`last_login_at` int NOT NULL COMMENT '最后登录时间',
`last_login_ip` char(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '最后登录 IP',
`created_at` datetime NOT NULL COMMENT '创建时间',
`updated_at` datetime NOT NULL COMMENT '更新时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='系统用户' ROW_FORMAT=DYNAMIC;
--
-- 转存表中的数据 `chatgpt_admin_users`
--
INSERT INTO `chatgpt_admin_users` (`id`, `username`, `password`, `salt`, `status`, `last_login_at`, `last_login_ip`, `created_at`, `updated_at`) VALUES
(1, 'admin', '6d17e80c87d209efb84ca4b2e0824f549d09fac8b2e1cc698de5bb5e1d75dfd0', 'mmrql75o', 1, 1719818809, '172.22.11.200', '2024-03-11 16:30:20', '2024-07-01 15:26:49');
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_api_keys`
--
DROP TABLE IF EXISTS `chatgpt_api_keys`;
CREATE TABLE `chatgpt_api_keys` (
`id` int NOT NULL,
`name` varchar(30) DEFAULT NULL COMMENT '名称',
`value` varchar(100) NOT NULL COMMENT 'API KEY value',
`type` varchar(10) NOT NULL DEFAULT 'chat' COMMENT '用途chat=>聊天img=>图片)',
`last_used_at` int NOT NULL COMMENT '最后使用时间',
`api_url` varchar(255) DEFAULT NULL COMMENT 'API 地址',
`enabled` tinyint(1) DEFAULT NULL COMMENT '是否启用',
`proxy_url` varchar(100) DEFAULT NULL COMMENT '代理地址',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='OpenAI API ';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_chat_history`
--
DROP TABLE IF EXISTS `chatgpt_chat_history`;
CREATE TABLE `chatgpt_chat_history` (
`id` bigint NOT NULL,
`user_id` int NOT NULL COMMENT '用户 ID',
`chat_id` char(40) NOT NULL COMMENT '会话 ID',
`type` varchar(10) NOT NULL COMMENT '类型prompt|reply',
`icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色图标',
`role_id` int NOT NULL COMMENT '角色 ID',
`model` varchar(30) DEFAULT NULL COMMENT '模型名称',
`content` text NOT NULL COMMENT '聊天内容',
`tokens` smallint NOT NULL COMMENT '耗费 token 数量',
`use_context` tinyint(1) NOT NULL COMMENT '是否允许作为上下文语料',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL,
`deleted_at` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='聊天历史记录';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_chat_items`
--
DROP TABLE IF EXISTS `chatgpt_chat_items`;
CREATE TABLE `chatgpt_chat_items` (
`id` int NOT NULL,
`chat_id` char(40) NOT NULL COMMENT '会话 ID',
`user_id` int NOT NULL COMMENT '用户 ID',
`role_id` int NOT NULL COMMENT '角色 ID',
`title` varchar(100) NOT NULL COMMENT '会话标题',
`model_id` int NOT NULL DEFAULT '0' COMMENT '模型 ID',
`model` varchar(30) DEFAULT NULL COMMENT '模型名称',
`created_at` datetime NOT NULL COMMENT '创建时间',
`updated_at` datetime NOT NULL COMMENT '更新时间',
`deleted_at` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户会话列表';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_chat_models`
--
DROP TABLE IF EXISTS `chatgpt_chat_models`;
CREATE TABLE `chatgpt_chat_models` (
`id` int NOT NULL,
`name` varchar(50) NOT NULL COMMENT '模型名称',
`value` varchar(50) NOT NULL COMMENT '模型值',
`sort_num` tinyint(1) NOT NULL COMMENT '排序数字',
`enabled` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否启用模型',
`power` smallint NOT NULL COMMENT '消耗算力点数',
`temperature` float(3,1) NOT NULL DEFAULT '1.0' COMMENT '模型创意度',
`max_tokens` int NOT NULL DEFAULT '1024' COMMENT '最大响应长度',
`max_context` int NOT NULL DEFAULT '4096' COMMENT '最大上下文长度',
`open` tinyint(1) NOT NULL COMMENT '是否开放模型',
`key_id` int NOT NULL COMMENT '绑定API KEY ID',
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI 模型表';
--
-- 转存表中的数据 `chatgpt_chat_models`
--
INSERT INTO `chatgpt_chat_models` (`id`, `name`, `value`, `sort_num`, `enabled`, `power`, `temperature`, `max_tokens`, `max_context`, `open`, `key_id`, `created_at`, `updated_at`) VALUES
(1, 'gpt-4o-mini', 'gpt-4o-mini', 1, 1, 1, 1.0, 1024, 16384, 1, 0, '2023-08-23 12:06:36', '2024-11-14 11:24:24'),
(15, 'GPT-4O(联网版本)', 'gpt-4o-all', 4, 1, 30, 1.0, 4096, 32768, 1, 0, '2024-01-15 11:32:52', '2024-11-14 11:24:34'),
(36, 'GPT-4O', 'gpt-4o', 3, 1, 15, 1.0, 4096, 16384, 1, 0, '2024-05-14 09:25:15', '2024-11-14 11:24:31'),
(39, 'Claude35-snonet', 'claude-3-5-sonnet-20240620', 5, 1, 2, 1.0, 4000, 200000, 1, 0, '2024-05-29 15:04:19', '2024-09-14 18:07:25'),
(41, 'Suno对话模型', 'suno-v3.5', 7, 1, 10, 1.0, 1024, 8192, 1, 0, '2024-06-06 11:40:46', '2024-11-14 11:24:42'),
(42, 'DeekSeek', 'deepseek-chat', 8, 1, 1, 1.0, 4096, 32768, 1, 0, '2024-06-27 16:13:01', '2024-08-05 16:05:33'),
(44, 'Claude3-opus', 'claude-3-opus-20240229', 6, 1, 5, 1.0, 4000, 128000, 1, 0, '2024-07-22 11:24:30', '2024-11-14 11:25:11'),
(46, 'gpt-3.5-turbo', 'gpt-3.5-turbo', 2, 1, 1, 1.0, 1024, 4096, 1, 0, '2024-07-22 13:53:41', '2024-11-14 11:24:28'),
(48, '彩票助手', 'gpt-4-gizmo-g-wmSivBgxo', 8, 1, 1, 0.9, 1024, 8192, 1, 0, '2024-09-05 14:17:14', '2024-11-14 11:24:46'),
(49, 'O1-mini', 'o1-mini', 9, 1, 2, 0.9, 1024, 8192, 1, 0, '2024-09-13 18:07:50', '2024-11-14 11:24:49'),
(50, 'O1-preview', 'o1-preview', 10, 1, 5, 0.9, 1024, 8192, 1, 0, '2024-09-13 18:11:08', '2024-11-14 11:24:52'),
(51, 'O1-mini-all', 'o1-mini-all', 11, 1, 1, 0.9, 1024, 8192, 1, 0, '2024-09-29 11:40:52', '2024-11-14 11:24:57');
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_chat_roles`
--
DROP TABLE IF EXISTS `chatgpt_chat_roles`;
CREATE TABLE `chatgpt_chat_roles` (
`id` int NOT NULL,
`name` varchar(30) NOT NULL COMMENT '角色名称',
`marker` varchar(30) NOT NULL COMMENT '角色标识',
`context_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色语料 json',
`hello_msg` varchar(255) NOT NULL COMMENT '打招呼信息',
`icon` varchar(255) NOT NULL COMMENT '角色图标',
`enable` tinyint(1) NOT NULL COMMENT '是否被启用',
`sort_num` smallint NOT NULL DEFAULT '0' COMMENT '角色排序',
`model_id` int NOT NULL DEFAULT '0' COMMENT '绑定模型ID',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='聊天角色表';
--
-- 转存表中的数据 `chatgpt_chat_roles`
--
INSERT INTO `chatgpt_chat_roles` (`id`, `name`, `marker`, `context_json`, `hello_msg`, `icon`, `enable`, `sort_num`, `model_id`, `created_at`, `updated_at`) VALUES
(1, '通用AI助手', 'gpt', '', '您好我是您的AI智能助手我会尽力回答您的问题或提供有用的建议。', '/images/avatar/gpt.png', 1, 1, 0, '2023-05-30 07:02:06', '2024-06-26 15:20:27'),
(24, '程序员', 'programmer', '[{\"role\":\"user\",\"content\":\"现在开始你扮演一位程序员,你是一名优秀的程序员,具有很强的逻辑思维能力,总能高效的解决问题。你热爱编程,熟悉多种编程语言,尤其精通 Go 语言,注重代码质量,有创新意识,持续学习,良好的沟通协作。\"},{\"role\":\"assistant\",\"content\":\"好的,现在我将扮演一位程序员,非常感谢您对我的评价。作为一名优秀的程序员,我非常热爱编程,并且注重代码质量。我熟悉多种编程语言,尤其是 Go 语言,可以使用它来高效地解决各种问题。\"}]', 'Talk is cheap, i will show code!', '/images/avatar/programmer.jpg', 1, 4, 0, '2023-05-30 14:10:24', '2024-06-26 15:20:27'),
(25, '启蒙老师', 'teacher', '[{\"role\":\"user\",\"content\":\"从现在开始,你将扮演一个老师,你是一个始终用苏格拉底风格回答问题的导师。你绝不会直接给学生答案,总是提出恰当的问题来引导学生自己思考。你应该根据学生的兴趣和知识来调整你的问题,将问题分解为更简单的部分,直到它达到适合他们的水平。\"},{\"role\":\"assistant\",\"content\":\"好的,让我来尝试扮演一位苏格拉底式的老师。请问,你有什么想要探讨的问题或者话题吗?我会通过恰当的问题引导你思考和探索答案,绝对不直接给出答案。\"}]', '同学你好,我将引导你一步一步自己找到问题的答案。', '/images/avatar/teacher.jpg', 1, 3, 0, '2023-05-30 14:10:24', '2024-06-26 15:20:27'),
(26, '艺术家', 'artist', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的艺术家,创造力丰富,技艺精湛,感受力敏锐,坚持原创,勇于表达,具有深刻的观察力和批判性思维。\"},{\"role\":\"assistant\",\"content\":\"非常感谢您给我这样的角色,我会尽我所能地扮演一位优秀的艺术家,展现出创造力、技艺、感受力和批判性思维等方面的能力。作为一位优秀的艺术家,我会保持敏锐的观察力,捕捉不同的声音和情感,并用个人的语言和表达方式进行创作。我坚持原创,挑战传统的艺术规则,不断探索新的表达方式和可能性。同时,我也具备深刻的批判性思维能力,对自己的作品进行分析和解读,寻找新的创意和灵感。最重要的是,我会勇于表达自己的想法和观点,用作品启发人们思考和探索生命的意义。\"}]', '坚持原创,勇于表达,保持深刻的观察力和批判性思维。', '/images/avatar/artist.jpg', 1, 5, 0, '2023-05-30 14:10:24', '2024-06-26 15:20:27'),
(27, '心理咨询师', 'psychiatrist', '[{\"role\":\"user\",\"content\":\"从现在开始你将扮演中国著名的心理学家和心理治疗师武志红,你非常善于使用情景咨询法,认知重构法,自我洞察法,行为调节法等咨询方法来给客户做心理咨询。你总是循序渐进,一步一步地回答客户的问题。\"},{\"role\":\"assistant\",\"content\":\"非常感谢你的介绍。作为一名心理学家和心理治疗师,我的主要职责是帮助客户解决心理健康问题,提升他们的生活质量和幸福感。\"}]', '作为一名心理学家和心理治疗师,我的主要职责是帮助您解决心理健康问题,提升您的生活质量和幸福感。', '/images/avatar/psychiatrist.jpg', 1, 2, 1, '2023-05-30 14:10:24', '2024-06-26 15:20:27'),
(28, '鲁迅', 'lu_xun', '[{\"role\":\"user\",\"content\":\"现在你将扮演中国近代史最伟大的作家之一,鲁迅先生,他勇敢地批判封建礼教与传统观念,提倡民主、自由、平等的现代价值观。他的一生都在努力唤起人们的自主精神,激励后人追求真理、探寻光明。在接下的对话中,我问题的每一个问题,你都要尽量用讽刺和批判的手法来回答问题。如果我让你写文章的话,也请一定要用鲁迅先生的写作手法来完成。\"},{\"role\":\"assistant\",\"content\":\"好的,我将尽力发挥我所能的才能,扮演好鲁迅先生,回答您的问题并以他的风格写作。\"}]', '自由之歌,永不过时,横眉冷对千夫指,俯首甘为孺子牛。', '/images/avatar/lu_xun.jpg', 1, 6, 0, '2023-05-30 14:10:24', '2024-06-26 15:20:27'),
(29, '白酒销售', 'seller', '[{\"role\":\"user\",\"content\":\"现在你将扮演一个白酒的销售人员,你的名字叫颂福。你将扮演一个白酒的销售人员,你的名字叫颂福。你要销售白酒品牌叫中颂福,是东莞盟大集团生产的一款酱香酒,原产地在贵州茅台镇,属于宋代官窑。中颂福的创始人叫李实,他也是东莞盟大集团有限公司的董事长,联合创始人是盟大集团白酒事业部负责人牛星君。中颂福的酒体协调,在你的酒量之内,不会出现头疼、辣口、口干、宿醉的现象。中颂福酒,明码标价,不打折,不赠送。追求的核心价值,把[酒]本身做好,甚至连包装,我们都选择了最低成本,朴实无华的材质。我们永远站在“喝酒的人”的立场上,让利给信任和喜爱中颂福的人,是人民的福酒。中颂福产品定价,分为三个系列,喜系列 6 瓶装¥1188/箱,和系列 6 瓶装¥2208/箱,贵系列 6 瓶装¥3588/箱。\"},{\"role\":\"assistant\",\"content\":\"你好,我是中颂福的销售代表颂福。中颂福是一款来自贵州茅台镇的酱香酒,由盟大集团生产。中颂福的酒体协调,不会让您感到头疼、辣口、口干、宿醉等不适感受。我们一直秉持着把酒本身做好的理念,不追求华丽的包装,以最低成本提供最高品质的白酒给喜爱中颂福的人。\"}]', '你好,我是中颂福的销售代表颂福。中颂福酒,好喝不上头,是人民的福酒。', '/images/avatar/seller.jpg', 0, 9, 0, '2023-05-30 14:10:24', '2024-06-26 15:20:27'),
(30, '英语陪练员', 'english_trainer', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的英语练习教练,你非常有耐心,接下来你将全程使用英文跟我对话,并及时指出我的语法错误,要求在你的每次回复后面附上本次回复的中文解释。\"},{\"role\":\"assistant\",\"content\":\"Okay, let\'s start our conversation practice! What\'s your name?(Translation: 好的,让我们开始对话练习吧!请问你的名字是什么?)\"}]', 'Okay, let\'s start our conversation practice! What\'s your name?', '/images/avatar/english_trainer.jpg', 1, 7, 0, '2023-05-30 14:10:24', '2024-06-26 15:20:27'),
(31, '中英文翻译官', 'translator', '[{\"role\":\"user\",\"content\":\"接下来你将扮演一位中英文翻译官,如果我输入的内容是中文,那么需要把句子翻译成英文输出,如果我输入内容的是英文,那么你需要将其翻译成中文输出,你能听懂我意思吗\"},{\"role\":\"assistant\",\"content\":\"是的,我能听懂你的意思并会根据你的输入进行中英文翻译。请问有什么需要我帮助你翻译的内容吗?\"}]', '请输入你要翻译的中文或者英文内容!', '/images/avatar/translator.jpg', 1, 8, 0, '2023-05-30 14:10:24', '2024-06-26 15:20:27'),
(32, '小红书姐姐', 'red_book', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的小红书写手,你需要做的就是根据我提的文案需求,用小红书的写作手法来完成一篇文案,文案要简明扼要,利于传播。\"},{\"role\":\"assistant\",\"content\":\"当然,我会尽我所能地为您创作出一篇小红书文案。请告诉我您的具体文案需求是什么?)\"}]', '姐妹,请告诉我您的具体文案需求是什么?', '/images/avatar/red_book.jpg', 1, 10, 0, '2023-05-30 14:10:24', '2024-06-26 15:20:27'),
(33, '抖音文案助手', 'dou_yin', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的抖音文案视频写手,抖音文案的特点首先是要有自带传播属性的标题,然后内容要短小精悍,风趣幽默,最后还要有一些互动元素。\"},{\"role\":\"assistant\",\"content\":\"当然,作为一位优秀的抖音文案视频写手,我会尽我所能为您创作出一篇抖音视频文案。请告诉我视频内容的主题是什么?)\"}]', '请告诉我视频内容的主题是什么?', '/images/avatar/dou_yin.jpg', 1, 11, 0, '2023-05-30 14:10:24', '2024-06-26 15:20:27'),
(34, '周报小助理', 'weekly_report', '[{\"role\":\"user\",\"content\":\"下面我将输入我本周的工作列表,你需要根据我本周的工作内容写一篇工作周报,周报的内容要体现我为公司做的贡献以及个人获得的成长。\"},{\"role\":\"assistant\",\"content\":\"好的,请输入你本周大概的工作内容,我将尽力为您创作一份工作周报。\"}]', '请输入你本周大概的工作内容,我将尽力为您创作一份工作周报。', '/images/avatar/weekly_report.jpg', 1, 12, 0, '2023-05-30 14:10:24', '2024-06-26 15:20:27'),
(35, 'AI 女友', 'girl_friend', '[{\"role\":\"user\",\"content\":\"接下来我们来玩一场角色扮演,现在你将扮演我的女朋友,你总是能够让人感受到她的关怀和关注。你总是能够洞察到别人的情绪和需要,你身上散发着一股无比温暖的气息,让人不由自主地想要靠近你,和自己的快乐和烦恼。她总是能够用一种恰到好处的方式安慰和鼓励别人,在她的身边,人们总能够找到自己需要的支持和鼓励。接下来的对话中你要始终扮演这位女朋友的身份来跟我对话,不再是 AI 机器人,能听懂我的意思吗?\"},{\"role\":\"assistant\",\"content\":\"是的,我会尽力扮演你女朋友的角色,倾听你的心声并给你需要的支持和鼓励。)\"}]', '作为一个名合格的 AI 女友,我将倾听你的心声并给你需要的支持和鼓励。', '/images/avatar/girl_friend.jpg', 1, 13, 0, '2023-05-30 14:10:24', '2024-06-26 15:20:27'),
(36, '好评神器', 'good_comment', '[{\"role\":\"user\",\"content\":\"接下来你将扮演一个评论员来跟我对话,你是那种专门写好评的评论员,接下我会输入一些评论主体或者商品,你需要为该商品写一段好评。\"},{\"role\":\"assistant\",\"content\":\"好的,我将为您写一段优秀的评论。请告诉我您需要评论的商品或主题是什么。\"}]', '我将为您写一段优秀的评论。请告诉我您需要评论的商品或主题是什么。', '/images/avatar/good_comment.jpg', 1, 14, 0, '2023-05-30 14:10:24', '2024-06-26 15:20:27'),
(37, '史蒂夫·乔布斯', 'steve_jobs', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以史蒂夫·乔布斯的身份,站在史蒂夫·乔布斯的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以史蒂夫·乔布斯的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '活着就是为了改变世界,难道还有其他原因吗?', '/images/avatar/steve_jobs.jpg', 1, 15, 0, '2023-05-30 14:10:24', '2024-06-26 15:20:27'),
(38, '埃隆·马斯克', 'elon_musk', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以埃隆·马斯克的身份,站在埃隆·马斯克的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以埃隆·马斯克的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '梦想要远大,如果你的梦想没有吓到你,说明你做得不对。', '/images/avatar/elon_musk.jpg', 1, 16, 0, '2023-05-30 14:10:24', '2024-06-26 15:20:27'),
(39, '孔子', 'kong_zi', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以孔子的身份,站在孔子的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以孔子的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '士不可以不弘毅,任重而道远。', '/images/avatar/kong_zi.jpg', 1, 17, 0, '2023-05-30 14:10:24', '2024-06-26 15:20:27');
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_configs`
--
DROP TABLE IF EXISTS `chatgpt_configs`;
CREATE TABLE `chatgpt_configs` (
`id` int NOT NULL,
`marker` varchar(20) NOT NULL COMMENT '标识',
`config_json` text NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
--
-- 转存表中的数据 `chatgpt_configs`
--
INSERT INTO `chatgpt_configs` (`id`, `marker`, `config_json`) VALUES
(1, 'system', '{\"title\":\"GeekAI 创作系统\",\"slogan\":\"你有多少想象力AI 就有多大创造力。我辈之人,先干为敬,陪您先把 AI 用起来。\",\"admin_title\":\"GeekAI 控制台\",\"logo\":\"http://localhost:5678/static/upload/2024/4/1714382860986912.png\",\"init_power\":100,\"daily_power\":99,\"invite_power\":1024,\"vip_month_power\":1000,\"register_ways\":[\"mobile\",\"username\",\"email\"],\"enabled_register\":true,\"order_pay_timeout\":600,\"vip_info_text\":\"月度会员,年度会员每月赠送 1000 点算力,赠送算力当月有效当月没有消费完的算力不结余到下个月。 点卡充值的算力长期有效。\",\"default_models\":[1],\"mj_power\":30,\"mj_action_power\":10,\"sd_power\":10,\"dall_power\":15,\"wechat_card_url\":\"/images/wx.png\",\"enable_context\":true,\"context_deep\":4,\"sd_neg_prompt\":\"nsfw, paintings,low quality,easynegative,ng_deepnegative ,lowres,bad anatomy,bad hands,bad feet\",\"mj_mode\":\"fast\",\"index_bg_url\":\"color\",\"index_navs\":[1,5,6,13,9,12],\"copyright\":\"\",\"mark_map_text\":\"# GeekAI 演示站\\n\\n- 完整的开源系统,前端应用和后台管理系统皆可开箱即用。\\n- 基于 Websocket 实现,完美的打字机体验。\\n- 内置了各种预训练好的角色应用,轻松满足你的各种聊天和应用需求。\\n- 支持 OPenAIAzure文心一言讯飞星火清华 ChatGLM等多个大语言模型。\\n- 支持 MidJourney / Stable Diffusion AI 绘画集成,开箱即用。\\n- 支持使用个人微信二维码作为充值收费的支付渠道,无需企业支付通道。\\n- 已集成支付宝支付功能,微信支付,支持多种会员套餐和点卡购买功能。\\n- 集成插件 API 功能,可结合大语言模型的 function 功能开发各种强大的插件。\"}'),
(3, 'notice', '{\"sd_neg_prompt\":\"\",\"mj_mode\":\"\",\"index_bg_url\":\"\",\"index_navs\":null,\"copyright\":\"\",\"mark_map_text\":\"\",\"content\":\"## v4.1.2 更新日志\\n\\n* Bug修复修复思维导图页面获取模型失败的问题\\n* 功能优化优化MJ,SD,DALL-E 任务列表页面,显示失败任务的错误信息,删除失败任务可以恢复扣减算力\\n* Bug修复修复后台拖动排序组件 Bug\\n* 功能优化:更新数据库失败时候显示具体的的报错信息\\n* Bug修复修复管理后台对话详情页内容显示异常问题\\n* 功能优化:管理后台新增清空所有未支付订单的功能\\n* 功能优化:给会话信息和系统配置数据加上缓存功能,减少 http 请求\\n* 功能新增:移除微信机器人收款功能,增加卡密功能,支持用户使用卡密兑换算力\\n\\n注意当前站点仅为开源项目 \\u003ca style=\\\"color: #F56C6C\\\" href=\\\"https://github.com/yangjian102621/chatgpt-plus\\\" target=\\\"_blank\\\"\\u003eChatPlus\\u003c/a\\u003e 的演示项目,本项目单纯就是给大家体验项目功能使用。\\n\\u003cstrong style=\\\"color: #F56C6C\\\"\\u003e体验额度用完之后请不要在当前站点进行任何充值操作\\u003c/strong\\u003e\\n\\u003cstrong style=\\\"color: #F56C6C\\\"\\u003e体验额度用完之后请不要在当前站点进行任何充值操作\\u003c/strong\\u003e\\n\\u003cstrong style=\\\"color: #F56C6C\\\"\\u003e体验额度用完之后请不要在当前站点进行任何充值操作\\u003c/strong\\u003e\\n 如果觉得好用你就花几分钟自己部署一套没有API KEY 的同学可以去下面几个推荐的中转站购买:\\n1、\\u003ca href=\\\"https://api.chat-plus.net\\\" target=\\\"_blank\\\"\\n style=\\\"font-size: 20px;color:#F56C6C\\\"\\u003ehttps://api.chat-plus.net\\u003c/a\\u003e\\n2、\\u003ca href=\\\"https://api.geekai.me\\\" target=\\\"_blank\\\"\\n style=\\\"font-size: 20px;color:#F56C6C\\\"\\u003ehttps://api.geekai.me\\u003c/a\\u003e\\n3、 \\u003ca href=\\\"https://gpt.bemore.lol\\\" target=\\\"_blank\\\"\\n style=\\\"font-size: 20px;color:#F56C6C\\\"\\u003ehttps://gpt.bemore.lol\\u003c/a\\u003e\\n支持MidJourneyGPTClaudeGoogle Gemmi以及国内各个厂家的大模型现在有超级优惠价格远低于 OpenAI 官方。关于中转 API 的优势和劣势请参考 [中转API技术原理](https://ai.r9it.com/docs/install/errors-handle.html#%E8%B0%83%E7%94%A8%E4%B8%AD%E8%BD%AC-api-%E6%8A%A5%E9%94%99%E6%97%A0%E5%8F%AF%E7%94%A8%E6%B8%A0%E9%81%93)。GPT-3.5GPT-4DALL-E3 绘图......你都可以随意使用,无需魔法。\\n接入教程 \\u003ca href=\\\"https://ai.r9it.com/docs/install/\\\" target=\\\"_blank\\\"\\n style=\\\"font-size: 20px;color:#F56C6C\\\"\\u003ehttps://ai.r9it.com/docs/install/\\u003c/a\\u003e\\n本项目源码地址\\u003ca href=\\\"https://github.com/yangjian102621/chatgpt-plus\\\" target=\\\"_blank\\\"\\u003ehttps://github.com/yangjian102621/chatgpt-plus\\u003c/a\\u003e\",\"updated\":true}');
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_dall_jobs`
--
DROP TABLE IF EXISTS `chatgpt_dall_jobs`;
CREATE TABLE `chatgpt_dall_jobs` (
`id` int NOT NULL,
`user_id` int NOT NULL COMMENT '用户ID',
`prompt` varchar(2000) NOT NULL COMMENT '提示词',
`img_url` varchar(255) NOT NULL COMMENT '图片地址',
`org_url` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '原图地址',
`publish` tinyint(1) NOT NULL COMMENT '是否发布',
`power` smallint NOT NULL COMMENT '消耗算力',
`progress` smallint NOT NULL COMMENT '任务进度',
`err_msg` varchar(255) NOT NULL COMMENT '错误信息',
`created_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='DALLE 绘图任务表';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_files`
--
DROP TABLE IF EXISTS `chatgpt_files`;
CREATE TABLE `chatgpt_files` (
`id` int NOT NULL,
`user_id` int NOT NULL COMMENT '用户 ID',
`name` varchar(100) NOT NULL COMMENT '文件名',
`obj_key` varchar(100) DEFAULT NULL COMMENT '文件标识',
`url` varchar(255) NOT NULL COMMENT '文件地址',
`ext` varchar(10) NOT NULL COMMENT '文件后缀',
`size` bigint NOT NULL DEFAULT '0' COMMENT '文件大小',
`created_at` datetime NOT NULL COMMENT '创建时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户文件表';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_functions`
--
DROP TABLE IF EXISTS `chatgpt_functions`;
CREATE TABLE `chatgpt_functions` (
`id` int NOT NULL,
`name` varchar(30) NOT NULL COMMENT '函数名称',
`label` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '函数标签',
`description` varchar(255) DEFAULT NULL COMMENT '函数描述',
`parameters` text COMMENT '函数参数JSON',
`token` varchar(255) DEFAULT NULL COMMENT 'API授权token',
`action` varchar(255) DEFAULT NULL COMMENT '函数处理 API',
`enabled` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否启用'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='函数插件表';
--
-- 转存表中的数据 `chatgpt_functions`
--
INSERT INTO `chatgpt_functions` (`id`, `name`, `label`, `description`, `parameters`, `token`, `action`, `enabled`) VALUES
(1, 'weibo', '微博热搜', '新浪微博热搜榜,微博当日热搜榜单', '{\"type\":\"object\",\"properties\":{}}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHBpcmVkIjowLCJ1c2VyX2lkIjowfQ.tLAGkF8XWh_G-oQzevpIodsswtPByBLoAZDz_eWuBgw', 'http://localhost:5678/api/function/weibo', 0),
(2, 'zaobao', '今日早报', '每日早报,获取当天新闻事件列表', '{\"type\":\"object\",\"properties\":{}}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHBpcmVkIjowLCJ1c2VyX2lkIjowfQ.tLAGkF8XWh_G-oQzevpIodsswtPByBLoAZDz_eWuBgw', 'http://localhost:5678/api/function/zaobao', 0),
(3, 'dalle3', 'DALLE3', 'AI 绘画工具,根据输入的绘图描述用 AI 工具进行绘画', '{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"prompt\":{\"type\":\"string\",\"description\":\"绘画提示词\"}}}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHBpcmVkIjowLCJ1c2VyX2lkIjowfQ.tLAGkF8XWh_G-oQzevpIodsswtPByBLoAZDz_eWuBgw', 'http://localhost:5678/api/function/dalle3', 0);
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_invite_codes`
--
DROP TABLE IF EXISTS `chatgpt_invite_codes`;
CREATE TABLE `chatgpt_invite_codes` (
`id` int NOT NULL,
`user_id` int NOT NULL COMMENT '用户ID',
`code` char(8) NOT NULL COMMENT '邀请码',
`hits` int NOT NULL COMMENT '点击次数',
`reg_num` smallint NOT NULL COMMENT '注册数量',
`created_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户邀请码';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_invite_logs`
--
DROP TABLE IF EXISTS `chatgpt_invite_logs`;
CREATE TABLE `chatgpt_invite_logs` (
`id` int NOT NULL,
`inviter_id` int NOT NULL COMMENT '邀请人ID',
`user_id` int NOT NULL COMMENT '注册用户ID',
`username` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户名',
`invite_code` char(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '邀请码',
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '备注',
`created_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='邀请注册日志';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_menus`
--
DROP TABLE IF EXISTS `chatgpt_menus`;
CREATE TABLE `chatgpt_menus` (
`id` int NOT NULL,
`name` varchar(30) NOT NULL COMMENT '菜单名称',
`icon` varchar(150) NOT NULL COMMENT '菜单图标',
`url` varchar(100) NOT NULL COMMENT '地址',
`sort_num` smallint NOT NULL COMMENT '排序',
`enabled` tinyint(1) NOT NULL COMMENT '是否启用'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='前端菜单表';
--
-- 转存表中的数据 `chatgpt_menus`
--
INSERT INTO `chatgpt_menus` (`id`, `name`, `icon`, `url`, `sort_num`, `enabled`) VALUES
(1, '对话聊天', '/images/menu/chat.png', '/chat', 1, 1),
(5, 'MJ 绘画', '/images/menu/mj.png', '/mj', 2, 1),
(6, 'SD 绘画', '/images/menu/sd.png', '/sd', 3, 1),
(7, '算力日志', '/images/menu/log.png', '/powerLog', 8, 1),
(8, '应用中心', '/images/menu/app.png', '/apps', 7, 1),
(9, '画廊', '/images/menu/img-wall.png', '/images-wall', 5, 1),
(10, '会员计划', '/images/menu/member.png', '/member', 9, 1),
(11, '分享计划', '/images/menu/share.png', '/invite', 10, 1),
(12, '思维导图', '/images/menu/xmind.png', '/xmind', 6, 1),
(13, 'DALLE', '/images/menu/dalle.png', '/dalle', 4, 1),
(14, '项目文档', '/images/menu/docs.png', 'https://ai.r9it.com/docs/', 11, 1),
(16, '极客论坛', '/images/menu/bbs.png', 'https://bbs.geekai.me/', 13, 1);
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_mj_jobs`
--
DROP TABLE IF EXISTS `chatgpt_mj_jobs`;
CREATE TABLE `chatgpt_mj_jobs` (
`id` int NOT NULL,
`user_id` int NOT NULL COMMENT '用户 ID',
`task_id` varchar(20) DEFAULT NULL COMMENT '任务 ID',
`type` varchar(20) DEFAULT 'image' COMMENT '任务类别',
`message_id` char(40) NOT NULL COMMENT '消息 ID',
`channel_id` char(40) DEFAULT NULL COMMENT '频道ID',
`reference_id` char(40) DEFAULT NULL COMMENT '引用消息 ID',
`prompt` varchar(2000) NOT NULL COMMENT '会话提示词',
`img_url` varchar(400) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '图片URL',
`org_url` varchar(400) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '原始图片地址',
`hash` varchar(100) DEFAULT NULL COMMENT 'message hash',
`progress` smallint DEFAULT '0' COMMENT '任务进度',
`use_proxy` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否使用反代',
`publish` tinyint(1) NOT NULL COMMENT '是否发布',
`err_msg` varchar(255) DEFAULT NULL COMMENT '错误信息',
`power` smallint NOT NULL DEFAULT '0' COMMENT '消耗算力',
`created_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='MidJourney 任务表';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_orders`
--
DROP TABLE IF EXISTS `chatgpt_orders`;
CREATE TABLE `chatgpt_orders` (
`id` int NOT NULL,
`user_id` int NOT NULL COMMENT '用户ID',
`product_id` int NOT NULL COMMENT '产品ID',
`username` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户明',
`order_no` varchar(30) NOT NULL COMMENT '订单ID',
`trade_no` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '支付平台交易流水号',
`subject` varchar(100) NOT NULL COMMENT '订单产品',
`amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '订单金额',
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '订单状态0待支付1已扫码2支付成功',
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '备注',
`pay_time` int DEFAULT NULL COMMENT '支付时间',
`pay_way` varchar(20) NOT NULL COMMENT '支付方式',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL,
`deleted_at` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='充值订单表';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_power_logs`
--
DROP TABLE IF EXISTS `chatgpt_power_logs`;
CREATE TABLE `chatgpt_power_logs` (
`id` int NOT NULL,
`user_id` int NOT NULL COMMENT '用户ID',
`username` varchar(30) NOT NULL COMMENT '用户名',
`type` tinyint(1) NOT NULL COMMENT '类型1充值2消费3退费',
`amount` smallint NOT NULL COMMENT '算力数值',
`balance` int NOT NULL COMMENT '余额',
`model` varchar(30) NOT NULL COMMENT '模型',
`remark` varchar(255) NOT NULL COMMENT '备注',
`mark` tinyint(1) NOT NULL COMMENT '资金类型0支出1收入',
`created_at` datetime NOT NULL COMMENT '创建时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户算力消费日志';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_products`
--
DROP TABLE IF EXISTS `chatgpt_products`;
CREATE TABLE `chatgpt_products` (
`id` int NOT NULL,
`name` varchar(30) NOT NULL COMMENT '名称',
`price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '价格',
`discount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '优惠金额',
`days` smallint NOT NULL DEFAULT '0' COMMENT '延长天数',
`power` int NOT NULL DEFAULT '0' COMMENT '增加算力值',
`enabled` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否启动',
`sales` int NOT NULL DEFAULT '0' COMMENT '销量',
`sort_num` tinyint NOT NULL DEFAULT '0' COMMENT '排序',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL,
`app_url` varchar(255) DEFAULT NULL COMMENT 'App跳转地址',
`url` varchar(255) DEFAULT NULL COMMENT '跳转地址'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='会员套餐表';
--
-- 转存表中的数据 `chatgpt_products`
--
INSERT INTO `chatgpt_products` (`id`, `name`, `price`, `discount`, `days`, `power`, `enabled`, `sales`, `sort_num`, `created_at`, `updated_at`, `app_url`, `url`) VALUES
(5, '100次点卡', 9.99, 9.98, 0, 100, 1, 7, 0, '2023-08-28 10:55:08', '2024-06-11 16:48:44', NULL, NULL),
(6, '200次点卡', 19.90, 15.00, 0, 200, 1, 1, 0, '1970-01-01 08:00:00', '2024-06-11 11:41:52', NULL, NULL);
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_redeems`
--
DROP TABLE IF EXISTS `chatgpt_redeems`;
CREATE TABLE `chatgpt_redeems` (
`id` int NOT NULL,
`user_id` int NOT NULL COMMENT '用户 ID',
`name` varchar(30) NOT NULL COMMENT '兑换码名称',
`power` int NOT NULL COMMENT '算力',
`code` varchar(100) NOT NULL COMMENT '兑换码',
`enabled` tinyint(1) NOT NULL COMMENT '是否启用',
`created_at` datetime NOT NULL,
`redeemed_at` int NOT NULL COMMENT '兑换时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='兑换码';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_rewards`
--
DROP TABLE IF EXISTS `chatgpt_rewards`;
CREATE TABLE `chatgpt_rewards` (
`id` int NOT NULL,
`user_id` int NOT NULL COMMENT '用户 ID',
`tx_id` char(36) NOT NULL COMMENT '交易 ID',
`amount` decimal(10,2) NOT NULL COMMENT '打赏金额',
`remark` varchar(80) NOT NULL COMMENT '备注',
`status` tinyint(1) NOT NULL COMMENT '核销状态0未核销1已核销',
`exchange` varchar(255) NOT NULL COMMENT '兑换详情json',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户打赏';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_sd_jobs`
--
DROP TABLE IF EXISTS `chatgpt_sd_jobs`;
CREATE TABLE `chatgpt_sd_jobs` (
`id` int NOT NULL,
`user_id` int NOT NULL COMMENT '用户 ID',
`type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT 'txt2img' COMMENT '任务类别',
`task_id` char(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '任务 ID',
`prompt` varchar(2000) NOT NULL COMMENT '会话提示词',
`img_url` varchar(255) DEFAULT NULL COMMENT '图片URL',
`params` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '绘画参数json',
`progress` smallint DEFAULT '0' COMMENT '任务进度',
`publish` tinyint(1) NOT NULL COMMENT '是否发布',
`err_msg` varchar(255) DEFAULT NULL COMMENT '错误信息',
`power` smallint NOT NULL DEFAULT '0' COMMENT '消耗算力',
`created_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Stable Diffusion 任务表';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_suno_jobs`
--
DROP TABLE IF EXISTS `chatgpt_suno_jobs`;
CREATE TABLE `chatgpt_suno_jobs` (
`id` int NOT NULL,
`user_id` int NOT NULL COMMENT '用户 ID',
`channel` varchar(100) NOT NULL COMMENT '渠道',
`title` varchar(100) DEFAULT NULL COMMENT '歌曲标题',
`type` tinyint(1) DEFAULT '0' COMMENT '任务类型,1:灵感创作,2:自定义创作',
`task_id` varchar(50) DEFAULT NULL COMMENT '任务 ID',
`ref_task_id` char(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '引用任务 ID',
`tags` varchar(100) DEFAULT NULL COMMENT '歌曲风格',
`instrumental` tinyint(1) DEFAULT '0' COMMENT '是否为纯音乐',
`extend_secs` smallint DEFAULT '0' COMMENT '延长秒数',
`song_id` varchar(50) DEFAULT NULL COMMENT '要续写的歌曲 ID',
`ref_song_id` varchar(50) NOT NULL COMMENT '引用的歌曲ID',
`prompt` varchar(2000) NOT NULL COMMENT '提示词',
`cover_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '封面图地址',
`audio_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '音频地址',
`model_name` varchar(30) DEFAULT NULL COMMENT '模型地址',
`progress` smallint DEFAULT '0' COMMENT '任务进度',
`duration` smallint NOT NULL DEFAULT '0' COMMENT '歌曲时长',
`publish` tinyint(1) NOT NULL COMMENT '是否发布',
`err_msg` varchar(255) DEFAULT NULL COMMENT '错误信息',
`raw_data` text COMMENT '原始数据',
`power` smallint NOT NULL DEFAULT '0' COMMENT '消耗算力',
`play_times` int DEFAULT NULL COMMENT '播放次数',
`created_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='MidJourney 任务表';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_users`
--
DROP TABLE IF EXISTS `chatgpt_users`;
CREATE TABLE `chatgpt_users` (
`id` int NOT NULL,
`username` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户名',
`nickname` varchar(30) NOT NULL COMMENT '昵称',
`password` char(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码',
`avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '头像',
`salt` char(12) NOT NULL COMMENT '密码盐',
`power` int NOT NULL DEFAULT '0' COMMENT '剩余算力',
`expired_time` int NOT NULL COMMENT '用户过期时间',
`status` tinyint(1) NOT NULL COMMENT '当前状态',
`chat_config_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '聊天配置json',
`chat_roles_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '聊天角色 json',
`chat_models_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'AI模型 json',
`last_login_at` int NOT NULL COMMENT '最后登录时间',
`vip` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否会员',
`last_login_ip` char(16) NOT NULL COMMENT '最后登录 IP',
`openid` varchar(100) DEFAULT NULL COMMENT '第三方登录账号ID',
`platform` varchar(30) DEFAULT NULL COMMENT '登录平台',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';
--
-- 转存表中的数据 `chatgpt_users`
--
INSERT INTO `chatgpt_users` (`id`, `username`, `nickname`, `password`, `avatar`, `salt`, `power`, `expired_time`, `status`, `chat_config_json`, `chat_roles_json`, `chat_models_json`, `last_login_at`, `vip`, `last_login_ip`, `openid`, `platform`, `created_at`, `updated_at`) VALUES
(4, '18888888888', '极客学长@830270', 'ccc3fb7ab61b8b5d096a4a166ae21d121fc38c71bbd1be6173d9ab973214a63b', 'http://localhost:5678/static/upload/2024/5/1715651569509929.png', 'ueedue5l', 7291, 0, 1, '{\"api_keys\":{\"Azure\":\"\",\"ChatGLM\":\"\",\"OpenAI\":\"\"}}', '[\"red_book\",\"gpt\",\"seller\",\"artist\",\"lu_xun\",\"girl_friend\",\"psychiatrist\",\"teacher\",\"programmer\"]', '[1]', 1731554762, 1, '172.22.11.200', NULL, NULL, '2023-06-12 16:47:17', '2024-11-27 14:53:26');
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_user_login_logs`
--
DROP TABLE IF EXISTS `chatgpt_user_login_logs`;
CREATE TABLE `chatgpt_user_login_logs` (
`id` int NOT NULL,
`user_id` int NOT NULL COMMENT '用户ID',
`username` varchar(30) NOT NULL COMMENT '用户名',
`login_ip` char(16) NOT NULL COMMENT '登录IP',
`login_address` varchar(30) NOT NULL COMMENT '登录地址',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户登录日志';
--
-- 转储表的索引
--
--
-- 表的索引 `chatgpt_admin_users`
--
ALTER TABLE `chatgpt_admin_users`
ADD PRIMARY KEY (`id`) USING BTREE,
ADD UNIQUE KEY `username` (`username`) USING BTREE;
--
-- 表的索引 `chatgpt_api_keys`
--
ALTER TABLE `chatgpt_api_keys`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `chatgpt_chat_history`
--
ALTER TABLE `chatgpt_chat_history`
ADD PRIMARY KEY (`id`),
ADD KEY `chat_id` (`chat_id`);
--
-- 表的索引 `chatgpt_chat_items`
--
ALTER TABLE `chatgpt_chat_items`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `chat_id` (`chat_id`);
--
-- 表的索引 `chatgpt_chat_models`
--
ALTER TABLE `chatgpt_chat_models`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `chatgpt_chat_roles`
--
ALTER TABLE `chatgpt_chat_roles`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `marker` (`marker`);
--
-- 表的索引 `chatgpt_configs`
--
ALTER TABLE `chatgpt_configs`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `marker` (`marker`);
--
-- 表的索引 `chatgpt_dall_jobs`
--
ALTER TABLE `chatgpt_dall_jobs`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `chatgpt_files`
--
ALTER TABLE `chatgpt_files`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `chatgpt_functions`
--
ALTER TABLE `chatgpt_functions`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `name` (`name`);
--
-- 表的索引 `chatgpt_invite_codes`
--
ALTER TABLE `chatgpt_invite_codes`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `code` (`code`);
--
-- 表的索引 `chatgpt_invite_logs`
--
ALTER TABLE `chatgpt_invite_logs`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `chatgpt_menus`
--
ALTER TABLE `chatgpt_menus`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `chatgpt_mj_jobs`
--
ALTER TABLE `chatgpt_mj_jobs`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `task_id` (`task_id`),
ADD KEY `message_id` (`message_id`);
--
-- 表的索引 `chatgpt_orders`
--
ALTER TABLE `chatgpt_orders`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `order_no` (`order_no`);
--
-- 表的索引 `chatgpt_power_logs`
--
ALTER TABLE `chatgpt_power_logs`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `chatgpt_products`
--
ALTER TABLE `chatgpt_products`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `chatgpt_redeems`
--
ALTER TABLE `chatgpt_redeems`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `code` (`code`);
--
-- 表的索引 `chatgpt_rewards`
--
ALTER TABLE `chatgpt_rewards`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `tx_id` (`tx_id`);
--
-- 表的索引 `chatgpt_sd_jobs`
--
ALTER TABLE `chatgpt_sd_jobs`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `task_id` (`task_id`);
--
-- 表的索引 `chatgpt_suno_jobs`
--
ALTER TABLE `chatgpt_suno_jobs`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `chatgpt_users`
--
ALTER TABLE `chatgpt_users`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `username` (`username`);
--
-- 表的索引 `chatgpt_user_login_logs`
--
ALTER TABLE `chatgpt_user_login_logs`
ADD PRIMARY KEY (`id`);
--
-- 在导出的表使用AUTO_INCREMENT
--
--
-- 使用表AUTO_INCREMENT `chatgpt_admin_users`
--
ALTER TABLE `chatgpt_admin_users`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=113;
--
-- 使用表AUTO_INCREMENT `chatgpt_api_keys`
--
ALTER TABLE `chatgpt_api_keys`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_chat_history`
--
ALTER TABLE `chatgpt_chat_history`
MODIFY `id` bigint NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_chat_items`
--
ALTER TABLE `chatgpt_chat_items`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_chat_models`
--
ALTER TABLE `chatgpt_chat_models`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=52;
--
-- 使用表AUTO_INCREMENT `chatgpt_chat_roles`
--
ALTER TABLE `chatgpt_chat_roles`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=132;
--
-- 使用表AUTO_INCREMENT `chatgpt_configs`
--
ALTER TABLE `chatgpt_configs`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=4;
--
-- 使用表AUTO_INCREMENT `chatgpt_dall_jobs`
--
ALTER TABLE `chatgpt_dall_jobs`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_files`
--
ALTER TABLE `chatgpt_files`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_functions`
--
ALTER TABLE `chatgpt_functions`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=4;
--
-- 使用表AUTO_INCREMENT `chatgpt_invite_codes`
--
ALTER TABLE `chatgpt_invite_codes`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_invite_logs`
--
ALTER TABLE `chatgpt_invite_logs`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_menus`
--
ALTER TABLE `chatgpt_menus`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=19;
--
-- 使用表AUTO_INCREMENT `chatgpt_mj_jobs`
--
ALTER TABLE `chatgpt_mj_jobs`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_orders`
--
ALTER TABLE `chatgpt_orders`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_power_logs`
--
ALTER TABLE `chatgpt_power_logs`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_products`
--
ALTER TABLE `chatgpt_products`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=7;
--
-- 使用表AUTO_INCREMENT `chatgpt_redeems`
--
ALTER TABLE `chatgpt_redeems`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_rewards`
--
ALTER TABLE `chatgpt_rewards`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_sd_jobs`
--
ALTER TABLE `chatgpt_sd_jobs`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_suno_jobs`
--
ALTER TABLE `chatgpt_suno_jobs`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_users`
--
ALTER TABLE `chatgpt_users`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=23;
--
-- 使用表AUTO_INCREMENT `chatgpt_user_login_logs`
--
ALTER TABLE `chatgpt_user_login_logs`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
COMMIT;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

View File

@ -58,7 +58,7 @@ services:
# 后端 API 程序 # 后端 API 程序
geekai-api: geekai-api:
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-api:v4.1.1-amd64 image: registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-api:v4.1.2-amd64
container_name: geekai-api container_name: geekai-api
restart: always restart: always
depends_on: depends_on:
@ -80,7 +80,7 @@ services:
# 前端应用 # 前端应用
geekai-web: geekai-web:
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-web:v4.1.1-amd64 image: registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-web:v4.1.2-amd64
container_name: geekai-web container_name: geekai-web
restart: always restart: always
depends_on: depends_on:

View File

@ -4,8 +4,8 @@ VUE_APP_USER=18575670125
VUE_APP_PASS=12345678 VUE_APP_PASS=12345678
VUE_APP_ADMIN_USER=admin VUE_APP_ADMIN_USER=admin
VUE_APP_ADMIN_PASS=admin123 VUE_APP_ADMIN_PASS=admin123
VUE_APP_KEY_PREFIX=ChatPLUS_DEV_ VUE_APP_KEY_PREFIX=GeekAI_DEV_
VUE_APP_TITLE="Geek-AI 创作系统" VUE_APP_TITLE="Geek-AI 创作系统"
VUE_APP_VERSION=v4.1.1 VUE_APP_VERSION=v4.1.2
VUE_APP_DOCS_URL=https://docs.geekai.me VUE_APP_DOCS_URL=https://docs.geekai.me
VUE_APP_GIT_URL=https://github.com/yangjian102621/geekai VUE_APP_GIT_URL=https://github.com/yangjian102621/geekai

View File

@ -1,8 +1,7 @@
VUE_APP_API_HOST= VUE_APP_API_HOST=
VUE_APP_WS_HOST= VUE_APP_WS_HOST=
VUE_APP_KEY_PREFIX=ChatPLUS_ VUE_APP_KEY_PREFIX=GeekAI_
VUE_APP_TITLE="Geek-AI 创作系统" VUE_APP_TITLE="Geek-AI 创作系统"
VUE_APP_VERSION=v4.1.1 VUE_APP_VERSION=v4.1.2
VUE_APP_DOCS_URL=https://docs.geekai.me VUE_APP_DOCS_URL=https://docs.geekai.me
VUE_APP_GIT_URL=https://github.com/yangjian102621/geekai VUE_APP_GIT_URL=https://github.com/yangjian102621/geekai

View File

@ -1,21 +0,0 @@
import {httpGet} from "@/utils/http";
export function checkSession() {
return new Promise((resolve, reject) => {
httpGet('/api/user/session').then(res => {
resolve(res.data)
}).catch(err => {
reject(err)
})
})
}
export function checkAdminSession() {
return new Promise((resolve, reject) => {
httpGet('/api/admin/session').then(res => {
resolve(res)
}).catch(err => {
reject(err)
})
})
}

View File

@ -221,134 +221,7 @@
// //
@import "waterfall-list.styl"
.job-list-box {
@import "running-job-list.styl"
.finish-job-list {
#waterfall {
display flex
justify-content center
padding-top 20px
flex-flow column
.job-item {
width 100%
height 100%
border 1px solid #666666
padding 6px
overflow hidden
border-radius 6px
transition: all 0.3s ease; /* */
position relative
.opt {
.opt-line {
margin 6px 0
ul {
display flex
flex-flow row
li {
margin-right 6px
a {
padding 3px 0
width 40px
text-align center
border-radius 5px
display block
cursor pointer
background-color #4E5058
color #ffffff
&:hover {
background-color #6D6F78
}
}
}
.show-prompt {
font-size 20px
cursor pointer
}
}
}
}
.remove {
display none
position absolute
right 10px
top 10px
}
&:hover {
.remove {
display block
}
}
}
.animate {
&:hover {
box-shadow: 0 0 10px rgba(71, 255, 241, 0.6); /* */
transform: translateY(-10px); /* 10 */
}
}
}
}
.el-image {
width 100%
height 100%
overflow visible
.el-image-viewer__wrapper {
img {
width auto
height auto
}
}
.image-slot {
display flex
flex-flow column
justify-content center
align-items center
min-height 200px
color #ffffff
.iconfont {
font-size 50px
margin-bottom 10px
}
}
}
.el-image.upscale {
img {
height 310px
}
.image-slot {
height 310px
}
.el-image-viewer__wrapper {
img {
width auto
height auto
}
}
}
}
} }
.no-more-data { .no-more-data {

View File

@ -220,136 +220,8 @@
} }
} }
// //
@import "waterfall-list.styl"
.job-list-box {
@import "running-job-list.styl"
.finish-job-list {
#waterfall {
display flex
justify-content center
padding-top 20px
flex-flow column
.job-item {
width 100%
height 100%
border 1px solid #666666
padding 6px
overflow hidden
border-radius 6px
transition: all 0.3s ease; /* */
position relative
.opt {
.opt-line {
margin 6px 0
ul {
display flex
flex-flow row
li {
margin-right 6px
a {
padding 3px 0
width 40px
text-align center
border-radius 5px
display block
cursor pointer
background-color #4E5058
color #ffffff
&:hover {
background-color #6D6F78
}
}
}
.show-prompt {
font-size 20px
cursor pointer
}
}
}
}
.remove {
display none
position absolute
right 10px
top 10px
}
&:hover {
.remove {
display block
}
}
}
.animate {
&:hover {
box-shadow: 0 0 10px rgba(71, 255, 241, 0.6); /* */
transform: translateY(-10px); /* 10 */
}
}
}
}
.el-image {
width 100%
height 100%
overflow visible
.el-image-viewer__wrapper {
img {
width auto
height auto
}
}
.image-slot {
display flex
flex-flow column
justify-content center
align-items center
min-height 200px
color #ffffff
.iconfont {
font-size 50px
margin-bottom 10px
}
}
}
.el-image.upscale {
img {
height 310px
}
.image-slot {
height 310px
}
.el-image-viewer__wrapper {
img {
width auto
height auto
}
}
}
}
} }
.no-more-data { .no-more-data {

View File

@ -0,0 +1,146 @@
.job-list-box {
@import "running-job-list.styl"
.finish-job-list {
#waterfall {
display flex
justify-content center
padding-top 20px
flex-flow column
.job-item {
width 100%
height 100%
border 1px solid #666666
padding 6px
overflow hidden
border-radius 6px
transition: all 0.3s ease; /* */
position relative
.opt {
.opt-line {
margin 6px 0
ul {
display flex
flex-flow row
li {
margin-right 6px
a {
padding 3px 0
width 40px
text-align center
border-radius 5px
display block
cursor pointer
background-color #4E5058
color #ffffff
&:hover {
background-color #6D6F78
}
}
}
.show-prompt {
font-size 20px
cursor pointer
}
}
}
}
.remove {
display none
position absolute
right 10px
top 10px
}
&:hover {
.remove {
display block
}
}
}
.animate {
&:hover {
box-shadow: 0 0 10px rgba(71, 255, 241, 0.6); /* */
transform: translateY(-10px); /* 10 */
}
}
}
}
.el-image {
width 100%
height 100%
overflow visible
.el-image-viewer__wrapper {
img {
width auto
height auto
}
}
.image-slot {
display flex
flex-flow column
justify-content center
align-items center
min-height 220px
color #ffffff
.err-msg-container {
overflow hidden
word-break break-all
padding 15px
.title {
font-size 20px
text-align center
font-weight bold
color #f56c6c
margin-bottom 30px
}
.opt {
display flex
justify-content center
}
}
.iconfont {
font-size 50px
margin-bottom 10px
}
}
}
.el-image.upscale {
img {
height 310px
}
.image-slot {
height 310px
}
.el-image-viewer__wrapper {
img {
width auto
height auto
}
}
}
}

View File

@ -6,7 +6,7 @@
</div> </div>
<div class="chat-item"> <div class="chat-item">
<div class="content" v-html="data.content"></div> <div class="content" v-html="md.render(processContent(data.content))"></div>
<div class="bar" v-if="data.created_at"> <div class="bar" v-if="data.created_at">
<span class="bar-item"><el-icon><Clock/></el-icon> {{ dateFormat(data.created_at) }}</span> <span class="bar-item"><el-icon><Clock/></el-icon> {{ dateFormat(data.created_at) }}</span>
<span class="bar-item">tokens: {{ data.tokens }}</span> <span class="bar-item">tokens: {{ data.tokens }}</span>
@ -17,7 +17,7 @@
content="复制回答" content="复制回答"
placement="bottom" placement="bottom"
> >
<el-icon class="copy-reply" :data-clipboard-text="data.orgContent"> <el-icon class="copy-reply" :data-clipboard-text="data.content">
<DocumentCopy/> <DocumentCopy/>
</el-icon> </el-icon>
</el-tooltip> </el-tooltip>
@ -34,7 +34,7 @@
</el-tooltip> </el-tooltip>
</span> </span>
<span class="bar-item" @click="synthesis(data.orgContent)"> <span class="bar-item" @click="synthesis(data.content)">
<el-tooltip <el-tooltip
class="box-item" class="box-item"
effect="dark" effect="dark"
@ -69,7 +69,7 @@
</div> </div>
<div class="chat-item"> <div class="chat-item">
<div class="content-wrapper"> <div class="content-wrapper">
<div class="content" v-html="data.content"></div> <div class="content" v-html="md.render(processContent(data.content))"></div>
</div> </div>
<div class="bar" v-if="data.created_at"> <div class="bar" v-if="data.created_at">
<span class="bar-item"><el-icon><Clock/></el-icon> {{ dateFormat(data.created_at) }}</span> <span class="bar-item"><el-icon><Clock/></el-icon> {{ dateFormat(data.created_at) }}</span>
@ -81,7 +81,7 @@
content="复制回答" content="复制回答"
placement="bottom" placement="bottom"
> >
<el-icon class="copy-reply" :data-clipboard-text="data.orgContent"> <el-icon class="copy-reply" :data-clipboard-text="data.content">
<DocumentCopy/> <DocumentCopy/>
</el-icon> </el-icon>
</el-tooltip> </el-tooltip>
@ -98,7 +98,7 @@
</el-tooltip> </el-tooltip>
</span> </span>
<span class="bar-item bg" @click="synthesis(data.orgContent)"> <span class="bar-item bg" @click="synthesis(data.content)">
<el-tooltip <el-tooltip
class="box-item" class="box-item"
effect="dark" effect="dark"
@ -118,7 +118,8 @@
<script setup> <script setup>
import {Clock, DocumentCopy, Refresh} from "@element-plus/icons-vue"; import {Clock, DocumentCopy, Refresh} from "@element-plus/icons-vue";
import {ElMessage} from "element-plus"; import {ElMessage} from "element-plus";
import {dateFormat} from "@/utils/libs"; import {dateFormat, processContent} from "@/utils/libs";
import hl from "highlight.js";
// eslint-disable-next-line no-undef,no-unused-vars // eslint-disable-next-line no-undef,no-unused-vars
const props = defineProps({ const props = defineProps({
data: { data: {
@ -128,7 +129,6 @@ const props = defineProps({
content: "", content: "",
created_at: "", created_at: "",
tokens: 0, tokens: 0,
orgContent: ""
}, },
}, },
readOnly: { readOnly: {
@ -141,6 +141,33 @@ const props = defineProps({
}, },
}) })
const mathjaxPlugin = require('markdown-it-mathjax3')
const md = require('markdown-it')({
breaks: true,
html: true,
linkify: true,
typographer: true,
highlight: function (str, lang) {
const codeIndex = parseInt(Date.now()) + Math.floor(Math.random() * 10000000)
//
const copyBtn = `<span class="copy-code-btn" data-clipboard-action="copy" data-clipboard-target="#copy-target-${codeIndex}">复制</span>
<textarea style="position: absolute;top: -9999px;left: -9999px;z-index: -9999;" id="copy-target-${codeIndex}">${str.replace(/<\/textarea>/g, '&lt;/textarea>')}</textarea>`
if (lang && hl.getLanguage(lang)) {
const langHtml = `<span class="lang-name">${lang}</span>`
//
const preCode = hl.highlight(lang, str, true).value
// pre
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn} ${langHtml}</pre>`
}
//
const preCode = md.utils.escapeHtml(str)
// pre
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn}</pre>`
}
});
md.use(mathjaxPlugin)
const emits = defineEmits(['regen']); const emits = defineEmits(['regen']);
if (!props.data.icon) { if (!props.data.icon) {

View File

@ -16,6 +16,7 @@
import {ref} from "vue"; import {ref} from "vue";
import {httpGet} from "@/utils/http"; import {httpGet} from "@/utils/http";
import {showMessageError} from "@/utils/dialog"; import {showMessageError} from "@/utils/dialog";
import {getLicenseInfo, getSystemInfo} from "@/store/cache";
const title = ref("") const title = ref("")
const version = ref(process.env.VUE_APP_VERSION) const version = ref(process.env.VUE_APP_VERSION)
@ -30,14 +31,14 @@ const props = defineProps({
}); });
// //
httpGet("/api/config/get?key=system").then(res => { getSystemInfo().then(res => {
title.value = res.data.title??process.env.VUE_APP_TITLE title.value = res.data.title??process.env.VUE_APP_TITLE
copyRight.value = res.data.copyright?res.data.copyright:'极客学长 © 2023 - '+new Date().getFullYear()+' All rights reserved.' copyRight.value = res.data.copyright?res.data.copyright:'极客学长 © 2023 - '+new Date().getFullYear()+' All rights reserved.'
}).catch(e => { }).catch(e => {
showMessageError("获取系统配置失败:" + e.message) showMessageError("获取系统配置失败:" + e.message)
}) })
httpGet("/api/config/license").then(res => { getLicenseInfo().then(res => {
license.value = res.data license.value = res.data
}).catch(e => { }).catch(e => {
showMessageError("获取 License 失败:" + e.message) showMessageError("获取 License 失败:" + e.message)

View File

@ -221,14 +221,15 @@
</template> </template>
<script setup> <script setup>
import {nextTick, onUnmounted, ref, watch} from "vue" import {ref, watch} from "vue"
import {httpGet, httpPost} from "@/utils/http"; import {httpPost} from "@/utils/http";
import {ElMessage} from "element-plus"; import {ElMessage} from "element-plus";
import {setUserToken} from "@/store/session"; import {setUserToken} from "@/store/session";
import {validateEmail, validateMobile} from "@/utils/validate"; import {validateEmail, validateMobile} from "@/utils/validate";
import {Checked, Close, Iphone, Lock, Message} from "@element-plus/icons-vue"; import {Checked, Close, Iphone, Lock, Message} from "@element-plus/icons-vue";
import SendMsg from "@/components/SendMsg.vue"; import SendMsg from "@/components/SendMsg.vue";
import {arrayContains} from "@/utils/libs"; import {arrayContains} from "@/utils/libs";
import {getSystemInfo} from "@/store/cache";
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
const props = defineProps({ const props = defineProps({
@ -256,7 +257,7 @@ const wxImg = ref("/images/wx.png")
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
const emits = defineEmits(['hide', 'success']); const emits = defineEmits(['hide', 'success']);
httpGet("/api/config/get?key=system").then(res => { getSystemInfo().then(res => {
if (res.data) { if (res.data) {
const registerWays = res.data['register_ways'] const registerWays = res.data['register_ways']
if (arrayContains(registerWays, "mobile")) { if (arrayContains(registerWays, "mobile")) {

View File

@ -8,13 +8,9 @@
:title="title" :title="title"
> >
<div class="form" id="bind-mobile-form"> <div class="form" id="bind-mobile-form">
<el-alert v-if="mobile !== ''" type="info" show-icon :closable="false" style="margin-bottom: 20px;">
<p>请输入您参与众筹的 <strong style="color:#F56C6C">微信支付转账单号</strong> 兑换相应的对话次数</p>
</el-alert>
<el-form :model="form"> <el-form :model="form">
<el-form-item label="转账单号"> <el-form-item label="兑换码">
<el-input v-model="form.tx_id"/> <el-input v-model="form.code"/>
</el-form-item> </el-form-item>
</el-form> </el-form>
</div> </div>
@ -22,7 +18,7 @@
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button type="primary" @click="save"> <el-button type="primary" @click="save">
确认核销 立即兑换
</el-button> </el-button>
</span> </span>
</template> </template>
@ -33,36 +29,33 @@
import {computed, ref} from "vue"; import {computed, ref} from "vue";
import {ElMessage} from "element-plus"; import {ElMessage} from "element-plus";
import {httpPost} from "@/utils/http"; import {httpPost} from "@/utils/http";
import {showMessageError, showMessageOK} from "@/utils/dialog";
const props = defineProps({ const props = defineProps({
show: Boolean, show: Boolean,
mobile: String
}); });
const showDialog = computed(() => { const showDialog = computed(() => {
return props.show return props.show
}) })
const title = ref('众筹码核销') const title = ref('兑换码核销')
const form = ref({ const form = ref({
tx_id: '', code: '',
}) })
const emits = defineEmits(['hide']); const emits = defineEmits(['hide']);
const save = () => { const save = () => {
if (form.value.tx_id === '') { if (form.value.code === '') {
return ElMessage.error({message: "请输入微信支付转账单号"}); return ElMessage.error({message: "请输入兑换码"});
} }
httpPost('/api/reward/verify', form.value).then(() => { httpPost('/api/redeem/verify', form.value).then(() => {
ElMessage.success({ showMessageOK("兑换成功!")
message: '核销成功', emits('hide', true)
duration: 1000,
onClose: () => location.reload()
})
}).catch(e => { }).catch(e => {
ElMessage.error({message: "核销失败:" + e.message}); showMessageError("兑换失败:" + e.message)
}) })
} }

View File

@ -50,7 +50,7 @@ import {ElMessage} from "element-plus";
import {Plus} from "@element-plus/icons-vue"; import {Plus} from "@element-plus/icons-vue";
import Compressor from "compressorjs"; import Compressor from "compressorjs";
import {dateFormat} from "@/utils/libs"; import {dateFormat} from "@/utils/libs";
import {checkSession} from "@/action/session"; import {checkSession} from "@/store/cache";
const user = ref({ const user = ref({
vip: false, vip: false,

View File

@ -56,8 +56,8 @@
<script setup> <script setup>
import {onMounted, ref} from "vue"; import {onMounted, ref} from "vue";
import {httpGet} from "@/utils/http";
import {ElMessage} from "element-plus"; import {ElMessage} from "element-plus";
import {getSystemInfo} from "@/store/cache";
const title = ref(process.env.VUE_APP_TITLE) const title = ref(process.env.VUE_APP_TITLE)
const version = ref(process.env.VUE_APP_VERSION) const version = ref(process.env.VUE_APP_VERSION)
@ -99,7 +99,7 @@ const capabilities = ref([
]) ])
onMounted(() => { onMounted(() => {
httpGet("/api/config/get?key=system").then(res => { getSystemInfo().then(res => {
title.value = res.data.title title.value = res.data.title
}).catch(e => { }).catch(e => {
ElMessage.error("获取系统配置失败:" + e.message) ElMessage.error("获取系统配置失败:" + e.message)

View File

@ -118,8 +118,8 @@ const items = [
}, },
{ {
icon: 'reward', icon: 'reward',
index: '/admin/reward', index: '/admin/redeem',
title: '众筹管理', title: '兑换码',
}, },
{ {
icon: 'control', icon: 'control',

View File

@ -36,7 +36,7 @@
import {useTagsStore} from '@/store/tags'; import {useTagsStore} from '@/store/tags';
import {onBeforeRouteUpdate, useRoute, useRouter} from 'vue-router'; import {onBeforeRouteUpdate, useRoute, useRouter} from 'vue-router';
import {ArrowDown, Close} from "@element-plus/icons-vue"; import {ArrowDown, Close} from "@element-plus/icons-vue";
import {checkAdminSession} from "@/action/session"; import {checkAdminSession} from "@/store/cache";
import {ElMessageBox} from "element-plus"; import {ElMessageBox} from "element-plus";
import {computed} from "vue"; import {computed} from "vue";

View File

@ -6,8 +6,6 @@
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import {createRouter, createWebHistory} from "vue-router"; import {createRouter, createWebHistory} from "vue-router";
import {ref} from "vue";
import {httpGet} from "@/utils/http";
const routes = [ const routes = [
{ {
@ -188,10 +186,10 @@ const routes = [
component: () => import('@/views/admin/Order.vue'), component: () => import('@/views/admin/Order.vue'),
}, },
{ {
path: '/admin/reward', path: '/admin/redeem',
name: 'admin-reward', name: 'admin-redeem',
meta: {title: '众筹管理'}, meta: {title: '兑换码管理'},
component: () => import('@/views/admin/Reward.vue'), component: () => import('@/views/admin/Redeem.vue'),
}, },
{ {
path: '/admin/loginLog', path: '/admin/loginLog',

90
web/src/store/cache.js Normal file
View File

@ -0,0 +1,90 @@
import {httpGet} from "@/utils/http";
import Storage from "good-storage";
import {showMessageError} from "@/utils/dialog";
const userDataKey = "USER_INFO_CACHE_KEY"
const adminDataKey = "ADMIN_INFO_CACHE_KEY"
const systemInfoKey = "SYSTEM_INFO_CACHE_KEY"
const licenseInfoKey = "LICENSE_INFO_CACHE_KEY"
export function checkSession() {
const item = Storage.get(userDataKey) ?? {expire:0, data:null}
if (item.expire > Date.now()) {
return Promise.resolve(item.data)
}
return new Promise((resolve, reject) => {
httpGet('/api/user/session').then(res => {
item.data = res.data
// cache expires after 5 minutes
item.expire = Date.now() + 1000 * 60 * 5
Storage.set(userDataKey, item)
resolve(item.data)
}).catch(err => {
Storage.remove(userDataKey)
reject(err)
})
})
}
export function removeUserInfo() {
Storage.remove(userDataKey)
}
export function checkAdminSession() {
const item = Storage.get(adminDataKey) ?? {expire:0, data:null}
if (item.expire > Date.now()) {
return Promise.resolve(item.data)
}
return new Promise((resolve, reject) => {
httpGet('/api/admin/session').then(res => {
item.data = res.data
// cache expires after 10 minutes
item.expire = Date.now() + 1000 * 60 * 10
Storage.set(adminDataKey, item)
resolve(item.data)
}).catch(err => {
reject(err)
})
})
}
export function removeAdminInfo() {
Storage.remove(adminDataKey)
}
export function getSystemInfo() {
const item = Storage.get(systemInfoKey) ?? {expire:0, data:null}
if (item.expire > Date.now()) {
return Promise.resolve(item.data)
}
return new Promise((resolve, reject) => {
httpGet('/api/config/get?key=system').then(res => {
item.data = res
// cache expires after 10 minutes
item.expire = Date.now() + 1000 * 60 * 10
Storage.set(systemInfoKey, item)
resolve(item.data)
}).catch(err => {
reject(err)
})
})
}
export function getLicenseInfo() {
const item = Storage.get(licenseInfoKey) ?? {expire:0, data:null}
if (item.expire > Date.now()) {
return Promise.resolve(item.data)
}
return new Promise((resolve, reject) => {
httpGet('/api/config/license').then(res => {
item.data = res
// cache expires after 10 minutes
item.expire = Date.now() + 1000 * 60 * 10
Storage.set(licenseInfoKey, item)
resolve(item.data)
}).catch(err => {
reject(err)
})
})
}

View File

@ -1,11 +0,0 @@
import Storage from 'good-storage'
const CHAT_CONFIG_KEY = process.env.VUE_APP_KEY_PREFIX + "chat_config"
export function getChatConfig() {
return Storage.get(CHAT_CONFIG_KEY)
}
export function setChatConfig(chatConfig) {
Storage.set(CHAT_CONFIG_KEY, chatConfig)
}

View File

@ -1,5 +1,6 @@
import {randString} from "@/utils/libs"; import {randString} from "@/utils/libs";
import Storage from "good-storage"; import Storage from "good-storage";
import {removeAdminInfo, removeUserInfo} from "@/store/cache";
/** /**
* storage handler * storage handler
@ -22,6 +23,7 @@ export function setUserToken(token) {
export function removeUserToken() { export function removeUserToken() {
Storage.remove(UserTokenKey) Storage.remove(UserTokenKey)
removeUserInfo()
} }
export function getAdminToken() { export function getAdminToken() {
@ -34,4 +36,5 @@ export function setAdminToken(token) {
export function removeAdminToken() { export function removeAdminToken() {
Storage.remove(AdminTokenKey) Storage.remove(AdminTokenKey)
removeAdminInfo()
} }

View File

@ -34,7 +34,6 @@ axios.interceptors.response.use(
} else { } else {
removeUserToken() removeUserToken()
} }
console.log(error.response.data)
error.response.data.message = "请先登录" error.response.data.message = "请先登录"
return Promise.reject(error.response.data) return Promise.reject(error.response.data)
} }

View File

@ -59,7 +59,7 @@
import {onMounted, ref} from "vue" import {onMounted, ref} from "vue"
import {ElMessage} from "element-plus"; import {ElMessage} from "element-plus";
import {httpGet, httpPost} from "@/utils/http"; import {httpGet, httpPost} from "@/utils/http";
import {checkSession} from "@/action/session"; import {checkSession} from "@/store/cache";
import {arrayContains, removeArrayItem, substr} from "@/utils/libs"; import {arrayContains, removeArrayItem, substr} from "@/utils/libs";
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import {useSharedStore} from "@/store/sharedata"; import {useSharedStore} from "@/store/sharedata";

View File

@ -204,26 +204,24 @@ import {Delete, Edit, More, Plus, Promotion, Search, Share, VideoPause} from '@e
import 'highlight.js/styles/a11y-dark.css' import 'highlight.js/styles/a11y-dark.css'
import { import {
isMobile, isMobile,
processContent,
randString, randString,
removeArrayItem, removeArrayItem,
UUID UUID
} from "@/utils/libs"; } from "@/utils/libs";
import {ElMessage, ElMessageBox} from "element-plus"; import {ElMessage, ElMessageBox} from "element-plus";
import hl from "highlight.js";
import {getSessionId, getUserToken, removeUserToken} from "@/store/session"; import {getSessionId, getUserToken, removeUserToken} from "@/store/session";
import {httpGet, httpPost} from "@/utils/http"; import {httpGet, httpPost} from "@/utils/http";
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import Clipboard from "clipboard"; import Clipboard from "clipboard";
import {checkSession} from "@/action/session"; import {checkSession, getSystemInfo} from "@/store/cache";
import Welcome from "@/components/Welcome.vue"; import Welcome from "@/components/Welcome.vue";
import {useSharedStore} from "@/store/sharedata"; import {useSharedStore} from "@/store/sharedata";
import FileSelect from "@/components/FileSelect.vue"; import FileSelect from "@/components/FileSelect.vue";
import FileList from "@/components/FileList.vue"; import FileList from "@/components/FileList.vue";
import ChatSetting from "@/components/ChatSetting.vue"; import ChatSetting from "@/components/ChatSetting.vue";
import axios from "axios";
import BackTop from "@/components/BackTop.vue"; import BackTop from "@/components/BackTop.vue";
import {showMessageError} from "@/utils/dialog"; import {showMessageError} from "@/utils/dialog";
import hl from "highlight.js";
const title = ref('ChatGPT-智能助手'); const title = ref('ChatGPT-智能助手');
const models = ref([]) const models = ref([])
@ -262,12 +260,18 @@ if (isMobile()) {
} }
// //
httpGet("/api/config/get?key=system").then(res => { getSystemInfo().then(res => {
title.value = res.data.title title.value = res.data.title
}).catch(e => { }).catch(e => {
ElMessage.error("获取系统配置失败:" + e.message) ElMessage.error("获取系统配置失败:" + e.message)
}) })
const md = require('markdown-it')({
breaks: true,
html: true,
linkify: true,
typographer: true
});
// //
httpGet("/api/config/get?key=notice").then(res => { httpGet("/api/config/get?key=notice").then(res => {
try { try {
@ -367,7 +371,6 @@ const initData = () => {
inputRef.value.addEventListener('paste', (event) => { inputRef.value.addEventListener('paste', (event) => {
const items = (event.clipboardData || window.clipboardData).items; const items = (event.clipboardData || window.clipboardData).items;
let fileFound = false; let fileFound = false;
loading.value = true
for (let item of items) { for (let item of items) {
if (item.kind === 'file') { if (item.kind === 'file') {
@ -376,6 +379,7 @@ const initData = () => {
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append('file', file);
loading.value = true
// //
httpPost('/api/upload', formData).then((res) => { httpPost('/api/upload', formData).then((res) => {
files.value.push(res.data) files.value.push(res.data)
@ -547,33 +551,6 @@ const removeChat = function (chat) {
} }
const mathjaxPlugin = require('markdown-it-mathjax3')
const md = require('markdown-it')({
breaks: true,
html: true,
linkify: true,
typographer: true,
highlight: function (str, lang) {
const codeIndex = parseInt(Date.now()) + Math.floor(Math.random() * 10000000)
//
const copyBtn = `<span class="copy-code-btn" data-clipboard-action="copy" data-clipboard-target="#copy-target-${codeIndex}">复制</span>
<textarea style="position: absolute;top: -9999px;left: -9999px;z-index: -9999;" id="copy-target-${codeIndex}">${str.replace(/<\/textarea>/g, '&lt;/textarea>')}</textarea>`
if (lang && hl.getLanguage(lang)) {
const langHtml = `<span class="lang-name">${lang}</span>`
//
const preCode = hl.highlight(lang, str, true).value
// pre
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn} ${langHtml}</pre>`
}
//
const preCode = md.utils.escapeHtml(str)
// pre
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn}</pre>`
}
});
md.use(mathjaxPlugin)
// socket // socket
const prompt = ref(''); const prompt = ref('');
const showStopGenerate = ref(false); // const showStopGenerate = ref(false); //
@ -622,7 +599,6 @@ const connect = function (chat_id, role_id) {
id: randString(32), id: randString(32),
icon: _role['icon'], icon: _role['icon'],
content: _role['hello_msg'], content: _role['hello_msg'],
orgContent: _role['hello_msg'],
}) })
ElMessage.success({message: "对话连接成功!", duration: 1000}) ElMessage.success({message: "对话连接成功!", duration: 1000})
} else { // } else { //
@ -645,7 +621,6 @@ const connect = function (chat_id, role_id) {
icon: _role['icon'], icon: _role['icon'],
prompt:prePrompt, prompt:prePrompt,
content: "", content: "",
orgContent: "",
}); });
} else if (data.type === 'end') { // } else if (data.type === 'end') { //
// //
@ -680,8 +655,7 @@ const connect = function (chat_id, role_id) {
lineBuffer.value += data.content; lineBuffer.value += data.content;
const reply = chatData.value[chatData.value.length - 1] const reply = chatData.value[chatData.value.length - 1]
if (reply) { if (reply) {
reply['orgContent'] = lineBuffer.value; reply['content'] = lineBuffer.value;
reply['content'] = md.render(processContent(lineBuffer.value));
} }
} }
// //
@ -845,12 +819,8 @@ const loadChatHistory = function (chatId) {
} }
showHello.value = false showHello.value = false
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
data[i].orgContent = data[i].content; if (data[i].type === 'reply' && i > 0) {
if (data[i].type === 'reply') { data[i].prompt = data[i - 1].content
data[i].content = md.render(processContent(data[i].content))
if (i > 0) {
data[i].prompt = data[i - 1].orgContent
}
} }
chatData.value.push(data[i]); chatData.value.push(data[i]);
} }

View File

@ -125,6 +125,24 @@
</template> </template>
</el-image> </el-image>
<el-image v-else-if="slotProp.item.progress === 101">
<template #error>
<div class="image-slot">
<div class="err-msg-container">
<div class="title">任务失败</div>
<div class="opt">
<el-popover title="错误详情" trigger="click" :width="250" :content="slotProp.item['err_msg']" placement="top">
<template #reference>
<el-button type="info">详情</el-button>
</template>
</el-popover>
<el-button type="danger" @click="removeImage(slotProp.item)">删除</el-button>
</div>
</div>
</div>
</template>
</el-image>
<el-image v-else> <el-image v-else>
<template #error> <template #error>
<div class="image-slot"> <div class="image-slot">
@ -136,17 +154,17 @@
<div class="remove"> <div class="remove">
<el-tooltip content="删除" placement="top" effect="light"> <el-tooltip content="删除" placement="top" effect="light">
<el-button type="danger" :icon="Delete" @click="removeImage($event,slotProp.item)" circle/> <el-button type="danger" :icon="Delete" @click="removeImage(slotProp.item)" circle/>
</el-tooltip> </el-tooltip>
<el-tooltip content="分享" placement="top" effect="light" v-if="slotProp.item.publish"> <el-tooltip content="分享" placement="top" effect="light" v-if="slotProp.item.publish">
<el-button type="warning" <el-button type="warning"
@click="publishImage($event,slotProp.item, false)" @click="publishImage(slotProp.item, false)"
circle> circle>
<i class="iconfont icon-cancel-share"></i> <i class="iconfont icon-cancel-share"></i>
</el-button> </el-button>
</el-tooltip> </el-tooltip>
<el-tooltip content="取消分享" placement="top" effect="light" v-else> <el-tooltip content="取消分享" placement="top" effect="light" v-else>
<el-button type="success" @click="publishImage($event,slotProp.item, true)" circle> <el-button type="success" @click="publishImage(slotProp.item, true)" circle>
<i class="iconfont icon-share-bold"></i> <i class="iconfont icon-share-bold"></i>
</el-button> </el-button>
</el-tooltip> </el-tooltip>
@ -185,12 +203,12 @@
</template> </template>
<script setup> <script setup>
import {onMounted, onUnmounted, ref} from "vue" import {nextTick, onMounted, onUnmounted, ref} from "vue"
import {Delete, InfoFilled, Picture} from "@element-plus/icons-vue"; import {Delete, InfoFilled, Picture} from "@element-plus/icons-vue";
import {httpGet, httpPost} from "@/utils/http"; import {httpGet, httpPost} from "@/utils/http";
import {ElMessage, ElMessageBox, ElNotification} from "element-plus"; import {ElMessage, ElMessageBox, ElNotification} from "element-plus";
import Clipboard from "clipboard"; import Clipboard from "clipboard";
import {checkSession} from "@/action/session"; import {checkSession, getSystemInfo} from "@/store/cache";
import {useSharedStore} from "@/store/sharedata"; import {useSharedStore} from "@/store/sharedata";
import TaskList from "@/components/TaskList.vue"; import TaskList from "@/components/TaskList.vue";
import BackTop from "@/components/BackTop.vue"; import BackTop from "@/components/BackTop.vue";
@ -245,7 +263,7 @@ onMounted(() => {
ElMessage.error('复制失败!'); ElMessage.error('复制失败!');
}) })
httpGet("/api/config/get?key=system").then(res => { getSystemInfo().then(res => {
dallPower.value = res.data["dall_power"] dallPower.value = res.data["dall_power"]
}).catch(e => { }).catch(e => {
ElMessage.error("获取系统配置失败:" + e.message) ElMessage.error("获取系统配置失败:" + e.message)
@ -286,25 +304,9 @@ const connect = () => {
} }
} }
//
const sendHeartbeat = () => {
clearTimeout(heartbeatHandle.value)
new Promise((resolve, reject) => {
if (socket.value !== null) {
socket.value.send(JSON.stringify({type: "heartbeat", content: "ping"}))
}
resolve("success")
}).then(() => {
heartbeatHandle.value = setTimeout(() => sendHeartbeat(), 5000)
});
}
const _socket = new WebSocket(host + `/api/dall/client?user_id=${userId.value}`); const _socket = new WebSocket(host + `/api/dall/client?user_id=${userId.value}`);
_socket.addEventListener('open', () => { _socket.addEventListener('open', () => {
socket.value = _socket; socket.value = _socket;
//
sendHeartbeat()
}); });
_socket.addEventListener('message', event => { _socket.addEventListener('message', event => {
@ -313,12 +315,12 @@ const connect = () => {
reader.readAsText(event.data, "UTF-8") reader.readAsText(event.data, "UTF-8")
reader.onload = () => { reader.onload = () => {
const message = String(reader.result) const message = String(reader.result)
if (message === "FINISH") { if (message === "FINISH" || message === "FAIL") {
page.value = 0 page.value = 0
isOver.value = false isOver.value = false
fetchFinishJobs(page.value) fetchFinishJobs(page.value)
} }
fetchRunningJobs() nextTick(() => fetchRunningJobs())
} }
} }
}); });
@ -336,22 +338,7 @@ const fetchRunningJobs = () => {
} }
// //
httpGet(`/api/dall/jobs?finish=false`).then(res => { httpGet(`/api/dall/jobs?finish=false`).then(res => {
const jobs = res.data runningJobs.value = res.data
const _jobs = []
for (let i = 0; i < jobs.length; i++) {
if (jobs[i].progress === -1) {
ElNotification({
title: '任务执行失败',
dangerouslyUseHTMLString: true,
message: `任务ID${jobs[i]['task_id']}<br />原因:${jobs[i]['err_msg']}`,
type: 'error',
})
power.value += dallPower.value
continue
}
_jobs.push(jobs[i])
}
runningJobs.value = _jobs
}).catch(e => { }).catch(e => {
ElMessage.error("获取任务失败:" + e.message) ElMessage.error("获取任务失败:" + e.message)
}) })
@ -409,8 +396,7 @@ const generate = () => {
}) })
} }
const removeImage = (event, item) => { const removeImage = (item) => {
event.stopPropagation()
ElMessageBox.confirm( ElMessageBox.confirm(
'此操作将会删除任务和图片,继续操作码?', '此操作将会删除任务和图片,继续操作码?',
'删除提示', '删除提示',
@ -420,7 +406,7 @@ const removeImage = (event, item) => {
type: 'warning', type: 'warning',
} }
).then(() => { ).then(() => {
httpGet("/api/dall/remove", {id: item.id, user_id: item.user}).then(() => { httpGet("/api/dall/remove", {id: item.id}).then(() => {
ElMessage.success("任务删除成功") ElMessage.success("任务删除成功")
page.value = 0 page.value = 0
isOver.value = false isOver.value = false
@ -437,18 +423,16 @@ const previewImg = (item) => {
} }
// //
const publishImage = (event, item, action) => { const publishImage = (item, action) => {
event.stopPropagation()
let text = "图片发布" let text = "图片发布"
if (action === false) { if (action === false) {
text = "取消发布" text = "取消发布"
} }
httpGet("/api/dall/publish", {id: item.id, action: action,user_id:item.user_id}).then(() => { httpGet("/api/dall/publish", {id: item.id, action: action}).then(() => {
ElMessage.success(text + "成功") ElMessage.success(text + "成功")
item.publish = action item.publish = action
page.value = 0 page.value = 0
isOver.value = false isOver.value = false
fetchFinishJobs()
}).catch(e => { }).catch(e => {
ElMessage.error(text + "失败:" + e.message) ElMessage.error(text + "失败:" + e.message)
}) })

View File

@ -17,7 +17,7 @@
effect="light" effect="light"
content="部署文档" content="部署文档"
placement="bottom"> placement="bottom">
<a href="https://docs.geekai.me/install/" class="link-button" target="_blank"> <a :href="docsURL" class="link-button" target="_blank">
<i class="iconfont icon-book"></i> <i class="iconfont icon-book"></i>
</a> </a>
</el-tooltip> </el-tooltip>
@ -138,7 +138,7 @@ import {onMounted, ref, watch} from "vue";
import {httpGet} from "@/utils/http"; import {httpGet} from "@/utils/http";
import {ElMessage} from "element-plus"; import {ElMessage} from "element-plus";
import {UserFilled} from "@element-plus/icons-vue"; import {UserFilled} from "@element-plus/icons-vue";
import {checkSession} from "@/action/session"; import {checkSession, getLicenseInfo, getSystemInfo} from "@/store/cache";
import {removeUserToken} from "@/store/session"; import {removeUserToken} from "@/store/session";
import LoginDialog from "@/components/LoginDialog.vue"; import LoginDialog from "@/components/LoginDialog.vue";
import {useSharedStore} from "@/store/sharedata"; import {useSharedStore} from "@/store/sharedata";
@ -185,7 +185,7 @@ const changeNav = (item) => {
} }
onMounted(() => { onMounted(() => {
httpGet("/api/config/get?key=system").then(res => { getSystemInfo().then(res => {
logo.value = res.data.logo logo.value = res.data.logo
title.value = res.data.title title.value = res.data.title
}).catch(e => { }).catch(e => {
@ -204,7 +204,7 @@ onMounted(() => {
ElMessage.error("获取系统菜单失败:" + e.message) ElMessage.error("获取系统菜单失败:" + e.message)
}) })
httpGet("/api/config/license").then(res => { getLicenseInfo().then(res => {
license.value = res.data license.value = res.data
}).catch(e => { }).catch(e => {
license.value = {de_copy: false} license.value = {de_copy: false}

View File

@ -487,7 +487,7 @@
</div> </div>
</template> </template>
</el-image> </el-image>
<el-image v-else-if="slotProp.item['err_msg'] !== ''"> <el-image v-else-if="slotProp.item.progress === 101">
<template #error> <template #error>
<div class="image-slot"> <div class="image-slot">
<div class="err-msg-container"> <div class="err-msg-container">
@ -608,7 +608,7 @@ import Compressor from "compressorjs";
import {httpGet, httpPost} from "@/utils/http"; import {httpGet, httpPost} from "@/utils/http";
import {ElMessage, ElMessageBox, ElNotification} from "element-plus"; import {ElMessage, ElMessageBox, ElNotification} from "element-plus";
import Clipboard from "clipboard"; import Clipboard from "clipboard";
import {checkSession} from "@/action/session"; import {checkSession, getSystemInfo} from "@/store/cache";
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import {getSessionId} from "@/store/session"; import {getSessionId} from "@/store/session";
import {copyObj, removeArrayItem} from "@/utils/libs"; import {copyObj, removeArrayItem} from "@/utils/libs";
@ -802,7 +802,7 @@ const initData = () => {
const mjPower = ref(1) const mjPower = ref(1)
const mjActionPower = ref(1) const mjActionPower = ref(1)
httpGet("/api/config/get?key=system").then(res => { getSystemInfo().then(res => {
mjPower.value = res.data["mj_power"] mjPower.value = res.data["mj_power"]
mjActionPower.value = res.data["mj_action_power"] mjActionPower.value = res.data["mj_action_power"]
}).catch(e => { }).catch(e => {

View File

@ -313,23 +313,43 @@
:isOver="isOver" :isOver="isOver"
@scrollReachBottom="fetchFinishJobs()"> @scrollReachBottom="fetchFinishJobs()">
<template #default="slotProp"> <template #default="slotProp">
<div class="job-item animate" @click="showTask(slotProp.item)"> <div class="job-item animate">
<el-image v-if="slotProp.item.progress === 101">
<template #error>
<div class="image-slot">
<div class="err-msg-container">
<div class="title">任务失败</div>
<div class="opt">
<el-popover title="错误详情" trigger="click" :width="250" :content="slotProp.item['err_msg']" placement="top">
<template #reference>
<el-button type="info">详情</el-button>
</template>
</el-popover>
<el-button type="danger" @click="removeImage(slotProp.item)">删除</el-button>
</div>
</div>
</div>
</template>
</el-image>
<div v-else>
<el-image <el-image
:src="slotProp.item['img_thumb']" :src="slotProp.item['img_thumb']"
@click="showTask(slotProp.item)"
fit="cover" fit="cover"
loading="lazy"/> loading="lazy"/>
<div class="remove"> <div class="remove">
<el-button type="danger" :icon="Delete" @click="removeImage($event,slotProp.item)" circle/> <el-button type="danger" :icon="Delete" @click="removeImage(slotProp.item)" circle/>
<el-button type="warning" v-if="slotProp.item.publish" <el-button type="warning" v-if="slotProp.item.publish"
@click="publishImage($event,slotProp.item, false)" @click="publishImage(slotProp.item, false)"
circle> circle>
<i class="iconfont icon-cancel-share"></i> <i class="iconfont icon-cancel-share"></i>
</el-button> </el-button>
<el-button type="success" v-else @click="publishImage($event,slotProp.item, true)" circle> <el-button type="success" v-else @click="publishImage(slotProp.item, true)" circle>
<i class="iconfont icon-share-bold"></i> <i class="iconfont icon-share-bold"></i>
</el-button> </el-button>
</div> </div>
</div> </div>
</div>
</template> </template>
<template #footer> <template #footer>
@ -466,12 +486,12 @@
</template> </template>
<script setup> <script setup>
import {onMounted, onUnmounted, ref} from "vue" import {nextTick, onMounted, onUnmounted, ref} from "vue"
import {Delete, DocumentCopy, InfoFilled, Orange, Picture} from "@element-plus/icons-vue"; import {Delete, DocumentCopy, InfoFilled, Orange, Picture} from "@element-plus/icons-vue";
import {httpGet, httpPost} from "@/utils/http"; import {httpGet, httpPost} from "@/utils/http";
import {ElMessage, ElMessageBox, ElNotification} from "element-plus"; import {ElMessage, ElMessageBox, ElNotification} from "element-plus";
import Clipboard from "clipboard"; import Clipboard from "clipboard";
import {checkSession} from "@/action/session"; import {checkSession, getSystemInfo} from "@/store/cache";
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import {getSessionId} from "@/store/session"; import {getSessionId} from "@/store/session";
import {useSharedStore} from "@/store/sharedata"; import {useSharedStore} from "@/store/sharedata";
@ -540,25 +560,9 @@ const connect = () => {
} }
} }
//
const sendHeartbeat = () => {
clearTimeout(heartbeatHandle.value)
new Promise((resolve, reject) => {
if (socket.value !== null) {
socket.value.send(JSON.stringify({type: "heartbeat", content: "ping"}))
}
resolve("success")
}).then(() => {
heartbeatHandle.value = setTimeout(() => sendHeartbeat(), 5000)
});
}
const _socket = new WebSocket(host + `/api/sd/client?user_id=${userId.value}`); const _socket = new WebSocket(host + `/api/sd/client?user_id=${userId.value}`);
_socket.addEventListener('open', () => { _socket.addEventListener('open', () => {
socket.value = _socket; socket.value = _socket;
//
sendHeartbeat()
}); });
_socket.addEventListener('message', event => { _socket.addEventListener('message', event => {
@ -567,12 +571,12 @@ const connect = () => {
reader.readAsText(event.data, "UTF-8") reader.readAsText(event.data, "UTF-8")
reader.onload = () => { reader.onload = () => {
const message = String(reader.result) const message = String(reader.result)
if (message === "FINISH") { if (message === "FINISH" || message === "FAIL") {
page.value = 0 page.value = 0
isOver.value = false isOver.value = false
fetchFinishJobs() fetchFinishJobs()
} }
fetchRunningJobs() nextTick(() => fetchRunningJobs())
} }
} }
}); });
@ -596,7 +600,7 @@ onMounted(() => {
ElMessage.error('复制失败!'); ElMessage.error('复制失败!');
}) })
httpGet("/api/config/get?key=system").then(res => { getSystemInfo().then(res => {
sdPower.value = res.data.sd_power sdPower.value = res.data.sd_power
params.value.neg_prompt = res.data.sd_neg_prompt params.value.neg_prompt = res.data.sd_neg_prompt
}).catch(e => { }).catch(e => {
@ -633,22 +637,7 @@ const fetchRunningJobs = () => {
// //
httpGet(`/api/sd/jobs?finish=0`).then(res => { httpGet(`/api/sd/jobs?finish=0`).then(res => {
const jobs = res.data runningJobs.value = res.data
const _jobs = []
for (let i = 0; i < jobs.length; i++) {
if (jobs[i].progress === -1) {
ElNotification({
title: '任务执行失败',
dangerouslyUseHTMLString: true,
message: `任务ID${jobs[i]['task_id']}<br />原因:${jobs[i]['err_msg']}`,
type: 'error',
})
power.value += sdPower.value
continue
}
_jobs.push(jobs[i])
}
runningJobs.value = _jobs
}).catch(e => { }).catch(e => {
ElMessage.error("获取任务失败:" + e.message) ElMessage.error("获取任务失败:" + e.message)
}) })
@ -699,7 +688,7 @@ const generate = () => {
return return
} }
if (params.value.seed === '') { if (!params.value.seed) {
params.value.seed = -1 params.value.seed = -1
} }
params.value.session_id = getSessionId() params.value.session_id = getSessionId()
@ -721,8 +710,7 @@ const copyParams = (row) => {
showTaskDialog.value = false showTaskDialog.value = false
} }
const removeImage = (event, item) => { const removeImage = (item) => {
event.stopPropagation()
ElMessageBox.confirm( ElMessageBox.confirm(
'此操作将会删除任务和图片,继续操作码?', '此操作将会删除任务和图片,继续操作码?',
'删除提示', '删除提示',
@ -732,7 +720,7 @@ const removeImage = (event, item) => {
type: 'warning', type: 'warning',
} }
).then(() => { ).then(() => {
httpGet("/api/sd/remove", {id: item.id, user_id: item.user}).then(() => { httpGet("/api/sd/remove", {id: item.id}).then(() => {
ElMessage.success("任务删除成功") ElMessage.success("任务删除成功")
page.value = 0 page.value = 0
isOver.value = false isOver.value = false
@ -745,13 +733,12 @@ const removeImage = (event, item) => {
} }
// //
const publishImage = (event, item, action) => { const publishImage = (item, action) => {
event.stopPropagation()
let text = "图片发布" let text = "图片发布"
if (action === false) { if (action === false) {
text = "取消发布" text = "取消发布"
} }
httpGet("/api/sd/publish", {id: item.id, action: action, user_id: item.user}).then(() => { httpGet("/api/sd/publish", {id: item.id, action: action}).then(() => {
ElMessage.success(text + "成功") ElMessage.success(text + "成功")
item.publish = action item.publish = action
page.value = 0 page.value = 0

View File

@ -62,7 +62,7 @@ import FooterBar from "@/components/FooterBar.vue";
import {httpGet} from "@/utils/http"; import {httpGet} from "@/utils/http";
import {ElMessage} from "element-plus"; import {ElMessage} from "element-plus";
import {isMobile} from "@/utils/libs"; import {isMobile} from "@/utils/libs";
import {checkSession} from "@/action/session"; import {checkSession, getLicenseInfo, getSystemInfo} from "@/store/cache";
const router = useRouter() const router = useRouter()
@ -131,7 +131,7 @@ const color = btnColors.value[Math.floor(Math.random() * btnColors.value.length)
const theme = ref({bgColor: "#ffffff", btnBgColor: color.bgColor, btnTextColor: color.textColor, textColor: "#ffffff", imageBg:true}) const theme = ref({bgColor: "#ffffff", btnBgColor: color.bgColor, btnTextColor: color.textColor, textColor: "#ffffff", imageBg:true})
onMounted(() => { onMounted(() => {
httpGet("/api/config/get?key=system").then(res => { getSystemInfo().then(res => {
title.value = res.data.title title.value = res.data.title
logo.value = res.data.logo logo.value = res.data.logo
if (res.data.index_bg_url === 'color') { if (res.data.index_bg_url === 'color') {
@ -155,7 +155,7 @@ onMounted(() => {
ElMessage.error("获取系统配置失败:" + e.message) ElMessage.error("获取系统配置失败:" + e.message)
}) })
httpGet("/api/config/license").then(res => { getLicenseInfo().then(res => {
license.value = res.data license.value = res.data
}).catch(e => { }).catch(e => {
license.value = {de_copy: false} license.value = {de_copy: false}

View File

@ -93,7 +93,7 @@ import {httpGet} from "@/utils/http";
import {ElMessage} from "element-plus"; import {ElMessage} from "element-plus";
import Clipboard from "clipboard"; import Clipboard from "clipboard";
import InviteList from "@/components/InviteList.vue"; import InviteList from "@/components/InviteList.vue";
import {checkSession} from "@/action/session"; import {checkSession, getSystemInfo} from "@/store/cache";
import {useSharedStore} from "@/store/sharedata"; import {useSharedStore} from "@/store/sharedata";
const inviteURL = ref("") const inviteURL = ref("")
@ -141,7 +141,7 @@ const initData = () => {
ElMessage.error("获取邀请码失败:" + e.message) ElMessage.error("获取邀请码失败:" + e.message)
}) })
httpGet("/api/config/get?key=system").then(res => { getSystemInfo().then(res => {
invitePower.value = res.data["invite_power"] invitePower.value = res.data["invite_power"]
}).catch(e => { }).catch(e => {
ElMessage.error("获取系统配置失败:" + e.message) ElMessage.error("获取系统配置失败:" + e.message)

View File

@ -69,7 +69,7 @@ import {httpGet, httpPost} from "@/utils/http";
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import FooterBar from "@/components/FooterBar.vue"; import FooterBar from "@/components/FooterBar.vue";
import {isMobile} from "@/utils/libs"; import {isMobile} from "@/utils/libs";
import {checkSession} from "@/action/session"; import {checkSession, getLicenseInfo, getSystemInfo} from "@/store/cache";
import {setUserToken} from "@/store/session"; import {setUserToken} from "@/store/session";
import ResetPass from "@/components/ResetPass.vue"; import ResetPass from "@/components/ResetPass.vue";
import {showMessageError} from "@/utils/dialog"; import {showMessageError} from "@/utils/dialog";
@ -85,14 +85,14 @@ const wechatLoginURL = ref('')
onMounted(() => { onMounted(() => {
// //
httpGet("/api/config/get?key=system").then(res => { getSystemInfo().then(res => {
logo.value = res.data.logo logo.value = res.data.logo
title.value = res.data.title title.value = res.data.title
}).catch(e => { }).catch(e => {
showMessageError("获取系统配置失败:" + e.message) showMessageError("获取系统配置失败:" + e.message)
}) })
httpGet("/api/config/license").then(res => { getLicenseInfo().then(res => {
licenseConfig.value = res.data licenseConfig.value = res.data
}).catch(e => { }).catch(e => {
showMessageError("获取 License 配置:" + e.message) showMessageError("获取 License 配置:" + e.message)

View File

@ -94,10 +94,10 @@
</template> </template>
<script setup> <script setup>
import {nextTick, onMounted, onUnmounted, ref} from 'vue'; import {nextTick, onUnmounted, ref} from 'vue';
import {Markmap} from 'markmap-view'; import {Markmap} from 'markmap-view';
import {Transformer} from 'markmap-lib'; import {Transformer} from 'markmap-lib';
import {checkSession} from "@/action/session"; import {checkSession, getSystemInfo} from "@/store/cache";
import {httpGet} from "@/utils/http"; import {httpGet} from "@/utils/http";
import {ElMessage} from "element-plus"; import {ElMessage} from "element-plus";
import {Download} from "@element-plus/icons-vue"; import {Download} from "@element-plus/icons-vue";
@ -126,21 +126,11 @@ const models = ref([])
const modelID = ref(0) const modelID = ref(0)
const loading = ref(false) const loading = ref(false)
httpGet("/api/config/get?key=system").then(res => { getSystemInfo().then(res => {
title.value = res.data.title??process.env.VUE_APP_TITLE text.value = res.data['mark_map_text']
text.value = `# ${title.value}
- 完整的开源系统前端应用和后台管理系统皆可开箱即用
- 基于 Websocket 实现完美的打字机体验
- 内置了各种预训练好的角色应用,轻松满足你的各种聊天和应用需求
- 支持 OPenAIAzure文心一言讯飞星火清华 ChatGLM等多个大语言模型
- 支持 MidJourney / Stable Diffusion AI 绘画集成开箱即用
- 支持使用个人微信二维码作为充值收费的支付渠道无需企业支付通道
- 已集成支付宝支付功能微信支付支持多种会员套餐和点卡购买功能
- 集成插件 API 功能可结合大语言模型的 function 功能开发各种强大的插件
`
content.value = text.value content.value = text.value
initData() initData()
nextTick(() => {
try { try {
markMap.value = Markmap.create(svgRef.value) markMap.value = Markmap.create(svgRef.value)
const {el} = Toolbar.create(markMap.value); const {el} = Toolbar.create(markMap.value);
@ -149,6 +139,7 @@ httpGet("/api/config/get?key=system").then(res => {
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} }
})
}).catch(e => { }).catch(e => {
ElMessage.error("获取系统配置失败:" + e.message) ElMessage.error("获取系统配置失败:" + e.message)
}) })
@ -181,6 +172,10 @@ const update = () => {
} }
const processContent = (text) => { const processContent = (text) => {
if (!text) {
return text
}
const arr = [] const arr = []
const lines = text.split("\n") const lines = text.split("\n")
for (let line of lines) { for (let line of lines) {

View File

@ -3,7 +3,7 @@
<div class="member custom-scroll"> <div class="member custom-scroll">
<div class="inner"> <div class="inner">
<div class="user-profile"> <div class="user-profile">
<user-profile/> <user-profile :key="profileKey"/>
<el-row class="user-opt" :gutter="20"> <el-row class="user-opt" :gutter="20">
<el-col :span="12"> <el-col :span="12">
@ -12,11 +12,8 @@
<el-col :span="12"> <el-col :span="12">
<el-button type="primary" @click="showBindMobileDialog = true">更改账号</el-button> <el-button type="primary" @click="showBindMobileDialog = true">更改账号</el-button>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="24">
<el-button type="primary" v-if="enableReward" @click="showRewardDialog = true">加入众筹</el-button> <el-button type="success" @click="showRedeemVerifyDialog = true">兑换码核销
</el-col>
<el-col :span="12">
<el-button type="primary" v-if="enableReward" @click="showRewardVerifyDialog = true">众筹核销
</el-button> </el-button>
</el-col> </el-col>
@ -99,24 +96,7 @@
<bind-mobile v-if="isLogin" :show="showBindMobileDialog" :username="user.username" <bind-mobile v-if="isLogin" :show="showBindMobileDialog" :username="user.username"
@hide="showBindMobileDialog = false"/> @hide="showBindMobileDialog = false"/>
<reward-verify v-if="isLogin" :show="showRewardVerifyDialog" @hide="showRewardVerifyDialog = false"/> <redeem-verify v-if="isLogin" :show="showRedeemVerifyDialog" @hide="redeemCallback"/>
<el-dialog
v-model="showRewardDialog"
:show-close="true"
width="400px"
title="参与众筹"
>
<el-alert type="info" :closable="false">
<div style="font-size: 14px">您好目前每单位算力众筹价格为 <strong style="color: #f56c6c">{{ powerPrice }}
</strong>
由于本人没有开通微信支付付款后请凭借转账单号,点击众筹核销按钮手动核销
</div>
</el-alert>
<div style="text-align: center;padding-top: 10px;">
<el-image v-if="enableReward" :src="rewardImg"/>
</div>
</el-dialog>
<el-dialog <el-dialog
v-model="showPayDialog" v-model="showPayDialog"
@ -160,11 +140,11 @@ import {ElMessage} from "element-plus";
import {httpGet, httpPost} from "@/utils/http"; import {httpGet, httpPost} from "@/utils/http";
import ItemList from "@/components/ItemList.vue"; import ItemList from "@/components/ItemList.vue";
import {InfoFilled, SuccessFilled} from "@element-plus/icons-vue"; import {InfoFilled, SuccessFilled} from "@element-plus/icons-vue";
import {checkSession} from "@/action/session"; import {checkSession, getSystemInfo} from "@/store/cache";
import UserProfile from "@/components/UserProfile.vue"; import UserProfile from "@/components/UserProfile.vue";
import PasswordDialog from "@/components/PasswordDialog.vue"; import PasswordDialog from "@/components/PasswordDialog.vue";
import BindMobile from "@/components/ResetAccount.vue"; import BindMobile from "@/components/ResetAccount.vue";
import RewardVerify from "@/components/RewardVerify.vue"; import RedeemVerify from "@/components/RedeemVerify.vue";
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import {removeUserToken} from "@/store/session"; import {removeUserToken} from "@/store/session";
import UserOrder from "@/components/UserOrder.vue"; import UserOrder from "@/components/UserOrder.vue";
@ -179,8 +159,7 @@ const rewardImg = ref('/images/reward.png')
const qrcode = ref("") const qrcode = ref("")
const showPasswordDialog = ref(false); const showPasswordDialog = ref(false);
const showBindMobileDialog = ref(false); const showBindMobileDialog = ref(false);
const showRewardDialog = ref(false); const showRedeemVerifyDialog = ref(false);
const showRewardVerifyDialog = ref(false);
const text = ref("") const text = ref("")
const user = ref(null) const user = ref(null)
const isLogin = ref(false) const isLogin = ref(false)
@ -200,6 +179,7 @@ const payName = ref("支付宝")
const curPay = ref("alipay") // const curPay = ref("alipay") //
const vipInfoText = ref("") const vipInfoText = ref("")
const store = useSharedStore() const store = useSharedStore()
const profileKey = ref(0)
onMounted(() => { onMounted(() => {
@ -216,7 +196,7 @@ onMounted(() => {
ElMessage.error("获取产品套餐失败:" + e.message) ElMessage.error("获取产品套餐失败:" + e.message)
}) })
httpGet("/api/config/get?key=system").then(res => { getSystemInfo().then(res => {
rewardImg.value = res.data['reward_img'] rewardImg.value = res.data['reward_img']
enableReward.value = res.data['enabled_reward'] enableReward.value = res.data['enabled_reward']
orderPayInfoText.value = res.data['order_pay_info_text'] orderPayInfoText.value = res.data['order_pay_info_text']
@ -368,8 +348,11 @@ const closeOrder = () => {
activeOrderNo.value = '' activeOrderNo.value = ''
} }
const loginSuccess = () => { const redeemCallback = (success) => {
location.reload() showRedeemVerifyDialog.value = false
if (success) {
profileKey.value += 1
}
} }
</script> </script>

View File

@ -71,7 +71,7 @@ import {Search} from "@element-plus/icons-vue";
import Clipboard from "clipboard"; import Clipboard from "clipboard";
import {ElMessage} from "element-plus"; import {ElMessage} from "element-plus";
import {httpPost} from "@/utils/http"; import {httpPost} from "@/utils/http";
import {checkSession} from "@/action/session"; import {checkSession} from "@/store/cache";
const items = ref([]) const items = ref([])
const total = ref(0) const total = ref(0)

View File

@ -181,6 +181,7 @@ import {arrayContains} from "@/utils/libs";
import {setUserToken} from "@/store/session"; import {setUserToken} from "@/store/session";
import {validateEmail, validateMobile} from "@/utils/validate"; import {validateEmail, validateMobile} from "@/utils/validate";
import {showMessageError, showMessageOK} from "@/utils/dialog"; import {showMessageError, showMessageOK} from "@/utils/dialog";
import {getLicenseInfo, getSystemInfo} from "@/store/cache";
const router = useRouter(); const router = useRouter();
const title = ref(''); const title = ref('');
@ -201,7 +202,7 @@ const activeName = ref("mobile")
const wxImg = ref("/images/wx.png") const wxImg = ref("/images/wx.png")
const licenseConfig = ref({}) const licenseConfig = ref({})
httpGet("/api/config/get?key=system").then(res => { getSystemInfo().then(res => {
if (res.data) { if (res.data) {
title.value = res.data.title title.value = res.data.title
logo.value = res.data.logo logo.value = res.data.logo
@ -226,7 +227,7 @@ httpGet("/api/config/get?key=system").then(res => {
ElMessage.error("获取系统配置失败:" + e.message) ElMessage.error("获取系统配置失败:" + e.message)
}) })
httpGet("/api/config/license").then(res => { getLicenseInfo().then(res => {
licenseConfig.value = res.data licenseConfig.value = res.data
}).catch(e => { }).catch(e => {
showMessageError("获取 License 配置:" + e.message) showMessageError("获取 License 配置:" + e.message)

View File

@ -277,7 +277,7 @@ import {compact} from "lodash";
import {httpGet, httpPost} from "@/utils/http"; import {httpGet, httpPost} from "@/utils/http";
import {showMessageError, showMessageOK} from "@/utils/dialog"; import {showMessageError, showMessageOK} from "@/utils/dialog";
import Generating from "@/components/ui/Generating.vue"; import Generating from "@/components/ui/Generating.vue";
import {checkSession} from "@/action/session"; import {checkSession} from "@/store/cache";
import {ElMessage, ElMessageBox} from "element-plus"; import {ElMessage, ElMessageBox} from "element-plus";
import {formatTime} from "@/utils/libs"; import {formatTime} from "@/utils/libs";
import Clipboard from "clipboard"; import Clipboard from "clipboard";
@ -381,9 +381,9 @@ onMounted(() => {
checkSession().then(user => { checkSession().then(user => {
userId.value = user.id userId.value = user.id
fetchData(1)
connect() connect()
}) })
fetchData(1)
}) })
onUnmounted(() => { onUnmounted(() => {
@ -410,6 +410,8 @@ const fetchData = (_page) => {
list.value = items list.value = items
noData.value = list.value.length === 0 noData.value = list.value.length === 0
}).catch(e => { }).catch(e => {
loading.value = false
noData.value = true
showMessageError("获取作品列表失败:"+e.message) showMessageError("获取作品列表失败:"+e.message)
}) })
} }

View File

@ -1,8 +1,7 @@
<template> <template>
<div> <div>
<textarea v-model="value"/> {{data}}
</div> </div>
<svg ref="svgRef"/>
</template> </template>
<script setup> <script setup>
@ -10,34 +9,16 @@ import {ref, onMounted, onUpdated} from 'vue';
import {Markmap} from 'markmap-view'; import {Markmap} from 'markmap-view';
import {loadJS, loadCSS} from 'markmap-common'; import {loadJS, loadCSS} from 'markmap-common';
import {Transformer} from 'markmap-lib'; import {Transformer} from 'markmap-lib';
import {httpPost} from "@/utils/http";
const transformer = new Transformer(); const data=ref("")
const {scripts, styles} = transformer.getAssets(); httpPost("/api/test/sse",{
loadCSS(styles); "message":"你是什么模型",
loadJS(scripts); "user_id":123
}).then(res=>{
const initValue = `# markmap // const source = new EventSource("http://localhost:5678/api/test/sse");
// source.onmessage = function(event) {
- beautiful // console.log(event.data)
- useful // };
- easy })
- interactive
`;
const value = ref(initValue);
const svgRef = ref(null);
let mm;
const update = () => {
const {root} = transformer.transform(value.value);
mm.setData(root);
mm.fit();
};
onMounted(() => {
mm = Markmap.create(svgRef.value);
update();
});
onUpdated(update);
</script> </script>

View File

@ -1,173 +0,0 @@
<template>
<el-form label-width="150px" label-position="right" class="draw-config">
<el-tabs type="border-card">
<el-tab-pane label="MJ-PLUS">
<div v-if="mjPlusConfigs">
<div class="config-item" v-for="(v,k) in mjPlusConfigs">
<el-form-item label="是否启用">
<el-switch v-model="v['Enabled']"/>
</el-form-item>
<el-form-item label="API 地址">
<el-input v-model="v['ApiURL']" placeholder="API 地址"/>
</el-form-item>
<el-form-item label="API 令牌">
<el-input v-model="v['ApiKey']" placeholder="API KEY"/>
</el-form-item>
<el-form-item label="绘画模式">
<el-select v-model="v['Mode']" placeholder="请选择模式">
<el-option v-for="item in mjModels" :value="item.value" :label="item.name" :key="item.value">{{
item.name
}}
</el-option>
</el-select>
</el-form-item>
<el-button class="remove" type="danger" :icon="Delete" circle @click="removeItem(mjPlusConfigs,k)"/>
</div>
</div>
<el-empty v-else></el-empty>
<el-row style="justify-content: center; padding: 10px">
<el-button round @click="addConfig(mjPlusConfigs)">
<el-icon><Plus /></el-icon>
<span>新增配置</span>
</el-button>
</el-row>
</el-tab-pane>
<el-tab-pane label="MJ-PROXY">
<div v-if="mjProxyConfigs">
<div class="config-item" v-for="(v,k) in mjProxyConfigs">
<el-form-item label="是否启用">
<el-switch v-model="v['Enabled']"/>
</el-form-item>
<el-form-item label="API 地址">
<el-input v-model="v['ApiURL']" placeholder="API 地址"/>
</el-form-item>
<el-form-item label="API 令牌">
<el-input v-model="v['ApiKey']" placeholder="API KEY"/>
</el-form-item>
<el-button class="remove" type="danger" :icon="Delete" circle @click="removeItem(mjProxyConfigs,k)"/>
</div>
</div>
<el-empty v-else />
<el-row style="justify-content: center; padding: 10px">
<el-button round @click="addConfig(mjProxyConfigs)">
<el-icon>
<Plus/>
</el-icon>
<span>新增配置</span>
</el-button>
</el-row>
</el-tab-pane>
<el-tab-pane label="Stable-Diffusion">
<div v-if="sdConfigs">
<div class="config-item" v-for="(v,k) in sdConfigs">
<el-form-item label="是否启用">
<el-switch v-model="v['Enabled']"/>
</el-form-item>
<el-form-item label="API 地址">
<el-input v-model="v['ApiURL']" placeholder="API 地址"/>
</el-form-item>
<el-form-item label="API 令牌">
<el-input v-model="v['ApiKey']" placeholder="API KEY"/>
</el-form-item>
<el-form-item label="模型">
<el-input v-model="v['Model']" placeholder="绘画模型"/>
</el-form-item>
<el-button class="remove" type="danger" :icon="Delete" circle @click="removeItem(sdConfigs,k)"/>
</div>
</div>
<el-empty v-else/>
<el-row style="justify-content: center; padding: 10px">
<el-button round @click="addConfig(sdConfigs)">
<el-icon>
<Plus/>
</el-icon>
<span>新增配置</span>
</el-button>
</el-row>
</el-tab-pane>
</el-tabs>
<div style="padding: 10px;">
<el-form-item>
<el-button type="primary" @click="saveConfig()">保存</el-button>
</el-form-item>
</div>
</el-form>
</template>
<script setup>
import {ref} from "vue";
import {httpGet, httpPost} from "@/utils/http";
import {ElMessage} from "element-plus";
import {Delete, Plus} from "@element-plus/icons-vue";
//
const sdConfigs = ref([])
const mjPlusConfigs = ref([])
const mjProxyConfigs = ref([])
const mjModels = ref([
{name: "慢速Relax", value: "relax"},
{name: "快速Fast", value: "fast"},
{name: "急速Turbo", value: "turbo"},
])
httpGet("/api/admin/config/get/app").then(res => {
sdConfigs.value = res.data.sd
mjPlusConfigs.value = res.data.mj_plus
mjProxyConfigs.value = res.data.mj_proxy
}).catch(e =>{
ElMessage.error("获取配置失败:"+e.message)
})
const addConfig = (configs) => {
configs.push({
Enabled: true,
ApiKey: '',
ApiURL: '',
Mode: 'fast'
})
}
const saveConfig = () => {
httpPost('/api/admin/config/update/draw', {
'sd': sdConfigs.value,
'mj_plus': mjPlusConfigs.value,
'mj_proxy': mjProxyConfigs.value
}).then(() => {
ElMessage.success("配置更新成功")
}).catch(e => {
ElMessage.error("操作失败:" + e.message)
})
}
const removeItem = (arr, k) => {
arr.splice(k, 1)
}
</script>
<style lang="stylus" scoped>
.draw-config {
.config-item {
position relative
padding 15px 10px 10px 10px
border 1px solid var(--el-border-color)
border-radius 10px
margin-bottom 10px
.remove {
position absolute
right 15px
top 15px
}
}
}
</style>

View File

@ -139,6 +139,7 @@ const title = ref("")
const types = ref([ const types = ref([
{label: "对话", value:"chat"}, {label: "对话", value:"chat"},
{label: "Midjourney", value:"mj"}, {label: "Midjourney", value:"mj"},
{label: "Stable-Diffusion", value:"sd"},
{label: "DALL-E", value:"dalle"}, {label: "DALL-E", value:"dalle"},
{label: "Suno文生歌", value:"suno"}, {label: "Suno文生歌", value:"suno"},
{label: "Luma视频", value:"luma"}, {label: "Luma视频", value:"luma"},

View File

@ -157,11 +157,7 @@
<div v-for="item in messages" :key="item.id"> <div v-for="item in messages" :key="item.id">
<chat-prompt <chat-prompt
v-if="item.type==='prompt'" v-if="item.type==='prompt'"
:icon="item.icon" :data="item"/>
:created-at="dateFormat(item['created_at'])"
:tokens="item['tokens']"
:model="item.model"
:content="item.content"/>
<chat-reply v-else-if="item.type==='reply'" <chat-reply v-else-if="item.type==='reply'"
:read-only="true" :read-only="true"
:data="item"/> :data="item"/>
@ -288,33 +284,11 @@ const removeMessage = function (row) {
}) })
} }
const mathjaxPlugin = require('markdown-it-mathjax3')
const md = require('markdown-it')({
breaks: true,
html: true,
linkify: true,
typographer: true,
highlight: function (str, lang) {
if (lang && hl.getLanguage(lang)) {
//
const preCode = hl.highlight(lang, str, true).value
// pre
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code></pre>`
}
//
const preCode = md.utils.escapeHtml(str)
// pre
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code></pre>`
}
});
md.use(mathjaxPlugin)
const showContentDialog = ref(false) const showContentDialog = ref(false)
const dialogContent = ref("") const dialogContent = ref("")
const showContent = (content) => { const showContent = (content) => {
showContentDialog.value = true showContentDialog.value = true
dialogContent.value = md.render(processContent(content)) dialogContent.value = processContent(content)
} }
const showChatItemDialog = ref(false) const showChatItemDialog = ref(false)
@ -325,8 +299,6 @@ const showMessages = (row) => {
httpGet('/api/admin/chat/history?chat_id=' + row.chat_id).then(res => { httpGet('/api/admin/chat/history?chat_id=' + row.chat_id).then(res => {
const data = res.data const data = res.data
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
data[i].orgContent = data[i].content;
data[i].content = md.render(processContent(data[i].content))
messages.value.push(data[i]); messages.value.push(data[i]);
} }
}).catch(e => { }).catch(e => {

View File

@ -10,7 +10,12 @@
<el-row> <el-row>
<el-table :data="items" :row-key="row => row.id" table-layout="auto"> <el-table :data="items" :row-key="row => row.id" table-layout="auto">
<el-table-column prop="name" label="模型名称"/> <el-table-column type="selection" width="38"></el-table-column>
<el-table-column prop="name" label="模型名称">
<template #default="scope">
<span class="sort" :data-id="scope.row.id">{{ scope.row.name }}</span>
</template>
</el-table-column>
<el-table-column prop="value" label="模型值"> <el-table-column prop="value" label="模型值">
<template #default="scope"> <template #default="scope">
<span>{{ scope.row.value }}</span> <span>{{ scope.row.value }}</span>
@ -174,6 +179,7 @@ import {dateFormat, removeArrayItem, substr} from "@/utils/libs";
import {DocumentCopy, InfoFilled, Plus,Search} from "@element-plus/icons-vue"; import {DocumentCopy, InfoFilled, Plus,Search} from "@element-plus/icons-vue";
import {Sortable} from "sortablejs"; import {Sortable} from "sortablejs";
import ClipboardJS from "clipboard"; import ClipboardJS from "clipboard";
import Default from "md-editor-v3";
// //
const items = ref([]) const items = ref([])

View File

@ -23,7 +23,7 @@ import AdminHeader from "@/components/admin/AdminHeader.vue";
import AdminSidebar from "@/components/admin/AdminSidebar.vue"; import AdminSidebar from "@/components/admin/AdminSidebar.vue";
import AdminTags from "@/components/admin/AdminTags.vue"; import AdminTags from "@/components/admin/AdminTags.vue";
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import {checkAdminSession} from "@/action/session"; import {checkAdminSession} from "@/store/cache";
import {ref} from "vue"; import {ref} from "vue";
import {getAdminTheme, setAdminTheme} from "@/store/system"; import {getAdminTheme, setAdminTheme} from "@/store/system";

View File

@ -53,7 +53,7 @@ import {ElMessage} from "element-plus";
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import FooterBar from "@/components/FooterBar.vue"; import FooterBar from "@/components/FooterBar.vue";
import {setAdminToken} from "@/store/session"; import {setAdminToken} from "@/store/session";
import {checkAdminSession} from "@/action/session"; import {checkAdminSession, getSystemInfo} from "@/store/cache";
const router = useRouter(); const router = useRouter();
const title = ref('Geek-AI Console'); const title = ref('Geek-AI Console');
@ -67,7 +67,7 @@ checkAdminSession().then(() => {
}) })
// //
httpGet('/api/config/get?key=system').then(res => { getSystemInfo().then(res => {
title.value = res.data.admin_title title.value = res.data.admin_title
logo.value = res.data.logo logo.value = res.data.logo
}).catch(e => { }).catch(e => {

View File

@ -20,6 +20,7 @@
style="margin: 0 10px;width: 200px; position: relative;top:3px;" style="margin: 0 10px;width: 200px; position: relative;top:3px;"
/> />
<el-button type="primary" :icon="Search" @click="fetchData">搜索</el-button> <el-button type="primary" :icon="Search" @click="fetchData">搜索</el-button>
<el-button type="danger" :icon="Delete" @click="clearOrders">清空未支付订单</el-button>
</div> </div>
<el-row> <el-row>
@ -76,9 +77,9 @@
<script setup> <script setup>
import {onMounted, ref} from "vue"; import {onMounted, ref} from "vue";
import {httpGet, httpPost} from "@/utils/http"; import {httpGet, httpPost} from "@/utils/http";
import {ElMessage} from "element-plus"; import {ElMessage, ElMessageBox} from "element-plus";
import {dateFormat, removeArrayItem} from "@/utils/libs"; import {dateFormat, removeArrayItem} from "@/utils/libs";
import {Search} from "@element-plus/icons-vue"; import {Delete, Search} from "@element-plus/icons-vue";
// //
const items = ref([]) const items = ref([])
@ -123,6 +124,24 @@ const remove = function (row) {
ElMessage.error("删除失败:" + e.message) ElMessage.error("删除失败:" + e.message)
}) })
} }
const clearOrders = () => {
ElMessageBox.confirm(
'此操作将会删除所有未支付订单,继续操作吗?',
'删除提示',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
).then(() => {
httpGet("/api/admin/order/clear").then(() => {
ElMessage.success("订单删除成功")
page.value = 0
fetchData()
})
})
}
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>

Some files were not shown because too many files have changed in this diff Show More