Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f057ca9d1 | ||
|
|
4a56621ec3 | ||
|
|
a398e7a550 | ||
|
|
96816c12ca | ||
|
|
9984926f69 | ||
|
|
a2a6081027 | ||
|
|
5a10ed37a7 | ||
|
|
1a9dd9de0b | ||
|
|
0dae5bef71 | ||
|
|
b4413ed726 | ||
|
|
5e1fe88b8b | ||
|
|
91ed41b536 | ||
|
|
024c0032eb | ||
|
|
4a9f7e3bce | ||
|
|
cf4dcc34ec | ||
|
|
4d612c15af | ||
|
|
8aec87cc02 | ||
|
|
442e411cde | ||
|
|
acec0194de | ||
|
|
8557f5b94a | ||
|
|
babef8baae | ||
|
|
efd4ab46f5 | ||
|
|
ae8239e5de | ||
|
|
f0994ba457 | ||
|
|
dae91ed243 | ||
|
|
de42a428e6 | ||
|
|
63c7041e1f | ||
|
|
b1263ddc69 | ||
|
|
7e50e17aaf | ||
|
|
a7265c4251 | ||
|
|
6f39f639bd | ||
|
|
a7db123437 | ||
|
|
241c714a8b | ||
|
|
67ac3cfe32 | ||
|
|
c926e0afcc | ||
|
|
5bc07e6d57 | ||
|
|
c3666a9a71 | ||
|
|
23b5ffa97d | ||
|
|
a2c7a75705 | ||
|
|
d68f2ef12c | ||
|
|
67d30353f0 | ||
|
|
4813163eac | ||
|
|
5c5210625e | ||
|
|
a4a1eec30b | ||
|
|
d35164506a |
17
CHANGELOG.md
@@ -1,4 +1,21 @@
|
||||
# 更新日志
|
||||
## v3.2.1
|
||||
* 功能优化:切换角色和模型的时候自动创建新的对话
|
||||
* Bug修复:修复文件上传失败No such file bug
|
||||
* 功能新增:MidJourney 绘画页面新增提示词翻译功能,新增多个绘画参数
|
||||
* Bug修复:[PC端对话在刷新后异常](https://github.com/yangjian102621/chatgpt-plus/issues/59)
|
||||
* 功能新增:增加 arm64 架构打包脚本
|
||||
* 功能新增:支持 dall-e3 绘图的 API 地址自定义配置
|
||||
* 功能新增:新增虎皮椒支付功能接入,支持微信和支付宝通道
|
||||
|
||||
## v3.2.0
|
||||
* 功能新增:新增邀请注册功能
|
||||
* 功能优化:增加中间件自动对HTTP请求的参数去掉首尾空格
|
||||
* 功能优化:增加中间件自动为大图片生成缩略图
|
||||
* 功能优化:MidJourney 页面图片加载优化,实现图片预览懒加载
|
||||
* 功能新增:新增 DALL-E-3 绘画支持,并作为对话页面默认绘画插件
|
||||
* Bug修复:修复阿里云 OSS 域名设置不起做用的bug
|
||||
* Bug修复:修复MidJourney绘图失败后重复添加到队列的问题
|
||||
|
||||
## v3.1.9
|
||||
* 功能新增:增加讯飞星火大模型 v3.0 支持
|
||||
|
||||
15
README.md
@@ -13,6 +13,11 @@ ChatGLM,讯飞星火,文心一言等多个平台的大语言模型。集成了
|
||||
* 集成插件 API 功能,可结合大语言模型的 function 功能开发各种强大的插件,已内置实现了微博热搜,今日头条,今日早报和 AI
|
||||
绘画函数插件。
|
||||
|
||||
## 关于部署镜像申明
|
||||
|
||||
由于目前部署人数越来越多,本人的阿里云镜像仓库流量不够支撑大家使用了。所以从 v3.2.0 版本开始,一键部署脚本和部署镜像将只提供给 **[付费技术交流群]** 内用户使用。
|
||||
代码依旧是全部开源的,大家可自行编译打包镜像。
|
||||
|
||||
## 功能截图
|
||||
|
||||
### PC 端聊天界面
|
||||
@@ -90,16 +95,16 @@ ChatGLM,讯飞星火,文心一言等多个平台的大语言模型。集成了
|
||||
|
||||
## 项目文档
|
||||
|
||||
请参考 [ChatGPT-Plus 文档](https://ai.r9it.com/docs/)。
|
||||
详细的部署和开发文档请参考 [**ChatGPT-Plus 文档**](https://ai.r9it.com/docs/)。
|
||||
|
||||
加微信进入微信讨论群可获取 **一键部署脚本(添加好友时请注明来自Github!!!)。**
|
||||
|
||||

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

|
||||
|
||||
#### 特此声明:由于个人时间有限,不接受在微信或者微信群给开发者提 Bug,有问题或者优化建议请提交 Issue 和 PR。非常感谢您的配合!
|
||||
|
||||
### Commit 类型
|
||||
|
||||
19
api/Makefile
@@ -1,19 +1,14 @@
|
||||
SHELL=/usr/bin/env bash
|
||||
NAME := chatgpt-plus
|
||||
all: window linux darwin
|
||||
all: amd64 arm64
|
||||
|
||||
amd64:
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/$(NAME)-linux main.go
|
||||
.PHONY: amd64
|
||||
|
||||
window:
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o bin/$(NAME)-amd64.exe main.go
|
||||
.PHONY: window
|
||||
|
||||
linux:
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/$(NAME)-amd64-linux main.go
|
||||
.PHONY: linux
|
||||
|
||||
darwin:
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o bin/$(NAME)-amd64-darwin main.go
|
||||
.PHONY: darwin
|
||||
arm64:
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 GOARM=7 go build -o bin/$(NAME)-linux main.go
|
||||
.PHONY: arm64
|
||||
|
||||
clean:
|
||||
rm -rf bin/$(NAME)-*
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"chatplus/core/types"
|
||||
"chatplus/service/fun"
|
||||
"chatplus/store/model"
|
||||
@@ -11,9 +12,14 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/nfnt/resize"
|
||||
"gorm.io/gorm"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -57,7 +63,9 @@ func (s *AppServer) Init(debug bool, client *redis.Client) {
|
||||
logger.Info("Enabled debug mode")
|
||||
}
|
||||
s.Engine.Use(corsMiddleware())
|
||||
s.Engine.Use(staticResourceMiddleware())
|
||||
s.Engine.Use(authorizeMiddleware(s, client))
|
||||
s.Engine.Use(parameterHandlerMiddleware())
|
||||
s.Engine.Use(errorHandler)
|
||||
// 添加静态资源访问
|
||||
s.Engine.Static("/static", s.Config.StaticDir)
|
||||
@@ -146,7 +154,9 @@ func authorizeMiddleware(s *AppServer, client *redis.Client) gin.HandlerFunc {
|
||||
c.Request.URL.Path == "/api/chat/detail" ||
|
||||
c.Request.URL.Path == "/api/role/list" ||
|
||||
c.Request.URL.Path == "/api/mj/jobs" ||
|
||||
c.Request.URL.Path == "/api/invite/hits" ||
|
||||
c.Request.URL.Path == "/api/sd/jobs" ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/test/") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/sms/") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/captcha/") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/payment/") ||
|
||||
@@ -159,9 +169,7 @@ func authorizeMiddleware(s *AppServer, client *redis.Client) gin.HandlerFunc {
|
||||
var tokenString string
|
||||
if strings.Contains(c.Request.URL.Path, "/api/admin/") { // 后台管理 API
|
||||
tokenString = c.GetHeader(types.AdminAuthHeader)
|
||||
} else if c.Request.URL.Path == "/api/chat/new" ||
|
||||
c.Request.URL.Path == "/api/mj/client" ||
|
||||
c.Request.URL.Path == "/api/sd/client" {
|
||||
} else if c.Request.URL.Path == "/api/chat/new" {
|
||||
tokenString = c.Query("token")
|
||||
} else {
|
||||
tokenString = c.GetHeader(types.UserAuthHeader)
|
||||
@@ -209,3 +217,128 @@ func authorizeMiddleware(s *AppServer, client *redis.Client) gin.HandlerFunc {
|
||||
c.Set(types.LoginUserID, claims["user_id"])
|
||||
}
|
||||
}
|
||||
|
||||
// 统一参数处理
|
||||
func parameterHandlerMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// GET 参数处理
|
||||
params := c.Request.URL.Query()
|
||||
for key, values := range params {
|
||||
for i, value := range values {
|
||||
params[key][i] = strings.TrimSpace(value)
|
||||
}
|
||||
}
|
||||
// update get parameters
|
||||
c.Request.URL.RawQuery = params.Encode()
|
||||
// skip file upload requests
|
||||
contentType := c.Request.Header.Get("Content-Type")
|
||||
if strings.Contains(contentType, "multipart/form-data") {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(contentType, "application/json") {
|
||||
// process POST JSON request body
|
||||
bodyBytes, err := io.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 还原请求体
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
// 将请求体解析为 JSON
|
||||
var jsonData map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&jsonData); err != nil {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 对 JSON 数据中的字符串值去除两端空格
|
||||
trimJSONStrings(jsonData)
|
||||
// 更新请求体
|
||||
c.Request.Body = io.NopCloser(bytes.NewBufferString(utils.JsonEncode(jsonData)))
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// 递归对 JSON 数据中的字符串值去除两端空格
|
||||
func trimJSONStrings(data interface{}) {
|
||||
switch v := data.(type) {
|
||||
case map[string]interface{}:
|
||||
for key, value := range v {
|
||||
switch valueType := value.(type) {
|
||||
case string:
|
||||
v[key] = strings.TrimSpace(valueType)
|
||||
case map[string]interface{}, []interface{}:
|
||||
trimJSONStrings(value)
|
||||
}
|
||||
}
|
||||
case []interface{}:
|
||||
for i, value := range v {
|
||||
switch valueType := value.(type) {
|
||||
case string:
|
||||
v[i] = strings.TrimSpace(valueType)
|
||||
case map[string]interface{}, []interface{}:
|
||||
trimJSONStrings(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 静态资源中间件
|
||||
func staticResourceMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
|
||||
url := c.Request.URL.String()
|
||||
// 拦截生成缩略图请求
|
||||
if strings.HasPrefix(url, "/static/") && strings.Contains(url, "?imageView2") {
|
||||
r := strings.SplitAfter(url, "imageView2")
|
||||
size := strings.Split(r[1], "/")
|
||||
if len(size) != 8 {
|
||||
c.String(http.StatusNotFound, "invalid thumb args")
|
||||
return
|
||||
}
|
||||
with := utils.IntValue(size[3], 0)
|
||||
height := utils.IntValue(size[5], 0)
|
||||
quality := utils.IntValue(size[7], 75)
|
||||
|
||||
// 打开图片文件
|
||||
filePath := strings.TrimLeft(c.Request.URL.Path, "/")
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
c.String(http.StatusNotFound, "Image not found")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 解码图片
|
||||
img, _, err := image.Decode(file)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "Error decoding image")
|
||||
return
|
||||
}
|
||||
|
||||
var newImg image.Image
|
||||
if height == 0 || with == 0 {
|
||||
// 固定宽度,高度自适应
|
||||
newImg = resize.Resize(uint(with), uint(height), img, resize.Lanczos3)
|
||||
} else {
|
||||
// 生成缩略图
|
||||
newImg = resize.Thumbnail(uint(with), uint(height), img, resize.Lanczos3)
|
||||
}
|
||||
var buffer bytes.Buffer
|
||||
err = jpeg.Encode(&buffer, newImg, &jpeg.Options{Quality: quality})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// 直接输出图像数据流
|
||||
c.Data(http.StatusOK, "image/jpeg", buffer.Bytes())
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,6 @@ func NewDefaultConfig() *types.AppConfig {
|
||||
BasePath: "./static/upload",
|
||||
},
|
||||
},
|
||||
MjConfig: types.MidJourneyConfig{Enabled: false},
|
||||
SdConfig: types.StableDiffusionConfig{Enabled: false, Txt2ImgJsonPath: "res/text2img.json"},
|
||||
WeChatBot: false,
|
||||
AlipayConfig: types.AlipayConfig{Enabled: false, SandBox: false},
|
||||
}
|
||||
|
||||
@@ -16,14 +16,15 @@ type AppConfig struct {
|
||||
Redis RedisConfig // redis 连接信息
|
||||
ApiConfig ChatPlusApiConfig // ChatPlus API authorization configs
|
||||
AesEncryptKey string
|
||||
SmsConfig AliYunSmsConfig // AliYun send message service config
|
||||
OSS OSSConfig // OSS config
|
||||
MjConfig MidJourneyConfig // mj 绘画配置
|
||||
WeChatBot bool // 是否启用微信机器人
|
||||
SdConfig StableDiffusionConfig // sd 绘画配置
|
||||
SmsConfig AliYunSmsConfig // AliYun send message service config
|
||||
OSS OSSConfig // OSS config
|
||||
MjConfigs []MidJourneyConfig // mj AI draw service pool
|
||||
WeChatBot bool // 是否启用微信机器人
|
||||
SdConfigs []StableDiffusionConfig // sd AI draw service pool
|
||||
|
||||
XXLConfig XXLConfig
|
||||
AlipayConfig AlipayConfig
|
||||
XXLConfig XXLConfig
|
||||
AlipayConfig AlipayConfig
|
||||
HuPiPayConfig HuPiPayConfig
|
||||
}
|
||||
|
||||
type ChatPlusApiConfig struct {
|
||||
@@ -40,10 +41,6 @@ type MidJourneyConfig struct {
|
||||
ChanelId string // Chanel ID
|
||||
}
|
||||
|
||||
type WeChatConfig struct {
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
type StableDiffusionConfig struct {
|
||||
Enabled bool
|
||||
ApiURL string
|
||||
@@ -61,7 +58,7 @@ type AliYunSmsConfig struct {
|
||||
}
|
||||
|
||||
type AlipayConfig struct {
|
||||
Enabled bool // 是否启用该服务
|
||||
Enabled bool // 是否启用该支付通道
|
||||
SandBox bool // 是否沙盒环境
|
||||
AppId string // 应用 ID
|
||||
UserId string // 支付宝用户 ID
|
||||
@@ -72,6 +69,15 @@ type AlipayConfig struct {
|
||||
NotifyURL string // 异步通知回调
|
||||
}
|
||||
|
||||
type HuPiPayConfig struct { //虎皮椒第四方支付配置
|
||||
Enabled bool // 是否启用该支付通道
|
||||
Name string // 支付名称,如:wechat/alipay
|
||||
AppId string // App ID
|
||||
AppSecret string // app 密钥
|
||||
NotifyURL string // 异步通知回调
|
||||
PayURL string // 支付网关
|
||||
}
|
||||
|
||||
type XXLConfig struct { // XXL 任务调度配置
|
||||
Enabled bool
|
||||
ServerAddr string
|
||||
@@ -106,9 +112,11 @@ type ChatConfig struct {
|
||||
Baidu ModelAPIConfig `json:"baidu"`
|
||||
XunFei ModelAPIConfig `json:"xun_fei"`
|
||||
|
||||
EnableContext bool `json:"enable_context"` // 是否开启聊天上下文
|
||||
EnableHistory bool `json:"enable_history"` // 是否允许保存聊天记录
|
||||
ContextDeep int `json:"context_deep"` // 上下文深度
|
||||
EnableContext bool `json:"enable_context"` // 是否开启聊天上下文
|
||||
EnableHistory bool `json:"enable_history"` // 是否允许保存聊天记录
|
||||
ContextDeep int `json:"context_deep"` // 上下文深度
|
||||
DallApiURL string `json:"dall_api_url"` // dall-e3 绘图 API 地址
|
||||
DallImgNum int `json:"dall_img_num"` // dall-e3 出图数量
|
||||
}
|
||||
|
||||
type Platform string
|
||||
@@ -124,6 +132,11 @@ type UserChatConfig struct {
|
||||
ApiKeys map[Platform]string `json:"api_keys"`
|
||||
}
|
||||
|
||||
type InviteReward struct {
|
||||
ChatCalls int `json:"chat_calls"`
|
||||
ImgCalls int `json:"img_calls"`
|
||||
}
|
||||
|
||||
type ModelAPIConfig struct {
|
||||
ApiURL string `json:"api_url,omitempty"`
|
||||
Temperature float32 `json:"temperature"`
|
||||
@@ -135,12 +148,11 @@ type SystemConfig struct {
|
||||
Title string `json:"title"`
|
||||
AdminTitle string `json:"admin_title"`
|
||||
Models []string `json:"models"`
|
||||
UserInitCalls int `json:"user_init_calls"` // 新用户注册默认总送多少次调用
|
||||
InitImgCalls int `json:"init_img_calls"`
|
||||
VipMonthCalls int `json:"vip_month_calls"` // 会员每个赠送的调用次数
|
||||
EnabledRegister bool `json:"enabled_register"`
|
||||
EnabledMsg bool `json:"enabled_msg"` // 启用短信验证码服务
|
||||
EnabledDraw bool `json:"enabled_draw"` // 启动 AI 绘画功能
|
||||
InitChatCalls int `json:"init_chat_calls"` // 新用户注册赠送对话次数
|
||||
InitImgCalls int `json:"init_img_calls"` // 新用户注册赠送绘图次数
|
||||
VipMonthCalls int `json:"vip_month_calls"` // 会员每个赠送的调用次数
|
||||
EnabledRegister bool `json:"enabled_register"` // 是否启用注册功能,关闭注册功能之后将无法注册
|
||||
EnabledMsg bool `json:"enabled_msg"` // 是否启用短信验证码服务
|
||||
RewardImg string `json:"reward_img"` // 众筹收款二维码地址
|
||||
EnabledFunction bool `json:"enabled_function"` // 启用 API 函数功能
|
||||
EnabledReward bool `json:"enabled_reward"` // 启用众筹功能
|
||||
@@ -148,4 +160,7 @@ type SystemConfig struct {
|
||||
OrderPayTimeout int `json:"order_pay_timeout"` //订单支付超时时间
|
||||
DefaultModels []string `json:"default_models"` // 默认开通的 AI 模型
|
||||
OrderPayInfoText string `json:"order_pay_info_text"` // 订单支付页面说明文字
|
||||
InviteChatCalls int `json:"invite_chat_calls"` // 邀请用户注册奖励对话次数
|
||||
InviteImgCalls int `json:"invite_img_calls"` // 邀请用户注册奖励绘图次数
|
||||
ForceInvite bool `json:"force_invite"` // 是否强制必须使用邀请码才能注册
|
||||
}
|
||||
|
||||
@@ -23,10 +23,10 @@ type Property struct {
|
||||
}
|
||||
|
||||
const (
|
||||
FuncZaoBao = "zao_bao" // 每日早报
|
||||
FuncHeadLine = "headline" // 今日头条
|
||||
FuncWeibo = "weibo_hot" // 微博热搜
|
||||
FuncMidJourney = "mid_journey" // MJ 绘画
|
||||
FuncZaoBao = "zao_bao" // 每日早报
|
||||
FuncHeadLine = "headline" // 今日头条
|
||||
FuncWeibo = "weibo_hot" // 微博热搜
|
||||
FuncImage = "draw_image" // AI 绘画
|
||||
)
|
||||
|
||||
var InnerFunctions = []Function{
|
||||
@@ -76,14 +76,14 @@ var InnerFunctions = []Function{
|
||||
},
|
||||
|
||||
{
|
||||
Name: FuncMidJourney,
|
||||
Description: "AI 绘画工具,使用 MJ MidJourney API 进行 AI 绘画",
|
||||
Name: FuncImage,
|
||||
Description: "AI 绘画工具,根据输入的绘图描述用 AI 工具进行绘画",
|
||||
Parameters: Parameters{
|
||||
Type: "object",
|
||||
Properties: map[string]Property{
|
||||
"prompt": {
|
||||
Type: "string",
|
||||
Description: "提示词,如果该参数中有中文的话,则需要翻译成英文。提示词中的参数作为提示的一部分,不要删除",
|
||||
Description: "提示词,如果该参数中有中文的话,则需要翻译成英文。",
|
||||
},
|
||||
},
|
||||
Required: []string{},
|
||||
|
||||
@@ -12,6 +12,7 @@ type MiniOssConfig struct {
|
||||
AccessKey string
|
||||
AccessSecret string
|
||||
Bucket string
|
||||
SubDir string
|
||||
UseSSL bool
|
||||
Domain string
|
||||
}
|
||||
@@ -21,6 +22,7 @@ type QiNiuOssConfig struct {
|
||||
AccessKey string
|
||||
AccessSecret string
|
||||
Bucket string
|
||||
SubDir string
|
||||
Domain string
|
||||
}
|
||||
|
||||
@@ -29,6 +31,7 @@ type AliYunOssConfig struct {
|
||||
AccessKey string
|
||||
AccessSecret string
|
||||
Bucket string
|
||||
SubDir string
|
||||
Domain string
|
||||
}
|
||||
|
||||
|
||||
@@ -11,28 +11,15 @@ const (
|
||||
TaskImage = TaskType("image")
|
||||
TaskUpscale = TaskType("upscale")
|
||||
TaskVariation = TaskType("variation")
|
||||
TaskTxt2Img = TaskType("text2img")
|
||||
)
|
||||
|
||||
// TaskSrc 任务来源
|
||||
type TaskSrc string
|
||||
|
||||
const (
|
||||
TaskSrcChat = TaskSrc("chat") // 来自聊天页面
|
||||
TaskSrcImg = TaskSrc("img") // 专业绘画页面
|
||||
)
|
||||
|
||||
// MjTask MidJourney 任务
|
||||
type MjTask struct {
|
||||
Id int `json:"id"`
|
||||
SessionId string `json:"session_id"`
|
||||
Src TaskSrc `json:"src"`
|
||||
Type TaskType `json:"type"`
|
||||
UserId int `json:"user_id"`
|
||||
Prompt string `json:"prompt,omitempty"`
|
||||
ChatId string `json:"chat_id,omitempty"`
|
||||
RoleId int `json:"role_id,omitempty"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Index int `json:"index,omitempty"`
|
||||
MessageId string `json:"message_id,omitempty"`
|
||||
MessageHash string `json:"message_hash,omitempty"`
|
||||
@@ -42,7 +29,6 @@ type MjTask struct {
|
||||
type SdTask struct {
|
||||
Id int `json:"id"` // job 数据库ID
|
||||
SessionId string `json:"session_id"`
|
||||
Src TaskSrc `json:"src"`
|
||||
Type TaskType `json:"type"`
|
||||
UserId int `json:"user_id"`
|
||||
Prompt string `json:"prompt,omitempty"`
|
||||
|
||||
@@ -27,6 +27,7 @@ func (h *ApiKeyHandler) Save(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
Platform string `json:"platform"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
@@ -40,7 +41,8 @@ func (h *ApiKeyHandler) Save(c *gin.Context) {
|
||||
}
|
||||
apiKey.Platform = data.Platform
|
||||
apiKey.Value = data.Value
|
||||
res := h.db.Debug().Save(&apiKey)
|
||||
apiKey.Type = data.Type
|
||||
res := h.db.Save(&apiKey)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
|
||||
@@ -44,6 +44,8 @@ func (h *OrderHandler) List(c *gin.Context) {
|
||||
end := utils.Str2stamp(data.PayTime[1] + " 00:00:00")
|
||||
session = session.Where("pay_time >= ? AND pay_time <= ?", start, end)
|
||||
}
|
||||
session = session.Where("status = ?", types.OrderPaidSuccess)
|
||||
|
||||
var total int64
|
||||
session.Model(&model.Order{}).Count(&total)
|
||||
var items []model.Order
|
||||
|
||||
@@ -49,3 +49,11 @@ func (h *BaseHandler) GetUserKey(c *gin.Context) string {
|
||||
}
|
||||
return fmt.Sprintf("users/%v", userId)
|
||||
}
|
||||
|
||||
func (h *BaseHandler) GetLoginUserId(c *gin.Context) uint {
|
||||
userId, ok := c.Get(types.LoginUserID)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
return uint(utils.IntValue(utils.InterfaceToString(userId), 0))
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"gorm.io/gorm"
|
||||
"html/template"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -127,12 +128,12 @@ func (h *ChatHandler) sendAzureMessage(
|
||||
logger.Debugf("函数名称: %s, 函数参数:%s", functionName, params)
|
||||
|
||||
// for creating image, check if the user's img_calls > 0
|
||||
if functionName == types.FuncMidJourney && userVo.ImgCalls <= 0 {
|
||||
if functionName == types.FuncImage && userVo.ImgCalls <= 0 {
|
||||
utils.ReplyMessage(ws, "**当前用户剩余绘图次数已用尽,请扫描下面二维码联系管理员!**")
|
||||
utils.ReplyMessage(ws, ErrImg)
|
||||
} else {
|
||||
f := h.App.Functions[functionName]
|
||||
if functionName == types.FuncMidJourney {
|
||||
if functionName == types.FuncImage {
|
||||
params["user_id"] = userVo.Id
|
||||
params["role_id"] = role.Id
|
||||
params["chat_id"] = session.ChatId
|
||||
@@ -149,9 +150,8 @@ func (h *ChatHandler) sendAzureMessage(
|
||||
contents = append(contents, msg)
|
||||
} else {
|
||||
content := data
|
||||
if functionName == types.FuncMidJourney {
|
||||
content = fmt.Sprintf("绘画提示词:%s 已推送任务到 MidJourney 机器人,请耐心等待任务执行...", data)
|
||||
h.mjService.ChatClients.Put(session.SessionId, ws)
|
||||
if functionName == types.FuncImage {
|
||||
content = fmt.Sprintf("下面是根据您的描述创作的图片,他们描绘了 【%s】 的场景", params["prompt"])
|
||||
// update user's img_calls
|
||||
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
|
||||
}
|
||||
@@ -201,7 +201,7 @@ func (h *ChatHandler) sendAzureMessage(
|
||||
RoleId: role.Id,
|
||||
Type: types.PromptMsg,
|
||||
Icon: userVo.Avatar,
|
||||
Content: prompt,
|
||||
Content: template.HTMLEscapeString(prompt),
|
||||
Tokens: promptToken,
|
||||
UseContext: useContext,
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -156,7 +157,7 @@ func (h *ChatHandler) sendBaiduMessage(
|
||||
RoleId: role.Id,
|
||||
Type: types.PromptMsg,
|
||||
Icon: userVo.Avatar,
|
||||
Content: prompt,
|
||||
Content: template.HTMLEscapeString(prompt),
|
||||
Tokens: promptToken,
|
||||
UseContext: true,
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"chatplus/core/types"
|
||||
"chatplus/handler"
|
||||
logger2 "chatplus/logger"
|
||||
"chatplus/service/mj"
|
||||
"chatplus/store/model"
|
||||
"chatplus/store/vo"
|
||||
"chatplus/utils"
|
||||
@@ -32,16 +31,14 @@ var logger = logger2.GetLogger()
|
||||
|
||||
type ChatHandler struct {
|
||||
handler.BaseHandler
|
||||
db *gorm.DB
|
||||
redis *redis.Client
|
||||
mjService *mj.Service
|
||||
db *gorm.DB
|
||||
redis *redis.Client
|
||||
}
|
||||
|
||||
func NewChatHandler(app *core.AppServer, db *gorm.DB, redis *redis.Client, service *mj.Service) *ChatHandler {
|
||||
func NewChatHandler(app *core.AppServer, db *gorm.DB, redis *redis.Client) *ChatHandler {
|
||||
h := ChatHandler{
|
||||
db: db,
|
||||
redis: redis,
|
||||
mjService: service,
|
||||
db: db,
|
||||
redis: redis,
|
||||
}
|
||||
h.App = app
|
||||
return &h
|
||||
@@ -224,9 +221,6 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
|
||||
if h.App.SysConfig.EnabledFunction {
|
||||
var functions = make([]types.Function, 0)
|
||||
for _, f := range types.InnerFunctions {
|
||||
if !h.App.SysConfig.EnabledDraw && f.Name == types.FuncMidJourney {
|
||||
continue
|
||||
}
|
||||
functions = append(functions, f)
|
||||
}
|
||||
req.Functions = functions
|
||||
@@ -405,7 +399,7 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platf
|
||||
}
|
||||
if *apiKey == "" {
|
||||
var key model.ApiKey
|
||||
res := h.db.Where("platform = ?", platform).Order("last_used_at ASC").First(&key)
|
||||
res := h.db.Where("platform = ? AND type = ?", platform, "chat").Order("last_used_at ASC").First(&key)
|
||||
if res.Error != nil {
|
||||
return nil, errors.New("no available key, please import key")
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"html/template"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -135,7 +136,7 @@ func (h *ChatHandler) sendChatGLMMessage(
|
||||
RoleId: role.Id,
|
||||
Type: types.PromptMsg,
|
||||
Icon: userVo.Avatar,
|
||||
Content: prompt,
|
||||
Content: template.HTMLEscapeString(prompt),
|
||||
Tokens: promptToken,
|
||||
UseContext: true,
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"gorm.io/gorm"
|
||||
"html/template"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -126,16 +127,15 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
logger.Debugf("函数名称: %s, 函数参数:%s", functionName, params)
|
||||
|
||||
// for creating image, check if the user's img_calls > 0
|
||||
if functionName == types.FuncMidJourney && userVo.ImgCalls <= 0 {
|
||||
if functionName == types.FuncImage && userVo.ImgCalls <= 0 {
|
||||
utils.ReplyMessage(ws, "**当前用户剩余绘图次数已用尽,请扫描下面二维码联系管理员!**")
|
||||
utils.ReplyMessage(ws, ErrImg)
|
||||
} else {
|
||||
f := h.App.Functions[functionName]
|
||||
if functionName == types.FuncMidJourney {
|
||||
if functionName == types.FuncImage {
|
||||
params["user_id"] = userVo.Id
|
||||
params["role_id"] = role.Id
|
||||
params["chat_id"] = session.ChatId
|
||||
params["icon"] = "/images/avatar/mid_journey.png"
|
||||
params["session_id"] = session.SessionId
|
||||
}
|
||||
data, err := f.Invoke(params)
|
||||
@@ -148,9 +148,8 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
contents = append(contents, msg)
|
||||
} else {
|
||||
content := data
|
||||
if functionName == types.FuncMidJourney {
|
||||
content = fmt.Sprintf("绘画提示词:%s 已推送任务到 MidJourney 机器人,请耐心等待任务执行...", data)
|
||||
h.mjService.ChatClients.Put(session.SessionId, ws)
|
||||
if functionName == types.FuncImage {
|
||||
content = fmt.Sprintf("下面是根据您的描述创作的图片,他们描绘了 【%s】 的场景。%s", params["prompt"], data)
|
||||
// update user's img_calls
|
||||
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
|
||||
}
|
||||
@@ -200,7 +199,7 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
RoleId: role.Id,
|
||||
Type: types.PromptMsg,
|
||||
Icon: userVo.Avatar,
|
||||
Content: prompt,
|
||||
Content: template.HTMLEscapeString(prompt),
|
||||
Tokens: promptToken,
|
||||
UseContext: useContext,
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gorilla/websocket"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -69,7 +70,7 @@ func (h *ChatHandler) sendXunFeiMessage(
|
||||
var apiKey = userVo.ChatConfig.ApiKeys[session.Model.Platform]
|
||||
if apiKey == "" {
|
||||
var key model.ApiKey
|
||||
res := h.db.Where("platform = ?", session.Model.Platform).Order("last_used_at ASC").First(&key)
|
||||
res := h.db.Where("platform = ? AND type = ?", session.Model.Platform, "chat").Order("last_used_at ASC").First(&key)
|
||||
if res.Error != nil {
|
||||
utils.ReplyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
|
||||
return nil
|
||||
@@ -198,7 +199,7 @@ func (h *ChatHandler) sendXunFeiMessage(
|
||||
RoleId: role.Id,
|
||||
Type: types.PromptMsg,
|
||||
Icon: userVo.Avatar,
|
||||
Content: prompt,
|
||||
Content: template.HTMLEscapeString(prompt),
|
||||
Tokens: promptToken,
|
||||
UseContext: true,
|
||||
}
|
||||
|
||||
96
api/handler/invite_handler.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"chatplus/core"
|
||||
"chatplus/core/types"
|
||||
"chatplus/store/model"
|
||||
"chatplus/store/vo"
|
||||
"chatplus/utils"
|
||||
"chatplus/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// InviteHandler 用户邀请
|
||||
type InviteHandler struct {
|
||||
BaseHandler
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewInviteHandler(app *core.AppServer, db *gorm.DB) *InviteHandler {
|
||||
h := InviteHandler{db: db}
|
||||
h.App = app
|
||||
return &h
|
||||
}
|
||||
|
||||
// Code 获取当前用户邀请码
|
||||
func (h *InviteHandler) Code(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
var inviteCode model.InviteCode
|
||||
res := h.db.Where("user_id = ?", userId).First(&inviteCode)
|
||||
// 如果邀请码不存在,则创建一个
|
||||
if res.Error != nil {
|
||||
code := strings.ToUpper(utils.RandString(8))
|
||||
for {
|
||||
res = h.db.Where("code = ?", code).First(&inviteCode)
|
||||
if res.Error != nil { // 不存在相同的邀请码则退出
|
||||
break
|
||||
}
|
||||
}
|
||||
inviteCode.UserId = userId
|
||||
inviteCode.Code = code
|
||||
h.db.Create(&inviteCode)
|
||||
}
|
||||
|
||||
var codeVo vo.InviteCode
|
||||
err := utils.CopyObject(inviteCode, &codeVo)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "拷贝对象失败")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, codeVo)
|
||||
}
|
||||
|
||||
// List Log 用户邀请记录
|
||||
func (h *InviteHandler) List(c *gin.Context) {
|
||||
|
||||
var data struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
userId := h.GetLoginUserId(c)
|
||||
session := h.db.Session(&gorm.Session{}).Where("inviter_id = ?", userId)
|
||||
var total int64
|
||||
session.Model(&model.InviteLog{}).Count(&total)
|
||||
var items []model.InviteLog
|
||||
var list = make([]vo.InviteLog, 0)
|
||||
offset := (data.Page - 1) * data.PageSize
|
||||
res := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&items)
|
||||
if res.Error == nil {
|
||||
for _, item := range items {
|
||||
var v vo.InviteLog
|
||||
err := utils.CopyObject(item, &v)
|
||||
if err == nil {
|
||||
v.Id = item.Id
|
||||
v.CreatedAt = item.CreatedAt.Unix()
|
||||
list = append(list, v)
|
||||
} else {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, list))
|
||||
}
|
||||
|
||||
// Hits 访问邀请码
|
||||
func (h *InviteHandler) Hits(c *gin.Context) {
|
||||
code := c.Query("code")
|
||||
h.db.Model(&model.InviteCode{}).Where("code = ?", code).UpdateColumn("hits", gorm.Expr("hits + ?", 1))
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package handler
|
||||
import (
|
||||
"chatplus/core"
|
||||
"chatplus/core/types"
|
||||
"chatplus/service"
|
||||
"chatplus/service/mj"
|
||||
"chatplus/store/model"
|
||||
"chatplus/store/vo"
|
||||
@@ -11,50 +12,29 @@ import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/gorilla/websocket"
|
||||
"gorm.io/gorm"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type MidJourneyHandler struct {
|
||||
BaseHandler
|
||||
redis *redis.Client
|
||||
db *gorm.DB
|
||||
mjService *mj.Service
|
||||
pool *mj.ServicePool
|
||||
snowflake *service.Snowflake
|
||||
}
|
||||
|
||||
func NewMidJourneyHandler(
|
||||
app *core.AppServer,
|
||||
client *redis.Client,
|
||||
db *gorm.DB,
|
||||
mjService *mj.Service) *MidJourneyHandler {
|
||||
func NewMidJourneyHandler(app *core.AppServer, db *gorm.DB, snowflake *service.Snowflake, pool *mj.ServicePool) *MidJourneyHandler {
|
||||
h := MidJourneyHandler{
|
||||
redis: client,
|
||||
db: db,
|
||||
mjService: mjService,
|
||||
snowflake: snowflake,
|
||||
pool: pool,
|
||||
}
|
||||
h.App = app
|
||||
return &h
|
||||
}
|
||||
|
||||
// Client WebSocket 客户端,用于通知任务状态变更
|
||||
func (h *MidJourneyHandler) Client(c *gin.Context) {
|
||||
ws, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}).Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
sessionId := c.Query("session_id")
|
||||
client := types.NewWsClient(ws)
|
||||
h.mjService.Clients.Put(sessionId, client)
|
||||
logger.Infof("New websocket connected, IP: %s", c.ClientIP())
|
||||
}
|
||||
|
||||
func (h *MidJourneyHandler) checkLimits(c *gin.Context) bool {
|
||||
func (h *MidJourneyHandler) preCheck(c *gin.Context) bool {
|
||||
user, err := utils.GetLoginUser(c, h.db)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
@@ -66,20 +46,21 @@ func (h *MidJourneyHandler) checkLimits(c *gin.Context) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
if !h.pool.HasAvailableService() {
|
||||
resp.ERROR(c, "MidJourney 池子中没有没有可用的服务!")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
// Image 创建一个绘画任务
|
||||
func (h *MidJourneyHandler) Image(c *gin.Context) {
|
||||
if !h.App.Config.MjConfig.Enabled {
|
||||
resp.ERROR(c, "MidJourney service is disabled")
|
||||
return
|
||||
}
|
||||
|
||||
var data struct {
|
||||
SessionId string `json:"session_id"`
|
||||
Prompt string `json:"prompt"`
|
||||
NegPrompt string `json:"neg_prompt"`
|
||||
Rate string `json:"rate"`
|
||||
Model string `json:"model"`
|
||||
Chaos int `json:"chaos"`
|
||||
@@ -87,13 +68,15 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
|
||||
Seed int64 `json:"seed"`
|
||||
Stylize int `json:"stylize"`
|
||||
Img string `json:"img"`
|
||||
Tile bool `json:"tile"`
|
||||
Quality float32 `json:"quality"`
|
||||
Weight float32 `json:"weight"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
if !h.checkLimits(c) {
|
||||
if !h.preCheck(c) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -119,15 +102,31 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
|
||||
if data.Raw {
|
||||
prompt += " --style raw"
|
||||
}
|
||||
if data.Quality > 0 {
|
||||
prompt += fmt.Sprintf(" --q %.2f", data.Quality)
|
||||
}
|
||||
if data.NegPrompt != "" {
|
||||
prompt += fmt.Sprintf(" --no %s", data.NegPrompt)
|
||||
}
|
||||
if data.Tile {
|
||||
prompt += " --tile "
|
||||
}
|
||||
if data.Model != "" && !strings.Contains(prompt, "--v") && !strings.Contains(prompt, "--niji") {
|
||||
prompt += data.Model
|
||||
prompt += fmt.Sprintf(" %s", data.Model)
|
||||
}
|
||||
|
||||
idValue, _ := c.Get(types.LoginUserID)
|
||||
userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
|
||||
// generate task id
|
||||
taskId, err := h.snowflake.Next(true)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with generate task id: "+err.Error())
|
||||
return
|
||||
}
|
||||
job := model.MidJourneyJob{
|
||||
Type: types.TaskImage.String(),
|
||||
UserId: userId,
|
||||
TaskId: taskId,
|
||||
Progress: 0,
|
||||
Prompt: prompt,
|
||||
CreatedAt: time.Now(),
|
||||
@@ -137,24 +136,13 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
h.mjService.PushTask(types.MjTask{
|
||||
h.pool.PushTask(types.MjTask{
|
||||
Id: int(job.Id),
|
||||
SessionId: data.SessionId,
|
||||
Src: types.TaskSrcImg,
|
||||
Type: types.TaskImage,
|
||||
Prompt: prompt,
|
||||
Prompt: fmt.Sprintf("%s %s", taskId, prompt),
|
||||
UserId: userId,
|
||||
})
|
||||
|
||||
var jobVo vo.MidJourneyJob
|
||||
err := utils.CopyObject(job, &jobVo)
|
||||
if err == nil {
|
||||
// 推送任务到前端
|
||||
client := h.mjService.Clients.Get(data.SessionId)
|
||||
if client != nil {
|
||||
utils.ReplyChunkMessage(client, jobVo)
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
@@ -178,65 +166,23 @@ func (h *MidJourneyHandler) Upscale(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if !h.checkLimits(c) {
|
||||
if !h.preCheck(c) {
|
||||
return
|
||||
}
|
||||
|
||||
idValue, _ := c.Get(types.LoginUserID)
|
||||
jobId := 0
|
||||
userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
|
||||
src := types.TaskSrc(data.Src)
|
||||
if src == types.TaskSrcImg {
|
||||
job := model.MidJourneyJob{
|
||||
Type: types.TaskUpscale.String(),
|
||||
UserId: userId,
|
||||
Hash: data.MessageHash,
|
||||
Progress: 0,
|
||||
Prompt: data.Prompt,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if res := h.db.Create(&job); res.Error == nil {
|
||||
jobId = int(job.Id)
|
||||
} else {
|
||||
resp.ERROR(c, "添加任务失败:"+res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var jobVo vo.MidJourneyJob
|
||||
err := utils.CopyObject(job, &jobVo)
|
||||
if err == nil {
|
||||
// 推送任务到前端
|
||||
client := h.mjService.Clients.Get(data.SessionId)
|
||||
if client != nil {
|
||||
utils.ReplyChunkMessage(client, jobVo)
|
||||
}
|
||||
}
|
||||
}
|
||||
h.mjService.PushTask(types.MjTask{
|
||||
h.pool.PushTask(types.MjTask{
|
||||
Id: jobId,
|
||||
SessionId: data.SessionId,
|
||||
Src: src,
|
||||
Type: types.TaskUpscale,
|
||||
Prompt: data.Prompt,
|
||||
UserId: userId,
|
||||
RoleId: data.RoleId,
|
||||
Icon: data.Icon,
|
||||
ChatId: data.ChatId,
|
||||
Index: data.Index,
|
||||
MessageId: data.MessageId,
|
||||
MessageHash: data.MessageHash,
|
||||
})
|
||||
|
||||
if src == types.TaskSrcChat {
|
||||
wsClient := h.App.ChatClients.Get(data.SessionId)
|
||||
if wsClient != nil {
|
||||
content := fmt.Sprintf("**%s** 已推送 upscale 任务到 MidJourney 机器人,请耐心等待任务执行...", data.Prompt)
|
||||
utils.ReplyMessage(wsClient, content)
|
||||
if h.mjService.ChatClients.Get(data.SessionId) == nil {
|
||||
h.mjService.ChatClients.Put(data.SessionId, wsClient)
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
@@ -248,67 +194,23 @@ func (h *MidJourneyHandler) Variation(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if !h.checkLimits(c) {
|
||||
if !h.preCheck(c) {
|
||||
return
|
||||
}
|
||||
|
||||
idValue, _ := c.Get(types.LoginUserID)
|
||||
jobId := 0
|
||||
userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
|
||||
src := types.TaskSrc(data.Src)
|
||||
if src == types.TaskSrcImg {
|
||||
job := model.MidJourneyJob{
|
||||
Type: types.TaskVariation.String(),
|
||||
UserId: userId,
|
||||
ImgURL: "",
|
||||
Hash: data.MessageHash,
|
||||
Progress: 0,
|
||||
Prompt: data.Prompt,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if res := h.db.Create(&job); res.Error == nil {
|
||||
jobId = int(job.Id)
|
||||
} else {
|
||||
resp.ERROR(c, "添加任务失败:"+res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var jobVo vo.MidJourneyJob
|
||||
err := utils.CopyObject(job, &jobVo)
|
||||
if err == nil {
|
||||
// 推送任务到前端
|
||||
client := h.mjService.Clients.Get(data.SessionId)
|
||||
if client != nil {
|
||||
utils.ReplyChunkMessage(client, jobVo)
|
||||
}
|
||||
}
|
||||
}
|
||||
h.mjService.PushTask(types.MjTask{
|
||||
h.pool.PushTask(types.MjTask{
|
||||
Id: jobId,
|
||||
SessionId: data.SessionId,
|
||||
Src: src,
|
||||
Type: types.TaskVariation,
|
||||
Prompt: data.Prompt,
|
||||
UserId: userId,
|
||||
RoleId: data.RoleId,
|
||||
Icon: data.Icon,
|
||||
ChatId: data.ChatId,
|
||||
Index: data.Index,
|
||||
MessageId: data.MessageId,
|
||||
MessageHash: data.MessageHash,
|
||||
})
|
||||
|
||||
if src == types.TaskSrcChat {
|
||||
// 从聊天窗口发送的请求,记录客户端信息
|
||||
wsClient := h.mjService.ChatClients.Get(data.SessionId)
|
||||
if wsClient != nil {
|
||||
content := fmt.Sprintf("**%s** 已推送 variation 任务到 MidJourney 机器人,请耐心等待任务执行...", data.Prompt)
|
||||
utils.ReplyMessage(wsClient, content)
|
||||
if h.mjService.Clients.Get(data.SessionId) == nil {
|
||||
h.mjService.Clients.Put(data.SessionId, wsClient)
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
@@ -347,19 +249,27 @@ func (h *MidJourneyHandler) JobList(c *gin.Context) {
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if job.Progress == -1 {
|
||||
h.db.Delete(&model.MidJourneyJob{Id: job.Id})
|
||||
}
|
||||
|
||||
if item.Progress < 100 {
|
||||
// 30 分钟还没完成的任务直接删除
|
||||
if time.Now().Sub(item.CreatedAt) > time.Minute*30 {
|
||||
// 10 分钟还没完成的任务直接删除
|
||||
if time.Now().Sub(item.CreatedAt) > time.Minute*10 {
|
||||
h.db.Delete(&item)
|
||||
continue
|
||||
}
|
||||
if item.ImgURL != "" { // 正在运行中任务使用代理访问图片
|
||||
image, err := utils.DownloadImage(item.ImgURL, h.App.Config.ProxyURL)
|
||||
|
||||
// 正在运行中任务使用代理访问图片
|
||||
if item.ImgURL == "" && item.OrgURL != "" {
|
||||
image, err := utils.DownloadImage(item.OrgURL, h.App.Config.ProxyURL)
|
||||
if err == nil {
|
||||
job.ImgURL = "data:image/png;base64," + base64.StdEncoding.EncodeToString(image)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jobs = append(jobs, job)
|
||||
}
|
||||
resp.SUCCESS(c, jobs)
|
||||
|
||||
@@ -21,31 +21,37 @@ import (
|
||||
|
||||
const (
|
||||
PayWayAlipay = "支付宝"
|
||||
PayWayWechat = "微信支付"
|
||||
PayWayXunHu = "虎皮椒"
|
||||
)
|
||||
|
||||
// PaymentHandler 支付服务回调 handler
|
||||
type PaymentHandler struct {
|
||||
BaseHandler
|
||||
alipayService *payment.AlipayService
|
||||
snowflake *service.Snowflake
|
||||
db *gorm.DB
|
||||
fs embed.FS
|
||||
lock sync.Mutex
|
||||
alipayService *payment.AlipayService
|
||||
huPiPayService *payment.HuPiPayService
|
||||
snowflake *service.Snowflake
|
||||
db *gorm.DB
|
||||
fs embed.FS
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func NewPaymentHandler(server *core.AppServer, alipayService *payment.AlipayService, snowflake *service.Snowflake, db *gorm.DB, fs embed.FS) *PaymentHandler {
|
||||
h := PaymentHandler{lock: sync.Mutex{}}
|
||||
func NewPaymentHandler(server *core.AppServer, alipayService *payment.AlipayService, huPiPayService *payment.HuPiPayService, snowflake *service.Snowflake, db *gorm.DB, fs embed.FS) *PaymentHandler {
|
||||
h := PaymentHandler{
|
||||
alipayService: alipayService,
|
||||
huPiPayService: huPiPayService,
|
||||
snowflake: snowflake,
|
||||
fs: fs,
|
||||
db: db,
|
||||
lock: sync.Mutex{},
|
||||
}
|
||||
h.App = server
|
||||
h.alipayService = alipayService
|
||||
h.snowflake = snowflake
|
||||
h.db = db
|
||||
h.fs = fs
|
||||
return &h
|
||||
}
|
||||
|
||||
func (h *PaymentHandler) Alipay(c *gin.Context) {
|
||||
func (h *PaymentHandler) DoPay(c *gin.Context) {
|
||||
orderNo := h.GetTrim(c, "order_no")
|
||||
payWay := h.GetTrim(c, "pay_way")
|
||||
|
||||
if orderNo == "" {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
@@ -60,21 +66,61 @@ func (h *PaymentHandler) Alipay(c *gin.Context) {
|
||||
|
||||
// 更新扫码状态
|
||||
h.db.Model(&order).UpdateColumn("status", types.OrderScanned)
|
||||
// 生成支付链接
|
||||
notifyURL := h.App.Config.AlipayConfig.NotifyURL
|
||||
returnURL := "" // 关闭同步回跳
|
||||
amount := fmt.Sprintf("%.2f", order.Amount)
|
||||
if payWay == "alipay" { // 支付宝
|
||||
// 生成支付链接
|
||||
notifyURL := h.App.Config.AlipayConfig.NotifyURL
|
||||
returnURL := "" // 关闭同步回跳
|
||||
amount := fmt.Sprintf("%.2f", order.Amount)
|
||||
|
||||
uri, err := h.alipayService.PayUrlMobile(order.OrderNo, notifyURL, returnURL, amount, order.Subject)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with generate pay url: "+err.Error())
|
||||
uri, err := h.alipayService.PayUrlMobile(order.OrderNo, notifyURL, returnURL, amount, order.Subject)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with generate pay url: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(302, uri)
|
||||
return
|
||||
}
|
||||
} else if payWay == "hupi" { // 虎皮椒支付
|
||||
params := map[string]string{
|
||||
"version": "1.1",
|
||||
"trade_order_id": orderNo,
|
||||
"total_fee": fmt.Sprintf("%f", order.Amount),
|
||||
"title": order.Subject,
|
||||
"notify_url": h.App.Config.HuPiPayConfig.NotifyURL,
|
||||
"return_url": "",
|
||||
"wap_name": "极客学长",
|
||||
"callback_url": "",
|
||||
}
|
||||
|
||||
c.Redirect(302, uri)
|
||||
res, err := h.huPiPayService.Pay(params)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with generate pay url: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var r struct {
|
||||
Openid int64 `json:"openid"`
|
||||
UrlQrcode string `json:"url_qrcode"`
|
||||
URL string `json:"url"`
|
||||
ErrCode int `json:"errcode"`
|
||||
ErrMsg string `json:"errmsg"`
|
||||
}
|
||||
err = utils.JsonDecode(res, &r)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with decode payment result: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if r.ErrCode != 0 {
|
||||
resp.ERROR(c, "error with generate pay url: "+r.ErrMsg)
|
||||
return
|
||||
}
|
||||
c.Redirect(302, r.URL)
|
||||
}
|
||||
resp.ERROR(c, "Invalid operations")
|
||||
}
|
||||
|
||||
// OrderQuery 清单状态查询
|
||||
// OrderQuery 查询订单状态
|
||||
func (h *PaymentHandler) OrderQuery(c *gin.Context) {
|
||||
var data struct {
|
||||
OrderNo string `json:"order_no"`
|
||||
@@ -111,16 +157,12 @@ func (h *PaymentHandler) OrderQuery(c *gin.Context) {
|
||||
resp.SUCCESS(c, gin.H{"status": order.Status})
|
||||
}
|
||||
|
||||
// AlipayQrcode 生成支付宝支付 URL 二维码
|
||||
func (h *PaymentHandler) AlipayQrcode(c *gin.Context) {
|
||||
if !h.App.SysConfig.EnabledAlipay || h.alipayService == nil {
|
||||
resp.ERROR(c, "当前支付通道已经关闭,请联系管理员开通!")
|
||||
return
|
||||
}
|
||||
|
||||
// PayQrcode 生成支付 URL 二维码
|
||||
func (h *PaymentHandler) PayQrcode(c *gin.Context) {
|
||||
var data struct {
|
||||
ProductId uint `json:"product_id"`
|
||||
UserId int `json:"user_id"`
|
||||
PayWay string `json:"pay_way"` // 支付方式
|
||||
ProductId uint `json:"product_id"`
|
||||
UserId int `json:"user_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
@@ -134,7 +176,7 @@ func (h *PaymentHandler) AlipayQrcode(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
orderNo, err := h.snowflake.Next()
|
||||
orderNo, err := h.snowflake.Next(false)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "error with generate trade no: "+err.Error())
|
||||
return
|
||||
@@ -146,6 +188,10 @@ func (h *PaymentHandler) AlipayQrcode(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
payWay := PayWayAlipay
|
||||
if data.PayWay == "hupi" {
|
||||
payWay = PayWayXunHu
|
||||
}
|
||||
// 创建订单
|
||||
remark := types.OrderRemark{
|
||||
Days: product.Days,
|
||||
@@ -162,7 +208,7 @@ func (h *PaymentHandler) AlipayQrcode(c *gin.Context) {
|
||||
Subject: product.Name,
|
||||
Amount: product.Price - product.Discount,
|
||||
Status: types.OrderNotPaid,
|
||||
PayWay: PayWayAlipay,
|
||||
PayWay: payWay,
|
||||
Remark: utils.JsonEncode(remark),
|
||||
}
|
||||
res = h.db.Create(&order)
|
||||
@@ -171,19 +217,30 @@ func (h *PaymentHandler) AlipayQrcode(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 生成二维码图片
|
||||
file, err := h.fs.Open("res/img/alipay.jpg")
|
||||
var logo string
|
||||
if data.PayWay == "alipay" {
|
||||
logo = "res/img/alipay.jpg"
|
||||
} else if data.PayWay == "hupi" {
|
||||
if h.App.Config.HuPiPayConfig.Name == "wechat" {
|
||||
logo = "res/img/wechat-pay.jpg"
|
||||
} else {
|
||||
logo = "res/img/alipay.jpg"
|
||||
}
|
||||
}
|
||||
|
||||
file, err := h.fs.Open(logo)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
resp.ERROR(c, "error with open qrcode log file: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
parse, err := url.Parse(h.App.Config.AlipayConfig.NotifyURL)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
imageURL := fmt.Sprintf("%s://%s/api/payment/alipay?order_no=%s", parse.Scheme, parse.Host, orderNo)
|
||||
imageURL := fmt.Sprintf("%s://%s/api/payment/doPay?order_no=%s&pay_way=%s", parse.Scheme, parse.Host, orderNo, data.PayWay)
|
||||
imgData, err := utils.GenQrcode(imageURL, 400, file)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
@@ -193,6 +250,7 @@ func (h *PaymentHandler) AlipayQrcode(c *gin.Context) {
|
||||
resp.SUCCESS(c, gin.H{"order_no": orderNo, "image": fmt.Sprintf("data:image/jpg;base64, %s", imgDataBase64), "url": imageURL})
|
||||
}
|
||||
|
||||
// AlipayNotify 支付宝支付回调
|
||||
func (h *PaymentHandler) AlipayNotify(c *gin.Context) {
|
||||
err := c.Request.ParseForm()
|
||||
if err != nil {
|
||||
@@ -212,27 +270,46 @@ func (h *PaymentHandler) AlipayNotify(c *gin.Context) {
|
||||
h.lock.Lock()
|
||||
defer h.lock.Unlock()
|
||||
|
||||
var order model.Order
|
||||
res := h.db.Where("order_no = ?", r.OutTradeNo).First(&order)
|
||||
if res.Error != nil {
|
||||
logger.Error(res.Error)
|
||||
err = h.notify(r.OutTradeNo)
|
||||
if err != nil {
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
c.String(http.StatusOK, "success")
|
||||
}
|
||||
|
||||
// 异步通知回调公共逻辑
|
||||
func (h *PaymentHandler) notify(orderNo string) error {
|
||||
var order model.Order
|
||||
res := h.db.Where("order_no = ?", orderNo).First(&order)
|
||||
if res.Error != nil {
|
||||
err := fmt.Errorf("error with fetch order: %v", res.Error)
|
||||
logger.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 已支付订单,直接返回
|
||||
if order.Status == types.OrderPaidSuccess {
|
||||
return nil
|
||||
}
|
||||
|
||||
var user model.User
|
||||
res = h.db.First(&user, order.UserId)
|
||||
if res.Error != nil {
|
||||
logger.Error(res.Error)
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
err := fmt.Errorf("error with fetch user info: %v", res.Error)
|
||||
logger.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
var remark types.OrderRemark
|
||||
err = utils.JsonDecode(order.Remark, &remark)
|
||||
err := utils.JsonDecode(order.Remark, &remark)
|
||||
if err != nil {
|
||||
logger.Error(res.Error)
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
err := fmt.Errorf("error with decode order remark: %v", err)
|
||||
logger.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 1. 点卡:days == 0, calls > 0
|
||||
// 2. vip 套餐:days > 0, calls == 0
|
||||
if remark.Days > 0 {
|
||||
@@ -256,18 +333,57 @@ func (h *PaymentHandler) AlipayNotify(c *gin.Context) {
|
||||
// 更新用户信息
|
||||
res = h.db.Updates(&user)
|
||||
if res.Error != nil {
|
||||
logger.Error(res.Error)
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
err := fmt.Errorf("error with update user info: %v", res.Error)
|
||||
logger.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新订单状态
|
||||
order.PayTime = time.Now().Unix()
|
||||
order.Status = types.OrderPaidSuccess
|
||||
h.db.Updates(&order)
|
||||
res = h.db.Updates(&order)
|
||||
if res.Error != nil {
|
||||
err := fmt.Errorf("error with update order info: %v", res.Error)
|
||||
logger.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新产品销量
|
||||
h.db.Model(&model.Product{}).Where("id = ?", order.ProductId).UpdateColumn("sales", gorm.Expr("sales + ?", 1))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPayWays 获取支付方式
|
||||
func (h *PaymentHandler) GetPayWays(c *gin.Context) {
|
||||
data := gin.H{}
|
||||
if h.App.Config.AlipayConfig.Enabled {
|
||||
data["alipay"] = gin.H{"name": "alipay"}
|
||||
}
|
||||
if h.App.Config.HuPiPayConfig.Enabled {
|
||||
data["hupi"] = gin.H{"name": h.App.Config.HuPiPayConfig.Name}
|
||||
}
|
||||
resp.SUCCESS(c, data)
|
||||
}
|
||||
|
||||
// HuPiPayNotify 虎皮椒支付异步回调
|
||||
func (h *PaymentHandler) HuPiPayNotify(c *gin.Context) {
|
||||
err := c.Request.ParseForm()
|
||||
if err != nil {
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
orderNo := c.Request.Form.Get("trade_order_id")
|
||||
logger.Infof("收到订单支付回调,订单 NO:%s", orderNo)
|
||||
// TODO 是否要保存订单交易流水号
|
||||
h.lock.Lock()
|
||||
defer h.lock.Unlock()
|
||||
|
||||
err = h.notify(orderNo)
|
||||
if err != nil {
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
|
||||
c.String(http.StatusOK, "success")
|
||||
}
|
||||
|
||||
120
api/handler/prompt_handler.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"chatplus/core"
|
||||
"chatplus/core/types"
|
||||
"chatplus/store/model"
|
||||
"chatplus/utils/resp"
|
||||
"fmt"
|
||||
|
||||
"github.com/imroc/req/v3"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const rewritePromptTemplate = "Please rewrite the following text into AI painting prompt words, and please try to add detailed description of the picture, painting style, scene, rendering effect, picture light and other elements. Please output directly in English without any explanation, within 150 words. The text to be rewritten is: [%s]"
|
||||
const translatePromptTemplate = "Translate the following painting prompt words into English keyword phrases. Without any explanation, directly output the keyword phrases separated by commas. The content to be translated is: [%s]"
|
||||
|
||||
type PromptHandler struct {
|
||||
BaseHandler
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewPromptHandler(app *core.AppServer, db *gorm.DB) *PromptHandler {
|
||||
h := &PromptHandler{db: db}
|
||||
h.App = app
|
||||
return h
|
||||
}
|
||||
|
||||
type apiRes struct {
|
||||
Model string `json:"model"`
|
||||
Choices []struct {
|
||||
Index int `json:"index"`
|
||||
Message struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
} `json:"message"`
|
||||
FinishReason string `json:"finish_reason"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
|
||||
type apiErrRes struct {
|
||||
Error struct {
|
||||
Code interface{} `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Param interface{} `json:"param"`
|
||||
Type string `json:"type"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
// Rewrite translate and rewrite prompt with ChatGPT
|
||||
func (h *PromptHandler) Rewrite(c *gin.Context) {
|
||||
var data struct {
|
||||
Prompt string `json:"prompt"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
content, err := h.request(data.Prompt, rewritePromptTemplate)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, content)
|
||||
}
|
||||
|
||||
func (h *PromptHandler) Translate(c *gin.Context) {
|
||||
var data struct {
|
||||
Prompt string `json:"prompt"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
content, err := h.request(data.Prompt, translatePromptTemplate)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, content)
|
||||
}
|
||||
|
||||
func (h *PromptHandler) request(prompt string, promptTemplate string) (string, error) {
|
||||
// 获取 OpenAI 的 API KEY
|
||||
var apiKey model.ApiKey
|
||||
res := h.db.Where("platform = ?", types.OpenAI).First(&apiKey)
|
||||
if res.Error != nil {
|
||||
return "", fmt.Errorf("error with fetch OpenAI API KEY:%v", res.Error)
|
||||
}
|
||||
|
||||
messages := make([]interface{}, 1)
|
||||
messages[0] = types.Message{
|
||||
Role: "user",
|
||||
Content: fmt.Sprintf(promptTemplate, prompt),
|
||||
}
|
||||
|
||||
var response apiRes
|
||||
var errRes apiErrRes
|
||||
r, err := req.C().SetProxyURL(h.App.Config.ProxyURL).R().SetHeader("Content-Type", "application/json").
|
||||
SetHeader("Authorization", "Bearer "+apiKey.Value).
|
||||
SetBody(types.ApiRequest{
|
||||
Model: "gpt-3.5-turbo",
|
||||
Temperature: 0.9,
|
||||
MaxTokens: 1024,
|
||||
Stream: false,
|
||||
Messages: messages,
|
||||
}).
|
||||
SetErrorResult(&errRes).
|
||||
SetSuccessResult(&response).Post(h.App.ChatConfig.OpenAI.ApiURL)
|
||||
if err != nil || r.IsErrorState() {
|
||||
return "", fmt.Errorf("error with http request: %v%v%s", err, r.Err, errRes.Error.Message)
|
||||
}
|
||||
|
||||
return response.Choices[0].Message.Content, nil
|
||||
}
|
||||
@@ -8,47 +8,30 @@ import (
|
||||
"chatplus/store/vo"
|
||||
"chatplus/utils"
|
||||
"chatplus/utils/resp"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/gorilla/websocket"
|
||||
"gorm.io/gorm"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SdJobHandler struct {
|
||||
BaseHandler
|
||||
redis *redis.Client
|
||||
db *gorm.DB
|
||||
service *sd.Service
|
||||
redis *redis.Client
|
||||
db *gorm.DB
|
||||
pool *sd.ServicePool
|
||||
}
|
||||
|
||||
func NewSdJobHandler(app *core.AppServer, redisCli *redis.Client, db *gorm.DB, service *sd.Service) *SdJobHandler {
|
||||
func NewSdJobHandler(app *core.AppServer, db *gorm.DB, pool *sd.ServicePool) *SdJobHandler {
|
||||
h := SdJobHandler{
|
||||
redis: redisCli,
|
||||
db: db,
|
||||
service: service,
|
||||
db: db,
|
||||
pool: pool,
|
||||
}
|
||||
h.App = app
|
||||
return &h
|
||||
}
|
||||
|
||||
// Client WebSocket 客户端,用于通知任务状态变更
|
||||
func (h *SdJobHandler) Client(c *gin.Context) {
|
||||
ws, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}).Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
sessionId := c.Query("session_id")
|
||||
client := types.NewWsClient(ws)
|
||||
// 删除旧的连接
|
||||
h.service.Clients.Put(sessionId, client)
|
||||
logger.Infof("New websocket connected, IP: %s", c.ClientIP())
|
||||
}
|
||||
|
||||
func (h *SdJobHandler) checkLimits(c *gin.Context) bool {
|
||||
user, err := utils.GetLoginUser(c, h.db)
|
||||
if err != nil {
|
||||
@@ -56,6 +39,11 @@ func (h *SdJobHandler) checkLimits(c *gin.Context) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
if !h.pool.HasAvailableService() {
|
||||
resp.ERROR(c, "Stable-Diffusion 池子中没有没有可用的服务!")
|
||||
return false
|
||||
}
|
||||
|
||||
if user.ImgCalls <= 0 {
|
||||
resp.ERROR(c, "您的绘图次数不足,请联系管理员充值!")
|
||||
return false
|
||||
@@ -67,11 +55,6 @@ func (h *SdJobHandler) checkLimits(c *gin.Context) bool {
|
||||
|
||||
// Image 创建一个绘画任务
|
||||
func (h *SdJobHandler) Image(c *gin.Context) {
|
||||
if !h.App.Config.SdConfig.Enabled {
|
||||
resp.ERROR(c, "Stable Diffusion service is disabled")
|
||||
return
|
||||
}
|
||||
|
||||
if !h.checkLimits(c) {
|
||||
return
|
||||
}
|
||||
@@ -129,7 +112,6 @@ func (h *SdJobHandler) Image(c *gin.Context) {
|
||||
Params: utils.JsonEncode(params),
|
||||
Prompt: data.Prompt,
|
||||
Progress: 0,
|
||||
Started: false,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
res := h.db.Create(&job)
|
||||
@@ -138,24 +120,15 @@ func (h *SdJobHandler) Image(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
h.service.PushTask(types.SdTask{
|
||||
h.pool.PushTask(types.SdTask{
|
||||
Id: int(job.Id),
|
||||
SessionId: data.SessionId,
|
||||
Src: types.TaskSrcImg,
|
||||
Type: types.TaskImage,
|
||||
Prompt: data.Prompt,
|
||||
Params: params,
|
||||
UserId: userId,
|
||||
})
|
||||
var jobVo vo.SdJob
|
||||
err := utils.CopyObject(job, &jobVo)
|
||||
if err == nil {
|
||||
// 推送任务到前端
|
||||
client := h.service.Clients.Get(data.SessionId)
|
||||
if client != nil {
|
||||
utils.ReplyChunkMessage(client, jobVo)
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
@@ -194,12 +167,22 @@ func (h *SdJobHandler) JobList(c *gin.Context) {
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if job.Progress == -1 {
|
||||
h.db.Delete(&model.MidJourneyJob{Id: job.Id})
|
||||
}
|
||||
|
||||
if item.Progress < 100 {
|
||||
// 30 分钟还没完成的任务直接删除
|
||||
if time.Now().Sub(item.CreatedAt) > time.Minute*30 {
|
||||
// 10 分钟还没完成的任务直接删除
|
||||
if time.Now().Sub(item.CreatedAt) > time.Minute*10 {
|
||||
h.db.Delete(&item)
|
||||
continue
|
||||
}
|
||||
// 正在运行中任务使用代理访问图片
|
||||
image, err := utils.DownloadImage(item.ImgURL, "")
|
||||
if err == nil {
|
||||
job.ImgURL = "data:image/png;base64," + base64.StdEncoding.EncodeToString(image)
|
||||
}
|
||||
}
|
||||
jobs = append(jobs, job)
|
||||
}
|
||||
|
||||
@@ -58,13 +58,3 @@ func (h *SmsHandler) SendCode(c *gin.Context) {
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
type statusVo struct {
|
||||
EnabledMsgService bool `json:"enabled_msg_service"`
|
||||
EnabledRegister bool `json:"enabled_register"`
|
||||
}
|
||||
|
||||
// Status check if the message service is enabled
|
||||
func (h *SmsHandler) Status(c *gin.Context) {
|
||||
resp.SUCCESS(c, statusVo{EnabledMsgService: h.App.SysConfig.EnabledMsg, EnabledRegister: h.App.SysConfig.EnabledRegister})
|
||||
}
|
||||
|
||||
40
api/handler/test_handler.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"chatplus/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type TestHandler struct {
|
||||
snowflake *service.Snowflake
|
||||
}
|
||||
|
||||
func NewTestHandler(snowflake *service.Snowflake) *TestHandler {
|
||||
return &TestHandler{snowflake: snowflake}
|
||||
}
|
||||
|
||||
func (h *TestHandler) TestPay(c *gin.Context) {
|
||||
//appId := "" //Appid
|
||||
//appSecret := "" //密钥
|
||||
//var host = "https://api.xunhupay.com/payment/do.html" //跳转支付页接口URL
|
||||
//client := payment.NewXunHuPay(appId, appSecret) //初始化调用
|
||||
//
|
||||
////支付参数,appid、time、nonce_str和hash这四个参数不用传,调用的时候执行方法内部已经处理
|
||||
//orderNo, _ := h.snowflake.Next()
|
||||
//params := map[string]string{
|
||||
// "version": "1.1",
|
||||
// "trade_order_id": orderNo,
|
||||
// "total_fee": "0.1",
|
||||
// "title": "测试支付",
|
||||
// "notify_url": "http://xxxxxxx.com",
|
||||
// "return_url": "http://localhost:8888",
|
||||
// "wap_name": "极客学长",
|
||||
// "callback_url": "",
|
||||
//}
|
||||
//
|
||||
//execute, err := client.Execute(host, params) //执行支付操作
|
||||
//if err != nil {
|
||||
// logger.Error(err)
|
||||
//}
|
||||
//resp.SUCCESS(c, execute)
|
||||
}
|
||||
@@ -39,9 +39,10 @@ func NewUserHandler(
|
||||
func (h *UserHandler) Register(c *gin.Context) {
|
||||
// parameters process
|
||||
var data struct {
|
||||
Mobile string `json:"mobile"`
|
||||
Password string `json:"password"`
|
||||
Code string `json:"code"`
|
||||
Mobile string `json:"mobile"`
|
||||
Password string `json:"password"`
|
||||
Code string `json:"code"`
|
||||
InviteCode string `json:"invite_code"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
@@ -68,6 +69,21 @@ func (h *UserHandler) Register(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// 验证邀请码
|
||||
inviteCode := model.InviteCode{}
|
||||
if data.InviteCode == "" {
|
||||
if h.App.SysConfig.ForceInvite {
|
||||
resp.ERROR(c, "当前系统设定必须使用邀请码才能注册")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
res := h.db.Where("code = ?", data.InviteCode).First(&inviteCode)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "无效的邀请码")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// check if the username is exists
|
||||
var item model.User
|
||||
res := h.db.Where("mobile = ?", data.Mobile).First(&item)
|
||||
@@ -92,7 +108,7 @@ func (h *UserHandler) Register(c *gin.Context) {
|
||||
types.ChatGLM: "",
|
||||
},
|
||||
}),
|
||||
Calls: h.App.SysConfig.UserInitCalls,
|
||||
Calls: h.App.SysConfig.InitChatCalls,
|
||||
ImgCalls: h.App.SysConfig.InitImgCalls,
|
||||
}
|
||||
res = h.db.Create(&user)
|
||||
@@ -102,6 +118,26 @@ func (h *UserHandler) Register(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 记录邀请关系
|
||||
if data.InviteCode != "" {
|
||||
// 增加邀请数量
|
||||
h.db.Model(&model.InviteCode{}).Where("code = ?", data.InviteCode).UpdateColumn("reg_num", gorm.Expr("reg_num + ?", 1))
|
||||
if h.App.SysConfig.InviteChatCalls > 0 {
|
||||
h.db.Model(&model.User{}).Where("id = ?", inviteCode.UserId).UpdateColumn("calls", gorm.Expr("calls + ?", h.App.SysConfig.InviteChatCalls))
|
||||
}
|
||||
if h.App.SysConfig.InviteImgCalls > 0 {
|
||||
h.db.Model(&model.User{}).Where("id = ?", inviteCode.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls + ?", h.App.SysConfig.InviteImgCalls))
|
||||
}
|
||||
|
||||
// 添加邀请记录
|
||||
h.db.Create(&model.InviteLog{
|
||||
InviterId: inviteCode.UserId,
|
||||
UserId: user.Id,
|
||||
Username: user.Mobile,
|
||||
InviteCode: inviteCode.Code,
|
||||
Reward: utils.JsonEncode(types.InviteReward{ChatCalls: h.App.SysConfig.InviteChatCalls, ImgCalls: h.App.SysConfig.InviteImgCalls}),
|
||||
})
|
||||
}
|
||||
if h.App.SysConfig.EnabledMsg {
|
||||
_ = h.redis.Del(c, key) // 注册成功,删除短信验证码
|
||||
}
|
||||
|
||||
62
api/main.go
@@ -17,7 +17,6 @@ import (
|
||||
"chatplus/store"
|
||||
"context"
|
||||
"embed"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
@@ -26,6 +25,8 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
|
||||
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
|
||||
"go.uber.org/fx"
|
||||
"gorm.io/gorm"
|
||||
@@ -162,36 +163,14 @@ func main() {
|
||||
}
|
||||
}),
|
||||
|
||||
// MidJourney 机器人
|
||||
fx.Provide(mj.NewBot),
|
||||
fx.Provide(mj.NewClient),
|
||||
fx.Invoke(func(config *types.AppConfig, bot *mj.Bot) {
|
||||
if config.MjConfig.Enabled {
|
||||
err := bot.Run()
|
||||
if err != nil {
|
||||
log.Fatal("MidJourney 服务启动失败:", err)
|
||||
}
|
||||
}
|
||||
}),
|
||||
fx.Invoke(func(config *types.AppConfig, mjService *mj.Service) {
|
||||
if config.MjConfig.Enabled {
|
||||
go func() {
|
||||
mjService.Run()
|
||||
}()
|
||||
}
|
||||
}),
|
||||
// MidJourney service pool
|
||||
fx.Provide(mj.NewServicePool),
|
||||
|
||||
// Stable Diffusion 机器人
|
||||
fx.Provide(sd.NewService),
|
||||
fx.Invoke(func(config *types.AppConfig, service *sd.Service) {
|
||||
if config.SdConfig.Enabled {
|
||||
go func() {
|
||||
service.Run()
|
||||
}()
|
||||
}
|
||||
}),
|
||||
fx.Provide(sd.NewServicePool),
|
||||
|
||||
fx.Provide(payment.NewAlipayService),
|
||||
fx.Provide(payment.NewHuPiPay),
|
||||
fx.Provide(service.NewSnowflake),
|
||||
fx.Provide(service.NewXXLJobExecutor),
|
||||
fx.Invoke(func(exec *service.XXLJobExecutor, config *types.AppConfig) {
|
||||
@@ -237,7 +216,6 @@ func main() {
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.SmsHandler) {
|
||||
group := s.Engine.Group("/api/sms/")
|
||||
group.GET("status", h.Status)
|
||||
group.POST("code", h.SendCode)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.CaptchaHandler) {
|
||||
@@ -255,13 +233,11 @@ func main() {
|
||||
group.POST("upscale", h.Upscale)
|
||||
group.POST("variation", h.Variation)
|
||||
group.GET("jobs", h.JobList)
|
||||
group.Any("client", h.Client)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.SdJobHandler) {
|
||||
group := s.Engine.Group("/api/sd")
|
||||
group.POST("image", h.Image)
|
||||
group.GET("jobs", h.JobList)
|
||||
group.Any("client", h.Client)
|
||||
}),
|
||||
|
||||
// 管理后台控制器
|
||||
@@ -319,10 +295,12 @@ func main() {
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.PaymentHandler) {
|
||||
group := s.Engine.Group("/api/payment/")
|
||||
group.GET("alipay", h.Alipay)
|
||||
group.GET("doPay", h.DoPay)
|
||||
group.GET("payWays", h.GetPayWays)
|
||||
group.POST("query", h.OrderQuery)
|
||||
group.POST("alipay/qrcode", h.AlipayQrcode)
|
||||
group.POST("qrcode", h.PayQrcode)
|
||||
group.POST("alipay/notify", h.AlipayNotify)
|
||||
group.POST("hupipay/notify", h.HuPiPayNotify)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ProductHandler) {
|
||||
group := s.Engine.Group("/api/admin/product/")
|
||||
@@ -346,6 +324,26 @@ func main() {
|
||||
group.GET("list", h.List)
|
||||
}),
|
||||
|
||||
fx.Provide(handler.NewInviteHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.InviteHandler) {
|
||||
group := s.Engine.Group("/api/invite/")
|
||||
group.GET("code", h.Code)
|
||||
group.POST("list", h.List)
|
||||
group.GET("hits", h.Hits)
|
||||
}),
|
||||
|
||||
fx.Provide(handler.NewPromptHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.PromptHandler) {
|
||||
group := s.Engine.Group("/api/prompt/")
|
||||
group.POST("rewrite", h.Rewrite)
|
||||
group.POST("translate", h.Translate)
|
||||
}),
|
||||
|
||||
fx.Provide(handler.NewTestHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.TestHandler) {
|
||||
group := s.Engine.Group("/test/")
|
||||
group.GET("pay", h.TestPay)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, db *gorm.DB) {
|
||||
err := s.Run(db)
|
||||
if err != nil {
|
||||
|
||||
BIN
api/res/img/wechat-pay.jpg
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
@@ -1,21 +1,21 @@
|
||||
{
|
||||
"data": [
|
||||
"task(m1wpaa4v60zedj8)",
|
||||
"a cute cat",
|
||||
"task(owy5niy1sbbnlq0)",
|
||||
"A beautiful Chinese girl plays the guitar on the beach. She is dressed in a flowing dress that matches the colors of the sunset. With her eyes closed, she strums the guitar with passion and confidence, her fingers dancing gracefully on the strings. The painting employs a vibrant color palette, capturing the warmth of the setting sun blending with the serene hues of the ocean. The artist uses a combination of impressionistic and realistic brushstrokes to convey both the girl's delicate features and the dynamic movement of the waves. The rendering effect creates a dream-like atmosphere, as if the viewer is being transported to a magical realm where music and nature intertwine. The picture is bathed in a soft, golden light, casting a warm glow on the girl's face, illuminating her joy and connection to the music she creates.",
|
||||
"",
|
||||
[],
|
||||
20,
|
||||
"DPM++ 2M Karras",
|
||||
30,
|
||||
"DPM++ 3M SDE Karras",
|
||||
1,
|
||||
1,
|
||||
7,
|
||||
512,
|
||||
384,
|
||||
true,
|
||||
512,
|
||||
false,
|
||||
0.7,
|
||||
2,
|
||||
"ESRGAN_4x",
|
||||
10,
|
||||
"Latent",
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
"Use same checkpoint",
|
||||
@@ -33,6 +33,9 @@
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
"positive",
|
||||
@@ -55,13 +58,22 @@
|
||||
false,
|
||||
false,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
50,
|
||||
[],
|
||||
"",
|
||||
"",
|
||||
""
|
||||
],
|
||||
"event_data": null,
|
||||
"fn_index": 96,
|
||||
"session_hash": "kmb0ojjfhdj"
|
||||
"fn_index": 316,
|
||||
"session_hash": "ttr8efgt63g"
|
||||
}
|
||||
116
api/service/fun/func_img.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package fun
|
||||
|
||||
import (
|
||||
"chatplus/core/types"
|
||||
"chatplus/service/oss"
|
||||
"chatplus/store/model"
|
||||
"chatplus/utils"
|
||||
"fmt"
|
||||
|
||||
"github.com/imroc/req/v3"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AI 绘画函数
|
||||
|
||||
type FuncImage struct {
|
||||
name string
|
||||
db *gorm.DB
|
||||
uploadManager *oss.UploaderManager
|
||||
proxyURL string
|
||||
}
|
||||
|
||||
func NewImageFunc(db *gorm.DB, manager *oss.UploaderManager, config *types.AppConfig) FuncImage {
|
||||
return FuncImage{
|
||||
db: db,
|
||||
name: "DALL-E3 绘画",
|
||||
uploadManager: manager,
|
||||
proxyURL: config.ProxyURL,
|
||||
}
|
||||
}
|
||||
|
||||
type imgReq struct {
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt"`
|
||||
N int `json:"n"`
|
||||
Size string `json:"size"`
|
||||
}
|
||||
|
||||
type imgRes struct {
|
||||
Created int64 `json:"created"`
|
||||
Data []struct {
|
||||
RevisedPrompt string `json:"revised_prompt"`
|
||||
Url string `json:"url"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type ErrRes struct {
|
||||
Error struct {
|
||||
Code interface{} `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Param interface{} `json:"param"`
|
||||
Type string `json:"type"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
func (f FuncImage) Invoke(params map[string]interface{}) (string, error) {
|
||||
logger.Infof("绘画参数:%+v", params)
|
||||
prompt := utils.InterfaceToString(params["prompt"])
|
||||
// get image generation API KEY
|
||||
var apiKey model.ApiKey
|
||||
tx := f.db.Where("platform = ? AND type = ?", types.OpenAI, "img").Order("last_used_at ASC").First(&apiKey)
|
||||
if tx.Error != nil {
|
||||
return "", fmt.Errorf("error with get generation API KEY: %v", tx.Error)
|
||||
}
|
||||
|
||||
// get image generation api URL
|
||||
var conf model.Config
|
||||
var chatConfig types.ChatConfig
|
||||
tx = f.db.Where("marker", "chat").First(&conf)
|
||||
if tx.Error != nil {
|
||||
return "", fmt.Errorf("error with get chat configs: %v", tx.Error)
|
||||
}
|
||||
|
||||
err := utils.JsonDecode(conf.Config, &chatConfig)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with decode chat config: %v", err)
|
||||
}
|
||||
|
||||
apiURL := chatConfig.DallApiURL
|
||||
if utils.IsEmptyValue(apiURL) {
|
||||
apiURL = "https://api.openai.com/v1/images/generations"
|
||||
}
|
||||
imgNum := chatConfig.DallImgNum
|
||||
if imgNum <= 0 {
|
||||
imgNum = 1
|
||||
}
|
||||
var res imgRes
|
||||
var errRes ErrRes
|
||||
r, err := req.C().SetProxyURL(f.proxyURL).R().SetHeader("Content-Type", "application/json").
|
||||
SetHeader("Authorization", "Bearer "+apiKey.Value).
|
||||
SetBody(imgReq{
|
||||
Model: "dall-e-3",
|
||||
Prompt: prompt,
|
||||
N: imgNum,
|
||||
Size: "1024x1024",
|
||||
}).
|
||||
SetErrorResult(&errRes).
|
||||
SetSuccessResult(&res).Post(apiURL)
|
||||
if err != nil || r.IsErrorState() {
|
||||
return "", fmt.Errorf("error with http request: %v%v%s", err, r.Err, errRes.Error.Message)
|
||||
}
|
||||
// 存储图片
|
||||
imgURL, err := f.uploadManager.GetUploadHandler().PutImg(res.Data[0].Url, false)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("下载图片失败: %s", err.Error())
|
||||
}
|
||||
|
||||
logger.Info(imgURL)
|
||||
return fmt.Sprintf("\n\n\n", imgURL), nil
|
||||
}
|
||||
|
||||
func (f FuncImage) Name() string {
|
||||
return f.name
|
||||
}
|
||||
|
||||
var _ Function = &FuncImage{}
|
||||
@@ -1,42 +0,0 @@
|
||||
package fun
|
||||
|
||||
import (
|
||||
"chatplus/core/types"
|
||||
"chatplus/service/mj"
|
||||
"chatplus/utils"
|
||||
)
|
||||
|
||||
// AI 绘画函数
|
||||
|
||||
type FuncMidJourney struct {
|
||||
name string
|
||||
service *mj.Service
|
||||
}
|
||||
|
||||
func NewMidJourneyFunc(mjService *mj.Service) FuncMidJourney {
|
||||
return FuncMidJourney{
|
||||
name: "MidJourney AI 绘画",
|
||||
service: mjService}
|
||||
}
|
||||
|
||||
func (f FuncMidJourney) Invoke(params map[string]interface{}) (string, error) {
|
||||
logger.Infof("MJ 绘画参数:%+v", params)
|
||||
prompt := utils.InterfaceToString(params["prompt"])
|
||||
f.service.PushTask(types.MjTask{
|
||||
SessionId: utils.InterfaceToString(params["session_id"]),
|
||||
Src: types.TaskSrcChat,
|
||||
Type: types.TaskImage,
|
||||
Prompt: prompt,
|
||||
UserId: utils.IntValue(utils.InterfaceToString(params["user_id"]), 0),
|
||||
RoleId: utils.IntValue(utils.InterfaceToString(params["role_id"]), 0),
|
||||
Icon: utils.InterfaceToString(params["icon"]),
|
||||
ChatId: utils.InterfaceToString(params["chat_id"]),
|
||||
})
|
||||
return prompt, nil
|
||||
}
|
||||
|
||||
func (f FuncMidJourney) Name() string {
|
||||
return f.name
|
||||
}
|
||||
|
||||
var _ Function = &FuncMidJourney{}
|
||||
@@ -3,7 +3,8 @@ package fun
|
||||
import (
|
||||
"chatplus/core/types"
|
||||
logger2 "chatplus/logger"
|
||||
"chatplus/service/mj"
|
||||
"chatplus/service/oss"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Function interface {
|
||||
@@ -29,11 +30,11 @@ type dataItem struct {
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
func NewFunctions(config *types.AppConfig, mjService *mj.Service) map[string]Function {
|
||||
func NewFunctions(config *types.AppConfig, db *gorm.DB, manager *oss.UploaderManager) map[string]Function {
|
||||
return map[string]Function{
|
||||
types.FuncZaoBao: NewZaoBao(config.ApiConfig),
|
||||
types.FuncWeibo: NewWeiboHot(config.ApiConfig),
|
||||
types.FuncHeadLine: NewHeadLines(config.ApiConfig),
|
||||
types.FuncMidJourney: NewMidJourneyFunc(mjService),
|
||||
types.FuncZaoBao: NewZaoBao(config.ApiConfig),
|
||||
types.FuncWeibo: NewWeiboHot(config.ApiConfig),
|
||||
types.FuncHeadLine: NewHeadLines(config.ApiConfig),
|
||||
types.FuncImage: NewImageFunc(db, manager, config),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,17 +19,18 @@ var logger = logger2.GetLogger()
|
||||
type Bot struct {
|
||||
config *types.MidJourneyConfig
|
||||
bot *discordgo.Session
|
||||
name string
|
||||
service *Service
|
||||
}
|
||||
|
||||
func NewBot(config *types.AppConfig, service *Service) (*Bot, error) {
|
||||
discord, err := discordgo.New("Bot " + config.MjConfig.BotToken)
|
||||
func NewBot(name string, proxy string, config *types.MidJourneyConfig, service *Service) (*Bot, error) {
|
||||
discord, err := discordgo.New("Bot " + config.BotToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if config.ProxyURL != "" {
|
||||
proxy, _ := url.Parse(config.ProxyURL)
|
||||
if proxy != "" {
|
||||
proxy, _ := url.Parse(proxy)
|
||||
discord.Client = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyURL(proxy),
|
||||
@@ -41,8 +42,9 @@ func NewBot(config *types.AppConfig, service *Service) (*Bot, error) {
|
||||
}
|
||||
|
||||
return &Bot{
|
||||
config: &config.MjConfig,
|
||||
config: config,
|
||||
bot: discord,
|
||||
name: name,
|
||||
service: service,
|
||||
}, nil
|
||||
}
|
||||
@@ -52,13 +54,13 @@ func (b *Bot) Run() error {
|
||||
b.bot.AddHandler(b.messageCreate)
|
||||
b.bot.AddHandler(b.messageUpdate)
|
||||
|
||||
logger.Info("Starting MidJourney Bot...")
|
||||
logger.Infof("Starting MidJourney %s", b.name)
|
||||
err := b.bot.Open()
|
||||
if err != nil {
|
||||
logger.Error("Error opening Discord connection:", err)
|
||||
logger.Errorf("Error opening Discord connection for %s, error: %v", b.name, err)
|
||||
return err
|
||||
}
|
||||
logger.Info("Starting MidJourney Bot successfully!")
|
||||
logger.Infof("Starting MidJourney %s successfully!", b.name)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -14,13 +14,14 @@ type Client struct {
|
||||
config *types.MidJourneyConfig
|
||||
}
|
||||
|
||||
func NewClient(config *types.AppConfig) *Client {
|
||||
func NewClient(config *types.MidJourneyConfig, proxy string) *Client {
|
||||
client := req.C().SetTimeout(10 * time.Second)
|
||||
// set proxy URL
|
||||
if config.ProxyURL != "" {
|
||||
client.SetProxyURL(config.ProxyURL)
|
||||
if proxy != "" {
|
||||
client.SetProxyURL(proxy)
|
||||
}
|
||||
return &Client{client: client, config: &config.MjConfig}
|
||||
logger.Info(proxy)
|
||||
return &Client{client: client, config: config}
|
||||
}
|
||||
|
||||
func (c *Client) Imagine(prompt string) error {
|
||||
|
||||
66
api/service/mj/pool.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package mj
|
||||
|
||||
import (
|
||||
"chatplus/core/types"
|
||||
"chatplus/service/oss"
|
||||
"chatplus/store"
|
||||
"fmt"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ServicePool Mj service pool
|
||||
type ServicePool struct {
|
||||
services []*Service
|
||||
taskQueue *store.RedisQueue
|
||||
}
|
||||
|
||||
func NewServicePool(db *gorm.DB, redisCli *redis.Client, manager *oss.UploaderManager, appConfig *types.AppConfig) *ServicePool {
|
||||
services := make([]*Service, 0)
|
||||
queue := store.NewRedisQueue("MidJourney_Task_Queue", redisCli)
|
||||
// create mj client and service
|
||||
for k, config := range appConfig.MjConfigs {
|
||||
if config.Enabled == false {
|
||||
continue
|
||||
}
|
||||
// create mj client
|
||||
client := NewClient(&config, appConfig.ProxyURL)
|
||||
|
||||
name := fmt.Sprintf("MjService-%d", k)
|
||||
// create mj service
|
||||
service := NewService(name, queue, 4, 600, db, client, manager, appConfig)
|
||||
botName := fmt.Sprintf("MjBot-%d", k)
|
||||
bot, err := NewBot(botName, appConfig.ProxyURL, &config, service)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
err = bot.Run()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// run mj service
|
||||
go func() {
|
||||
service.Run()
|
||||
}()
|
||||
|
||||
services = append(services, service)
|
||||
}
|
||||
|
||||
return &ServicePool{
|
||||
taskQueue: queue,
|
||||
services: services,
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -5,60 +5,59 @@ import (
|
||||
"chatplus/service/oss"
|
||||
"chatplus/store"
|
||||
"chatplus/store/model"
|
||||
"chatplus/store/vo"
|
||||
"chatplus/utils"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"gorm.io/gorm"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MJ 绘画服务
|
||||
|
||||
const RunningJobKey = "MidJourney_Running_Job"
|
||||
|
||||
// Service MJ 绘画服务
|
||||
type Service struct {
|
||||
client *Client // MJ 客户端
|
||||
taskQueue *store.RedisQueue
|
||||
redis *redis.Client
|
||||
db *gorm.DB
|
||||
uploadManager *oss.UploaderManager
|
||||
Clients *types.LMap[string, *types.WsClient] // MJ 绘画页面 websocket 连接池,用户推送绘画消息
|
||||
ChatClients *types.LMap[string, *types.WsClient] // 聊天页面 websocket 连接池,用于推送绘画消息
|
||||
proxyURL string
|
||||
name string // service name
|
||||
client *Client // MJ client
|
||||
taskQueue *store.RedisQueue
|
||||
db *gorm.DB
|
||||
uploadManager *oss.UploaderManager
|
||||
proxyURL string
|
||||
maxHandleTaskNum int32 // max task number current service can handle
|
||||
handledTaskNum int32 // already handled task number
|
||||
taskStartTimes map[int]time.Time // task start time, to check if the task is timeout
|
||||
taskTimeout int64
|
||||
}
|
||||
|
||||
func NewService(redisCli *redis.Client, db *gorm.DB, client *Client, manager *oss.UploaderManager, config *types.AppConfig) *Service {
|
||||
func NewService(name string, queue *store.RedisQueue, maxTaskNum int32, timeout int64, db *gorm.DB, client *Client, manager *oss.UploaderManager, config *types.AppConfig) *Service {
|
||||
return &Service{
|
||||
redis: redisCli,
|
||||
db: db,
|
||||
taskQueue: store.NewRedisQueue("MidJourney_Task_Queue", redisCli),
|
||||
client: client,
|
||||
uploadManager: manager,
|
||||
Clients: types.NewLMap[string, *types.WsClient](),
|
||||
ChatClients: types.NewLMap[string, *types.WsClient](),
|
||||
proxyURL: config.ProxyURL,
|
||||
name: name,
|
||||
db: db,
|
||||
taskQueue: queue,
|
||||
client: client,
|
||||
uploadManager: manager,
|
||||
taskTimeout: timeout,
|
||||
maxHandleTaskNum: maxTaskNum,
|
||||
proxyURL: config.ProxyURL,
|
||||
taskStartTimes: make(map[int]time.Time, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Run() {
|
||||
logger.Info("Starting MidJourney job consumer.")
|
||||
ctx := context.Background()
|
||||
logger.Infof("Starting MidJourney job consumer for %s", s.name)
|
||||
for {
|
||||
_, err := s.redis.Get(ctx, RunningJobKey).Result()
|
||||
if err == nil { // 队列串行执行
|
||||
s.checkTasks()
|
||||
if !s.canHandleTask() {
|
||||
// current service is full, can not handle more task
|
||||
// waiting for running task finish
|
||||
time.Sleep(time.Second * 3)
|
||||
continue
|
||||
}
|
||||
|
||||
var task types.MjTask
|
||||
err = s.taskQueue.LPop(&task)
|
||||
err := s.taskQueue.LPop(&task)
|
||||
if err != nil {
|
||||
logger.Errorf("taking task with error: %v", err)
|
||||
continue
|
||||
}
|
||||
logger.Infof("Consuming Task: %+v", task)
|
||||
|
||||
logger.Infof("%s handle a new MidJourney task: %+v", s.name, task)
|
||||
switch task.Type {
|
||||
case types.TaskImage:
|
||||
err = s.client.Imagine(task.Prompt)
|
||||
@@ -70,42 +69,43 @@ func (s *Service) Run() {
|
||||
case types.TaskVariation:
|
||||
err = s.client.Variation(task.Index, task.MessageId, task.MessageHash)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Error("绘画任务执行失败:", err)
|
||||
if task.RetryCount <= 5 {
|
||||
s.taskQueue.RPush(task)
|
||||
}
|
||||
task.RetryCount += 1
|
||||
time.Sleep(time.Second * 3)
|
||||
// update the task progress
|
||||
s.db.Model(&model.MidJourneyJob{Id: uint(task.Id)}).UpdateColumn("progress", -1)
|
||||
atomic.AddInt32(&s.handledTaskNum, -1)
|
||||
continue
|
||||
}
|
||||
|
||||
// 更新任务的执行状态
|
||||
s.db.Model(&model.MidJourneyJob{}).Where("id = ?", task.Id).UpdateColumn("started", true)
|
||||
// 锁定任务执行通道,直到任务超时(5分钟)
|
||||
s.redis.Set(ctx, RunningJobKey, utils.JsonEncode(task), time.Minute*5)
|
||||
// lock the task until the execute timeout
|
||||
s.taskStartTimes[task.Id] = time.Now()
|
||||
atomic.AddInt32(&s.handledTaskNum, 1)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) PushTask(task types.MjTask) {
|
||||
logger.Infof("add a new MidJourney Task: %+v", task)
|
||||
s.taskQueue.RPush(task)
|
||||
// check if current service instance can handle more task
|
||||
func (s *Service) canHandleTask() bool {
|
||||
handledNum := atomic.LoadInt32(&s.handledTaskNum)
|
||||
return handledNum < s.maxHandleTaskNum
|
||||
}
|
||||
|
||||
// remove the expired tasks
|
||||
func (s *Service) checkTasks() {
|
||||
for k, t := range s.taskStartTimes {
|
||||
if time.Now().Unix()-t.Unix() > s.taskTimeout {
|
||||
delete(s.taskStartTimes, k)
|
||||
atomic.AddInt32(&s.handledTaskNum, -1)
|
||||
// delete task from database
|
||||
s.db.Delete(&model.MidJourneyJob{Id: uint(k)}, "progress < 100")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Notify(data CBReq) {
|
||||
taskString, err := s.redis.Get(context.Background(), RunningJobKey).Result()
|
||||
if err != nil { // 过期任务,丢弃
|
||||
logger.Warn("任务已过期:", err)
|
||||
return
|
||||
}
|
||||
|
||||
var task types.MjTask
|
||||
err = utils.JsonDecode(taskString, &task)
|
||||
if err != nil { // 非标准任务,丢弃
|
||||
logger.Warn("任务解析失败:", err)
|
||||
return
|
||||
}
|
||||
|
||||
// extract the task ID
|
||||
split := strings.Split(data.Prompt, " ")
|
||||
var job model.MidJourneyJob
|
||||
res := s.db.Where("message_id = ?", data.MessageId).First(&job)
|
||||
if res.Error == nil && data.Status == Finished {
|
||||
@@ -113,137 +113,40 @@ func (s *Service) Notify(data CBReq) {
|
||||
return
|
||||
}
|
||||
|
||||
if task.Src == types.TaskSrcImg { // 绘画任务
|
||||
var job model.MidJourneyJob
|
||||
res := s.db.Where("id = ?", task.Id).First(&job)
|
||||
if res.Error != nil {
|
||||
logger.Warn("非法任务:", res.Error)
|
||||
return
|
||||
}
|
||||
job.MessageId = data.MessageId
|
||||
job.ReferenceId = data.ReferenceId
|
||||
job.Progress = data.Progress
|
||||
job.Prompt = data.Prompt
|
||||
job.Hash = data.Image.Hash
|
||||
res = s.db.Where("task_id = ?", split[0]).First(&job)
|
||||
if res.Error != nil {
|
||||
logger.Warn("非法任务:", res.Error)
|
||||
return
|
||||
}
|
||||
job.MessageId = data.MessageId
|
||||
job.ReferenceId = data.ReferenceId
|
||||
job.Progress = data.Progress
|
||||
job.Prompt = data.Prompt
|
||||
job.Hash = data.Image.Hash
|
||||
job.OrgURL = data.Image.URL
|
||||
|
||||
// 任务完成,将最终的图片下载下来
|
||||
if data.Progress == 100 {
|
||||
imgURL, err := s.uploadManager.GetUploadHandler().PutImg(data.Image.URL, true)
|
||||
if err != nil {
|
||||
logger.Error("error with download img: ", err.Error())
|
||||
return
|
||||
}
|
||||
job.ImgURL = imgURL
|
||||
} else {
|
||||
// 临时图片直接保存,访问的时候使用代理进行转发
|
||||
job.ImgURL = data.Image.URL
|
||||
}
|
||||
res = s.db.Updates(&job)
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update job: ", res.Error)
|
||||
return
|
||||
}
|
||||
|
||||
var jobVo vo.MidJourneyJob
|
||||
err := utils.CopyObject(job, &jobVo)
|
||||
if err == nil {
|
||||
if data.Progress < 100 {
|
||||
image, err := utils.DownloadImage(jobVo.ImgURL, s.proxyURL)
|
||||
if err == nil {
|
||||
jobVo.ImgURL = "data:image/png;base64," + base64.StdEncoding.EncodeToString(image)
|
||||
}
|
||||
}
|
||||
|
||||
// 推送任务到前端
|
||||
client := s.Clients.Get(task.SessionId)
|
||||
if client != nil {
|
||||
utils.ReplyChunkMessage(client, jobVo)
|
||||
}
|
||||
}
|
||||
|
||||
} else if task.Src == types.TaskSrcChat { // 聊天任务
|
||||
wsClient := s.ChatClients.Get(task.SessionId)
|
||||
if data.Status == Finished {
|
||||
if wsClient != nil && data.ReferenceId != "" {
|
||||
content := fmt.Sprintf("**%s** 任务执行成功,正在从 MidJourney 服务器下载图片,请稍后...", data.Prompt)
|
||||
utils.ReplyMessage(wsClient, content)
|
||||
}
|
||||
// download image
|
||||
imgURL, err := s.uploadManager.GetUploadHandler().PutImg(data.Image.URL, true)
|
||||
if err != nil {
|
||||
logger.Error("error with download image: ", err)
|
||||
if wsClient != nil && data.ReferenceId != "" {
|
||||
content := fmt.Sprintf("**%s** 图片下载失败:%s", data.Prompt, err.Error())
|
||||
utils.ReplyMessage(wsClient, content)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
tx := s.db.Begin()
|
||||
data.Image.URL = imgURL
|
||||
message := model.HistoryMessage{
|
||||
UserId: uint(task.UserId),
|
||||
ChatId: task.ChatId,
|
||||
RoleId: uint(task.RoleId),
|
||||
Type: types.MjMsg,
|
||||
Icon: task.Icon,
|
||||
Content: utils.JsonEncode(data),
|
||||
Tokens: 0,
|
||||
UseContext: false,
|
||||
}
|
||||
res = tx.Create(&message)
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database: ", err)
|
||||
return
|
||||
}
|
||||
|
||||
// save the job
|
||||
job.UserId = task.UserId
|
||||
job.Type = task.Type.String()
|
||||
job.MessageId = data.MessageId
|
||||
job.ReferenceId = data.ReferenceId
|
||||
job.Prompt = data.Prompt
|
||||
job.ImgURL = imgURL
|
||||
job.Progress = data.Progress
|
||||
job.Hash = data.Image.Hash
|
||||
job.CreatedAt = time.Now()
|
||||
res = tx.Create(&job)
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update database: ", err)
|
||||
tx.Rollback()
|
||||
return
|
||||
}
|
||||
tx.Commit()
|
||||
}
|
||||
|
||||
if wsClient == nil { // 客户端断线,则丢弃
|
||||
logger.Errorf("Client is offline: %+v", data)
|
||||
return
|
||||
}
|
||||
|
||||
if data.Status == Finished {
|
||||
utils.ReplyChunkMessage(wsClient, types.WsMessage{Type: types.WsMjImg, Content: data})
|
||||
utils.ReplyChunkMessage(wsClient, types.WsMessage{Type: types.WsEnd})
|
||||
// 本次绘画完毕,移除客户端
|
||||
s.ChatClients.Delete(task.SessionId)
|
||||
} else {
|
||||
// 使用代理临时转发图片
|
||||
if data.Image.URL != "" {
|
||||
image, err := utils.DownloadImage(data.Image.URL, s.proxyURL)
|
||||
if err == nil {
|
||||
data.Image.URL = "data:image/png;base64," + base64.StdEncoding.EncodeToString(image)
|
||||
}
|
||||
}
|
||||
utils.ReplyChunkMessage(wsClient, types.WsMessage{Type: types.WsMjImg, Content: data})
|
||||
}
|
||||
res = s.db.Updates(&job)
|
||||
if res.Error != nil {
|
||||
logger.Error("error with update job: ", res.Error)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新用户剩余绘图次数
|
||||
// TODO: 放大图片是否需要消耗绘图次数?
|
||||
// upload image
|
||||
if data.Status == Finished {
|
||||
s.db.Model(&model.User{}).Where("id = ?", task.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
|
||||
// 解除任务锁定
|
||||
s.redis.Del(context.Background(), RunningJobKey)
|
||||
imgURL, err := s.uploadManager.GetUploadHandler().PutImg(data.Image.URL, true)
|
||||
if err != nil {
|
||||
logger.Error("error with download img: ", err.Error())
|
||||
return
|
||||
}
|
||||
job.ImgURL = imgURL
|
||||
s.db.Updates(&job)
|
||||
}
|
||||
|
||||
if data.Status == Finished {
|
||||
// update user's img calls
|
||||
s.db.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
|
||||
// release lock task
|
||||
atomic.AddInt32(&s.handledTaskNum, -1)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -32,6 +32,10 @@ func NewAliYunOss(appConfig *types.AppConfig) (*AliYunOss, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if config.SubDir == "" {
|
||||
config.SubDir = "gpt"
|
||||
}
|
||||
|
||||
return &AliYunOss{
|
||||
config: config,
|
||||
bucket: bucket,
|
||||
@@ -54,14 +58,14 @@ func (s AliYunOss) PutFile(ctx *gin.Context, name string) (string, error) {
|
||||
defer src.Close()
|
||||
|
||||
fileExt := filepath.Ext(file.Filename)
|
||||
objectKey := fmt.Sprintf("%d%s", time.Now().UnixMicro(), fileExt)
|
||||
objectKey := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), fileExt)
|
||||
// 上传文件
|
||||
err = s.bucket.PutObject(objectKey, src)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("https://%s.%s/%s", s.config.Bucket, s.config.Endpoint, objectKey), nil
|
||||
return fmt.Sprintf("%s/%s", s.config.Domain, objectKey), nil
|
||||
}
|
||||
|
||||
func (s AliYunOss) PutImg(imageURL string, useProxy bool) (string, error) {
|
||||
@@ -80,18 +84,19 @@ func (s AliYunOss) PutImg(imageURL string, useProxy bool) (string, error) {
|
||||
return "", fmt.Errorf("error with parse image URL: %v", err)
|
||||
}
|
||||
fileExt := filepath.Ext(parse.Path)
|
||||
objectKey := fmt.Sprintf("%d%s", time.Now().UnixMicro(), fileExt)
|
||||
objectKey := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), fileExt)
|
||||
// 上传文件字节数据
|
||||
err = s.bucket.PutObject(objectKey, bytes.NewReader(imageData))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("https://%s.%s/%s", s.config.Bucket, s.config.Endpoint, objectKey), nil
|
||||
return fmt.Sprintf("%s/%s", s.config.Domain, objectKey), nil
|
||||
}
|
||||
|
||||
func (s AliYunOss) Delete(fileURL string) error {
|
||||
objectName := filepath.Base(fileURL)
|
||||
return s.bucket.DeleteObject(objectName)
|
||||
key := fmt.Sprintf("%s/%s", s.config.SubDir, objectName)
|
||||
return s.bucket.DeleteObject(key)
|
||||
}
|
||||
|
||||
var _ Uploader = AliYunOss{}
|
||||
|
||||
@@ -29,6 +29,9 @@ func NewMiniOss(appConfig *types.AppConfig) (MiniOss, error) {
|
||||
if err != nil {
|
||||
return MiniOss{}, err
|
||||
}
|
||||
if config.SubDir == "" {
|
||||
config.SubDir = "gpt"
|
||||
}
|
||||
return MiniOss{config: config, client: minioClient, proxyURL: appConfig.ProxyURL}, nil
|
||||
}
|
||||
|
||||
@@ -48,7 +51,7 @@ func (s MiniOss) PutImg(imageURL string, useProxy bool) (string, error) {
|
||||
return "", fmt.Errorf("error with parse image URL: %v", err)
|
||||
}
|
||||
fileExt := filepath.Ext(parse.Path)
|
||||
filename := fmt.Sprintf("%d%s", time.Now().UnixMicro(), fileExt)
|
||||
filename := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), fileExt)
|
||||
info, err := s.client.PutObject(
|
||||
context.Background(),
|
||||
s.config.Bucket,
|
||||
@@ -75,7 +78,7 @@ func (s MiniOss) PutFile(ctx *gin.Context, name string) (string, error) {
|
||||
defer fileReader.Close()
|
||||
|
||||
fileExt := filepath.Ext(file.Filename)
|
||||
filename := fmt.Sprintf("%d%s", time.Now().UnixMicro(), fileExt)
|
||||
filename := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), fileExt)
|
||||
info, err := s.client.PutObject(ctx, s.config.Bucket, filename, fileReader, file.Size, minio.PutObjectOptions{
|
||||
ContentType: file.Header.Get("Content-Type"),
|
||||
})
|
||||
@@ -88,7 +91,8 @@ func (s MiniOss) PutFile(ctx *gin.Context, name string) (string, error) {
|
||||
|
||||
func (s MiniOss) Delete(fileURL string) error {
|
||||
objectName := filepath.Base(fileURL)
|
||||
return s.client.RemoveObject(context.Background(), s.config.Bucket, objectName, minio.RemoveObjectOptions{})
|
||||
key := fmt.Sprintf("%s/%s", s.config.SubDir, objectName)
|
||||
return s.client.RemoveObject(context.Background(), s.config.Bucket, key, minio.RemoveObjectOptions{})
|
||||
}
|
||||
|
||||
var _ Uploader = MiniOss{}
|
||||
|
||||
@@ -21,7 +21,6 @@ type QinNiuOss struct {
|
||||
uploader *storage.FormUploader
|
||||
manager *storage.BucketManager
|
||||
proxyURL string
|
||||
dir string
|
||||
}
|
||||
|
||||
func NewQiNiuOss(appConfig *types.AppConfig) QinNiuOss {
|
||||
@@ -38,6 +37,9 @@ func NewQiNiuOss(appConfig *types.AppConfig) QinNiuOss {
|
||||
putPolicy := storage.PutPolicy{
|
||||
Scope: config.Bucket,
|
||||
}
|
||||
if config.SubDir == "" {
|
||||
config.SubDir = "gpt"
|
||||
}
|
||||
return QinNiuOss{
|
||||
config: config,
|
||||
mac: mac,
|
||||
@@ -45,7 +47,6 @@ func NewQiNiuOss(appConfig *types.AppConfig) QinNiuOss {
|
||||
uploader: formUploader,
|
||||
manager: storage.NewBucketManager(mac, &storeConfig),
|
||||
proxyURL: appConfig.ProxyURL,
|
||||
dir: "chatgpt-plus",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +64,7 @@ func (s QinNiuOss) PutFile(ctx *gin.Context, name string) (string, error) {
|
||||
defer src.Close()
|
||||
|
||||
fileExt := filepath.Ext(file.Filename)
|
||||
key := fmt.Sprintf("%s/%d%s", s.dir, time.Now().UnixMicro(), fileExt)
|
||||
key := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), fileExt)
|
||||
// 上传文件
|
||||
ret := storage.PutRet{}
|
||||
extra := storage.PutExtra{}
|
||||
@@ -91,7 +92,7 @@ func (s QinNiuOss) PutImg(imageURL string, useProxy bool) (string, error) {
|
||||
return "", fmt.Errorf("error with parse image URL: %v", err)
|
||||
}
|
||||
fileExt := filepath.Ext(parse.Path)
|
||||
key := fmt.Sprintf("%s/%d%s", s.dir, time.Now().UnixMicro(), fileExt)
|
||||
key := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), fileExt)
|
||||
ret := storage.PutRet{}
|
||||
extra := storage.PutExtra{}
|
||||
// 上传文件字节数据
|
||||
@@ -104,7 +105,7 @@ func (s QinNiuOss) PutImg(imageURL string, useProxy bool) (string, error) {
|
||||
|
||||
func (s QinNiuOss) Delete(fileURL string) error {
|
||||
objectName := filepath.Base(fileURL)
|
||||
key := fmt.Sprintf("%s/%s", s.dir, objectName)
|
||||
key := fmt.Sprintf("%s/%s", s.config.SubDir, objectName)
|
||||
return s.manager.Delete(s.config.Bucket, key)
|
||||
}
|
||||
|
||||
|
||||
72
api/service/payment/hupipay_serive.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"chatplus/core/types"
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type HuPiPayService struct {
|
||||
appId string
|
||||
appSecret string
|
||||
host string
|
||||
}
|
||||
|
||||
func NewHuPiPay(config *types.AppConfig) *HuPiPayService {
|
||||
return &HuPiPayService{
|
||||
appId: config.HuPiPayConfig.AppId,
|
||||
appSecret: config.HuPiPayConfig.AppSecret,
|
||||
host: config.HuPiPayConfig.PayURL,
|
||||
}
|
||||
}
|
||||
|
||||
// Pay 执行支付请求操作
|
||||
func (s *HuPiPayService) Pay(params map[string]string) (string, error) {
|
||||
data := url.Values{}
|
||||
simple := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
params["appid"] = s.appId
|
||||
params["time"] = simple
|
||||
params["nonce_str"] = simple
|
||||
for k, v := range params {
|
||||
data.Add(k, v)
|
||||
}
|
||||
data.Add("hash", s.Sign(params))
|
||||
resp, err := http.PostForm(s.host, data)
|
||||
if err != nil {
|
||||
return "error", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
all, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "error", err
|
||||
}
|
||||
return string(all), err
|
||||
}
|
||||
|
||||
// Sign 签名方法
|
||||
func (s *HuPiPayService) Sign(params map[string]string) string {
|
||||
var data string
|
||||
keys := make([]string, 0, 0)
|
||||
params["appid"] = s.appId
|
||||
for key := range params {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
//拼接
|
||||
for _, k := range keys {
|
||||
data = fmt.Sprintf("%s%s=%s&", data, k, params[k])
|
||||
}
|
||||
data = strings.Trim(data, "&")
|
||||
data = fmt.Sprintf("%s%s", data, s.appSecret)
|
||||
m := md5.New()
|
||||
m.Write([]byte(data))
|
||||
sign := fmt.Sprintf("%x", m.Sum(nil))
|
||||
return sign
|
||||
}
|
||||
52
api/service/sd/pool.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package sd
|
||||
|
||||
import (
|
||||
"chatplus/core/types"
|
||||
"chatplus/service/oss"
|
||||
"chatplus/store"
|
||||
"fmt"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ServicePool struct {
|
||||
services []*Service
|
||||
taskQueue *store.RedisQueue
|
||||
}
|
||||
|
||||
func NewServicePool(db *gorm.DB, redisCli *redis.Client, manager *oss.UploaderManager, appConfig *types.AppConfig) *ServicePool {
|
||||
services := make([]*Service, 0)
|
||||
queue := store.NewRedisQueue("StableDiffusion_Task_Queue", redisCli)
|
||||
// create mj client and service
|
||||
for k, config := range appConfig.SdConfigs {
|
||||
if config.Enabled == false {
|
||||
continue
|
||||
}
|
||||
|
||||
// create sd service
|
||||
name := fmt.Sprintf("StableDifffusion Service-%d", k)
|
||||
service := NewService(name, 4, 600, &config, queue, db, manager)
|
||||
// run sd service
|
||||
go func() {
|
||||
service.Run()
|
||||
}()
|
||||
|
||||
services = append(services, service)
|
||||
}
|
||||
|
||||
return &ServicePool{
|
||||
taskQueue: queue,
|
||||
services: services,
|
||||
}
|
||||
}
|
||||
|
||||
// PushTask push a new mj task in to task queue
|
||||
func (p *ServicePool) PushTask(task types.SdTask) {
|
||||
logger.Debugf("add a new MidJourney task to the task list: %+v", task)
|
||||
p.taskQueue.RPush(task)
|
||||
}
|
||||
|
||||
// HasAvailableService check if it has available mj service in pool
|
||||
func (p *ServicePool) HasAvailableService() bool {
|
||||
return len(p.services) > 0
|
||||
}
|
||||
@@ -5,84 +5,96 @@ import (
|
||||
"chatplus/service/oss"
|
||||
"chatplus/store"
|
||||
"chatplus/store/model"
|
||||
"chatplus/store/vo"
|
||||
"chatplus/utils"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/imroc/req/v3"
|
||||
"gorm.io/gorm"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SD 绘画服务
|
||||
|
||||
const RunningJobKey = "StableDiffusion_Running_Job"
|
||||
|
||||
type Service struct {
|
||||
httpClient *req.Client
|
||||
config *types.StableDiffusionConfig
|
||||
taskQueue *store.RedisQueue
|
||||
redis *redis.Client
|
||||
db *gorm.DB
|
||||
uploadManager *oss.UploaderManager
|
||||
Clients *types.LMap[string, *types.WsClient] // SD 绘画页面 websocket 连接池
|
||||
httpClient *req.Client
|
||||
config *types.StableDiffusionConfig
|
||||
taskQueue *store.RedisQueue
|
||||
db *gorm.DB
|
||||
uploadManager *oss.UploaderManager
|
||||
name string // service name
|
||||
maxHandleTaskNum int32 // max task number current service can handle
|
||||
handledTaskNum int32 // already handled task number
|
||||
taskStartTimes map[int]time.Time // task start time, to check if the task is timeout
|
||||
taskTimeout int64
|
||||
}
|
||||
|
||||
func NewService(config *types.AppConfig, redisCli *redis.Client, db *gorm.DB, manager *oss.UploaderManager) *Service {
|
||||
func NewService(name string, maxTaskNum int32, timeout int64, config *types.StableDiffusionConfig, queue *store.RedisQueue, db *gorm.DB, manager *oss.UploaderManager) *Service {
|
||||
return &Service{
|
||||
config: &config.SdConfig,
|
||||
httpClient: req.C(),
|
||||
redis: redisCli,
|
||||
db: db,
|
||||
uploadManager: manager,
|
||||
Clients: types.NewLMap[string, *types.WsClient](),
|
||||
taskQueue: store.NewRedisQueue("stable_diffusion_task_queue", redisCli),
|
||||
name: name,
|
||||
config: config,
|
||||
httpClient: req.C(),
|
||||
taskQueue: queue,
|
||||
db: db,
|
||||
uploadManager: manager,
|
||||
taskTimeout: timeout,
|
||||
maxHandleTaskNum: maxTaskNum,
|
||||
taskStartTimes: make(map[int]time.Time),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Run() {
|
||||
logger.Info("Starting StableDiffusion job consumer.")
|
||||
ctx := context.Background()
|
||||
for {
|
||||
_, err := s.redis.Get(ctx, RunningJobKey).Result()
|
||||
if err == nil { // 队列串行执行
|
||||
s.checkTasks()
|
||||
if !s.canHandleTask() {
|
||||
// current service is full, can not handle more task
|
||||
// waiting for running task finish
|
||||
time.Sleep(time.Second * 3)
|
||||
continue
|
||||
}
|
||||
|
||||
var task types.SdTask
|
||||
err = s.taskQueue.LPop(&task)
|
||||
err := s.taskQueue.LPop(&task)
|
||||
if err != nil {
|
||||
logger.Errorf("taking task with error: %v", err)
|
||||
continue
|
||||
}
|
||||
logger.Infof("Consuming Task: %+v", task)
|
||||
logger.Infof("%s handle a new Stable-Diffusion task: %+v", s.name, task)
|
||||
err = s.Txt2Img(task)
|
||||
if err != nil {
|
||||
logger.Error("绘画任务执行失败:", err)
|
||||
if task.RetryCount <= 5 {
|
||||
s.taskQueue.RPush(task)
|
||||
}
|
||||
task.RetryCount += 1
|
||||
time.Sleep(time.Second * 3)
|
||||
// update the task progress
|
||||
s.db.Model(&model.SdJob{Id: uint(task.Id)}).UpdateColumn("progress", -1)
|
||||
// release task num
|
||||
atomic.AddInt32(&s.handledTaskNum, -1)
|
||||
continue
|
||||
}
|
||||
|
||||
// 更新任务的执行状态
|
||||
s.db.Model(&model.SdJob{}).Where("id = ?", task.Id).UpdateColumn("started", true)
|
||||
// 锁定任务执行通道,直到任务超时(5分钟)
|
||||
s.redis.Set(ctx, RunningJobKey, utils.JsonEncode(task), time.Minute*5)
|
||||
// lock the task until the execute timeout
|
||||
s.taskStartTimes[task.Id] = time.Now()
|
||||
atomic.AddInt32(&s.handledTaskNum, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// PushTask 推送任务到队列
|
||||
func (s *Service) PushTask(task types.SdTask) {
|
||||
logger.Infof("add a new Stable Diffusion Task: %+v", task)
|
||||
s.taskQueue.RPush(task)
|
||||
// check if current service instance can handle more task
|
||||
func (s *Service) canHandleTask() bool {
|
||||
handledNum := atomic.LoadInt32(&s.handledTaskNum)
|
||||
return handledNum < s.maxHandleTaskNum
|
||||
}
|
||||
|
||||
// remove the expired tasks
|
||||
func (s *Service) checkTasks() {
|
||||
for k, t := range s.taskStartTimes {
|
||||
if time.Now().Unix()-t.Unix() > s.taskTimeout {
|
||||
delete(s.taskStartTimes, k)
|
||||
atomic.AddInt32(&s.handledTaskNum, -1)
|
||||
// delete task from database
|
||||
s.db.Delete(&model.MidJourneyJob{Id: uint(k)}, "progress < 100")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Txt2Img 文生图 API
|
||||
@@ -135,7 +147,6 @@ func (s *Service) runTask(taskInfo TaskInfo, client *req.Client) {
|
||||
"fn_index": taskInfo.FnIndex,
|
||||
"session_hash": taskInfo.SessionHash,
|
||||
}
|
||||
logger.Debug(utils.JsonEncode(body))
|
||||
var result = make(chan CBReq)
|
||||
go func() {
|
||||
var res struct {
|
||||
@@ -231,7 +242,6 @@ func (s *Service) runTask(taskInfo TaskInfo, client *req.Client) {
|
||||
|
||||
cbReq.ImageData = progressRes.LivePreview
|
||||
cbReq.Progress = int(progressRes.Progress * 100)
|
||||
logger.Debug(cbReq)
|
||||
s.callback(cbReq)
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
@@ -239,9 +249,8 @@ func (s *Service) runTask(taskInfo TaskInfo, client *req.Client) {
|
||||
}
|
||||
|
||||
func (s *Service) callback(data CBReq) {
|
||||
// 释放任务锁
|
||||
s.redis.Del(context.Background(), RunningJobKey)
|
||||
client := s.Clients.Get(data.SessionId)
|
||||
// release task num
|
||||
atomic.AddInt32(&s.handledTaskNum, -1)
|
||||
if data.Success { // 任务成功
|
||||
var job model.SdJob
|
||||
res := s.db.Where("id = ?", data.JobId).First(&job)
|
||||
@@ -261,13 +270,15 @@ func (s *Service) callback(data CBReq) {
|
||||
|
||||
params.Seed = data.Seed
|
||||
if data.ImageName != "" { // 下载图片
|
||||
imageURL := fmt.Sprintf("%s/file=%s", s.config.ApiURL, data.ImageName)
|
||||
imageURL, err := s.uploadManager.GetUploadHandler().PutImg(imageURL, false)
|
||||
if err != nil {
|
||||
logger.Error("error with download img: ", err.Error())
|
||||
return
|
||||
job.ImgURL = fmt.Sprintf("%s/file=%s", s.config.ApiURL, data.ImageName)
|
||||
if data.Progress == 100 {
|
||||
imageURL, err := s.uploadManager.GetUploadHandler().PutImg(job.ImgURL, false)
|
||||
if err != nil {
|
||||
logger.Error("error with download img: ", err.Error())
|
||||
return
|
||||
}
|
||||
job.ImgURL = imageURL
|
||||
}
|
||||
job.ImgURL = imageURL
|
||||
}
|
||||
|
||||
job.Params = utils.JsonEncode(params)
|
||||
@@ -277,33 +288,16 @@ func (s *Service) callback(data CBReq) {
|
||||
return
|
||||
}
|
||||
|
||||
var jobVo vo.SdJob
|
||||
err = utils.CopyObject(job, &jobVo)
|
||||
if err != nil {
|
||||
logger.Error("error with copy object: ", err)
|
||||
return
|
||||
logger.Debugf("绘图进度:%d", data.Progress)
|
||||
|
||||
// 扣减绘图次数
|
||||
if data.Progress == 100 {
|
||||
s.db.Model(&model.User{}).Where("id = ? AND img_calls > 0", job.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
|
||||
}
|
||||
|
||||
if data.Progress < 100 && data.ImageData != "" {
|
||||
jobVo.ImgURL = data.ImageData
|
||||
}
|
||||
// 扣减绘图次数
|
||||
s.db.Model(&model.User{}).Where("id = ?", jobVo.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
|
||||
// 推送任务到前端
|
||||
if client != nil {
|
||||
utils.ReplyChunkMessage(client, jobVo)
|
||||
}
|
||||
} else { // 任务失败
|
||||
logger.Error("任务执行失败:", data.Message)
|
||||
// 删除任务
|
||||
s.db.Delete(&model.SdJob{Id: uint(data.JobId)})
|
||||
// 推送消息到前端
|
||||
if client != nil {
|
||||
utils.ReplyChunkMessage(client, vo.SdJob{
|
||||
Id: uint(data.JobId),
|
||||
Progress: -1,
|
||||
TaskId: data.TaskId,
|
||||
})
|
||||
}
|
||||
// update the task progress
|
||||
s.db.Model(&model.SdJob{Id: uint(data.JobId)}).UpdateColumn("progress", -1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ func NewSnowflake() *Snowflake {
|
||||
}
|
||||
|
||||
// Next 生成一个新的唯一ID
|
||||
func (s *Snowflake) Next() (string, error) {
|
||||
func (s *Snowflake) Next(raw bool) (string, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
@@ -43,6 +43,9 @@ func (s *Snowflake) Next() (string, error) {
|
||||
|
||||
s.lastTimestamp = timestamp
|
||||
id := (timestamp << 22) | (int64(s.workerID) << 10) | int64(s.sequence)
|
||||
if raw {
|
||||
return fmt.Sprintf("%d", id), nil
|
||||
}
|
||||
now := time.Now()
|
||||
return fmt.Sprintf("%d%02d%02d%d", now.Year(), now.Month(), now.Day(), id), nil
|
||||
}
|
||||
|
||||
@@ -63,7 +63,8 @@ func (e *XXLJobExecutor) ClearOrders(cxt context.Context, param *xxl.RunReq) (ms
|
||||
}
|
||||
timeout := time.Now().Unix() - int64(config.OrderPayTimeout)
|
||||
start := utils.Stamp2str(timeout)
|
||||
res = e.db.Where("status != ? AND created_at < ?", types.OrderPaidSuccess, start).Delete(&model.Order{})
|
||||
// 这里不是用软删除,而是永久删除订单
|
||||
res = e.db.Unscoped().Where("status != ? AND created_at < ?", types.OrderPaidSuccess, start).Delete(&model.Order{})
|
||||
return fmt.Sprintf("Clear order successfully, affect rows: %d", res.RowsAffected)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ package model
|
||||
type ApiKey struct {
|
||||
BaseModel
|
||||
Platform string
|
||||
Type string // 用途 chat => 聊天,img => 绘图
|
||||
Value string // API Key 的值
|
||||
LastUsedAt int64 // 最后使用时间
|
||||
}
|
||||
|
||||
12
api/store/model/invite_code.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type InviteCode struct {
|
||||
Id uint `gorm:"primarykey;column:id"`
|
||||
UserId uint
|
||||
Code string
|
||||
Hits int // 点击次数
|
||||
RegNum int // 注册人数
|
||||
CreatedAt time.Time
|
||||
}
|
||||
15
api/store/model/invite_log.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type InviteLog struct {
|
||||
Id uint `gorm:"primarykey;column:id"`
|
||||
InviterId uint
|
||||
UserId uint
|
||||
Username string
|
||||
InviteCode string
|
||||
Reward string `gorm:"column:reward_json"` // 邀请奖励
|
||||
CreatedAt time.Time
|
||||
}
|
||||
@@ -6,13 +6,14 @@ type MidJourneyJob struct {
|
||||
Id uint `gorm:"primarykey;column:id"`
|
||||
Type string
|
||||
UserId int
|
||||
TaskId string
|
||||
MessageId string
|
||||
ReferenceId string
|
||||
ImgURL string
|
||||
OrgURL string // 原图地址
|
||||
Hash string // message hash
|
||||
Progress int
|
||||
Prompt string
|
||||
Started bool
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ type SdJob struct {
|
||||
Progress int
|
||||
Prompt string
|
||||
Params string
|
||||
Started bool
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,9 @@ func NewGormConfig() *gorm.Config {
|
||||
|
||||
func NewMysql(config *gorm.Config, appConfig *types.AppConfig) (*gorm.DB, error) {
|
||||
db, err := gorm.Open(mysql.Open(appConfig.MysqlDns), config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
@@ -29,8 +32,6 @@ func NewMysql(config *gorm.Config, appConfig *types.AppConfig) (*gorm.DB, error)
|
||||
sqlDB.SetMaxIdleConns(32)
|
||||
sqlDB.SetMaxOpenConns(512)
|
||||
sqlDB.SetConnMaxLifetime(time.Hour)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ package vo
|
||||
type ApiKey struct {
|
||||
BaseVo
|
||||
Platform string `json:"platform"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value"` // API Key 的值
|
||||
LastUsedAt int64 `json:"last_used_at"` // 最后使用时间
|
||||
}
|
||||
|
||||
10
api/store/vo/invite_code.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package vo
|
||||
|
||||
type InviteCode struct {
|
||||
Id uint `json:"id"`
|
||||
UserId uint `json:"user_id"`
|
||||
Code string `json:"code"`
|
||||
Hits int `json:"hits"`
|
||||
RegNum int `json:"reg_num"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
15
api/store/vo/invite_log.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package vo
|
||||
|
||||
import (
|
||||
"chatplus/core/types"
|
||||
)
|
||||
|
||||
type InviteLog struct {
|
||||
Id uint `json:"id"`
|
||||
InviterId uint `json:"inviter_id"`
|
||||
UserId uint `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
InviteCode string `json:"invite_code"`
|
||||
Reward types.InviteReward `json:"reward"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
@@ -6,12 +6,13 @@ type MidJourneyJob struct {
|
||||
Id uint `json:"id"`
|
||||
Type string `json:"type"`
|
||||
UserId int `json:"user_id"`
|
||||
TaskId string `json:"task_id"`
|
||||
MessageId string `json:"message_id"`
|
||||
ReferenceId string `json:"reference_id"`
|
||||
ImgURL string `json:"img_url"`
|
||||
OrgURL string `json:"org_url"`
|
||||
Hash string `json:"hash"`
|
||||
Progress int `json:"progress"`
|
||||
Prompt string `json:"prompt"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Started bool `json:"started"`
|
||||
}
|
||||
|
||||
@@ -15,5 +15,4 @@ type SdJob struct {
|
||||
Progress int `json:"progress"`
|
||||
Prompt string `json:"prompt"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Started bool `json:"started"`
|
||||
}
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func main() {
|
||||
value, err := strconv.Atoi("012345")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(value)
|
||||
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
|
||||
"github.com/nfnt/resize"
|
||||
"github.com/skip2/go-qrcode"
|
||||
"image"
|
||||
@@ -14,8 +15,6 @@ import (
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
|
||||
)
|
||||
|
||||
// CopyObject 拷贝对象
|
||||
@@ -60,6 +59,8 @@ func CopyObject(src interface{}, dst interface{}) error {
|
||||
value.Set(reflect.ValueOf(""))
|
||||
}
|
||||
}
|
||||
} else if field.Type.Kind() != value.Type().Kind() { // 不同类型的字段过滤掉
|
||||
continue
|
||||
} else { // 简单数据类型的强制类型转换
|
||||
switch value.Kind() {
|
||||
case reflect.Int:
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
#!/bin/bash
|
||||
|
||||
version=$1
|
||||
# build go api
|
||||
arch=${2:-amd64}
|
||||
|
||||
# build go api program
|
||||
cd ../api
|
||||
make clean linux
|
||||
make clean $arch
|
||||
|
||||
# build web app
|
||||
cd ../web
|
||||
npm run build
|
||||
|
||||
cd ../docker
|
||||
cd ../build
|
||||
|
||||
# remove docker image if exists
|
||||
docker rmi -f registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-api:$version
|
||||
docker rmi -f registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-api:$version-$arch
|
||||
# build docker image for chatgpt-plus-go
|
||||
docker build -t registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-api:$version -f dockerfile-api-go ../
|
||||
docker build -t registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-api:$version-$arch -f dockerfile-api-go ../
|
||||
|
||||
# build docker image for chatgpt-plus-vue
|
||||
docker rmi -f registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-web:$version
|
||||
docker build --platform linux/amd64 -t registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-web:$version -f dockerfile-vue ../
|
||||
docker rmi -f registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-web:$version-$arch
|
||||
docker build --platform linux/amd64 -t registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-web:$version-$arch -f dockerfile-vue ../
|
||||
|
||||
if [ "$2" = "push" ];then
|
||||
docker push registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-api:$version
|
||||
docker push registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-web:$version
|
||||
fi
|
||||
if [ "$3" = "push" ];then
|
||||
docker push registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-api:$version-$arch
|
||||
docker push registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-web:$version-$arch
|
||||
fi
|
||||
@@ -4,9 +4,9 @@ FROM alpine:3.18.2
|
||||
MAINTAINER yangjian<yangjian102621@163.com>
|
||||
|
||||
WORKDIR /var/www/app
|
||||
COPY ./api/bin/chatgpt-plus-amd64-linux /var/www/app
|
||||
COPY ./api/bin/chatgpt-plus-linux /var/www/app
|
||||
|
||||
EXPOSE 5678
|
||||
|
||||
# 容器启动时执行的命令
|
||||
CMD ["./chatgpt-plus-amd64-linux"]
|
||||
CMD ["./chatgpt-plus-linux"]
|
||||
1603
database/chatgpt_plus-v3.1.9.sql
Normal file
28
database/update-v3.2.0.sql
Normal file
@@ -0,0 +1,28 @@
|
||||
|
||||
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(11) 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 '邀请码',
|
||||
`calls` smallint NOT NULL COMMENT '奖励对话次数',
|
||||
`created_at` datetime NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='邀请注册日志';
|
||||
ALTER TABLE `chatgpt_invite_logs` ADD PRIMARY KEY (`id`);
|
||||
ALTER TABLE `chatgpt_invite_logs` MODIFY `id` int NOT NULL AUTO_INCREMENT;
|
||||
ALTER TABLE `chatgpt_invite_logs` CHANGE `calls` `reward_json` TEXT NOT NULL COMMENT '邀请奖励';
|
||||
|
||||
|
||||
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='用户邀请码';
|
||||
ALTER TABLE `chatgpt_invite_codes` ADD PRIMARY KEY (`id`);
|
||||
ALTER TABLE `chatgpt_invite_codes` MODIFY `id` int NOT NULL AUTO_INCREMENT;
|
||||
ALTER TABLE `chatgpt_invite_codes` ADD UNIQUE(`code`);
|
||||
|
||||
ALTER TABLE `chatgpt_api_keys` ADD `type` VARCHAR(10) NOT NULL DEFAULT 'chat' COMMENT '用途(chat=>聊天,img=>图片)' AFTER `value`;
|
||||
1
docker/.gitignore → deploy/.gitignore
vendored
@@ -2,3 +2,4 @@ mysql/data/*
|
||||
mysql/logs/*
|
||||
logs
|
||||
static/*
|
||||
redis/data/*
|
||||
@@ -1,6 +1,6 @@
|
||||
Listen = "0.0.0.0:5678"
|
||||
ProxyURL = "" # 如 http://127.0.0.1:7777
|
||||
MysqlDns = "root:12345678@tcp(172.22.11.200:3307)/chatgpt_plus?charset=utf8&parseTime=True&loc=Local"
|
||||
MysqlDns = "root:12345678@tcp(chatgpt-plus-mysql:3306)/chatgpt_plus?charset=utf8&parseTime=True&loc=Local"
|
||||
StaticDir = "./static" # 静态资源的目录
|
||||
StaticUrl = "/static" # 静态资源访问 URL
|
||||
AesEncryptKey = ""
|
||||
@@ -15,9 +15,9 @@ WeChatBot = false
|
||||
Password = "admin123" # 如果是生产环境的话,这里管理员的密码记得修改
|
||||
|
||||
[Redis] # redis 配置信息
|
||||
Host = "localhost"
|
||||
Host = "chatgpt-plus-redis"
|
||||
Port = 6379
|
||||
Password = ""
|
||||
Password = "12345678"
|
||||
DB = 0
|
||||
|
||||
[ApiConfig] # 微博热搜,今日头条等函数服务 API 配置,此为第三方插件服务,如需使用请联系作者开通
|
||||
@@ -37,7 +37,7 @@ WeChatBot = false
|
||||
Active = "local" # 默认使用本地文件存储引擎
|
||||
[OSS.Local]
|
||||
BasePath = "./static/upload" # 本地文件上传根路径
|
||||
BaseURL = "http://localhost:5678/static/upload" # 本地上传文件根 URL 如果是线上,则直接设置为 /static/upload 即可
|
||||
BaseURL = "/static/upload" # 本地上传文件根 URL 如果是线上,则直接设置为 /static/upload 即可
|
||||
[OSS.Minio]
|
||||
Endpoint = "" # 如 172.22.11.200:9000
|
||||
AccessKey = "" # 自己去 Minio 控制台去创建一个 Access Key
|
||||
@@ -67,7 +67,7 @@ WeChatBot = false
|
||||
|
||||
[XXLConfig] # xxl-job 配置,需要你部署 XXL-JOB 定时任务工具,用来定期清理未支付订单和清理过期 VIP,如果你没有启用支付服务,则该服务也无需启动
|
||||
Enabled = false # 是否启用 XXL JOB 服务
|
||||
ServerAddr = "http://172.22.11.47:8080/xxl-job-admin" # xxl-job-admin 管理地址
|
||||
ServerAddr = "http://chatgpt-plus-xxl-job:8080/xxl-job-admin" # xxl-job-admin 管理地址
|
||||
ExecutorIp = "172.22.11.47" # 执行器 IP 地址
|
||||
ExecutorPort = "9999" # 执行器服务端口
|
||||
AccessToken = "xxl-job-api-token" # 执行器 API 通信 token
|
||||
@@ -82,4 +82,12 @@ WeChatBot = false
|
||||
PublicKey = "certs/alipay/appPublicCert.crt" # 应用公钥证书
|
||||
AlipayPublicKey = "certs/alipay/alipayPublicCert.crt" # 支付宝公钥证书
|
||||
RootCert = "certs/alipay/alipayRootCert.crt" # 支付宝根证书
|
||||
NotifyURL = "http://r9it.com:6004/api/payment/alipay/notify" # 支付异步回调地址
|
||||
NotifyURL = "http://ai.r9it.com/api/payment/alipay/notify" # 支付异步回调地址
|
||||
|
||||
[HuPiPayConfig] # 虎皮椒支付配置
|
||||
Enabled = false
|
||||
Name = "wechat"
|
||||
AppId = ""
|
||||
AppSecret = ""
|
||||
PayURL = "https://api.xunhupay.com/payment/do.html"
|
||||
NotifyURL = "http://ai.r9it.com/api/payment/hupipay/notify"
|
||||
@@ -35,12 +35,12 @@ server {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_pass http://172.22.11.47:5678; # 这里改成后端服务的内网 IP 地址
|
||||
proxy_pass http://chatgpt-plus-api:5678;
|
||||
}
|
||||
|
||||
# 静态资源转发
|
||||
location /static/ {
|
||||
proxy_pass http://172.22.11.47:5678; # 这里改成后端服务的内网 IP 地址
|
||||
proxy_pass http://chatgpt-plus-api:5678;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,40 @@
|
||||
version: '3'
|
||||
services:
|
||||
# mysql
|
||||
chatgpt-plus-mysql:
|
||||
image: mysql:8.0.33
|
||||
container_name: chatgpt-plus-mysql
|
||||
command: --default-authentication-plugin=mysql_native_password
|
||||
restart: always
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=12345678
|
||||
ports:
|
||||
- "3307:3306"
|
||||
volumes :
|
||||
- ./mysql/conf/my.cnf:/etc/mysql/my.cnf
|
||||
- ./mysql/data:/var/lib/mysql
|
||||
- ./mysql/logs:/var/log/mysql
|
||||
- ./mysql/init.d:/docker-entrypoint-initdb.d/
|
||||
|
||||
# redis
|
||||
chatgpt-plus-redis:
|
||||
image: redis:6.0.16
|
||||
restart: always
|
||||
container_name: chatgpt-plus-redis
|
||||
command: redis-server --requirepass 12345678
|
||||
volumes :
|
||||
- ./redis/data:/data
|
||||
ports:
|
||||
- "6380:6379"
|
||||
|
||||
# 后端 API 程序
|
||||
chatgpt-plus-api:
|
||||
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-api:v3.1.5
|
||||
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-api:v3.1.8.1
|
||||
container_name: chatgpt-plus-api
|
||||
restart: always
|
||||
depends_on:
|
||||
- chatgpt-plus-mysql
|
||||
- chatgpt-plus-redis
|
||||
environment:
|
||||
- DEBUG=false
|
||||
- LOG_LEVEL=info
|
||||
@@ -20,9 +50,11 @@ services:
|
||||
|
||||
# 前端应用
|
||||
chatgpt-plus-web:
|
||||
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-web:v3.1.5
|
||||
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-web:v3.1.8.1
|
||||
container_name: chatgpt-plus-web
|
||||
restart: always
|
||||
depends_on:
|
||||
- chatgpt-plus-api
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
1603
deploy/mysql/init.d/chatgpt_plus-v3.1.9.sql
Normal file
@@ -1,14 +0,0 @@
|
||||
version: '3'
|
||||
services:
|
||||
minio:
|
||||
image: minio/minio
|
||||
container_name: minio
|
||||
volumes:
|
||||
- ./data:/data
|
||||
ports:
|
||||
- "9010:9000"
|
||||
- "9011:9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: minio
|
||||
MINIO_ROOT_PASSWORD: minio@pass
|
||||
command: server /data --console-address ":9001" --address ":9000"
|
||||
@@ -1,18 +0,0 @@
|
||||
version: '3'
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.0.33
|
||||
container_name: chatgpt-plus-mysql
|
||||
command: --default-authentication-plugin=mysql_native_password
|
||||
restart: always
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=12345678
|
||||
ports:
|
||||
- "3307:3306"
|
||||
volumes:
|
||||
- ./conf/my.cnf:/etc/mysql/my.cnf
|
||||
- ./data:/var/lib/mysql
|
||||
- ./logs:/var/log/mysql
|
||||
|
||||
|
||||
|
||||
323
web/package-lock.json
generated
@@ -21,6 +21,7 @@
|
||||
"markdown-it": "^13.0.1",
|
||||
"md-editor-v3": "^2.2.1",
|
||||
"pinia": "^2.1.4",
|
||||
"qrcode": "^1.5.3",
|
||||
"qs": "^6.11.1",
|
||||
"sortablejs": "^1.15.0",
|
||||
"v3-waterfall": "^1.2.1",
|
||||
@@ -3361,7 +3362,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -4763,6 +4763,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/decode-uri-component": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
|
||||
@@ -4973,6 +4981,11 @@
|
||||
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/dijkstrajs": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="
|
||||
},
|
||||
"node_modules/dir-glob": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||
@@ -5150,8 +5163,7 @@
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||
},
|
||||
"node_modules/emojis-list": {
|
||||
"version": "3.0.0",
|
||||
@@ -5162,6 +5174,11 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/encode-utf8": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/encode-utf8/-/encode-utf8-1.0.3.tgz",
|
||||
"integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw=="
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
@@ -6035,7 +6052,6 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz",
|
||||
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"locate-path": "^5.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
@@ -6181,7 +6197,6 @@
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
@@ -6720,7 +6735,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -7089,7 +7103,6 @@
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz",
|
||||
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"p-locate": "^4.1.0"
|
||||
},
|
||||
@@ -8115,7 +8128,6 @@
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz",
|
||||
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"p-try": "^2.0.0"
|
||||
},
|
||||
@@ -8127,7 +8139,6 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz",
|
||||
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"p-limit": "^2.2.0"
|
||||
},
|
||||
@@ -8152,7 +8163,6 @@
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz",
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
@@ -8238,7 +8248,6 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -8358,6 +8367,14 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pngjs": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/pngjs/-/pngjs-5.0.0.tgz",
|
||||
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/portfinder": {
|
||||
"version": "1.0.28",
|
||||
"resolved": "https://registry.npmmirror.com/portfinder/-/portfinder-1.0.28.tgz",
|
||||
@@ -9020,6 +9037,119 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode": {
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmmirror.com/qrcode/-/qrcode-1.5.3.tgz",
|
||||
"integrity": "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==",
|
||||
"dependencies": {
|
||||
"dijkstrajs": "^1.0.1",
|
||||
"encode-utf8": "^1.0.3",
|
||||
"pngjs": "^5.0.0",
|
||||
"yargs": "^15.3.1"
|
||||
},
|
||||
"bin": {
|
||||
"qrcode": "bin/qrcode"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/camelcase": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz",
|
||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/cliui": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/cliui/-/cliui-6.0.0.tgz",
|
||||
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wrap-ansi": "^6.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"node_modules/qrcode/node_modules/wrap-ansi": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/y18n": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/y18n/-/y18n-4.0.3.tgz",
|
||||
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="
|
||||
},
|
||||
"node_modules/qrcode/node_modules/yargs": {
|
||||
"version": "15.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/yargs/-/yargs-15.4.1.tgz",
|
||||
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||
"dependencies": {
|
||||
"cliui": "^6.0.0",
|
||||
"decamelize": "^1.2.0",
|
||||
"find-up": "^4.1.0",
|
||||
"get-caller-file": "^2.0.1",
|
||||
"require-directory": "^2.1.1",
|
||||
"require-main-filename": "^2.0.0",
|
||||
"set-blocking": "^2.0.0",
|
||||
"string-width": "^4.2.0",
|
||||
"which-module": "^2.0.0",
|
||||
"y18n": "^4.0.0",
|
||||
"yargs-parser": "^18.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/yargs-parser": {
|
||||
"version": "18.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||
"dependencies": {
|
||||
"camelcase": "^5.0.0",
|
||||
"decamelize": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.11.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.1.tgz",
|
||||
@@ -9258,7 +9388,6 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -9272,6 +9401,11 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-main-filename": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
|
||||
},
|
||||
"node_modules/requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/requires-port/-/requires-port-1.0.0.tgz",
|
||||
@@ -9547,6 +9681,11 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
@@ -9852,7 +9991,6 @@
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
@@ -9866,7 +10004,6 @@
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
@@ -11194,6 +11331,11 @@
|
||||
"which": "bin/which"
|
||||
}
|
||||
},
|
||||
"node_modules/which-module": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/which-module/-/which-module-2.0.1.tgz",
|
||||
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="
|
||||
},
|
||||
"node_modules/wildcard": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/wildcard/-/wildcard-2.0.0.tgz",
|
||||
@@ -13972,8 +14114,7 @@
|
||||
"ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "3.2.1",
|
||||
@@ -15081,6 +15222,11 @@
|
||||
"ms": "2.1.2"
|
||||
}
|
||||
},
|
||||
"decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="
|
||||
},
|
||||
"decode-uri-component": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
|
||||
@@ -15242,6 +15388,11 @@
|
||||
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
|
||||
"dev": true
|
||||
},
|
||||
"dijkstrajs": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="
|
||||
},
|
||||
"dir-glob": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||
@@ -15399,8 +15550,7 @@
|
||||
"emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||
},
|
||||
"emojis-list": {
|
||||
"version": "3.0.0",
|
||||
@@ -15408,6 +15558,11 @@
|
||||
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
|
||||
"dev": true
|
||||
},
|
||||
"encode-utf8": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/encode-utf8/-/encode-utf8-1.0.3.tgz",
|
||||
"integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw=="
|
||||
},
|
||||
"encodeurl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
@@ -16104,7 +16259,6 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz",
|
||||
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"locate-path": "^5.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
@@ -16210,8 +16364,7 @@
|
||||
"get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
|
||||
},
|
||||
"get-intrinsic": {
|
||||
"version": "1.1.1",
|
||||
@@ -16639,8 +16792,7 @@
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
|
||||
},
|
||||
"is-glob": {
|
||||
"version": "4.0.3",
|
||||
@@ -16933,7 +17085,6 @@
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz",
|
||||
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"p-locate": "^4.1.0"
|
||||
}
|
||||
@@ -17749,7 +17900,6 @@
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz",
|
||||
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"p-try": "^2.0.0"
|
||||
}
|
||||
@@ -17758,7 +17908,6 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz",
|
||||
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"p-limit": "^2.2.0"
|
||||
}
|
||||
@@ -17776,8 +17925,7 @@
|
||||
"p-try": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz",
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
|
||||
},
|
||||
"param-case": {
|
||||
"version": "3.0.4",
|
||||
@@ -17852,8 +18000,7 @@
|
||||
"path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"dev": true
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="
|
||||
},
|
||||
"path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
@@ -17922,6 +18069,11 @@
|
||||
"find-up": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"pngjs": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/pngjs/-/pngjs-5.0.0.tgz",
|
||||
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="
|
||||
},
|
||||
"portfinder": {
|
||||
"version": "1.0.28",
|
||||
"resolved": "https://registry.npmmirror.com/portfinder/-/portfinder-1.0.28.tgz",
|
||||
@@ -18364,6 +18516,97 @@
|
||||
"integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
|
||||
"dev": true
|
||||
},
|
||||
"qrcode": {
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmmirror.com/qrcode/-/qrcode-1.5.3.tgz",
|
||||
"integrity": "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==",
|
||||
"requires": {
|
||||
"dijkstrajs": "^1.0.1",
|
||||
"encode-utf8": "^1.0.3",
|
||||
"pngjs": "^5.0.0",
|
||||
"yargs": "^15.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"camelcase": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz",
|
||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
|
||||
},
|
||||
"cliui": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/cliui/-/cliui-6.0.0.tgz",
|
||||
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||
"requires": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wrap-ansi": "^6.2.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"wrap-ansi": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||
"requires": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"y18n": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/y18n/-/y18n-4.0.3.tgz",
|
||||
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="
|
||||
},
|
||||
"yargs": {
|
||||
"version": "15.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/yargs/-/yargs-15.4.1.tgz",
|
||||
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||
"requires": {
|
||||
"cliui": "^6.0.0",
|
||||
"decamelize": "^1.2.0",
|
||||
"find-up": "^4.1.0",
|
||||
"get-caller-file": "^2.0.1",
|
||||
"require-directory": "^2.1.1",
|
||||
"require-main-filename": "^2.0.0",
|
||||
"set-blocking": "^2.0.0",
|
||||
"string-width": "^4.2.0",
|
||||
"which-module": "^2.0.0",
|
||||
"y18n": "^4.0.0",
|
||||
"yargs-parser": "^18.1.2"
|
||||
}
|
||||
},
|
||||
"yargs-parser": {
|
||||
"version": "18.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||
"requires": {
|
||||
"camelcase": "^5.0.0",
|
||||
"decamelize": "^1.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.11.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.1.tgz",
|
||||
@@ -18559,8 +18802,7 @@
|
||||
"require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="
|
||||
},
|
||||
"require-from-string": {
|
||||
"version": "2.0.2",
|
||||
@@ -18568,6 +18810,11 @@
|
||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||
"dev": true
|
||||
},
|
||||
"require-main-filename": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
|
||||
},
|
||||
"requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/requires-port/-/requires-port-1.0.0.tgz",
|
||||
@@ -18809,6 +19056,11 @@
|
||||
"send": "0.17.2"
|
||||
}
|
||||
},
|
||||
"set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
|
||||
},
|
||||
"setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
@@ -19077,7 +19329,6 @@
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
@@ -19088,7 +19339,6 @@
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
}
|
||||
@@ -20109,6 +20359,11 @@
|
||||
"isexe": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"which-module": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/which-module/-/which-module-2.0.1.tgz",
|
||||
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="
|
||||
},
|
||||
"wildcard": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/wildcard/-/wildcard-2.0.0.tgz",
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"markdown-it": "^13.0.1",
|
||||
"md-editor-v3": "^2.2.1",
|
||||
"pinia": "^2.1.4",
|
||||
"qrcode": "^1.5.3",
|
||||
"qs": "^6.11.1",
|
||||
"sortablejs": "^1.15.0",
|
||||
"v3-waterfall": "^1.2.1",
|
||||
|
||||
|
Before Width: | Height: | Size: 476 KiB After Width: | Height: | Size: 476 KiB |
BIN
web/public/images/mj/mj-v4.jpg
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
web/public/images/mj/mj-v5.1.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 526 KiB After Width: | Height: | Size: 526 KiB |
BIN
web/public/images/mj/mj-v5.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
web/public/images/mj/nj1.jpg
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
web/public/images/mj/nj2.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
web/public/images/mj/nj3.jpg
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
web/public/images/mj/nj4.jpg
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
web/public/images/mj/rate_16_9.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
web/public/images/mj/rate_1_1.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
web/public/images/mj/rate_1_2.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
web/public/images/mj/rate_2_1.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
web/public/images/mj/rate_3_4.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
web/public/images/mj/rate_4_3.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
web/public/images/mj/rate_9_16.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
153
web/src/App.vue
@@ -39,4 +39,157 @@ html, body {
|
||||
}
|
||||
}
|
||||
|
||||
/* 省略显示 */
|
||||
.ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.sl {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
.sl3 {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
overflow: hidden;
|
||||
}
|
||||
.sl4 {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 居中布局 */
|
||||
.auto_center{
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%,-50%);
|
||||
}
|
||||
.h_center{
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
-webkit-transform: translateY(-50%);
|
||||
}
|
||||
.w_center{
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
-webkit-transform: translateX(-50%);
|
||||
}
|
||||
|
||||
/* flex布局 */
|
||||
.flex-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.justify-start {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.justify-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.justify-around {
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.justify-evenly {
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.items-start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.items-end {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.items-baseline {
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.items-stretch {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.self-start {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.self-end {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.self-center {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.self-baseline {
|
||||
align-self: baseline;
|
||||
}
|
||||
|
||||
.self-stretch {
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
|
||||
.flex-auto {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.grow-0 {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.shrink {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.shrink-0 {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.shrink-1 {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .el-icon {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content {
|
||||
background-color: #383838;
|
||||
@@ -43,26 +42,62 @@
|
||||
padding: 8px 14px;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content:hover {
|
||||
background-color: #585858;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content .icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content .shape {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 5px;
|
||||
margin-bottom: 5px;
|
||||
border: 1px solid #c4c4c4;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content .shape.vertical {
|
||||
width: 12px;
|
||||
height: 20px;
|
||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content .shape.size9-16 {
|
||||
width: 9px;
|
||||
height: 16px;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content .shape.horizontal {
|
||||
height: 12px;
|
||||
width: 20px;
|
||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content .shape.size16-9 {
|
||||
height: 9px;
|
||||
width: 16px;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
margin: 4px 0 7px;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content .shape.size3-4 {
|
||||
width: 12px;
|
||||
height: 16px;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content .shape.size4-3 {
|
||||
height: 12px;
|
||||
width: 16px;
|
||||
position: relative;
|
||||
margin: 4px 0 4px;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content .shape.size2-3 {
|
||||
width: 11px;
|
||||
height: 16px;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content .shape.size3-2 {
|
||||
height: 11px;
|
||||
width: 16px;
|
||||
position: relative;
|
||||
margin: 4px 0 5px;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content .shape.size1-2 {
|
||||
width: 8px;
|
||||
height: 16px;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content .shape.size2-1 {
|
||||
height: 8px;
|
||||
width: 16px;
|
||||
position: relative;
|
||||
margin: 4px 0 8px;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .grid-content.active {
|
||||
color: #47fff1;
|
||||
@@ -76,6 +111,7 @@
|
||||
border: 1px solid #454545;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
@@ -98,10 +134,20 @@
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .form-item-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .form-item-inner .el-select {
|
||||
--el-select-input-focus-border-color: #47fff1;
|
||||
--el-input-focus-border-color: #47fff1;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .form-item-inner .el-input__wrapper {
|
||||
background: #383838;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .form-item-inner .el-input__inner {
|
||||
color: #fff;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .form-item-inner .el-icon {
|
||||
margin-left: 10px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line .img-uploader .el-upload {
|
||||
border: 1px dashed var(--el-border-color);
|
||||
@@ -123,19 +169,11 @@
|
||||
text-align: center;
|
||||
}
|
||||
.page-mj .inner .mj-box .mj-params .param-line.pt {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
.page-mj .inner .mj-box .submit-btn {
|
||||
padding: 10px 15px 0 15px;
|
||||
text-align: center;
|
||||
}
|
||||
.page-mj .inner .mj-box .submit-btn .el-button {
|
||||
width: 100%;
|
||||
}
|
||||
.page-mj .inner .mj-box .submit-btn .el-button span {
|
||||
color: #2d3a4b;
|
||||
}
|
||||
.page-mj .inner .el-form .el-form-item__label {
|
||||
color: #fff;
|
||||
}
|
||||
@@ -149,6 +187,86 @@
|
||||
color: #fff;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .el-tabs {
|
||||
--el-tabs-header-height: 55px;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .el-tabs__item {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .title-tabs .el-tabs__item.is-active {
|
||||
color: #47fff1;
|
||||
font-size: 18px;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .title-tabs .el-tabs__active-bar {
|
||||
background-color: #47fff1;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .title-tabs .el-tabs__content {
|
||||
padding: 10px 0;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .el-textarea {
|
||||
--el-input-focus-border-color: #47fff1;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .el-textarea__inner {
|
||||
background: transparent;
|
||||
color: #fff;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .el-input__wrapper {
|
||||
background: transparent;
|
||||
padding: 5px;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .text {
|
||||
margin-bottom: 10px;
|
||||
color: #6b778c;
|
||||
font-size: 15px;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .param-line.pt {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .form-item-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .form-item-inner .el-icon {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .el-form-item__label {
|
||||
color: #fff;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .img-uploader .el-upload {
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 300px;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .img-uploader .el-upload:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .img-uploader .el-upload .el-icon.uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .submit-btn {
|
||||
display: flex;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .submit-btn .el-button {
|
||||
width: 200px;
|
||||
}
|
||||
.page-mj .inner .task-list-box .task-list-inner .submit-btn .text-info {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
align-items: center;
|
||||
}
|
||||
.page-mj .inner .task-list-box .running-job-list .job-item {
|
||||
width: 100%;
|
||||
padding: 2px;
|
||||
@@ -253,5 +371,4 @@
|
||||
margin-left: 10px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,6 @@
|
||||
|
||||
.el-icon {
|
||||
position relative
|
||||
top 3px
|
||||
}
|
||||
|
||||
.grid-content {
|
||||
@@ -49,29 +48,72 @@
|
||||
padding 8px 14px
|
||||
display flex
|
||||
cursor pointer
|
||||
margin-bottom: 10px;
|
||||
|
||||
&:hover {
|
||||
background-color #585858
|
||||
}
|
||||
|
||||
.icon {
|
||||
width 20px
|
||||
height 20px
|
||||
margin-bottom 5px
|
||||
}
|
||||
|
||||
.shape {
|
||||
width 16px
|
||||
height 16px
|
||||
margin-right 5px
|
||||
margin-bottom 5px
|
||||
border 1px solid #C4C4C4
|
||||
border-radius 3px
|
||||
}
|
||||
|
||||
.shape.vertical {
|
||||
width 12px
|
||||
height 20px
|
||||
.shape.size9-16 {
|
||||
width 9px
|
||||
height 16px
|
||||
}
|
||||
|
||||
.shape.horizontal {
|
||||
height 12px
|
||||
width 20px
|
||||
.shape.size16-9 {
|
||||
height 9px
|
||||
width 16px
|
||||
position relative
|
||||
top 3px
|
||||
margin 4px 0 7px
|
||||
}
|
||||
|
||||
.shape.size3-4 {
|
||||
width 12px
|
||||
height 16px
|
||||
}
|
||||
|
||||
.shape.size4-3 {
|
||||
height 12px
|
||||
width 16px
|
||||
position relative
|
||||
margin 4px 0 4px
|
||||
}
|
||||
|
||||
.shape.size2-3 {
|
||||
width 11px
|
||||
height 16px
|
||||
}
|
||||
|
||||
.shape.size3-2 {
|
||||
height 11px
|
||||
width 16px
|
||||
position relative
|
||||
margin 4px 0 5px
|
||||
}
|
||||
|
||||
.shape.size1-2 {
|
||||
width 8px
|
||||
height 16px
|
||||
}
|
||||
|
||||
.shape.size2-1 {
|
||||
height 8px
|
||||
width 16px
|
||||
position relative
|
||||
margin 4px 0 8px
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +132,7 @@
|
||||
border 1px solid #454545
|
||||
border-radius 5px
|
||||
padding 10px
|
||||
margin-bottom 10px
|
||||
display flex
|
||||
flex-flow column
|
||||
align-items center
|
||||
@@ -118,14 +161,26 @@
|
||||
|
||||
.form-item-inner {
|
||||
display flex
|
||||
align-items: center
|
||||
|
||||
.el-select {
|
||||
--el-select-input-focus-border-color: #47FFF1;
|
||||
--el-input-focus-border-color: #47FFF1;
|
||||
}
|
||||
|
||||
.el-input__wrapper {
|
||||
background: #383838;
|
||||
}
|
||||
|
||||
.el-input__inner {
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
margin-left 10px
|
||||
margin-top 2px
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.img-uploader {
|
||||
.el-upload {
|
||||
border: 1px dashed var(--el-border-color);
|
||||
@@ -153,23 +208,12 @@
|
||||
}
|
||||
|
||||
.param-line.pt {
|
||||
display: flex
|
||||
align-items: center
|
||||
padding-top 5px
|
||||
padding-bottom 5px
|
||||
}
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
padding 10px 15px 0 15px
|
||||
text-align center
|
||||
|
||||
.el-button {
|
||||
width 100%
|
||||
|
||||
span {
|
||||
color #2D3A4B
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-form {
|
||||
@@ -192,6 +236,5 @@
|
||||
margin-left 10px
|
||||
cursor pointer
|
||||
position relative
|
||||
top 2px
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,86 @@
|
||||
color: #fff;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .el-tabs {
|
||||
--el-tabs-header-height: 55px;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .el-tabs__item {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .title-tabs .el-tabs__item.is-active {
|
||||
color: #47fff1;
|
||||
font-size: 18px;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .title-tabs .el-tabs__active-bar {
|
||||
background-color: #47fff1;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .title-tabs .el-tabs__content {
|
||||
padding: 10px 0;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .el-textarea {
|
||||
--el-input-focus-border-color: #47fff1;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .el-textarea__inner {
|
||||
background: transparent;
|
||||
color: #fff;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .el-input__wrapper {
|
||||
background: transparent;
|
||||
padding: 5px;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .text {
|
||||
margin-bottom: 10px;
|
||||
color: #6b778c;
|
||||
font-size: 15px;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .param-line.pt {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .form-item-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .form-item-inner .el-icon {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .el-form-item__label {
|
||||
color: #fff;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .img-uploader .el-upload {
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 300px;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .img-uploader .el-upload:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .img-uploader .el-upload .el-icon.uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .submit-btn {
|
||||
display: flex;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .submit-btn .el-button {
|
||||
width: 200px;
|
||||
}
|
||||
.page-sd .inner .task-list-box .task-list-inner .submit-btn .text-info {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
align-items: center;
|
||||
}
|
||||
.page-sd .inner .task-list-box .running-job-list .job-item {
|
||||
width: 100%;
|
||||
padding: 2px;
|
||||
|
||||
@@ -1,115 +1,186 @@
|
||||
.task-list-box {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
color: #fff;
|
||||
overflow-x: hidden;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
color: #fff;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.task-list-box .task-list-inner .el-tabs {
|
||||
--el-tabs-header-height: 55px;
|
||||
}
|
||||
.task-list-box .task-list-inner .el-tabs__item {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
}
|
||||
.task-list-box .task-list-inner .title-tabs .el-tabs__item.is-active {
|
||||
color: #47fff1;
|
||||
font-size: 18px;
|
||||
}
|
||||
.task-list-box .task-list-inner .title-tabs .el-tabs__active-bar {
|
||||
background-color: #47fff1;
|
||||
}
|
||||
.task-list-box .task-list-inner .title-tabs .el-tabs__content {
|
||||
padding: 10px 0;
|
||||
}
|
||||
.task-list-box .task-list-inner .el-textarea {
|
||||
--el-input-focus-border-color: #47fff1;
|
||||
}
|
||||
.task-list-box .task-list-inner .el-textarea__inner {
|
||||
background: transparent;
|
||||
color: #fff;
|
||||
}
|
||||
.task-list-box .task-list-inner .el-input__wrapper {
|
||||
background: transparent;
|
||||
padding: 5px;
|
||||
}
|
||||
.task-list-box .task-list-inner .text {
|
||||
margin-bottom: 10px;
|
||||
color: #6b778c;
|
||||
font-size: 15px;
|
||||
}
|
||||
.task-list-box .task-list-inner .param-line.pt {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
.task-list-box .task-list-inner .form-item-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.task-list-box .task-list-inner .form-item-inner .el-icon {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.task-list-box .task-list-inner .el-form-item__label {
|
||||
color: #fff;
|
||||
}
|
||||
.task-list-box .task-list-inner .img-uploader .el-upload {
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 300px;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.task-list-box .task-list-inner .img-uploader .el-upload:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
.task-list-box .task-list-inner .img-uploader .el-upload .el-icon.uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
.task-list-box .task-list-inner .submit-btn {
|
||||
display: flex;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.task-list-box .task-list-inner .submit-btn .el-button {
|
||||
width: 200px;
|
||||
}
|
||||
.task-list-box .task-list-inner .submit-btn .text-info {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.task-list-box .running-job-list .job-item {
|
||||
width: 100%;
|
||||
padding: 2px;
|
||||
background-color: #555;
|
||||
width: 100%;
|
||||
padding: 2px;
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
.task-list-box .running-job-list .job-item .job-item-inner {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.task-list-box .running-job-list .job-item .job-item-inner .progress {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.task-list-box .running-job-list .job-item .job-item-inner .progress span {
|
||||
font-size: 20px;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.task-list-box .finish-job-list .job-item {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid #666;
|
||||
padding: 6px;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s ease; /* 添加过渡效果 */
|
||||
}
|
||||
|
||||
.task-list-box .finish-job-list .job-item .opt .opt-line {
|
||||
margin: 6px 0;
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.task-list-box .finish-job-list .job-item .opt .opt-line ul {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
}
|
||||
|
||||
.task-list-box .finish-job-list .job-item .opt .opt-line ul li {
|
||||
margin-right: 10px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.task-list-box .finish-job-list .job-item .opt .opt-line ul li a {
|
||||
padding: 3px 0;
|
||||
width: 44px;
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
background-color: #4e5058;
|
||||
color: #fff;
|
||||
padding: 3px 0;
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
background-color: #4e5058;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.task-list-box .finish-job-list .job-item .opt .opt-line ul li a:hover {
|
||||
background-color: #6d6f78;
|
||||
background-color: #6d6f78;
|
||||
}
|
||||
|
||||
.task-list-box .finish-job-list .job-item .opt .opt-line ul .show-prompt {
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.task-list-box .finish-job-list .animate:hover {
|
||||
box-shadow: 0 0 10px rgba(71,255,241,0.6); /* 添加阴影效果 */
|
||||
transform: translateY(-10px); /* 向上移动10像素 */
|
||||
}
|
||||
|
||||
.task-list-box .el-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 240px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.task-list-box .el-image img {
|
||||
height: 240px;
|
||||
height: 240px;
|
||||
}
|
||||
|
||||
.task-list-box .el-image .el-image-viewer__wrapper img {
|
||||
width: auto;
|
||||
height: auto;
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.task-list-box .el-image .image-slot {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
min-height: 200px;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
min-height: 200px;
|
||||
color: #fff;
|
||||
height: 240px;
|
||||
}
|
||||
|
||||
.task-list-box .el-image .image-slot .iconfont {
|
||||
font-size: 50px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 50px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.task-list-box .el-image.upscale {
|
||||
max-height: 312px;
|
||||
max-height: 310px;
|
||||
}
|
||||
|
||||
.task-list-box .el-image.upscale img {
|
||||
height: 312px;
|
||||
height: 310px;
|
||||
}
|
||||
|
||||
.task-list-box .el-image.upscale .el-image-viewer__wrapper img {
|
||||
width: auto;
|
||||
height: auto;
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,109 @@
|
||||
color #ffffff
|
||||
overflow-x hidden
|
||||
|
||||
.task-list-inner {
|
||||
.el-tabs {
|
||||
--el-tabs-header-height: 55px;
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.title-tabs .el-tabs__item.is-active {
|
||||
color: #47FFF1;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.title-tabs .el-tabs__active-bar {
|
||||
background-color: #47FFF1;
|
||||
}
|
||||
|
||||
.title-tabs .el-tabs__content {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.el-textarea {
|
||||
--el-input-focus-border-color: #47FFF1;
|
||||
}
|
||||
|
||||
.el-textarea__inner {
|
||||
background: transparent;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.el-input__wrapper {
|
||||
background: transparent;
|
||||
padding 5px
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-bottom: 10px;
|
||||
color: #6b778c;
|
||||
font-size: 15px
|
||||
}
|
||||
|
||||
.param-line.pt {
|
||||
padding-top 5px
|
||||
padding-bottom 5px
|
||||
}
|
||||
|
||||
.form-item-inner {
|
||||
display flex
|
||||
align-items: center
|
||||
|
||||
.el-icon {
|
||||
margin-left 10px
|
||||
}
|
||||
}
|
||||
|
||||
.el-form-item__label {
|
||||
color #ffffff
|
||||
}
|
||||
|
||||
.img-uploader {
|
||||
.el-upload {
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width 300px;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
margin-bottom: 20px;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.el-icon.uploader-icon {
|
||||
font-size: 28px
|
||||
color: #8c939d
|
||||
width 100%
|
||||
height: 120px
|
||||
text-align: center
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
display flex
|
||||
margin: 20px 0
|
||||
|
||||
.el-button {
|
||||
width 200px
|
||||
}
|
||||
|
||||
.text-info {
|
||||
width 100%
|
||||
display flex
|
||||
justify-content right
|
||||
align-items center
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.running-job-list {
|
||||
.job-item {
|
||||
//border: 1px solid #454545;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 4125778 */
|
||||
src: url('iconfont.woff2?t=1697164072791') format('woff2'),
|
||||
url('iconfont.woff?t=1697164072791') format('woff'),
|
||||
url('iconfont.ttf?t=1697164072791') format('truetype');
|
||||
src: url('iconfont.woff2?t=1702024026523') format('woff2'),
|
||||
url('iconfont.woff?t=1702024026523') format('woff'),
|
||||
url('iconfont.ttf?t=1702024026523') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
@@ -13,6 +13,10 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-alipay:before {
|
||||
content: "\e634";
|
||||
}
|
||||
|
||||
.icon-face:before {
|
||||
content: "\e64b";
|
||||
}
|
||||
|
||||
@@ -5,6 +5,13 @@
|
||||
"css_prefix_text": "icon-",
|
||||
"description": "",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "1486848",
|
||||
"name": "支付宝支付",
|
||||
"font_class": "alipay",
|
||||
"unicode": "e634",
|
||||
"unicode_decimal": 58932
|
||||
},
|
||||
{
|
||||
"icon_id": "845789",
|
||||
"name": "笑脸",
|
||||
|
||||