upgrade to v4.0.4
@ -12,11 +12,11 @@ import (
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/nfnt/resize"
|
||||
"golang.org/x/image/webp"
|
||||
"gorm.io/gorm"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
@ -215,9 +215,12 @@ func needLogin(c *gin.Context) bool {
|
||||
c.Request.URL.Path == "/api/invite/hits" ||
|
||||
c.Request.URL.Path == "/api/sd/imgWall" ||
|
||||
c.Request.URL.Path == "/api/sd/client" ||
|
||||
c.Request.URL.Path == "/api/dall/imgWall" ||
|
||||
c.Request.URL.Path == "/api/dall/client" ||
|
||||
c.Request.URL.Path == "/api/config/get" ||
|
||||
c.Request.URL.Path == "/api/product/list" ||
|
||||
c.Request.URL.Path == "/api/menu/list" ||
|
||||
c.Request.URL.Path == "/api/markMap/client" ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/test") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/function/") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/sms/") ||
|
||||
@ -327,6 +330,10 @@ func staticResourceMiddleware() gin.HandlerFunc {
|
||||
|
||||
// 解码图片
|
||||
img, _, err := image.Decode(file)
|
||||
// for .webp image
|
||||
if err != nil {
|
||||
img, err = webp.Decode(file)
|
||||
}
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "Error decoding image")
|
||||
return
|
||||
@ -343,7 +350,9 @@ func staticResourceMiddleware() gin.HandlerFunc {
|
||||
var buffer bytes.Buffer
|
||||
err = jpeg.Encode(&buffer, newImg, &jpeg.Options{Quality: quality})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
logger.Error(err)
|
||||
c.String(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 设置图片缓存有效期为一年 (365天)
|
||||
|
@ -23,7 +23,7 @@ func NewDefaultConfig() *types.AppConfig {
|
||||
SecretKey: utils.RandString(64),
|
||||
MaxAge: 86400,
|
||||
},
|
||||
ApiConfig: types.ChatPlusApiConfig{},
|
||||
ApiConfig: types.ApiConfig{},
|
||||
OSS: types.OSSConfig{
|
||||
Active: "local",
|
||||
Local: types.LocalStorageConfig{
|
||||
|
@ -8,7 +8,7 @@ type ApiRequest struct {
|
||||
Stream bool `json:"stream"`
|
||||
Messages []interface{} `json:"messages,omitempty"`
|
||||
Prompt []interface{} `json:"prompt,omitempty"` // 兼容 ChatGLM
|
||||
Tools []interface{} `json:"tools,omitempty"`
|
||||
Tools []Tool `json:"tools,omitempty"`
|
||||
Functions []interface{} `json:"functions,omitempty"` // 兼容中转平台
|
||||
|
||||
ToolChoice string `json:"tool_choice,omitempty"`
|
||||
@ -62,6 +62,7 @@ type ChatModel struct {
|
||||
MaxTokens int `json:"max_tokens"` // 最大响应长度
|
||||
MaxContext int `json:"max_context"` // 最大上下文长度
|
||||
Temperature float32 `json:"temperature"` // 模型温度
|
||||
KeyId int `json:"key_id"` // 绑定 API KEY
|
||||
}
|
||||
|
||||
type ApiError struct {
|
||||
|
@ -14,7 +14,7 @@ type AppConfig struct {
|
||||
StaticDir string // 静态资源目录
|
||||
StaticUrl string // 静态资源 URL
|
||||
Redis RedisConfig // redis 连接信息
|
||||
ApiConfig ChatPlusApiConfig // ChatPlus API authorization configs
|
||||
ApiConfig ApiConfig // ChatPlus API authorization configs
|
||||
SMS SMSConfig // send mobile message config
|
||||
OSS OSSConfig // OSS config
|
||||
MjProxyConfigs []MjProxyConfig // MJ proxy config
|
||||
@ -30,6 +30,7 @@ type AppConfig struct {
|
||||
}
|
||||
|
||||
type SmtpConfig struct {
|
||||
UseTls bool // 是否使用 TLS 发送
|
||||
Host string
|
||||
Port int
|
||||
AppName string // 应用名称
|
||||
@ -37,7 +38,7 @@ type SmtpConfig struct {
|
||||
Password string // 发件人邮箱密码
|
||||
}
|
||||
|
||||
type ChatPlusApiConfig struct {
|
||||
type ApiConfig struct {
|
||||
ApiURL string
|
||||
AppId string
|
||||
Token string
|
||||
@ -114,6 +115,17 @@ type RedisConfig struct {
|
||||
DB int
|
||||
}
|
||||
|
||||
// LicenseKey 存储许可证书的 KEY
|
||||
const LicenseKey = "Geek-AI-License"
|
||||
|
||||
type License struct {
|
||||
Key string // 许可证书密钥
|
||||
MachineId string // 机器码
|
||||
UserNum int // 用户数量
|
||||
ExpiredAt int64 // 过期时间
|
||||
IsActive bool // 是否激活
|
||||
}
|
||||
|
||||
func (c RedisConfig) Url() string {
|
||||
return fmt.Sprintf("%s:%d", c.Host, c.Port)
|
||||
}
|
||||
@ -136,7 +148,7 @@ type SystemConfig struct {
|
||||
InvitePower int `json:"invite_power,omitempty"` // 邀请新用户赠送算力值
|
||||
VipMonthPower int `json:"vip_month_power,omitempty"` // VIP 会员每月赠送的算力值
|
||||
|
||||
RegisterWays []string `json:"register_ways,omitempty"` // 注册方式:支持手机,邮箱注册,账号密码注册
|
||||
RegisterWays []string `json:"register_ways,omitempty"` // 注册方式:支持手机(mobile),邮箱注册(email),账号密码注册
|
||||
EnabledRegister bool `json:"enabled_register,omitempty"` // 是否开放注册
|
||||
|
||||
RewardImg string `json:"reward_img,omitempty"` // 众筹收款二维码地址
|
||||
|
@ -8,19 +8,14 @@ type ToolCall struct {
|
||||
} `json:"function"`
|
||||
}
|
||||
|
||||
type Tool struct {
|
||||
Type string `json:"type"`
|
||||
Function Function `json:"function"`
|
||||
}
|
||||
|
||||
type Function struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Parameters Parameters `json:"parameters"`
|
||||
}
|
||||
|
||||
type Parameters struct {
|
||||
Type string `json:"type"`
|
||||
Required []string `json:"required"`
|
||||
Properties map[string]Property `json:"properties"`
|
||||
}
|
||||
|
||||
type Property struct {
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Parameters map[string]interface{} `json:"parameters"`
|
||||
Required interface{} `json:"required,omitempty"`
|
||||
}
|
||||
|
@ -59,3 +59,16 @@ type SdTaskParams struct {
|
||||
HdScaleAlg string `json:"hd_scale_alg"` // 放大算法
|
||||
HdSteps int `json:"hd_steps"` // 高清修复迭代步数
|
||||
}
|
||||
|
||||
// DallTask DALL-E task
|
||||
type DallTask struct {
|
||||
JobId uint `json:"job_id"`
|
||||
UserId uint `json:"user_id"`
|
||||
Prompt string `json:"prompt"`
|
||||
N int `json:"n"`
|
||||
Quality string `json:"quality"`
|
||||
Size string `json:"size"`
|
||||
Style string `json:"style"`
|
||||
|
||||
Power int `json:"power"`
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ const (
|
||||
WsStart = WsMsgType("start")
|
||||
WsMiddle = WsMsgType("middle")
|
||||
WsEnd = WsMsgType("end")
|
||||
WsMjImg = WsMsgType("mj")
|
||||
WsErr = WsMsgType("error")
|
||||
)
|
||||
|
||||
type BizCode int
|
||||
|
@ -27,14 +27,19 @@ require github.com/xxl-job/xxl-job-executor-go v1.2.0
|
||||
|
||||
require (
|
||||
github.com/mojocn/base64Captcha v1.3.1
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible
|
||||
github.com/shopspring/decimal v1.3.1
|
||||
github.com/syndtr/goleveldb v1.0.0
|
||||
golang.org/x/image v0.0.0-20211028202545-6944b10bf410
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect
|
||||
golang.org/x/image v0.0.0-20190501045829-6d32002ffd75 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.13 // indirect
|
||||
github.com/tklauser/numcpus v0.7.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
@ -107,6 +112,6 @@ require (
|
||||
go.uber.org/fx v1.19.3
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
golang.org/x/crypto v0.12.0
|
||||
golang.org/x/sys v0.11.0 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
gorm.io/gorm v1.25.1
|
||||
)
|
||||
|
18
api/go.sum
@ -40,6 +40,8 @@ github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SU
|
||||
github.com/go-basic/ipv4 v1.0.0 h1:gjyFAa1USC1hhXTkPOwBWDPfMcUaIM+tvo1XzV9EZxs=
|
||||
github.com/go-basic/ipv4 v1.0.0/go.mod h1:etLBnaxbidQfuqE6wgZQfs38nEWNmzALkxDZe4xY8Dg=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
@ -175,6 +177,8 @@ github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUA
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
||||
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
@ -203,6 +207,10 @@ github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gt
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
||||
github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4=
|
||||
github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0=
|
||||
github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4=
|
||||
github.com/tklauser/numcpus v0.7.0/go.mod h1:bb6dMVcj8A42tSE7i32fsIUCbQNllK5iDguyOZRUzAY=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=
|
||||
@ -215,6 +223,8 @@ github.com/xxl-job/xxl-job-executor-go v1.2.0 h1:MTl2DpwrK2+hNjRRks2k7vB3oy+3onq
|
||||
github.com/xxl-job/xxl-job-executor-go v1.2.0/go.mod h1:bUFhz/5Irp9zkdYk5MxhQcDDT6LlZrI8+rv5mHtQ1mo=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
@ -239,8 +249,9 @@ golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||
golang.org/x/image v0.0.0-20190501045829-6d32002ffd75 h1:TbGuee8sSq15Iguxu4deQ7+Bqq/d2rsQejGcEtADAMQ=
|
||||
golang.org/x/image v0.0.0-20190501045829-6d32002ffd75/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
|
||||
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
|
||||
@ -263,6 +274,7 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@ -274,8 +286,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"chatplus/store/vo"
|
||||
"chatplus/utils"
|
||||
"chatplus/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@ -65,14 +66,20 @@ func (h *ApiKeyHandler) Save(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *ApiKeyHandler) List(c *gin.Context) {
|
||||
if err := utils.CheckPermission(c, h.DB); err != nil {
|
||||
resp.NotPermission(c)
|
||||
return
|
||||
}
|
||||
status := h.GetBool(c, "status")
|
||||
t := h.GetTrim(c, "type")
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if status {
|
||||
session = session.Where("enabled", true)
|
||||
}
|
||||
if t != "" {
|
||||
session = session.Where("type", t)
|
||||
}
|
||||
|
||||
var items []model.ApiKey
|
||||
var keys = make([]vo.ApiKey, 0)
|
||||
res := h.DB.Find(&items)
|
||||
res := session.Find(&items)
|
||||
if res.Error == nil {
|
||||
for _, item := range items {
|
||||
var key vo.ApiKey
|
||||
@ -122,6 +129,5 @@ func (h *ApiKeyHandler) Remove(c *gin.Context) {
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
@ -33,11 +33,6 @@ type chatItemVo struct {
|
||||
}
|
||||
|
||||
func (h *ChatHandler) List(c *gin.Context) {
|
||||
if err := utils.CheckPermission(c, h.DB); err != nil {
|
||||
resp.NotPermission(c)
|
||||
return
|
||||
}
|
||||
|
||||
var data struct {
|
||||
Title string `json:"title"`
|
||||
UserId uint `json:"user_id"`
|
||||
|
@ -10,7 +10,6 @@ import (
|
||||
"chatplus/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ChatModelHandler struct {
|
||||
@ -34,6 +33,7 @@ func (h *ChatModelHandler) Save(c *gin.Context) {
|
||||
MaxTokens int `json:"max_tokens"` // 最大响应长度
|
||||
MaxContext int `json:"max_context"` // 最大上下文长度
|
||||
Temperature float32 `json:"temperature"` // 模型温度
|
||||
KeyId int `json:"key_id,omitempty"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
@ -51,12 +51,15 @@ func (h *ChatModelHandler) Save(c *gin.Context) {
|
||||
MaxTokens: data.MaxTokens,
|
||||
MaxContext: data.MaxContext,
|
||||
Temperature: data.Temperature,
|
||||
KeyId: data.KeyId,
|
||||
Power: data.Power}
|
||||
item.Id = data.Id
|
||||
if item.Id > 0 {
|
||||
item.CreatedAt = time.Unix(data.CreatedAt, 0)
|
||||
var res *gorm.DB
|
||||
if data.Id > 0 {
|
||||
item.Id = data.Id
|
||||
res = h.DB.Select("*").Omit("created_at").Updates(&item)
|
||||
} else {
|
||||
res = h.DB.Create(&item)
|
||||
}
|
||||
res := h.DB.Save(&item)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
@ -75,11 +78,6 @@ func (h *ChatModelHandler) Save(c *gin.Context) {
|
||||
|
||||
// List 模型列表
|
||||
func (h *ChatModelHandler) List(c *gin.Context) {
|
||||
if err := utils.CheckPermission(c, h.DB); err != nil {
|
||||
resp.NotPermission(c)
|
||||
return
|
||||
}
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
enable := h.GetBool(c, "enable")
|
||||
if enable {
|
||||
@ -88,18 +86,33 @@ func (h *ChatModelHandler) List(c *gin.Context) {
|
||||
var items []model.ChatModel
|
||||
var cms = make([]vo.ChatModel, 0)
|
||||
res := session.Order("sort_num ASC").Find(&items)
|
||||
if res.Error == nil {
|
||||
for _, item := range items {
|
||||
var cm vo.ChatModel
|
||||
err := utils.CopyObject(item, &cm)
|
||||
if err == nil {
|
||||
cm.Id = item.Id
|
||||
cm.CreatedAt = item.CreatedAt.Unix()
|
||||
cm.UpdatedAt = item.UpdatedAt.Unix()
|
||||
cms = append(cms, cm)
|
||||
} else {
|
||||
logger.Error(err)
|
||||
}
|
||||
if res.Error != nil {
|
||||
resp.SUCCESS(c, cms)
|
||||
return
|
||||
}
|
||||
|
||||
// initialize key name
|
||||
keyIds := make([]int, 0)
|
||||
for _, v := range items {
|
||||
keyIds = append(keyIds, v.KeyId)
|
||||
}
|
||||
var keys []model.ApiKey
|
||||
keyMap := make(map[uint]string)
|
||||
h.DB.Where("id IN ?", keyIds).Find(&keys)
|
||||
for _, v := range keys {
|
||||
keyMap[v.Id] = v.Name
|
||||
}
|
||||
for _, item := range items {
|
||||
var cm vo.ChatModel
|
||||
err := utils.CopyObject(item, &cm)
|
||||
if err == nil {
|
||||
cm.Id = item.Id
|
||||
cm.CreatedAt = item.CreatedAt.Unix()
|
||||
cm.UpdatedAt = item.UpdatedAt.Unix()
|
||||
cm.KeyName = keyMap[uint(item.KeyId)]
|
||||
cms = append(cms, cm)
|
||||
} else {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, cms)
|
||||
|
@ -8,9 +8,10 @@ import (
|
||||
"chatplus/store/vo"
|
||||
"chatplus/utils"
|
||||
"chatplus/utils/resp"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ChatRoleHandler struct {
|
||||
@ -50,11 +51,6 @@ func (h *ChatRoleHandler) Save(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *ChatRoleHandler) List(c *gin.Context) {
|
||||
if err := utils.CheckPermission(c, h.DB); err != nil {
|
||||
resp.NotPermission(c)
|
||||
return
|
||||
}
|
||||
|
||||
var items []model.ChatRole
|
||||
var roles = make([]vo.ChatRole, 0)
|
||||
res := h.DB.Order("sort_num ASC").Find(&items)
|
||||
@ -63,6 +59,25 @@ func (h *ChatRoleHandler) List(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// initialize model mane for role
|
||||
modelIds := make([]int, 0)
|
||||
for _, v := range items {
|
||||
if v.ModelId > 0 {
|
||||
modelIds = append(modelIds, v.ModelId)
|
||||
}
|
||||
}
|
||||
|
||||
modelNameMap := make(map[int]string)
|
||||
if len(modelIds) > 0 {
|
||||
var models []model.ChatModel
|
||||
tx := h.DB.Where("id IN ?", modelIds).Find(&models)
|
||||
if tx.Error == nil {
|
||||
for _, m := range models {
|
||||
modelNameMap[int(m.Id)] = m.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range items {
|
||||
var role vo.ChatRole
|
||||
err := utils.CopyObject(v, &role)
|
||||
@ -70,6 +85,7 @@ func (h *ChatRoleHandler) List(c *gin.Context) {
|
||||
role.Id = v.Id
|
||||
role.CreatedAt = v.CreatedAt.Unix()
|
||||
role.UpdatedAt = v.UpdatedAt.Unix()
|
||||
role.ModelName = modelNameMap[role.ModelId]
|
||||
roles = append(roles, role)
|
||||
}
|
||||
}
|
||||
|
@ -4,20 +4,24 @@ import (
|
||||
"chatplus/core"
|
||||
"chatplus/core/types"
|
||||
"chatplus/handler"
|
||||
"chatplus/service"
|
||||
"chatplus/store"
|
||||
"chatplus/store/model"
|
||||
"chatplus/utils"
|
||||
"chatplus/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/shirou/gopsutil/host"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ConfigHandler struct {
|
||||
handler.BaseHandler
|
||||
levelDB *store.LevelDB
|
||||
licenseService *service.LicenseService
|
||||
}
|
||||
|
||||
func NewConfigHandler(app *core.AppServer, db *gorm.DB) *ConfigHandler {
|
||||
return &ConfigHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||
func NewConfigHandler(app *core.AppServer, db *gorm.DB, levelDB *store.LevelDB, licenseService *service.LicenseService) *ConfigHandler {
|
||||
return &ConfigHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}, levelDB: levelDB, licenseService: licenseService}
|
||||
}
|
||||
|
||||
func (h *ConfigHandler) Update(c *gin.Context) {
|
||||
@ -70,11 +74,6 @@ func (h *ConfigHandler) Update(c *gin.Context) {
|
||||
|
||||
// Get 获取指定的系统配置
|
||||
func (h *ConfigHandler) Get(c *gin.Context) {
|
||||
if err := utils.CheckPermission(c, h.DB); err != nil {
|
||||
resp.NotPermission(c)
|
||||
return
|
||||
}
|
||||
|
||||
key := c.Query("key")
|
||||
var config model.Config
|
||||
res := h.DB.Where("marker", key).First(&config)
|
||||
@ -92,3 +91,27 @@ func (h *ConfigHandler) Get(c *gin.Context) {
|
||||
|
||||
resp.SUCCESS(c, value)
|
||||
}
|
||||
|
||||
// Active 激活系统
|
||||
func (h *ConfigHandler) Active(c *gin.Context) {
|
||||
var data struct {
|
||||
License string `json:"license"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
info, err := host.Info()
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = h.licenseService.ActiveLicense(data.License, info.HostID)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, info.HostID)
|
||||
}
|
||||
|
@ -71,11 +71,6 @@ func (h *FunctionHandler) Set(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *FunctionHandler) List(c *gin.Context) {
|
||||
if err := utils.CheckPermission(c, h.DB); err != nil {
|
||||
resp.NotPermission(c)
|
||||
return
|
||||
}
|
||||
|
||||
var items []model.Function
|
||||
res := h.DB.Find(&items)
|
||||
if res.Error != nil {
|
||||
|
@ -22,11 +22,6 @@ func NewOrderHandler(app *core.AppServer, db *gorm.DB) *OrderHandler {
|
||||
}
|
||||
|
||||
func (h *OrderHandler) List(c *gin.Context) {
|
||||
if err := utils.CheckPermission(c, h.DB); err != nil {
|
||||
resp.NotPermission(c)
|
||||
return
|
||||
}
|
||||
|
||||
var data struct {
|
||||
OrderNo string `json:"order_no"`
|
||||
Status int `json:"status"`
|
||||
|
@ -21,11 +21,6 @@ func NewRewardHandler(app *core.AppServer, db *gorm.DB) *RewardHandler {
|
||||
}
|
||||
|
||||
func (h *RewardHandler) List(c *gin.Context) {
|
||||
if err := utils.CheckPermission(c, h.DB); err != nil {
|
||||
resp.NotPermission(c)
|
||||
return
|
||||
}
|
||||
|
||||
var items []model.Reward
|
||||
res := h.DB.Order("id DESC").Find(&items)
|
||||
var rewards = make([]vo.Reward, 0)
|
||||
|
@ -25,11 +25,6 @@ func NewUserHandler(app *core.AppServer, db *gorm.DB) *UserHandler {
|
||||
|
||||
// List 用户列表
|
||||
func (h *UserHandler) List(c *gin.Context) {
|
||||
if err := utils.CheckPermission(c, h.DB); err != nil {
|
||||
resp.NotPermission(c)
|
||||
return
|
||||
}
|
||||
|
||||
page := h.GetInt(c, "page", 1)
|
||||
pageSize := h.GetInt(c, "page_size", 20)
|
||||
username := h.GetTrim(c, "username")
|
||||
|
@ -30,7 +30,7 @@ func (h *ChatHandler) sendAzureMessage(
|
||||
promptCreatedAt := time.Now() // 记录提问时间
|
||||
start := time.Now()
|
||||
var apiKey = model.ApiKey{}
|
||||
response, err := h.doRequest(ctx, req, session.Model.Platform, &apiKey)
|
||||
response, err := h.doRequest(ctx, req, session, &apiKey)
|
||||
logger.Info("HTTP请求完成,耗时:", time.Now().Sub(start))
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "context canceled") {
|
||||
|
@ -47,7 +47,7 @@ func (h *ChatHandler) sendBaiduMessage(
|
||||
promptCreatedAt := time.Now() // 记录提问时间
|
||||
start := time.Now()
|
||||
var apiKey = model.ApiKey{}
|
||||
response, err := h.doRequest(ctx, req, session.Model.Platform, &apiKey)
|
||||
response, err := h.doRequest(ctx, req, session, &apiKey)
|
||||
logger.Info("HTTP请求完成,耗时:", time.Now().Sub(start))
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "context canceled") {
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"chatplus/core/types"
|
||||
"chatplus/handler"
|
||||
logger2 "chatplus/logger"
|
||||
"chatplus/service"
|
||||
"chatplus/service/oss"
|
||||
"chatplus/store/model"
|
||||
"chatplus/store/vo"
|
||||
@ -35,15 +36,17 @@ var logger = logger2.GetLogger()
|
||||
|
||||
type ChatHandler struct {
|
||||
handler.BaseHandler
|
||||
redis *redis.Client
|
||||
uploadManager *oss.UploaderManager
|
||||
redis *redis.Client
|
||||
uploadManager *oss.UploaderManager
|
||||
licenseService *service.LicenseService
|
||||
}
|
||||
|
||||
func NewChatHandler(app *core.AppServer, db *gorm.DB, redis *redis.Client, manager *oss.UploaderManager) *ChatHandler {
|
||||
func NewChatHandler(app *core.AppServer, db *gorm.DB, redis *redis.Client, manager *oss.UploaderManager, licenseService *service.LicenseService) *ChatHandler {
|
||||
return &ChatHandler{
|
||||
BaseHandler: handler.BaseHandler{App: app, DB: db},
|
||||
redis: redis,
|
||||
uploadManager: manager,
|
||||
BaseHandler: handler.BaseHandler{App: app, DB: db},
|
||||
redis: redis,
|
||||
uploadManager: manager,
|
||||
licenseService: licenseService,
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,9 +71,20 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) {
|
||||
modelId := h.GetInt(c, "model_id", 0)
|
||||
|
||||
client := types.NewWsClient(ws)
|
||||
var chatRole model.ChatRole
|
||||
res := h.DB.First(&chatRole, roleId)
|
||||
if res.Error != nil || !chatRole.Enable {
|
||||
utils.ReplyMessage(client, "当前聊天角色不存在或者未启用,连接已关闭!!!")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
// if the role bind a model_id, use role's bind model_id
|
||||
if chatRole.ModelId > 0 {
|
||||
modelId = chatRole.ModelId
|
||||
}
|
||||
// get model info
|
||||
var chatModel model.ChatModel
|
||||
res := h.DB.First(&chatModel, modelId)
|
||||
res = h.DB.First(&chatModel, modelId)
|
||||
if res.Error != nil || chatModel.Enabled == false {
|
||||
utils.ReplyMessage(client, "当前AI模型暂未启用,连接已关闭!!!")
|
||||
c.Abort()
|
||||
@ -111,15 +125,9 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) {
|
||||
MaxTokens: chatModel.MaxTokens,
|
||||
MaxContext: chatModel.MaxContext,
|
||||
Temperature: chatModel.Temperature,
|
||||
KeyId: chatModel.KeyId,
|
||||
Platform: types.Platform(chatModel.Platform)}
|
||||
logger.Infof("New websocket connected, IP: %s, Username: %s", c.ClientIP(), session.Username)
|
||||
var chatRole model.ChatRole
|
||||
res = h.DB.First(&chatRole, roleId)
|
||||
if res.Error != nil || !chatRole.Enable {
|
||||
utils.ReplyMessage(client, "当前聊天角色不存在或者未启用,连接已关闭!!!")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
h.Init()
|
||||
|
||||
@ -235,7 +243,7 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
|
||||
break
|
||||
}
|
||||
|
||||
var tools = make([]interface{}, 0)
|
||||
var tools = make([]types.Tool, 0)
|
||||
for _, v := range items {
|
||||
var parameters map[string]interface{}
|
||||
err = utils.JsonDecode(v.Parameters, ¶meters)
|
||||
@ -244,15 +252,20 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
|
||||
}
|
||||
required := parameters["required"]
|
||||
delete(parameters, "required")
|
||||
tools = append(tools, gin.H{
|
||||
"type": "function",
|
||||
"function": gin.H{
|
||||
"name": v.Name,
|
||||
"description": v.Description,
|
||||
"parameters": parameters,
|
||||
"required": required,
|
||||
tool := types.Tool{
|
||||
Type: "function",
|
||||
Function: types.Function{
|
||||
Name: v.Name,
|
||||
Description: v.Description,
|
||||
Parameters: parameters,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Fixed: compatible for gpt4-turbo-xxx model
|
||||
if !strings.HasPrefix(req.Model, "gpt-4-turbo-") {
|
||||
tool.Function.Required = required
|
||||
}
|
||||
tools = append(tools, tool)
|
||||
}
|
||||
|
||||
if len(tools) > 0 {
|
||||
@ -332,6 +345,34 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
|
||||
Content: prompt,
|
||||
})
|
||||
req.Input["messages"] = reqMgs
|
||||
} else if session.Model.Platform == types.OpenAI { // extract image for gpt-vision model
|
||||
imgURLs := utils.ExtractImgURL(prompt)
|
||||
logger.Debugf("detected IMG: %+v", imgURLs)
|
||||
var content interface{}
|
||||
if len(imgURLs) > 0 {
|
||||
data := make([]interface{}, 0)
|
||||
text := prompt
|
||||
for _, v := range imgURLs {
|
||||
text = strings.Replace(text, v, "", 1)
|
||||
data = append(data, gin.H{
|
||||
"type": "image_url",
|
||||
"image_url": gin.H{
|
||||
"url": v,
|
||||
},
|
||||
})
|
||||
}
|
||||
data = append(data, gin.H{
|
||||
"type": "text",
|
||||
"text": text,
|
||||
})
|
||||
content = data
|
||||
} else {
|
||||
content = prompt
|
||||
}
|
||||
req.Messages = append(reqMgs, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": content,
|
||||
})
|
||||
} else {
|
||||
req.Messages = append(reqMgs, map[string]interface{}{
|
||||
"role": "user",
|
||||
@ -339,6 +380,8 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
|
||||
})
|
||||
}
|
||||
|
||||
logger.Debugf("%+v", req.Messages)
|
||||
|
||||
switch session.Model.Platform {
|
||||
case types.Azure:
|
||||
return h.sendAzureMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
|
||||
@ -426,13 +469,29 @@ func (h *ChatHandler) StopGenerate(c *gin.Context) {
|
||||
|
||||
// 发送请求到 OpenAI 服务器
|
||||
// useOwnApiKey: 是否使用了用户自己的 API KEY
|
||||
func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platform types.Platform, apiKey *model.ApiKey) (*http.Response, error) {
|
||||
res := h.DB.Where("platform = ?", platform).Where("type = ?", "chat").Where("enabled = ?", true).Order("last_used_at ASC").First(apiKey)
|
||||
if res.Error != nil {
|
||||
func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, session *types.ChatSession, apiKey *model.ApiKey) (*http.Response, error) {
|
||||
// if the chat model bind a KEY, use it directly
|
||||
if session.Model.KeyId > 0 {
|
||||
h.DB.Debug().Where("id", session.Model.KeyId).Find(apiKey)
|
||||
}
|
||||
// use the last unused key
|
||||
if apiKey.Id == 0 {
|
||||
h.DB.Debug().Where("platform = ?", session.Model.Platform).Where("type = ?", "chat").Where("enabled = ?", true).Order("last_used_at ASC").First(apiKey)
|
||||
}
|
||||
if apiKey.Id == 0 {
|
||||
return nil, errors.New("no available key, please import key")
|
||||
}
|
||||
|
||||
// ONLY allow apiURL in blank list
|
||||
if session.Model.Platform == types.OpenAI {
|
||||
err := h.licenseService.IsValidApiURL(apiKey.ApiURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var apiURL string
|
||||
switch platform {
|
||||
switch session.Model.Platform {
|
||||
case types.Azure:
|
||||
md := strings.Replace(req.Model, ".", "", 1)
|
||||
apiURL = strings.Replace(apiKey.ApiURL, "{model}", md, 1)
|
||||
@ -455,7 +514,7 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platf
|
||||
// 更新 API KEY 的最后使用时间
|
||||
h.DB.Model(apiKey).UpdateColumn("last_used_at", time.Now().Unix())
|
||||
// 百度文心,需要串接 access_token
|
||||
if platform == types.Baidu {
|
||||
if session.Model.Platform == types.Baidu {
|
||||
token, err := h.getBaiduToken(apiKey.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -479,7 +538,6 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platf
|
||||
|
||||
request = request.WithContext(ctx)
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
var proxyURL string
|
||||
if len(apiKey.ProxyURL) > 5 { // 使用代理
|
||||
proxy, _ := url.Parse(apiKey.ProxyURL)
|
||||
client = &http.Client{
|
||||
@ -490,8 +548,8 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platf
|
||||
} else {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
logger.Debugf("Sending %s request, ApiURL:%s, API KEY:%s, PROXY: %s, Model: %s", platform, apiURL, apiKey.Value, proxyURL, req.Model)
|
||||
switch platform {
|
||||
logger.Debugf("Sending %s request, ApiURL:%s, API KEY:%s, PROXY: %s, Model: %s", session.Model.Platform, apiURL, apiKey.Value, apiKey.ProxyURL, req.Model)
|
||||
switch session.Model.Platform {
|
||||
case types.Azure:
|
||||
request.Header.Set("api-key", apiKey.Value)
|
||||
break
|
||||
|
@ -31,7 +31,7 @@ func (h *ChatHandler) sendChatGLMMessage(
|
||||
promptCreatedAt := time.Now() // 记录提问时间
|
||||
start := time.Now()
|
||||
var apiKey = model.ApiKey{}
|
||||
response, err := h.doRequest(ctx, req, session.Model.Platform, &apiKey)
|
||||
response, err := h.doRequest(ctx, req, session, &apiKey)
|
||||
logger.Info("HTTP请求完成,耗时:", time.Now().Sub(start))
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "context canceled") {
|
||||
|
@ -31,7 +31,7 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
promptCreatedAt := time.Now() // 记录提问时间
|
||||
start := time.Now()
|
||||
var apiKey = model.ApiKey{}
|
||||
response, err := h.doRequest(ctx, req, session.Model.Platform, &apiKey)
|
||||
response, err := h.doRequest(ctx, req, session, &apiKey)
|
||||
logger.Info("HTTP请求完成,耗时:", time.Now().Sub(start))
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
@ -74,6 +74,10 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
utils.ReplyMessage(ws, ErrImg)
|
||||
break
|
||||
}
|
||||
if responseBody.Choices[0].FinishReason == "stop" && len(contents) == 0 {
|
||||
utils.ReplyMessage(ws, "抱歉😔😔😔,AI助手由于未知原因已经停止输出内容。")
|
||||
break
|
||||
}
|
||||
|
||||
var tool types.ToolCall
|
||||
if len(responseBody.Choices[0].Delta.ToolCalls) > 0 {
|
||||
@ -98,8 +102,10 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
res := h.DB.Where("name = ?", tool.Function.Name).First(&function)
|
||||
if res.Error == nil {
|
||||
toolCall = true
|
||||
callMsg := fmt.Sprintf("正在调用工具 `%s` 作答 ...\n\n", function.Label)
|
||||
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
|
||||
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: fmt.Sprintf("正在调用工具 `%s` 作答 ...\n\n", function.Label)})
|
||||
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: callMsg})
|
||||
contents = append(contents, callMsg)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ func (h *ChatHandler) sendQWenMessage(
|
||||
promptCreatedAt := time.Now() // 记录提问时间
|
||||
start := time.Now()
|
||||
var apiKey = model.ApiKey{}
|
||||
response, err := h.doRequest(ctx, req, session.Model.Platform, &apiKey)
|
||||
response, err := h.doRequest(ctx, req, session, &apiKey)
|
||||
logger.Info("HTTP请求完成,耗时:", time.Now().Sub(start))
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "context canceled") {
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gorilla/websocket"
|
||||
"gorm.io/gorm"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
@ -69,7 +70,15 @@ func (h *ChatHandler) sendXunFeiMessage(
|
||||
ws *types.WsClient) error {
|
||||
promptCreatedAt := time.Now() // 记录提问时间
|
||||
var apiKey model.ApiKey
|
||||
res := h.DB.Where("platform = ?", session.Model.Platform).Where("type = ?", "chat").Where("enabled = ?", true).Order("last_used_at ASC").First(&apiKey)
|
||||
var res *gorm.DB
|
||||
// use the bind key
|
||||
if session.Model.KeyId > 0 {
|
||||
res = h.DB.Where("id", session.Model.KeyId).Find(&apiKey)
|
||||
}
|
||||
// use the last unused key
|
||||
if res.Error != nil {
|
||||
res = h.DB.Where("platform = ?", session.Model.Platform).Where("type = ?", "chat").Where("enabled = ?", true).Order("last_used_at ASC").First(&apiKey)
|
||||
}
|
||||
if res.Error != nil {
|
||||
utils.ReplyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
|
||||
return nil
|
||||
|
255
api/handler/dalle_handler.go
Normal file
@ -0,0 +1,255 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"chatplus/core"
|
||||
"chatplus/core/types"
|
||||
"chatplus/service/dalle"
|
||||
"chatplus/service/oss"
|
||||
"chatplus/store/model"
|
||||
"chatplus/store/vo"
|
||||
"chatplus/utils"
|
||||
"chatplus/utils/resp"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DallJobHandler struct {
|
||||
BaseHandler
|
||||
redis *redis.Client
|
||||
service *dalle.Service
|
||||
uploader *oss.UploaderManager
|
||||
}
|
||||
|
||||
func NewDallJobHandler(app *core.AppServer, db *gorm.DB, service *dalle.Service, manager *oss.UploaderManager) *DallJobHandler {
|
||||
return &DallJobHandler{
|
||||
service: service,
|
||||
uploader: manager,
|
||||
BaseHandler: BaseHandler{
|
||||
App: app,
|
||||
DB: db,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Client WebSocket 客户端,用于通知任务状态变更
|
||||
func (h *DallJobHandler) Client(c *gin.Context) {
|
||||
ws, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}).Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
userId := h.GetInt(c, "user_id", 0)
|
||||
if userId == 0 {
|
||||
logger.Info("Invalid user ID")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
client := types.NewWsClient(ws)
|
||||
h.service.Clients.Put(uint(userId), client)
|
||||
logger.Infof("New websocket connected, IP: %s", c.RemoteIP())
|
||||
go func() {
|
||||
for {
|
||||
_, msg, err := client.Receive()
|
||||
if err != nil {
|
||||
client.Close()
|
||||
h.service.Clients.Delete(uint(userId))
|
||||
return
|
||||
}
|
||||
|
||||
var message types.WsMessage
|
||||
err = utils.JsonDecode(string(msg), &message)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 心跳消息
|
||||
if message.Type == "heartbeat" {
|
||||
logger.Debug("收到 DallE 心跳消息:", message.Content)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (h *DallJobHandler) preCheck(c *gin.Context) bool {
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return false
|
||||
}
|
||||
|
||||
if user.Power < h.App.SysConfig.SdPower {
|
||||
resp.ERROR(c, "当前用户剩余算力不足以完成本次绘画!")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
// Image 创建一个绘画任务
|
||||
func (h *DallJobHandler) Image(c *gin.Context) {
|
||||
if !h.preCheck(c) {
|
||||
return
|
||||
}
|
||||
|
||||
var data types.DallTask
|
||||
if err := c.ShouldBindJSON(&data); err != nil || data.Prompt == "" {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
idValue, _ := c.Get(types.LoginUserID)
|
||||
userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
|
||||
job := model.DallJob{
|
||||
UserId: uint(userId),
|
||||
Prompt: data.Prompt,
|
||||
Power: h.App.SysConfig.DallPower,
|
||||
}
|
||||
res := h.DB.Create(&job)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "error with save job: "+res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.service.PushTask(types.DallTask{
|
||||
JobId: job.Id,
|
||||
UserId: uint(userId),
|
||||
Prompt: data.Prompt,
|
||||
Quality: data.Quality,
|
||||
Size: data.Size,
|
||||
Style: data.Style,
|
||||
Power: job.Power,
|
||||
})
|
||||
|
||||
client := h.service.Clients.Get(job.UserId)
|
||||
if client != nil {
|
||||
_ = client.Send([]byte("Task Updated"))
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// ImgWall 照片墙
|
||||
func (h *DallJobHandler) ImgWall(c *gin.Context) {
|
||||
page := h.GetInt(c, "page", 0)
|
||||
pageSize := h.GetInt(c, "page_size", 0)
|
||||
err, jobs := h.getData(true, 0, page, pageSize, true)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, jobs)
|
||||
}
|
||||
|
||||
// JobList 获取 SD 任务列表
|
||||
func (h *DallJobHandler) JobList(c *gin.Context) {
|
||||
status := h.GetBool(c, "status")
|
||||
userId := h.GetLoginUserId(c)
|
||||
page := h.GetInt(c, "page", 0)
|
||||
pageSize := h.GetInt(c, "page_size", 0)
|
||||
publish := h.GetBool(c, "publish")
|
||||
|
||||
err, jobs := h.getData(status, userId, page, pageSize, publish)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, jobs)
|
||||
}
|
||||
|
||||
// JobList 获取任务列表
|
||||
func (h *DallJobHandler) getData(finish bool, userId uint, page int, pageSize int, publish bool) (error, []vo.DallJob) {
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if finish {
|
||||
session = session.Where("progress = ?", 100).Order("id DESC")
|
||||
} else {
|
||||
session = session.Where("progress < ?", 100).Order("id ASC")
|
||||
}
|
||||
if userId > 0 {
|
||||
session = session.Where("user_id = ?", userId)
|
||||
}
|
||||
if publish {
|
||||
session = session.Where("publish", publish)
|
||||
}
|
||||
if page > 0 && pageSize > 0 {
|
||||
offset := (page - 1) * pageSize
|
||||
session = session.Offset(offset).Limit(pageSize)
|
||||
}
|
||||
|
||||
var items []model.DallJob
|
||||
res := session.Find(&items)
|
||||
if res.Error != nil {
|
||||
return res.Error, nil
|
||||
}
|
||||
|
||||
var jobs = make([]vo.DallJob, 0)
|
||||
for _, item := range items {
|
||||
var job vo.DallJob
|
||||
err := utils.CopyObject(item, &job)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
jobs = append(jobs, job)
|
||||
}
|
||||
|
||||
return nil, jobs
|
||||
}
|
||||
|
||||
// Remove remove task image
|
||||
func (h *DallJobHandler) Remove(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
UserId uint `json:"user_id"`
|
||||
ImgURL string `json:"img_url"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
// remove job recode
|
||||
res := h.DB.Delete(&model.DallJob{Id: data.Id})
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// remove image
|
||||
err := h.uploader.GetUploadHandler().Delete(data.ImgURL)
|
||||
if err != nil {
|
||||
logger.Error("remove image failed: ", err)
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
// Publish 发布/取消发布图片到画廊显示
|
||||
func (h *DallJobHandler) Publish(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
Action bool `json:"action"` // 发布动作,true => 发布,false => 取消分享
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
res := h.DB.Model(&model.DallJob{Id: data.Id}).UpdateColumn("publish", true)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "更新数据库失败")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
@ -3,27 +3,35 @@ package handler
|
||||
import (
|
||||
"chatplus/core"
|
||||
"chatplus/core/types"
|
||||
"chatplus/service/dalle"
|
||||
"chatplus/service/oss"
|
||||
"chatplus/store/model"
|
||||
"chatplus/utils"
|
||||
"chatplus/utils/resp"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/imroc/req/v3"
|
||||
"gorm.io/gorm"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type FunctionHandler struct {
|
||||
BaseHandler
|
||||
config types.ChatPlusApiConfig
|
||||
config types.ApiConfig
|
||||
uploadManager *oss.UploaderManager
|
||||
dallService *dalle.Service
|
||||
}
|
||||
|
||||
func NewFunctionHandler(server *core.AppServer, db *gorm.DB, config *types.AppConfig, manager *oss.UploaderManager) *FunctionHandler {
|
||||
func NewFunctionHandler(
|
||||
server *core.AppServer,
|
||||
db *gorm.DB,
|
||||
config *types.AppConfig,
|
||||
manager *oss.UploaderManager,
|
||||
dallService *dalle.Service) *FunctionHandler {
|
||||
return &FunctionHandler{
|
||||
BaseHandler: BaseHandler{
|
||||
App: server,
|
||||
@ -31,6 +39,7 @@ func NewFunctionHandler(server *core.AppServer, db *gorm.DB, config *types.AppCo
|
||||
},
|
||||
config: config.ApiConfig,
|
||||
uploadManager: manager,
|
||||
dallService: dallService,
|
||||
}
|
||||
}
|
||||
|
||||
@ -151,30 +160,6 @@ func (h *FunctionHandler) ZaoBao(c *gin.Context) {
|
||||
resp.SUCCESS(c, strings.Join(builder, "\n\n"))
|
||||
}
|
||||
|
||||
type imgReq struct {
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt"`
|
||||
N int `json:"n"`
|
||||
Size string `json:"size"`
|
||||
}
|
||||
|
||||
type imgRes struct {
|
||||
Created int64 `json:"created"`
|
||||
Data []struct {
|
||||
RevisedPrompt string `json:"revised_prompt"`
|
||||
Url string `json:"url"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type ErrRes struct {
|
||||
Error struct {
|
||||
Code interface{} `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Param interface{} `json:"param"`
|
||||
Type string `json:"type"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
// Dall3 DallE3 AI 绘图
|
||||
func (h *FunctionHandler) Dall3(c *gin.Context) {
|
||||
if err := h.checkAuth(c); err != nil {
|
||||
@ -190,85 +175,40 @@ func (h *FunctionHandler) Dall3(c *gin.Context) {
|
||||
|
||||
logger.Debugf("绘画参数:%+v", params)
|
||||
var user model.User
|
||||
tx := h.DB.Where("id = ?", params["user_id"]).First(&user)
|
||||
if tx.Error != nil {
|
||||
res := h.DB.Where("id = ?", params["user_id"]).First(&user)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "当前用户不存在!")
|
||||
return
|
||||
}
|
||||
|
||||
if user.Power < h.App.SysConfig.DallPower {
|
||||
resp.ERROR(c, "当前用户剩余算力不足以完成本次绘画!")
|
||||
return
|
||||
}
|
||||
|
||||
// create dall task
|
||||
prompt := utils.InterfaceToString(params["prompt"])
|
||||
// get image generation API KEY
|
||||
var apiKey model.ApiKey
|
||||
tx = h.DB.Where("platform = ?", types.OpenAI).Where("type = ?", "img").Where("enabled = ?", true).Order("last_used_at ASC").First(&apiKey)
|
||||
if tx.Error != nil {
|
||||
resp.ERROR(c, "获取绘图 API KEY 失败: "+tx.Error.Error())
|
||||
job := model.DallJob{
|
||||
UserId: user.Id,
|
||||
Prompt: prompt,
|
||||
Power: h.App.SysConfig.DallPower,
|
||||
}
|
||||
res = h.DB.Create(&job)
|
||||
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "创建 DALL-E 绘图任务失败:"+res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// translate prompt
|
||||
const translatePromptTemplate = "Translate the following painting prompt words into English keyword phrases. Without any explanation, directly output the keyword phrases separated by commas. The content to be translated is: [%s]"
|
||||
pt, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(translatePromptTemplate, params["prompt"]))
|
||||
if err == nil {
|
||||
logger.Debugf("翻译绘画提示词,原文:%s,译文:%s", prompt, pt)
|
||||
prompt = pt
|
||||
}
|
||||
var res imgRes
|
||||
var errRes ErrRes
|
||||
var request *req.Request
|
||||
if len(apiKey.ProxyURL) > 5 {
|
||||
request = req.C().SetProxyURL(apiKey.ProxyURL).R()
|
||||
} else {
|
||||
request = req.C().R()
|
||||
}
|
||||
logger.Debugf("Sending %s request, ApiURL:%s, API KEY:%s, PROXY: %s", apiKey.Platform, apiKey.ApiURL, apiKey.Value, apiKey.ProxyURL)
|
||||
r, err := request.SetHeader("Content-Type", "application/json").
|
||||
SetHeader("Authorization", "Bearer "+apiKey.Value).
|
||||
SetBody(imgReq{
|
||||
Model: "dall-e-3",
|
||||
Prompt: prompt,
|
||||
N: 1,
|
||||
Size: "1024x1024",
|
||||
}).
|
||||
SetErrorResult(&errRes).
|
||||
SetSuccessResult(&res).Post(apiKey.ApiURL)
|
||||
if r.IsErrorState() {
|
||||
resp.ERROR(c, "请求 OpenAI API 失败: "+errRes.Error.Message)
|
||||
return
|
||||
}
|
||||
// 更新 API KEY 的最后使用时间
|
||||
h.DB.Model(&apiKey).UpdateColumn("last_used_at", time.Now().Unix())
|
||||
logger.Debugf("%+v", res)
|
||||
// 存储图片
|
||||
imgURL, err := h.uploadManager.GetUploadHandler().PutImg(res.Data[0].Url, false)
|
||||
content, err := h.dallService.Image(types.DallTask{
|
||||
JobId: job.Id,
|
||||
UserId: user.Id,
|
||||
Prompt: job.Prompt,
|
||||
N: 1,
|
||||
Quality: "standard",
|
||||
Size: "1024x1024",
|
||||
Style: "vivid",
|
||||
Power: job.Power,
|
||||
}, true)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "下载图片失败: "+err.Error())
|
||||
resp.ERROR(c, "任务执行失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
content := fmt.Sprintf("下面是根据您的描述创作的图片,它描绘了 【%s】 的场景。 \n\n\n", prompt, imgURL)
|
||||
// 更新用户算力
|
||||
tx = h.DB.Model(&model.User{}).Where("id", user.Id).UpdateColumn("power", gorm.Expr("power - ?", h.App.SysConfig.DallPower))
|
||||
// 记录算力变化日志
|
||||
if tx.Error == nil && tx.RowsAffected > 0 {
|
||||
var u model.User
|
||||
h.DB.Where("id", user.Id).First(&u)
|
||||
h.DB.Create(&model.PowerLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
Type: types.PowerConsume,
|
||||
Amount: h.App.SysConfig.DallPower,
|
||||
Balance: u.Power,
|
||||
Mark: types.PowerSub,
|
||||
Model: "dall-e-3",
|
||||
Remark: fmt.Sprintf("绘画提示词:%s", utils.CutWords(prompt, 10)),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, content)
|
||||
}
|
||||
|
227
api/handler/markmap_handler.go
Normal file
@ -0,0 +1,227 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"chatplus/core"
|
||||
"chatplus/core/types"
|
||||
"chatplus/store/model"
|
||||
"chatplus/utils"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"gorm.io/gorm"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MarkMapHandler 生成思维导图
|
||||
type MarkMapHandler struct {
|
||||
BaseHandler
|
||||
clients *types.LMap[int, *types.WsClient]
|
||||
}
|
||||
|
||||
func NewMarkMapHandler(app *core.AppServer, db *gorm.DB) *MarkMapHandler {
|
||||
return &MarkMapHandler{
|
||||
BaseHandler: BaseHandler{App: app, DB: db},
|
||||
clients: types.NewLMap[int, *types.WsClient](),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *MarkMapHandler) Client(c *gin.Context) {
|
||||
ws, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}).Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
modelId := h.GetInt(c, "model_id", 0)
|
||||
userId := h.GetInt(c, "user_id", 0)
|
||||
|
||||
client := types.NewWsClient(ws)
|
||||
h.clients.Put(userId, client)
|
||||
go func() {
|
||||
for {
|
||||
_, msg, err := client.Receive()
|
||||
if err != nil {
|
||||
client.Close()
|
||||
h.clients.Delete(userId)
|
||||
return
|
||||
}
|
||||
|
||||
var message types.WsMessage
|
||||
err = utils.JsonDecode(string(msg), &message)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 心跳消息
|
||||
if message.Type == "heartbeat" {
|
||||
logger.Debug("收到 MarkMap 心跳消息:", message.Content)
|
||||
continue
|
||||
}
|
||||
// change model
|
||||
if message.Type == "model_id" {
|
||||
modelId = utils.IntValue(utils.InterfaceToString(message.Content), 0)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Info("Receive a message: ", message.Content)
|
||||
err = h.sendMessage(client, utils.InterfaceToString(message.Content), modelId, userId)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
utils.ReplyChunkMessage(client, types.WsMessage{Type: types.WsErr, Content: err.Error()})
|
||||
}
|
||||
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (h *MarkMapHandler) sendMessage(client *types.WsClient, prompt string, modelId int, userId int) error {
|
||||
var user model.User
|
||||
res := h.DB.Model(&model.User{}).First(&user, userId)
|
||||
if res.Error != nil {
|
||||
return fmt.Errorf("error with query user info: %v", res.Error)
|
||||
}
|
||||
var chatModel model.ChatModel
|
||||
res = h.DB.Where("id", modelId).First(&chatModel)
|
||||
if res.Error != nil {
|
||||
return fmt.Errorf("error with query chat model: %v", res.Error)
|
||||
}
|
||||
|
||||
if user.Status == false {
|
||||
return errors.New("当前用户被禁用")
|
||||
}
|
||||
|
||||
if user.Power < chatModel.Power {
|
||||
return fmt.Errorf("您当前剩余算力(%d)已不足以支付当前模型算力(%d)!", user.Power, chatModel.Power)
|
||||
}
|
||||
|
||||
messages := make([]interface{}, 0)
|
||||
messages = append(messages, types.Message{Role: "system", Content: "你是一位非常优秀的思维导图助手,你会把用户的所有提问都总结成思维导图,然后以 Markdown 格式输出。不要输出任何解释性的语句。"})
|
||||
messages = append(messages, types.Message{Role: "user", Content: prompt})
|
||||
var req = types.ApiRequest{
|
||||
Model: chatModel.Value,
|
||||
Stream: true,
|
||||
Messages: messages,
|
||||
}
|
||||
|
||||
var apiKey model.ApiKey
|
||||
response, err := h.doRequest(req, chatModel, &apiKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("请求 OpenAI API 失败: %s", err)
|
||||
}
|
||||
|
||||
defer response.Body.Close()
|
||||
|
||||
contentType := response.Header.Get("Content-Type")
|
||||
if strings.Contains(contentType, "text/event-stream") {
|
||||
// 循环读取 Chunk 消息
|
||||
var message = types.Message{}
|
||||
scanner := bufio.NewScanner(response.Body)
|
||||
var isNew = true
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if !strings.Contains(line, "data:") || len(line) < 30 {
|
||||
continue
|
||||
}
|
||||
|
||||
var responseBody = types.ApiResponse{}
|
||||
err = json.Unmarshal([]byte(line[6:]), &responseBody)
|
||||
if err != nil || len(responseBody.Choices) == 0 { // 数据解析出错
|
||||
return fmt.Errorf("error with decode data: %v", err)
|
||||
}
|
||||
|
||||
// 初始化 role
|
||||
if responseBody.Choices[0].Delta.Role != "" && message.Role == "" {
|
||||
message.Role = responseBody.Choices[0].Delta.Role
|
||||
continue
|
||||
} else if responseBody.Choices[0].FinishReason != "" {
|
||||
break // 输出完成或者输出中断了
|
||||
} else {
|
||||
if isNew {
|
||||
utils.ReplyChunkMessage(client, types.WsMessage{Type: types.WsStart})
|
||||
isNew = false
|
||||
}
|
||||
utils.ReplyChunkMessage(client, types.WsMessage{
|
||||
Type: types.WsMiddle,
|
||||
Content: utils.InterfaceToString(responseBody.Choices[0].Delta.Content),
|
||||
})
|
||||
}
|
||||
} // end for
|
||||
|
||||
utils.ReplyChunkMessage(client, types.WsMessage{Type: types.WsEnd})
|
||||
|
||||
} else {
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取响应失败: %v", err)
|
||||
}
|
||||
var res types.ApiError
|
||||
err = json.Unmarshal(body, &res)
|
||||
if err != nil {
|
||||
return fmt.Errorf("解析响应失败: %v", err)
|
||||
}
|
||||
|
||||
// OpenAI API 调用异常处理
|
||||
if strings.Contains(res.Error.Message, "This key is associated with a deactivated account") {
|
||||
// remove key
|
||||
h.DB.Where("value = ?", apiKey).Delete(&model.ApiKey{})
|
||||
return errors.New("请求 OpenAI API 失败:API KEY 所关联的账户被禁用。")
|
||||
} else if strings.Contains(res.Error.Message, "You exceeded your current quota") {
|
||||
return errors.New("请求 OpenAI API 失败:API KEY 触发并发限制,请稍后再试。")
|
||||
} else {
|
||||
return fmt.Errorf("请求 OpenAI API 失败:%v", res.Error.Message)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MarkMapHandler) doRequest(req types.ApiRequest, chatModel model.ChatModel, apiKey *model.ApiKey) (*http.Response, error) {
|
||||
// if the chat model bind a KEY, use it directly
|
||||
var res *gorm.DB
|
||||
if chatModel.KeyId > 0 {
|
||||
res = h.DB.Where("id", chatModel.KeyId).Find(apiKey)
|
||||
}
|
||||
// use the last unused key
|
||||
if res.Error != nil {
|
||||
res = h.DB.Where("platform = ?", types.OpenAI).Where("type = ?", "chat").Where("enabled = ?", true).Order("last_used_at ASC").First(apiKey)
|
||||
}
|
||||
if res.Error != nil {
|
||||
return nil, errors.New("no available key, please import key")
|
||||
}
|
||||
apiURL := apiKey.ApiURL
|
||||
// 更新 API KEY 的最后使用时间
|
||||
h.DB.Model(apiKey).UpdateColumn("last_used_at", time.Now().Unix())
|
||||
|
||||
// 创建 HttpClient 请求对象
|
||||
var client *http.Client
|
||||
requestBody, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
request, err := http.NewRequest(http.MethodPost, apiURL, bytes.NewBuffer(requestBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
if len(apiKey.ProxyURL) > 5 { // 使用代理
|
||||
proxy, _ := url.Parse(apiKey.ProxyURL)
|
||||
client = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyURL(proxy),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey.Value))
|
||||
return client.Do(request)
|
||||
}
|
@ -146,7 +146,7 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
|
||||
}
|
||||
|
||||
if data.SRef != "" {
|
||||
params += fmt.Sprintf(" --sref %s", data.CRef)
|
||||
params += fmt.Sprintf(" --sref %s", data.SRef)
|
||||
}
|
||||
if data.Model != "" && !strings.Contains(params, "--v") && !strings.Contains(params, "--niji") {
|
||||
params += fmt.Sprintf(" %s", data.Model)
|
||||
|
@ -65,7 +65,7 @@ func (h *SdJobHandler) Client(c *gin.Context) {
|
||||
logger.Infof("New websocket connected, IP: %s", c.RemoteIP())
|
||||
}
|
||||
|
||||
func (h *SdJobHandler) checkLimits(c *gin.Context) bool {
|
||||
func (h *SdJobHandler) preCheck(c *gin.Context) bool {
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
@ -88,7 +88,7 @@ func (h *SdJobHandler) checkLimits(c *gin.Context) bool {
|
||||
|
||||
// Image 创建一个绘画任务
|
||||
func (h *SdJobHandler) Image(c *gin.Context) {
|
||||
if !h.checkLimits(c) {
|
||||
if !h.preCheck(c) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -260,9 +260,10 @@ func (h *SdJobHandler) getData(finish bool, userId uint, page int, pageSize int,
|
||||
|
||||
if item.Progress < 100 {
|
||||
// 从 leveldb 中获取图片预览数据
|
||||
imageData, err := h.leveldb.Get(item.TaskId)
|
||||
var imageData string
|
||||
err = h.leveldb.Get(item.TaskId, &imageData)
|
||||
if err == nil {
|
||||
job.ImgURL = "data:image/png;base64," + string(imageData)
|
||||
job.ImgURL = "data:image/png;base64," + imageData
|
||||
}
|
||||
}
|
||||
jobs = append(jobs, job)
|
||||
@ -298,7 +299,7 @@ func (h *SdJobHandler) Remove(c *gin.Context) {
|
||||
|
||||
client := h.pool.Clients.Get(data.UserId)
|
||||
if client != nil {
|
||||
_ = client.Send([]byte("Task Updated"))
|
||||
_ = client.Send([]byte(sd.Finished))
|
||||
}
|
||||
|
||||
resp.SUCCESS(c)
|
||||
|
36
api/main.go
@ -8,6 +8,7 @@ import (
|
||||
"chatplus/handler/chatimpl"
|
||||
logger2 "chatplus/logger"
|
||||
"chatplus/service"
|
||||
"chatplus/service/dalle"
|
||||
"chatplus/service/mj"
|
||||
"chatplus/service/oss"
|
||||
"chatplus/service/payment"
|
||||
@ -43,13 +44,13 @@ type AppLifecycle struct {
|
||||
|
||||
// OnStart 应用程序启动时执行
|
||||
func (l *AppLifecycle) OnStart(context.Context) error {
|
||||
log.Println("AppLifecycle OnStart")
|
||||
logger.Info("AppLifecycle OnStart")
|
||||
return nil
|
||||
}
|
||||
|
||||
// OnStop 应用程序停止时执行
|
||||
func (l *AppLifecycle) OnStop(context.Context) error {
|
||||
log.Println("AppLifecycle OnStop")
|
||||
logger.Info("AppLifecycle OnStop")
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -153,9 +154,18 @@ func main() {
|
||||
}),
|
||||
fx.Provide(oss.NewUploaderManager),
|
||||
fx.Provide(mj.NewService),
|
||||
fx.Provide(dalle.NewService),
|
||||
fx.Invoke(func(service *dalle.Service) {
|
||||
service.Run()
|
||||
service.CheckTaskNotify()
|
||||
service.DownloadImages()
|
||||
service.CheckTaskStatus()
|
||||
}),
|
||||
|
||||
// 邮件服务
|
||||
fx.Provide(service.NewSmtpService),
|
||||
// License 服务
|
||||
fx.Provide(service.NewLicenseService),
|
||||
|
||||
// 微信机器人服务
|
||||
fx.Provide(wx.NewWeChatBot),
|
||||
@ -277,9 +287,10 @@ func main() {
|
||||
|
||||
// 管理后台控制器
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ConfigHandler) {
|
||||
group := s.Engine.Group("/api/admin/config/")
|
||||
group.POST("update", h.Update)
|
||||
group.GET("get", h.Get)
|
||||
group := s.Engine.Group("/api/admin/")
|
||||
group.POST("config/update", h.Update)
|
||||
group.GET("config/get", h.Get)
|
||||
group.POST("active", h.Active)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ManagerHandler) {
|
||||
group := s.Engine.Group("/api/admin/")
|
||||
@ -436,6 +447,21 @@ func main() {
|
||||
group := s.Engine.Group("/api/menu/")
|
||||
group.GET("list", h.List)
|
||||
}),
|
||||
fx.Provide(handler.NewMarkMapHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.MarkMapHandler) {
|
||||
group := s.Engine.Group("/api/markMap/")
|
||||
group.Any("client", h.Client)
|
||||
}),
|
||||
fx.Provide(handler.NewDallJobHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.DallJobHandler) {
|
||||
group := s.Engine.Group("/api/dall")
|
||||
group.Any("client", h.Client)
|
||||
group.POST("image", h.Image)
|
||||
group.GET("jobs", h.JobList)
|
||||
group.GET("imgWall", h.ImgWall)
|
||||
group.POST("remove", h.Remove)
|
||||
group.POST("publish", h.Publish)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, db *gorm.DB) {
|
||||
go func() {
|
||||
err := s.Run(db)
|
||||
|
@ -9,11 +9,11 @@ import (
|
||||
)
|
||||
|
||||
type CaptchaService struct {
|
||||
config types.ChatPlusApiConfig
|
||||
config types.ApiConfig
|
||||
client *req.Client
|
||||
}
|
||||
|
||||
func NewCaptchaService(config types.ChatPlusApiConfig) *CaptchaService {
|
||||
func NewCaptchaService(config types.ApiConfig) *CaptchaService {
|
||||
return &CaptchaService{
|
||||
config: config,
|
||||
client: req.C().SetTimeout(10 * time.Second),
|
||||
|
300
api/service/dalle/service.go
Normal file
@ -0,0 +1,300 @@
|
||||
package dalle
|
||||
|
||||
import (
|
||||
"chatplus/core/types"
|
||||
logger2 "chatplus/logger"
|
||||
"chatplus/service"
|
||||
"chatplus/service/oss"
|
||||
"chatplus/service/sd"
|
||||
"chatplus/store"
|
||||
"chatplus/store/model"
|
||||
"chatplus/utils"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"time"
|
||||
|
||||
"github.com/imroc/req/v3"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
// DALL-E 绘画服务
|
||||
|
||||
type Service struct {
|
||||
httpClient *req.Client
|
||||
db *gorm.DB
|
||||
uploadManager *oss.UploaderManager
|
||||
taskQueue *store.RedisQueue
|
||||
notifyQueue *store.RedisQueue
|
||||
Clients *types.LMap[uint, *types.WsClient] // UserId => Client
|
||||
}
|
||||
|
||||
func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Client) *Service {
|
||||
return &Service{
|
||||
httpClient: req.C().SetTimeout(time.Minute * 3),
|
||||
db: db,
|
||||
taskQueue: store.NewRedisQueue("DallE_Task_Queue", redisCli),
|
||||
notifyQueue: store.NewRedisQueue("DallE_Notify_Queue", redisCli),
|
||||
Clients: types.NewLMap[uint, *types.WsClient](),
|
||||
uploadManager: manager,
|
||||
}
|
||||
}
|
||||
|
||||
// PushTask push a new mj task in to task queue
|
||||
func (s *Service) PushTask(task types.DallTask) {
|
||||
logger.Debugf("add a new MidJourney task to the task list: %+v", task)
|
||||
s.taskQueue.RPush(task)
|
||||
}
|
||||
|
||||
func (s *Service) Run() {
|
||||
go func() {
|
||||
for {
|
||||
var task types.DallTask
|
||||
err := s.taskQueue.LPop(&task)
|
||||
if err != nil {
|
||||
logger.Errorf("taking task with error: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = s.Image(task, false)
|
||||
if err != nil {
|
||||
logger.Errorf("error with image task: %v", err)
|
||||
s.db.Model(&model.DallJob{Id: task.JobId}).UpdateColumns(map[string]interface{}{
|
||||
"progress": -1,
|
||||
"err_msg": err.Error(),
|
||||
})
|
||||
s.notifyQueue.RPush(sd.NotifyMessage{UserId: int(task.UserId), JobId: int(task.JobId), Message: sd.Failed})
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
type imgReq struct {
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt"`
|
||||
N int `json:"n"`
|
||||
Size string `json:"size"`
|
||||
Quality string `json:"quality"`
|
||||
Style string `json:"style"`
|
||||
}
|
||||
|
||||
type imgRes struct {
|
||||
Created int64 `json:"created"`
|
||||
Data []struct {
|
||||
RevisedPrompt string `json:"revised_prompt"`
|
||||
Url string `json:"url"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type ErrRes struct {
|
||||
Error struct {
|
||||
Code interface{} `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Param interface{} `json:"param"`
|
||||
Type string `json:"type"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
|
||||
logger.Debugf("绘画参数:%+v", task)
|
||||
prompt := task.Prompt
|
||||
// translate prompt
|
||||
if utils.HasChinese(task.Prompt) {
|
||||
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, task.Prompt))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with translate prompt: %v", err)
|
||||
}
|
||||
prompt = content
|
||||
logger.Debugf("重写后提示词:%s", prompt)
|
||||
}
|
||||
|
||||
var user model.User
|
||||
s.db.Where("id", task.UserId).First(&user)
|
||||
if user.Power < task.Power {
|
||||
return "", errors.New("insufficient of power")
|
||||
}
|
||||
|
||||
// get image generation API KEY
|
||||
var apiKey model.ApiKey
|
||||
tx := s.db.Where("platform", types.OpenAI).
|
||||
Where("type", "img").
|
||||
Where("enabled", true).
|
||||
Order("last_used_at ASC").First(&apiKey)
|
||||
if tx.Error != nil {
|
||||
return "", fmt.Errorf("no available IMG api key: %v", tx.Error)
|
||||
}
|
||||
|
||||
var res imgRes
|
||||
var errRes ErrRes
|
||||
if len(apiKey.ProxyURL) > 5 {
|
||||
s.httpClient.SetProxyURL(apiKey.ProxyURL).R()
|
||||
}
|
||||
logger.Debugf("Sending %s request, ApiURL:%s, API KEY:%s, PROXY: %s", apiKey.Platform, apiKey.ApiURL, apiKey.Value, apiKey.ProxyURL)
|
||||
r, err := s.httpClient.R().SetHeader("Content-Type", "application/json").
|
||||
SetHeader("Authorization", "Bearer "+apiKey.Value).
|
||||
SetBody(imgReq{
|
||||
Model: "dall-e-3",
|
||||
Prompt: prompt,
|
||||
N: 1,
|
||||
Size: "1024x1024",
|
||||
Style: task.Style,
|
||||
Quality: task.Quality,
|
||||
}).
|
||||
SetErrorResult(&errRes).
|
||||
SetSuccessResult(&res).Post(apiKey.ApiURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with send request: %v", err)
|
||||
}
|
||||
|
||||
if r.IsErrorState() {
|
||||
return "", fmt.Errorf("error with send request: %v", errRes.Error)
|
||||
}
|
||||
// update the api key last use time
|
||||
s.db.Model(&apiKey).UpdateColumn("last_used_at", time.Now().Unix())
|
||||
// update task progress
|
||||
s.db.Model(&model.DallJob{Id: task.JobId}).UpdateColumns(map[string]interface{}{
|
||||
"progress": 100,
|
||||
"org_url": res.Data[0].Url,
|
||||
"prompt": prompt,
|
||||
})
|
||||
|
||||
s.notifyQueue.RPush(sd.NotifyMessage{UserId: int(task.UserId), JobId: int(task.JobId), Message: sd.Finished})
|
||||
var content string
|
||||
if sync {
|
||||
imgURL, err := s.downloadImage(task.JobId, int(task.UserId), res.Data[0].Url)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with download image: %v", err)
|
||||
}
|
||||
content = fmt.Sprintf("```\n%s\n```\n下面是我为你创作的图片:\n\n\n", prompt, imgURL)
|
||||
}
|
||||
|
||||
// 更新用户算力
|
||||
tx = s.db.Model(&model.User{}).Where("id", user.Id).UpdateColumn("power", gorm.Expr("power - ?", task.Power))
|
||||
// 记录算力变化日志
|
||||
if tx.Error == nil && tx.RowsAffected > 0 {
|
||||
var u model.User
|
||||
s.db.Where("id", user.Id).First(&u)
|
||||
s.db.Create(&model.PowerLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
Type: types.PowerConsume,
|
||||
Amount: task.Power,
|
||||
Balance: u.Power,
|
||||
Mark: types.PowerSub,
|
||||
Model: "dall-e-3",
|
||||
Remark: fmt.Sprintf("绘画提示词:%s", utils.CutWords(task.Prompt, 10)),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
return content, nil
|
||||
}
|
||||
|
||||
func (s *Service) CheckTaskNotify() {
|
||||
go func() {
|
||||
logger.Info("Running DALL-E task notify checking ...")
|
||||
for {
|
||||
var message sd.NotifyMessage
|
||||
err := s.notifyQueue.LPop(&message)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
client := s.Clients.Get(uint(message.UserId))
|
||||
if client == nil {
|
||||
continue
|
||||
}
|
||||
err = client.Send([]byte(message.Message))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Service) DownloadImages() {
|
||||
go func() {
|
||||
var items []model.DallJob
|
||||
for {
|
||||
res := s.db.Where("img_url = ? AND progress = ?", "", 100).Find(&items)
|
||||
if res.Error != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// download images
|
||||
for _, v := range items {
|
||||
if v.OrgURL == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Infof("try to download image: %s", v.OrgURL)
|
||||
imgURL, err := s.downloadImage(v.Id, int(v.UserId), v.OrgURL)
|
||||
if err != nil {
|
||||
logger.Error("error with download image: %s, error: %v", imgURL, err)
|
||||
continue
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 5)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Service) downloadImage(jobId uint, userId int, orgURL string) (string, error) {
|
||||
// sava image
|
||||
imgURL, err := s.uploadManager.GetUploadHandler().PutImg(orgURL, false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// update img_url
|
||||
res := s.db.Model(&model.DallJob{Id: jobId}).UpdateColumn("img_url", imgURL)
|
||||
if res.Error != nil {
|
||||
return "", err
|
||||
}
|
||||
s.notifyQueue.RPush(sd.NotifyMessage{UserId: userId, JobId: int(jobId), Message: sd.Failed})
|
||||
return imgURL, nil
|
||||
}
|
||||
|
||||
// CheckTaskStatus 检查任务状态,自动删除过期或者失败的任务
|
||||
func (s *Service) CheckTaskStatus() {
|
||||
go func() {
|
||||
logger.Info("Running Stable-Diffusion task status checking ...")
|
||||
for {
|
||||
var jobs []model.SdJob
|
||||
res := s.db.Where("progress < ?", 100).Find(&jobs)
|
||||
if res.Error != nil {
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, job := range jobs {
|
||||
// 5 分钟还没完成的任务直接删除
|
||||
if time.Now().Sub(job.CreatedAt) > time.Minute*5 || job.Progress == -1 {
|
||||
s.db.Delete(&job)
|
||||
var user model.User
|
||||
s.db.Where("id = ?", job.UserId).First(&user)
|
||||
// 退回绘图次数
|
||||
res = s.db.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power + ?", job.Power))
|
||||
if res.Error == nil && res.RowsAffected > 0 {
|
||||
s.db.Create(&model.PowerLog{
|
||||
UserId: user.Id,
|
||||
Username: user.Username,
|
||||
Type: types.PowerConsume,
|
||||
Amount: job.Power,
|
||||
Balance: user.Power + job.Power,
|
||||
Mark: types.PowerAdd,
|
||||
Model: "dall-e-3",
|
||||
Remark: fmt.Sprintf("任务失败,退回算力。任务ID:%s", job.TaskId),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
||||
}()
|
||||
}
|
108
api/service/license_service.go
Normal file
@ -0,0 +1,108 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"chatplus/core"
|
||||
"chatplus/core/types"
|
||||
"chatplus/store"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/imroc/req/v3"
|
||||
"github.com/shirou/gopsutil/host"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LicenseService struct {
|
||||
config types.ApiConfig
|
||||
levelDB *store.LevelDB
|
||||
license types.License
|
||||
machineId string
|
||||
}
|
||||
|
||||
func NewLicenseService(server *core.AppServer, levelDB *store.LevelDB) * LicenseService {
|
||||
var license types.License
|
||||
var machineId string
|
||||
_ = levelDB.Get(types.LicenseKey, &license)
|
||||
info, err := host.Info()
|
||||
if err == nil {
|
||||
machineId = info.HostID
|
||||
}
|
||||
return &LicenseService{
|
||||
config: server.Config.ApiConfig,
|
||||
levelDB: levelDB,
|
||||
license: license,
|
||||
machineId: machineId,
|
||||
}
|
||||
}
|
||||
|
||||
// ActiveLicense 激活 License
|
||||
func (s *LicenseService) ActiveLicense(license string, machineId string) error {
|
||||
var res struct {
|
||||
Code types.BizCode `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
Name string `json:"name"`
|
||||
License string `json:"license"`
|
||||
Mid string `json:"mid"`
|
||||
ExpiredAt int64 `json:"expired_at"`
|
||||
UserNum int `json:"user_num"`
|
||||
}
|
||||
}
|
||||
apiURL := fmt.Sprintf("%s/%s", s.config.ApiURL, "api/license/active")
|
||||
response, err := req.C().R().
|
||||
SetBody(map[string]string{"license": license, "machine_id": machineId}).
|
||||
SetSuccessResult(&res).Post(apiURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("发送激活请求失败: %v", err)
|
||||
}
|
||||
|
||||
if response.IsErrorState() {
|
||||
return fmt.Errorf( "发送激活请求失败:%v", response.Status)
|
||||
}
|
||||
|
||||
if res.Code != types.Success {
|
||||
return fmt.Errorf( "激活失败:%v", res.Message)
|
||||
}
|
||||
|
||||
err = s.levelDB.Put(types.LicenseKey, types.License{
|
||||
Key: license,
|
||||
MachineId: machineId,
|
||||
UserNum: res.Data.UserNum,
|
||||
ExpiredAt: res.Data.ExpiredAt,
|
||||
IsActive: true,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存许可证书失败:%v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLicense 获取许可信息
|
||||
func (s *LicenseService) GetLicense() types.License {
|
||||
return s.license
|
||||
}
|
||||
|
||||
// IsValidApiURL 判断是否合法的中转 URL
|
||||
func (s *LicenseService) IsValidApiURL(uri string) error {
|
||||
// 获得许可授权的直接放行
|
||||
if s.license.IsActive {
|
||||
if s.license.MachineId != s.machineId {
|
||||
return errors.New("系统使用了盗版的许可证书")
|
||||
}
|
||||
|
||||
if time.Now().Unix() > s.license.ExpiredAt {
|
||||
return errors.New("系统许可证书已经过期")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(uri, "https://gpt.bemore.lol") &&
|
||||
!strings.HasPrefix(uri, "https://api.openai.com") &&
|
||||
!strings.HasPrefix(uri, "http://cdn.chat-plus.net") &&
|
||||
!strings.HasPrefix(uri, "https://api.chat-plus.net") {
|
||||
return fmt.Errorf("当前 API 地址 %s 不在白名单列表当中。",uri)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -73,6 +73,7 @@ func (c *PlusClient) Imagine(task types.MjTask) (ImageRes, error) {
|
||||
// Blend 融图
|
||||
func (c *PlusClient) Blend(task types.MjTask) (ImageRes, error) {
|
||||
apiURL := fmt.Sprintf("%s/mj-%s/mj/submit/blend", c.apiURL, c.Config.Mode)
|
||||
logger.Info("API URL: ", apiURL)
|
||||
body := ImageReq{
|
||||
BotType: "MID_JOURNEY",
|
||||
Dimensions: "SQUARE",
|
||||
@ -164,6 +165,7 @@ func (c *PlusClient) Upscale(task types.MjTask) (ImageRes, error) {
|
||||
"taskId": task.MessageId,
|
||||
}
|
||||
apiURL := fmt.Sprintf("%s/mj-%s/mj/submit/action", c.apiURL, c.Config.Mode)
|
||||
logger.Info("API URL: ", apiURL)
|
||||
var res ImageRes
|
||||
var errRes ErrRes
|
||||
r, err := c.client.R().
|
||||
@ -190,6 +192,7 @@ func (c *PlusClient) Variation(task types.MjTask) (ImageRes, error) {
|
||||
"taskId": task.MessageId,
|
||||
}
|
||||
apiURL := fmt.Sprintf("%s/mj-%s/mj/submit/action", c.apiURL, c.Config.Mode)
|
||||
logger.Info("API URL: ", apiURL)
|
||||
var res ImageRes
|
||||
var errRes ErrRes
|
||||
r, err := req.C().R().
|
||||
|
@ -3,7 +3,9 @@ package mj
|
||||
import (
|
||||
"chatplus/core/types"
|
||||
logger2 "chatplus/logger"
|
||||
"chatplus/service"
|
||||
"chatplus/service/oss"
|
||||
"chatplus/service/sd"
|
||||
"chatplus/store"
|
||||
"chatplus/store/model"
|
||||
"fmt"
|
||||
@ -25,7 +27,7 @@ type ServicePool struct {
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
func NewServicePool(db *gorm.DB, redisCli *redis.Client, manager *oss.UploaderManager, appConfig *types.AppConfig) *ServicePool {
|
||||
func NewServicePool(db *gorm.DB, redisCli *redis.Client, manager *oss.UploaderManager, appConfig *types.AppConfig, licenseService *service.LicenseService) *ServicePool {
|
||||
services := make([]*Service, 0)
|
||||
taskQueue := store.NewRedisQueue("MidJourney_Task_Queue", redisCli)
|
||||
notifyQueue := store.NewRedisQueue("MidJourney_Notify_Queue", redisCli)
|
||||
@ -34,13 +36,19 @@ func NewServicePool(db *gorm.DB, redisCli *redis.Client, manager *oss.UploaderMa
|
||||
if config.Enabled == false {
|
||||
continue
|
||||
}
|
||||
err := licenseService.IsValidApiURL(config.ApiURL)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
cli := NewPlusClient(config)
|
||||
name := fmt.Sprintf("mj-plus-service-%d", k)
|
||||
service := NewService(name, taskQueue, notifyQueue, db, cli)
|
||||
plusService := NewService(name, taskQueue, notifyQueue, db, cli)
|
||||
go func() {
|
||||
service.Run()
|
||||
plusService.Run()
|
||||
}()
|
||||
services = append(services, service)
|
||||
services = append(services, plusService)
|
||||
}
|
||||
|
||||
for k, config := range appConfig.MjProxyConfigs {
|
||||
@ -49,11 +57,11 @@ func NewServicePool(db *gorm.DB, redisCli *redis.Client, manager *oss.UploaderMa
|
||||
}
|
||||
cli := NewProxyClient(config)
|
||||
name := fmt.Sprintf("mj-proxy-service-%d", k)
|
||||
service := NewService(name, taskQueue, notifyQueue, db, cli)
|
||||
proxyService := NewService(name, taskQueue, notifyQueue, db, cli)
|
||||
go func() {
|
||||
service.Run()
|
||||
proxyService.Run()
|
||||
}()
|
||||
services = append(services, service)
|
||||
services = append(services, proxyService)
|
||||
}
|
||||
|
||||
return &ServicePool{
|
||||
@ -69,16 +77,16 @@ func NewServicePool(db *gorm.DB, redisCli *redis.Client, manager *oss.UploaderMa
|
||||
func (p *ServicePool) CheckTaskNotify() {
|
||||
go func() {
|
||||
for {
|
||||
var userId uint
|
||||
err := p.notifyQueue.LPop(&userId)
|
||||
var message sd.NotifyMessage
|
||||
err := p.notifyQueue.LPop(&message)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
cli := p.Clients.Get(userId)
|
||||
cli := p.Clients.Get(uint(message.UserId))
|
||||
if cli == nil {
|
||||
continue
|
||||
}
|
||||
err = cli.Send([]byte("Task Updated"))
|
||||
err = cli.Send([]byte(message.Message))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@ -127,7 +135,7 @@ func (p *ServicePool) DownloadImages() {
|
||||
if cli == nil {
|
||||
continue
|
||||
}
|
||||
err = cli.Send([]byte("Task Updated"))
|
||||
err = cli.Send([]byte(sd.Finished))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@ -162,7 +170,6 @@ func (p *ServicePool) SyncTaskProgress() {
|
||||
for _, job := range items {
|
||||
// 失败或者 30 分钟还没完成的任务删除并退回算力
|
||||
if time.Now().Sub(job.CreatedAt) > time.Minute*30 || job.Progress == -1 {
|
||||
// 删除任务
|
||||
p.db.Delete(&job)
|
||||
// 退回算力
|
||||
tx := p.db.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power + ?", job.Power))
|
||||
@ -189,7 +196,7 @@ func (p *ServicePool) SyncTaskProgress() {
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(time.Second)
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package mj
|
||||
import (
|
||||
"chatplus/core/types"
|
||||
"chatplus/service"
|
||||
"chatplus/service/sd"
|
||||
"chatplus/store"
|
||||
"chatplus/store/model"
|
||||
"chatplus/utils"
|
||||
@ -53,7 +54,7 @@ func (s *Service) Run() {
|
||||
|
||||
// translate prompt
|
||||
if utils.HasChinese(task.Prompt) {
|
||||
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.Prompt))
|
||||
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, task.Prompt))
|
||||
if err == nil {
|
||||
task.Prompt = content
|
||||
} else {
|
||||
@ -62,7 +63,7 @@ func (s *Service) Run() {
|
||||
}
|
||||
// translate negative prompt
|
||||
if task.NegPrompt != "" && utils.HasChinese(task.NegPrompt) {
|
||||
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.NegPrompt))
|
||||
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, task.NegPrompt))
|
||||
if err == nil {
|
||||
task.NegPrompt = content
|
||||
} else {
|
||||
@ -105,7 +106,7 @@ func (s *Service) Run() {
|
||||
// update the task progress
|
||||
s.db.Updates(&job)
|
||||
// 任务失败,通知前端
|
||||
s.notifyQueue.RPush(task.UserId)
|
||||
s.notifyQueue.RPush(sd.NotifyMessage{UserId: task.UserId, JobId: int(job.Id), Message: sd.Failed})
|
||||
continue
|
||||
}
|
||||
logger.Infof("任务提交成功:%+v", res)
|
||||
@ -147,7 +148,7 @@ func (s *Service) Notify(job model.MidJourneyJob) error {
|
||||
"progress": -1,
|
||||
"err_msg": task.FailReason,
|
||||
})
|
||||
s.notifyQueue.RPush(job.UserId)
|
||||
s.notifyQueue.RPush(sd.NotifyMessage{UserId: job.UserId, JobId: int(job.Id), Message: sd.Failed})
|
||||
return fmt.Errorf("task failed: %v", task.FailReason)
|
||||
}
|
||||
|
||||
@ -166,7 +167,11 @@ func (s *Service) Notify(job model.MidJourneyJob) error {
|
||||
}
|
||||
// 通知前端更新任务进度
|
||||
if oldProgress != job.Progress {
|
||||
s.notifyQueue.RPush(job.UserId)
|
||||
message := sd.Running
|
||||
if job.Progress == 100 {
|
||||
message = sd.Finished
|
||||
}
|
||||
s.notifyQueue.RPush(sd.NotifyMessage{UserId: job.UserId, JobId: int(job.Id), Message: message})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -60,16 +60,16 @@ func (p *ServicePool) CheckTaskNotify() {
|
||||
go func() {
|
||||
logger.Info("Running Stable-Diffusion task notify checking ...")
|
||||
for {
|
||||
var userId uint
|
||||
err := p.notifyQueue.LPop(&userId)
|
||||
var message NotifyMessage
|
||||
err := p.notifyQueue.LPop(&message)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
client := p.Clients.Get(userId)
|
||||
client := p.Clients.Get(uint(message.UserId))
|
||||
if client == nil {
|
||||
continue
|
||||
}
|
||||
err = client.Send([]byte("Task Updated"))
|
||||
err = client.Send([]byte(message.Message))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@ -113,7 +113,7 @@ func (p *ServicePool) CheckTaskStatus() {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
@ -8,10 +8,11 @@ import (
|
||||
"chatplus/store/model"
|
||||
"chatplus/utils"
|
||||
"fmt"
|
||||
"github.com/imroc/req/v3"
|
||||
"gorm.io/gorm"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/imroc/req/v3"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// SD 绘画服务
|
||||
@ -80,7 +81,7 @@ func (s *Service) Run() {
|
||||
"err_msg": err.Error(),
|
||||
})
|
||||
// 通知前端,任务失败
|
||||
s.notifyQueue.RPush(task.UserId)
|
||||
s.notifyQueue.RPush(NotifyMessage{UserId: task.UserId, JobId: task.Id, Message: Failed})
|
||||
continue
|
||||
}
|
||||
}
|
||||
@ -145,8 +146,13 @@ func (s *Service) Txt2Img(task types.SdTask) error {
|
||||
var errChan = make(chan error)
|
||||
apiURL := fmt.Sprintf("%s/sdapi/v1/txt2img", s.config.ApiURL)
|
||||
logger.Debugf("send image request to %s", apiURL)
|
||||
// send a request to sd api endpoint
|
||||
go func() {
|
||||
response, err := s.httpClient.R().SetBody(body).SetSuccessResult(&res).Post(apiURL)
|
||||
response, err := s.httpClient.R().
|
||||
SetHeader("Authorization", s.config.ApiKey).
|
||||
SetBody(body).
|
||||
SetSuccessResult(&res).
|
||||
Post(apiURL)
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
@ -174,14 +180,17 @@ func (s *Service) Txt2Img(task types.SdTask) error {
|
||||
errChan <- nil
|
||||
}()
|
||||
|
||||
// waiting for task finish
|
||||
for {
|
||||
select {
|
||||
case err := <-errChan: // 任务完成
|
||||
case err := <-errChan:
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// task finished
|
||||
s.db.Model(&model.SdJob{Id: uint(task.Id)}).UpdateColumn("progress", 100)
|
||||
s.notifyQueue.RPush(task.UserId)
|
||||
s.notifyQueue.RPush(NotifyMessage{UserId: task.UserId, JobId: task.Id, Message: Finished})
|
||||
// 从 leveldb 中删除预览图片数据
|
||||
_ = s.leveldb.Delete(task.Params.TaskId)
|
||||
return nil
|
||||
@ -191,7 +200,7 @@ func (s *Service) Txt2Img(task types.SdTask) error {
|
||||
if err == nil && resp.Progress > 0 {
|
||||
s.db.Model(&model.SdJob{Id: uint(task.Id)}).UpdateColumn("progress", int(resp.Progress*100))
|
||||
// 发送更新状态信号
|
||||
s.notifyQueue.RPush(task.UserId)
|
||||
s.notifyQueue.RPush(NotifyMessage{UserId: task.UserId, JobId: task.Id, Message: Running})
|
||||
// 保存预览图片数据
|
||||
if resp.CurrentImage != "" {
|
||||
_ = s.leveldb.Put(task.Params.TaskId, resp.CurrentImage)
|
||||
@ -207,7 +216,10 @@ func (s *Service) Txt2Img(task types.SdTask) error {
|
||||
func (s *Service) checkTaskProgress() (error, *TaskProgressResp) {
|
||||
apiURL := fmt.Sprintf("%s/sdapi/v1/progress?skip_current_image=false", s.config.ApiURL)
|
||||
var res TaskProgressResp
|
||||
response, err := s.httpClient.R().SetSuccessResult(&res).Get(apiURL)
|
||||
response, err := s.httpClient.R().
|
||||
SetHeader("Authorization", s.config.ApiKey).
|
||||
SetSuccessResult(&res).
|
||||
Get(apiURL)
|
||||
if err != nil {
|
||||
return err, nil
|
||||
}
|
||||
|
@ -4,44 +4,14 @@ import logger2 "chatplus/logger"
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
type TaskInfo struct {
|
||||
UserId uint `json:"user_id"`
|
||||
SessionId string `json:"session_id"`
|
||||
JobId int `json:"job_id"`
|
||||
TaskId string `json:"task_id"`
|
||||
Data []interface{} `json:"data"`
|
||||
EventData interface{} `json:"event_data"`
|
||||
FnIndex int `json:"fn_index"`
|
||||
SessionHash string `json:"session_hash"`
|
||||
type NotifyMessage struct {
|
||||
UserId int `json:"user_id"`
|
||||
JobId int `json:"job_id"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type CBReq struct {
|
||||
UserId uint
|
||||
SessionId string
|
||||
JobId int
|
||||
TaskId string
|
||||
ImageName string
|
||||
ImageData string
|
||||
Progress int
|
||||
Seed int64
|
||||
Success bool
|
||||
Message string
|
||||
}
|
||||
|
||||
var ParamKeys = map[string]int{
|
||||
"task_id": 0,
|
||||
"prompt": 1,
|
||||
"negative_prompt": 2,
|
||||
"steps": 4,
|
||||
"sampler": 5,
|
||||
"face_fix": 7, // 面部修复
|
||||
"cfg_scale": 8,
|
||||
"seed": 27,
|
||||
"height": 10,
|
||||
"width": 9,
|
||||
"hd_fix": 11,
|
||||
"hd_redraw_rate": 12, //高清修复重绘幅度
|
||||
"hd_scale": 13, // 高清修复放大倍数
|
||||
"hd_scale_alg": 14, // 高清修复放大算法
|
||||
"hd_sample_num": 15, // 高清修复采样次数
|
||||
}
|
||||
const (
|
||||
Running = "RUNNING"
|
||||
Finished = "FINISH"
|
||||
Failed = "FAIL"
|
||||
)
|
||||
|
@ -3,9 +3,11 @@ package service
|
||||
import (
|
||||
"bytes"
|
||||
"chatplus/core/types"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"mime"
|
||||
"net/smtp"
|
||||
"net/textproto"
|
||||
)
|
||||
|
||||
type SmtpService struct {
|
||||
@ -19,12 +21,18 @@ func NewSmtpService(appConfig *types.AppConfig) *SmtpService {
|
||||
}
|
||||
|
||||
func (s *SmtpService) SendVerifyCode(to string, code int) error {
|
||||
subject := "ChatPlus注册验证码"
|
||||
body := fmt.Sprintf("您正在注册 ChatPlus AI 助手账户,注册验证码为 %d,请不要告诉他人。如非本人操作,请忽略此邮件。", code)
|
||||
subject := "Geek-AI 注册验证码"
|
||||
body := fmt.Sprintf("您正在注册 Geek-AI 助手账户,注册验证码为 %d,请不要告诉他人。如非本人操作,请忽略此邮件。", code)
|
||||
|
||||
// 设置SMTP客户端配置
|
||||
auth := smtp.PlainAuth("", s.config.From, s.config.Password, s.config.Host)
|
||||
if s.config.UseTls {
|
||||
return s.sendTLS(auth, to, subject, body)
|
||||
} else {
|
||||
return s.send(auth, to, subject, body)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SmtpService) send(auth smtp.Auth, to string, subject string, body string) error {
|
||||
// 对主题进行MIME编码
|
||||
encodedSubject := mime.QEncoding.Encode("UTF-8", subject)
|
||||
// 组装邮件
|
||||
@ -34,11 +42,83 @@ func (s *SmtpService) SendVerifyCode(to string, code int) error {
|
||||
message.WriteString(fmt.Sprintf("Subject: %s\r\n", encodedSubject))
|
||||
message.WriteString("\r\n" + body)
|
||||
|
||||
// 发送邮件
|
||||
// 发送邮件
|
||||
err := smtp.SendMail(s.config.Host+":"+fmt.Sprint(s.config.Port), auth, s.config.From, []string{to}, message.Bytes())
|
||||
if err != nil {
|
||||
return fmt.Errorf("error sending email: %v", err)
|
||||
}
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
func (s *SmtpService) sendTLS(auth smtp.Auth, to string, subject string, body string) error {
|
||||
// TLS配置
|
||||
tlsConfig := &tls.Config{
|
||||
ServerName: s.config.Host,
|
||||
}
|
||||
|
||||
// 建立TLS连接
|
||||
conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", s.config.Host, s.config.Port), tlsConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error connecting to SMTP server: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client, err := smtp.NewClient(conn, s.config.Host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating SMTP client: %v", err)
|
||||
}
|
||||
defer client.Quit()
|
||||
|
||||
// 身份验证
|
||||
if err = client.Auth(auth); err != nil {
|
||||
return fmt.Errorf("error authenticating: %v", err)
|
||||
}
|
||||
|
||||
// 设置寄件人
|
||||
if err = client.Mail(s.config.From); err != nil {
|
||||
return fmt.Errorf("error setting sender: %v", err)
|
||||
}
|
||||
|
||||
// 设置收件人
|
||||
if err = client.Rcpt(to); err != nil {
|
||||
return fmt.Errorf("error setting recipient: %v", err)
|
||||
}
|
||||
|
||||
// 发送邮件内容
|
||||
wc, err := client.Data()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting data writer: %v", err)
|
||||
}
|
||||
defer wc.Close()
|
||||
|
||||
header := make(textproto.MIMEHeader)
|
||||
header.Set("From", s.config.From)
|
||||
header.Set("To", to)
|
||||
header.Set("Subject", subject)
|
||||
|
||||
// 将邮件头写入
|
||||
for key, values := range header {
|
||||
for _, value := range values {
|
||||
_, err = fmt.Fprintf(wc, "%s: %s\r\n", key, value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error sending email header: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
_, _ = fmt.Fprintln(wc)
|
||||
// 将邮件内容写入
|
||||
_, err = fmt.Fprintf(wc, body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error sending email: %v", err)
|
||||
}
|
||||
|
||||
// 发送完毕
|
||||
err = wc.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error closing data writer: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
package service
|
||||
|
||||
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 RewritePromptTemplate = "Please rewrite the following text into AI painting prompt words, and please try to add detailed description of the picture, painting style, scene, rendering effect, picture light and other creative elements. Just output the final prompt word directly. Do not output any explanation lines. The text to be rewritten is: [%s]"
|
||||
const TranslatePromptTemplate = "Translate the following painting prompt words into English keyword phrases. Without any explanation, directly output the keyword phrases separated by commas. The content to be translated is: [%s]"
|
||||
|
@ -35,13 +35,12 @@ func (db *LevelDB) Put(key string, value interface{}) error {
|
||||
return db.driver.Put([]byte(key), byteData, nil)
|
||||
}
|
||||
|
||||
func (db *LevelDB) Get(key string) ([]byte, error) {
|
||||
func (db *LevelDB) Get(key string, dist interface{}) error {
|
||||
bytes, err := db.driver.Get([]byte(key), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
return bytes, nil
|
||||
return json.Unmarshal(bytes, dist)
|
||||
}
|
||||
|
||||
func (db *LevelDB) Search(prefix string) []string {
|
||||
|
@ -12,4 +12,5 @@ type ChatModel struct {
|
||||
MaxTokens int // 最大响应长度
|
||||
MaxContext int // 最大上下文长度
|
||||
Temperature float32 // 模型温度
|
||||
KeyId int // 绑定 API KEY ID
|
||||
}
|
||||
|
@ -9,4 +9,5 @@ type ChatRole struct {
|
||||
Icon string // 角色聊天图标
|
||||
Enable bool // 是否启用被启用
|
||||
SortNum int //排序数字
|
||||
ModelId int // 绑定模型ID,绑定模型ID的角色只能用指定的模型来问答
|
||||
}
|
||||
|
16
api/store/model/dalle_job.go
Normal file
@ -0,0 +1,16 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type DallJob struct {
|
||||
Id uint `gorm:"primarykey;column:id"`
|
||||
UserId uint
|
||||
Prompt string
|
||||
ImgURL string
|
||||
OrgURL string
|
||||
Publish bool
|
||||
Power int
|
||||
Progress int
|
||||
ErrMsg string
|
||||
CreatedAt time.Time
|
||||
}
|
@ -12,4 +12,6 @@ type ChatModel struct {
|
||||
MaxTokens int `json:"max_tokens"` // 最大响应长度
|
||||
MaxContext int `json:"max_context"` // 最大上下文长度
|
||||
Temperature float32 `json:"temperature"` // 模型温度
|
||||
KeyId int `json:"key_id"`
|
||||
KeyName string `json:"key_name"`
|
||||
}
|
||||
|
@ -4,11 +4,13 @@ import "chatplus/core/types"
|
||||
|
||||
type ChatRole struct {
|
||||
BaseVo
|
||||
Key string `json:"key"` // 角色唯一标识
|
||||
Name string `json:"name"` // 角色名称
|
||||
Context []types.Message `json:"context"` // 角色语料信息
|
||||
HelloMsg string `json:"hello_msg"` // 打招呼的消息
|
||||
Icon string `json:"icon"` // 角色聊天图标
|
||||
Enable bool `json:"enable"` // 是否启用被启用
|
||||
SortNum int `json:"sort"` // 排序
|
||||
Key string `json:"key"` // 角色唯一标识
|
||||
Name string `json:"name"` // 角色名称
|
||||
Context []types.Message `json:"context"` // 角色语料信息
|
||||
HelloMsg string `json:"hello_msg"` // 打招呼的消息
|
||||
Icon string `json:"icon"` // 角色聊天图标
|
||||
Enable bool `json:"enable"` // 是否启用被启用
|
||||
SortNum int `json:"sort"` // 排序
|
||||
ModelId int `json:"model_id"` // 绑定模型 ID
|
||||
ModelName string `json:"model_name"` // 模型名称
|
||||
}
|
||||
|
14
api/store/vo/dalle_job.go
Normal file
@ -0,0 +1,14 @@
|
||||
package vo
|
||||
|
||||
type DallJob struct {
|
||||
Id uint `json:"id"`
|
||||
UserId int `json:"user_id"`
|
||||
Prompt string `json:"prompt"`
|
||||
ImgURL string `json:"img_url"`
|
||||
OrgURL string `json:"org_url"`
|
||||
Publish bool `json:"publish"`
|
||||
Power int `json:"power"`
|
||||
Progress int `json:"progress"`
|
||||
ErrMsg string `json:"err_msg"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
@ -1,12 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"chatplus/utils"
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func main() {
|
||||
text := 1
|
||||
bytes := reflect.ValueOf(text).Bytes()
|
||||
fmt.Println(bytes)
|
||||
text := "https://nk.img.r9it.com/chatgpt-plus/1712709360012445.png 请简单描述一下这幅图上的内容 "
|
||||
imgURL := utils.ExtractImgURL(text)
|
||||
fmt.Println(imgURL)
|
||||
}
|
||||
|
@ -83,4 +83,4 @@ func OpenAIRequest(db *gorm.DB, prompt string) (string, error) {
|
||||
db.Model(&apiKey).UpdateColumn("last_used_at", time.Now().Unix())
|
||||
|
||||
return response.Choices[0].Message.Content, nil
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@ -79,3 +80,15 @@ func GetImgExt(filename string) string {
|
||||
}
|
||||
return ext
|
||||
}
|
||||
|
||||
func ExtractImgURL(text string) []string {
|
||||
re := regexp.MustCompile(`(http[s]?:\/\/.*?\.(?:png|jpg|jpeg|gif))`)
|
||||
matches := re.FindAllStringSubmatch(text, 10)
|
||||
urls := make([]string, 0)
|
||||
if len(matches) > 0 {
|
||||
for _, m := range matches {
|
||||
urls = append(urls, m[1])
|
||||
}
|
||||
}
|
||||
return urls
|
||||
}
|
||||
|
@ -22,11 +22,15 @@
|
||||
"markdown-it": "^13.0.1",
|
||||
"markdown-it-latex2img": "^0.0.6",
|
||||
"markdown-it-mathjax": "^2.0.0",
|
||||
"markmap-common": "^0.16.0",
|
||||
"markmap-lib": "^0.16.1",
|
||||
"markmap-view": "^0.16.0",
|
||||
"md-editor-v3": "^2.2.1",
|
||||
"pinia": "^2.1.4",
|
||||
"qrcode": "^1.5.3",
|
||||
"qs": "^6.11.1",
|
||||
"sortablejs": "^1.15.0",
|
||||
"three": "^0.128.0",
|
||||
"v3-waterfall": "^1.2.1",
|
||||
"vant": "^4.5.0",
|
||||
"vue": "^3.2.13",
|
||||
|
BIN
web/public/images/avatar/seller.jpg
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
web/public/images/land_ocean_ice_cloud_2048.jpg
Normal file
After Width: | Height: | Size: 580 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 250 KiB |
BIN
web/public/images/menu/dalle.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
web/public/images/menu/more.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
web/public/images/menu/xmind.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
@ -47,6 +47,7 @@
|
||||
|
||||
.opt {
|
||||
position: relative;
|
||||
width 100%
|
||||
top -5px
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ $borderColor = #4676d0;
|
||||
|
||||
.el-aside {
|
||||
background-color: $sideBgColor;
|
||||
height 100vh
|
||||
|
||||
.title-box {
|
||||
padding: 6px 10px;
|
||||
|
88
web/src/assets/css/image-dall.styl
Normal file
@ -0,0 +1,88 @@
|
||||
.page-dall {
|
||||
background-color: #282c34;
|
||||
|
||||
.inner {
|
||||
display: flex;
|
||||
|
||||
.sd-box {
|
||||
margin 10px
|
||||
background-color #262626
|
||||
border 1px solid #454545
|
||||
min-width 300px
|
||||
max-width 300px
|
||||
padding 10px
|
||||
border-radius 10px
|
||||
color #ffffff;
|
||||
font-size 14px
|
||||
|
||||
h2 {
|
||||
font-weight: bold;
|
||||
font-size 20px
|
||||
text-align center
|
||||
color #47fff1
|
||||
}
|
||||
|
||||
// 隐藏滚动条
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.sd-params {
|
||||
margin-top 10px
|
||||
overflow auto
|
||||
|
||||
|
||||
.param-line {
|
||||
padding 0 10px
|
||||
|
||||
.grid-content
|
||||
.form-item-inner {
|
||||
display flex
|
||||
|
||||
.info-icon {
|
||||
margin-left 10px
|
||||
position relative
|
||||
top 8px
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.param-line.pt {
|
||||
padding-top 5px
|
||||
padding-bottom 5px
|
||||
}
|
||||
|
||||
.text-info {
|
||||
padding 10px
|
||||
}
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
padding 10px 15px 0 15px
|
||||
text-align center
|
||||
|
||||
.el-button {
|
||||
width 100%
|
||||
|
||||
span {
|
||||
color #2D3A4B
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-form {
|
||||
.el-form-item__label {
|
||||
color #ffffff
|
||||
}
|
||||
}
|
||||
|
||||
@import "task-list.styl"
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -38,24 +38,14 @@
|
||||
.param-line {
|
||||
padding 0 10px
|
||||
|
||||
.el-icon {
|
||||
position relative
|
||||
top 3px
|
||||
}
|
||||
|
||||
.el-input__suffix-inner {
|
||||
.el-icon {
|
||||
top 0
|
||||
}
|
||||
}
|
||||
|
||||
.grid-content
|
||||
.form-item-inner {
|
||||
display flex
|
||||
|
||||
.el-icon {
|
||||
.info-icon {
|
||||
margin-left 10px
|
||||
margin-top 2px
|
||||
position relative
|
||||
top 8px
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,10 +58,6 @@
|
||||
|
||||
.text-info {
|
||||
padding 10px
|
||||
|
||||
.el-tag {
|
||||
margin-right 10px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
134
web/src/assets/css/mark-map.styl
Normal file
@ -0,0 +1,134 @@
|
||||
.page-mark-map {
|
||||
background-color: #282c34;
|
||||
height 100vh
|
||||
|
||||
.inner {
|
||||
display: flex;
|
||||
|
||||
.mark-map-box {
|
||||
margin 10px
|
||||
background-color #262626
|
||||
border 1px solid #454545
|
||||
min-width 300px
|
||||
max-width 300px
|
||||
padding 10px
|
||||
border-radius 10px
|
||||
color #ffffff;
|
||||
font-size 14px
|
||||
|
||||
h2 {
|
||||
font-weight: bold;
|
||||
font-size 20px
|
||||
text-align center
|
||||
color #47fff1
|
||||
}
|
||||
|
||||
// 隐藏滚动条
|
||||
::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.mark-map-params {
|
||||
margin-top 10px
|
||||
overflow auto
|
||||
|
||||
|
||||
.param-line {
|
||||
padding 10px
|
||||
|
||||
.el-button {
|
||||
width 100%
|
||||
|
||||
span {
|
||||
color #2D3A4B
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.text-info {
|
||||
padding 10px
|
||||
|
||||
.el-tag {
|
||||
margin-right 10px
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-form {
|
||||
.el-form-item__label {
|
||||
color #ffffff
|
||||
}
|
||||
}
|
||||
|
||||
.right-box {
|
||||
width 100%
|
||||
|
||||
.top-bar {
|
||||
display flex
|
||||
justify-content space-between
|
||||
align-items center
|
||||
|
||||
h2 {
|
||||
color #ffffff
|
||||
}
|
||||
|
||||
.el-button {
|
||||
margin-right 20px
|
||||
}
|
||||
}
|
||||
|
||||
.markdown {
|
||||
color #ffffff
|
||||
display flex
|
||||
justify-content center
|
||||
align-items center
|
||||
|
||||
h1 {
|
||||
color: #47fff1;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #ffcc00;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
margin-left: 20px;
|
||||
|
||||
li {
|
||||
line-height 1.5
|
||||
}
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
em {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
display flex
|
||||
justify-content center
|
||||
align-items center
|
||||
|
||||
.markmap {
|
||||
width 100%
|
||||
color #ffffff
|
||||
font-size 12px
|
||||
|
||||
.markmap-foreign {
|
||||
//height 30px
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 4125778 */
|
||||
src: url('iconfont.woff2?t=1708054962140') format('woff2'),
|
||||
url('iconfont.woff?t=1708054962140') format('woff'),
|
||||
url('iconfont.ttf?t=1708054962140') format('truetype');
|
||||
src: url('iconfont.woff2?t=1713766977199') format('woff2'),
|
||||
url('iconfont.woff?t=1713766977199') format('woff'),
|
||||
url('iconfont.ttf?t=1713766977199') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
@ -13,6 +13,38 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-more:before {
|
||||
content: "\e63c";
|
||||
}
|
||||
|
||||
.icon-mj:before {
|
||||
content: "\e643";
|
||||
}
|
||||
|
||||
.icon-dalle:before {
|
||||
content: "\e646";
|
||||
}
|
||||
|
||||
.icon-xmind:before {
|
||||
content: "\e610";
|
||||
}
|
||||
|
||||
.icon-version:before {
|
||||
content: "\e68d";
|
||||
}
|
||||
|
||||
.icon-sd:before {
|
||||
content: "\e62b";
|
||||
}
|
||||
|
||||
.icon-huihua1:before {
|
||||
content: "\e606";
|
||||
}
|
||||
|
||||
.icon-chat:before {
|
||||
content: "\e68a";
|
||||
}
|
||||
|
||||
.icon-prompt:before {
|
||||
content: "\e6ce";
|
||||
}
|
||||
|
@ -5,6 +5,62 @@
|
||||
"css_prefix_text": "icon-",
|
||||
"description": "",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "1421807",
|
||||
"name": "更多",
|
||||
"font_class": "more",
|
||||
"unicode": "e63c",
|
||||
"unicode_decimal": 58940
|
||||
},
|
||||
{
|
||||
"icon_id": "36264781",
|
||||
"name": "MidJourney",
|
||||
"font_class": "mj",
|
||||
"unicode": "e643",
|
||||
"unicode_decimal": 58947
|
||||
},
|
||||
{
|
||||
"icon_id": "37677137",
|
||||
"name": "DALL·E 3",
|
||||
"font_class": "dalle",
|
||||
"unicode": "e646",
|
||||
"unicode_decimal": 58950
|
||||
},
|
||||
{
|
||||
"icon_id": "2629858",
|
||||
"name": "逻辑图",
|
||||
"font_class": "xmind",
|
||||
"unicode": "e610",
|
||||
"unicode_decimal": 58896
|
||||
},
|
||||
{
|
||||
"icon_id": "1061336",
|
||||
"name": "version",
|
||||
"font_class": "version",
|
||||
"unicode": "e68d",
|
||||
"unicode_decimal": 59021
|
||||
},
|
||||
{
|
||||
"icon_id": "3901033",
|
||||
"name": "绘画",
|
||||
"font_class": "sd",
|
||||
"unicode": "e62b",
|
||||
"unicode_decimal": 58923
|
||||
},
|
||||
{
|
||||
"icon_id": "39185683",
|
||||
"name": "绘画",
|
||||
"font_class": "huihua1",
|
||||
"unicode": "e606",
|
||||
"unicode_decimal": 58886
|
||||
},
|
||||
{
|
||||
"icon_id": "2341972",
|
||||
"name": "chat",
|
||||
"font_class": "chat",
|
||||
"unicode": "e68a",
|
||||
"unicode_decimal": 59018
|
||||
},
|
||||
{
|
||||
"icon_id": "8017627",
|
||||
"name": "prompt",
|
||||
|
@ -90,6 +90,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
.chat-item {
|
||||
width 100%
|
||||
position: relative;
|
||||
padding: 0 5px 0 0;
|
||||
overflow: hidden;
|
||||
|
@ -93,6 +93,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
.chat-item {
|
||||
width 100%
|
||||
position: relative;
|
||||
padding: 0 0 0 5px;
|
||||
overflow: hidden;
|
||||
|
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="foot-container">
|
||||
<div class="footer">
|
||||
Powered by {{ author }} @
|
||||
<el-link type="primary" href="https://github.com/yangjian102621/chatgpt-plus" target="_blank">
|
||||
<el-link type="primary" href="https://github.com/yangjian102621/chatgpt-plus" target="_blank" style="--el-link-text-color:#ffffff">
|
||||
{{ title }} -
|
||||
{{ version }}
|
||||
</el-link>
|
||||
@ -19,7 +19,7 @@ const author = ref('极客学长')
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
.container {
|
||||
.foot-container {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
|
@ -226,10 +226,9 @@ import {httpGet, httpPost} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {setUserToken} from "@/store/session";
|
||||
import {validateEmail, validateMobile} from "@/utils/validate";
|
||||
import {Checked, Close, Iphone, Lock, Message, Position, User} from "@element-plus/icons-vue";
|
||||
import {Checked, Close, Iphone, Lock, Message} from "@element-plus/icons-vue";
|
||||
import SendMsg from "@/components/SendMsg.vue";
|
||||
import {arrayContains} from "@/utils/libs";
|
||||
import {useRouter} from "vue-router";
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const props = defineProps({
|
||||
@ -359,7 +358,7 @@ const close = function () {
|
||||
.close-icon {
|
||||
cursor pointer
|
||||
position absolute
|
||||
right -10px
|
||||
right 0
|
||||
top 0
|
||||
font-weight normal
|
||||
font-size 20px
|
||||
|
@ -45,6 +45,9 @@
|
||||
<span>{{ sysTitle }}</span>
|
||||
</el-dropdown-item>
|
||||
</a>
|
||||
<el-dropdown-item>
|
||||
<i class="iconfont icon-version"></i> 当前版本:{{ version }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="showDialog = true">
|
||||
<i class="iconfont icon-reward"></i>
|
||||
<span>打赏作者</span>
|
||||
@ -86,6 +89,7 @@ import {removeAdminToken} from "@/store/session";
|
||||
|
||||
const message = ref(5);
|
||||
const sysTitle = ref(process.env.VUE_APP_TITLE)
|
||||
const version = ref(process.env.VUE_APP_VERSION)
|
||||
const avatar = ref('/images/user-info.jpg')
|
||||
const donateImg = ref('/images/wechat-pay.png')
|
||||
const showDialog = ref(false)
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="sidebar">
|
||||
<div class="logo">
|
||||
<el-image :src="logo"/>
|
||||
<span class="text" v-show="!sidebar.collapse">{{ title }} - {{ version }}</span>
|
||||
<span class="text" v-show="!sidebar.collapse">{{ title }}</span>
|
||||
</div>
|
||||
|
||||
<el-menu
|
||||
@ -60,11 +60,11 @@ import {ElMessage} from "element-plus";
|
||||
|
||||
const title = ref('Chat-Plus-Admin')
|
||||
const logo = ref('/images/logo.png')
|
||||
const version = ref(process.env.VUE_APP_VERSION)
|
||||
|
||||
// 加载系统配置
|
||||
httpGet('/api/admin/config/get?key=system').then(res => {
|
||||
title.value = res.data['admin_title'];
|
||||
title.value = res.data['admin_title']
|
||||
logo.value = res.data['logo']
|
||||
}).catch(e => {
|
||||
ElMessage.error("加载系统配置失败: " + e.message)
|
||||
})
|
||||
@ -192,9 +192,9 @@ setMenuItems(items)
|
||||
padding 6px 15px;
|
||||
|
||||
.el-image {
|
||||
width 30px;
|
||||
height 30px;
|
||||
padding-top 8px;
|
||||
width 36px;
|
||||
height 36px;
|
||||
padding-top 5px;
|
||||
border-radius 100%
|
||||
|
||||
.el-image__inner {
|
||||
|
@ -2,8 +2,14 @@ import {createRouter, createWebHistory} from "vue-router";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
name: 'home',
|
||||
name: 'Index',
|
||||
path: '/',
|
||||
meta: {title: process.env.VUE_APP_TITLE},
|
||||
component: () => import('@/views/Index.vue'),
|
||||
},
|
||||
{
|
||||
name: 'home',
|
||||
path: '/home',
|
||||
redirect: '/chat',
|
||||
meta: {title: '首页'},
|
||||
component: () => import('@/views/Home.vue'),
|
||||
@ -56,6 +62,18 @@ const routes = [
|
||||
meta: {title: '消费日志'},
|
||||
component: () => import('@/views/PowerLog.vue'),
|
||||
},
|
||||
{
|
||||
name: 'xmind',
|
||||
path: '/xmind',
|
||||
meta: {title: '思维导图'},
|
||||
component: () => import('@/views/MarkMap.vue'),
|
||||
},
|
||||
{
|
||||
name: 'dalle',
|
||||
path: '/dalle',
|
||||
meta: {title: 'DALLE-3'},
|
||||
component: () => import('@/views/Dalle.vue'),
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -1,7 +1,7 @@
|
||||
import axios from 'axios'
|
||||
import {getAdminToken, getSessionId, getUserToken} from "@/store/session";
|
||||
|
||||
axios.defaults.timeout = 30000
|
||||
axios.defaults.timeout = 180000
|
||||
axios.defaults.baseURL = process.env.VUE_APP_API_HOST
|
||||
axios.defaults.withCredentials = true;
|
||||
axios.defaults.headers.post['Content-Type'] = 'application/json'
|
||||
|
@ -12,14 +12,10 @@
|
||||
<div class="title">
|
||||
<span class="name">{{ scope.item.name }}</span>
|
||||
<div class="opt">
|
||||
|
||||
<el-button v-if="hasRole(scope.item.key)" size="small" type="danger"
|
||||
@click="updateRole(scope.item,'remove')">
|
||||
<el-icon>
|
||||
<Delete/>
|
||||
</el-icon>
|
||||
<span>移除应用</span>
|
||||
</el-button>
|
||||
<div v-if="hasRole(scope.item.key)">
|
||||
<el-button size="small" type="success" @click="useRole(scope.item.id)">使用</el-button>
|
||||
<el-button size="small" type="danger" @click="updateRole(scope.item,'remove')">移除</el-button>
|
||||
</div>
|
||||
<el-button v-else size="small"
|
||||
style="--el-color-primary:#009999"
|
||||
@click="updateRole(scope.item, 'add')">
|
||||
@ -47,10 +43,11 @@ import {onMounted, ref} from "vue"
|
||||
import {ElMessage} from "element-plus";
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import ItemList from "@/components/ItemList.vue";
|
||||
import {Delete, Plus} from "@element-plus/icons-vue";
|
||||
import {Plus} from "@element-plus/icons-vue";
|
||||
import LoginDialog from "@/components/LoginDialog.vue";
|
||||
import {checkSession} from "@/action/session";
|
||||
import {arrayContains, removeArrayItem, substr} from "@/utils/libs";
|
||||
import {useRouter} from "vue-router";
|
||||
|
||||
const listBoxHeight = window.innerHeight - 97
|
||||
const list = ref([])
|
||||
@ -111,6 +108,11 @@ const updateRole = (row, opt) => {
|
||||
const hasRole = (roleKey) => {
|
||||
return arrayContains(roles.value, roleKey, (v1, v2) => v1 === v2)
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const useRole = (roleId) => {
|
||||
router.push({name: "chat", params: {role_id: roleId}})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
|
@ -82,8 +82,8 @@
|
||||
<el-main v-loading="loading" element-loading-background="rgba(122, 122, 122, 0.3)">
|
||||
<div class="chat-head">
|
||||
<div class="chat-config">
|
||||
<!-- <span class="role-select-label">聊天角色:</span>-->
|
||||
<el-select v-model="roleId" filterable placeholder="角色" class="role-select" @change="_newChat">
|
||||
<el-select v-model="roleId" filterable placeholder="角色" class="role-select" @change="_newChat"
|
||||
style="width:150px">
|
||||
<el-option
|
||||
v-for="item in roles"
|
||||
:key="item.id"
|
||||
@ -97,7 +97,8 @@
|
||||
</el-option>
|
||||
</el-select>
|
||||
|
||||
<el-select v-model="modelID" placeholder="模型" @change="_newChat">
|
||||
<el-select v-model="modelID" placeholder="模型" @change="_newChat" :disabled="disableModel"
|
||||
style="width:150px">
|
||||
<el-option
|
||||
v-for="item in models"
|
||||
:key="item.id"
|
||||
@ -122,28 +123,6 @@
|
||||
<i class="iconfont icon-export"></i>
|
||||
<span>导出会话</span>
|
||||
</el-button>
|
||||
|
||||
<el-tooltip class="box-item"
|
||||
effect="dark"
|
||||
content="部署文档"
|
||||
placement="bottom">
|
||||
<a href="https://ai.r9it.com/docs/install/" target="_blank">
|
||||
<el-button type="primary" circle>
|
||||
<i class="iconfont icon-book"></i>
|
||||
</el-button>
|
||||
</a>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip class="box-item"
|
||||
effect="dark"
|
||||
content="项目源码"
|
||||
placement="bottom">
|
||||
<a href="https://github.com/yangjian102621/chatgpt-plus" target="_blank">
|
||||
<el-button type="success" circle>
|
||||
<i class="iconfont icon-github"></i>
|
||||
</el-button>
|
||||
</a>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -294,9 +273,9 @@ const leftBoxHeight = ref(0);
|
||||
const loading = ref(true);
|
||||
const loginUser = ref(null);
|
||||
const roles = ref([]);
|
||||
const router = useRouter();
|
||||
const roleId = ref(0)
|
||||
const newChatItem = ref(null);
|
||||
const router = useRouter();
|
||||
const showConfigDialog = ref(false);
|
||||
const showLoginDialog = ref(false)
|
||||
const isLogin = ref(false)
|
||||
@ -327,6 +306,7 @@ httpGet("/api/config/get?key=notice").then(res => {
|
||||
showNotice.value = true
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
}
|
||||
|
||||
}).catch(e => {
|
||||
@ -375,17 +355,14 @@ const initData = () => {
|
||||
// 加载角色列表
|
||||
httpGet(`/api/role/list`).then((res) => {
|
||||
roles.value = res.data;
|
||||
roleId.value = roles.value[0]['id'];
|
||||
|
||||
const chatId = localStorage.getItem("chat_id")
|
||||
const chat = getChatById(chatId)
|
||||
if (chat === null) {
|
||||
// 创建新的对话
|
||||
newChat();
|
||||
console.log()
|
||||
if (router.currentRoute.value.params.role_id) {
|
||||
roleId.value = parseInt(router.currentRoute.value.params["role_id"])
|
||||
} else {
|
||||
// 加载对话
|
||||
loadChat(chat)
|
||||
roleId.value = roles.value[0]['id']
|
||||
}
|
||||
|
||||
newChat();
|
||||
}).catch((e) => {
|
||||
ElMessage.error('获取聊天角色失败: ' + e.messages)
|
||||
})
|
||||
@ -445,6 +422,8 @@ const _newChat = () => {
|
||||
newChat()
|
||||
}
|
||||
}
|
||||
|
||||
const disableModel = ref(false)
|
||||
// 新建会话
|
||||
const newChat = () => {
|
||||
if (!isLogin.value) {
|
||||
@ -452,10 +431,11 @@ const newChat = () => {
|
||||
return;
|
||||
}
|
||||
const role = getRoleById(roleId.value)
|
||||
if (role.key === 'gpt') {
|
||||
showHello.value = true
|
||||
} else {
|
||||
showHello.value = false
|
||||
showHello.value = role.key === 'gpt';
|
||||
// if the role bind a model, disable model change
|
||||
if (role.model_id > 0) {
|
||||
modelID.value = role.model_id
|
||||
disableModel.value = true
|
||||
}
|
||||
// 已有新开的会话
|
||||
if (newChatItem.value !== null && newChatItem.value['role_id'] === roles.value[0]['role_id']) {
|
||||
@ -678,6 +658,7 @@ const connect = function (chat_id, role_id) {
|
||||
reader.onload = () => {
|
||||
const data = JSON.parse(String(reader.result));
|
||||
if (data.type === 'start') {
|
||||
console.log(data)
|
||||
chatData.value.push({
|
||||
type: "reply",
|
||||
id: randString(32),
|
||||
|
452
web/src/views/Dalle.vue
Normal file
@ -0,0 +1,452 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-dall">
|
||||
<div class="inner custom-scroll">
|
||||
<div class="sd-box">
|
||||
<h2>DALL-E 创作中心</h2>
|
||||
|
||||
<div class="sd-params" :style="{ height: paramBoxHeight + 'px' }">
|
||||
<el-form :model="params" label-width="80px" label-position="left">
|
||||
<div class="param-line" style="padding-top: 10px">
|
||||
<el-form-item label="图片质量">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-select v-model="params.quality" style="width:176px">
|
||||
<el-option v-for="v in qualities" :label="v.name" :value="v.value" :key="v.value"/>
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="param-line">
|
||||
<el-form-item label="图片尺寸">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-select v-model="params.size" style="width:176px">
|
||||
<el-option v-for="v in sizes" :label="v" :value="v" :key="v"/>
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="param-line">
|
||||
<el-form-item label="图片样式">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-select v-model="params.style" style="width:176px">
|
||||
<el-option v-for="v in styles" :label="v.name" :value="v.value" :key="v.value"/>
|
||||
</el-select>
|
||||
<el-tooltip
|
||||
effect="light"
|
||||
content="生动使模型倾向于生成超真实和戏剧性的图像"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon class="info-icon">
|
||||
<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="请在此输入绘画提示词,系统会自动翻译中文提示词,高手请直接输入英文提示词"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="text-info">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">
|
||||
<el-tag>每次绘图消耗{{ dallPower }}算力</el-tag>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-tag type="success">当前可用{{ power }}算力</el-tag>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</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" @scrollend="handleScrollEnd">
|
||||
<div class="task-list-inner" :style="{ height: listBoxHeight + 'px' }">
|
||||
<div class="job-list-box">
|
||||
<h2>任务列表</h2>
|
||||
<div class="running-job-list">
|
||||
<ItemList :items="runningJobs" v-if="runningJobs.length > 0" :width="240">
|
||||
<template #default>
|
||||
<div class="job-item">
|
||||
<el-image fit="cover">
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<i class="iconfont icon-quick-start"></i>
|
||||
<span>任务正在排队中</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
</template>
|
||||
</ItemList>
|
||||
<el-empty :image-size="100" v-else/>
|
||||
</div>
|
||||
|
||||
<h2>创作记录</h2>
|
||||
<div class="finish-job-list" v-loading="loading" element-loading-background="rgba(0, 0, 0, 0.5)">
|
||||
<div v-if="finishedJobs.length > 0">
|
||||
<ItemList :items="finishedJobs" :width="240" :gap="16">
|
||||
<template #default="scope">
|
||||
<div class="job-item">
|
||||
<el-image v-if="scope.item['img_url']"
|
||||
:src="scope.item['img_url']+'?imageView2/1/w/240/h/240/q/75'"
|
||||
fit="cover"
|
||||
:preview-src-list="[scope.item['img_url']]"
|
||||
loading="lazy">
|
||||
<template #placeholder>
|
||||
<div class="image-slot">
|
||||
正在加载图片
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<el-icon>
|
||||
<Picture/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
|
||||
<el-image v-else
|
||||
:src="scope.item['org_url']"
|
||||
fit="cover"
|
||||
:preview-src-list="[scope.item['org_url']]"
|
||||
loading="lazy">
|
||||
<template #placeholder>
|
||||
<div class="image-slot">
|
||||
正在加载图片
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<el-icon>
|
||||
<Picture/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
|
||||
<div class="remove">
|
||||
<el-tooltip content="删除" placement="top" effect="light">
|
||||
<el-button type="danger" :icon="Delete" @click="removeImage($event,scope.item)" circle/>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="分享" placement="top" effect="light" v-if="scope.item.publish">
|
||||
<el-button type="warning"
|
||||
@click="publishImage($event,scope.item, false)"
|
||||
circle>
|
||||
<i class="iconfont icon-cancel-share"></i>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="取消分享" placement="top" effect="light" v-else>
|
||||
<el-button type="success" @click="publishImage($event,scope.item, true)" circle>
|
||||
<i class="iconfont icon-share-bold"></i>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip content="复制提示词" placement="top" effect="light">
|
||||
<el-button type="info" circle class="copy-prompt" :data-clipboard-text="scope.item.prompt">
|
||||
<i class="iconfont icon-file"></i>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ItemList>
|
||||
|
||||
<div class="no-more-data" v-if="isOver">
|
||||
<span>没有更多数据了</span>
|
||||
<i class="iconfont icon-face"></i>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty :image-size="100" v-else/>
|
||||
</div> <!-- end finish job list-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- end task list box -->
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<login-dialog :show="showLoginDialog" @hide="showLoginDialog = false" @success="initData"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, onUnmounted, ref} from "vue"
|
||||
import {Delete, InfoFilled, Picture} from "@element-plus/icons-vue";
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import {ElMessage, ElMessageBox, ElNotification} from "element-plus";
|
||||
import ItemList from "@/components/ItemList.vue";
|
||||
import Clipboard from "clipboard";
|
||||
import {checkSession} from "@/action/session";
|
||||
import LoginDialog from "@/components/LoginDialog.vue";
|
||||
|
||||
const listBoxHeight = ref(window.innerHeight - 40)
|
||||
const paramBoxHeight = ref(window.innerHeight - 150)
|
||||
const showLoginDialog = ref(false)
|
||||
const isLogin = ref(false)
|
||||
|
||||
window.onresize = () => {
|
||||
listBoxHeight.value = window.innerHeight - 40
|
||||
paramBoxHeight.value = window.innerHeight - 150
|
||||
}
|
||||
const qualities = [
|
||||
{name: "标准", value: "standard"},
|
||||
{name: "高清", value: "hd"},
|
||||
]
|
||||
const sizes = ["1024x1024", "1792x1024", "1024x1792"]
|
||||
const styles = [
|
||||
{name: "生动", value: "vivid"},
|
||||
{name: "自然", value: "natural"}
|
||||
]
|
||||
const params = ref({
|
||||
quality: "standard",
|
||||
size: "1024x1024",
|
||||
style: "vivid",
|
||||
prompt: ""
|
||||
})
|
||||
|
||||
const finishedJobs = ref([])
|
||||
const runningJobs = ref([])
|
||||
const power = ref(0)
|
||||
const dallPower = ref(0) // 画一张 SD 图片消耗算力
|
||||
const clipboard = ref(null)
|
||||
const userId = ref(0)
|
||||
onMounted(() => {
|
||||
initData()
|
||||
clipboard.value = new Clipboard('.copy-prompt');
|
||||
clipboard.value.on('success', () => {
|
||||
ElMessage.success("复制成功!");
|
||||
})
|
||||
|
||||
clipboard.value.on('error', () => {
|
||||
ElMessage.error('复制失败!');
|
||||
})
|
||||
|
||||
httpGet("/api/config/get?key=system").then(res => {
|
||||
dallPower.value = res.data["dall_power"]
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取系统配置失败:" + e.message)
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clipboard.value.destroy()
|
||||
if (socket.value !== null) {
|
||||
socket.value.close()
|
||||
socket.value = null
|
||||
}
|
||||
})
|
||||
|
||||
const initData = () => {
|
||||
checkSession().then(user => {
|
||||
power.value = user['power']
|
||||
userId.value = user.id
|
||||
isLogin.value = true
|
||||
fetchRunningJobs()
|
||||
fetchFinishJobs(1)
|
||||
connect()
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
});
|
||||
}
|
||||
|
||||
const handleScrollEnd = () => {
|
||||
if (isOver.value === true) {
|
||||
return
|
||||
}
|
||||
page.value += 1
|
||||
fetchFinishJobs(page.value)
|
||||
}
|
||||
|
||||
const socket = ref(null)
|
||||
const heartbeatHandle = ref(null)
|
||||
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 sendHeartbeat = () => {
|
||||
clearTimeout(heartbeatHandle.value)
|
||||
new Promise((resolve, reject) => {
|
||||
if (socket.value !== null) {
|
||||
socket.value.send(JSON.stringify({type: "heartbeat", content: "ping"}))
|
||||
}
|
||||
resolve("success")
|
||||
}).then(() => {
|
||||
heartbeatHandle.value = setTimeout(() => sendHeartbeat(), 5000)
|
||||
});
|
||||
}
|
||||
|
||||
const _socket = new WebSocket(host + `/api/dall/client?user_id=${userId.value}`);
|
||||
_socket.addEventListener('open', () => {
|
||||
socket.value = _socket;
|
||||
|
||||
// 发送心跳消息
|
||||
sendHeartbeat()
|
||||
});
|
||||
|
||||
_socket.addEventListener('message', event => {
|
||||
if (event.data instanceof Blob) {
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(event.data, "UTF-8")
|
||||
reader.onload = () => {
|
||||
const message = String(reader.result)
|
||||
if (message === "FINISH") {
|
||||
page.value = 1
|
||||
fetchFinishJobs(page.value)
|
||||
isOver.value = false
|
||||
}
|
||||
fetchRunningJobs()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
_socket.addEventListener('close', () => {
|
||||
if (socket.value !== null) {
|
||||
connect()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const fetchRunningJobs = () => {
|
||||
// 获取运行中的任务
|
||||
httpGet(`/api/dall/jobs?status=0`).then(res => {
|
||||
const jobs = res.data
|
||||
const _jobs = []
|
||||
for (let i = 0; i < jobs.length; i++) {
|
||||
if (jobs[i].progress === -1) {
|
||||
ElNotification({
|
||||
title: '任务执行失败',
|
||||
dangerouslyUseHTMLString: true,
|
||||
message: `任务ID:${jobs[i]['task_id']}<br />原因:${jobs[i]['err_msg']}`,
|
||||
type: 'error',
|
||||
})
|
||||
power.value += dallPower.value
|
||||
continue
|
||||
}
|
||||
_jobs.push(jobs[i])
|
||||
}
|
||||
runningJobs.value = _jobs
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取任务失败:" + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const page = ref(1)
|
||||
const pageSize = ref(15)
|
||||
const isOver = ref(false)
|
||||
const loading = ref(false)
|
||||
// 获取已完成的任务
|
||||
const fetchFinishJobs = (page) => {
|
||||
loading.value = true
|
||||
httpGet(`/api/dall/jobs?status=1&page=${page}&page_size=${pageSize.value}`).then(res => {
|
||||
if (res.data.length < pageSize.value) {
|
||||
isOver.value = true
|
||||
}
|
||||
if (page === 1) {
|
||||
finishedJobs.value = res.data
|
||||
} else {
|
||||
finishedJobs.value = finishedJobs.value.concat(res.data)
|
||||
}
|
||||
loading.value = false
|
||||
}).catch(e => {
|
||||
loading.value = false
|
||||
ElMessage.error("获取任务失败:" + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// 创建绘图任务
|
||||
const promptRef = ref(null)
|
||||
const generate = () => {
|
||||
if (params.value.prompt === '') {
|
||||
promptRef.value.focus()
|
||||
return ElMessage.error("请输入绘画提示词!")
|
||||
}
|
||||
|
||||
if (!isLogin.value) {
|
||||
showLoginDialog.value = true
|
||||
return
|
||||
}
|
||||
httpPost("/api/dall/image", params.value).then(() => {
|
||||
ElMessage.success("任务执行成功!")
|
||||
power.value -= dallPower.value
|
||||
}).catch(e => {
|
||||
ElMessage.error("任务执行失败:" + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const removeImage = (event, item) => {
|
||||
event.stopPropagation()
|
||||
ElMessageBox.confirm(
|
||||
'此操作将会删除任务和图片,继续操作码?',
|
||||
'删除提示',
|
||||
{
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
).then(() => {
|
||||
httpPost("/api/dall/remove", {id: item.id, img_url: item.img_url, user_id: userId.value}).then(() => {
|
||||
ElMessage.success("任务删除成功")
|
||||
fetchFinishJobs(1)
|
||||
}).catch(e => {
|
||||
ElMessage.error("任务删除失败:" + e.message)
|
||||
})
|
||||
}).catch(() => {
|
||||
})
|
||||
}
|
||||
|
||||
// 发布图片到作品墙
|
||||
const publishImage = (event, item, action) => {
|
||||
event.stopPropagation()
|
||||
let text = "图片发布"
|
||||
if (action === false) {
|
||||
text = "取消发布"
|
||||
}
|
||||
httpPost("/api/dall/publish", {id: item.id, action: action}).then(() => {
|
||||
ElMessage.success(text + "成功")
|
||||
item.publish = action
|
||||
}).catch(e => {
|
||||
ElMessage.error(text + "失败:" + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@import "@/assets/css/image-dall.styl"
|
||||
@import "@/assets/css/custom-scroll.styl"
|
||||
</style>
|
@ -2,16 +2,40 @@
|
||||
<div class="home">
|
||||
<div class="navigator">
|
||||
<div class="logo">
|
||||
<el-image :src="logo"/>
|
||||
<el-image :src="logo" @click="router.push('/')"/>
|
||||
<div class="divider"></div>
|
||||
</div>
|
||||
<ul class="nav-items">
|
||||
<li v-for="item in navs" :key="item.url">
|
||||
<li v-for="item in mainNavs" :key="item.url">
|
||||
<a @click="changeNav(item)" :class="item.url === curPath ? 'active' : ''">
|
||||
<el-image :src="item.icon" style="width: 30px;height: 30px"/>
|
||||
</a>
|
||||
<div :class="item.url === curPath ? 'title active' : 'title'">{{ item.name }}</div>
|
||||
</li>
|
||||
|
||||
<el-popover
|
||||
v-if="moreNavs.length > 0"
|
||||
placement="right-end"
|
||||
trigger="hover"
|
||||
>
|
||||
<template #reference>
|
||||
<li>
|
||||
<a class="active">
|
||||
<el-image src="/images/menu/more.png" style="width: 30px;height: 30px"/>
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
<template #default>
|
||||
<ul class="more-menus">
|
||||
<li v-for="item in moreNavs" :key="item.url" :class="item.url === curPath ? 'active' : ''">
|
||||
<a @click="changeNav(item)">
|
||||
<el-image :src="item.icon" style="width: 20px;height: 20px"/>
|
||||
<span :class="item.url === curPath ? 'title active' : 'title'">{{ item.name }}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</el-popover>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="content">
|
||||
@ -33,7 +57,8 @@ import {ElMessage} from "element-plus";
|
||||
|
||||
const router = useRouter();
|
||||
const logo = ref('/images/logo.png');
|
||||
const navs = ref([])
|
||||
const mainNavs = ref([])
|
||||
const moreNavs = ref([])
|
||||
const curPath = ref(router.currentRoute.value.path)
|
||||
|
||||
const changeNav = (item) => {
|
||||
@ -49,7 +74,11 @@ onMounted(() => {
|
||||
})
|
||||
// 获取菜单
|
||||
httpGet("/api/menu/list").then(res => {
|
||||
navs.value = res.data
|
||||
mainNavs.value = res.data
|
||||
if (res.data.length > 7) {
|
||||
mainNavs.value = res.data.slice(0, 7)
|
||||
moreNavs.value = res.data.slice(7)
|
||||
}
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取系统菜单失败:" + e.message)
|
||||
})
|
||||
@ -75,6 +104,7 @@ onMounted(() => {
|
||||
display flex
|
||||
flex-flow column
|
||||
align-items center
|
||||
cursor pointer
|
||||
|
||||
.el-image {
|
||||
width 50px
|
||||
@ -89,7 +119,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.nav-items {
|
||||
margin-top: 20px;
|
||||
margin-top: 10px;
|
||||
padding 0 5px
|
||||
|
||||
li {
|
||||
@ -131,13 +161,40 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
box-sizing: border-box;
|
||||
width: 100%
|
||||
height: 100vh
|
||||
box-sizing: border-box
|
||||
background-color #282c34
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.el-popper {
|
||||
.more-menus {
|
||||
li {
|
||||
padding 10px 15px
|
||||
cursor pointer
|
||||
border-radius 5px
|
||||
margin 5px 0
|
||||
|
||||
.el-image {
|
||||
position: relative
|
||||
top 5px
|
||||
right 5px
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color #f1f1f1
|
||||
}
|
||||
}
|
||||
|
||||
li.active {
|
||||
background-color #f1f1f1
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -4,7 +4,7 @@
|
||||
<div class="mj-box">
|
||||
<h2>MidJourney 创作中心</h2>
|
||||
|
||||
<div class="mj-params" :style="{ height: mjBoxHeight + 'px' }">
|
||||
<div class="mj-params" :style="{ height: paramBoxHeight + 'px' }">
|
||||
<el-form :model="params" label-width="80px" label-position="left">
|
||||
<div class="param-line pt">
|
||||
<span>图片比例:</span>
|
||||
@ -33,7 +33,7 @@
|
||||
<el-form-item label="图片画质">
|
||||
<template #default>
|
||||
<div class="form-item-inner flex-row items-center">
|
||||
<el-select v-model="params.quality" placeholder="请选择">
|
||||
<el-select v-model="params.quality" placeholder="请选择" style="width:175px">
|
||||
<el-option v-for="item in options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
@ -525,42 +525,10 @@
|
||||
<div class="opt" v-if="scope.item['can_opt']">
|
||||
<div class="opt-line">
|
||||
<ul>
|
||||
<li>
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="light"
|
||||
content="放大第一张"
|
||||
placement="top">
|
||||
<a @click="upscale(1, scope.item)">U1</a>
|
||||
</el-tooltip>
|
||||
</li>
|
||||
<li>
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="light"
|
||||
content="放大第二张"
|
||||
placement="top">
|
||||
<a @click="upscale(2, scope.item)">U2</a>
|
||||
</el-tooltip>
|
||||
</li>
|
||||
<li>
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="light"
|
||||
content="放大第三张"
|
||||
placement="top">
|
||||
<a @click="upscale(3, scope.item)">U3</a>
|
||||
</el-tooltip>
|
||||
</li>
|
||||
<li>
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="light"
|
||||
content="放大第四张"
|
||||
placement="top">
|
||||
<a @click="upscale(4, scope.item)">U4</a>
|
||||
</el-tooltip>
|
||||
</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">
|
||||
@ -586,42 +554,10 @@
|
||||
|
||||
<div class="opt-line">
|
||||
<ul>
|
||||
<li>
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="light"
|
||||
content="变化第一张"
|
||||
placement="top">
|
||||
<a @click="variation(1, scope.item)">V1</a>
|
||||
</el-tooltip>
|
||||
</li>
|
||||
<li>
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="light"
|
||||
content="变化第二张"
|
||||
placement="top">
|
||||
<a @click="variation(2, scope.item)">V2</a>
|
||||
</el-tooltip>
|
||||
</li>
|
||||
<li>
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="light"
|
||||
content="变化第三张"
|
||||
placement="top">
|
||||
<a @click="variation(3, scope.item)">V3</a>
|
||||
</el-tooltip>
|
||||
</li>
|
||||
<li>
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="light"
|
||||
content="变化第四张"
|
||||
placement="top">
|
||||
<a @click="variation(4, scope.item)">V4</a>
|
||||
</el-tooltip>
|
||||
</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>
|
||||
@ -671,12 +607,12 @@ import {copyObj, removeArrayItem} from "@/utils/libs";
|
||||
import LoginDialog from "@/components/LoginDialog.vue";
|
||||
|
||||
const listBoxHeight = ref(window.innerHeight - 40)
|
||||
const mjBoxHeight = ref(window.innerHeight - 150)
|
||||
const paramBoxHeight = ref(window.innerHeight - 150)
|
||||
const showLoginDialog = ref(false)
|
||||
|
||||
window.onresize = () => {
|
||||
listBoxHeight.value = window.innerHeight - 40
|
||||
mjBoxHeight.value = window.innerHeight - 150
|
||||
paramBoxHeight.value = window.innerHeight - 150
|
||||
}
|
||||
const rates = [
|
||||
{css: "square", value: "1:1", text: "1:1", img: "/images/mj/rate_1_1.png"},
|
||||
@ -789,10 +725,17 @@ const connect = () => {
|
||||
|
||||
_socket.addEventListener('message', event => {
|
||||
if (event.data instanceof Blob) {
|
||||
fetchRunningJobs()
|
||||
isOver.value = false
|
||||
page.value = 1
|
||||
fetchFinishJobs(page.value)
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(event.data, "UTF-8")
|
||||
reader.onload = () => {
|
||||
const message = String(reader.result)
|
||||
if (message === "FINISH") {
|
||||
page.value = 1
|
||||
fetchFinishJobs(page.value)
|
||||
isOver.value = false
|
||||
}
|
||||
fetchRunningJobs()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -994,8 +937,6 @@ const generate = () => {
|
||||
httpPost("/api/mj/image", params.value).then(() => {
|
||||
ElMessage.success("绘画任务推送成功,请耐心等待任务执行...")
|
||||
power.value -= mjPower.value
|
||||
params.value = copyObj(initParams)
|
||||
imgList.value = []
|
||||
}).catch(e => {
|
||||
ElMessage.error("任务推送失败:" + e.message)
|
||||
})
|
||||
|
@ -5,13 +5,13 @@
|
||||
<div class="sd-box">
|
||||
<h2>Stable Diffusion 创作中心</h2>
|
||||
|
||||
<div class="sd-params" :style="{ height: mjBoxHeight + 'px' }">
|
||||
<div class="sd-params" :style="{ height: paramBoxHeight + 'px' }">
|
||||
<el-form :model="params" label-width="80px" label-position="left">
|
||||
<div class="param-line" style="padding-top: 10px">
|
||||
<el-form-item label="采样方法">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-select v-model="params.sampler" size="small">
|
||||
<el-select v-model="params.sampler" style="width:176px">
|
||||
<el-option v-for="item in samplers" :label="item" :value="item" :key="item"/>
|
||||
</el-select>
|
||||
<el-tooltip
|
||||
@ -20,7 +20,7 @@
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<el-icon class="info-icon">
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
@ -35,10 +35,10 @@
|
||||
<div class="form-item-inner">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-input v-model.number="params.width" size="small" placeholder="图片宽度"/>
|
||||
<el-input v-model.number="params.width" placeholder="图片宽度"/>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-input v-model.number="params.height" size="small" placeholder="图片高度"/>
|
||||
<el-input v-model.number="params.height" placeholder="图片高度"/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
@ -50,14 +50,14 @@
|
||||
<el-form-item label="迭代步数">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-input v-model.number="params.steps" size="small"/>
|
||||
<el-input v-model.number="params.steps"/>
|
||||
<el-tooltip
|
||||
effect="light"
|
||||
content="值越大则代表细节越多,同时也意味着出图速度越慢"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<el-icon class="info-icon">
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
@ -70,14 +70,14 @@
|
||||
<el-form-item label="引导系数">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-input v-model.number="params.cfg_scale" size="small"/>
|
||||
<el-input v-model.number="params.cfg_scale"/>
|
||||
<el-tooltip
|
||||
effect="light"
|
||||
content="提示词引导系数,图像在多大程度上服从提示词<br/> 较低值会产生更有创意的结果"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<el-icon class="info-icon">
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
@ -90,14 +90,14 @@
|
||||
<el-form-item label="随机因子">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-input v-model.number="params.seed" size="small"/>
|
||||
<el-input v-model.number="params.seed"/>
|
||||
<el-tooltip
|
||||
effect="light"
|
||||
content="随机数种子,相同的种子会得到相同的结果<br/> 设置为 -1 则每次随机生成种子"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<el-icon class="info-icon">
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
@ -108,7 +108,7 @@
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon @click="params.seed = -1">
|
||||
<el-icon @click="params.seed = -1" class="info-icon">
|
||||
<Orange/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
@ -121,14 +121,14 @@
|
||||
<el-form-item label="高清修复">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-switch v-model="params.hd_fix" style="--el-switch-on-color: #47fff1;"/>
|
||||
<el-switch v-model="params.hd_fix" style="--el-switch-on-color: #47fff1;" size="large"/>
|
||||
<el-tooltip
|
||||
effect="light"
|
||||
content="先以较小的分辨率生成图像,接着方法图像<br />然后在不更改构图的情况下再修改细节"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon style="margin-top: 6px">
|
||||
<el-icon style="margin-left: 10px; top: 12px">
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
@ -150,7 +150,7 @@
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon style="margin-top: 6px">
|
||||
<el-icon class="info-icon">
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
@ -163,7 +163,7 @@
|
||||
<el-form-item label="放大算法">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-select v-model="params.hd_scale_alg" size="small">
|
||||
<el-select v-model="params.hd_scale_alg" style="width:176px">
|
||||
<el-option v-for="item in scaleAlg" :label="item" :value="item" :key="item"/>
|
||||
</el-select>
|
||||
<el-tooltip
|
||||
@ -172,7 +172,7 @@
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<el-icon class="info-icon">
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
@ -185,14 +185,14 @@
|
||||
<el-form-item label="放大倍数">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-input v-model.number="params.hd_scale" size="small"/>
|
||||
<el-input v-model.number="params.hd_scale"/>
|
||||
<el-tooltip
|
||||
effect="light"
|
||||
content="随机数种子,相同的种子会得到相同的结果<br/> 设置为 -1 则每次随机生成种子"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<el-icon class="info-icon">
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
@ -205,14 +205,14 @@
|
||||
<el-form-item label="迭代步数">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-input v-model.number="params.hd_steps" size="small"/>
|
||||
<el-input v-model.number="params.hd_steps"/>
|
||||
<el-tooltip
|
||||
effect="light"
|
||||
content="重绘迭代步数,如果设置为0,则设置跟原图相同的迭代步数"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<el-icon class="info-icon">
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
@ -239,7 +239,7 @@
|
||||
content="不希望出现的元素,下面给了默认的起手式"
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<el-icon class="info-icon">
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
@ -254,8 +254,14 @@
|
||||
</div>
|
||||
|
||||
<div class="text-info">
|
||||
<el-tag>每次绘图消耗{{ sdPower }}算力</el-tag>
|
||||
<el-tag type="success">当前可用算力:{{ power }}</el-tag>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">
|
||||
<el-tag>单次绘图消耗{{ sdPower }}算力</el-tag>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-tag type="success">当前可用{{ power }}算力</el-tag>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
</el-form>
|
||||
@ -492,7 +498,7 @@ import {getSessionId} from "@/store/session";
|
||||
import LoginDialog from "@/components/LoginDialog.vue";
|
||||
|
||||
const listBoxHeight = ref(window.innerHeight - 40)
|
||||
const mjBoxHeight = ref(window.innerHeight - 150)
|
||||
const paramBoxHeight = ref(window.innerHeight - 150)
|
||||
const fullImgHeight = ref(window.innerHeight - 60)
|
||||
const showTaskDialog = ref(false)
|
||||
const item = ref({})
|
||||
@ -501,7 +507,7 @@ const isLogin = ref(false)
|
||||
|
||||
window.onresize = () => {
|
||||
listBoxHeight.value = window.innerHeight - 40
|
||||
mjBoxHeight.value = window.innerHeight - 150
|
||||
paramBoxHeight.value = window.innerHeight - 150
|
||||
}
|
||||
const samplers = ["Euler a", "DPM++ 2S a Karras", "DPM++ 2M Karras", "DPM++ SDE Karras", "DPM++ 2M SDE Karras"]
|
||||
const scaleAlg = ["Latent", "ESRGAN_4x", "R-ESRGAN 4x+", "SwinIR_4x", "LDSR"]
|
||||
@ -568,10 +574,17 @@ const connect = () => {
|
||||
|
||||
_socket.addEventListener('message', event => {
|
||||
if (event.data instanceof Blob) {
|
||||
fetchRunningJobs()
|
||||
isOver.value = false
|
||||
page.value = 1
|
||||
fetchFinishJobs(page.value)
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(event.data, "UTF-8")
|
||||
reader.onload = () => {
|
||||
const message = String(reader.result)
|
||||
if (message === "FINISH") {
|
||||
page.value = 1
|
||||
fetchFinishJobs(page.value)
|
||||
isOver.value = false
|
||||
}
|
||||
fetchRunningJobs()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -579,7 +592,7 @@ const connect = () => {
|
||||
if (socket.value !== null) {
|
||||
connect()
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
const clipboard = ref(null)
|
||||
|
@ -7,6 +7,7 @@
|
||||
<el-radio-group v-model="imgType" @change="changeImgType">
|
||||
<el-radio label="mj" size="large">MidJourney</el-radio>
|
||||
<el-radio label="sd" size="large">Stable Diffusion</el-radio>
|
||||
<el-radio label="dall" size="large">DALL-E</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
@ -71,6 +72,57 @@
|
||||
</template>
|
||||
</v3-waterfall>
|
||||
|
||||
<v3-waterfall v-if="imgType === 'dall'"
|
||||
id="waterfall"
|
||||
:list="data['dall']"
|
||||
srcKey="img_thumb"
|
||||
:gap="12"
|
||||
:bottomGap="-5"
|
||||
:colWidth="colWidth"
|
||||
:distanceToScroll="100"
|
||||
:isLoading="loading"
|
||||
:isOver="false"
|
||||
@scrollReachBottom="getNext">
|
||||
<template #default="slotProp">
|
||||
<div class="list-item">
|
||||
<div class="image">
|
||||
<el-image :src="slotProp.item['img_thumb']"
|
||||
:zoom-rate="1.2"
|
||||
:preview-src-list="[slotProp.item['img_url']]"
|
||||
:preview-teleported="true"
|
||||
:initial-index="10"
|
||||
loading="lazy">
|
||||
<template #placeholder>
|
||||
<div class="image-slot">
|
||||
正在加载图片
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<el-icon>
|
||||
<Picture/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
<div class="opt">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="light"
|
||||
content="复制提示词"
|
||||
placement="top"
|
||||
>
|
||||
<el-icon class="copy-prompt-wall" :data-clipboard-text="slotProp.item.prompt">
|
||||
<DocumentCopy/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</v3-waterfall>
|
||||
|
||||
<v3-waterfall v-else
|
||||
id="waterfall"
|
||||
:list="data['sd']"
|
||||
@ -252,7 +304,8 @@ import {useRouter} from "vue-router";
|
||||
|
||||
const data = ref({
|
||||
"mj": [],
|
||||
"sd": []
|
||||
"sd": [],
|
||||
"dall": [],
|
||||
})
|
||||
const loading = ref(true)
|
||||
const isOver = ref(false)
|
||||
@ -284,10 +337,22 @@ const getNext = () => {
|
||||
|
||||
loading.value = true
|
||||
page.value = page.value + 1
|
||||
const url = imgType.value === "mj" ? "/api/mj/imgWall" : "/api/sd/imgWall"
|
||||
let url = ""
|
||||
console.log(imgType.value)
|
||||
switch (imgType.value) {
|
||||
case "mj":
|
||||
url = "/api/mj/imgWall"
|
||||
break
|
||||
case "sd":
|
||||
url = "/api/sd/imgWall"
|
||||
break
|
||||
case "dall":
|
||||
url = "/api/dall/imgWall"
|
||||
break
|
||||
}
|
||||
httpGet(`${url}?page=${page.value}&page_size=${pageSize.value}`).then(res => {
|
||||
loading.value = false
|
||||
if (res.data.length === 0) {
|
||||
if (!res.data || res.data.length === 0) {
|
||||
isOver.value = true
|
||||
return
|
||||
}
|
||||
@ -335,7 +400,8 @@ const changeImgType = () => {
|
||||
page.value = 0
|
||||
data.value = {
|
||||
"mj": [],
|
||||
"sd": []
|
||||
"sd": [],
|
||||
"dall": [],
|
||||
}
|
||||
loading.value = true
|
||||
isOver.value = false
|
||||
|
237
web/src/views/Index.vue
Normal file
@ -0,0 +1,237 @@
|
||||
<template>
|
||||
<div class="index-page" :style="{height: winHeight+'px'}">
|
||||
<div class="menu-box">
|
||||
<el-menu
|
||||
mode="horizontal"
|
||||
:ellipsis="false"
|
||||
>
|
||||
<div class="menu-item">
|
||||
<el-image :src="logo" alt="Geek-AI"/>
|
||||
<div class="title">{{ title }}</div>
|
||||
</div>
|
||||
<div class="menu-item">
|
||||
<a href="https://ai.r9it.com/docs/install/" target="_blank">
|
||||
<el-button type="primary" round>
|
||||
<i class="iconfont icon-book"></i>
|
||||
<span>部署文档</span>
|
||||
</el-button>
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/yangjian102621/chatgpt-plus" target="_blank">
|
||||
<el-button type="success" round>
|
||||
<i class="iconfont icon-github"></i>
|
||||
<span>项目源码</span>
|
||||
</el-button>
|
||||
</a>
|
||||
</div>
|
||||
</el-menu>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h1>欢迎使用 {{ title }}</h1>
|
||||
<p>{{ slogan }}</p>
|
||||
<el-button @click="router.push('/chat')" color="#ffffff" style="color:#007bff" :dark="false">
|
||||
<i class="iconfont icon-chat"></i>
|
||||
<span>AI聊天</span>
|
||||
</el-button>
|
||||
<el-button @click="router.push('/mj')" color="#C4CCFD" style="color:#424282" :dark="false">
|
||||
<i class="iconfont icon-mj"></i>
|
||||
<span>AI-MJ绘画</span>
|
||||
</el-button>
|
||||
|
||||
<el-button @click="router.push('/sd')" color="#4AE6DF" style="color:#424282" :dark="false">
|
||||
<i class="iconfont icon-sd"></i>
|
||||
<span>AI-SD绘画</span>
|
||||
</el-button>
|
||||
<el-button @click="router.push('/xmind')" color="#FFFD55" style="color:#424282" :dark="false">
|
||||
<i class="iconfont icon-xmind"></i>
|
||||
<span>思维导图</span>
|
||||
</el-button>
|
||||
<div id="animation-container"></div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<footer-bar />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
import * as THREE from 'three';
|
||||
import {onMounted, ref} from "vue";
|
||||
import {useRouter} from "vue-router";
|
||||
import FooterBar from "@/components/FooterBar.vue";
|
||||
import {httpGet} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const title = ref("Geek-AI 创作系统")
|
||||
const logo = ref("/images/logo.png")
|
||||
const slogan = ref("我辈之人,先干为敬,陪您先把 AI 用起来")
|
||||
const size = Math.max(window.innerWidth * 0.5, window.innerHeight * 0.8)
|
||||
const winHeight = window.innerHeight - 150
|
||||
|
||||
onMounted(() => {
|
||||
httpGet("/api/config/get?key=system").then(res => {
|
||||
title.value = res.data.title
|
||||
logo.value = res.data.logo
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取系统配置失败:" + e.message)
|
||||
})
|
||||
init()
|
||||
})
|
||||
|
||||
const init = () => {
|
||||
// 创建场景
|
||||
// 创建场景
|
||||
const scene = new THREE.Scene();
|
||||
|
||||
// 创建相机
|
||||
const camera = new THREE.PerspectiveCamera(30, 1, 0.1, 1000);
|
||||
camera.position.z = 3.88;
|
||||
|
||||
// 创建渲染器
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
||||
renderer.setSize(size, size);
|
||||
renderer.setClearColor(0x000000, 0);
|
||||
const container = document.getElementById('animation-container');
|
||||
container.appendChild(renderer.domElement);
|
||||
|
||||
// 加载地球纹理
|
||||
const loader = new THREE.TextureLoader();
|
||||
loader.load(
|
||||
'/images/land_ocean_ice_cloud_2048.jpg',
|
||||
function (texture) {
|
||||
// 创建地球球体
|
||||
const geometry = new THREE.SphereGeometry(1, 32, 32);
|
||||
const material = new THREE.MeshPhongMaterial({
|
||||
map: texture,
|
||||
bumpMap: texture, // 使用同一张纹理作为凹凸贴图
|
||||
bumpScale: 0.05, // 调整凹凸贴图的影响程度
|
||||
specularMap: texture, // 高光贴图
|
||||
specular: new THREE.Color('#007bff'), // 高光颜色
|
||||
});
|
||||
const earth = new THREE.Mesh(geometry, material);
|
||||
scene.add(earth);
|
||||
|
||||
// 添加环境光和点光源
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.3);
|
||||
scene.add(ambientLight);
|
||||
const pointLight = new THREE.PointLight(0xffffff, 0.8);
|
||||
pointLight.position.set(5, 5, 5);
|
||||
scene.add(pointLight);
|
||||
|
||||
// 创建动画
|
||||
const animate = function () {
|
||||
requestAnimationFrame(animate);
|
||||
|
||||
// 使地球自转和公转
|
||||
earth.rotation.y += 0.0006;
|
||||
|
||||
renderer.render(scene, camera);
|
||||
};
|
||||
|
||||
// 执行动画
|
||||
animate();
|
||||
}
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import '@/assets/iconfont/iconfont.css'
|
||||
.index-page {
|
||||
margin: 0
|
||||
background-color #007bff /* 科技蓝色背景 */
|
||||
overflow hidden
|
||||
color #ffffff
|
||||
display flex
|
||||
justify-content center
|
||||
align-items baseline
|
||||
padding-top 150px
|
||||
|
||||
.menu-box {
|
||||
position absolute
|
||||
top 0
|
||||
width 100%
|
||||
display flex
|
||||
|
||||
.el-menu {
|
||||
padding 0 30px
|
||||
width 100%
|
||||
display flex
|
||||
justify-content space-between
|
||||
background none
|
||||
border none
|
||||
|
||||
.menu-item {
|
||||
display flex
|
||||
padding 20px 0
|
||||
|
||||
color #ffffff
|
||||
|
||||
.title {
|
||||
font-size 24px
|
||||
padding 10px 10px 0 10px
|
||||
}
|
||||
|
||||
.el-image {
|
||||
height 50px
|
||||
}
|
||||
|
||||
.el-button {
|
||||
margin-left 10px
|
||||
|
||||
span {
|
||||
margin-left 5px
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
text-align: center;
|
||||
position relative
|
||||
|
||||
h1 {
|
||||
font-size: 5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
padding: 25px 20px;
|
||||
font-size: 1.3rem;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
.iconfont {
|
||||
font-size 1.6rem
|
||||
margin-right 10px
|
||||
}
|
||||
}
|
||||
|
||||
#animation-container {
|
||||
display flex
|
||||
justify-content center
|
||||
width 100%
|
||||
height: 300px;
|
||||
position: absolute;
|
||||
top: 350px
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
.el-link__inner {
|
||||
color #ffffff
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
331
web/src/views/MarkMap.vue
Normal file
@ -0,0 +1,331 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-mark-map">
|
||||
<div class="inner custom-scroll">
|
||||
<div class="mark-map-box">
|
||||
<h2>思维导图创作中心</h2>
|
||||
|
||||
<div class="mark-map-params" :style="{ height: leftBoxHeight + 'px' }">
|
||||
<el-form label-width="80px" label-position="left">
|
||||
<div class="param-line">
|
||||
你的需求?
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-input
|
||||
v-model="prompt"
|
||||
:autosize="{ minRows: 4, maxRows: 6 }"
|
||||
type="textarea"
|
||||
placeholder="请给AI输入提示词,让AI帮你完善"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="param-line">
|
||||
请选择生成思维导图的AI模型
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-select v-model="modelID" placeholder="请选择模型" @change="changeModel" style="width:100%">
|
||||
<el-option
|
||||
v-for="item in models"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
>
|
||||
<span>{{ item.name }}</span>
|
||||
<el-tag style="margin-left: 5px; position: relative; top:-2px" type="info" size="small">{{
|
||||
item.power
|
||||
}}算力
|
||||
</el-tag>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div class="text-info">
|
||||
<el-tag type="success">当前可用算力:{{ loginUser.power }}</el-tag>
|
||||
</div>
|
||||
|
||||
<div class="param-line">
|
||||
<el-button color="#47fff1" :dark="false" round @click="generateAI" :loading="loading">
|
||||
智能生成思维导图
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="param-line">
|
||||
使用已有内容生成?
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-input
|
||||
v-model="content"
|
||||
:autosize="{ minRows: 4, maxRows: 6 }"
|
||||
type="textarea"
|
||||
placeholder="请用markdown语法输入您想要生成思维导图的内容!"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="param-line">
|
||||
<el-button color="#C5F9AE" :dark="false" round @click="generate">直接生成(免费)</el-button>
|
||||
</div>
|
||||
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right-box">
|
||||
<div class="top-bar">
|
||||
<h2>思维导图</h2>
|
||||
<el-button @click="downloadImage" type="primary">
|
||||
<el-icon>
|
||||
<Download/>
|
||||
</el-icon>
|
||||
<span>下载图片</span>
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="markdown" v-if="loading">
|
||||
<div v-html="html"></div>
|
||||
</div>
|
||||
<div class="body" id="markmap" v-show="!loading">
|
||||
<svg ref="svgRef" :style="{ height: rightBoxHeight + 'px' }"/>
|
||||
</div>
|
||||
</div><!-- end task list box -->
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<login-dialog :show="showLoginDialog" @hide="showLoginDialog = false" @success="initData"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import LoginDialog from "@/components/LoginDialog.vue";
|
||||
import {nextTick, onMounted, onUnmounted, ref} from 'vue';
|
||||
import {Markmap} from 'markmap-view';
|
||||
import {loadCSS, loadJS} from 'markmap-common';
|
||||
import {Transformer} from 'markmap-lib';
|
||||
import {checkSession} from "@/action/session";
|
||||
import {httpGet} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {Download} from "@element-plus/icons-vue";
|
||||
|
||||
const leftBoxHeight = ref(window.innerHeight - 105)
|
||||
const rightBoxHeight = ref(window.innerHeight - 85)
|
||||
|
||||
const prompt = ref("")
|
||||
const text = ref(`# Geek-AI 助手
|
||||
|
||||
- 完整的开源系统,前端应用和后台管理系统皆可开箱即用。
|
||||
- 基于 Websocket 实现,完美的打字机体验。
|
||||
- 内置了各种预训练好的角色应用,轻松满足你的各种聊天和应用需求。
|
||||
- 支持 OPenAI,Azure,文心一言,讯飞星火,清华 ChatGLM等多个大语言模型。
|
||||
- 支持 MidJourney / Stable Diffusion AI 绘画集成,开箱即用。
|
||||
- 支持使用个人微信二维码作为充值收费的支付渠道,无需企业支付通道。
|
||||
- 已集成支付宝支付功能,微信支付,支持多种会员套餐和点卡购买功能。
|
||||
- 集成插件 API 功能,可结合大语言模型的 function 功能开发各种强大的插件。
|
||||
`)
|
||||
const md = require('markdown-it')({breaks: true});
|
||||
const content = ref(text.value)
|
||||
const html = ref("")
|
||||
|
||||
const showLoginDialog = ref(false)
|
||||
const isLogin = ref(false)
|
||||
const loginUser = ref({power: 0})
|
||||
const transformer = new Transformer();
|
||||
const {scripts, styles} = transformer.getAssets()
|
||||
loadCSS(styles);
|
||||
loadJS(scripts);
|
||||
|
||||
|
||||
const svgRef = ref(null)
|
||||
const markMap = ref(null)
|
||||
const models = ref([])
|
||||
const modelID = ref(0)
|
||||
const loading = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
initData()
|
||||
markMap.value = Markmap.create(svgRef.value)
|
||||
update()
|
||||
});
|
||||
|
||||
const initData = () => {
|
||||
checkSession().then(user => {
|
||||
loginUser.value = user
|
||||
isLogin.value = true
|
||||
|
||||
httpGet("/api/model/list").then(res => {
|
||||
for (let v of res.data) {
|
||||
if (v.platform === "OpenAI") {
|
||||
models.value.push(v)
|
||||
}
|
||||
}
|
||||
modelID.value = models.value[0].id
|
||||
connect(user.id)
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取模型失败:" + e.message)
|
||||
})
|
||||
}).catch(() => {
|
||||
});
|
||||
}
|
||||
|
||||
const update = () => {
|
||||
|
||||
const {root} = transformer.transform(processContent(text.value))
|
||||
markMap.value.setData(root)
|
||||
markMap.value.fit()
|
||||
}
|
||||
|
||||
const processContent = (text) => {
|
||||
const arr = []
|
||||
const lines = text.split("\n")
|
||||
for (let line of lines) {
|
||||
if (line.indexOf("```") !== -1) {
|
||||
continue
|
||||
}
|
||||
line = line.replace(/([*_~`>])|(\d+\.)\s/g, '')
|
||||
arr.push(line)
|
||||
}
|
||||
return arr.join("\n")
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (socket.value !== null) {
|
||||
socket.value.close()
|
||||
}
|
||||
socket.value = null
|
||||
})
|
||||
|
||||
window.onresize = () => {
|
||||
leftBoxHeight.value = window.innerHeight - 145
|
||||
rightBoxHeight.value = window.innerHeight - 85
|
||||
}
|
||||
|
||||
const socket = ref(null)
|
||||
const heartbeatHandle = ref(null)
|
||||
const connect = (userId) => {
|
||||
if (socket.value !== null) {
|
||||
socket.value.close()
|
||||
}
|
||||
|
||||
let host = process.env.VUE_APP_WS_HOST
|
||||
if (host === '') {
|
||||
if (location.protocol === 'https:') {
|
||||
host = 'wss://' + location.host;
|
||||
} else {
|
||||
host = 'ws://' + location.host;
|
||||
}
|
||||
}
|
||||
|
||||
// 心跳函数
|
||||
const sendHeartbeat = () => {
|
||||
clearTimeout(heartbeatHandle.value)
|
||||
new Promise((resolve, reject) => {
|
||||
if (socket.value !== null) {
|
||||
socket.value.send(JSON.stringify({type: "heartbeat", content: "ping"}))
|
||||
}
|
||||
resolve("success")
|
||||
}).then(() => {
|
||||
heartbeatHandle.value = setTimeout(() => sendHeartbeat(), 5000)
|
||||
});
|
||||
}
|
||||
|
||||
const _socket = new WebSocket(host + `/api/markMap/client?user_id=${userId}&model_id=${modelID.value}`);
|
||||
_socket.addEventListener('open', () => {
|
||||
socket.value = _socket;
|
||||
|
||||
// 发送心跳消息
|
||||
sendHeartbeat()
|
||||
});
|
||||
|
||||
_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))
|
||||
switch (data.type) {
|
||||
case "start":
|
||||
text.value = ""
|
||||
break
|
||||
case "middle":
|
||||
text.value += data.content
|
||||
html.value = md.render(processContent(text.value))
|
||||
break
|
||||
case "end":
|
||||
loading.value = false
|
||||
content.value = processContent(text.value)
|
||||
nextTick(() => update())
|
||||
break
|
||||
case "error":
|
||||
loading.value = false
|
||||
ElMessage.error(data.content)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
_socket.addEventListener('close', () => {
|
||||
loading.value = false
|
||||
if (socket.value !== null) {
|
||||
connect(userId)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const generate = () => {
|
||||
text.value = content.value
|
||||
update()
|
||||
}
|
||||
|
||||
// 使用 AI 智能生成
|
||||
const generateAI = () => {
|
||||
html.value = ''
|
||||
text.value = ''
|
||||
if (prompt.value === '') {
|
||||
return ElMessage.error("请输入你的需求")
|
||||
}
|
||||
if (!isLogin.value) {
|
||||
showLoginDialog.value = true
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
socket.value.send(JSON.stringify({type: "message", content: prompt.value}))
|
||||
}
|
||||
|
||||
const changeModel = () => {
|
||||
if (socket.value !== null) {
|
||||
socket.value.send(JSON.stringify({type: "model_id", content: modelID.value}))
|
||||
}
|
||||
}
|
||||
|
||||
// download SVG to png file
|
||||
const downloadImage = () => {
|
||||
const svgElement = document.getElementById("markmap");
|
||||
// 将 SVG 渲染到图片对象
|
||||
const serializer = new XMLSerializer()
|
||||
const source = '<?xml version="1.0" standalone="no"?>\r\n' + serializer.serializeToString(svgRef.value)
|
||||
const image = new Image()
|
||||
image.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(source)
|
||||
|
||||
// 将图片对象渲染
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = svgElement.offsetWidth
|
||||
canvas.height = svgElement.offsetHeight
|
||||
let context = canvas.getContext('2d')
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
image.onload = function () {
|
||||
context.drawImage(image, 0, 0)
|
||||
const a = document.createElement('a')
|
||||
a.download = "geek-ai-xmind.png"
|
||||
a.href = canvas.toDataURL(`image/png`)
|
||||
a.click()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@import "@/assets/css/mark-map.styl"
|
||||
@import "@/assets/css/custom-scroll.styl"
|
||||
</style>
|
@ -19,7 +19,12 @@
|
||||
</div>
|
||||
|
||||
<el-row v-if="items.length > 0">
|
||||
<el-table :data="items" :row-key="row => row.id" table-layout="auto" border>
|
||||
<el-table :data="items" :row-key="row => row.id" table-layout="auto" border
|
||||
style="--el-table-border-color:#373C47;
|
||||
--el-table-tr-bg-color:#2D323B;
|
||||
--el-table-row-hover-bg-color:#373C47;
|
||||
--el-table-header-bg-color:#474E5C;
|
||||
--el-table-text-color:#d1d1d1">
|
||||
<el-table-column prop="username" label="用户"/>
|
||||
<el-table-column prop="model" label="模型"/>
|
||||
<el-table-column prop="type" label="类型">
|
||||
@ -64,7 +69,7 @@
|
||||
<script setup>
|
||||
import {onMounted, ref} from "vue"
|
||||
import {dateFormat} from "@/utils/libs";
|
||||
import {Back, DocumentCopy, Search} from "@element-plus/icons-vue";
|
||||
import {Search} from "@element-plus/icons-vue";
|
||||
import Clipboard from "clipboard";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {httpPost} from "@/utils/http";
|
||||
@ -79,7 +84,7 @@ const query = ref({
|
||||
model: "",
|
||||
date: []
|
||||
})
|
||||
const tagColors = ref(["", "success", "primary", "danger", "info", "warning"])
|
||||
const tagColors = ref(["", "success", "", "danger", "info", "warning"])
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
@ -119,8 +124,7 @@ const fetchData = () => {
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.power-log {
|
||||
background-color #ffffff
|
||||
|
||||
color #ffffff
|
||||
.inner {
|
||||
padding 0 20px 20px 20px
|
||||
|
||||
|
@ -229,7 +229,7 @@ const register = function () {
|
||||
align-items center
|
||||
|
||||
.contain {
|
||||
padding 0 40px 20px 40px;
|
||||
padding 20px 40px 20px 40px;
|
||||
width 100%
|
||||
color #ffffff
|
||||
border-radius 10px;
|
||||
|
@ -1,16 +1,43 @@
|
||||
<template>
|
||||
<div>
|
||||
{{ title }}
|
||||
<textarea v-model="value"/>
|
||||
</div>
|
||||
<svg ref="svgRef"/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref, onMounted, onUpdated} from 'vue';
|
||||
import {Markmap} from 'markmap-view';
|
||||
import {loadJS, loadCSS} from 'markmap-common';
|
||||
import {Transformer} from 'markmap-lib';
|
||||
|
||||
import {ref} from "vue";
|
||||
const transformer = new Transformer();
|
||||
const {scripts, styles} = transformer.getAssets();
|
||||
loadCSS(styles);
|
||||
loadJS(scripts);
|
||||
|
||||
const title = ref('Test Page')
|
||||
const initValue = `# markmap
|
||||
|
||||
- beautiful
|
||||
- useful
|
||||
- easy
|
||||
- interactive
|
||||
`;
|
||||
|
||||
const value = ref(initValue);
|
||||
const svgRef = ref(null);
|
||||
let mm;
|
||||
|
||||
const update = () => {
|
||||
const {root} = transformer.transform(value.value);
|
||||
mm.setData(root);
|
||||
mm.fit();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
mm = Markmap.create(svgRef.value);
|
||||
update();
|
||||
});
|
||||
|
||||
onUpdated(update);
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
</style>
|
@ -125,8 +125,8 @@
|
||||
import {onMounted, onUnmounted, reactive, ref} from "vue";
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {dateFormat, disabledDate, removeArrayItem, substr} from "@/utils/libs";
|
||||
import {DocumentCopy, InfoFilled, Plus, ShoppingCart} from "@element-plus/icons-vue";
|
||||
import {dateFormat, removeArrayItem, substr} from "@/utils/libs";
|
||||
import {DocumentCopy, Plus, ShoppingCart} from "@element-plus/icons-vue";
|
||||
import ClipboardJS from "clipboard";
|
||||
|
||||
// 变量定义
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="container list" v-loading="loading">
|
||||
<div class="container model-list" v-loading="loading">
|
||||
|
||||
<div class="handle-box">
|
||||
<el-button type="primary" :icon="Plus" @click="add">新增</el-button>
|
||||
@ -13,7 +13,14 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="模型名称"/>
|
||||
<el-table-column prop="value" label="模型值"/>
|
||||
<el-table-column prop="value" label="模型值">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.value }}</span>
|
||||
<el-icon class="copy-model" :data-clipboard-text="scope.row.value">
|
||||
<DocumentCopy/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="power" label="费率"/>
|
||||
<el-table-column prop="max_tokens" label="最大响应长度"/>
|
||||
<el-table-column prop="max_context" label="最大上下文长度"/>
|
||||
@ -29,12 +36,12 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="创建时间">
|
||||
<template #default="scope">
|
||||
<span>{{ dateFormat(scope.row['created_at']) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- <el-table-column label="创建时间">-->
|
||||
<!-- <template #default="scope">-->
|
||||
<!-- <span>{{ dateFormat(scope.row['created_at']) }}</span>-->
|
||||
<!-- </template>-->
|
||||
<!-- </el-table-column>-->
|
||||
<el-table-column prop="key_name" label="绑定API-KEY"/>
|
||||
<el-table-column label="操作" width="180">
|
||||
<template #default="scope">
|
||||
<el-button size="small" type="primary" @click="edit(scope.row)">编辑</el-button>
|
||||
@ -75,7 +82,7 @@
|
||||
<el-form-item label="费率:" prop="weight">
|
||||
<template #default>
|
||||
<div class="tip-input">
|
||||
<el-input-number :min="1" v-model="item.power" autocomplete="off"/>
|
||||
<el-input-number :min="0" v-model="item.power" autocomplete="off"/>
|
||||
<div class="info">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
@ -144,6 +151,15 @@
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="绑定API-KEY:" prop="apikey">
|
||||
<el-select v-model="item.key_id" placeholder="请选择 API KEY" clearable>
|
||||
<el-option v-for="v in apiKeys" :value="v.id" :label="v.name" :key="v.id">
|
||||
{{ v.name }}
|
||||
<el-text type="info" size="small">{{ substr(v.api_url, 50) }}</el-text>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="启用状态:" prop="enable">
|
||||
<el-switch v-model="item.enabled"/>
|
||||
</el-form-item>
|
||||
@ -178,12 +194,13 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, reactive, ref} from "vue";
|
||||
import {onMounted, onUnmounted, reactive, ref} from "vue";
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {dateFormat, removeArrayItem} from "@/utils/libs";
|
||||
import {InfoFilled, Plus} from "@element-plus/icons-vue";
|
||||
import {dateFormat, removeArrayItem, substr} from "@/utils/libs";
|
||||
import {DocumentCopy, InfoFilled, Plus} from "@element-plus/icons-vue";
|
||||
import {Sortable} from "sortablejs";
|
||||
import ClipboardJS from "clipboard";
|
||||
|
||||
// 变量定义
|
||||
const items = ref([])
|
||||
@ -207,23 +224,34 @@ const platforms = ref([
|
||||
|
||||
])
|
||||
|
||||
// 获取数据
|
||||
httpGet('/api/admin/model/list').then((res) => {
|
||||
if (res.data) {
|
||||
// 初始化数据
|
||||
const arr = res.data;
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
arr[i].last_used_at = dateFormat(arr[i].last_used_at)
|
||||
}
|
||||
items.value = arr
|
||||
}
|
||||
loading.value = false
|
||||
}).catch(() => {
|
||||
ElMessage.error("获取数据失败");
|
||||
// 获取 API KEY
|
||||
const apiKeys = ref([])
|
||||
httpGet('/api/admin/apikey/list?status=true&type=chat').then(res => {
|
||||
apiKeys.value = res.data
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取 API KEY 失败:" + e.message)
|
||||
})
|
||||
|
||||
// 获取数据
|
||||
const fetchData = () => {
|
||||
httpGet('/api/admin/model/list').then((res) => {
|
||||
if (res.data) {
|
||||
// 初始化数据
|
||||
const arr = res.data;
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
arr[i].last_used_at = dateFormat(arr[i].last_used_at)
|
||||
}
|
||||
items.value = arr
|
||||
}
|
||||
loading.value = false
|
||||
}).catch(() => {
|
||||
ElMessage.error("获取数据失败");
|
||||
})
|
||||
}
|
||||
|
||||
const clipboard = ref(null)
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
const drawBodyWrapper = document.querySelector('.el-table__body tbody')
|
||||
|
||||
// 初始化拖动排序插件
|
||||
@ -250,6 +278,19 @@ onMounted(() => {
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
clipboard.value = new ClipboardJS('.copy-model');
|
||||
clipboard.value.on('success', () => {
|
||||
ElMessage.success('复制成功!');
|
||||
})
|
||||
|
||||
clipboard.value.on('error', () => {
|
||||
ElMessage.error('复制失败!');
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clipboard.value.destroy()
|
||||
})
|
||||
|
||||
const add = function () {
|
||||
@ -267,14 +308,15 @@ const edit = function (row) {
|
||||
const save = function () {
|
||||
formRef.value.validate((valid) => {
|
||||
item.value.temperature = parseFloat(item.value.temperature)
|
||||
if (!item.value.sort_num) {
|
||||
item.value.sort_num = items.value.length
|
||||
}
|
||||
if (valid) {
|
||||
showDialog.value = false
|
||||
httpPost('/api/admin/model/save', item.value).then((res) => {
|
||||
item.value.key_id = parseInt(item.value.key_id)
|
||||
httpPost('/api/admin/model/save', item.value).then(() => {
|
||||
ElMessage.success('操作成功!')
|
||||
if (!item.value['id']) {
|
||||
const newItem = res.data
|
||||
items.value.push(newItem)
|
||||
}
|
||||
fetchData()
|
||||
}).catch((e) => {
|
||||
ElMessage.error('操作失败,' + e.message)
|
||||
})
|
||||
@ -306,7 +348,7 @@ const remove = function (row) {
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import "@/assets/css/admin/form.styl";
|
||||
.list {
|
||||
.model-list {
|
||||
|
||||
.opt-box {
|
||||
padding-bottom: 10px;
|
||||
@ -318,6 +360,13 @@ const remove = function (row) {
|
||||
}
|
||||
}
|
||||
|
||||
.cell {
|
||||
.copy-model {
|
||||
margin-left 6px
|
||||
cursor pointer
|
||||
}
|
||||
}
|
||||
|
||||
.el-select {
|
||||
width: 100%
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ import {setAdminToken} from "@/store/session";
|
||||
import {checkAdminSession} from "@/action/session";
|
||||
|
||||
const router = useRouter();
|
||||
const title = ref('Geek-AI 控制台登录');
|
||||
const title = ref('ChatGPT Plus Admin');
|
||||
const username = ref(process.env.VUE_APP_ADMIN_USER);
|
||||
const password = ref(process.env.VUE_APP_ADMIN_PASS);
|
||||
|
||||
|
@ -21,6 +21,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="角色标识" prop="key"/>
|
||||
<el-table-column label="绑定模型" prop="model_name"/>
|
||||
<el-table-column label="启用状态">
|
||||
<template #default="scope">
|
||||
<el-switch v-model="scope.row['enable']" @change="roleSet('enable',scope.row)"/>
|
||||
@ -47,7 +48,7 @@
|
||||
|
||||
<el-dialog
|
||||
v-model="showDialog"
|
||||
title="编辑角色"
|
||||
:title="optTitle"
|
||||
:close-on-click-modal="false"
|
||||
width="50%"
|
||||
>
|
||||
@ -67,10 +68,33 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="角色图标:" prop="icon">
|
||||
<el-input
|
||||
v-model="role.icon"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<el-input v-model="role.icon">
|
||||
<template #append>
|
||||
<el-upload
|
||||
:auto-upload="true"
|
||||
:show-file-list="false"
|
||||
:http-request="uploadImg"
|
||||
>
|
||||
上传
|
||||
</el-upload>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="绑定模型:" prop="model_id">
|
||||
<el-select
|
||||
v-model="role.model_id"
|
||||
filterable
|
||||
placeholder="请选择模型"
|
||||
clearable
|
||||
>
|
||||
<el-option
|
||||
v-for="item in models"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="打招呼信息:" prop="hello_msg">
|
||||
@ -143,6 +167,7 @@ import {httpGet, httpPost} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {copyObj, removeArrayItem} from "@/utils/libs";
|
||||
import {Sortable} from "sortablejs"
|
||||
import Compressor from "compressorjs";
|
||||
|
||||
const showDialog = ref(false)
|
||||
const parentBorder = ref(true)
|
||||
@ -151,7 +176,7 @@ const tableData = ref([])
|
||||
const sortedTableData = ref([])
|
||||
const role = ref({context: []})
|
||||
const formRef = ref(null)
|
||||
const editRow = ref({})
|
||||
const optTitle = ref({})
|
||||
const loading = ref(true)
|
||||
|
||||
const rules = reactive({
|
||||
@ -165,18 +190,30 @@ const rules = reactive({
|
||||
hello_msg: [{required: true, message: '请输入打招呼信息', trigger: 'change',}]
|
||||
})
|
||||
|
||||
// 获取角色列表
|
||||
httpGet('/api/admin/role/list').then((res) => {
|
||||
tableData.value = res.data
|
||||
sortedTableData.value = copyObj(tableData.value)
|
||||
loading.value = false
|
||||
}).catch(() => {
|
||||
ElMessage.error("获取聊天角色失败");
|
||||
const models = ref([])
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
|
||||
// get chat models
|
||||
httpGet('/api/admin/model/list?enable=1').then((res) => {
|
||||
models.value = res.data
|
||||
}).catch(() => {
|
||||
ElMessage.error("获取AI模型数据失败");
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
const drawBodyWrapper = document.querySelector('.el-table__body tbody')
|
||||
const fetchData = () => {
|
||||
// 获取角色列表
|
||||
httpGet('/api/admin/role/list').then((res) => {
|
||||
tableData.value = res.data
|
||||
sortedTableData.value = copyObj(tableData.value)
|
||||
loading.value = false
|
||||
}).catch(() => {
|
||||
ElMessage.error("获取聊天角色失败");
|
||||
})
|
||||
|
||||
const drawBodyWrapper = document.querySelector('.el-table__body tbody')
|
||||
// 初始化拖动排序插件
|
||||
Sortable.create(drawBodyWrapper, {
|
||||
sort: true,
|
||||
@ -199,7 +236,7 @@ onMounted(() => {
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const roleSet = (filed, row) => {
|
||||
httpPost('/api/admin/role/set', {id: row.id, filed: filed, value: row[filed]}).then(() => {
|
||||
@ -212,12 +249,14 @@ const roleSet = (filed, row) => {
|
||||
// 编辑
|
||||
const curIndex = ref(0)
|
||||
const rowEdit = function (index, row) {
|
||||
optTitle.value = "修改角色"
|
||||
curIndex.value = index
|
||||
role.value = copyObj(row)
|
||||
showDialog.value = true
|
||||
}
|
||||
|
||||
const addRole = function () {
|
||||
optTitle.value = "添加新角色"
|
||||
role.value = {context: []}
|
||||
showDialog.value = true
|
||||
}
|
||||
@ -226,14 +265,9 @@ const save = function () {
|
||||
formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
showDialog.value = false
|
||||
httpPost('/api/admin/role/save', role.value).then((res) => {
|
||||
httpPost('/api/admin/role/save', role.value).then(() => {
|
||||
ElMessage.success('操作成功')
|
||||
// 更新当前数据行
|
||||
if (role.value.id) {
|
||||
tableData.value[curIndex.value] = role.value
|
||||
} else {
|
||||
tableData.value.push(res.data)
|
||||
}
|
||||
fetchData()
|
||||
}).catch((e) => {
|
||||
ElMessage.error('操作失败,' + e.message)
|
||||
})
|
||||
@ -263,6 +297,27 @@ const removeContext = function (index) {
|
||||
role.value.context.splice(index, 1);
|
||||
}
|
||||
|
||||
// 图片上传
|
||||
const uploadImg = (file) => {
|
||||
// 压缩图片并上传
|
||||
new Compressor(file.file, {
|
||||
quality: 0.6,
|
||||
success(result) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', result, result.name);
|
||||
// 执行上传操作
|
||||
httpPost('/api/admin/upload', formData).then((res) => {
|
||||
role.value.icon = res.data.url
|
||||
ElMessage.success('上传成功')
|
||||
}).catch((e) => {
|
||||
ElMessage.error('上传失败:' + e.message)
|
||||
})
|
||||
},
|
||||
error(e) {
|
||||
ElMessage.error('上传失败:' + e.message)
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
|
@ -257,6 +257,20 @@
|
||||
<el-tab-pane label="菜单配置" name="menu">
|
||||
<Menu/>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="授权激活" name="license">
|
||||
<div class="container">
|
||||
<el-form :model="system" label-width="150px" label-position="right">
|
||||
<el-form-item label="许可授权码" prop="license">
|
||||
<el-input v-model="license"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="active">立即激活</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
@ -275,8 +289,8 @@ const activeName = ref('basic')
|
||||
const system = ref({models: []})
|
||||
const loading = ref(true)
|
||||
const systemFormRef = ref(null)
|
||||
const chatFormRef = ref(null)
|
||||
const models = ref([])
|
||||
const openAIModels = ref([])
|
||||
const notice = ref("")
|
||||
|
||||
onMounted(() => {
|
||||
@ -295,6 +309,7 @@ onMounted(() => {
|
||||
|
||||
httpGet('/api/admin/model/list').then(res => {
|
||||
models.value = res.data
|
||||
openAIModels.value = models.value.filter(v => v.platform === "OpenAI")
|
||||
loading.value = false
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取模型失败:" + e.message)
|
||||
@ -320,19 +335,6 @@ const save = function (key) {
|
||||
})
|
||||
}
|
||||
})
|
||||
} else if (key === 'chat') {
|
||||
if (chat.value.context_deep % 2 !== 0) {
|
||||
return ElMessage.error("会话上下文深度必须为偶数!")
|
||||
}
|
||||
chatFormRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
httpPost('/api/admin/config/update', {key: key, config: chat.value}).then(() => {
|
||||
ElMessage.success("操作成功!")
|
||||
}).catch(e => {
|
||||
ElMessage.error("操作失败:" + e.message)
|
||||
})
|
||||
}
|
||||
})
|
||||
} else if (key === 'notice') {
|
||||
httpPost('/api/admin/config/update', {key: key, config: {content: notice.value, updated: true}}).then(() => {
|
||||
ElMessage.success("操作成功!")
|
||||
@ -342,6 +344,19 @@ const save = function (key) {
|
||||
}
|
||||
}
|
||||
|
||||
// 激活授权
|
||||
const license = ref("")
|
||||
const active = () => {
|
||||
if (license.value === "") {
|
||||
return ElMessage.error("请输入授权码")
|
||||
}
|
||||
httpPost("/api/admin/active", {license: license.value}).then(res => {
|
||||
ElMessage.success("授权成功,机器编码为:" + res.data)
|
||||
}).catch(e => {
|
||||
ElMessage.error(e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const configKey = ref("")
|
||||
const beforeUpload = (key) => {
|
||||
configKey.value = key
|
||||
|
@ -19,7 +19,7 @@ import {ref} from "vue";
|
||||
import ImageMj from "@/views/mobile/ImageMj.vue";
|
||||
import ImageSd from "@/views/mobile/ImageSd.vue";
|
||||
|
||||
const activeName = ref("sd")
|
||||
const activeName = ref("mj")
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
|