Compare commits

..

25 Commits

Author SHA1 Message Date
RockYang
4a56621ec3 chore: add sub dir support for OSS 2023-12-13 17:02:49 +08:00
RockYang
a398e7a550 refactor: add midjourney pool implementation, add translate prompt for mj drawing 2023-12-13 16:38:27 +08:00
RockYang
96816c12ca fix: fixed bug for aliyun OSS img url 2023-12-13 09:49:55 +08:00
RockYang
9984926f69 refactor mj service, add mj service pool support 2023-12-12 18:33:24 +08:00
RockYang
a2a6081027 opt: remove default value for stable-diffusion page 2023-12-12 09:59:20 +08:00
RockYang
5a10ed37a7 docs: update readme file 2023-12-12 09:52:28 +08:00
RockYang
1a9dd9de0b docs: update build config.toml 2023-12-12 07:25:36 +08:00
RockYang
0dae5bef71 docs: update changelog file 2023-12-11 17:17:17 +08:00
RockYang
b4413ed726 add translate api for midjourney 2023-12-11 17:01:02 +08:00
RockYang
5e1fe88b8b feat: add prompt translate handler 2023-12-11 06:56:00 +08:00
RockYang
91ed41b536 feat: add system config item for dall e3 generate image num 2023-12-10 17:13:25 +08:00
RockYang
024c0032eb chore: change default params for stable diffusion 2023-12-10 14:45:22 +08:00
RockYang
4a9f7e3bce feat: add HuPiPay payment support 2023-12-08 19:43:13 +08:00
RockYang
cf4dcc34ec feat: add image generation API URL in chat configurations 2023-12-07 16:31:32 +08:00
RockYang
4d612c15af docs: add arm64 build script 2023-12-07 15:44:20 +08:00
RockYang
8aec87cc02 fix: fixed bug for prompt code format, prevent xss attacks 2023-12-07 14:02:13 +08:00
RockYang
442e411cde opt: save chat ID when the chat websocket disconnect 2023-12-07 11:07:08 +08:00
RockYang
acec0194de feat: adjust task list component styles 2023-12-06 19:05:51 +08:00
RockYang
8557f5b94a Merge branch 'pr_3' into dev 2023-12-06 18:54:45 +08:00
RockYang
babef8baae feat: refactor midjourney image creating page 2023-12-06 18:54:30 +08:00
RockYang
efd4ab46f5 docs: update readme file 2023-12-06 14:44:06 +08:00
liyuwanglan
ae8239e5de 修改 2023-12-05 11:08:03 +08:00
RockYang
f0994ba457 docs: update comments 2023-11-30 17:35:56 +08:00
RockYang
dae91ed243 fix: fixed bug for upload image failed 2023-11-29 17:46:46 +08:00
RockYang
de42a428e6 opt: create new chat session when change role or model, fix bug for mobile no validate 2023-11-29 17:36:27 +08:00
79 changed files with 2045 additions and 1018 deletions

View File

@@ -1,4 +1,12 @@
# 更新日志
## 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
* 功能新增:新增邀请注册功能

View File

@@ -13,6 +13,11 @@ ChatGLM,讯飞星火,文心一言等多个平台的大语言模型。集成了
* 集成插件 API 功能,可结合大语言模型的 function 功能开发各种强大的插件,已内置实现了微博热搜,今日头条,今日早报和 AI
绘画函数插件。
## 关于部署镜像申明
由于目前部署人数越来越多,本人的阿里云镜像仓库流量不够支撑大家使用了。所以从 v3.2.0 版本开始,一键部署脚本和部署镜像将只提供给 **[付费技术交流群]** 内用户使用。
代码依旧是全部开源的,大家可自行编译打包镜像。
## 功能截图
### PC 端聊天界面
@@ -90,22 +95,16 @@ ChatGLM,讯飞星火,文心一言等多个平台的大语言模型。集成了
## 项目文档
chatgpt-plus v3.2.0 一键部署脚本来了,真的只需运行一条命令,就可以完成部署:
```shell
bash -c "$(curl -fsSL https://img.r9it.com/tmp/install-v3.2.0-24e4849229.sh)"
```
目前只支持 Ubuntu 系统,推荐 Ubuntu 22.04 LTS
详细的部署和开发文档请参考 [**ChatGPT-Plus 文档**](https://ai.r9it.com/docs/)。
详细部署文档请参考 [ChatGPT-Plus 文档](https://ai.r9it.com/docs/)。
加微信进入微信讨论群可获取 **一键部署脚本添加好友时请注明来自Github!!!)。**
![微信名片](docs/imgs/wx.png)
## 参与贡献
个人的力量始终有限,任何形式的贡献都是欢迎的,包括但不限于贡献代码,优化文档,提交 issue 和 PR 等。
如果有兴趣的话,也可以加微信进入微信讨论群(**添加好友时请注明来自Github!!!**)。
![微信名片](docs/imgs/wx.png)
#### 特此声明:由于个人时间有限,不接受在微信或者微信群给开发者提 Bug有问题或者优化建议请提交 Issue 和 PR。非常感谢您的配合
### Commit 类型

View File

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

View File

@@ -156,6 +156,7 @@ func authorizeMiddleware(s *AppServer, client *redis.Client) gin.HandlerFunc {
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/") ||
@@ -229,29 +230,37 @@ func parameterHandlerMiddleware() gin.HandlerFunc {
params[key][i] = strings.TrimSpace(value)
}
}
// 更新参数
// update get parameters
c.Request.URL.RawQuery = params.Encode()
// POST JSON 参数处理
bodyBytes, err := io.ReadAll(c.Request.Body)
if err != nil {
// skip file upload requests
contentType := c.Request.Header.Get("Content-Type")
if strings.Contains(contentType, "multipart/form-data") {
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
}
if strings.Contains(contentType, "application/json") {
// process POST JSON request body
bodyBytes, err := io.ReadAll(c.Request.Body)
if err != nil {
c.Next()
return
}
// 对 JSON 数据中的字符串值去除两端空格
trimJSONStrings(jsonData)
// 更新请求体
c.Request.Body = io.NopCloser(bytes.NewBufferString(utils.JsonEncode(jsonData)))
// 还原请求体
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()
}

View File

@@ -33,7 +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},

View File

@@ -18,12 +18,13 @@ type AppConfig struct {
AesEncryptKey string
SmsConfig AliYunSmsConfig // AliYun send message service config
OSS OSSConfig // OSS config
MjConfig MidJourneyConfig // mj 绘画配置
MjConfigs []MidJourneyConfig // mj 绘画配置池子
WeChatBot bool // 是否启用微信机器人
SdConfig StableDiffusionConfig // sd 绘画配置
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

View File

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

View File

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

View File

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

View File

@@ -10,6 +10,7 @@ import (
"encoding/json"
"fmt"
"gorm.io/gorm"
"html/template"
"io"
"strings"
"time"
@@ -200,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,
}

View File

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

View File

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

View File

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

View File

@@ -10,6 +10,7 @@ import (
"encoding/json"
"fmt"
"gorm.io/gorm"
"html/template"
"io"
"strings"
"time"
@@ -135,7 +136,6 @@ func (h *ChatHandler) sendOpenAiMessage(
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)
@@ -199,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,
}

View File

@@ -12,6 +12,7 @@ import (
"encoding/json"
"fmt"
"github.com/gorilla/websocket"
"html/template"
"io"
"net/http"
"net/url"
@@ -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,
}

View File

@@ -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 += 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 {
// 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)

View File

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

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

View File

@@ -141,7 +141,6 @@ func (h *SdJobHandler) Image(c *gin.Context) {
h.service.PushTask(types.SdTask{
Id: int(job.Id),
SessionId: data.SessionId,
Src: types.TaskSrcImg,
Type: types.TaskImage,
Prompt: data.Prompt,
Params: params,

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

View File

@@ -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,24 +163,8 @@ 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),
@@ -192,6 +177,7 @@ func main() {
}),
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) {
@@ -254,13 +240,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)
}),
// 管理后台控制器
@@ -318,10 +302,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/")
@@ -353,6 +339,18 @@ func main() {
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@@ -1,20 +1,20 @@
{
"data": [
"task(m1wpaa4v60zedj8)",
"a cute cat",
"task(s95jqt5jr8yppcp)",
"A beautiful Chinese girl in a garden",
"",
[],
20,
"DPM++ 2M Karras",
30,
"Euler a",
1,
1,
7,
512,
384,
512,
true,
0.7,
2,
"ESRGAN_4x",
"Latent",
10,
0,
0,
@@ -55,13 +55,13 @@
false,
false,
0,
false,
[],
[
],
"",
"",
""
],
"event_data": null,
"fn_index": 96,
"session_hash": "kmb0ojjfhdj"
"fn_index": 95,
"session_hash": "eqwumnt3rov"
}

View File

@@ -6,6 +6,7 @@ import (
"chatplus/store/model"
"chatplus/utils"
"fmt"
"github.com/imroc/req/v3"
"gorm.io/gorm"
)
@@ -14,7 +15,6 @@ import (
type FuncImage struct {
name string
apiURL string
db *gorm.DB
uploadManager *oss.UploaderManager
proxyURL string
@@ -26,7 +26,6 @@ func NewImageFunc(db *gorm.DB, manager *oss.UploaderManager, config *types.AppCo
name: "DALL-E3 绘画",
uploadManager: manager,
proxyURL: config.ProxyURL,
apiURL: "https://api.openai.com/v1/images/generations",
}
}
@@ -57,9 +56,34 @@ type ErrRes struct {
func (f FuncImage) Invoke(params map[string]interface{}) (string, error) {
logger.Infof("绘画参数:%+v", params)
prompt := utils.InterfaceToString(params["prompt"])
// 获取绘图 API KEY
// get image generation API KEY
var apiKey model.ApiKey
f.db.Where("platform = ? AND type = ?", types.OpenAI, "img").Order("last_used_at ASC").First(&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").
@@ -67,11 +91,11 @@ func (f FuncImage) Invoke(params map[string]interface{}) (string, error) {
SetBody(imgReq{
Model: "dall-e-3",
Prompt: prompt,
N: 1,
N: imgNum,
Size: "1024x1024",
}).
SetErrorResult(&errRes).
SetSuccessResult(&res).Post(f.apiURL)
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)
}

View File

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

View File

@@ -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
View 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 has available mj service in pool
func (p *ServicePool) HasAvailableService() bool {
return len(p.services) > 0
}

View File

@@ -2,63 +2,64 @@ package mj
import (
"chatplus/core/types"
"chatplus/service"
"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
snowflake *service.Snowflake
}
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,50 +71,43 @@ func (s *Service) Run() {
case types.TaskVariation:
err = s.client.Variation(task.Index, task.MessageId, task.MessageHash)
}
if err != nil {
logger.Error("绘画任务执行失败:", err)
// 删除任务
s.db.Delete(&model.MidJourneyJob{Id: uint(task.Id)})
// 推送任务到前端
client := s.Clients.Get(task.SessionId)
if client != nil {
utils.ReplyChunkMessage(client, vo.MidJourneyJob{
Type: task.Type.String(),
UserId: task.UserId,
MessageId: task.MessageId,
Progress: -1,
Prompt: task.Prompt,
})
}
// 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 {
@@ -121,137 +115,39 @@ 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)
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
// upload image
if data.Status == Finished {
imgURL, err := s.uploadManager.GetUploadHandler().PutImg(data.Image.URL, true)
if err != nil {
logger.Error("error with download img: ", err.Error())
return
}
job.MessageId = data.MessageId
job.ReferenceId = data.ReferenceId
job.Progress = data.Progress
job.Prompt = data.Prompt
job.Hash = data.Image.Hash
// 任务完成,将最终的图片下载下来
if data.Progress == 100 {
imgURL, err := s.uploadManager.GetUploadHandler().PutImg(data.Image.URL, true)
if err != nil {
logger.Error("error with download img: ", err.Error())
return
}
job.ImgURL = imgURL
} else {
// 临时图片直接保存,访问的时候使用代理进行转发
job.ImgURL = data.Image.URL
}
res = s.db.Updates(&job)
if res.Error != nil {
logger.Error("error with update job: ", res.Error)
return
}
var jobVo vo.MidJourneyJob
err := utils.CopyObject(job, &jobVo)
if err == nil {
if data.Progress < 100 {
image, err := utils.DownloadImage(jobVo.ImgURL, s.proxyURL)
if err == nil {
jobVo.ImgURL = "data:image/png;base64," + base64.StdEncoding.EncodeToString(image)
}
}
// 推送任务到前端
client := s.Clients.Get(task.SessionId)
if client != nil {
utils.ReplyChunkMessage(client, jobVo)
}
}
} else if task.Src == types.TaskSrcChat { // 聊天任务
wsClient := s.ChatClients.Get(task.SessionId)
if data.Status == Finished {
if wsClient != nil && data.ReferenceId != "" {
content := fmt.Sprintf("**%s** 任务执行成功,正在从 MidJourney 服务器下载图片,请稍后...", data.Prompt)
utils.ReplyMessage(wsClient, content)
}
// download image
imgURL, err := s.uploadManager.GetUploadHandler().PutImg(data.Image.URL, true)
if err != nil {
logger.Error("error with download image: ", err)
if wsClient != nil && data.ReferenceId != "" {
content := fmt.Sprintf("**%s** 图片下载失败:%s", data.Prompt, err.Error())
utils.ReplyMessage(wsClient, content)
}
return
}
tx := s.db.Begin()
data.Image.URL = imgURL
message := model.HistoryMessage{
UserId: uint(task.UserId),
ChatId: task.ChatId,
RoleId: uint(task.RoleId),
Type: types.MjMsg,
Icon: task.Icon,
Content: utils.JsonEncode(data),
Tokens: 0,
UseContext: false,
}
res = tx.Create(&message)
if res.Error != nil {
logger.Error("error with update database: ", err)
return
}
// save the job
job.UserId = task.UserId
job.Type = task.Type.String()
job.MessageId = data.MessageId
job.ReferenceId = data.ReferenceId
job.Prompt = data.Prompt
job.ImgURL = imgURL
job.Progress = data.Progress
job.Hash = data.Image.Hash
job.CreatedAt = time.Now()
res = tx.Create(&job)
if res.Error != nil {
logger.Error("error with update database: ", err)
tx.Rollback()
return
}
tx.Commit()
}
if wsClient == nil { // 客户端断线,则丢弃
logger.Errorf("Client is offline: %+v", data)
return
}
if data.Status == Finished {
utils.ReplyChunkMessage(wsClient, types.WsMessage{Type: types.WsMjImg, Content: data})
utils.ReplyChunkMessage(wsClient, types.WsMessage{Type: types.WsEnd})
// 本次绘画完毕,移除客户端
s.ChatClients.Delete(task.SessionId)
} else {
// 使用代理临时转发图片
if data.Image.URL != "" {
image, err := utils.DownloadImage(data.Image.URL, s.proxyURL)
if err == nil {
data.Image.URL = "data:image/png;base64," + base64.StdEncoding.EncodeToString(image)
}
}
utils.ReplyChunkMessage(wsClient, types.WsMessage{Type: types.WsMjImg, Content: data})
}
job.ImgURL = imgURL
}
res = s.db.Updates(&job)
if res.Error != nil {
logger.Error("error with update job: ", res.Error)
return
}
// 更新用户剩余绘图次数
// TODO: 放大图片是否需要消耗绘图次数?
if data.Status == Finished {
s.db.Model(&model.User{}).Where("id = ?", task.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
// 解除任务锁定
s.redis.Del(context.Background(), RunningJobKey)
// 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)
}
}

View File

@@ -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.Domain, 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.Domain, 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{}

View File

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

View File

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

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

View File

@@ -135,7 +135,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 +230,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)
}
@@ -287,8 +285,13 @@ func (s *Service) callback(data CBReq) {
if data.Progress < 100 && data.ImageData != "" {
jobVo.ImgURL = data.ImageData
}
logger.Infof("绘图进度:%d", data.Progress)
// 扣减绘图次数
s.db.Model(&model.User{}).Where("id = ?", jobVo.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
if data.Progress == 100 {
s.db.Model(&model.User{}).Where("id = ? AND img_calls > 0", jobVo.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
}
// 推送任务到前端
if client != nil {
utils.ReplyChunkMessage(client, jobVo)

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,11 @@
#!/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
@@ -12,15 +14,15 @@ npm run build
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
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

View File

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

View File

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

View File

Before

Width:  |  Height:  |  Size: 476 KiB

After

Width:  |  Height:  |  Size: 476 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 526 KiB

After

Width:  |  Height:  |  Size: 526 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,108 @@
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 {

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -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": "笑脸",

Binary file not shown.

View File

@@ -6,6 +6,6 @@ export function validateEmail(email) {
}
export function validateMobile(mobile) {
const regex = /^1[345789]\d{9}$/;
const regex = /^1[3456789]\d{9}$/;
return regex.test(mobile);
}

View File

@@ -83,7 +83,7 @@
<div class="chat-head">
<div class="chat-config">
<span class="role-select-label">聊天角色</span>
<el-select v-model="roleId" filterable placeholder="角色" class="role-select">
<el-select v-model="roleId" filterable placeholder="角色" class="role-select" @change="newChat">
<el-option
v-for="item in roles"
:key="item.id"
@@ -97,7 +97,7 @@
</el-option>
</el-select>
<el-select v-model="modelID" placeholder="模型">
<el-select v-model="modelID" placeholder="模型" @change="newChat">
<el-option
v-for="item in models"
:key="item.id"
@@ -105,12 +105,12 @@
:value="item.id"
/>
</el-select>
<el-button type="primary" @click="newChat">
<el-icon>
<Plus/>
</el-icon>
新建
</el-button>
<el-button type="primary" @click="newChat">
<el-icon>
<Plus/>
</el-icon>
新建
</el-button>
<el-button type="success" @click="exportChat" plain>
<i class="iconfont icon-export"></i>
@@ -237,8 +237,7 @@ import {
Check,
Close,
Delete,
Edit,
Plus,
Edit, Plus,
Promotion,
RefreshRight,
Search,
@@ -246,7 +245,7 @@ import {
VideoPause
} from '@element-plus/icons-vue'
import 'highlight.js/styles/a11y-dark.css'
import {dateFormat, isMobile, randString, removeArrayItem, renderInputText, UUID} from "@/utils/libs";
import {dateFormat, isMobile, randString, removeArrayItem, UUID} from "@/utils/libs";
import {ElMessage, ElMessageBox} from "element-plus";
import hl from "highlight.js";
import {getSessionId, getUserToken, removeUserToken} from "@/store/session";
@@ -625,6 +624,7 @@ const connect = function (chat_id, role_id) {
// 将聊天框的滚动条滑动到最底部
nextTick(() => {
document.getElementById('chat-box').scrollTo(0, document.getElementById('chat-box').scrollHeight)
localStorage.setItem("chat_id", chat_id)
})
};
}
@@ -690,13 +690,12 @@ const sendMessage = function () {
if (prompt.value.trim().length === 0 || canSend.value === false) {
return false;
}
// 追加消息
chatData.value.push({
type: "prompt",
id: randString(32),
icon: loginUser.value.avatar,
content: renderInputText(prompt.value),
content: md.render(prompt.value),
created_at: new Date().getTime(),
});
@@ -761,10 +760,7 @@ const loadChatHistory = function (chatId) {
}
showHello.value = false
for (let i = 0; i < data.length; i++) {
if (data[i].type === "prompt") {
chatData.value.push(data[i]);
continue;
} else if (data[i].type === "mj") {
if (data[i].type === "mj") {
data[i].content = JSON.parse(data[i].content)
data[i].content.html = md.render(data[i].content?.content)
chatData.value.push(data[i]);
@@ -802,7 +798,7 @@ const reGenerate = function () {
type: "prompt",
id: randString(32),
icon: loginUser.value.avatar,
content: renderInputText(text)
content: md.render(text)
});
socket.value.send(text);
}

View File

@@ -2,22 +2,21 @@
<div class="home">
<div class="navigator">
<div class="logo">
<el-image :src="logo"/>
<el-link href="/">
<el-image :src="logo"/>
</el-link>
<div class="divider"></div>
</div>
<ul class="nav-items">
<li v-for="item in navs" :key="item.path">
<el-tooltip
effect="light"
:content="item.title"
placement="right"
>
<a @click="changeNav(item)" :class="item.path === curPath?'active':''">
<el-image :src="item.icon_path" :width="20" v-if="item.icon_path"/>
<i :class="'iconfont icon-'+item.icon" v-else></i>
</a>
</el-tooltip>
<!-- <el-tooltip effect="light" :content="item.title" placement="right">-->
<!-- -->
<!-- </el-tooltip>-->
<a @click="changeNav(item)" :class="item.path === curPath ? 'active' : ''">
<el-image :src="item.icon_path" :width="20" v-if="item.icon_path"/>
<i :class="'iconfont icon-' + item.icon" v-else></i>
</a>
<div :class="item.path === curPath ? 'title active' : 'title'">{{ item.title }}</div>
</li>
</ul>
</div>
@@ -34,6 +33,8 @@
<script setup>
import {useRouter} from "vue-router";
import {checkSession} from "@/action/session";
import {isMobile} from "@/utils/libs";
import {ref} from "vue";
const router = useRouter();
@@ -44,7 +45,7 @@ const navs = ref([
{path: "/sd", icon_path: "/images/sd.png", title: "SD 绘画"},
{path: "/apps", icon: "menu", title: "应用中心"},
{path: "/images-wall", icon: "image-list", title: "作品展示"},
{path: "/knowledge", icon: "book", title: "我的知识库"},
{path: "/knowledge", icon: "book", title: "知识库"},
{path: "/member", icon: "vip-user", title: "会员计划"},
{path: "/invite", icon: "share", title: "推广计划"},
])
@@ -67,7 +68,7 @@ const changeNav = (item) => {
.navigator {
display flex
flex-flow column
width 48px
width 70px
padding 10px 6px
border-right: 1px solid #3c3c3c
@@ -75,7 +76,7 @@ const changeNav = (item) => {
display flex
flex-flow column
align-items center
height 60px
.divider {
border-bottom 1px solid #4A4A4A
@@ -85,7 +86,9 @@ const changeNav = (item) => {
}
.nav-items {
margin-top 10px
margin-top: 20px;
padding-left: 10px;
padding-right: 10px;
li {
margin-bottom 15px
@@ -94,8 +97,8 @@ const changeNav = (item) => {
color #DADBDC
background-color #40444A
border-radius 10px
width 44px
height 44px
width 48px
height 48px
display flex
justify-content center
align-items center
@@ -113,6 +116,17 @@ const changeNav = (item) => {
a:hover, a.active {
color #47fff1
}
.title {
font-size: 12px
padding-top: 5px
color: #e5e7eb;
text-align: center;
}
.active {
color #47fff1
}
}
}
}

View File

@@ -8,11 +8,7 @@
<el-form :model="params" label-width="80px" label-position="left">
<div class="param-line pt">
<span>图片比例</span>
<el-tooltip
effect="light"
content="生成图片的尺寸比例"
placement="right"
>
<el-tooltip effect="light" content="生成图片的尺寸比例" placement="right">
<el-icon>
<InfoFilled/>
</el-icon>
@@ -22,23 +18,42 @@
<div class="param-line pt">
<el-row :gutter="10">
<el-col :span="8" v-for="item in rates" :key="item.value">
<div :class="item.value === params.rate?'grid-content active':'grid-content'"
<div class="flex-col items-center"
:class="item.value === params.rate ? 'grid-content active' : 'grid-content'"
@click="changeRate(item)">
<div :class="'shape '+item.css"></div>
<!-- <div :class="'shape ' + item.css"></div>-->
<el-image class="icon" :src="item.img" fit="cover"></el-image>
<div class="text">{{ item.text }}</div>
</div>
</el-col>
</el-row>
</div>
<div class="param-line" style="padding-top: 10px">
<el-form-item label="图片画质">
<template #default>
<div class="form-item-inner flex-row items-center">
<el-select v-model="params.quality" placeholder="请选择">
<el-option v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
<el-tooltip effect="light" content="生成的图片质量,质量越好出图越慢" placement="right">
<el-icon>
<InfoFilled/>
</el-icon>
</el-tooltip>
</div>
</template>
</el-form-item>
</div>
<div class="param-line pt">
<span>模型选择</span>
<el-tooltip
effect="light"
content="MJ: 偏真实通用模型 <br/>NIJI: 偏动漫风格、适用于二次元模型"
raw-content
placement="right"
>
<el-tooltip effect="light" content="MJ: 偏真实通用模型 <br/>NIJI: 偏动漫风格、适用于二次元模型" raw-content
placement="right">
<el-icon>
<InfoFilled/>
</el-icon>
@@ -47,7 +62,7 @@
<div class="param-line pt">
<el-row :gutter="10">
<el-col :span="12" v-for="item in models" :key="item.value">
<div :class="item.value === params.model?'model active':'model'"
<div :class="item.value === params.model ? 'model active' : 'model'"
@click="changeModel(item)">
<el-image :src="item.img" fit="cover"></el-image>
<div class="text">{{ item.text }}</div>
@@ -56,17 +71,51 @@
</el-row>
</div>
<div class="param-line">
<el-form-item label="重复平铺">
<template #default>
<div class="form-item-inner">
<el-switch v-model="params.tile" inactive-color="#464649" active-color="#47fff1"/>
<el-tooltip effect="light"
content="重复:--tile参数释义生成可用作重复平铺的图像以创建无缝图案。" raw-content
placement="right">
<el-icon>
<InfoFilled/>
</el-icon>
</el-tooltip>
</div>
</template>
</el-form-item>
</div>
<div class="param-line">
<el-form-item label="原始模式">
<template #default>
<div class="form-item-inner">
<el-switch v-model="params.raw" inactive-color="#464649" active-color="#47fff1"/>
<el-tooltip effect="light"
content="启用新的RAW模式呈现的人物写实感更加逼真人物细节、光源、流畅度也更加接近原始作品。<br/> 同时也意味着您需要添加更长的提示。"
raw-content
placement="right">
<el-icon>
<InfoFilled/>
</el-icon>
</el-tooltip>
</div>
</template>
</el-form-item>
</div>
<div class="param-line" style="padding-top: 10px">
<el-form-item label="创意度">
<template #default>
<div class="form-item-inner">
<el-input v-model.number="params.chaos" size="small"/>
<el-tooltip
effect="light"
content="参数用法:--chaos 或--c取值范围: 0-100 <br/> 取值越高结果越发散,反之则稳定收敛<br /> 默认值0最为精准稳定"
raw-content
placement="right"
>
<el-slider v-model.number="params.chaos" :max="100" :step="1"
style="width: 180px;--el-slider-main-bg-color:#47fff1"/>
<el-tooltip effect="light"
content="参数用法:--chaos 或--c取值范围: 0-100 <br/> 取值越高结果越发散,反之则稳定收敛<br /> 默认值0最为精准稳定"
raw-content placement="right">
<el-icon>
<InfoFilled/>
</el-icon>
@@ -80,13 +129,11 @@
<el-form-item label="风格化">
<template #default>
<div class="form-item-inner">
<el-input v-model.number="params.stylize" size="small"/>
<el-tooltip
effect="light"
content="风格化:--stylize 或 --s范围 1-1000默认值100 <br/>高取值会产生非常艺术化但与提示关联性较低的图像"
raw-content
placement="right"
>
<el-slider v-model.number="params.stylize" :min="0" :max="1000" :step="1"
style="width: 180px;--el-slider-main-bg-color:#47fff1"/>
<el-tooltip effect="light"
content="风格化:--stylize 或 --s范围 1-1000默认值100 <br/>高取值会产生非常艺术化但与提示关联性较低的图像"
raw-content placement="right">
<el-icon>
<InfoFilled/>
</el-icon>
@@ -100,13 +147,11 @@
<el-form-item label="随机种子">
<template #default>
<div class="form-item-inner">
<el-input v-model.number="params.seed" size="small"/>
<el-tooltip
effect="light"
content="随机种子:--seed默认值0表示随机产生 <br/>使用相同的种子参数和描述将产生相似的图像"
raw-content
placement="right"
>
<el-input v-model.number="params.seed" style="--el-input-focus-border-color:#47fff1"/>
<el-tooltip effect="light"
content="随机种子:--seed默认值0表示随机产生 <br/>使用相同的种子参数和描述将产生相似的图像"
raw-content
placement="right">
<el-icon>
<InfoFilled/>
</el-icon>
@@ -115,123 +160,166 @@
</template>
</el-form-item>
</div>
<div class="param-line">
<el-form-item label="原始模式">
<template #default>
<div class="form-item-inner">
<el-switch v-model="params.raw" style="--el-switch-on-color: #47fff1;"/>
<el-tooltip
effect="light"
content="启用新的RAW模式以“不带偏见”的方式生成图像。<br/> 同时也意味着您需要添加更长的提示。"
raw-content
placement="right"
>
<el-icon style="margin-top: 6px">
<InfoFilled/>
</el-icon>
</el-tooltip>
</div>
</template>
</el-form-item>
</div>
<div class="param-line">
<el-form-item label="图生图">
<template #default>
<div class="form-item-inner">
<el-input v-model="params.img" size="small" placeholder="请输入图片地址或者上传图片"
style="width: 160px"/>
<el-icon @click="params.img = ''" title="清空图片">
<DeleteFilled/>
</el-icon>
<el-tooltip
effect="light"
content="垫图:以某张图片为底稿参考来创作绘画 <br/> 支持 PNG 和 JPG 格式图片"
raw-content
placement="right"
>
<el-icon>
<InfoFilled/>
</el-icon>
</el-tooltip>
</div>
</template>
</el-form-item>
</div>
<div class="param-line">
<el-upload
class="img-uploader"
:auto-upload="true"
:show-file-list="false"
:http-request="afterRead"
style="--el-color-primary:#47fff1"
>
<el-image v-if="params.img !== ''" :src="params.img" fit="cover"/>
<el-icon v-else class="uploader-icon">
<Plus/>
</el-icon>
</el-upload>
</div>
<div class="param-line" style="padding-top: 10px">
<el-form-item label="图像权重">
<template #default>
<div class="form-item-inner">
<el-slider v-model.number="params.weight" :max="1" :step="0.01"
style="width: 180px;--el-slider-main-bg-color:#47fff1"/>
<el-tooltip
effect="light"
content="使用图像权重参数--iw来调整图像 URL 与文本的重要性 <br/>权重较高时意味着图像提示将对完成的作业产生更大的影响"
raw-content
placement="right"
>
<el-icon style="margin-top: 6px">
<InfoFilled/>
</el-icon>
</el-tooltip>
</div>
</template>
</el-form-item>
</div>
<div class="param-line">
<el-input
v-model="params.prompt"
:autosize="{ minRows: 4, maxRows: 6 }"
type="textarea"
ref="promptRef"
placeholder="这里输入你的英文咒语例如A chinese girl walking in the middle of a cobblestone street"
/>
</div>
<div class="param-line pt">
<el-form-item label="剩余次数">
<template #default>
<el-tag type="info">{{ imgCalls }}</el-tag>
</template>
</el-form-item>
</div>
</el-form>
</div>
<div class="submit-btn">
<el-button color="#47fff1" :dark="false" round @click="generate">立即生成</el-button>
</div>
</div>
<div class="task-list-box">
<div class="task-list-inner" :style="{ height: listBoxHeight + 'px' }">
<h2>AI绘画</h2>
<el-form>
<el-tabs v-model="activeName" class="title-tabs">
<el-tab-pane label="图生图(可选)" name="图生图">
<div class="text">图生图以某张图片为底稿参考来创作绘画生成类似风格或类型图像支持 PNG JPG 格式图片
</div>
<div class="param-line pt">
<el-form-item label="">
<template #default>
<div class="form-item-inner flex-row items-center">
<el-input v-model="params.img" size="small" placeholder="请输入图片地址或者上传图片"
style="width: 300px;"/>
<el-icon @click="params.img = ''" title="清空图片">
<DeleteFilled/>
</el-icon>
<el-tooltip effect="light"
content="垫图:以某张图片为底稿参考来创作绘画 <br/> 支持 PNG 和 JPG 格式图片"
raw-content placement="right">
<el-icon>
<InfoFilled/>
</el-icon>
</el-tooltip>
</div>
</template>
</el-form-item>
</div>
<div class="param-line">
<el-upload class="img-uploader" :auto-upload="true" :show-file-list="false"
:http-request="afterRead" style="--el-color-primary:#47fff1">
<el-image v-if="params.img !== ''" :src="params.img" fit="cover"/>
<el-icon v-else class="uploader-icon">
<Plus/>
</el-icon>
</el-upload>
</div>
<div class="param-line" style="padding-top: 10px">
<el-form-item label="图像权重:">
<template #default>
<div class="form-item-inner">
<el-slider v-model.number="params.weight" :max="1" :step="0.01"
style="width: 180px;--el-slider-main-bg-color:#47fff1"/>
<el-tooltip effect="light"
content="使用图像权重参数--iw来调整图像 URL 与文本的重要性 <br/>权重较高时意味着图像提示将对完成的作业产生更大的影响"
raw-content placement="right">
<el-icon style="margin-top: 9px">
<InfoFilled/>
</el-icon>
</el-tooltip>
</div>
</template>
</el-form-item>
</div>
</el-tab-pane>
<el-tab-pane label="图生文(可选)" name="图生文">
<div class="text">图生文功能正在紧锣密鼓开发中敬请期待...</div>
<!-- <div class="param-line pt">-->
<!-- <el-empty image-size="100px" description="功能建设中"/>-->
<!-- </div>-->
</el-tab-pane>
<el-tab-pane label="融图(可选)" name="融图">
<div class="text">融图功能正在紧锣密鼓开发中敬请期待...</div>
</el-tab-pane>
</el-tabs>
<div v-loading="loading" element-loading-background="rgba(122, 122, 122, 0.8)">
<div class="param-line pt">
<div class="flex-row justify-between items-center">
<div class="flex-row justify-start items-center">
<span>提示词</span>
<el-tooltip effect="light" content="输入你想要的内容,用逗号分割" placement="right">
<el-icon>
<InfoFilled/>
</el-icon>
</el-tooltip>
</div>
<div>
<el-button type="primary" @click="translatePrompt">
<el-icon style="margin-right: 6px;font-size: 18px;">
<Refresh/>
</el-icon>
翻译
</el-button>
<el-tooltip
class="box-item"
effect="light"
raw-content
content="使用 AI 翻译并重写提示词,<br/>增加更多细节,风格等描述"
placement="top-end"
>
<el-button type="success" @click="rewritePrompt">
<el-icon style="margin-right: 6px;font-size: 18px;">
<Refresh/>
</el-icon>
翻译并重写
</el-button>
</el-tooltip>
</div>
</div>
</div>
<div class="param-line pt">
<el-input v-model="params.prompt" :autosize="{ minRows: 4, maxRows: 6 }" type="textarea"
ref="promptRef"
placeholder="这里输入你的英文咒语例如A chinese girl walking in the middle of a cobblestone street"/>
</div>
</div>
<div class="param-line pt">
<div class="flex-row justify-between items-center">
<div class="flex-row justify-start items-center">
<span>不希望出现的内容可选</span>
<el-tooltip effect="light" content="不想出现在图片上的元素(例如:树,建筑)" placement="right">
<el-icon>
<InfoFilled/>
</el-icon>
</el-tooltip>
</div>
<!-- <el-button type="success">-->
<!-- <el-icon style="margin-right: 6px;font-size: 18px;">-->
<!-- <Refresh/>-->
<!-- </el-icon>-->
<!-- 翻译-->
<!-- </el-button>-->
</div>
</div>
<div class="param-line pt">
<el-input v-model="params.neg_prompt" :autosize="{ minRows: 4, maxRows: 6 }" type="textarea"
ref="promptRef"
placeholder="这里输入你不希望出现在图片上的内容,元素"/>
</div>
<div class="submit-btn">
<el-button color="#47fff1" :dark="false" @click="generate" round>立即生成</el-button>
<div class="text-info">
<el-tag type="success">绘图可用额度{{ imgCalls }}</el-tag>
</div>
</div>
</el-form>
<h2>任务列表</h2>
<div class="running-job-list">
<ItemList :items="runningJobs" v-if="runningJobs.length > 0">
<template #default="scope">
<div class="job-item">
<div v-if="scope.item.progress > 0" class="job-item-inner">
<el-image :src="scope.item['img_url']"
:zoom-rate="1.2"
:preview-src-list="[scope.item['img_url']]"
fit="cover"
:initial-index="0" loading="lazy">
<el-image :src="scope.item['img_url']" :zoom-rate="1.2"
:preview-src-list="[scope.item['img_url']]" fit="cover" :initial-index="0"
loading="lazy">
<template #placeholder>
<div class="image-slot">
正在加载图片
@@ -248,7 +336,8 @@
</el-image>
<div class="progress">
<el-progress type="circle" :percentage="scope.item.progress" :width="100" color="#47fff1"/>
<el-progress type="circle" :percentage="scope.item.progress" :width="100"
color="#47fff1"/>
</div>
</div>
<el-image fit="cover" v-else>
@@ -267,16 +356,14 @@
<h2>创作记录</h2>
<div class="finish-job-list">
<ItemList :items="finishedJobs" v-if="finishedJobs.length > 0" width="240" :gap="16">
<ItemList :items="finishedJobs" v-if="finishedJobs.length > 0" :width="240" :gap="16">
<template #default="scope">
<div class="job-item">
<el-image
:src="scope.item.type === 'upscale'?scope.item['img_url']+'?imageView2/1/w/240/h/300/q/75':scope.item['img_url']+'?imageView2/1/w/240/h/240/q/75'"
:class="scope.item.type === 'upscale'?'upscale':''"
:zoom-rate="1.2"
:preview-src-list="[scope.item['img_url']]"
fit="cover"
:initial-index="scope.index" loading="lazy" v-if="scope.item.progress > 0">
:src="scope.item.type === 'upscale' ? scope.item['img_url'] + '?imageView2/1/w/240/h/300/q/75' : scope.item['img_url'] + '?imageView2/1/w/240/h/240/q/75'"
:class="scope.item.type === 'upscale' ? 'upscale' : ''" :zoom-rate="1.2"
:preview-src-list="[scope.item['img_url']]" fit="cover" :initial-index="scope.index"
loading="lazy" v-if="scope.item.progress > 0">
<template #placeholder>
<div class="image-slot">
正在加载图片
@@ -295,18 +382,13 @@
<div class="opt" v-if="scope.item.type !== 'upscale'">
<div class="opt-line">
<ul>
<li><a @click="upscale(1,scope.item)">U1</a></li>
<li><a @click="upscale(2,scope.item)">U2</a></li>
<li><a @click="upscale(3,scope.item)">U3</a></li>
<li><a @click="upscale(4,scope.item)">U4</a></li>
<li><a @click="upscale(1, scope.item)">U1</a></li>
<li><a @click="upscale(2, scope.item)">U2</a></li>
<li><a @click="upscale(3, scope.item)">U3</a></li>
<li><a @click="upscale(4, scope.item)">U4</a></li>
<li class="show-prompt">
<el-popover
placement="left"
title="提示词"
:width="240"
trigger="hover"
>
<el-popover placement="left" title="提示词" :width="240" trigger="hover">
<template #reference>
<el-icon>
<ChromeFilled/>
@@ -316,7 +398,8 @@
<template #default>
<div class="mj-list-item-prompt">
<span>{{ scope.item.prompt }}</span>
<el-icon class="copy-prompt" :data-clipboard-text="scope.item.prompt">
<el-icon class="copy-prompt"
:data-clipboard-text="scope.item.prompt">
<DocumentCopy/>
</el-icon>
</div>
@@ -328,10 +411,10 @@
<div class="opt-line">
<ul>
<li><a @click="variation(1,scope.item)">V1</a></li>
<li><a @click="variation(2,scope.item)">V2</a></li>
<li><a @click="variation(3,scope.item)">V3</a></li>
<li><a @click="variation(4,scope.item)">V4</a></li>
<li><a @click="variation(1, scope.item)">V1</a></li>
<li><a @click="variation(2, scope.item)">V2</a></li>
<li><a @click="variation(3, scope.item)">V3</a></li>
<li><a @click="variation(4, scope.item)">V4</a></li>
</ul>
</div>
</div>
@@ -351,7 +434,15 @@
<script setup>
import {onMounted, ref} from "vue"
import {ChromeFilled, DeleteFilled, DocumentCopy, InfoFilled, Picture, Plus} from "@element-plus/icons-vue";
import {
ChromeFilled,
DeleteFilled,
DocumentCopy,
InfoFilled,
Picture,
Plus,
Refresh
} from "@element-plus/icons-vue";
import Compressor from "compressorjs";
import {httpGet, httpPost} from "@/utils/http";
import {ElMessage, ElNotification} from "element-plus";
@@ -359,9 +450,7 @@ import ItemList from "@/components/ItemList.vue";
import Clipboard from "clipboard";
import {checkSession} from "@/action/session";
import {useRouter} from "vue-router";
import {getSessionId, getUserToken} from "@/store/session";
import {removeArrayItem} from "@/utils/libs";
import axios from "axios";
import {getSessionId} from "@/store/session";
const listBoxHeight = ref(window.innerHeight - 40)
const mjBoxHeight = ref(window.innerHeight - 150)
@@ -370,118 +459,95 @@ window.onresize = () => {
mjBoxHeight.value = window.innerHeight - 150
}
const rates = [
{css: "horizontal", value: "16:9", text: "横图"},
{css: "square", value: "1:1", text: "方图"},
{css: "vertical", value: "9:16", text: "竖图"},
{css: "square", value: "1:1", text: "1:1", img: "/images/mj/rate_1_1.png"},
{css: "size1-2", value: "1:2", text: "1:2", img: "/images/mj/rate_1_2.png"},
{css: "size2-1", value: "2:1", text: "2:1", img: "/images/mj/rate_2_1.png"},
{css: "size2-3", value: "2:3", text: "2:3", img: "/images/mj/rate_3_4.png"},
{css: "size3-2", value: "3:2", text: "3:2", img: "/images/mj/rate_4_3.png"},
{css: "size3-4", value: "3:4", text: "3:4", img: "/images/mj/rate_3_4.png"},
{css: "size4-3", value: "4:3", text: "4:3", img: "/images/mj/rate_4_3.png"},
{css: "size16-9", value: "16:9", text: "16:9", img: "/images/mj/rate_16_9.png"},
{css: "size9-16", value: "9:16", text: "9:16", img: "/images/mj/rate_9_16.png"},
]
const models = [
{text: "标准模型", value: " --v 5.2", img: "/images/mj-normal.png"},
{text: "动漫模型", value: " --niji 5", img: "/images/mj-niji.png"},
{text: "最新模式MJ-5.2", value: " --v 5.2", img: "/images/mj/mj-v5.2.png"},
{text: "优质模式MJ-5.1", value: " --v 5.1", img: "/images/mj/mj-v5.1.jpg"},
{text: "虚幻模式MJ-5", value: " --v 5", img: "/images/mj/mj-v5.jpg"},
{text: "真实模式MJ-4", value: " --v 4", img: "/images/mj/mj-v4.jpg"},
{text: "动漫风niji5 原始", value: " --niji 5", img: "/images/mj/mj-niji.png"},
{text: "动漫风niji5 可爱", value: " --niji 5 --style cute", img: "/images/mj/nj1.jpg"},
{text: "动漫风niji5 风景", value: " --niji 5 --style scenic", img: "/images/mj/nj2.jpg"},
{text: "动漫风niji5 表现力", value: " --niji 5 --style expressive", img: "/images/mj/nj3.jpg"},
{text: "动漫风niji4", value: " --niji 4", img: "/images/mj/nj4.jpg"},
]
const options = [
{
value: 0.25,
label: '普通'
},
{
value: 0.5,
label: '清晰'
},
{
value: 1,
label: '高清'
},
]
const params = ref({
rate: rates[1].value,
rate: rates[0].value,
model: models[0].value,
chaos: 0,
stylize: 0,
stylize: 100,
seed: 0,
raw: false,
img: "",
weight: 0.25,
prompt: ""
prompt: "",
neg_prompt: "",
tile: false,
quality: 0.5
})
const activeName = ref('图生图')
const runningJobs = ref([])
const finishedJobs = ref([])
const router = useRouter()
const socket = ref(null)
const imgCalls = ref(0)
const loading = ref(false)
const connect = () => {
let host = process.env.VUE_APP_WS_HOST
if (host === '') {
if (location.protocol === 'https:') {
host = 'wss://' + location.host;
} else {
host = 'ws://' + location.host;
}
}
const _socket = new WebSocket(host + `/api/mj/client?session_id=${getSessionId()}&token=${getUserToken()}`);
_socket.addEventListener('open', () => {
socket.value = _socket;
});
const rewritePrompt = () => {
loading.value = true
httpPost("/api/prompt/rewrite", {"prompt": params.value.prompt}).then(res => {
params.value.prompt = res.data
loading.value = false
}).catch(e => {
ElMessage.error("翻译失败:" + e.message)
})
}
_socket.addEventListener('message', event => {
if (event.data instanceof Blob) {
const reader = new FileReader();
reader.readAsText(event.data, "UTF-8");
reader.onload = () => {
const data = JSON.parse(String(reader.result));
let isNew = true
if (data.progress === 100) {
for (let i = 0; i < finishedJobs.value.length; i++) {
if (finishedJobs.value[i].id === data.id) {
isNew = false
break
}
}
for (let i = 0; i < runningJobs.value.length; i++) {
if (runningJobs.value[i].id === data.id) {
runningJobs.value.splice(i, 1)
break
}
}
if (isNew) {
finishedJobs.value.unshift(data)
}
} else if (data.progress === -1) { // 任务执行失败
ElNotification({
title: '任务执行失败',
message: "提示词:" + data['prompt'],
type: 'error',
})
runningJobs.value = removeArrayItem(runningJobs.value, data, (v1, v2) => v1.id === v2.id)
} else {
for (let i = 0; i < runningJobs.value.length; i++) {
if (runningJobs.value[i].id === data.id) {
isNew = false
runningJobs.value[i] = data
break
}
}
if (isNew) {
runningJobs.value.push(data)
}
}
}
}
});
_socket.addEventListener('close', () => {
connect()
});
const translatePrompt = () => {
loading.value = true
httpPost("/api/prompt/translate", {"prompt": params.value.prompt}).then(res => {
params.value.prompt = res.data
loading.value = false
}).catch(e => {
ElMessage.error("翻译失败:" + e.message)
})
}
onMounted(() => {
checkSession().then(user => {
imgCalls.value = user['img_calls']
// 获取运行中的任务
httpGet(`/api/mj/jobs?status=0&user_id=${user['id']}`).then(res => {
runningJobs.value = res.data
}).catch(e => {
ElMessage.error("获取任务失败:" + e.message)
})
// 获取已完成的任务
httpGet(`/api/mj/jobs?status=1&user_id=${user['id']}`).then(res => {
finishedJobs.value = res.data
}).catch(e => {
ElMessage.error("获取任务失败:" + e.message)
})
fetchRunningJobs(user.id)
fetchFinishJobs(user.id)
// 连接 socket
connect();
}).catch(() => {
router.push('/login')
});
@@ -496,6 +562,41 @@ onMounted(() => {
})
})
// 获取运行中的任务
const fetchRunningJobs = (userId) => {
httpGet(`/api/mj/jobs?status=0&user_id=${userId}`).then(res => {
const jobs = res.data
const _jobs = []
for (let i = 0; i < jobs.length; i++) {
if (jobs[i].progress === -1) {
ElNotification({
title: '任务执行失败',
message: "任务ID" + jobs[i]['task_id'],
type: 'error',
})
continue
}
_jobs.push(jobs[i])
}
runningJobs.value = _jobs
setTimeout(() => fetchRunningJobs(userId), 10000)
}).catch(e => {
ElMessage.error("获取任务失败:" + e.message)
})
}
const fetchFinishJobs = (userId) => {
// 获取已完成的任务
httpGet(`/api/mj/jobs?status=1&user_id=${userId}`).then(res => {
finishedJobs.value = res.data
setTimeout(() => fetchFinishJobs(userId), 10000)
}).catch(e => {
ElMessage.error("获取任务失败:" + e.message)
})
}
// 切换图片比例
const changeRate = (item) => {
params.value.rate = item.value
@@ -558,7 +659,6 @@ const variation = (index, item) => {
const send = (url, index, item) => {
httpPost(url, {
index: index,
src: "img",
message_id: item.message_id,
message_hash: item.hash,
session_id: getSessionId(),

View File

@@ -500,21 +500,21 @@ window.onresize = () => {
}
const samplers = ["Euler a", "Euler", "DPM2 a Karras", "DPM++ 2S a Karras", "DPM++ 2M Karras", "DPM++ SDE Karras", "DPM2", "DPM2 a", "DPM++ 2S a", "DPM++ 2M", "DPM++ SDE", "DPM fast", "DPM adaptive",
"LMS Karras", "DPM2 Karras", "DDIM", "PLMS", "UniPC", "LMS", "Heun",]
const scaleAlg = ["ESRGAN_4x", "R-ESRGAN 4x+", "SwinIR_4x", "LDSR"]
const scaleAlg = ["Latent", "ESRGAN_4x", "R-ESRGAN 4x+", "SwinIR_4x", "LDSR"]
const params = ref({
width: 1024,
height: 1024,
sampler: samplers[0],
seed: -1,
steps: 20,
steps: 30,
cfg_scale: 7,
face_fix: false,
hd_fix: false,
hd_redraw_rate: 0.3,
hd_redraw_rate: 0.7,
hd_scale: 2,
hd_scale_alg: scaleAlg[0],
hd_steps: 0,
prompt: "A beautiful Chinese girl riding on a tiger",
hd_steps: 10,
prompt: "",
negative_prompt: "nsfw, paintings, cartoon, anime, sketches, low quality,easynegative,ng_deepnegative _v1 75t,(worst quality:2),(low quality:2),(normalquality:2),lowres,bad anatomy,bad hands,normal quality,((monochrome)),((grayscale)),((watermark))",
})

View File

@@ -101,7 +101,7 @@ const login = function () {
httpPost('/api/user/login', {username: username.value.trim(), password: password.value.trim()}).then((res) => {
setUserToken(res.data)
if (prevRoute.path === '') {
if (prevRoute.path === '' || prevRoute.path === '/register') {
if (isMobile()) {
router.push('/mobile')
} else {

View File

@@ -41,7 +41,7 @@
<ItemList :items="list" v-if="list.length > 0" :gap="30" :width="240">
<template #default="scope">
<div class="product-item" :style="{width: scope.width+'px'}" @click="orderPay(scope.item)">
<div class="product-item" :style="{width: scope.width+'px'}">
<div class="image-container">
<el-image :src="vipImg" fit="cover"/>
</div>
@@ -62,6 +62,16 @@
<span class="expire" v-if="scope.item.days > 0">{{ scope.item.days }}</span>
<span class="expire" v-else>当月有效</span>
</div>
<div class="pay-way">
<el-button type="primary" @click="alipay(scope.item)" size="small" v-if="payWays['alipay']">
<i class="iconfont icon-alipay"></i> 支付宝
</el-button>
<el-button type="success" @click="huPiPay(scope.item)" size="small" v-if="payWays['hupi']">
<span v-if="payWays['hupi']['name'] === 'wechat'"><i class="iconfont icon-wechat-pay"></i> 微信</span>
<span v-else><i class="iconfont icon-alipay"></i> 支付宝</span>
</el-button>
</div>
</div>
</div>
</template>
@@ -113,7 +123,7 @@
title="充值订单支付">
<div class="pay-container">
<div class="count-down">
<count-down :second="orderTimeout" @timeout="orderPay" ref="countDown"/>
<count-down :second="orderTimeout" @timeout="refreshPayCode" ref="countDownRef"/>
</div>
<div class="pay-qrcode" v-loading="loading">
@@ -130,9 +140,10 @@
<el-icon>
<InfoFilled/>
</el-icon>
<span class="text">请打开手机支付宝扫码支付</span>
<span class="text">请打开手机{{ payName }}扫码支付</span>
</div>
</div>
</el-dialog>
</div>
</template>
@@ -152,6 +163,7 @@ import RewardVerify from "@/components/RewardVerify.vue";
import {useRouter} from "vue-router";
import {removeUserToken} from "@/store/session";
import UserOrder from "@/components/UserOrder.vue";
import CountDown from "@/components/CountDown.vue";
const listBoxHeight = window.innerHeight - 97
const list = ref([])
@@ -171,11 +183,15 @@ const isLogin = ref(false)
const router = useRouter()
const curPayProduct = ref(null)
const activeOrderNo = ref("")
const countDown = ref(null)
const countDownRef = ref(null)
const orderTimeout = ref(1800)
const loading = ref(true)
const orderPayInfoText = ref("")
const payWays = ref({})
const payName = ref("支付宝")
const curPay = ref("alipay") // 当前支付方式
onMounted(() => {
checkSession().then(_user => {
@@ -200,9 +216,48 @@ onMounted(() => {
}).catch(e => {
ElMessage.error("获取系统配置失败:" + e.message)
})
httpGet("/api/payment/payWays").then(res => {
payWays.value = res.data
}).catch(e => {
ElMessage.error("获取支付方式失败:" + e.message)
})
})
const orderPay = (row) => {
// refresh payment qrcode
const refreshPayCode = () => {
if (curPay.value === 'alipay') {
alipay()
} else if (curPay.value === 'hupi') {
huPiPay()
}
}
const genPayQrcode = () => {
loading.value = true
text.value = ""
httpPost("/api/payment/qrcode", {
pay_way: curPay.value,
product_id: curPayProduct.value.id,
user_id: user.value.id
}).then(res => {
showPayDialog.value = true
qrcode.value = res.data['image']
activeOrderNo.value = res.data['order_no']
queryOrder(activeOrderNo.value)
loading.value = false
// 重置计数器
if (countDownRef.value) {
countDownRef.value.resetTimer()
}
}).catch(e => {
ElMessage.error("生成支付订单失败:" + e.message)
})
}
const alipay = (row) => {
payName.value = "支付宝"
curPay.value = "alipay"
if (!user.value.id) {
showLoginDialog.value = true
return
@@ -210,21 +265,22 @@ const orderPay = (row) => {
if (row) {
curPayProduct.value = row
}
loading.value = true
text.value = ""
httpPost("/api/payment/alipay/qrcode", {product_id: curPayProduct.value.id, user_id: user.value.id}).then(res => {
showPayDialog.value = true
qrcode.value = res.data['image']
activeOrderNo.value = res.data['order_no']
queryOrder(activeOrderNo.value)
loading.value = false
// 重置计数器
if (countDown.value) {
countDown.value.resetTimer()
}
}).catch(e => {
ElMessage.error("生成支付订单失败:" + e.message)
})
genPayQrcode()
}
// 虎皮椒支付
const huPiPay = (row) => {
payName.value = payWays.value["hupi"]["name"] === "wechat" ? '微信' : '支付宝'
curPay.value = "hupi"
if (!user.value.id) {
showLoginDialog.value = true
return
}
if (row) {
curPayProduct.value = row
}
genPayQrcode()
}
const queryOrder = (orderNo) => {
@@ -416,6 +472,16 @@ const logout = function () {
}
}
.pay-way {
padding 10px 0
display flex
justify-content: space-between
.iconfont {
margin-right 5px
}
}
}
&:hover {

View File

@@ -127,14 +127,6 @@ import {isMobile} from "@/utils/libs";
import {setUserToken} from "@/store/session";
import {checkSession} from "@/action/session";
checkSession().then(() => {
if (isMobile()) {
router.push('/mobile')
} else {
router.push('/chat')
}
}).catch(() => {
})
const router = useRouter();
const title = ref('ChatGPT-PLUS 用户注册');
const formData = ref({

View File

@@ -57,7 +57,10 @@
<el-form :model="item" label-width="120px" ref="formRef" :rules="rules">
<el-form-item label="所属平台" prop="platform">
<el-select v-model="item.platform" placeholder="请选择平台">
<el-option v-for="item in platforms" :value="item.value" :key="item.value">{{ item.name }}</el-option>
<el-option v-for="item in platforms" :value="item.value" :label="item.name" :key="item.value">{{
item.name
}}
</el-option>
</el-select>
</el-form-item>
@@ -66,7 +69,10 @@
</el-form-item>
<el-form-item label="用途" prop="type">
<el-select v-model="item.type" placeholder="请选择用途">
<el-option v-for="item in types" :value="item.value" :key="item.value">{{ item.name }}</el-option>
<el-option v-for="item in types" :value="item.value" :label="item.name" :key="item.value">{{
item.name
}}
</el-option>
</el-select>
</el-form-item>

View File

@@ -53,7 +53,10 @@
<el-form :model="item" label-width="120px" ref="formRef" :rules="rules">
<el-form-item label="所属平台" prop="platform">
<el-select v-model="item.platform" placeholder="请选择平台">
<el-option v-for="item in platforms" :value="item.value" :key="item.value">{{ item.name }}</el-option>
<el-option v-for="item in platforms" :value="item.value" :label="item.name" :key="item.value">{{
item.name
}}
</el-option>
</el-select>
</el-form-item>

View File

@@ -249,6 +249,13 @@
<el-input v-model.number="chat['xun_fei']['max_tokens']" placeholder="回复的最大字数最大4096"/>
</el-form-item>
<el-divider content-position="center">AI绘图</el-divider>
<el-form-item label="DALL-E3 API地址">
<el-input v-model="chat['dall_api_url']" placeholder="OpenAI官方API需要配合代理使用"/>
</el-form-item>
<el-form-item label="默认出图数量">
<el-input v-model.number="chat['dall_img_num']" placeholder="调用 DALL E3 API 传入的出图数量"/>
</el-form-item>
<el-form-item style="text-align: right">
<el-button type="primary" @click="save('chat')">保存</el-button>
</el-form-item>
@@ -274,6 +281,7 @@ const chat = ref({
context_deep: 0,
enable_context: true,
enable_history: true,
dall_api_url: "",
})
const loading = ref(true)
const systemFormRef = ref(null)
@@ -290,25 +298,7 @@ onMounted(() => {
// 加载聊天配置
httpGet('/api/admin/config/get?key=chat').then(res => {
// chat.value = res.data
if (res.data.open_ai) {
chat.value.open_ai = res.data.open_ai
}
if (res.data.azure) {
chat.value.azure = res.data.azure
}
if (res.data.chat_gml) {
chat.value.chat_gml = res.data.chat_gml
}
if (res.data.baidu) {
chat.value.baidu = res.data.baidu
}
if (res.data.xun_fei) {
chat.value.xun_fei = res.data.xun_fei
}
chat.value.context_deep = res.data.context_deep
chat.value.enable_context = res.data.enable_context
chat.value.enable_history = res.data.enable_history
chat.value = res.data
loading.value = false
}).catch(e => {
ElMessage.error("加载聊天配置失败: " + e.message)

View File

@@ -3,6 +3,7 @@ let webpack = require('webpack')
module.exports = defineConfig({
transpileDependencies: true,
lintOnSave: false, //关闭eslint校验
productionSourceMap: false, //在生产模式中禁用 Source Map既可以减少包大小也可以加密源码
configureWebpack: {
// disable performance hints
performance: {