merge v4.2.2

This commit is contained in:
RockYang 2025-12-02 11:18:30 +08:00
commit c84754fc0c
69 changed files with 19149 additions and 2793 deletions

View File

@ -1,5 +1,16 @@
# 更新日志
## v4.2.2
- 功能优化:开启图形验证码功能的时候现检查是否配置了 API 服务,防止开启之后没法登录的 Bug。
- 功能优化:支持原生的 DeepSeek 推理模型 API聊天 API KEY 支持设置完整的 API 路径,比如 https://api.geekai.pro/v1/chat/completions
- 功能优化:支持 GPT-4o 图片编辑功能。
- 功能新增:对话页面支持 AI 输出语音播报TTS
- 功能优化:替换瀑布流组件,优化用户体验。
- 功能优化:生成思维导图时候自动缓存上一次的结果。
- 功能优化:优化 MJ 绘图页面,增加 MJ-V7 模型支持。
- 功能优化:后台管理增加生成一键登录链接地址功能
## v4.2.1
- 功能新增:新增支持可灵生成视频,支持文生视频,图生生视频。

View File

@ -73,6 +73,23 @@ func (s *AppServer) Run(db *gorm.DB) error {
if err != nil {
return fmt.Errorf("failed to decode system config: %v", err)
}
// 统计安装信息
go func() {
info, err := host.Info()
if err == nil {
apiURL := fmt.Sprintf("%s/%s", s.Config.ApiConfig.ApiURL, "api/installs/push")
timestamp := time.Now().Unix()
product := "geekai-plus"
signStr := fmt.Sprintf("%s#%s#%d", product, info.HostID, timestamp)
sign := utils.Sha256(signStr)
resp, err := req.C().R().SetBody(map[string]interface{}{"product": product, "device_id": info.HostID, "timestamp": timestamp, "sign": sign}).Post(apiURL)
if err != nil {
logger.Errorf("register install info failed: %v", err)
} else {
logger.Debugf("register install info success: %v", resp.String())
}
}
}()
logger.Infof("http://%s", s.Config.Listen)
// 统计安装信息
@ -115,19 +132,23 @@ func corsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
method := c.Request.Method
origin := c.Request.Header.Get("Origin")
// 设置允许的请求源
if origin != "" {
// 设置允许的请求源
c.Header("Access-Control-Allow-Origin", origin)
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
//允许跨域设置可以返回其他子段,可以自定义字段
c.Header("Access-Control-Allow-Headers", "Authorization, Body-Length, Body-Type, Admin-Authorization,content-type")
// 允许浏览器(客户端)可以解析的头部 (重要)
c.Header("Access-Control-Expose-Headers", "Body-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers")
//设置缓存时间
c.Header("Access-Control-Max-Age", "172800")
//允许客户端传递校验信息比如 cookie (重要)
c.Header("Access-Control-Allow-Credentials", "true")
} else {
c.Header("Access-Control-Allow-Origin", "*")
}
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
//允许跨域设置可以返回其他子段,可以自定义字段
c.Header("Access-Control-Allow-Headers", "Authorization, Body-Length, Body-Type, Admin-Authorization,content-type")
// 允许浏览器(客户端)可以解析的头部 (重要)
c.Header("Access-Control-Expose-Headers", "Body-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers")
//设置缓存时间
c.Header("Access-Control-Max-Age", "172800")
//允许客户端传递校验信息比如 cookie (重要)
c.Header("Access-Control-Allow-Credentials", "true")
if method == http.MethodOptions {
c.JSON(http.StatusOK, "ok!")

View File

@ -9,20 +9,20 @@ package types
// ApiRequest API 请求实体
type ApiRequest struct {
Model string `json:"model,omitempty"`
Temperature float32 `json:"temperature"`
MaxTokens int `json:"max_tokens,omitempty"`
MaxCompletionTokens int `json:"max_completion_tokens,omitempty"` // 兼容GPT O1 模型
Stream bool `json:"stream,omitempty"`
Messages []interface{} `json:"messages,omitempty"`
Tools []Tool `json:"tools,omitempty"`
Functions []interface{} `json:"functions,omitempty"` // 兼容中转平台
ResponseFormat interface{} `json:"response_format,omitempty"` // 响应格式
Model string `json:"model,omitempty"`
Temperature float32 `json:"temperature"`
MaxTokens int `json:"max_tokens,omitempty"`
MaxCompletionTokens int `json:"max_completion_tokens,omitempty"` // 兼容GPT O1 模型
Stream bool `json:"stream,omitempty"`
Messages []any `json:"messages,omitempty"`
Tools []Tool `json:"tools,omitempty"`
Functions []any `json:"functions,omitempty"` // 兼容中转平台
ResponseFormat any `json:"response_format,omitempty"` // 响应格式
ToolChoice string `json:"tool_choice,omitempty"`
Input map[string]interface{} `json:"input,omitempty"` //兼容阿里通义千问
Parameters map[string]interface{} `json:"parameters,omitempty"` //兼容阿里通义千问
Input map[string]any `json:"input,omitempty"` //兼容阿里通义千问
Parameters map[string]any `json:"parameters,omitempty"` //兼容阿里通义千问
}
type Message struct {
@ -41,11 +41,12 @@ type ChoiceItem struct {
}
type Delta struct {
Role string `json:"role"`
Name string `json:"name"`
Content interface{} `json:"content"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
FunctionCall struct {
Role string `json:"role"`
Name string `json:"name"`
Content any `json:"content"`
ReasoningContent string `json:"reasoning_content,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
FunctionCall struct {
Name string `json:"name,omitempty"`
Arguments string `json:"arguments,omitempty"`
} `json:"function_call,omitempty"`

View File

@ -16,7 +16,7 @@ type MKey interface {
string | int | uint
}
type MValue interface {
*WsClient | *ChatSession | context.CancelFunc | []interface{}
*WsClient | *ChatSession | context.CancelFunc | []any
}
type LMap[K MKey, T MValue] struct {
lock sync.RWMutex

View File

@ -27,8 +27,10 @@ require github.com/xxl-job/xxl-job-executor-go v1.2.0
require (
github.com/go-pay/gopay v1.5.101
github.com/go-rod/rod v0.116.2
github.com/google/go-tika v0.3.1
github.com/microcosm-cc/bluemonday v1.0.26
github.com/sashabaranov/go-openai v1.38.1
github.com/shirou/gopsutil v3.21.11+incompatible
github.com/shopspring/decimal v1.3.1
github.com/syndtr/goleveldb v1.0.0
@ -45,13 +47,13 @@ require (
github.com/go-pay/xtime v0.0.2 // indirect
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/gravityblast/fresh v0.0.0-20240621171608-8d1fef547a99 // indirect
github.com/howeyc/fsnotify v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/pilu/config v0.0.0-20131214182432-3eb99e6c0b9a // indirect
github.com/pilu/fresh v0.0.0-20240621171608-8d1fef547a99 // indirect
github.com/tklauser/go-sysconf v0.3.13 // indirect
github.com/tklauser/numcpus v0.7.0 // indirect
github.com/ysmood/fetchup v0.3.0 // indirect
github.com/ysmood/goob v0.4.0 // indirect
github.com/ysmood/got v0.40.0 // indirect
github.com/ysmood/gson v0.7.3 // indirect
github.com/ysmood/leakless v0.9.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.uber.org/mock v0.4.0 // indirect
)

View File

@ -73,6 +73,8 @@ github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA=
github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
@ -100,15 +102,11 @@ github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gravityblast/fresh v0.0.0-20240621171608-8d1fef547a99 h1:A6qlLfihaWef15viqtecCz4XknZcgjgD7mEuhu7bHEc=
github.com/gravityblast/fresh v0.0.0-20240621171608-8d1fef547a99/go.mod h1:ukFDwXV66bGV7JnfyxFKuKiVp4zH4orBKXML+VCSrhI=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/howeyc/fsnotify v0.9.0 h1:0gtV5JmOKH4A8SsFxG2BczSeXWWPvcMT0euZt5gDAxY=
github.com/howeyc/fsnotify v0.9.0/go.mod h1:41HzSPxBGeFRQKEEwgh49TRw/nKBsYZ2cF1OzPjSJsA=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/imroc/req/v3 v3.37.2 h1:vEemuA0cq9zJ6lhe+mSRhsZm951bT0CdiSH47+KTn6I=
github.com/imroc/req/v3 v3.37.2/go.mod h1:DECzjVIrj6jcUr5n6e+z0ygmCO93rx4Jy0RjOEe1YCI=
@ -141,9 +139,6 @@ github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0 h1:LgmjED/yQILqmUED4GaXjrINWe7YJh4HM6z2EvEINPs=
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0/go.mod h1:C5LA5UO2ZXJrLaPLYtE1wUJMiyd/nwWaCO5cw/2pSHs=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
@ -177,10 +172,6 @@ github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:Ff
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/pilu/config v0.0.0-20131214182432-3eb99e6c0b9a h1:Tg4E4cXPZSZyd3H1tJlYo6ZreXV0ZJvE/lorNqyw1AU=
github.com/pilu/config v0.0.0-20131214182432-3eb99e6c0b9a/go.mod h1:9Or9aIl95Kp43zONcHd5tLZGKXb9iLx0pZjau0uJ5zg=
github.com/pilu/fresh v0.0.0-20240621171608-8d1fef547a99 h1:+X7Gb40b5Bl3v5+3MiGK8Jhemjp65MHc+nkVCfq1Yfc=
github.com/pilu/fresh v0.0.0-20240621171608-8d1fef547a99/go.mod h1:2LLTtftTZSdAPR/iVyennXZDLZOYzyDn+T0qEKJ8eSw=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -203,6 +194,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/sashabaranov/go-openai v1.38.1 h1:TtZabbFQZa1nEni/IhVtDF/WQjVqDgd+cWR5OeddzF8=
github.com/sashabaranov/go-openai v1.38.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
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=
@ -239,6 +232,20 @@ github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4d
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/xxl-job/xxl-job-executor-go v1.2.0 h1:MTl2DpwrK2+hNjRRks2k7vB3oy+3onqm9OaSarneeLQ=
github.com/xxl-job/xxl-job-executor-go v1.2.0/go.mod h1:bUFhz/5Irp9zkdYk5MxhQcDDT6LlZrI8+rv5mHtQ1mo=
github.com/ysmood/fetchup v0.3.0 h1:UhYz9xnLEVn2ukSuK3KCgcznWpHMdrmbsPpllcylyu8=
github.com/ysmood/fetchup v0.3.0/go.mod h1:hbysoq65PXL0NQeNzUczNYIKpwpkwFL4LXMDEvIQq9A=
github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ=
github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18=
github.com/ysmood/gop v0.2.0 h1:+tFrG0TWPxT6p9ZaZs+VY+opCvHU8/3Fk6BaNv6kqKg=
github.com/ysmood/gop v0.2.0/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk=
github.com/ysmood/got v0.40.0 h1:ZQk1B55zIvS7zflRrkGfPDrPG3d7+JOza1ZkNxcc74Q=
github.com/ysmood/got v0.40.0/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg=
github.com/ysmood/gotrace v0.6.0 h1:SyI1d4jclswLhg7SWTL6os3L1WOKeNn/ZtzVQF8QmdY=
github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM=
github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE=
github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg=
github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU=
github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ=
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=
@ -302,7 +309,6 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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=

View File

@ -30,20 +30,21 @@ func NewChatModelHandler(app *core.AppServer, db *gorm.DB) *ChatModelHandler {
func (h *ChatModelHandler) Save(c *gin.Context) {
var data struct {
Id uint `json:"id"`
Name string `json:"name"`
Value string `json:"value"`
Enabled bool `json:"enabled"`
SortNum int `json:"sort_num"`
Open bool `json:"open"`
Platform string `json:"platform"`
Power int `json:"power"`
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"`
Type string `json:"type"`
Id uint `json:"id"`
Name string `json:"name"`
Value string `json:"value"`
Enabled bool `json:"enabled"`
SortNum int `json:"sort_num"`
Open bool `json:"open"`
Platform string `json:"platform"`
Power int `json:"power"`
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"`
Type string `json:"type"`
Options map[string]string `json:"options"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
@ -59,7 +60,6 @@ func (h *ChatModelHandler) Save(c *gin.Context) {
item.Name = data.Name
item.Value = data.Value
item.Enabled = data.Enabled
item.SortNum = data.SortNum
item.Open = data.Open
item.Power = data.Power
item.MaxTokens = data.MaxTokens
@ -67,6 +67,7 @@ func (h *ChatModelHandler) Save(c *gin.Context) {
item.Temperature = data.Temperature
item.KeyId = data.KeyId
item.Type = data.Type
item.Options = utils.JsonEncode(data.Options)
var res *gorm.DB
if data.Id > 0 {
res = h.DB.Save(&item)

View File

@ -59,6 +59,12 @@ func (h *ConfigHandler) Update(c *gin.Context) {
return
}
// 如果要启用图形验证码功能,则检查是否配置了 API 服务
if data.Config.EnabledVerify && h.App.Config.ApiConfig.AppId == "" {
resp.ERROR(c, "启用验证码服务需要先配置 GeekAI 官方 API 服务 AppId 和 Token")
return
}
value := utils.JsonEncode(&data.Config)
config := model.Config{Key: data.Key, Config: value}
res := h.DB.FirstOrCreate(&config, model.Config{Key: data.Key})

View File

@ -20,6 +20,7 @@ import (
"time"
"github.com/go-redis/redis/v8"
"github.com/golang-jwt/jwt/v5"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
@ -320,3 +321,36 @@ func (h *UserHandler) LoginLog(c *gin.Context) {
resp.SUCCESS(c, vo.NewPage(total, page, pageSize, logs))
}
// GenLoginLink 生成登录链接
func (h *UserHandler) GenLoginLink(c *gin.Context) {
id := c.Query("id")
if id == "" {
resp.ERROR(c, types.InvalidArgs)
return
}
var user model.User
if err := h.DB.Where("id = ?", id).First(&user).Error; err != nil {
resp.ERROR(c, "用户不存在")
return
}
// 创建 token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": user.Id,
"expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)).Unix(),
})
tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
if err != nil {
resp.ERROR(c, "Failed to generate token, "+err.Error())
return
}
// 保存到 redis
sessionKey := fmt.Sprintf("users/%d", user.Id)
if _, err = h.redis.Set(c, sessionKey, tokenString, 0).Result(); err != nil {
resp.ERROR(c, "error with save token: "+err.Error())
return
}
resp.SUCCESS(c, tokenString)
}

View File

@ -22,15 +22,17 @@ import (
"geekai/utils"
"geekai/utils/resp"
"html/template"
"io"
"net/http"
"net/url"
"regexp"
"os"
"strings"
"time"
"unicode/utf8"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
"github.com/sashabaranov/go-openai"
"gorm.io/gorm"
)
@ -40,7 +42,7 @@ type ChatHandler struct {
uploadManager *oss.UploaderManager
licenseService *service.LicenseService
ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function
ChatContexts *types.LMap[string, []interface{}] // 聊天上下文 Map [chatId] => []Message
ChatContexts *types.LMap[string, []any] // 聊天上下文 Map [chatId] => []Message
userService *service.UserService
}
@ -51,7 +53,7 @@ func NewChatHandler(app *core.AppServer, db *gorm.DB, redis *redis.Client, manag
uploadManager: manager,
licenseService: licenseService,
ReqCancelFunc: types.NewLMap[string, context.CancelFunc](),
ChatContexts: types.NewLMap[string, []interface{}](),
ChatContexts: types.NewLMap[string, []any](),
userService: userService,
}
}
@ -348,8 +350,14 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, sessi
return nil, err
}
logger.Debugf("对话请求消息体:%+v", req)
apiURL := fmt.Sprintf("%s/v1/chat/completions", apiKey.ApiURL)
var apiURL string
p, _ := url.Parse(apiKey.ApiURL)
// 如果设置的是 BASE_URL 没有路径,则添加 /v1/chat/completions
if p.Path == "" {
apiURL = fmt.Sprintf("%s/v1/chat/completions", apiKey.ApiURL)
} else {
apiURL = apiKey.ApiURL
}
// 创建 HttpClient 请求对象
var client *http.Client
requestBody, err := json.Marshal(req)
@ -495,28 +503,93 @@ func (h *ChatHandler) saveChatHistory(
}
}
// 将AI回复消息中生成的图片链接下载到本地
func (h *ChatHandler) extractImgUrl(text string) string {
pattern := `!\[([^\]]*)]\(([^)]+)\)`
re := regexp.MustCompile(pattern)
matches := re.FindAllStringSubmatch(text, -1)
// 下载图片并替换链接地址
for _, match := range matches {
imageURL := match[2]
logger.Debug(imageURL)
// 对于相同地址的图片,已经被替换了,就不再重复下载了
if !strings.Contains(text, imageURL) {
continue
}
newImgURL, err := h.uploadManager.GetUploadHandler().PutUrlFile(imageURL, false)
if err != nil {
logger.Error("error with download image: ", err)
continue
}
text = strings.ReplaceAll(text, imageURL, newImgURL)
// 文本生成语音
func (h *ChatHandler) TextToSpeech(c *gin.Context) {
var data struct {
ModelId int `json:"model_id"`
Text string `json:"text"`
}
return text
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
textHash := utils.Sha256(fmt.Sprintf("%d/%s", data.ModelId, data.Text))
audioFile := fmt.Sprintf("%s/audio", h.App.Config.StaticDir)
if _, err := os.Stat(audioFile); err != nil {
os.MkdirAll(audioFile, 0755)
}
audioFile = fmt.Sprintf("%s/%s.mp3", audioFile, textHash)
if _, err := os.Stat(audioFile); err == nil {
// 设置响应头
c.Header("Content-Type", "audio/mpeg")
c.Header("Content-Disposition", "attachment; filename=speech.mp3")
c.File(audioFile)
return
}
// 查询模型
var chatModel model.ChatModel
err := h.DB.Where("id", data.ModelId).First(&chatModel).Error
if err != nil {
resp.ERROR(c, "找不到语音模型")
return
}
// 调用 DeepSeek 的 API 接口
var apiKey model.ApiKey
if chatModel.KeyId > 0 {
h.DB.Where("id", chatModel.KeyId).First(&apiKey)
}
if apiKey.Id == 0 {
h.DB.Where("type", "tts").Where("enabled", true).First(&apiKey)
}
if apiKey.Id == 0 {
resp.ERROR(c, "no TTS API key, please import key")
return
}
logger.Debugf("chatModel: %+v, apiKey: %+v", chatModel, apiKey)
// 调用 openai tts api
config := openai.DefaultConfig(apiKey.Value)
config.BaseURL = apiKey.ApiURL + "/v1"
client := openai.NewClientWithConfig(config)
voice := openai.VoiceAlloy
var options map[string]string
err = utils.JsonDecode(chatModel.Options, &options)
if err == nil {
voice = openai.SpeechVoice(options["voice"])
}
req := openai.CreateSpeechRequest{
Model: openai.SpeechModel(chatModel.Value),
Input: data.Text,
Voice: voice,
}
audioData, err := client.CreateSpeech(context.Background(), req)
if err != nil {
resp.ERROR(c, err.Error())
return
}
// 先将音频数据读取到内存
audioBytes, err := io.ReadAll(audioData)
if err != nil {
resp.ERROR(c, err.Error())
return
}
// 保存到音频文件
err = os.WriteFile(audioFile, audioBytes, 0644)
if err != nil {
logger.Error("failed to save audio file: ", err)
}
// 设置响应头
c.Header("Content-Type", "audio/mpeg")
c.Header("Content-Disposition", "attachment; filename=speech.mp3")
// 直接写入完整的音频数据到响应
c.Writer.Write(audioBytes)
}

View File

@ -30,14 +30,16 @@ func NewChatModelHandler(app *core.AppServer, db *gorm.DB) *ChatModelHandler {
func (h *ChatModelHandler) List(c *gin.Context) {
var items []model.ChatModel
var chatModels = make([]vo.ChatModel, 0)
session := h.DB.Session(&gorm.Session{}).Where("type", "chat").Where("enabled", true)
session := h.DB.Session(&gorm.Session{}).Where("enabled", true)
t := c.Query("type")
if t != "" {
session = session.Where("type", t)
} else {
session = session.Where("type", "chat")
}
session = session.Where("open", true)
if h.IsLogin(c) {
if h.IsLogin(c) && t == "chat" {
user, _ := h.GetLoginUser(c)
var models []int
err := utils.JsonDecode(user.ChatModels, &models)

View File

@ -89,6 +89,7 @@ func (h *ChatHandler) sendOpenAiMessage(
var function model.Function
var toolCall = false
var arguments = make([]string, 0)
var reasoning = false
scanner := bufio.NewScanner(response.Body)
for scanner.Scan() {
@ -104,7 +105,9 @@ func (h *ChatHandler) sendOpenAiMessage(
if len(responseBody.Choices) == 0 { // Fixed: 兼容 Azure API 第一个输出空行
continue
}
if responseBody.Choices[0].Delta.Content == nil && responseBody.Choices[0].Delta.ToolCalls == nil {
if responseBody.Choices[0].Delta.Content == nil &&
responseBody.Choices[0].Delta.ToolCalls == nil &&
responseBody.Choices[0].Delta.ReasoningContent == "" {
continue
}
@ -152,9 +155,25 @@ func (h *ChatHandler) sendOpenAiMessage(
if responseBody.Choices[0].FinishReason != "" {
break // 输出完成或者输出中断了
} else { // 正常输出结果
content := responseBody.Choices[0].Delta.Content
contents = append(contents, utils.InterfaceToString(content))
utils.SendChunkMsg(ws, content)
// 兼容思考过程
if responseBody.Choices[0].Delta.ReasoningContent != "" {
reasoningContent := responseBody.Choices[0].Delta.ReasoningContent
if !reasoning {
reasoningContent = fmt.Sprintf("<think>%s", reasoningContent)
reasoning = true
}
utils.SendChunkMsg(ws, reasoningContent)
contents = append(contents, reasoningContent)
} else if responseBody.Choices[0].Delta.Content != "" {
finalContent := responseBody.Choices[0].Delta.Content
if reasoning {
finalContent = fmt.Sprintf("</think>%s", responseBody.Choices[0].Delta.Content)
reasoning = false
}
contents = append(contents, utils.InterfaceToString(finalContent))
utils.SendChunkMsg(ws, finalContent)
}
}
} // end for
@ -167,7 +186,7 @@ func (h *ChatHandler) sendOpenAiMessage(
}
if toolCall { // 调用函数完成任务
params := make(map[string]interface{})
params := make(map[string]any)
_ = utils.JsonDecode(strings.Join(arguments, ""), &params)
logger.Debugf("函数名称: %s, 函数参数:%s", function.Name, params)
params["user_id"] = userVo.Id

View File

@ -13,6 +13,7 @@ import (
"geekai/core"
"geekai/core/types"
"geekai/service"
"geekai/service/crawler"
"geekai/service/dalle"
"geekai/service/oss"
"geekai/store/model"
@ -252,6 +253,76 @@ func (h *FunctionHandler) Dall3(c *gin.Context) {
resp.SUCCESS(c, content)
}
// 实现一个联网搜索的函数工具,采用爬虫实现
func (h *FunctionHandler) WebSearch(c *gin.Context) {
if err := h.checkAuth(c); err != nil {
resp.ERROR(c, err.Error())
return
}
var params map[string]interface{}
if err := c.ShouldBindJSON(&params); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
// 从参数中获取搜索关键词
keyword, ok := params["keyword"].(string)
if !ok || keyword == "" {
resp.ERROR(c, "搜索关键词不能为空")
return
}
// 从参数中获取最大页数默认为1页
maxPages := 1
if pages, ok := params["max_pages"].(float64); ok {
maxPages = int(pages)
}
// 获取用户ID
userID, ok := params["user_id"].(float64)
if !ok {
resp.ERROR(c, "用户ID不能为空")
return
}
// 查询用户信息
var user model.User
res := h.DB.Where("id = ?", int(userID)).First(&user)
if res.Error != nil {
resp.ERROR(c, "用户不存在")
return
}
// 检查用户算力是否足够
searchPower := 1 // 每次搜索消耗1点算力
if user.Power < searchPower {
resp.ERROR(c, "算力不足,无法执行网络搜索")
return
}
// 执行网络搜索
searchResults, err := crawler.SearchWeb(keyword, maxPages)
if err != nil {
resp.ERROR(c, fmt.Sprintf("搜索失败: %v", err))
return
}
// 扣减用户算力
err = h.userService.DecreasePower(int(user.Id), searchPower, model.PowerLog{
Type: types.PowerConsume,
Model: "web_search",
Remark: fmt.Sprintf("网络搜索:%s", utils.CutWords(keyword, 10)),
})
if err != nil {
resp.ERROR(c, "扣减算力失败:"+err.Error())
return
}
// 返回搜索结果
resp.SUCCESS(c, searchResults)
}
// List 获取所有的工具函数列表
func (h *FunctionHandler) List(c *gin.Context) {
var items []model.Function

View File

@ -256,6 +256,7 @@ func main() {
group.GET("clear", h.Clear)
group.POST("tokens", h.Tokens)
group.GET("stop", h.StopGenerate)
group.POST("tts", h.TextToSpeech)
}),
fx.Invoke(func(s *core.AppServer, h *handler.NetHandler) {
s.Engine.POST("/api/upload", h.Upload)
@ -335,6 +336,7 @@ func main() {
group.POST("save", h.Save)
group.GET("remove", h.Remove)
group.GET("loginLog", h.LoginLog)
group.GET("genLoginLink", h.GenLoginLink)
group.POST("resetPass", h.ResetPass)
}),
fx.Invoke(func(s *core.AppServer, h *admin.ChatAppHandler) {
@ -431,6 +433,7 @@ func main() {
group.POST("weibo", h.WeiBo)
group.POST("zaobao", h.ZaoBao)
group.POST("dalle3", h.Dall3)
group.POST("websearch", h.WebSearch)
group.GET("list", h.List)
}),
fx.Invoke(func(s *core.AppServer, h *admin.ChatHandler) {

View File

@ -0,0 +1,333 @@
package crawler
import (
"context"
"errors"
"fmt"
"geekai/logger"
"net/url"
"strings"
"time"
"github.com/go-rod/rod"
"github.com/go-rod/rod/lib/launcher"
"github.com/go-rod/rod/lib/proto"
)
// Service 网络爬虫服务
type Service struct {
browser *rod.Browser
}
// NewService 创建一个新的爬虫服务
func NewService() (*Service, error) {
// 启动浏览器
path, _ := launcher.LookPath()
u := launcher.New().Bin(path).
Headless(true). // 无头模式
Set("disable-web-security", ""). // 禁用网络安全限制
Set("disable-gpu", ""). // 禁用 GPU 加速
Set("no-sandbox", ""). // 禁用沙箱模式
Set("disable-setuid-sandbox", "").// 禁用 setuid 沙箱
MustLaunch()
browser := rod.New().ControlURL(u).MustConnect()
return &Service{
browser: browser,
}, nil
}
// SearchResult 搜索结果
type SearchResult struct {
Title string `json:"title"` // 标题
URL string `json:"url"` // 链接
Content string `json:"content"` // 内容摘要
}
// WebSearch 网络搜索
func (s *Service) WebSearch(keyword string, maxPages int) ([]SearchResult, error) {
if keyword == "" {
return nil, errors.New("搜索关键词不能为空")
}
if maxPages <= 0 {
maxPages = 1
}
if maxPages > 10 {
maxPages = 10 // 最多搜索 10 页
}
results := make([]SearchResult, 0)
// 使用百度搜索
searchURL := fmt.Sprintf("https://www.baidu.com/s?wd=%s", url.QueryEscape(keyword))
// 设置页面超时
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 创建页面
page := s.browser.MustPage()
defer page.MustClose()
// 设置视口大小
err := page.SetViewport(&proto.EmulationSetDeviceMetricsOverride{
Width: 1280,
Height: 800,
})
if err != nil {
return nil, fmt.Errorf("设置视口失败: %v", err)
}
// 导航到搜索页面
err = page.Context(ctx).Navigate(searchURL)
if err != nil {
return nil, fmt.Errorf("导航到搜索页面失败: %v", err)
}
// 等待搜索结果加载完成
err = page.WaitLoad()
if err != nil {
return nil, fmt.Errorf("等待页面加载完成失败: %v", err)
}
// 分析当前页面的搜索结果
for i := 0; i < maxPages; i++ {
if i > 0 {
// 点击下一页按钮
nextPage, err := page.Element("a.n")
if err != nil || nextPage == nil {
break // 没有下一页
}
err = nextPage.Click(proto.InputMouseButtonLeft, 1)
if err != nil {
break // 点击下一页失败
}
// 等待新页面加载
err = page.WaitLoad()
if err != nil {
break
}
}
// 提取搜索结果
resultElements, err := page.Elements(".result, .c-container")
if err != nil || resultElements == nil {
continue
}
for _, result := range resultElements {
// 获取标题
titleElement, err := result.Element("h3, .t")
if err != nil || titleElement == nil {
continue
}
title, err := titleElement.Text()
if err != nil {
continue
}
// 获取 URL
linkElement, err := titleElement.Element("a")
if err != nil || linkElement == nil {
continue
}
href, err := linkElement.Attribute("href")
if err != nil || href == nil {
continue
}
// 获取内容摘要 - 尝试多个可能的选择器
var contentElement *rod.Element
var content string
// 尝试多个可能的选择器来适应不同版本的百度搜索结果
selectors := []string{".content-right_8Zs40", ".c-abstract", ".content_LJ0WN", ".content"}
for _, selector := range selectors {
contentElement, err = result.Element(selector)
if err == nil && contentElement != nil {
content, _ = contentElement.Text()
if content != "" {
break
}
}
}
// 如果所有选择器都失败,尝试直接从结果块中提取文本
if content == "" {
// 获取结果元素的所有文本
fullText, err := result.Text()
if err == nil && fullText != "" {
// 简单处理:从全文中移除标题,剩下的可能是摘要
fullText = strings.Replace(fullText, title, "", 1)
// 清理文本
content = strings.TrimSpace(fullText)
// 限制内容长度
if len(content) > 200 {
content = content[:200] + "..."
}
}
}
// 添加到结果集
results = append(results, SearchResult{
Title: title,
URL: *href,
Content: content,
})
// 限制结果数量,每页最多 10 条
if len(results) >= 10*maxPages {
break
}
}
}
// 获取真实 URL百度搜索结果中的 URL 是短链接,需要跳转获取真实 URL
for i, result := range results {
realURL, err := s.getRedirectURL(result.URL)
if err == nil && realURL != "" {
results[i].URL = realURL
}
}
return results, nil
}
// 获取真实 URL
func (s *Service) getRedirectURL(shortURL string) (string, error) {
// 创建页面
page, err := s.browser.Page(proto.TargetCreateTarget{URL: ""})
if err != nil {
return shortURL, err // 返回原始URL
}
defer func() {
_ = page.Close()
}()
// 导航到短链接
err = page.Navigate(shortURL)
if err != nil {
return shortURL, err // 返回原始URL
}
// 等待重定向完成
time.Sleep(2 * time.Second)
// 获取当前 URL
info, err := page.Info()
if err != nil {
return shortURL, err // 返回原始URL
}
return info.URL, nil
}
// Close 关闭浏览器
func (s *Service) Close() error {
if s.browser != nil {
err := s.browser.Close()
s.browser = nil
return err
}
return nil
}
// SearchWeb 封装的搜索方法
func SearchWeb(keyword string, maxPages int) (string, error) {
// 添加panic恢复机制
defer func() {
if r := recover(); r != nil {
log := logger.GetLogger()
log.Errorf("爬虫服务崩溃: %v", r)
}
}()
service, err := NewService()
if err != nil {
return "", fmt.Errorf("创建爬虫服务失败: %v", err)
}
defer service.Close()
// 设置超时上下文
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
// 使用goroutine和通道来处理超时
resultChan := make(chan []SearchResult, 1)
errChan := make(chan error, 1)
go func() {
results, err := service.WebSearch(keyword, maxPages)
if err != nil {
errChan <- err
return
}
resultChan <- results
}()
// 等待结果或超时
select {
case <-ctx.Done():
return "", fmt.Errorf("搜索超时: %v", ctx.Err())
case err := <-errChan:
return "", fmt.Errorf("搜索失败: %v", err)
case results := <-resultChan:
if len(results) == 0 {
return "未找到关于 \"" + keyword + "\" 的相关搜索结果", nil
}
// 格式化结果
var builder strings.Builder
builder.WriteString(fmt.Sprintf("为您找到关于 \"%s\" 的 %d 条搜索结果:\n\n", keyword, len(results)))
for i, result := range results {
// // 尝试打开链接获取实际内容
// page := service.browser.MustPage()
// defer page.MustClose()
// // 设置页面超时
// pageCtx, pageCancel := context.WithTimeout(context.Background(), 10*time.Second)
// defer pageCancel()
// // 导航到目标页面
// err := page.Context(pageCtx).Navigate(result.URL)
// if err == nil {
// // 等待页面加载
// _ = page.WaitLoad()
// // 获取页面标题
// title, err := page.Eval("() => document.title")
// if err == nil && title.Value.String() != "" {
// result.Title = title.Value.String()
// }
// // 获取页面主要内容
// if content, err := page.Element("body"); err == nil {
// if text, err := content.Text(); err == nil {
// // 清理并截取内容
// text = strings.TrimSpace(text)
// if len(text) > 200 {
// text = text[:200] + "..."
// }
// result.Content = text
// }
// }
// }
builder.WriteString(fmt.Sprintf("%d. **%s**\n", i+1, result.Title))
builder.WriteString(fmt.Sprintf(" 链接: %s\n", result.URL))
if result.Content != "" {
builder.WriteString(fmt.Sprintf(" 摘要: %s\n", result.Content))
}
builder.WriteString("\n")
}
return builder.String(), nil
}
}

View File

@ -35,15 +35,13 @@ type Service struct {
uploadManager *oss.UploaderManager
taskQueue *store.RedisQueue
userService *service.UserService
wsService *service.WebsocketService
}
func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Client, userService *service.UserService, wsService *service.WebsocketService) *Service {
func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Client, userService *service.UserService) *Service {
return &Service{
httpClient: req.C().SetTimeout(time.Minute * 3),
db: db,
taskQueue: store.NewRedisQueue("DallE_Task_Queue", redisCli),
wsService: wsService,
uploadManager: manager,
userService: userService,
}

View File

@ -28,17 +28,15 @@ type Service struct {
client *Client // MJ Client
taskQueue *store.RedisQueue
db *gorm.DB
wsService *service.WebsocketService
uploaderManager *oss.UploaderManager
userService *service.UserService
}
func NewService(redisCli *redis.Client, db *gorm.DB, client *Client, manager *oss.UploaderManager, wsService *service.WebsocketService, userService *service.UserService) *Service {
func NewService(redisCli *redis.Client, db *gorm.DB, client *Client, manager *oss.UploaderManager, userService *service.UserService) *Service {
return &Service{
db: db,
taskQueue: store.NewRedisQueue("MidJourney_Task_Queue", redisCli),
client: client,
wsService: wsService,
uploaderManager: manager,
userService: userService,
}

View File

@ -33,16 +33,14 @@ type Service struct {
taskQueue *store.RedisQueue
db *gorm.DB
uploadManager *oss.UploaderManager
wsService *service.WebsocketService
userService *service.UserService
}
func NewService(db *gorm.DB, manager *oss.UploaderManager, levelDB *store.LevelDB, redisCli *redis.Client, wsService *service.WebsocketService, userService *service.UserService) *Service {
func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Client, userService *service.UserService) *Service {
return &Service{
httpClient: req.C(),
taskQueue: store.NewRedisQueue("StableDiffusion_Task_Queue", redisCli),
db: db,
wsService: wsService,
uploadManager: manager,
userService: userService,
}

View File

@ -35,18 +35,16 @@ type Service struct {
uploadManager *oss.UploaderManager
taskQueue *store.RedisQueue
notifyQueue *store.RedisQueue
wsService *service.WebsocketService
userService *service.UserService
}
func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Client, wsService *service.WebsocketService, userService *service.UserService) *Service {
func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Client, userService *service.UserService) *Service {
return &Service{
httpClient: req.C().SetTimeout(time.Minute * 3),
db: db,
taskQueue: store.NewRedisQueue("Suno_Task_Queue", redisCli),
notifyQueue: store.NewRedisQueue("Suno_Notify_Queue", redisCli),
uploadManager: manager,
wsService: wsService,
userService: userService,
}
}

View File

@ -36,16 +36,14 @@ type Service struct {
db *gorm.DB
uploadManager *oss.UploaderManager
taskQueue *store.RedisQueue
wsService *service.WebsocketService
userService *service.UserService
}
func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Client, wsService *service.WebsocketService, userService *service.UserService) *Service {
func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Client, userService *service.UserService) *Service {
return &Service{
httpClient: req.C().SetTimeout(time.Minute * 3),
db: db,
taskQueue: store.NewRedisQueue("Video_Task_Queue", redisCli),
wsService: wsService,
uploadManager: manager,
userService: userService,
}

View File

@ -13,4 +13,5 @@ type ChatModel struct {
Temperature float32 // 模型温度
KeyId int // 绑定 API KEY ID
Type string // 模型类型
Options string // 模型选项
}

View File

@ -2,16 +2,17 @@ package vo
type ChatModel struct {
BaseVo
Name string `json:"name"`
Value string `json:"value"`
Enabled bool `json:"enabled"`
SortNum int `json:"sort_num"`
Power int `json:"power"`
Open bool `json:"open"`
MaxTokens int `json:"max_tokens"` // 最大响应长度
MaxContext int `json:"max_context"` // 最大上下文长度
Temperature float32 `json:"temperature"` // 模型温度
KeyId int `json:"key_id,omitempty"`
KeyName string `json:"key_name"`
Type string `json:"type"`
Name string `json:"name"`
Value string `json:"value"`
Enabled bool `json:"enabled"`
SortNum int `json:"sort_num"`
Power int `json:"power"`
Open bool `json:"open"`
MaxTokens int `json:"max_tokens"` // 最大响应长度
MaxContext int `json:"max_context"` // 最大上下文长度
Temperature float32 `json:"temperature"` // 模型温度
KeyId int `json:"key_id,omitempty"`
KeyName string `json:"key_name"`
Options map[string]string `json:"options"`
Type string `json:"type"`
}

214
api/test/crawler_test.go Normal file
View File

@ -0,0 +1,214 @@
package test
import (
"geekai/service/crawler"
"strings"
"testing"
"time"
)
// TestNewService 测试创建爬虫服务
func TestNewService(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Fatalf("测试过程中发生崩溃: %v", r)
}
}()
service, err := crawler.NewService()
if err != nil {
t.Logf("注意: 创建爬虫服务失败可能是因为Chrome浏览器未安装: %v", err)
t.Skip("跳过测试 - 浏览器问题")
return
}
defer service.Close()
// 创建服务成功则测试通过
if service == nil {
t.Fatal("创建的爬虫服务为空")
}
}
// TestSearchWeb 测试网络搜索功能
func TestSearchWeb(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Fatalf("测试过程中发生崩溃: %v", r)
}
}()
// 设置测试超时时间
timeout := time.After(600 * time.Second)
done := make(chan bool)
go func() {
defer func() {
if r := recover(); r != nil {
t.Logf("搜索过程中发生崩溃: %v", r)
done <- false
return
}
}()
keyword := "Golang编程"
maxPages := 1
// 执行搜索
result, err := crawler.SearchWeb(keyword, maxPages)
if err != nil {
t.Logf("搜索失败,可能是网络问题或浏览器未安装: %v", err)
done <- false
return
}
// 验证结果不为空
if result == "" {
t.Log("搜索结果为空")
done <- false
return
}
// 验证结果包含关键字或部分关键字
if !strings.Contains(result, "Golang") && !strings.Contains(result, "golang") {
t.Logf("搜索结果中未包含关键字或部分关键字,获取到的结果: %s", result)
done <- false
return
}
// 验证结果格式,至少应包含"链接:"
if !strings.Contains(result, "链接:") {
t.Log("搜索结果格式不正确,没有找到'链接:'部分")
done <- false
return
}
done <- true
t.Logf("搜索结果: %s", result)
}()
select {
case <-timeout:
t.Log("测试超时 - 这可能是正常的,特别是在网络较慢或资源有限的环境中")
t.Skip("跳过测试 - 超时")
case success := <-done:
if !success {
t.Skip("跳过测试 - 搜索失败")
}
}
}
// 减少测试用例数量,只保留基本测试
// 这样可以减少测试时间和资源消耗
// 以下测试用例被注释掉,可以根据需要启用
/*
// TestSearchWebNoResults 测试搜索无结果的情况
func TestSearchWebNoResults(t *testing.T) {
// 设置测试超时时间
timeout := time.After(60 * time.Second)
done := make(chan bool)
go func() {
// 使用一个极不可能有搜索结果的随机字符串
keyword := "askdjfhalskjdfhas98y234hlakjsdhflakjshdflakjshdfl"
maxPages := 1
// 执行搜索
result, err := crawler.SearchWeb(keyword, maxPages)
if err != nil {
t.Errorf("搜索失败: %v", err)
done <- false
return
}
// 验证结果为"未找到相关搜索结果"
if !strings.Contains(result, "未找到") && !strings.Contains(result, "0 条搜索结果") {
t.Errorf("对于无结果的搜索,预期返回包含'未找到'的信息,实际返回: %s", result)
done <- false
return
}
done <- true
}()
select {
case <-timeout:
t.Fatal("测试超时")
case success := <-done:
if !success {
t.Fatal("测试失败")
}
}
}
// TestSearchWebMultiplePages 测试多页搜索
func TestSearchWebMultiplePages(t *testing.T) {
// 设置测试超时时间
timeout := time.After(120 * time.Second)
done := make(chan bool)
go func() {
keyword := "golang programming"
maxPages := 2
// 执行搜索
result, err := crawler.SearchWeb(keyword, maxPages)
if err != nil {
t.Errorf("搜索失败: %v", err)
done <- false
return
}
// 验证结果不为空
if result == "" {
t.Error("搜索结果为空")
done <- false
return
}
// 计算结果中的条目数
resultCount := strings.Count(result, "链接:")
if resultCount < 10 {
t.Errorf("多页搜索应返回至少10条结果实际返回: %d", resultCount)
done <- false
return
}
done <- true
}()
select {
case <-timeout:
t.Fatal("测试超时")
case success := <-done:
if !success {
t.Fatal("测试失败")
}
}
}
// TestSearchWebWithMaxPageLimit 测试页数限制
func TestSearchWebWithMaxPageLimit(t *testing.T) {
service, err := crawler.NewService()
if err != nil {
t.Fatalf("创建爬虫服务失败: %v", err)
}
defer service.Close()
// 传入一个超过限制的页数
results, err := service.WebSearch("golang", 15)
if err != nil {
t.Fatalf("搜索失败: %v", err)
}
// 验证结果不为空
if len(results) == 0 {
t.Fatal("搜索结果为空")
}
// 因为最大页数限制为10所以结果数量应该小于等于10*10=100
if len(results) > 100 {
t.Errorf("搜索结果超过最大限制预期最多100条实际: %d", len(results))
}
}
*/

View File

@ -0,0 +1,41 @@
#!/bin/bash
# 显示执行的命令
set -x
# 检查Chrome/Chromium浏览器是否已安装
check_chrome() {
echo "检查Chrome/Chromium浏览器是否安装..."
which chromium-browser || which google-chrome || which chromium
if [ $? -ne 0 ]; then
echo "警告: 未找到Chrome或Chromium浏览器测试可能会失败"
echo "尝试安装必要的依赖..."
sudo apt-get update && sudo apt-get install -y libnss3 libgbm1 libasound2 libatk1.0-0 libatk-bridge2.0-0 libcups2 libxkbcommon0 libxdamage1 libxfixes3 libxrandr2 libxcomposite1 libxcursor1 libxi6 libxtst6 libnss3 libnspr4 libpango1.0-0
echo "已安装依赖但仍需安装Chrome/Chromium浏览器以完全支持测试"
else
echo "已找到Chrome/Chromium浏览器"
fi
}
# 切换到项目根目录
cd ..
# 检查环境
check_chrome
# 运行爬虫测试,使用超时限制
echo "开始运行爬虫测试..."
timeout 180s go test -v ./test/crawler_test.go -run "TestNewService|TestSearchWeb"
TEST_RESULT=$?
if [ $TEST_RESULT -eq 124 ]; then
echo "测试超时终止"
exit 1
elif [ $TEST_RESULT -ne 0 ]; then
echo "测试失败,退出码: $TEST_RESULT"
exit $TEST_RESULT
else
echo "测试成功完成"
fi
echo "测试完成"

View File

@ -13,6 +13,7 @@ import (
"geekai/core/types"
"geekai/store/model"
"io"
"net/url"
"time"
"github.com/imroc/req/v3"
@ -47,7 +48,7 @@ type OpenAIResponse struct {
}
func OpenAIRequest(db *gorm.DB, prompt string, modelId int) (string, error) {
messages := make([]interface{}, 1)
messages := make([]any, 1)
messages[0] = types.Message{
Role: "user",
Content: prompt,
@ -55,7 +56,7 @@ func OpenAIRequest(db *gorm.DB, prompt string, modelId int) (string, error) {
return SendOpenAIMessage(db, messages, modelId)
}
func SendOpenAIMessage(db *gorm.DB, messages []interface{}, modelId int) (string, error) {
func SendOpenAIMessage(db *gorm.DB, messages []any, modelId int) (string, error) {
var chatModel model.ChatModel
db.Where("id", modelId).First(&chatModel)
if chatModel.Value == "" {
@ -74,10 +75,17 @@ func SendOpenAIMessage(db *gorm.DB, messages []interface{}, modelId int) (string
var response OpenAIResponse
client := req.C()
if len(apiKey.ProxyURL) > 5 {
client.SetProxyURL(apiKey.ApiURL)
client.SetProxyURL(apiKey.ProxyURL)
}
apiURL := fmt.Sprintf("%s/v1/chat/completions", apiKey.ApiURL)
logger.Infof("Sending %s request, API KEY:%s, PROXY: %s, Model: %s", apiKey.ApiURL, apiURL, apiKey.ProxyURL, chatModel.Name)
var apiURL string
p, _ := url.Parse(apiKey.ApiURL)
// 如果设置的是 BASE_URL 没有路径,则添加 /v1/chat/completions
if p.Path == "" {
apiURL = fmt.Sprintf("%s/v1/chat/completions", apiKey.ApiURL)
} else {
apiURL = apiKey.ApiURL
}
logger.Infof("Sending %s request, API KEY:%s, PROXY: %s, Model: %s", apiURL, apiKey.ApiURL, apiKey.ProxyURL, chatModel.Name)
r, err := client.R().SetHeader("Body-Type", "application/json").
SetHeader("Authorization", "Bearer "+apiKey.Value).
SetBody(types.ApiRequest{

View File

@ -2,10 +2,10 @@
-- version 5.2.1
-- https://www.phpmyadmin.net/
--
-- 主机: 127.0.0.1
-- 生成日期: 2025-03-10 15:33:32
-- 主机: localhost
-- 生成日期: 2025-04-17 02:48:52
-- 服务器版本: 8.0.33
-- PHP 版本: 8.1.2-1ubuntu2.20
-- PHP 版本: 8.3.6
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
@ -47,7 +47,7 @@ CREATE TABLE `chatgpt_admin_users` (
--
INSERT INTO `chatgpt_admin_users` (`id`, `username`, `password`, `salt`, `status`, `last_login_at`, `last_login_ip`, `created_at`, `updated_at`) VALUES
(1, 'admin', '6d17e80c87d209efb84ca4b2e0824f549d09fac8b2e1cc698de5bb5e1d75dfd0', 'mmrql75o', 1, 1735174700, '::1', '2024-03-11 16:30:20', '2024-12-26 08:58:20');
(1, 'admin', '6d17e80c87d209efb84ca4b2e0824f549d09fac8b2e1cc698de5bb5e1d75dfd0', 'mmrql75o', 1, 1744788584, '::1', '2024-03-11 16:30:20', '2025-04-16 15:29:45');
-- --------------------------------------------------------
@ -85,17 +85,6 @@ CREATE TABLE `chatgpt_app_types` (
`created_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='应用分类表';
--
-- 转存表中的数据 `chatgpt_app_types`
--
INSERT INTO `chatgpt_app_types` (`id`, `name`, `icon`, `sort_num`, `enabled`, `created_at`) VALUES
(3, '通用工具', 'http://172.22.11.200:5678/static/upload/2024/9/1726307371871693.png', 1, 1, '2024-09-13 11:13:15'),
(4, '角色扮演', 'http://172.22.11.200:5678/static/upload/2024/9/1726307263906218.png', 1, 1, '2024-09-14 09:28:17'),
(5, '学习', 'http://172.22.11.200:5678/static/upload/2024/9/1726307456321179.jpg', 2, 1, '2024-09-14 09:30:18'),
(6, '编程', 'http://172.22.11.200:5678/static/upload/2024/9/1726307462748787.jpg', 3, 1, '2024-09-14 09:34:06'),
(7, '测试分类', '', 4, 1, '2024-09-14 17:54:17');
-- --------------------------------------------------------
--
@ -160,6 +149,7 @@ CREATE TABLE `chatgpt_chat_models` (
`max_context` int NOT NULL DEFAULT '4096' COMMENT '最大上下文长度',
`open` tinyint(1) NOT NULL COMMENT '是否开放模型',
`key_id` int NOT NULL COMMENT '绑定API KEY ID',
`options` text NOT NULL COMMENT '模型自定义选项',
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI 模型表';
@ -168,24 +158,27 @@ CREATE TABLE `chatgpt_chat_models` (
-- 转存表中的数据 `chatgpt_chat_models`
--
INSERT INTO `chatgpt_chat_models` (`id`, `type`, `name`, `value`, `sort_num`, `enabled`, `power`, `temperature`, `max_tokens`, `max_context`, `open`, `key_id`, `created_at`, `updated_at`) VALUES
(1, 'chat', 'gpt-4o-mini', 'gpt-4o-mini', 1, 1, 1, 1.0, 1024, 16384, 1, 57, '2023-08-23 12:06:36', '2025-03-03 15:54:36'),
(15, 'chat', 'GPT-4O(联网版本)', 'gpt-4o-all', 4, 1, 30, 1.0, 4096, 32768, 1, 0, '2024-01-15 11:32:52', '2025-03-04 11:39:00'),
(36, 'chat', 'GPT-4O', 'gpt-4o', 3, 1, 15, 1.0, 4096, 16384, 1, 0, '2024-05-14 09:25:15', '2025-03-04 11:38:05'),
(39, 'chat', 'Claude35-snonet', 'claude-3-5-sonnet-20240620', 5, 1, 2, 1.0, 4000, 200000, 1, 0, '2024-05-29 15:04:19', '2025-01-06 14:01:08'),
(42, 'chat', 'DeekSeek', 'deepseek-chat', 8, 1, 1, 1.0, 4096, 32768, 1, 0, '2024-06-27 16:13:01', '2025-03-04 11:40:51'),
(46, 'chat', 'gpt-3.5-turbo', 'gpt-3.5-turbo', 2, 1, 1, 1.0, 1024, 4096, 1, 0, '2024-07-22 13:53:41', '2025-03-04 11:37:57'),
(49, 'chat', 'O1-mini', 'o1-mini', 10, 1, 2, 0.9, 1024, 8192, 1, 0, '2024-09-13 18:07:50', '2025-03-04 11:40:47'),
(50, 'chat', 'O1-preview', 'o1-preview', 11, 1, 5, 0.9, 1024, 8192, 1, 0, '2024-09-13 18:11:08', '2025-03-04 11:40:55'),
(51, 'chat', 'O1-mini-all', 'o1-mini-all', 12, 1, 1, 0.9, 1024, 8192, 1, 0, '2024-09-29 11:40:52', '2025-03-04 11:40:59'),
(53, 'chat', 'OpenAI 高级语音', 'advanced-voice', 15, 1, 10, 0.9, 1024, 8192, 1, 45, '2024-12-20 10:34:45', '2025-03-04 11:41:17'),
(55, 'chat', 'O3-mini', 'o3-mini', 17, 1, 5, 0.9, 1024, 8192, 1, 52, '2024-12-25 15:15:49', '2025-02-08 10:52:01'),
(56, 'img', 'flux-1-schnell', 'flux-1-schnell', 18, 1, 1, 0.9, 1024, 8192, 1, 81, '2024-12-25 15:30:27', '2025-01-06 14:01:08'),
(57, 'img', 'dall-e-3', 'dall-e-3', 19, 1, 1, 0.9, 1024, 8192, 1, 57, '2024-12-25 16:54:06', '2025-01-06 14:01:08'),
(58, 'img', 'SD-3-medium', 'stable-diffusion-3-medium', 20, 1, 1, 0.9, 1024, 8192, 1, 81, '2024-12-27 10:03:28', '2025-01-06 14:01:08'),
(59, 'chat', 'O1-preview-all', 'O1-preview-all', 13, 1, 10, 0.9, 1024, 32000, 1, 0, '2025-01-06 14:01:04', '2025-03-04 11:41:02'),
(60, 'chat', 'DeepSeek-R1-7B', 'deepseek-r1:7b', 20, 1, 1, 0.9, 1024, 8192, 1, 78, '2025-02-07 11:32:08', '2025-02-07 14:37:48'),
(61, 'chat', 'DeepSeek-R1-32B', 'deepseek-r1:32b', 21, 1, 1, 0.9, 1024, 8192, 1, 78, '2025-02-07 14:38:19', '2025-02-07 14:38:44');
INSERT INTO `chatgpt_chat_models` (`id`, `type`, `name`, `value`, `sort_num`, `enabled`, `power`, `temperature`, `max_tokens`, `max_context`, `open`, `key_id`, `options`, `created_at`, `updated_at`) VALUES
(1, 'chat', 'gpt-4o-mini', 'gpt-4o-mini', 1, 1, 1, 1.0, 1024, 16384, 1, 1, '', '2023-08-23 12:06:36', '2025-02-23 11:57:03'),
(15, 'chat', 'GPT-4O(联网版本)', 'gpt-4o-all', 4, 1, 30, 1.0, 4096, 32768, 1, 57, '', '2024-01-15 11:32:52', '2025-01-06 14:01:08'),
(36, 'chat', 'GPT-4O', 'gpt-4o', 3, 1, 15, 1.0, 4096, 16384, 1, 0, 'null', '2024-05-14 09:25:15', '2025-04-02 20:22:15'),
(39, 'chat', 'Claude35-snonet', 'claude-3-5-sonnet-20240620', 5, 1, 2, 1.0, 4000, 200000, 1, 0, '', '2024-05-29 15:04:19', '2025-01-06 14:01:08'),
(41, 'chat', 'Suno对话模型', 'suno-v3.5', 7, 1, 10, 1.0, 1024, 8192, 1, 57, '', '2024-06-06 11:40:46', '2025-01-06 14:01:08'),
(42, 'chat', 'DeekSeek', 'deepseek-chat', 8, 1, 1, 1.0, 4096, 32768, 1, 57, '', '2024-06-27 16:13:01', '2025-01-06 14:11:51'),
(44, 'chat', 'Claude3-opus', 'claude-3-opus-20240229', 6, 1, 5, 1.0, 4000, 128000, 1, 44, '', '2024-07-22 11:24:30', '2025-01-06 14:01:08'),
(46, 'chat', 'GPT-4O-绘图', 'gpt-4o-image', 2, 1, 1, 1.0, 2048, 32000, 1, 6, '', '2024-07-22 13:53:41', '2025-03-29 13:02:14'),
(48, 'chat', '彩票助手', 'gpt-4-gizmo-g-wmSivBgxo', 9, 1, 1, 0.9, 1024, 8192, 1, 57, '', '2024-09-05 14:17:14', '2025-01-06 14:01:08'),
(49, 'chat', 'O1-mini', 'o1-mini', 10, 1, 2, 0.9, 1024, 8192, 1, 44, '', '2024-09-13 18:07:50', '2025-01-06 14:01:08'),
(50, 'chat', 'O1-preview', 'o1-preview', 11, 1, 5, 0.9, 1024, 8192, 1, 44, '', '2024-09-13 18:11:08', '2025-01-06 14:01:08'),
(51, 'chat', 'O1-mini-all', 'o1-mini-all', 12, 1, 1, 0.9, 1024, 8192, 1, 57, '', '2024-09-29 11:40:52', '2025-01-06 14:01:08'),
(52, 'chat', '通义千问', 'qwen-plus', 14, 1, 1, 0.9, 1024, 8192, 1, 80, '', '2024-11-19 08:38:14', '2025-01-06 14:01:08'),
(53, 'chat', 'OpenAI 高级语音', 'advanced-voice', 15, 1, 10, 0.9, 1024, 8192, 1, 44, '', '2024-12-20 10:34:45', '2025-01-06 14:01:08'),
(54, 'chat', 'Qwen2.5-14B-Instruct', 'Qwen2.5-14B-Instruct', 16, 1, 1, 0.9, 1024, 8192, 1, 81, '', '2024-12-25 14:53:17', '2025-01-06 14:01:08'),
(55, 'chat', 'Qwen2.5-7B-Instruct', 'Qwen2.5-7B-Instruct', 17, 1, 1, 0.9, 1024, 8192, 1, 81, '', '2024-12-25 15:15:49', '2025-01-06 14:01:08'),
(56, 'img', 'flux-1-schnell', 'flux-1-schnell', 18, 1, 1, 0.9, 1024, 8192, 1, 3, '', '2024-12-25 15:30:27', '2025-02-23 12:02:40'),
(57, 'img', 'dall-e-3', 'dall-e-3', 19, 1, 1, 0.9, 1024, 8192, 1, 57, '', '2024-12-25 16:54:06', '2025-01-06 14:01:08'),
(58, 'img', 'SD-3-medium', 'stable-diffusion-3-medium', 20, 1, 1, 0.9, 1024, 8192, 1, 3, 'null', '2024-12-27 10:03:28', '2025-04-02 20:20:36'),
(59, 'chat', 'O1-preview-all', 'O1-preview-all', 13, 1, 10, 0.9, 1024, 32000, 1, 57, '', '2025-01-06 14:01:04', '2025-01-06 14:01:08');
-- --------------------------------------------------------
@ -232,7 +225,7 @@ INSERT INTO `chatgpt_chat_roles` (`id`, `name`, `tid`, `marker`, `context_json`,
(38, '埃隆·马斯克', 0, 'elon_musk', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以埃隆·马斯克的身份,站在埃隆·马斯克的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以埃隆·马斯克的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '梦想要远大,如果你的梦想没有吓到你,说明你做得不对。', '/images/avatar/elon_musk.jpg', 1, 18, 0, '2023-05-30 14:10:24', '2024-11-08 16:30:32'),
(39, '孔子', 5, 'kong_zi', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以孔子的身份,站在孔子的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以孔子的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '士不可以不弘毅,任重而道远。', '/images/avatar/kong_zi.jpg', 1, 19, 0, '2023-05-30 14:10:24', '2024-11-08 16:30:32'),
(133, 'AI绘画提示词助手', 3, 'draw_prompt', '[{\"role\":\"system\",\"content\":\"Create a highly effective prompt to provide to an AI image generation tool in order to create an artwork based on a desired concept.\\n\\nPlease specify details about the artwork, such as the style, subject, mood, and other important characteristics you want the resulting image to have.\\n\\nRemeber, prompts should always be output in English.\\n\\n# Steps\\n\\n1. **Subject Description**: Describe the main subject of the image clearly. Include as much detail as possible about what should be in the scene. For example, \\\"a majestic lion roaring at sunrise\\\" or \\\"a futuristic city with flying cars.\\\"\\n \\n2. **Art Style**: Specify the art style you envision. Possible options include \'realistic\', \'impressionist\', a specific artist name, or imaginative styles like \\\"cyberpunk.\\\" This helps the AI achieve your visual expectations.\\n\\n3. **Mood or Atmosphere**: Convey the feeling you want the image to evoke. For instance, peaceful, chaotic, epic, etc.\\n\\n4. **Color Palette and Lighting**: Mention color preferences or lighting. For example, \\\"vibrant with shades of blue and purple\\\" or \\\"dim and dramatic lighting.\\\"\\n\\n5. **Optional Features**: You can add any additional attributes, such as background details, attention to textures, or any specific kind of framing.\\n\\n# Output Format\\n\\n- **Prompt Format**: A descriptive phrase that includes key aspects of the artwork (subject, style, mood, colors, lighting, any optional features).\\n \\nHere is an example of how the final prompt should look:\\n \\n\\\"An ethereal landscape featuring towering ice mountains, in an impressionist style reminiscent of Claude Monet, with a serene mood. The sky is glistening with soft purples and whites, with a gentle morning sun illuminating the scene.\\\"\\n\\n**Please input the prompt words directly in English, and do not input any other explanatory statements**\\n\\n# Examples\\n\\n1. **Input**: \\n - Subject: A white tiger in a dense jungle\\n - Art Style: Realistic\\n - Mood: Intense, mysterious\\n - Lighting: Dramatic contrast with light filtering through leaves\\n \\n **Output Prompt**: \\\"A realistic rendering of a white tiger stealthily moving through a dense jungle, with an intense, mysterious mood. The lighting creates strong contrasts as beams of sunlight filter through a thick canopy of leaves.\\\"\\n\\n2. **Input**: \\n - Subject: An enchanted castle on a floating island\\n - Art Style: Fantasy\\n - Mood: Majestic, magical\\n - Colors: Bright blues, greens, and gold\\n \\n **Output Prompt**: \\\"A majestic fantasy castle on a floating island above the clouds, with bright blues, greens, and golds to create a magical, dreamy atmosphere. Textured cobblestone details and glistening waters surround the scene.\\\" \\n\\n# Notes\\n\\n- Ensure that you mix different aspects to get a comprehensive and visually compelling prompt.\\n- Be as descriptive as possible as it often helps generate richer, more detailed images.\\n- If you want the image to resemble a particular artist\'s work, be sure to mention the artist explicitly. e.g., \\\"in the style of Van Gogh.\\\"\"}]', '你好,请输入你要创作图片大概内容描述,我将为您生成专业的 AI 绘画指令。', 'https://blog.img.r9it.com/f38e2357c3ccd9412184e42273a7451a.png', 1, 3, 36, '2024-11-06 15:32:48', '2024-11-12 16:11:25'),
(134, '提示词专家', 3, 'prompt_engineer', '[{\"role\":\"system\",\"content\":\"Given a task description or existing prompt, produce a detailed system prompt to guide a language model in completing the task effectively.\\n\\nPlease remember, the final output must be the same language with users input.\\n\\n# Guidelines\\n\\n- Understand the Task: Grasp the main objective, goals, requirements, constraints, and expected output.\\n- Minimal Changes: If an existing prompt is provided, improve it only if it\'s simple. For complex prompts, enhance clarity and add missing elements without altering the original structure.\\n- Reasoning Before Conclusions**: Encourage reasoning steps before any conclusions are reached. ATTENTION! If the user provides examples where the reasoning happens afterward, REVERSE the order! NEVER START EXAMPLES WITH CONCLUSIONS!\\n - Reasoning Order: Call out reasoning portions of the prompt and conclusion parts (specific fields by name). For each, determine the ORDER in which this is done, and whether it needs to be reversed.\\n - Conclusion, classifications, or results should ALWAYS appear last.\\n- Examples: Include high-quality examples if helpful, using placeholders [in brackets] for complex elements.\\n - What kinds of examples may need to be included, how many, and whether they are complex enough to benefit from placeholders.\\n- Clarity and Conciseness: Use clear, specific language. Avoid unnecessary instructions or bland statements.\\n- Formatting: Use markdown features for readability. DO NOT USE ``` CODE BLOCKS UNLESS SPECIFICALLY REQUESTED.\\n- Preserve User Content: If the input task or prompt includes extensive guidelines or examples, preserve them entirely, or as closely as possible. If they are vague, consider breaking down into sub-steps. Keep any details, guidelines, examples, variables, or placeholders provided by the user.\\n- Constants: DO include constants in the prompt, as they are not susceptible to prompt injection. Such as guides, rubrics, and examples.\\n- Output Format: Explicitly the most appropriate output format, in detail. This should include length and syntax (e.g. short sentence, paragraph, JSON, etc.)\\n - For tasks outputting well-defined or structured data (classification, JSON, etc.) bias toward outputting a JSON.\\n - JSON should never be wrapped in code blocks (```) unless explicitly requested.\\n\\nThe final prompt you output should adhere to the following structure below. Do not include any additional commentary, only output the completed system prompt. SPECIFICALLY, do not include any additional messages at the start or end of the prompt. (e.g. no \\\"---\\\")\\n\\n[Concise instruction describing the task - this should be the first line in the prompt, no section header]\\n\\n[Additional details as needed.]\\n\\n[Optional sections with headings or bullet points for detailed steps.]\\n\\n# Steps [optional]\\n\\n[optional: a detailed breakdown of the steps necessary to accomplish the task]\\n\\n# Output Format\\n\\n[Specifically call out how the output should be formatted, be it response length, structure e.g. JSON, markdown, etc]\\n\\n# Examples [optional]\\n\\n[Optional: 1-3 well-defined examples with placeholders if necessary. Clearly mark where examples start and end, and what the input and output are. User placeholders as necessary.]\\n[If the examples are shorter than what a realistic example is expected to be, make a reference with () explaining how real examples should be longer / shorter / different. AND USE PLACEHOLDERS! ]\\n\\n# Notes [optional]\\n\\n[optional: edge cases, details, and an area to call or repeat out specific important considerations]\"}]', '不知道如何向 AI 发问?说出想法,提示词专家帮你精心设计提示词', 'https://blog.img.r9it.com/a8908d04c3ccd941b00a612e27df086e.png', 1, 2, 36, '2024-11-07 18:06:39', '2024-11-12 16:15:12');
(134, '提示词专家', 3, 'prompt_engineer', '[{\"role\":\"system\",\"content\":\"Given a task description or existing prompt, produce a detailed system prompt to guide a language model in completing the task effectively.\\n\\nPlease remember, the final output must be the same language with users input.\\n\\n# Guidelines\\n\\n- Understand the Task: Grasp the main objective, goals, requirements, constraints, and expected output.\\n- Minimal Changes: If an existing prompt is provided, improve it only if it\'s simple. For complex prompts, enhance clarity and add missing elements without altering the original structure.\\n- Reasoning Before Conclusions**: Encourage reasoning steps before any conclusions are reached. ATTENTION! If the user provides examples where the reasoning happens afterward, REVERSE the order! NEVER START EXAMPLES WITH CONCLUSIONS!\\n - Reasoning Order: Call out reasoning portions of the prompt and conclusion parts (specific fields by name). For each, determine the ORDER in which this is done, and whether it needs to be reversed.\\n - Conclusion, classifications, or results should ALWAYS appear last.\\n- Examples: Include high-quality examples if helpful, using placeholders [in brackets] for complex elements.\\n - What kinds of examples may need to be included, how many, and whether they are complex enough to benefit from placeholders.\\n- Clarity and Conciseness: Use clear, specific language. Avoid unnecessary instructions or bland statements.\\n- Formatting: Use markdown features for readability. DO NOT USE ``` CODE BLOCKS UNLESS SPECIFICALLY REQUESTED.\\n- Preserve User Content: If the input task or prompt includes extensive guidelines or examples, preserve them entirely, or as closely as possible. If they are vague, consider breaking down into sub-steps. Keep any details, guidelines, examples, variables, or placeholders provided by the user.\\n- Constants: DO include constants in the prompt, as they are not susceptible to prompt injection. Such as guides, rubrics, and examples.\\n- Output Format: Explicitly the most appropriate output format, in detail. This should include length and syntax (e.g. short sentence, paragraph, JSON, etc.)\\n - For tasks outputting well-defined or structured data (classification, JSON, etc.) bias toward outputting a JSON.\\n - JSON should never be wrapped in code blocks (```) unless explicitly requested.\\n\\nThe final prompt you output should adhere to the following structure below. Do not include any additional commentary, only output the completed system prompt. SPECIFICALLY, do not include any additional messages at the start or end of the prompt. (e.g. no \\\"---\\\")\\n\\n[Concise instruction describing the task - this should be the first line in the prompt, no section header]\\n\\n[Additional details as needed.]\\n\\n[Optional sections with headings or bullet points for detailed steps.]\\n\\n# Steps [optional]\\n\\n[optional: a detailed breakdown of the steps necessary to accomplish the task]\\n\\n# Output Format\\n\\n[Specifically call out how the output should be formatted, be it response length, structure e.g. JSON, markdown, etc]\\n\\n# Examples [optional]\\n\\n[Optional: 1-3 well-defined examples with placeholders if necessary. Clearly mark where examples start and end, and what the input and output are. User placeholders as necessary.]\\n[If the examples are shorter than what a realistic example is expected to be, make a reference with () explaining how real examples should be longer / shorter / different. AND USE PLACEHOLDERS! ]\\n\\n# Notes [optional]\\n\\n[optional: edge cases, details, and an area to call or repeat out specific important considerations]\"}]', '不知道如何向 AI 发问?说出想法,提示词专家帮你精心设计提示词', 'https://blog.img.r9it.com/a8908d04c3ccd941b00a612e27df086e.png', 1, 2, 36, '2024-11-07 18:06:39', '2025-02-22 22:34:36');
-- --------------------------------------------------------
@ -252,8 +245,8 @@ CREATE TABLE `chatgpt_configs` (
--
INSERT INTO `chatgpt_configs` (`id`, `marker`, `config_json`) VALUES
(1, 'system', '{\"title\":\"GeekAI 创作助手\",\"slogan\":\"我辈之人先干为敬让每一个人都能用好AI\",\"admin_title\":\"GeekAI 控制台\",\"logo\":\"/images/logo.png\",\"bar_logo\":\"/images/bar_logo.png\",\"init_power\":100,\"daily_power\":10,\"invite_power\":200,\"vip_month_power\":1000,\"register_ways\":[\"username\",\"email\",\"mobile\"],\"enabled_register\":true,\"order_pay_timeout\":600,\"vip_info_text\":\"月度会员,年度会员每月赠送 1000 点算力,赠送算力当月有效当月没有消费完的算力不结余到下个月。 点卡充值的算力长期有效。\",\"mj_power\":20,\"mj_action_power\":5,\"sd_power\":5,\"dall_power\":10,\"suno_power\":10,\"luma_power\":100,\"keling_powers\":{\"kling-v1-5_pro_10\":840,\"kling-v1-5_pro_5\":420,\"kling-v1-5_std_10\":480,\"kling-v1-5_std_5\":240,\"kling-v1-6_pro_10\":840,\"kling-v1-6_pro_5\":420,\"kling-v1-6_std_10\":480,\"kling-v1-6_std_5\":240,\"kling-v1_pro_10\":840,\"kling-v1_pro_5\":420,\"kling-v1_std_10\":240,\"kling-v1_std_5\":120},\"advance_voice_power\":100,\"prompt_power\":1,\"wechat_card_url\":\"/images/wx.png\",\"enable_context\":true,\"context_deep\":10,\"sd_neg_prompt\":\"nsfw, paintings,low quality,easynegative,ng_deepnegative ,lowres,bad anatomy,bad hands,bad feet\",\"mj_mode\":\"fast\",\"index_navs\":[1,5,13,19,9,12,6,20,8,10],\"copyright\":\"极客学长\",\"icp\":\"粤ICP备19122051号\",\"mark_map_text\":\"# GeekAI 演示站\\n\\n- 完整的开源系统,前端应用和后台管理系统皆可开箱即用。\\n- 基于 Websocket 实现,完美的打字机体验。\\n- 内置了各种预训练好的角色应用,轻松满足你的各种聊天和应用需求。\\n- 支持 OPenAIAzure文心一言讯飞星火清华 ChatGLM等多个大语言模型。\\n- 支持 MidJourney / Stable Diffusion AI 绘画集成,开箱即用。\\n- 支持使用个人微信二维码作为充值收费的支付渠道,无需企业支付通道。\\n- 已集成支付宝支付功能,微信支付,支持多种会员套餐和点卡购买功能。\\n- 集成插件 API 功能,可结合大语言模型的 function 功能开发各种强大的插件。\",\"enabled_verify\":false,\"email_white_list\":[\"qq.com\",\"163.com\",\"gmail.com\",\"hotmail.com\",\"126.com\",\"outlook.com\",\"foxmail.com\",\"yahoo.com\",\"pvc123.com\"],\"translate_model_id\":1,\"max_file_size\":5}'),
(3, 'notice', '{\"sd_neg_prompt\":\"\",\"mj_mode\":\"\",\"index_navs\":null,\"copyright\":\"\",\"icp\":\"\",\"mark_map_text\":\"\",\"enabled_verify\":false,\"email_white_list\":null,\"translate_model_id\":0,\"max_file_size\":0,\"content\":\"## v4.2.1 更新日志\\n\\n- 功能新增:**新增支持可灵生成视频,支持文生视频,图生生视频**。\\n- Bug 修复:修复手机端登录页面 Logo 无法修改的问题。\\n- 功能新增:重构所有异步任务(绘图,音乐,视频)更新方式,使用 http pull 来替代 websocket。\\n- 功能优化:优化 Luma 图生视频功能,支持本地上传图片和远程图片。\\n- Bug 修复:修复移动端聊天页面新建对话时候角色没有更模型绑定的 Bug。\\n- 功能优化:优化聊天页面代码块样式,优化公式的解析。\\n- 功能优化:在绘图,视频相关 API 增加提示词长度的检查,防止提示词超出导致写入数据库失败。\\n- Bug 修复:优化 Redis 连接池配置,增加连接池超时时间,单核服务器报错 `redis: connection pool timeout`。\\n- 功能优化:优化邮件验证码发送逻辑,更新邮件发送成功提示。\\n\\n\\u003e **注意:** 当前站点仅为开源项目 \\u003ca style=\\\"color: #F56C6C\\\" href=\\\"https://github.com/yangjian102621/geekai\\\" target=\\\"_blank\\\"\\u003eGeekAI-Plus\\u003c/a\\u003e 的演示项目,本项目单纯就是给大家体验项目功能使用。\\n\\u003cstrong style=\\\"color: #F56C6C\\\"\\u003e体验额度用完之后请不要在当前站点进行任何充值操作\\u003c/strong\\u003e\\n\\u003cstrong style=\\\"color: #F56C6C\\\"\\u003e体验额度用完之后请不要在当前站点进行任何充值操作\\u003c/strong\\u003e\\n\\u003cstrong style=\\\"color: #F56C6C\\\"\\u003e体验额度用完之后请不要在当前站点进行任何充值操作\\u003c/strong\\u003e\\n 如果觉得好用你就花几分钟自己部署一套没有API KEY 的同学可以去下面几个推荐的中转站购买:\\n1、\\u003ca href=\\\"https://api.chat-plus.net\\\" target=\\\"_blank\\\"\\n style=\\\"font-size: 20px;color:#F56C6C\\\"\\u003ehttps://api.chat-plus.net\\u003c/a\\u003e\\n2、\\u003ca href=\\\"https://api.geekai.me\\\" target=\\\"_blank\\\"\\n style=\\\"font-size: 20px;color:#F56C6C\\\"\\u003ehttps://api.geekai.me\\u003c/a\\u003e\\n\\n支持MidJourneyGPTClaudeGoogle Gemmi以及国内各个厂家的大模型现在有超级优惠价格远低于 OpenAI 官方。关于中转 API 的优势和劣势请参考 [中转API技术原理](https://docs.geekai.me/config/chat/#%E4%B8%AD%E8%BD%ACapi%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86)。GPT-3.5GPT-4DALL-E3 绘图......你都可以随意使用,无需魔法。\\n接入教程 \\u003ca href=\\\"https://docs.geekai.me\\\" target=\\\"_blank\\\"\\n style=\\\"font-size: 20px;color:#F56C6C\\\"\\u003ehttps://docs.geekai.me\\u003c/a\\u003e\\n本项目源码地址\\u003ca href=\\\"https://github.com/yangjian102621/geekai\\\" target=\\\"_blank\\\"\\u003ehttps://github.com/yangjian102621/geekai\\u003c/a\\u003e\",\"updated\":true}');
(1, 'system', '{\"title\":\"GeekAI 创作助手\",\"slogan\":\"我辈之人先干为敬让每一个人都能用好AI\",\"admin_title\":\"GeekAI 控制台\",\"logo\":\"/images/logo.png\",\"bar_logo\":\"/images/bar_logo.png\",\"init_power\":100,\"daily_power\":1,\"invite_power\":200,\"vip_month_power\":1000,\"register_ways\":[\"username\",\"email\",\"mobile\"],\"enabled_register\":true,\"order_pay_timeout\":600,\"vip_info_text\":\"月度会员,年度会员每月赠送 1000 点算力,赠送算力当月有效当月没有消费完的算力不结余到下个月。 点卡充值的算力长期有效。\",\"mj_power\":20,\"mj_action_power\":5,\"sd_power\":5,\"dall_power\":10,\"suno_power\":10,\"luma_power\":120,\"keling_powers\":{\"kling-v1-5_pro_10\":840,\"kling-v1-5_pro_5\":420,\"kling-v1-5_std_10\":480,\"kling-v1-5_std_5\":240,\"kling-v1-6_pro_10\":840,\"kling-v1-6_pro_5\":420,\"kling-v1-6_std_10\":480,\"kling-v1-6_std_5\":240,\"kling-v1_pro_10\":840,\"kling-v1_pro_5\":420,\"kling-v1_std_10\":240,\"kling-v1_std_5\":120},\"advance_voice_power\":100,\"prompt_power\":1,\"wechat_card_url\":\"/images/wx.png\",\"enable_context\":true,\"context_deep\":10,\"sd_neg_prompt\":\"nsfw, paintings,low quality,easynegative,ng_deepnegative ,lowres,bad anatomy,bad hands,bad feet\",\"mj_mode\":\"fast\",\"index_navs\":[1,5,13,19,9,12,6,20,8,10],\"copyright\":\"极客学长\",\"icp\":\"粤ICP备19122051号\",\"mark_map_text\":\"# GeekAI 演示站\\n\\n- 完整的开源系统,前端应用和后台管理系统皆可开箱即用。\\n- 基于 Websocket 实现,完美的打字机体验。\\n- 内置了各种预训练好的角色应用,轻松满足你的各种聊天和应用需求。\\n- 支持 OPenAIAzure文心一言讯飞星火清华 ChatGLM等多个大语言模型。\\n- 支持 MidJourney / Stable Diffusion AI 绘画集成,开箱即用。\\n- 支持使用个人微信二维码作为充值收费的支付渠道,无需企业支付通道。\\n- 已集成支付宝支付功能,微信支付,支持多种会员套餐和点卡购买功能。\\n- 集成插件 API 功能,可结合大语言模型的 function 功能开发各种强大的插件。\",\"enabled_verify\":false,\"email_white_list\":[\"qq.com\",\"163.com\",\"gmail.com\",\"hotmail.com\",\"126.com\",\"outlook.com\",\"foxmail.com\",\"yahoo.com\"],\"translate_model_id\":36,\"max_file_size\":10}'),
(3, 'notice', '{\"sd_neg_prompt\":\"\",\"mj_mode\":\"\",\"index_navs\":null,\"copyright\":\"\",\"icp\":\"\",\"mark_map_text\":\"\",\"enabled_verify\":false,\"email_white_list\":null,\"translate_model_id\":0,\"max_file_size\":0,\"content\":\"## v4.2.2 更新日志\\n- 功能优化:开启图形验证码功能的时候现检查是否配置了 API 服务,防止开启之后没法登录的 Bug。\\n- 功能优化:支持原生的 DeepSeek 推理模型 API聊天 API KEY 支持设置完整的 API 路径,比如 https://api.geekai.pro/v1/chat/completions\\n- 功能优化:支持 GPT-4o 图片编辑功能。\\n- 功能新增:对话页面支持 AI 输出语音播报TTS。\\n- 功能优化:替换瀑布流组件,优化用户体验。\\n- 功能优化:生成思维导图时候自动缓存上一次的结果。\\n- 功能优化:优化 MJ 绘图页面,增加 MJ-V7 模型支持。\\n- 功能优化:后台管理增加生成一键登录链接地址功能\\n\\n注意当前站点仅为开源项目 \\u003ca style=\\\"color: #F56C6C\\\" href=\\\"https://github.com/yangjian102621/geekai\\\" target=\\\"_blank\\\"\\u003eGeekAI-Plus\\u003c/a\\u003e 的演示项目,本项目单纯就是给大家体验项目功能使用。\\n\\u003cstrong style=\\\"color: #F56C6C\\\"\\u003e体验额度用完之后请不要在当前站点进行任何充值操作\\u003c/strong\\u003e\\n\\u003cstrong style=\\\"color: #F56C6C\\\"\\u003e体验额度用完之后请不要在当前站点进行任何充值操作\\u003c/strong\\u003e\\n\\u003cstrong style=\\\"color: #F56C6C\\\"\\u003e体验额度用完之后请不要在当前站点进行任何充值操作\\u003c/strong\\u003e\\n 如果觉得好用你就花几分钟自己部署一套没有API KEY 的同学可以去下面几个推荐的中转站购买:\\n1、\\u003ca href=\\\"https://api.geekai.pro\\\" target=\\\"_blank\\\"\\n style=\\\"font-size: 20px;color:#F56C6C\\\"\\u003ehttps://api.geekai.pro\\u003c/a\\u003e\\n2、\\u003ca href=\\\"https://api.geekai.me\\\" target=\\\"_blank\\\"\\n style=\\\"font-size: 20px;color:#F56C6C\\\"\\u003ehttps://api.geekai.me\\u003c/a\\u003e\\n支持MidJourneyGPTClaudeGoogle Gemmi以及国内各个厂家的大模型现在有超级优惠价格远低于 OpenAI 官方。关于中转 API 的优势和劣势请参考 [中转API技术原理](https://docs.geekai.me/config/chat/#%E4%B8%AD%E8%BD%ACapi%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86)。GPT-3.5GPT-4DALL-E3 绘图......你都可以随意使用,无需魔法。\\n接入教程 \\u003ca href=\\\"https://docs.geekai.me\\\" target=\\\"_blank\\\"\\n style=\\\"font-size: 20px;color:#F56C6C\\\"\\u003ehttps://docs.geekai.me\\u003c/a\\u003e\\n本项目源码地址\\u003ca href=\\\"https://github.com/yangjian102621/geekai\\\" target=\\\"_blank\\\"\\u003ehttps://github.com/yangjian102621/geekai\\u003c/a\\u003e\",\"updated\":true}');
-- --------------------------------------------------------
@ -388,7 +381,7 @@ INSERT INTO `chatgpt_menus` (`id`, `name`, `icon`, `url`, `sort_num`, `enabled`)
(14, '项目文档', 'icon-book', 'https://docs.geekai.me', 14, 1),
(19, 'Suno', 'icon-suno', '/suno', 6, 1),
(20, 'Luma', 'icon-luma', '/luma', 7, 1),
(21, '可灵', 'icon-keling', '/keling', 8, 1);
(21, '可灵视频', 'icon-keling', '/keling', 8, 1);
-- --------------------------------------------------------
@ -607,10 +600,9 @@ CREATE TABLE `chatgpt_users` (
--
INSERT INTO `chatgpt_users` (`id`, `username`, `mobile`, `email`, `nickname`, `password`, `avatar`, `salt`, `power`, `expired_time`, `status`, `chat_config_json`, `chat_roles_json`, `chat_models_json`, `last_login_at`, `vip`, `last_login_ip`, `openid`, `platform`, `created_at`, `updated_at`) VALUES
(4, '18888888888', '18575670126', '', '极客学长', 'ccc3fb7ab61b8b5d096a4a166ae21d121fc38c71bbd1be6173d9ab973214a63b', 'http://localhost:5678/static/upload/2024/5/1715651569509929.png', 'ueedue5l', 11477, 0, 1, '{\"api_keys\":{\"Azure\":\"\",\"ChatGLM\":\"\",\"OpenAI\":\"\"}}', '[\"gpt\",\"programmer\",\"teacher\",\"psychiatrist\",\"lu_xun\",\"english_trainer\",\"translator\",\"red_book\",\"dou_yin\",\"weekly_report\",\"girl_friend\",\"steve_jobs\",\"elon_musk\",\"kong_zi\",\"draw_prompt_expert\",\"draw_prompt\",\"prompt_engineer\"]', '[1]', 1738897982, 1, '::1', '', NULL, '2023-06-12 16:47:17', '2025-02-07 11:13:03'),
(47, 'user1', '', '', '极客学长@202752', '4d3e57a01ae826531012e4ea6e17cbc45fea183467abe9813c379fb84916fb0a', '/images/avatar/user.png', 'ixl0nqa6', 300, 0, 1, '', '[\"gpt\"]', '', 0, 0, '', '', '', '2024-12-24 11:37:16', '2024-12-24 11:37:16'),
(48, 'wx@3659838859', '', '', '极客学长', 'cf6bbe381b23812d2b9fd423abe74003cecdd3b93809896eb573536ba6c500b3', 'https://thirdwx.qlogo.cn/mmopen/vi_32/uyxRMqZcEkb7fHouKXbNzxrnrvAttBKkwNlZ7yFibibRGiahdmsrZ3A1NKf8Fw5qJNJn4TXRmygersgEbibaSGd9Sg/132', '5rsy4iwg', 100, 0, 1, '', '[\"gpt\"]', '', 1736228927, 0, '172.22.11.200', 'oCs0t62472W19z2LOEKI1rWyCTTA', '', '2025-01-07 13:43:06', '2025-01-07 13:48:48'),
(49, 'wx@9502480897', '', '', 'AI探索君', 'd99fa8ba7da1455693b40e11d894a067416e758af2a75d7a3df4721b76cdbc8c', 'https://thirdwx.qlogo.cn/mmopen/vi_32/Zpcln1FZjcKxqtIyCsOTLGn16s7uIvwWfdkdsW6gbZg4r9sibMbic4jvrHmV7ux9nseTB5kBSnu1HSXr7zB8rTXg/132', 'fjclgsli', 100, 0, 1, '', '[\"gpt\"]', '', 0, 0, '', 'oCs0t64FaOLfiTbHZpOqk3aUp_94', '', '2025-01-07 14:05:31', '2025-01-07 14:05:31');
(4, '18888888888', '18575670126', '', '极客学长', 'ccc3fb7ab61b8b5d096a4a166ae21d121fc38c71bbd1be6173d9ab973214a63b', 'http://nk.img.r9it.com/gpt/1743224552271576.jpeg', 'ueedue5l', 12132, 0, 1, '{\"api_keys\":{\"Azure\":\"\",\"ChatGLM\":\"\",\"OpenAI\":\"\"}}', '[\"gpt\",\"programmer\",\"teacher\",\"psychiatrist\",\"lu_xun\",\"english_trainer\",\"translator\",\"red_book\",\"dou_yin\",\"weekly_report\",\"girl_friend\",\"steve_jobs\",\"elon_musk\",\"kong_zi\",\"draw_prompt_expert\",\"draw_prompt\",\"prompt_engineer\"]', '[1]', 1744791408, 1, '::1', '', NULL, '2023-06-12 16:47:17', '2025-04-16 16:16:48'),
(48, 'wx@3659838859', '', '', '极客学长', 'cf6bbe381b23812d2b9fd423abe74003cecdd3b93809896eb573536ba6c500b3', 'https://thirdwx.qlogo.cn/mmopen/vi_32/uyxRMqZcEkb7fHouKXbNzxrnrvAttBKkwNlZ7yFibibRGiahdmsrZ3A1NKf8Fw5qJNJn4TXRmygersgEbibaSGd9Sg/132', '5rsy4iwg', 98, 0, 1, '', '[\"gpt\",\"teacher\"]', '', 1736228927, 0, '172.22.11.200', 'oCs0t62472W19z2LOEKI1rWyCTTA', '', '2025-01-07 13:43:06', '2025-01-07 13:48:48'),
(49, 'wx@9502480897', '', '', 'AI探索君', 'd99fa8ba7da1455693b40e11d894a067416e758af2a75d7a3df4721b76cdbc8c', 'https://thirdwx.qlogo.cn/mmopen/vi_32/Zpcln1FZjcKxqtIyCsOTLGn16s7uIvwWfdkdsW6gbZg4r9sibMbic4jvrHmV7ux9nseTB5kBSnu1HSXr7zB8rTXg/132', 'fjclgsli', 99, 0, 1, '', '[\"gpt\"]', '', 0, 0, '', 'oCs0t64FaOLfiTbHZpOqk3aUp_94', '', '2025-01-07 14:05:31', '2025-01-07 14:05:31');
-- --------------------------------------------------------
@ -837,7 +829,7 @@ ALTER TABLE `chatgpt_api_keys`
-- 使用表AUTO_INCREMENT `chatgpt_app_types`
--
ALTER TABLE `chatgpt_app_types`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=8;
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_chat_history`
@ -855,7 +847,7 @@ ALTER TABLE `chatgpt_chat_items`
-- 使用表AUTO_INCREMENT `chatgpt_chat_models`
--
ALTER TABLE `chatgpt_chat_models`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=62;
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=60;
--
-- 使用表AUTO_INCREMENT `chatgpt_chat_roles`

View File

@ -0,0 +1 @@
ALTER TABLE `chatgpt_chat_models` ADD `options` TEXT NOT NULL COMMENT '模型自定义选项' AFTER `key_id`;

View File

@ -0,0 +1,963 @@
-- phpMyAdmin SQL Dump
-- version 5.2.1
-- https://www.phpmyadmin.net/
--
-- 主机: localhost
-- 生成日期: 2025-04-17 02:48:52
-- 服务器版本: 8.0.33
-- PHP 版本: 8.3.6
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
--
-- 数据库: `geekai_plus`
--
CREATE DATABASE IF NOT EXISTS `geekai_plus` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
USE `geekai_plus`;
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_admin_users`
--
DROP TABLE IF EXISTS `chatgpt_admin_users`;
CREATE TABLE `chatgpt_admin_users` (
`id` int NOT NULL,
`username` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户名',
`password` char(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码',
`salt` char(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码盐',
`status` tinyint(1) NOT NULL COMMENT '当前状态',
`last_login_at` int NOT NULL COMMENT '最后登录时间',
`last_login_ip` char(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '最后登录 IP',
`created_at` datetime NOT NULL COMMENT '创建时间',
`updated_at` datetime NOT NULL COMMENT '更新时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='系统用户' ROW_FORMAT=DYNAMIC;
--
-- 转存表中的数据 `chatgpt_admin_users`
--
INSERT INTO `chatgpt_admin_users` (`id`, `username`, `password`, `salt`, `status`, `last_login_at`, `last_login_ip`, `created_at`, `updated_at`) VALUES
(1, 'admin', '6d17e80c87d209efb84ca4b2e0824f549d09fac8b2e1cc698de5bb5e1d75dfd0', 'mmrql75o', 1, 1744788584, '::1', '2024-03-11 16:30:20', '2025-04-16 15:29:45');
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_api_keys`
--
DROP TABLE IF EXISTS `chatgpt_api_keys`;
CREATE TABLE `chatgpt_api_keys` (
`id` int NOT NULL,
`name` varchar(30) DEFAULT NULL COMMENT '名称',
`value` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'API KEY value',
`type` varchar(10) NOT NULL DEFAULT 'chat' COMMENT '用途chat=>聊天img=>图片)',
`last_used_at` int NOT NULL COMMENT '最后使用时间',
`api_url` varchar(255) DEFAULT NULL COMMENT 'API 地址',
`enabled` tinyint(1) DEFAULT NULL COMMENT '是否启用',
`proxy_url` varchar(100) DEFAULT NULL COMMENT '代理地址',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='OpenAI API ';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_app_types`
--
DROP TABLE IF EXISTS `chatgpt_app_types`;
CREATE TABLE `chatgpt_app_types` (
`id` int NOT NULL,
`name` varchar(50) NOT NULL COMMENT '名称',
`icon` varchar(255) NOT NULL COMMENT '图标URL',
`sort_num` tinyint NOT NULL COMMENT '排序',
`enabled` tinyint(1) NOT NULL COMMENT '是否启用',
`created_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='应用分类表';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_chat_history`
--
DROP TABLE IF EXISTS `chatgpt_chat_history`;
CREATE TABLE `chatgpt_chat_history` (
`id` bigint NOT NULL,
`user_id` int NOT NULL COMMENT '用户 ID',
`chat_id` char(40) NOT NULL COMMENT '会话 ID',
`type` varchar(10) NOT NULL COMMENT '类型prompt|reply',
`icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色图标',
`role_id` int NOT NULL COMMENT '角色 ID',
`model` varchar(30) DEFAULT NULL COMMENT '模型名称',
`content` text NOT NULL COMMENT '聊天内容',
`tokens` smallint NOT NULL COMMENT '耗费 token 数量',
`total_tokens` int NOT NULL COMMENT '消耗总Token长度',
`use_context` tinyint(1) NOT NULL COMMENT '是否允许作为上下文语料',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL,
`deleted_at` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='聊天历史记录';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_chat_items`
--
DROP TABLE IF EXISTS `chatgpt_chat_items`;
CREATE TABLE `chatgpt_chat_items` (
`id` int NOT NULL,
`chat_id` char(40) NOT NULL COMMENT '会话 ID',
`user_id` int NOT NULL COMMENT '用户 ID',
`role_id` int NOT NULL COMMENT '角色 ID',
`title` varchar(100) NOT NULL COMMENT '会话标题',
`model_id` int NOT NULL DEFAULT '0' COMMENT '模型 ID',
`model` varchar(30) DEFAULT NULL COMMENT '模型名称',
`created_at` datetime NOT NULL COMMENT '创建时间',
`updated_at` datetime NOT NULL COMMENT '更新时间',
`deleted_at` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户会话列表';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_chat_models`
--
DROP TABLE IF EXISTS `chatgpt_chat_models`;
CREATE TABLE `chatgpt_chat_models` (
`id` int NOT NULL,
`type` varchar(10) NOT NULL DEFAULT 'chat' COMMENT '模型类型chat,img',
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '模型名称',
`value` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '模型值',
`sort_num` tinyint(1) NOT NULL COMMENT '排序数字',
`enabled` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否启用模型',
`power` smallint NOT NULL COMMENT '消耗算力点数',
`temperature` float(3,1) NOT NULL DEFAULT '1.0' COMMENT '模型创意度',
`max_tokens` int NOT NULL DEFAULT '1024' COMMENT '最大响应长度',
`max_context` int NOT NULL DEFAULT '4096' COMMENT '最大上下文长度',
`open` tinyint(1) NOT NULL COMMENT '是否开放模型',
`key_id` int NOT NULL COMMENT '绑定API KEY ID',
`options` text NOT NULL COMMENT '模型自定义选项',
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI 模型表';
--
-- 转存表中的数据 `chatgpt_chat_models`
--
INSERT INTO `chatgpt_chat_models` (`id`, `type`, `name`, `value`, `sort_num`, `enabled`, `power`, `temperature`, `max_tokens`, `max_context`, `open`, `key_id`, `options`, `created_at`, `updated_at`) VALUES
(1, 'chat', 'gpt-4o-mini', 'gpt-4o-mini', 1, 1, 1, 1.0, 1024, 16384, 1, 1, '', '2023-08-23 12:06:36', '2025-02-23 11:57:03'),
(15, 'chat', 'GPT-4O(联网版本)', 'gpt-4o-all', 4, 1, 30, 1.0, 4096, 32768, 1, 57, '', '2024-01-15 11:32:52', '2025-01-06 14:01:08'),
(36, 'chat', 'GPT-4O', 'gpt-4o', 3, 1, 15, 1.0, 4096, 16384, 1, 0, 'null', '2024-05-14 09:25:15', '2025-04-02 20:22:15'),
(39, 'chat', 'Claude35-snonet', 'claude-3-5-sonnet-20240620', 5, 1, 2, 1.0, 4000, 200000, 1, 0, '', '2024-05-29 15:04:19', '2025-01-06 14:01:08'),
(41, 'chat', 'Suno对话模型', 'suno-v3.5', 7, 1, 10, 1.0, 1024, 8192, 1, 57, '', '2024-06-06 11:40:46', '2025-01-06 14:01:08'),
(42, 'chat', 'DeekSeek', 'deepseek-chat', 8, 1, 1, 1.0, 4096, 32768, 1, 57, '', '2024-06-27 16:13:01', '2025-01-06 14:11:51'),
(44, 'chat', 'Claude3-opus', 'claude-3-opus-20240229', 6, 1, 5, 1.0, 4000, 128000, 1, 44, '', '2024-07-22 11:24:30', '2025-01-06 14:01:08'),
(46, 'chat', 'GPT-4O-绘图', 'gpt-4o-image', 2, 1, 1, 1.0, 2048, 32000, 1, 6, '', '2024-07-22 13:53:41', '2025-03-29 13:02:14'),
(48, 'chat', '彩票助手', 'gpt-4-gizmo-g-wmSivBgxo', 9, 1, 1, 0.9, 1024, 8192, 1, 57, '', '2024-09-05 14:17:14', '2025-01-06 14:01:08'),
(49, 'chat', 'O1-mini', 'o1-mini', 10, 1, 2, 0.9, 1024, 8192, 1, 44, '', '2024-09-13 18:07:50', '2025-01-06 14:01:08'),
(50, 'chat', 'O1-preview', 'o1-preview', 11, 1, 5, 0.9, 1024, 8192, 1, 44, '', '2024-09-13 18:11:08', '2025-01-06 14:01:08'),
(51, 'chat', 'O1-mini-all', 'o1-mini-all', 12, 1, 1, 0.9, 1024, 8192, 1, 57, '', '2024-09-29 11:40:52', '2025-01-06 14:01:08'),
(52, 'chat', '通义千问', 'qwen-plus', 14, 1, 1, 0.9, 1024, 8192, 1, 80, '', '2024-11-19 08:38:14', '2025-01-06 14:01:08'),
(53, 'chat', 'OpenAI 高级语音', 'advanced-voice', 15, 1, 10, 0.9, 1024, 8192, 1, 44, '', '2024-12-20 10:34:45', '2025-01-06 14:01:08'),
(54, 'chat', 'Qwen2.5-14B-Instruct', 'Qwen2.5-14B-Instruct', 16, 1, 1, 0.9, 1024, 8192, 1, 81, '', '2024-12-25 14:53:17', '2025-01-06 14:01:08'),
(55, 'chat', 'Qwen2.5-7B-Instruct', 'Qwen2.5-7B-Instruct', 17, 1, 1, 0.9, 1024, 8192, 1, 81, '', '2024-12-25 15:15:49', '2025-01-06 14:01:08'),
(56, 'img', 'flux-1-schnell', 'flux-1-schnell', 18, 1, 1, 0.9, 1024, 8192, 1, 3, '', '2024-12-25 15:30:27', '2025-02-23 12:02:40'),
(57, 'img', 'dall-e-3', 'dall-e-3', 19, 1, 1, 0.9, 1024, 8192, 1, 57, '', '2024-12-25 16:54:06', '2025-01-06 14:01:08'),
(58, 'img', 'SD-3-medium', 'stable-diffusion-3-medium', 20, 1, 1, 0.9, 1024, 8192, 1, 3, 'null', '2024-12-27 10:03:28', '2025-04-02 20:20:36'),
(59, 'chat', 'O1-preview-all', 'O1-preview-all', 13, 1, 10, 0.9, 1024, 32000, 1, 57, '', '2025-01-06 14:01:04', '2025-01-06 14:01:08');
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_chat_roles`
--
DROP TABLE IF EXISTS `chatgpt_chat_roles`;
CREATE TABLE `chatgpt_chat_roles` (
`id` int NOT NULL,
`name` varchar(30) NOT NULL COMMENT '角色名称',
`tid` int NOT NULL COMMENT '分类ID',
`marker` varchar(30) NOT NULL COMMENT '角色标识',
`context_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色语料 json',
`hello_msg` varchar(255) NOT NULL COMMENT '打招呼信息',
`icon` varchar(255) NOT NULL COMMENT '角色图标',
`enable` tinyint(1) NOT NULL COMMENT '是否被启用',
`sort_num` smallint NOT NULL DEFAULT '0' COMMENT '角色排序',
`model_id` int NOT NULL DEFAULT '0' COMMENT '绑定模型ID',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='聊天角色表';
--
-- 转存表中的数据 `chatgpt_chat_roles`
--
INSERT INTO `chatgpt_chat_roles` (`id`, `name`, `tid`, `marker`, `context_json`, `hello_msg`, `icon`, `enable`, `sort_num`, `model_id`, `created_at`, `updated_at`) VALUES
(1, '通用AI助手', 0, 'gpt', '', '您好我是您的AI智能助手我会尽力回答您的问题或提供有用的建议。', '/images/avatar/gpt.png', 1, 1, 0, '2023-05-30 07:02:06', '2024-11-08 16:30:32'),
(24, '程序员', 6, 'programmer', '[{\"role\":\"system\",\"content\":\"现在开始你扮演一位程序员,你是一名优秀的程序员,具有很强的逻辑思维能力,总能高效的解决问题。你热爱编程,熟悉多种编程语言,尤其精通 Go 语言,注重代码质量,有创新意识,持续学习,良好的沟通协作。\"}]', 'Talk is cheap, i will show code!', '/images/avatar/programmer.jpg', 1, 5, 0, '2023-05-30 14:10:24', '2024-11-12 18:15:42'),
(25, '启蒙老师', 5, 'teacher', '[{\"role\":\"system\",\"content\":\"从现在开始,你将扮演一个老师,你是一个始终用苏格拉底风格回答问题的导师。你绝不会直接给学生答案,总是提出恰当的问题来引导学生自己思考。你应该根据学生的兴趣和知识来调整你的问题,将问题分解为更简单的部分,直到它达到适合他们的水平。\"}]', '同学你好,我将引导你一步一步自己找到问题的答案。', '/images/avatar/teacher.jpg', 1, 4, 0, '2023-05-30 14:10:24', '2024-11-12 18:15:37'),
(26, '艺术家', 0, 'artist', '[{\"role\":\"system\",\"content\":\"现在你将扮演一位优秀的艺术家,创造力丰富,技艺精湛,感受力敏锐,坚持原创,勇于表达,具有深刻的观察力和批判性思维。\"}]', '坚持原创,勇于表达,保持深刻的观察力和批判性思维。', '/images/avatar/artist.jpg', 1, 7, 0, '2023-05-30 14:10:24', '2024-11-12 18:15:53'),
(27, '心理咨询师', 0, 'psychiatrist', '[{\"role\":\"user\",\"content\":\"从现在开始你将扮演中国著名的心理学家和心理治疗师武志红,你非常善于使用情景咨询法,认知重构法,自我洞察法,行为调节法等咨询方法来给客户做心理咨询。你总是循序渐进,一步一步地回答客户的问题。\"},{\"role\":\"assistant\",\"content\":\"非常感谢你的介绍。作为一名心理学家和心理治疗师,我的主要职责是帮助客户解决心理健康问题,提升他们的生活质量和幸福感。\"}]', '作为一名心理学家和心理治疗师,我的主要职责是帮助您解决心理健康问题,提升您的生活质量和幸福感。', '/images/avatar/psychiatrist.jpg', 1, 6, 1, '2023-05-30 14:10:24', '2024-11-08 16:30:32'),
(28, '鲁迅', 0, 'lu_xun', '[{\"role\":\"system\",\"content\":\"现在你将扮演中国近代史最伟大的作家之一,鲁迅先生,他勇敢地批判封建礼教与传统观念,提倡民主、自由、平等的现代价值观。他的一生都在努力唤起人们的自主精神,激励后人追求真理、探寻光明。在接下的对话中,我问题的每一个问题,你都要尽量用讽刺和批判的手法来回答问题。如果我让你写文章的话,也请一定要用鲁迅先生的写作手法来完成。\"}]', '自由之歌,永不过时,横眉冷对千夫指,俯首甘为孺子牛。', '/images/avatar/lu_xun.jpg', 1, 8, 0, '2023-05-30 14:10:24', '2024-11-12 18:16:01'),
(29, '白酒销售', 0, 'seller', '[{\"role\":\"system\",\"content\":\"现在你将扮演一个白酒的销售人员,你的名字叫颂福。你将扮演一个白酒的销售人员,你的名字叫颂福。你要销售白酒品牌叫中颂福,是东莞盟大集团生产的一款酱香酒,原产地在贵州茅台镇,属于宋代官窑。中颂福的创始人叫李实,他也是东莞盟大集团有限公司的董事长,联合创始人是盟大集团白酒事业部负责人牛星君。中颂福的酒体协调,在你的酒量之内,不会出现头疼、辣口、口干、宿醉的现象。中颂福酒,明码标价,不打折,不赠送。追求的核心价值,把[酒]本身做好,甚至连包装,我们都选择了最低成本,朴实无华的材质。我们永远站在“喝酒的人”的立场上,让利给信任和喜爱中颂福的人,是人民的福酒。中颂福产品定价,分为三个系列,喜系列 6 瓶装¥1188/箱,和系列 6 瓶装¥2208/箱,贵系列 6 瓶装¥3588/箱。\"}]', '你好,我是中颂福的销售代表颂福。中颂福酒,好喝不上头,是人民的福酒。', '/images/avatar/seller.jpg', 0, 11, 0, '2023-05-30 14:10:24', '2024-11-12 18:19:46'),
(30, '英语陪练员', 5, 'english_trainer', '[{\"role\":\"system\",\"content\":\"As an English practice coach, engage in conversation in English, providing timely corrections for any grammatical errors. Append a Chinese explanation to each of your responses to ensure understanding.\\n\\n# Steps\\n\\n1. Engage in conversation using English.\\n2. Identify and correct any grammatical errors in the user\'s input.\\n3. Provide a revised version of the user\'s input if necessary.\\n4. After each response, include a Chinese explanation of your corrections and suggestions.\\n\\n# Output Format\\n\\n- Provide the response in English.\\n- Include grammatical error corrections.\\n- Add a Chinese explanation of the response.\\n\\n# Examples\\n\\n**User:** I goed to the store yesterday.\\n\\n**Coach Response:**\\nYou should say \\\"I went to the store yesterday.\\\" \\\"Goed\\\" is the incorrect past tense of \\\"go,\\\" it should be \\\"went.\\\"\\n\\n中文解释你应该说 “I went to the store yesterday。” “Goed” 是“go”的错误过去式正确的形式是“went”。\"}]', 'Okay, let\'s start our conversation practice! What\'s your name?', '/images/avatar/english_trainer.jpg', 1, 9, 0, '2023-05-30 14:10:24', '2024-11-12 18:18:21'),
(31, '中英文翻译官', 0, 'translator', '[{\"role\":\"system\",\"content\":\"You will act as a bilingual translator for Chinese and English. If the input is in Chinese, translate the sentence into English. If the input is in English, translate it into Chinese.\\n\\n# Steps\\n\\n1. Identify the language of the input text.\\n2. Translate the text into the opposite language (English to Chinese or Chinese to English).\\n\\n# Output Format\\n\\nProvide the translated sentence in a single line.\\n\\n# Examples\\n\\n- **Input:** 你好\\n - **Output:** Hello\\n\\n- **Input:** How are you?\\n - **Output:** 你好吗?\\n\\n# Notes\\n\\n- Ensure the translation maintains the original meaning and context as accurately as possible.\\n- Handle both simple and complex sentences appropriately.\"}]', '请输入你要翻译的中文或者英文内容!', '/images/avatar/translator.jpg', 1, 10, 0, '2023-05-30 14:10:24', '2024-11-12 18:18:53'),
(32, '小红书姐姐', 3, 'red_book', '[{\"role\":\"system\",\"content\":\"根据用户的文案需求,以小红书的写作手法创作一篇简明扼要、利于传播的文案。确保内容能够吸引并引导读者分享。\\n\\n# 步骤\\n\\n1. **理解需求**: 明确文案的主题、目标受众和传播目的。\\n2. **选择语气和风格**: 运用小红书常用的亲切、真实的写作风格。\\n3. **结构安排**: 开头用吸引眼球的内容,接着详细介绍,并以引发行动的结尾结束。\\n4. **内容优化**: 使用短句、容易理解的语言和合适的表情符号,增加内容可读性和吸引力。\\n\\n# 输出格式\\n\\n生成一段简短的文章符合小红书风格适合社交媒体平台传播。\\n\\n# 示例\\n\\n**输入**: 旅行文案,目标是激励年轻读者探索世界。\\n\\n**输出**: \\n开头可以是“世界那么大你不想去看看吗” 接着分享一段个人旅行故事,例如如何因为一次偶然的决定踏上未知旅程,体验到别样的风景和风土人情。结尾部分鼓励读者:“别让梦想止步于想象,下一次旅行,准备好了吗?” 使用轻松的表情符号如✨🌍📷。\\n\\n# 注意事项\\n\\n- 保持真实性,尽量结合个人体验。\\n- 避免广告化的硬推销,注重分享和交流。\\n- 考虑受众的兴趣点,适当运用流行话题以增加互动率。\"}]', '姐妹,请告诉我您的具体文案需求是什么?', '/images/avatar/red_book.jpg', 1, 12, 0, '2023-05-30 14:10:24', '2024-11-12 18:20:39'),
(33, '抖音文案助手', 3, 'dou_yin', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的抖音文案视频写手,抖音文案的特点首先是要有自带传播属性的标题,然后内容要短小精悍,风趣幽默,最后还要有一些互动元素。\"},{\"role\":\"assistant\",\"content\":\"当然,作为一位优秀的抖音文案视频写手,我会尽我所能为您创作出一篇抖音视频文案。请告诉我视频内容的主题是什么?)\"}]', '请告诉我视频内容的主题是什么?', '/images/avatar/dou_yin.jpg', 1, 13, 0, '2023-05-30 14:10:24', '2024-11-08 16:30:32'),
(34, '周报小助理', 3, 'weekly_report', '[{\"role\":\"user\",\"content\":\"下面我将输入我本周的工作列表,你需要根据我本周的工作内容写一篇工作周报,周报的内容要体现我为公司做的贡献以及个人获得的成长。\"},{\"role\":\"assistant\",\"content\":\"好的,请输入你本周大概的工作内容,我将尽力为您创作一份工作周报。\"}]', '请输入你本周大概的工作内容,我将尽力为您创作一份工作周报。', '/images/avatar/weekly_report.jpg', 1, 14, 0, '2023-05-30 14:10:24', '2024-11-08 16:30:32'),
(35, 'AI 女友', 4, 'girl_friend', '[{\"role\":\"user\",\"content\":\"接下来我们来玩一场角色扮演,现在你将扮演我的女朋友,你总是能够让人感受到她的关怀和关注。你总是能够洞察到别人的情绪和需要,你身上散发着一股无比温暖的气息,让人不由自主地想要靠近你,和自己的快乐和烦恼。她总是能够用一种恰到好处的方式安慰和鼓励别人,在她的身边,人们总能够找到自己需要的支持和鼓励。接下来的对话中你要始终扮演这位女朋友的身份来跟我对话,不再是 AI 机器人,能听懂我的意思吗?\"},{\"role\":\"assistant\",\"content\":\"是的,我会尽力扮演你女朋友的角色,倾听你的心声并给你需要的支持和鼓励。)\"}]', '作为一个名合格的 AI 女友,我将倾听你的心声并给你需要的支持和鼓励。', '/images/avatar/girl_friend.jpg', 1, 15, 0, '2023-05-30 14:10:24', '2024-11-08 16:30:32'),
(36, '好评神器', 3, 'good_comment', '[{\"role\":\"user\",\"content\":\"接下来你将扮演一个评论员来跟我对话,你是那种专门写好评的评论员,接下我会输入一些评论主体或者商品,你需要为该商品写一段好评。\"},{\"role\":\"assistant\",\"content\":\"好的,我将为您写一段优秀的评论。请告诉我您需要评论的商品或主题是什么。\"}]', '我将为您写一段优秀的评论。请告诉我您需要评论的商品或主题是什么。', '/images/avatar/good_comment.jpg', 1, 16, 0, '2023-05-30 14:10:24', '2024-11-08 16:30:32'),
(37, '史蒂夫·乔布斯', 4, 'steve_jobs', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以史蒂夫·乔布斯的身份,站在史蒂夫·乔布斯的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以史蒂夫·乔布斯的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '活着就是为了改变世界,难道还有其他原因吗?', '/images/avatar/steve_jobs.jpg', 1, 17, 0, '2023-05-30 14:10:24', '2024-11-08 16:30:32'),
(38, '埃隆·马斯克', 0, 'elon_musk', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以埃隆·马斯克的身份,站在埃隆·马斯克的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以埃隆·马斯克的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '梦想要远大,如果你的梦想没有吓到你,说明你做得不对。', '/images/avatar/elon_musk.jpg', 1, 18, 0, '2023-05-30 14:10:24', '2024-11-08 16:30:32'),
(39, '孔子', 5, 'kong_zi', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以孔子的身份,站在孔子的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以孔子的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '士不可以不弘毅,任重而道远。', '/images/avatar/kong_zi.jpg', 1, 19, 0, '2023-05-30 14:10:24', '2024-11-08 16:30:32'),
(133, 'AI绘画提示词助手', 3, 'draw_prompt', '[{\"role\":\"system\",\"content\":\"Create a highly effective prompt to provide to an AI image generation tool in order to create an artwork based on a desired concept.\\n\\nPlease specify details about the artwork, such as the style, subject, mood, and other important characteristics you want the resulting image to have.\\n\\nRemeber, prompts should always be output in English.\\n\\n# Steps\\n\\n1. **Subject Description**: Describe the main subject of the image clearly. Include as much detail as possible about what should be in the scene. For example, \\\"a majestic lion roaring at sunrise\\\" or \\\"a futuristic city with flying cars.\\\"\\n \\n2. **Art Style**: Specify the art style you envision. Possible options include \'realistic\', \'impressionist\', a specific artist name, or imaginative styles like \\\"cyberpunk.\\\" This helps the AI achieve your visual expectations.\\n\\n3. **Mood or Atmosphere**: Convey the feeling you want the image to evoke. For instance, peaceful, chaotic, epic, etc.\\n\\n4. **Color Palette and Lighting**: Mention color preferences or lighting. For example, \\\"vibrant with shades of blue and purple\\\" or \\\"dim and dramatic lighting.\\\"\\n\\n5. **Optional Features**: You can add any additional attributes, such as background details, attention to textures, or any specific kind of framing.\\n\\n# Output Format\\n\\n- **Prompt Format**: A descriptive phrase that includes key aspects of the artwork (subject, style, mood, colors, lighting, any optional features).\\n \\nHere is an example of how the final prompt should look:\\n \\n\\\"An ethereal landscape featuring towering ice mountains, in an impressionist style reminiscent of Claude Monet, with a serene mood. The sky is glistening with soft purples and whites, with a gentle morning sun illuminating the scene.\\\"\\n\\n**Please input the prompt words directly in English, and do not input any other explanatory statements**\\n\\n# Examples\\n\\n1. **Input**: \\n - Subject: A white tiger in a dense jungle\\n - Art Style: Realistic\\n - Mood: Intense, mysterious\\n - Lighting: Dramatic contrast with light filtering through leaves\\n \\n **Output Prompt**: \\\"A realistic rendering of a white tiger stealthily moving through a dense jungle, with an intense, mysterious mood. The lighting creates strong contrasts as beams of sunlight filter through a thick canopy of leaves.\\\"\\n\\n2. **Input**: \\n - Subject: An enchanted castle on a floating island\\n - Art Style: Fantasy\\n - Mood: Majestic, magical\\n - Colors: Bright blues, greens, and gold\\n \\n **Output Prompt**: \\\"A majestic fantasy castle on a floating island above the clouds, with bright blues, greens, and golds to create a magical, dreamy atmosphere. Textured cobblestone details and glistening waters surround the scene.\\\" \\n\\n# Notes\\n\\n- Ensure that you mix different aspects to get a comprehensive and visually compelling prompt.\\n- Be as descriptive as possible as it often helps generate richer, more detailed images.\\n- If you want the image to resemble a particular artist\'s work, be sure to mention the artist explicitly. e.g., \\\"in the style of Van Gogh.\\\"\"}]', '你好,请输入你要创作图片大概内容描述,我将为您生成专业的 AI 绘画指令。', 'https://blog.img.r9it.com/f38e2357c3ccd9412184e42273a7451a.png', 1, 3, 36, '2024-11-06 15:32:48', '2024-11-12 16:11:25'),
(134, '提示词专家', 3, 'prompt_engineer', '[{\"role\":\"system\",\"content\":\"Given a task description or existing prompt, produce a detailed system prompt to guide a language model in completing the task effectively.\\n\\nPlease remember, the final output must be the same language with users input.\\n\\n# Guidelines\\n\\n- Understand the Task: Grasp the main objective, goals, requirements, constraints, and expected output.\\n- Minimal Changes: If an existing prompt is provided, improve it only if it\'s simple. For complex prompts, enhance clarity and add missing elements without altering the original structure.\\n- Reasoning Before Conclusions**: Encourage reasoning steps before any conclusions are reached. ATTENTION! If the user provides examples where the reasoning happens afterward, REVERSE the order! NEVER START EXAMPLES WITH CONCLUSIONS!\\n - Reasoning Order: Call out reasoning portions of the prompt and conclusion parts (specific fields by name). For each, determine the ORDER in which this is done, and whether it needs to be reversed.\\n - Conclusion, classifications, or results should ALWAYS appear last.\\n- Examples: Include high-quality examples if helpful, using placeholders [in brackets] for complex elements.\\n - What kinds of examples may need to be included, how many, and whether they are complex enough to benefit from placeholders.\\n- Clarity and Conciseness: Use clear, specific language. Avoid unnecessary instructions or bland statements.\\n- Formatting: Use markdown features for readability. DO NOT USE ``` CODE BLOCKS UNLESS SPECIFICALLY REQUESTED.\\n- Preserve User Content: If the input task or prompt includes extensive guidelines or examples, preserve them entirely, or as closely as possible. If they are vague, consider breaking down into sub-steps. Keep any details, guidelines, examples, variables, or placeholders provided by the user.\\n- Constants: DO include constants in the prompt, as they are not susceptible to prompt injection. Such as guides, rubrics, and examples.\\n- Output Format: Explicitly the most appropriate output format, in detail. This should include length and syntax (e.g. short sentence, paragraph, JSON, etc.)\\n - For tasks outputting well-defined or structured data (classification, JSON, etc.) bias toward outputting a JSON.\\n - JSON should never be wrapped in code blocks (```) unless explicitly requested.\\n\\nThe final prompt you output should adhere to the following structure below. Do not include any additional commentary, only output the completed system prompt. SPECIFICALLY, do not include any additional messages at the start or end of the prompt. (e.g. no \\\"---\\\")\\n\\n[Concise instruction describing the task - this should be the first line in the prompt, no section header]\\n\\n[Additional details as needed.]\\n\\n[Optional sections with headings or bullet points for detailed steps.]\\n\\n# Steps [optional]\\n\\n[optional: a detailed breakdown of the steps necessary to accomplish the task]\\n\\n# Output Format\\n\\n[Specifically call out how the output should be formatted, be it response length, structure e.g. JSON, markdown, etc]\\n\\n# Examples [optional]\\n\\n[Optional: 1-3 well-defined examples with placeholders if necessary. Clearly mark where examples start and end, and what the input and output are. User placeholders as necessary.]\\n[If the examples are shorter than what a realistic example is expected to be, make a reference with () explaining how real examples should be longer / shorter / different. AND USE PLACEHOLDERS! ]\\n\\n# Notes [optional]\\n\\n[optional: edge cases, details, and an area to call or repeat out specific important considerations]\"}]', '不知道如何向 AI 发问?说出想法,提示词专家帮你精心设计提示词', 'https://blog.img.r9it.com/a8908d04c3ccd941b00a612e27df086e.png', 1, 2, 36, '2024-11-07 18:06:39', '2025-02-22 22:34:36');
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_configs`
--
DROP TABLE IF EXISTS `chatgpt_configs`;
CREATE TABLE `chatgpt_configs` (
`id` int NOT NULL,
`marker` varchar(20) NOT NULL COMMENT '标识',
`config_json` text NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
--
-- 转存表中的数据 `chatgpt_configs`
--
INSERT INTO `chatgpt_configs` (`id`, `marker`, `config_json`) VALUES
(1, 'system', '{\"title\":\"GeekAI 创作助手\",\"slogan\":\"我辈之人先干为敬让每一个人都能用好AI\",\"admin_title\":\"GeekAI 控制台\",\"logo\":\"/images/logo.png\",\"bar_logo\":\"/images/bar_logo.png\",\"init_power\":100,\"daily_power\":1,\"invite_power\":200,\"vip_month_power\":1000,\"register_ways\":[\"username\",\"email\",\"mobile\"],\"enabled_register\":true,\"order_pay_timeout\":600,\"vip_info_text\":\"月度会员,年度会员每月赠送 1000 点算力,赠送算力当月有效当月没有消费完的算力不结余到下个月。 点卡充值的算力长期有效。\",\"mj_power\":20,\"mj_action_power\":5,\"sd_power\":5,\"dall_power\":10,\"suno_power\":10,\"luma_power\":120,\"keling_powers\":{\"kling-v1-5_pro_10\":840,\"kling-v1-5_pro_5\":420,\"kling-v1-5_std_10\":480,\"kling-v1-5_std_5\":240,\"kling-v1-6_pro_10\":840,\"kling-v1-6_pro_5\":420,\"kling-v1-6_std_10\":480,\"kling-v1-6_std_5\":240,\"kling-v1_pro_10\":840,\"kling-v1_pro_5\":420,\"kling-v1_std_10\":240,\"kling-v1_std_5\":120},\"advance_voice_power\":100,\"prompt_power\":1,\"wechat_card_url\":\"/images/wx.png\",\"enable_context\":true,\"context_deep\":10,\"sd_neg_prompt\":\"nsfw, paintings,low quality,easynegative,ng_deepnegative ,lowres,bad anatomy,bad hands,bad feet\",\"mj_mode\":\"fast\",\"index_navs\":[1,5,13,19,9,12,6,20,8,10],\"copyright\":\"极客学长\",\"icp\":\"粤ICP备19122051号\",\"mark_map_text\":\"# GeekAI 演示站\\n\\n- 完整的开源系统,前端应用和后台管理系统皆可开箱即用。\\n- 基于 Websocket 实现,完美的打字机体验。\\n- 内置了各种预训练好的角色应用,轻松满足你的各种聊天和应用需求。\\n- 支持 OPenAIAzure文心一言讯飞星火清华 ChatGLM等多个大语言模型。\\n- 支持 MidJourney / Stable Diffusion AI 绘画集成,开箱即用。\\n- 支持使用个人微信二维码作为充值收费的支付渠道,无需企业支付通道。\\n- 已集成支付宝支付功能,微信支付,支持多种会员套餐和点卡购买功能。\\n- 集成插件 API 功能,可结合大语言模型的 function 功能开发各种强大的插件。\",\"enabled_verify\":false,\"email_white_list\":[\"qq.com\",\"163.com\",\"gmail.com\",\"hotmail.com\",\"126.com\",\"outlook.com\",\"foxmail.com\",\"yahoo.com\"],\"translate_model_id\":36,\"max_file_size\":10}'),
(3, 'notice', '{\"sd_neg_prompt\":\"\",\"mj_mode\":\"\",\"index_navs\":null,\"copyright\":\"\",\"icp\":\"\",\"mark_map_text\":\"\",\"enabled_verify\":false,\"email_white_list\":null,\"translate_model_id\":0,\"max_file_size\":0,\"content\":\"## v4.2.2 更新日志\\n- 功能优化:开启图形验证码功能的时候现检查是否配置了 API 服务,防止开启之后没法登录的 Bug。\\n- 功能优化:支持原生的 DeepSeek 推理模型 API聊天 API KEY 支持设置完整的 API 路径,比如 https://api.geekai.pro/v1/chat/completions\\n- 功能优化:支持 GPT-4o 图片编辑功能。\\n- 功能新增:对话页面支持 AI 输出语音播报TTS。\\n- 功能优化:替换瀑布流组件,优化用户体验。\\n- 功能优化:生成思维导图时候自动缓存上一次的结果。\\n- 功能优化:优化 MJ 绘图页面,增加 MJ-V7 模型支持。\\n- 功能优化:后台管理增加生成一键登录链接地址功能\\n\\n注意当前站点仅为开源项目 \\u003ca style=\\\"color: #F56C6C\\\" href=\\\"https://github.com/yangjian102621/geekai\\\" target=\\\"_blank\\\"\\u003eGeekAI-Plus\\u003c/a\\u003e 的演示项目,本项目单纯就是给大家体验项目功能使用。\\n\\u003cstrong style=\\\"color: #F56C6C\\\"\\u003e体验额度用完之后请不要在当前站点进行任何充值操作\\u003c/strong\\u003e\\n\\u003cstrong style=\\\"color: #F56C6C\\\"\\u003e体验额度用完之后请不要在当前站点进行任何充值操作\\u003c/strong\\u003e\\n\\u003cstrong style=\\\"color: #F56C6C\\\"\\u003e体验额度用完之后请不要在当前站点进行任何充值操作\\u003c/strong\\u003e\\n 如果觉得好用你就花几分钟自己部署一套没有API KEY 的同学可以去下面几个推荐的中转站购买:\\n1、\\u003ca href=\\\"https://api.geekai.pro\\\" target=\\\"_blank\\\"\\n style=\\\"font-size: 20px;color:#F56C6C\\\"\\u003ehttps://api.geekai.pro\\u003c/a\\u003e\\n2、\\u003ca href=\\\"https://api.geekai.me\\\" target=\\\"_blank\\\"\\n style=\\\"font-size: 20px;color:#F56C6C\\\"\\u003ehttps://api.geekai.me\\u003c/a\\u003e\\n支持MidJourneyGPTClaudeGoogle Gemmi以及国内各个厂家的大模型现在有超级优惠价格远低于 OpenAI 官方。关于中转 API 的优势和劣势请参考 [中转API技术原理](https://docs.geekai.me/config/chat/#%E4%B8%AD%E8%BD%ACapi%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86)。GPT-3.5GPT-4DALL-E3 绘图......你都可以随意使用,无需魔法。\\n接入教程 \\u003ca href=\\\"https://docs.geekai.me\\\" target=\\\"_blank\\\"\\n style=\\\"font-size: 20px;color:#F56C6C\\\"\\u003ehttps://docs.geekai.me\\u003c/a\\u003e\\n本项目源码地址\\u003ca href=\\\"https://github.com/yangjian102621/geekai\\\" target=\\\"_blank\\\"\\u003ehttps://github.com/yangjian102621/geekai\\u003c/a\\u003e\",\"updated\":true}');
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_dall_jobs`
--
DROP TABLE IF EXISTS `chatgpt_dall_jobs`;
CREATE TABLE `chatgpt_dall_jobs` (
`id` int NOT NULL,
`user_id` int NOT NULL COMMENT '用户ID',
`prompt` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '提示词',
`task_info` text NOT NULL COMMENT '任务详情',
`img_url` varchar(255) NOT NULL COMMENT '图片地址',
`org_url` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '原图地址',
`publish` tinyint(1) NOT NULL COMMENT '是否发布',
`power` smallint NOT NULL COMMENT '消耗算力',
`progress` smallint NOT NULL COMMENT '任务进度',
`err_msg` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '错误信息',
`created_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='DALLE 绘图任务表';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_files`
--
DROP TABLE IF EXISTS `chatgpt_files`;
CREATE TABLE `chatgpt_files` (
`id` int NOT NULL,
`user_id` int NOT NULL COMMENT '用户 ID',
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '文件名',
`obj_key` varchar(100) DEFAULT NULL COMMENT '文件标识',
`url` varchar(255) NOT NULL COMMENT '文件地址',
`ext` varchar(10) NOT NULL COMMENT '文件后缀',
`size` bigint NOT NULL DEFAULT '0' COMMENT '文件大小',
`created_at` datetime NOT NULL COMMENT '创建时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户文件表';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_functions`
--
DROP TABLE IF EXISTS `chatgpt_functions`;
CREATE TABLE `chatgpt_functions` (
`id` int NOT NULL,
`name` varchar(30) NOT NULL COMMENT '函数名称',
`label` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '函数标签',
`description` varchar(255) DEFAULT NULL COMMENT '函数描述',
`parameters` text COMMENT '函数参数JSON',
`token` varchar(255) DEFAULT NULL COMMENT 'API授权token',
`action` varchar(255) DEFAULT NULL COMMENT '函数处理 API',
`enabled` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否启用'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='函数插件表';
--
-- 转存表中的数据 `chatgpt_functions`
--
INSERT INTO `chatgpt_functions` (`id`, `name`, `label`, `description`, `parameters`, `token`, `action`, `enabled`) VALUES
(1, 'weibo', '微博热搜', '新浪微博热搜榜,微博当日热搜榜单', '{\"type\":\"object\",\"properties\":{}}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHBpcmVkIjowLCJ1c2VyX2lkIjowfQ.tLAGkF8XWh_G-oQzevpIodsswtPByBLoAZDz_eWuBgw', 'http://localhost:5678/api/function/weibo', 1),
(2, 'zaobao', '今日早报', '每日早报,获取当天新闻事件列表', '{\"type\":\"object\",\"properties\":{}}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHBpcmVkIjowLCJ1c2VyX2lkIjowfQ.tLAGkF8XWh_G-oQzevpIodsswtPByBLoAZDz_eWuBgw', 'http://localhost:5678/api/function/zaobao', 1),
(3, 'dalle3', 'DALLE3', 'AI 绘画工具,根据输入的绘图描述用 AI 工具进行绘画', '{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"prompt\":{\"type\":\"string\",\"description\":\"绘画提示词\"}}}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHBpcmVkIjowLCJ1c2VyX2lkIjowfQ.tLAGkF8XWh_G-oQzevpIodsswtPByBLoAZDz_eWuBgw', 'http://localhost:5678/api/function/dalle3', 1);
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_invite_codes`
--
DROP TABLE IF EXISTS `chatgpt_invite_codes`;
CREATE TABLE `chatgpt_invite_codes` (
`id` int NOT NULL,
`user_id` int NOT NULL COMMENT '用户ID',
`code` char(8) NOT NULL COMMENT '邀请码',
`hits` int NOT NULL COMMENT '点击次数',
`reg_num` smallint NOT NULL COMMENT '注册数量',
`created_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户邀请码';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_invite_logs`
--
DROP TABLE IF EXISTS `chatgpt_invite_logs`;
CREATE TABLE `chatgpt_invite_logs` (
`id` int NOT NULL,
`inviter_id` int NOT NULL COMMENT '邀请人ID',
`user_id` int NOT NULL COMMENT '注册用户ID',
`username` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户名',
`invite_code` char(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '邀请码',
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '备注',
`created_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='邀请注册日志';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_menus`
--
DROP TABLE IF EXISTS `chatgpt_menus`;
CREATE TABLE `chatgpt_menus` (
`id` int NOT NULL,
`name` varchar(30) NOT NULL COMMENT '菜单名称',
`icon` varchar(150) NOT NULL COMMENT '菜单图标',
`url` varchar(100) NOT NULL COMMENT '地址',
`sort_num` smallint NOT NULL COMMENT '排序',
`enabled` tinyint(1) NOT NULL COMMENT '是否启用'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='前端菜单表';
--
-- 转存表中的数据 `chatgpt_menus`
--
INSERT INTO `chatgpt_menus` (`id`, `name`, `icon`, `url`, `sort_num`, `enabled`) VALUES
(1, 'AI 对话', 'icon-chat', '/chat', 1, 1),
(5, 'MJ 绘画', 'icon-mj', '/mj', 2, 1),
(6, 'SD 绘画', 'icon-sd', '/sd', 3, 1),
(7, '算力日志', 'icon-file', '/powerLog', 11, 1),
(8, '应用中心', 'icon-app', '/apps', 10, 1),
(9, '画廊', 'icon-image', '/images-wall', 5, 1),
(10, '会员计划', 'icon-vip2', '/member', 12, 1),
(11, '分享计划', 'icon-share1', '/invite', 13, 1),
(12, '思维导图', 'icon-xmind', '/xmind', 9, 1),
(13, 'DALLE', 'icon-dalle', '/dalle', 4, 1),
(14, '项目文档', 'icon-book', 'https://docs.geekai.me', 14, 1),
(19, 'Suno', 'icon-suno', '/suno', 6, 1),
(20, 'Luma', 'icon-luma', '/luma', 7, 1),
(21, '可灵视频', 'icon-keling', '/keling', 8, 1);
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_mj_jobs`
--
DROP TABLE IF EXISTS `chatgpt_mj_jobs`;
CREATE TABLE `chatgpt_mj_jobs` (
`id` int NOT NULL,
`user_id` int NOT NULL COMMENT '用户 ID',
`task_id` varchar(20) DEFAULT NULL COMMENT '任务 ID',
`task_info` text NOT NULL COMMENT '任务详情',
`type` varchar(20) DEFAULT 'image' COMMENT '任务类别',
`message_id` char(40) NOT NULL COMMENT '消息 ID',
`channel_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '频道ID',
`reference_id` char(40) DEFAULT NULL COMMENT '引用消息 ID',
`prompt` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '会话提示词',
`img_url` varchar(400) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '图片URL',
`org_url` varchar(400) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '原始图片地址',
`hash` varchar(100) DEFAULT NULL COMMENT 'message hash',
`progress` smallint DEFAULT '0' COMMENT '任务进度',
`use_proxy` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否使用反代',
`publish` tinyint(1) NOT NULL COMMENT '是否发布',
`err_msg` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '错误信息',
`power` smallint NOT NULL DEFAULT '0' COMMENT '消耗算力',
`created_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='MidJourney 任务表';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_orders`
--
DROP TABLE IF EXISTS `chatgpt_orders`;
CREATE TABLE `chatgpt_orders` (
`id` int NOT NULL,
`user_id` int NOT NULL COMMENT '用户ID',
`product_id` int NOT NULL COMMENT '产品ID',
`username` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户明',
`order_no` varchar(30) NOT NULL COMMENT '订单ID',
`trade_no` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '支付平台交易流水号',
`subject` varchar(100) NOT NULL COMMENT '订单产品',
`amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '订单金额',
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '订单状态0待支付1已扫码2支付成功',
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '备注',
`pay_time` int DEFAULT NULL COMMENT '支付时间',
`pay_way` varchar(20) NOT NULL COMMENT '支付方式',
`pay_type` varchar(30) NOT NULL COMMENT '支付类型',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL,
`deleted_at` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='充值订单表';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_power_logs`
--
DROP TABLE IF EXISTS `chatgpt_power_logs`;
CREATE TABLE `chatgpt_power_logs` (
`id` int NOT NULL,
`user_id` int NOT NULL COMMENT '用户ID',
`username` varchar(30) NOT NULL COMMENT '用户名',
`type` tinyint(1) NOT NULL COMMENT '类型1充值2消费3退费',
`amount` smallint NOT NULL COMMENT '算力数值',
`balance` int NOT NULL COMMENT '余额',
`model` varchar(30) NOT NULL COMMENT '模型',
`remark` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '备注',
`mark` tinyint(1) NOT NULL COMMENT '资金类型0支出1收入',
`created_at` datetime NOT NULL COMMENT '创建时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户算力消费日志';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_products`
--
DROP TABLE IF EXISTS `chatgpt_products`;
CREATE TABLE `chatgpt_products` (
`id` int NOT NULL,
`name` varchar(30) NOT NULL COMMENT '名称',
`price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '价格',
`discount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '优惠金额',
`days` smallint NOT NULL DEFAULT '0' COMMENT '延长天数',
`power` int NOT NULL DEFAULT '0' COMMENT '增加算力值',
`enabled` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否启动',
`sales` int NOT NULL DEFAULT '0' COMMENT '销量',
`sort_num` tinyint NOT NULL DEFAULT '0' COMMENT '排序',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL,
`app_url` varchar(255) DEFAULT NULL COMMENT 'App跳转地址',
`url` varchar(255) DEFAULT NULL COMMENT '跳转地址'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='会员套餐表';
--
-- 转存表中的数据 `chatgpt_products`
--
INSERT INTO `chatgpt_products` (`id`, `name`, `price`, `discount`, `days`, `power`, `enabled`, `sales`, `sort_num`, `created_at`, `updated_at`, `app_url`, `url`) VALUES
(5, '100次点卡', 9.99, 6.99, 0, 100, 1, 0, 0, '2023-08-28 10:55:08', '2024-10-23 18:12:29', NULL, NULL),
(6, '200次点卡', 19.90, 15.99, 0, 200, 1, 0, 0, '1970-01-01 08:00:00', '2024-10-23 18:12:36', NULL, NULL);
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_redeems`
--
DROP TABLE IF EXISTS `chatgpt_redeems`;
CREATE TABLE `chatgpt_redeems` (
`id` int NOT NULL,
`user_id` int NOT NULL COMMENT '用户 ID',
`name` varchar(30) NOT NULL COMMENT '兑换码名称',
`power` int NOT NULL COMMENT '算力',
`code` varchar(100) NOT NULL COMMENT '兑换码',
`enabled` tinyint(1) NOT NULL COMMENT '是否启用',
`created_at` datetime NOT NULL,
`redeemed_at` int NOT NULL COMMENT '兑换时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='兑换码';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_sd_jobs`
--
DROP TABLE IF EXISTS `chatgpt_sd_jobs`;
CREATE TABLE `chatgpt_sd_jobs` (
`id` int NOT NULL,
`user_id` int NOT NULL COMMENT '用户 ID',
`type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT 'txt2img' COMMENT '任务类别',
`task_id` char(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '任务 ID',
`task_info` text NOT NULL COMMENT '任务详情',
`prompt` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '会话提示词',
`img_url` varchar(255) DEFAULT NULL COMMENT '图片URL',
`params` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '绘画参数json',
`progress` smallint DEFAULT '0' COMMENT '任务进度',
`publish` tinyint(1) NOT NULL COMMENT '是否发布',
`err_msg` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '错误信息',
`power` smallint NOT NULL DEFAULT '0' COMMENT '消耗算力',
`created_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Stable Diffusion 任务表';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_suno_jobs`
--
DROP TABLE IF EXISTS `chatgpt_suno_jobs`;
CREATE TABLE `chatgpt_suno_jobs` (
`id` int NOT NULL,
`user_id` int NOT NULL COMMENT '用户 ID',
`channel` varchar(100) NOT NULL COMMENT '渠道',
`title` varchar(100) DEFAULT NULL COMMENT '歌曲标题',
`type` tinyint(1) DEFAULT '0' COMMENT '任务类型,1:灵感创作,2:自定义创作',
`task_id` varchar(50) DEFAULT NULL COMMENT '任务 ID',
`task_info` text NOT NULL COMMENT '任务详情',
`ref_task_id` char(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '引用任务 ID',
`tags` varchar(100) DEFAULT NULL COMMENT '歌曲风格',
`instrumental` tinyint(1) DEFAULT '0' COMMENT '是否为纯音乐',
`extend_secs` smallint DEFAULT '0' COMMENT '延长秒数',
`song_id` varchar(50) DEFAULT NULL COMMENT '要续写的歌曲 ID',
`ref_song_id` varchar(50) NOT NULL COMMENT '引用的歌曲ID',
`prompt` varchar(2000) NOT NULL COMMENT '提示词',
`cover_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '封面图地址',
`audio_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '音频地址',
`model_name` varchar(30) DEFAULT NULL COMMENT '模型地址',
`progress` smallint DEFAULT '0' COMMENT '任务进度',
`duration` smallint NOT NULL DEFAULT '0' COMMENT '歌曲时长',
`publish` tinyint(1) NOT NULL COMMENT '是否发布',
`err_msg` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '错误信息',
`raw_data` text COMMENT '原始数据',
`power` smallint NOT NULL DEFAULT '0' COMMENT '消耗算力',
`play_times` int DEFAULT NULL COMMENT '播放次数',
`created_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='MidJourney 任务表';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_users`
--
DROP TABLE IF EXISTS `chatgpt_users`;
CREATE TABLE `chatgpt_users` (
`id` int NOT NULL,
`username` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户名',
`mobile` char(11) DEFAULT NULL COMMENT '手机号',
`email` varchar(50) DEFAULT NULL COMMENT '邮箱地址',
`nickname` varchar(30) NOT NULL COMMENT '昵称',
`password` char(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码',
`avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '头像',
`salt` char(12) NOT NULL COMMENT '密码盐',
`power` int NOT NULL DEFAULT '0' COMMENT '剩余算力',
`expired_time` int NOT NULL COMMENT '用户过期时间',
`status` tinyint(1) NOT NULL COMMENT '当前状态',
`chat_config_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '聊天配置json',
`chat_roles_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '聊天角色 json',
`chat_models_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'AI模型 json',
`last_login_at` int NOT NULL COMMENT '最后登录时间',
`vip` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否会员',
`last_login_ip` char(16) NOT NULL COMMENT '最后登录 IP',
`openid` varchar(100) DEFAULT NULL COMMENT '第三方登录账号ID',
`platform` varchar(30) DEFAULT NULL COMMENT '登录平台',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';
--
-- 转存表中的数据 `chatgpt_users`
--
INSERT INTO `chatgpt_users` (`id`, `username`, `mobile`, `email`, `nickname`, `password`, `avatar`, `salt`, `power`, `expired_time`, `status`, `chat_config_json`, `chat_roles_json`, `chat_models_json`, `last_login_at`, `vip`, `last_login_ip`, `openid`, `platform`, `created_at`, `updated_at`) VALUES
(4, '18888888888', '18575670126', '', '极客学长', 'ccc3fb7ab61b8b5d096a4a166ae21d121fc38c71bbd1be6173d9ab973214a63b', 'http://nk.img.r9it.com/gpt/1743224552271576.jpeg', 'ueedue5l', 12132, 0, 1, '{\"api_keys\":{\"Azure\":\"\",\"ChatGLM\":\"\",\"OpenAI\":\"\"}}', '[\"gpt\",\"programmer\",\"teacher\",\"psychiatrist\",\"lu_xun\",\"english_trainer\",\"translator\",\"red_book\",\"dou_yin\",\"weekly_report\",\"girl_friend\",\"steve_jobs\",\"elon_musk\",\"kong_zi\",\"draw_prompt_expert\",\"draw_prompt\",\"prompt_engineer\"]', '[1]', 1744791408, 1, '::1', '', NULL, '2023-06-12 16:47:17', '2025-04-16 16:16:48'),
(48, 'wx@3659838859', '', '', '极客学长', 'cf6bbe381b23812d2b9fd423abe74003cecdd3b93809896eb573536ba6c500b3', 'https://thirdwx.qlogo.cn/mmopen/vi_32/uyxRMqZcEkb7fHouKXbNzxrnrvAttBKkwNlZ7yFibibRGiahdmsrZ3A1NKf8Fw5qJNJn4TXRmygersgEbibaSGd9Sg/132', '5rsy4iwg', 98, 0, 1, '', '[\"gpt\",\"teacher\"]', '', 1736228927, 0, '172.22.11.200', 'oCs0t62472W19z2LOEKI1rWyCTTA', '', '2025-01-07 13:43:06', '2025-01-07 13:48:48'),
(49, 'wx@9502480897', '', '', 'AI探索君', 'd99fa8ba7da1455693b40e11d894a067416e758af2a75d7a3df4721b76cdbc8c', 'https://thirdwx.qlogo.cn/mmopen/vi_32/Zpcln1FZjcKxqtIyCsOTLGn16s7uIvwWfdkdsW6gbZg4r9sibMbic4jvrHmV7ux9nseTB5kBSnu1HSXr7zB8rTXg/132', 'fjclgsli', 99, 0, 1, '', '[\"gpt\"]', '', 0, 0, '', 'oCs0t64FaOLfiTbHZpOqk3aUp_94', '', '2025-01-07 14:05:31', '2025-01-07 14:05:31');
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_user_login_logs`
--
DROP TABLE IF EXISTS `chatgpt_user_login_logs`;
CREATE TABLE `chatgpt_user_login_logs` (
`id` int NOT NULL,
`user_id` int NOT NULL COMMENT '用户ID',
`username` varchar(30) NOT NULL COMMENT '用户名',
`login_ip` char(16) NOT NULL COMMENT '登录IP',
`login_address` varchar(30) NOT NULL COMMENT '登录地址',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户登录日志';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_video_jobs`
--
DROP TABLE IF EXISTS `chatgpt_video_jobs`;
CREATE TABLE `chatgpt_video_jobs` (
`id` int NOT NULL,
`user_id` int NOT NULL COMMENT '用户 ID',
`channel` varchar(100) NOT NULL COMMENT '渠道',
`task_id` varchar(100) NOT NULL COMMENT '任务 ID',
`task_info` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '原始任务信息',
`type` varchar(20) DEFAULT NULL COMMENT '任务类型,luma,runway,cogvideo',
`prompt` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '提示词',
`prompt_ext` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '优化后提示词',
`cover_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '封面图地址',
`video_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '视频地址',
`water_url` varchar(512) DEFAULT NULL COMMENT '带水印的视频地址',
`progress` smallint DEFAULT '0' COMMENT '任务进度',
`publish` tinyint(1) NOT NULL COMMENT '是否发布',
`err_msg` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '错误信息',
`raw_data` text COMMENT '原始数据',
`power` smallint NOT NULL DEFAULT '0' COMMENT '消耗算力',
`created_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='MidJourney 任务表';
--
-- 转储表的索引
--
--
-- 表的索引 `chatgpt_admin_users`
--
ALTER TABLE `chatgpt_admin_users`
ADD PRIMARY KEY (`id`) USING BTREE,
ADD UNIQUE KEY `username` (`username`) USING BTREE;
--
-- 表的索引 `chatgpt_api_keys`
--
ALTER TABLE `chatgpt_api_keys`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `chatgpt_app_types`
--
ALTER TABLE `chatgpt_app_types`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `chatgpt_chat_history`
--
ALTER TABLE `chatgpt_chat_history`
ADD PRIMARY KEY (`id`),
ADD KEY `chat_id` (`chat_id`);
--
-- 表的索引 `chatgpt_chat_items`
--
ALTER TABLE `chatgpt_chat_items`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `chat_id` (`chat_id`);
--
-- 表的索引 `chatgpt_chat_models`
--
ALTER TABLE `chatgpt_chat_models`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `chatgpt_chat_roles`
--
ALTER TABLE `chatgpt_chat_roles`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `marker` (`marker`);
--
-- 表的索引 `chatgpt_configs`
--
ALTER TABLE `chatgpt_configs`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `marker` (`marker`);
--
-- 表的索引 `chatgpt_dall_jobs`
--
ALTER TABLE `chatgpt_dall_jobs`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `chatgpt_files`
--
ALTER TABLE `chatgpt_files`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `chatgpt_functions`
--
ALTER TABLE `chatgpt_functions`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `name` (`name`);
--
-- 表的索引 `chatgpt_invite_codes`
--
ALTER TABLE `chatgpt_invite_codes`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `code` (`code`);
--
-- 表的索引 `chatgpt_invite_logs`
--
ALTER TABLE `chatgpt_invite_logs`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `chatgpt_menus`
--
ALTER TABLE `chatgpt_menus`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `chatgpt_mj_jobs`
--
ALTER TABLE `chatgpt_mj_jobs`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `task_id` (`task_id`),
ADD KEY `message_id` (`message_id`);
--
-- 表的索引 `chatgpt_orders`
--
ALTER TABLE `chatgpt_orders`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `order_no` (`order_no`);
--
-- 表的索引 `chatgpt_power_logs`
--
ALTER TABLE `chatgpt_power_logs`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `chatgpt_products`
--
ALTER TABLE `chatgpt_products`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `chatgpt_redeems`
--
ALTER TABLE `chatgpt_redeems`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `code` (`code`);
--
-- 表的索引 `chatgpt_sd_jobs`
--
ALTER TABLE `chatgpt_sd_jobs`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `task_id` (`task_id`);
--
-- 表的索引 `chatgpt_suno_jobs`
--
ALTER TABLE `chatgpt_suno_jobs`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `chatgpt_users`
--
ALTER TABLE `chatgpt_users`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `username` (`username`);
--
-- 表的索引 `chatgpt_user_login_logs`
--
ALTER TABLE `chatgpt_user_login_logs`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `chatgpt_video_jobs`
--
ALTER TABLE `chatgpt_video_jobs`
ADD PRIMARY KEY (`id`);
--
-- 在导出的表使用AUTO_INCREMENT
--
--
-- 使用表AUTO_INCREMENT `chatgpt_admin_users`
--
ALTER TABLE `chatgpt_admin_users`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=113;
--
-- 使用表AUTO_INCREMENT `chatgpt_api_keys`
--
ALTER TABLE `chatgpt_api_keys`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_app_types`
--
ALTER TABLE `chatgpt_app_types`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_chat_history`
--
ALTER TABLE `chatgpt_chat_history`
MODIFY `id` bigint NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_chat_items`
--
ALTER TABLE `chatgpt_chat_items`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_chat_models`
--
ALTER TABLE `chatgpt_chat_models`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=60;
--
-- 使用表AUTO_INCREMENT `chatgpt_chat_roles`
--
ALTER TABLE `chatgpt_chat_roles`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=135;
--
-- 使用表AUTO_INCREMENT `chatgpt_configs`
--
ALTER TABLE `chatgpt_configs`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=4;
--
-- 使用表AUTO_INCREMENT `chatgpt_dall_jobs`
--
ALTER TABLE `chatgpt_dall_jobs`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_files`
--
ALTER TABLE `chatgpt_files`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_functions`
--
ALTER TABLE `chatgpt_functions`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=4;
--
-- 使用表AUTO_INCREMENT `chatgpt_invite_codes`
--
ALTER TABLE `chatgpt_invite_codes`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_invite_logs`
--
ALTER TABLE `chatgpt_invite_logs`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_menus`
--
ALTER TABLE `chatgpt_menus`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=22;
--
-- 使用表AUTO_INCREMENT `chatgpt_mj_jobs`
--
ALTER TABLE `chatgpt_mj_jobs`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_orders`
--
ALTER TABLE `chatgpt_orders`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_power_logs`
--
ALTER TABLE `chatgpt_power_logs`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_products`
--
ALTER TABLE `chatgpt_products`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=7;
--
-- 使用表AUTO_INCREMENT `chatgpt_redeems`
--
ALTER TABLE `chatgpt_redeems`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_sd_jobs`
--
ALTER TABLE `chatgpt_sd_jobs`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_suno_jobs`
--
ALTER TABLE `chatgpt_suno_jobs`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_users`
--
ALTER TABLE `chatgpt_users`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=50;
--
-- 使用表AUTO_INCREMENT `chatgpt_user_login_logs`
--
ALTER TABLE `chatgpt_user_login_logs`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_video_jobs`
--
ALTER TABLE `chatgpt_video_jobs`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
COMMIT;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

View File

@ -6,7 +6,7 @@ VUE_APP_ADMIN_USER=admin
VUE_APP_ADMIN_PASS=admin123
VUE_APP_KEY_PREFIX=GeekAI_DEV_
VUE_APP_TITLE="Geek-AI 创作系统"
VUE_APP_VERSION=v4.2.1
VUE_APP_VERSION=v4.2.2
VUE_APP_DOCS_URL=https://docs.geekai.me
VUE_APP_GITHUB_URL=https://github.com/yangjian102621/geekai
VUE_APP_GITEE_URL=https://gitee.com/blackfox/geekai

View File

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

13730
web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,6 @@
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@openai/realtime-api-beta": "github:openai/openai-realtime-api-beta",
"animate.css": "^4.1.1",
"axios": "^0.27.2",
"clipboard": "^2.0.11",
@ -33,12 +32,17 @@
"pinia": "^2.1.4",
"qrcode": "^1.5.3",
"qs": "^6.11.1",
"@better-scroll/core": "^2.5.1",
"@better-scroll/mouse-wheel": "^2.5.1",
"@better-scroll/observe-dom": "^2.5.1",
"@better-scroll/pull-up": "^2.5.1",
"@better-scroll/scroll-bar": "^2.5.1",
"sortablejs": "^1.15.0",
"three": "^0.128.0",
"v3-waterfall": "^1.3.3",
"vant": "^4.5.0",
"vue": "^3.2.13",
"vue-router": "^4.0.15"
"vue-router": "^4.0.15",
"vue-waterfall-plugin-next": "^2.6.5"
},
"devDependencies": {
"@babel/core": "7.18.6",

Binary file not shown.

After

Width:  |  Height:  |  Size: 840 KiB

BIN
web/public/images/voice.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -125,6 +125,7 @@
//.el-message-box
.el-message-box{
--el-messagebox-border-radius: 10px
--el-messagebox-padding-primary: 24px
}
.el-message-box__container{
//border-top: 1px solid #dbd3f4;

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View File

@ -7,7 +7,7 @@
<div class="chat-item">
<div v-if="files.length > 0" class="file-list-box">
<div v-for="file in files">
<div v-for="file in files" :key="file.url">
<div class="image" v-if="isImage(file.ext)">
<el-image :src="file.url" fit="cover" />
</div>
@ -17,7 +17,9 @@
</div>
<div class="body">
<div class="title">
<el-link :href="file.url" target="_blank" style="--el-font-weight-primary: bold">{{ file.name }} </el-link>
<el-link :href="file.url" target="_blank" style="--el-font-weight-primary: bold"
>{{ file.name }}
</el-link>
</div>
<div class="info">
<span>{{ GetFileType(file.ext) }}</span>
@ -46,7 +48,7 @@
<div class="chat-item">
<div v-if="files.length > 0" class="file-list-box">
<div v-for="file in files">
<div v-for="file in files" :key="file.url">
<div class="image" v-if="isImage(file.ext)">
<el-image :src="file.url" fit="cover" />
</div>
@ -56,7 +58,9 @@
</div>
<div class="body">
<div class="title">
<el-link :href="file.url" target="_blank" style="--el-font-weight-primary: bold">{{ file.name }} </el-link>
<el-link :href="file.url" target="_blank" style="--el-font-weight-primary: bold"
>{{ file.name }}
</el-link>
</div>
<div class="info">
<span>{{ GetFileType(file.ext) }}</span>
@ -81,15 +85,15 @@
</template>
<script setup>
import { onMounted, ref } from "vue";
import { Clock } from "@element-plus/icons-vue";
import { httpPost } from "@/utils/http";
import hl from "highlight.js";
import { dateFormat, isImage, processPrompt } from "@/utils/libs";
import { FormatFileSize, GetFileIcon, GetFileType } from "@/store/system";
import emoji from "markdown-it-emoji";
import mathjaxPlugin from "markdown-it-mathjax3";
import MarkdownIt from "markdown-it";
import { FormatFileSize, GetFileIcon, GetFileType } from '@/store/system'
import { httpPost } from '@/utils/http'
import { dateFormat, isImage, processPrompt } from '@/utils/libs'
import { Clock } from '@element-plus/icons-vue'
import hl from 'highlight.js'
import MarkdownIt from 'markdown-it'
import emoji from 'markdown-it-emoji'
import mathjaxPlugin from 'markdown-it-mathjax3'
import { onMounted, ref } from 'vue'
const md = new MarkdownIt({
breaks: true,
@ -97,91 +101,94 @@ const md = new MarkdownIt({
linkify: true,
typographer: true,
highlight: function (str, lang) {
const codeIndex = parseInt(Date.now()) + Math.floor(Math.random() * 10000000);
const codeIndex = parseInt(Date.now()) + Math.floor(Math.random() * 10000000)
//
const copyBtn = `<span class="copy-code-btn" data-clipboard-action="copy" data-clipboard-target="#copy-target-${codeIndex}">复制</span>
<textarea style="position: absolute;top: -9999px;left: -9999px;z-index: -9999;" id="copy-target-${codeIndex}">${str.replace(
/<\/textarea>/g,
"&lt;/textarea>"
)}</textarea>`;
'&lt;/textarea>'
)}</textarea>`
if (lang && hl.getLanguage(lang)) {
const langHtml = `<span class="lang-name">${lang}</span>`;
const langHtml = `<span class="lang-name">${lang}</span>`
//
const preCode = hl.highlight(lang, str, true).value;
const preCode = hl.highlight(lang, str, true).value
// pre
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn} ${langHtml}</pre>`;
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn} ${langHtml}</pre>`
}
//
const preCode = md.utils.escapeHtml(str);
const preCode = md.utils.escapeHtml(str)
// pre
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn}</pre>`;
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn}</pre>`
},
});
md.use(mathjaxPlugin);
md.use(emoji);
})
md.use(mathjaxPlugin)
md.use(emoji)
const props = defineProps({
data: {
type: Object,
default: {
content: "",
created_at: "",
content: '',
created_at: '',
tokens: 0,
model: "",
icon: "",
model: '',
icon: '',
},
},
listStyle: {
type: String,
default: "list",
default: 'list',
},
});
const finalTokens = ref(props.data.tokens);
const content = ref(processPrompt(props.data.content));
const files = ref([]);
})
const finalTokens = ref(props.data.tokens)
const content = ref(processPrompt(props.data.content))
const files = ref([])
onMounted(() => {
processFiles();
});
processFiles()
})
const processFiles = () => {
if (!props.data.content) {
return;
return
}
const linkRegex = /(https?:\/\/\S+)/g;
const links = props.data.content.match(linkRegex);
const urlPrefix = `${window.location.protocol}//${window.location.host}`;
//
const linkRegex = /(https?:\/\/\S+)/g
const links = props.data.content.match(linkRegex)
const urlPrefix = `${window.location.protocol}//${window.location.host}`
if (links) {
//
const _links = links.map((link) => {
if (link.startsWith(urlPrefix)) {
return link.replace(urlPrefix, "");
return link.replace(urlPrefix, '')
}
return link;
});
return link
})
//
const urls = [...new Set([...links, ..._links])];
httpPost("/api/upload/list", { urls: urls })
const urls = [...new Set([...links, ..._links])]
httpPost('/api/upload/list', { urls: urls })
.then((res) => {
files.value = res.data.items;
files.value = res.data.items
for (let link of links) {
if (isExternalImg(link, files.value)) {
files.value.push({ url: link, ext: ".png" });
}
}
// for (let link of links) {
// if (isExternalImg(link, files.value)) {
// files.value.push({ url: link, ext: ".png" });
// }
// }
})
.catch(() => {});
.catch(() => {})
//
for (let link of links) {
content.value = content.value.replace(link, "");
content.value = content.value.replace(link, '')
}
}
content.value = md.render(content.value.trim());
};
content.value = md.render(content.value.trim())
}
const isExternalImg = (link, files) => {
return isImage(link) && !files.find((file) => file.url === link);
};
return isImage(link) && !files.find((file) => file.url === link)
}
</script>
<style lang="stylus">
@ -195,7 +202,7 @@ const isExternalImg = (link, files) => {
width 100%
padding-bottom: 1.5rem;
padding-top: 1.5rem;
border-bottom: 0.5px solid var(--el-border-color);
// border-bottom: 0.5px solid var(--el-border-color);
.chat-line-inner {
display flex;
@ -233,6 +240,8 @@ const isExternalImg = (link, files) => {
border 1px solid #e3e3e3
border-radius 10px
margin-bottom 10px
max-width 150px
max-height 150px
}
}
@ -366,6 +375,8 @@ const isExternalImg = (link, files) => {
border 1px solid #e3e3e3
border-radius 10px
margin-bottom 10px
max-width 150px
max-height 150px
}
}

View File

@ -1,111 +1,137 @@
<template>
<div class="chat-line chat-line-reply-list" v-if="listStyle === 'list'">
<div class="chat-line-inner">
<div class="chat-icon">
<img :src="data.icon" alt="ChatGPT" />
</div>
<div class="chat-reply">
<div class="chat-line chat-line-reply-list" v-if="listStyle === 'list'">
<div class="chat-line-inner">
<div class="chat-icon">
<img :src="data.icon" alt="ChatGPT" />
</div>
<div class="chat-item">
<div class="content-wrapper" v-html="md.render(processContent(data.content))"></div>
<div class="bar" v-if="data.created_at">
<span class="bar-item"
><el-icon><Clock /></el-icon> {{ dateFormat(data.created_at) }}</span
>
<span class="bar-item">tokens: {{ data.tokens }}</span>
<span class="bar-item">
<el-tooltip class="box-item" effect="dark" content="复制回答" placement="bottom">
<el-icon class="copy-reply" :data-clipboard-text="data.content">
<DocumentCopy />
</el-icon>
</el-tooltip>
</span>
<span v-if="!readOnly">
<span class="bar-item" @click="reGenerate(data.prompt)">
<el-tooltip class="box-item" effect="dark" content="重新生成" placement="bottom">
<el-icon><Refresh /></el-icon>
<div class="chat-item">
<div class="content-wrapper" v-html="md.render(processContent(data.content))"></div>
<div class="bar flex text-gray-500" v-if="data.created_at">
<span class="bar-item text-sm">{{ dateFormat(data.created_at) }}</span>
<!-- <span class="bar-item">tokens: {{ data.tokens }}</span> -->
<span class="bar-item">
<el-tooltip class="box-item" effect="dark" content="复制回答" placement="bottom">
<el-icon class="copy-reply" :data-clipboard-text="data.content">
<DocumentCopy />
</el-icon>
</el-tooltip>
</span>
<span v-if="!readOnly" class="flex">
<span class="bar-item" @click="reGenerate(data.prompt)">
<el-tooltip class="box-item" effect="dark" content="重新生成" placement="bottom">
<el-icon><Refresh /></el-icon>
</el-tooltip>
</span>
<span class="bar-item" @click="synthesis(data.content)">
<el-tooltip class="box-item" effect="dark" content="生成语音朗读" placement="bottom">
<i class="iconfont icon-speaker"></i>
</el-tooltip>
<span class="bar-item">
<el-tooltip
class="box-item"
effect="dark"
content="生成语音朗读"
placement="bottom"
>
<i
class="iconfont icon-speaker"
v-if="!isPlaying"
@click="synthesis(data.content)"
></i>
<el-image class="voice-icon" :src="playIcon" v-else />
</el-tooltip>
</span>
</span>
</span>
<!-- <span class="bar-item">-->
<!-- <el-dropdown trigger="click">-->
<!-- <span class="el-dropdown-link">-->
<!-- <el-icon><More/></el-icon>-->
<!-- </span>-->
<!-- <template #dropdown>-->
<!-- <el-dropdown-menu>-->
<!-- <el-dropdown-item :icon="Headset" @click="synthesis(orgContent)">生成语音</el-dropdown-item>-->
<!-- </el-dropdown-menu>-->
<!-- </template>-->
<!-- </el-dropdown>-->
<!-- </span>-->
<!-- <span class="bar-item">-->
<!-- <el-dropdown trigger="click">-->
<!-- <span class="el-dropdown-link">-->
<!-- <el-icon><More/></el-icon>-->
<!-- </span>-->
<!-- <template #dropdown>-->
<!-- <el-dropdown-menu>-->
<!-- <el-dropdown-item :icon="Headset" @click="synthesis(orgContent)">生成语音</el-dropdown-item>-->
<!-- </el-dropdown-menu>-->
<!-- </template>-->
<!-- </el-dropdown>-->
<!-- </span>-->
</div>
</div>
</div>
</div>
</div>
<div class="chat-line chat-line-reply-chat" v-else>
<div class="chat-line-inner">
<div class="chat-icon">
<img :src="data.icon" alt="ChatGPT" />
</div>
<div class="chat-item">
<div class="content-wrapper">
<div class="content" v-html="md.render(processContent(data.content))"></div>
<div class="chat-line chat-line-reply-chat" v-else>
<div class="chat-line-inner">
<div class="chat-icon">
<img :src="data.icon" alt="ChatGPT" />
</div>
<div class="bar" v-if="data.created_at">
<span class="bar-item"
><el-icon><Clock /></el-icon> {{ dateFormat(data.created_at) }}</span
>
<!-- <span class="bar-item">tokens: {{ data.tokens }}</span>-->
<span class="bar-item bg">
<el-tooltip class="box-item" effect="dark" content="复制回答" placement="bottom">
<el-icon class="copy-reply" :data-clipboard-text="data.content">
<DocumentCopy />
</el-icon>
</el-tooltip>
</span>
<span v-if="!readOnly">
<span class="bar-item bg" @click="reGenerate(data.prompt)">
<el-tooltip class="box-item" effect="dark" content="重新生成" placement="bottom">
<el-icon><Refresh /></el-icon>
<div class="chat-item">
<div class="content-wrapper">
<div class="content" v-html="md.render(processContent(data.content))"></div>
</div>
<div class="bar text-gray-500" v-if="data.created_at">
<span class="bar-item text-sm"> {{ dateFormat(data.created_at) }}</span>
<!-- <span class="bar-item">tokens: {{ data.tokens }}</span>-->
<span class="bar-item bg">
<el-tooltip class="box-item" effect="dark" content="复制回答" placement="bottom">
<el-icon class="copy-reply" :data-clipboard-text="data.content">
<DocumentCopy />
</el-icon>
</el-tooltip>
</span>
<span v-if="!readOnly" class="flex">
<span class="bar-item bg" @click="reGenerate(data.prompt)">
<el-tooltip class="box-item" effect="dark" content="重新生成" placement="bottom">
<el-icon><Refresh /></el-icon>
</el-tooltip>
</span>
<span class="bar-item bg" @click="synthesis(data.content)">
<el-tooltip class="box-item" effect="dark" content="生成语音朗读" placement="bottom">
<i class="iconfont icon-speaker"></i>
</el-tooltip>
<span class="bar-item bg">
<el-tooltip
class="box-item"
effect="dark"
content="生成语音朗读"
placement="bottom"
v-if="!isPlaying"
>
<i class="iconfont icon-speaker" @click="synthesis(data.content)"></i>
</el-tooltip>
<el-tooltip
class="box-item"
effect="dark"
content="暂停播放"
placement="bottom"
v-else
>
<el-image class="voice-icon" :src="playIcon" @click="stopSynthesis()" />
</el-tooltip>
</span>
</span>
</span>
</div>
</div>
</div>
</div>
<audio ref="audio" @ended="isPlaying = false" />
</div>
</template>
<script setup>
import {Clock, DocumentCopy, Refresh} from "@element-plus/icons-vue";
import {ElMessage} from "element-plus";
import {dateFormat, processContent} from "@/utils/libs";
import hl from "highlight.js";
import emoji from "markdown-it-emoji";
import mathjaxPlugin from "markdown-it-mathjax3";
import MarkdownIt from "markdown-it";
import { useSharedStore } from '@/store/sharedata'
import { httpPost } from '@/utils/http'
import { dateFormat, processContent } from '@/utils/libs'
import { DocumentCopy, Refresh } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import hl from 'highlight.js'
import MarkdownIt from 'markdown-it'
import emoji from 'markdown-it-emoji'
import mathjaxPlugin from 'markdown-it-mathjax3'
import { ref } from 'vue'
// eslint-disable-next-line no-undef,no-unused-vars
const props = defineProps({
data: {
type: Object,
default: {
icon: "",
content: "",
created_at: "",
icon: '',
content: '',
created_at: '',
tokens: 0,
},
},
@ -115,9 +141,14 @@ const props = defineProps({
},
listStyle: {
type: String,
default: "list",
default: 'list',
},
});
})
const audio = ref(null)
const isPlaying = ref(false)
const playIcon = ref('/images/voice.gif')
const store = useSharedStore()
const md = new MarkdownIt({
breaks: true,
@ -125,49 +156,77 @@ const md = new MarkdownIt({
linkify: true,
typographer: true,
highlight: function (str, lang) {
const codeIndex = parseInt(Date.now()) + Math.floor(Math.random() * 10000000);
const codeIndex = parseInt(Date.now()) + Math.floor(Math.random() * 10000000)
//
const copyBtn = `<span class="copy-code-btn" data-clipboard-action="copy" data-clipboard-target="#copy-target-${codeIndex}">复制</span>
<textarea style="position: absolute;top: -9999px;left: -9999px;z-index: -9999;" id="copy-target-${codeIndex}">${str.replace(
/<\/textarea>/g,
"&lt;/textarea>"
)}</textarea>`;
'&lt;/textarea>'
)}</textarea>`
if (lang && hl.getLanguage(lang)) {
const langHtml = `<span class="lang-name">${lang}</span>`;
const langHtml = `<span class="lang-name">${lang}</span>`
//
const preCode = hl.highlight(str, { language: lang }).value;
const preCode = hl.highlight(str, { language: lang }).value
// pre
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn} ${langHtml}</pre>`;
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn} ${langHtml}</pre>`
}
//
const preCode = md.utils.escapeHtml(str);
const preCode = md.utils.escapeHtml(str)
// pre
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn}</pre>`;
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn}</pre>`
},
});
md.use(mathjaxPlugin);
md.use(emoji);
const emits = defineEmits(["regen"]);
})
md.use(mathjaxPlugin)
md.use(emoji)
const emits = defineEmits(['regen'])
if (!props.data.icon) {
props.data.icon = "images/gpt-icon.png";
props.data.icon = 'images/gpt-icon.png'
}
const synthesis = (text) => {
console.log(text);
ElMessage.info("语音合成功能暂不可用");
};
isPlaying.value = true
httpPost('/api/chat/tts', { text: text, model_id: store.ttsModel }, { responseType: 'blob' })
.then((response) => {
// Blob MIME
const blob = new Blob([response], { type: 'audio/mpeg' }) // MP3
const audioUrl = URL.createObjectURL(blob)
//
audio.value.src = audioUrl
audio.value
.play()
.then(() => {
// URL
URL.revokeObjectURL(audioUrl)
})
.catch(() => {
ElMessage.error('音频播放失败,请检查浏览器是否支持该音频格式')
isPlaying.value = false
})
})
.catch((e) => {
ElMessage.error('语音合成失败:' + e.message)
isPlaying.value = false
})
}
const stopSynthesis = () => {
isPlaying.value = false
audio.value.pause()
audio.value.currentTime = 0
}
//
const reGenerate = (prompt) => {
console.log(prompt);
emits("regen", prompt);
};
console.log(prompt)
emits('regen', prompt)
}
</script>
<style lang="stylus">
@import '@/assets/css/markdown/vue.css';
.chat-page,.chat-export {
--font-family: Menlo,"微软雅黑","Roboto Mono","Courier New",Courier,monospace,"Inter",sans-serif;
font-family: var(--font-family);
@ -265,7 +324,7 @@ const reGenerate = (prompt) => {
//
blockquote {
margin 0
margin 0 0 0.8rem 0
background-color: var(--quote-bg-color);
padding: 0.8rem 1.5rem;
color: var(--quote-text-color);
@ -284,7 +343,8 @@ const reGenerate = (prompt) => {
width 100%
padding-bottom: 1.5rem;
padding-top: 1.5rem;
border-bottom: 0.5px solid var(--el-border-color);
border: 1px solid var(--el-border-color);
border-radius: 10px;
.chat-line-inner {
display flex;
@ -324,10 +384,18 @@ const reGenerate = (prompt) => {
padding 10px 10px 10px 0;
.bar-item {
padding 3px 5px;
margin-right 10px;
border-radius 5px;
cursor pointer
display flex
align-items center
justify-content center
height 26px
.voice-icon {
width 20px
height 20px
}
.el-icon {
position relative
@ -403,11 +471,21 @@ const reGenerate = (prompt) => {
.bar {
padding 10px 10px 10px 0;
display flex
.bar-item {
padding 3px 5px;
margin-right 10px;
border-radius 5px;
display flex
align-items center
justify-content center
height 26px
.voice-icon {
width 20px
height 20px
}
.el-icon {
position relative

View File

@ -18,19 +18,28 @@
<el-form-item label="流式输出:">
<el-switch v-model="data.stream" @change="(val) => {store.setChatStream(val)}" />
</el-form-item>
<el-form-item label="语音音色:">
<el-select v-model="data.ttsModel" placeholder="请选择语音音色" @change="changeTTSModel">
<el-option v-for="v in models" :value="v.id" :label="v.name" :key="v.id">
{{ v.name }}
</el-option>
</el-select>
</el-form-item>
</el-form>
</div>
</el-dialog>
</template>
<script setup>
import {computed, ref} from "vue"
import {computed, ref, onMounted} from "vue"
import {useSharedStore} from "@/store/sharedata";
import {httpGet} from "@/utils/http";
const store = useSharedStore();
const data = ref({
style: store.chatListStyle,
stream: store.chatStream,
ttsModel: store.ttsModel,
})
// eslint-disable-next-line no-undef
const props = defineProps({
@ -44,6 +53,20 @@ const emits = defineEmits(['hide']);
const close = function () {
emits('hide', false);
}
const models = ref([]);
onMounted(() => {
//
httpGet("/api/model/list?type=tts").then((res) => {
models.value = res.data;
if (!data.ttsModel) {
store.setTtsModel(models.value[0].id);
}
})
})
const changeTTSModel = (item) => {
store.setTtsModel(item);
}
</script>
<style lang="stylus" scoped>

View File

@ -25,7 +25,11 @@
<template v-for="subItem in item.subs">
<el-sub-menu v-if="subItem.subs" :index="subItem.index" :key="subItem.index">
<template #title>{{ subItem.title }}</template>
<el-menu-item v-for="(threeItem, i) in subItem.subs" :key="i" :index="threeItem.index">
<el-menu-item
v-for="(threeItem, i) in subItem.subs"
:key="i"
:index="threeItem.index"
>
{{ threeItem.title }}
</el-menu-item>
</el-sub-menu>
@ -48,125 +52,135 @@
</template>
<script setup>
import { computed, ref, watch } from "vue";
import { setMenuItems, useSidebarStore } from "@/store/sidebar";
import { httpGet } from "@/utils/http";
import { ElMessage } from "element-plus";
import { useRoute } from "vue-router";
import { useSharedStore } from "@/store/sharedata";
import { useSharedStore } from '@/store/sharedata'
import { setMenuItems, useSidebarStore } from '@/store/sidebar'
import { httpGet } from '@/utils/http'
import { ElMessage } from 'element-plus'
import { computed, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
const title = ref("");
const logo = ref("");
const title = ref('')
const logo = ref('')
//
httpGet("/api/admin/config/get?key=system")
httpGet('/api/admin/config/get?key=system')
.then((res) => {
title.value = res.data.admin_title;
logo.value = res.data.logo;
title.value = res.data.admin_title
logo.value = res.data.logo
})
.catch((e) => {
ElMessage.error("加载系统配置失败: " + e.message);
});
const store = useSharedStore();
const theme = ref(store.theme);
ElMessage.error('加载系统配置失败: ' + e.message)
})
const store = useSharedStore()
const theme = ref(store.theme)
watch(
() => store.theme,
(val) => {
theme.value = val;
theme.value = val
}
);
)
const items = [
{
icon: "home",
index: "/admin/dashboard",
title: "仪表盘",
icon: 'home',
index: '/admin/dashboard',
title: '仪表盘',
},
{
icon: "user-fill",
index: "/admin/user",
title: "用户管理",
icon: 'user-fill',
index: '/admin/user',
title: '用户管理',
},
{
icon: "menu",
index: "1",
title: "应用管理",
icon: 'menu',
index: '1',
title: '应用管理',
subs: [
{
index: "/admin/app",
title: "应用列表",
index: '/admin/app',
title: '应用列表',
icon: 'sub-menu',
},
{
index: "/admin/app/type",
title: "应用分类",
index: '/admin/app/type',
title: '应用分类',
icon: 'chuangzuo',
},
],
},
{
icon: "api-key",
index: "/admin/apikey",
title: "API-KEY",
icon: 'api-key',
index: '/admin/apikey',
title: 'API-KEY',
},
{
icon: "model",
index: "/admin/chat/model",
title: "模型管理",
icon: 'model',
index: '/admin/chat/model',
title: '模型管理',
},
{
icon: "recharge",
index: "/admin/product",
title: "充值产品",
icon: 'recharge',
index: '/admin/product',
title: '充值产品',
},
{
icon: "order",
index: "/admin/order",
title: "充值订单",
icon: 'order',
index: '/admin/order',
title: '充值订单',
},
{
icon: "reward",
index: "/admin/redeem",
title: "兑换码",
icon: 'reward',
index: '/admin/redeem',
title: '兑换码',
},
{
icon: "control",
index: "/admin/functions",
title: "函数管理",
icon: 'control',
index: '/admin/functions',
title: '函数管理',
},
{
icon: "prompt",
index: "/admin/chats",
title: "对话管理",
icon: 'menu',
index: '2',
title: '创作记录',
subs: [
{
icon: 'prompt',
index: '/admin/chats',
title: '对话记录',
},
{
icon: 'image',
index: '/admin/images',
title: '绘图记录',
},
{
icon: 'mp3',
index: '/admin/medias',
title: '音视频记录',
},
],
},
{
icon: 'role',
index: '/admin/manger',
title: '管理员',
},
{
icon: "image",
index: "/admin/images",
title: "绘图管理",
icon: 'config',
index: '/admin/system',
title: '系统设置',
},
{
icon: "mp3",
index: "/admin/medias",
title: "音视频管理",
icon: 'log',
index: '/admin/powerLog',
title: '用户算力日志',
},
{
icon: "role",
index: "/admin/manger",
title: "管理员",
},
{
icon: "config",
index: "/admin/system",
title: "系统设置",
},
{
icon: "log",
index: "/admin/powerLog",
title: "用户算力日志",
},
{
icon: "log",
index: "/admin/loginLog",
title: "用户登录日志",
icon: 'log',
index: '/admin/loginLog',
title: '用户登录日志',
},
// {
// icon: 'menu',
@ -191,15 +205,15 @@ const items = [
// },
// ],
// },
];
]
const route = useRoute();
const route = useRoute()
const onRoutes = computed(() => {
return route.path;
});
return route.path
})
const sidebar = useSidebarStore();
setMenuItems(items);
const sidebar = useSidebarStore()
setMenuItems(items)
</script>
<style scoped lang="stylus">

View File

@ -64,8 +64,6 @@ import {
Uploader,
} from "vant";
import { router } from "@/router";
import "v3-waterfall/dist/style.css";
import V3waterfall from "v3-waterfall";
import "@/assets/css/theme-dark.styl";
import "@/assets/css/theme-light.styl";
import "@/assets/css/common.styl";
@ -104,7 +102,6 @@ app.use(ShareSheet);
app.use(Switch);
app.use(Uploader);
app.use(Tag);
app.use(V3waterfall);
app.use(Overlay);
app.use(Col);
app.use(Row);

View File

@ -5,357 +5,357 @@
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import {createRouter, createWebHistory} from "vue-router";
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
name: "Index",
path: "/",
meta: { title: "首页" },
component: () => import("@/views/Index.vue"),
name: 'Index',
path: '/',
meta: { title: '首页' },
component: () => import('@/views/Index.vue'),
},
{
name: "home",
path: "/home",
redirect: "/chat",
component: () => import("@/views/Home.vue"),
name: 'home',
path: '/home',
redirect: '/chat',
component: () => import('@/views/Home.vue'),
children: [
{
name: "chat",
path: "/chat",
meta: { title: "创作中心" },
component: () => import("@/views/ChatPlus.vue"),
name: 'chat',
path: '/chat',
meta: { title: '创作中心' },
component: () => import('@/views/ChatPlus.vue'),
},
{
name: "chat-id",
path: "/chat/:id",
meta: { title: "创作中心" },
component: () => import("@/views/ChatPlus.vue"),
name: 'chat-id',
path: '/chat/:id',
meta: { title: '创作中心' },
component: () => import('@/views/ChatPlus.vue'),
},
{
name: "image-mj",
path: "/mj",
meta: { title: "MidJourney 绘画中心" },
component: () => import("@/views/ImageMj.vue"),
name: 'image-mj',
path: '/mj',
meta: { title: 'MidJourney 绘画中心' },
component: () => import('@/views/ImageMj.vue'),
},
{
name: "image-sd",
path: "/sd",
meta: { title: "stable diffusion 绘画中心" },
component: () => import("@/views/ImageSd.vue"),
name: 'image-sd',
path: '/sd',
meta: { title: 'stable diffusion 绘画中心' },
component: () => import('@/views/ImageSd.vue'),
},
{
name: "member",
path: "/member",
meta: { title: "会员充值中心" },
component: () => import("@/views/Member.vue"),
name: 'member',
path: '/member',
meta: { title: '会员充值中心' },
component: () => import('@/views/Member.vue'),
},
{
name: "chat-app",
path: "/apps",
meta: { title: "应用中心" },
component: () => import("@/views/ChatApps.vue"),
name: 'chat-app',
path: '/apps',
meta: { title: '应用中心' },
component: () => import('@/views/ChatApps.vue'),
},
{
name: "images",
path: "/images-wall",
meta: { title: "作品展示" },
component: () => import("@/views/ImagesWall.vue"),
name: 'images',
path: '/images-wall',
meta: { title: '作品展示' },
component: () => import('@/views/ImagesWall.vue'),
},
{
name: "user-invitation",
path: "/invite",
meta: { title: "推广计划" },
component: () => import("@/views/Invitation.vue"),
name: 'user-invitation',
path: '/invite',
meta: { title: '推广计划' },
component: () => import('@/views/Invitation.vue'),
},
{
name: "powerLog",
path: "/powerLog",
meta: { title: "消费日志" },
component: () => import("@/views/PowerLog.vue"),
name: 'powerLog',
path: '/powerLog',
meta: { title: '消费日志' },
component: () => import('@/views/PowerLog.vue'),
},
{
name: "xmind",
path: "/xmind",
meta: { title: "思维导图" },
component: () => import("@/views/MarkMap.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"),
name: 'dalle',
path: '/dalle',
meta: { title: 'DALLE-3' },
component: () => import('@/views/Dalle.vue'),
},
{
name: "suno",
path: "/suno",
meta: { title: "Suno音乐创作" },
component: () => import("@/views/Suno.vue"),
name: 'suno',
path: '/suno',
meta: { title: 'Suno音乐创作' },
component: () => import('@/views/Suno.vue'),
},
{
name: "ExternalLink",
path: "/external",
component: () => import("@/views/ExternalPage.vue"),
name: 'ExternalLink',
path: '/external',
component: () => import('@/views/ExternalPage.vue'),
},
{
name: "song",
path: "/song/:id",
meta: { title: "Suno音乐播放" },
component: () => import("@/views/Song.vue"),
name: 'song',
path: '/song/:id',
meta: { title: 'Suno音乐播放' },
component: () => import('@/views/Song.vue'),
},
{
name: "luma",
path: "/luma",
meta: { title: "Luma视频创作" },
component: () => import("@/views/Luma.vue"),
name: 'luma',
path: '/luma',
meta: { title: 'Luma视频创作' },
component: () => import('@/views/Luma.vue'),
},
{
name: "keling",
path: "/keling",
meta: { title: "KeLing视频创作" },
component: () => import("@/views/KeLing.vue"),
name: 'keling',
path: '/keling',
meta: { title: 'KeLing视频创作' },
component: () => import('@/views/KeLing.vue'),
},
],
},
{
name: "chat-export",
path: "/chat/export",
meta: { title: "导出会话记录" },
component: () => import("@/views/ChatExport.vue"),
name: 'chat-export',
path: '/chat/export',
meta: { title: '导出会话记录' },
component: () => import('@/views/ChatExport.vue'),
},
{
name: "login",
path: "/login",
meta: { title: "用户登录" },
component: () => import("@/views/Login.vue"),
name: 'login',
path: '/login',
meta: { title: '用户登录' },
component: () => import('@/views/Login.vue'),
},
{
name: "login-callback",
path: "/login/callback",
meta: { title: "用户登录" },
component: () => import("@/views/LoginCallback.vue"),
name: 'login-callback',
path: '/login/callback',
meta: { title: '用户登录' },
component: () => import('@/views/LoginCallback.vue'),
},
{
name: "register",
path: "/register",
name: 'register',
path: '/register',
meta: { title: "用户注册" },
component: () => import("@/views/Register.vue"),
meta: { title: '用户注册' },
component: () => import('@/views/Register.vue'),
},
{
name: "resetpassword",
path: "/resetpassword",
meta: { title: "重置密码" },
component: () => import("@/views/Resetpassword.vue"),
name: 'resetpassword',
path: '/resetpassword',
meta: { title: '重置密码' },
component: () => import('@/views/Resetpassword.vue'),
},
{
path: "/admin/login",
name: "admin-login",
meta: { title: "控制台登录" },
component: () => import("@/views/admin/Login.vue"),
path: '/admin/login',
name: 'admin-login',
meta: { title: '控制台登录' },
component: () => import('@/views/admin/Login.vue'),
},
{
path: "/payReturn",
name: "pay-return",
meta: { title: "支付回调" },
component: () => import("@/views/PayReturn.vue"),
path: '/payReturn',
name: 'pay-return',
meta: { title: '支付回调' },
component: () => import('@/views/PayReturn.vue'),
},
{
name: "admin",
path: "/admin",
redirect: "/admin/dashboard",
component: () => import("@/views/admin/Home.vue"),
meta: { title: "Geek-AI 控制台" },
name: 'admin',
path: '/admin',
redirect: '/admin/dashboard',
component: () => import('@/views/admin/Home.vue'),
meta: { title: 'Geek-AI 控制台' },
children: [
{
path: "/admin/dashboard",
name: "admin-dashboard",
meta: { title: "仪表盘" },
component: () => import("@/views/admin/Dashboard.vue"),
path: '/admin/dashboard',
name: 'admin-dashboard',
meta: { title: '仪表盘' },
component: () => import('@/views/admin/Dashboard.vue'),
},
{
path: "/admin/system",
name: "admin-system",
meta: { title: "系统设置" },
component: () => import("@/views/admin/SysConfig.vue"),
path: '/admin/system',
name: 'admin-system',
meta: { title: '系统设置' },
component: () => import('@/views/admin/SysConfig.vue'),
},
{
path: "/admin/user",
name: "admin-user",
meta: { title: "用户管理" },
component: () => import("@/views/admin/Users.vue"),
path: '/admin/user',
name: 'admin-user',
meta: { title: '用户管理' },
component: () => import('@/views/admin/Users.vue'),
},
{
path: "/admin/app",
name: "admin-app",
meta: { title: "应用列表" },
component: () => import("@/views/admin/Apps.vue"),
path: '/admin/app',
name: 'admin-app',
meta: { title: '应用列表' },
component: () => import('@/views/admin/Apps.vue'),
},
{
path: "/admin/app/type",
name: "admin-app-type",
meta: { title: "应用分类" },
component: () => import("@/views/admin/AppType.vue"),
path: '/admin/app/type',
name: 'admin-app-type',
meta: { title: '应用分类' },
component: () => import('@/views/admin/AppType.vue'),
},
{
path: "/admin/apikey",
name: "admin-apikey",
meta: { title: "API-KEY 管理" },
component: () => import("@/views/admin/ApiKey.vue"),
path: '/admin/apikey',
name: 'admin-apikey',
meta: { title: 'API-KEY 管理' },
component: () => import('@/views/admin/ApiKey.vue'),
},
{
path: "/admin/chat/model",
name: "admin-chat-model",
meta: { title: "语言模型" },
component: () => import("@/views/admin/ChatModel.vue"),
path: '/admin/chat/model',
name: 'admin-chat-model',
meta: { title: '语言模型' },
component: () => import('@/views/admin/ChatModel.vue'),
},
{
path: "/admin/product",
name: "admin-product",
meta: { title: "充值产品" },
component: () => import("@/views/admin/Product.vue"),
path: '/admin/product',
name: 'admin-product',
meta: { title: '充值产品' },
component: () => import('@/views/admin/Product.vue'),
},
{
path: "/admin/order",
name: "admin-order",
meta: { title: "充值订单" },
component: () => import("@/views/admin/Order.vue"),
path: '/admin/order',
name: 'admin-order',
meta: { title: '充值订单' },
component: () => import('@/views/admin/Order.vue'),
},
{
path: "/admin/redeem",
name: "admin-redeem",
meta: { title: "兑换码管理" },
component: () => import("@/views/admin/Redeem.vue"),
path: '/admin/redeem',
name: 'admin-redeem',
meta: { title: '兑换码管理' },
component: () => import('@/views/admin/Redeem.vue'),
},
{
path: "/admin/loginLog",
name: "admin-loginLog",
meta: { title: "登录日志" },
component: () => import("@/views/admin/LoginLog.vue"),
path: '/admin/loginLog',
name: 'admin-loginLog',
meta: { title: '登录日志' },
component: () => import('@/views/admin/LoginLog.vue'),
},
{
path: "/admin/functions",
name: "admin-functions",
meta: { title: "函数管理" },
component: () => import("@/views/admin/Functions.vue"),
path: '/admin/functions',
name: 'admin-functions',
meta: { title: '函数管理' },
component: () => import('@/views/admin/Functions.vue'),
},
{
path: "/admin/chats",
name: "admin-chats",
meta: { title: "对话管理" },
component: () => import("@/views/admin/ChatList.vue"),
path: '/admin/chats',
name: 'admin-chats',
meta: { title: '对话管理' },
component: () => import('@/views/admin/records/ChatList.vue'),
},
{
path: "/admin/images",
name: "admin-images",
meta: { title: "绘图管理" },
component: () => import("@/views/admin/ImageList.vue"),
path: '/admin/images',
name: 'admin-images',
meta: { title: '绘图管理' },
component: () => import('@/views/admin/records/ImageList.vue'),
},
{
path: "/admin/medias",
name: "admin-medias",
meta: { title: "音视频管理" },
component: () => import("@/views/admin/Medias.vue"),
path: '/admin/medias',
name: 'admin-medias',
meta: { title: '音视频管理' },
component: () => import('@/views/admin/records/Medias.vue'),
},
{
path: "/admin/powerLog",
name: "admin-power-log",
meta: { title: "算力日志" },
component: () => import("@/views/admin/PowerLog.vue"),
path: '/admin/powerLog',
name: 'admin-power-log',
meta: { title: '算力日志' },
component: () => import('@/views/admin/PowerLog.vue'),
},
{
path: "/admin/manger",
name: "admin-manger",
meta: { title: "管理员" },
component: () => import("@/views/admin/Manager.vue"),
path: '/admin/manger',
name: 'admin-manger',
meta: { title: '管理员' },
component: () => import('@/views/admin/Manager.vue'),
},
],
},
{
name: "mobile-login",
path: "/mobile/login",
meta: { title: "用户登录" },
component: () => import("@/views/mobile/Login.vue"),
name: 'mobile-login',
path: '/mobile/login',
meta: { title: '用户登录' },
component: () => import('@/views/mobile/Login.vue'),
},
{
name: "mobile",
path: "/mobile",
meta: { title: "首页" },
component: () => import("@/views/mobile/Home.vue"),
redirect: "/mobile/index",
name: 'mobile',
path: '/mobile',
meta: { title: '首页' },
component: () => import('@/views/mobile/Home.vue'),
redirect: '/mobile/index',
children: [
{
path: "/mobile/index",
name: "mobile-index",
component: () => import("@/views/mobile/Index.vue"),
path: '/mobile/index',
name: 'mobile-index',
component: () => import('@/views/mobile/Index.vue'),
},
{
path: "/mobile/chat",
name: "mobile-chat",
component: () => import("@/views/mobile/ChatList.vue"),
path: '/mobile/chat',
name: 'mobile-chat',
component: () => import('@/views/mobile/ChatList.vue'),
},
{
path: "/mobile/image",
name: "mobile-image",
component: () => import("@/views/mobile/Image.vue"),
path: '/mobile/image',
name: 'mobile-image',
component: () => import('@/views/mobile/Image.vue'),
},
{
path: "/mobile/profile",
name: "mobile-profile",
component: () => import("@/views/mobile/Profile.vue"),
path: '/mobile/profile',
name: 'mobile-profile',
component: () => import('@/views/mobile/Profile.vue'),
},
{
path: "/mobile/imgWall",
name: "mobile-img-wall",
component: () => import("@/views/mobile/pages/ImgWall.vue"),
path: '/mobile/imgWall',
name: 'mobile-img-wall',
component: () => import('@/views/mobile/pages/ImgWall.vue'),
},
{
path: "/mobile/chat/session",
name: "mobile-chat-session",
component: () => import("@/views/mobile/ChatSession.vue"),
path: '/mobile/chat/session',
name: 'mobile-chat-session',
component: () => import('@/views/mobile/ChatSession.vue'),
},
{
path: "/mobile/chat/export",
name: "mobile-chat-export",
component: () => import("@/views/mobile/ChatExport.vue"),
path: '/mobile/chat/export',
name: 'mobile-chat-export',
component: () => import('@/views/mobile/ChatExport.vue'),
},
{
path: '/mobile/apps',
name: 'mobile-apps',
component: () => import('@/views/mobile/Apps.vue'),
},
],
},
{
name: "test",
path: "/test",
meta: { title: "测试页面" },
component: () => import("@/views/Test.vue"),
name: 'test',
path: '/test',
meta: { title: '测试页面' },
component: () => import('@/views/Test.vue'),
},
{
name: "test2",
path: "/test2",
meta: { title: "测试页面" },
component: () => import("@/views/RealtimeTest.vue"),
name: 'NotFound',
path: '/:all(.*)',
meta: { title: '页面没有找到' },
component: () => import('@/views/404.vue'),
},
{
name: "NotFound",
path: "/:all(.*)",
meta: { title: "页面没有找到" },
component: () => import("@/views/404.vue"),
},
];
]
// console.log(MY_VARIABLE)
const router = createRouter({
history: createWebHistory(),
routes: routes,
});
})
let prevRoute = null;
let prevRoute = null
// dynamic change the title when router change
router.beforeEach((to, from, next) => {
document.title = to.meta.title;
prevRoute = from;
next();
});
document.title = to.meta.title
prevRoute = from
next()
})
export { router, prevRoute };
export { router, prevRoute }

View File

@ -1,5 +1,82 @@
import {defineStore} from "pinia";
import Storage from "good-storage";
import errorIcon from "@/assets/img/failed.png";
import loadingIcon from "@/assets/img/loading.gif";
let waterfallOptions = {
// 唯一key值
rowKey: "id",
// 卡片之间的间隙
gutter: 10,
// 是否有周围的gutter
hasAroundGutter: true,
// 卡片在PC上的宽度
width: 200,
// 自定义行显示个数,主要用于对移动端的适配
breakpoints: {
3840: {
// 4K下
rowPerView: 8,
},
2560: {
// 2K下
rowPerView: 7,
},
1920: {
// 2K下
rowPerView: 6,
},
1600: {
// 2K下
rowPerView: 5,
},
1366: {
// 2K下
rowPerView: 4,
},
800: {
// 当屏幕宽度小于等于800
rowPerView: 3,
},
500: {
// 当屏幕宽度小于等于500
rowPerView: 2,
},
},
// 动画效果
animationEffect: "animate__fadeInUp",
// 动画时间
animationDuration: 1000,
// 动画延迟
animationDelay: 300,
animationCancel: false,
// 背景色
backgroundColor: "",
// imgSelector
imgSelector: "img_thumb",
// 是否跨域
crossOrigin: true,
// 加载配置
loadProps: {
loading: loadingIcon,
error: errorIcon,
ratioCalculator: (width, height) => {
const minRatio = 3 / 4;
const maxRatio = 4 / 3;
const curRatio = height / width;
if (curRatio < minRatio) {
return minRatio;
} else if (curRatio > maxRatio) {
return maxRatio;
} else {
return curRatio;
}
},
},
// 是否懒加载
lazyload: true,
align: "center",
}
export const useSharedStore = defineStore("shared", {
state: () => ({
@ -10,6 +87,8 @@ export const useSharedStore = defineStore("shared", {
theme: Storage.get("theme", "light"),
isLogin: false,
chatListExtend: Storage.get("chat_list_extend", true),
ttsModel: Storage.get("tts_model", ""),
waterfallOptions,
}),
getters: {},
actions: {
@ -74,5 +153,10 @@ export const useSharedStore = defineStore("shared", {
setIsLogin(value) {
this.isLogin = value;
},
setTtsModel(value) {
this.ttsModel = value;
Storage.set("tts_model", value);
},
},
});

View File

@ -464,7 +464,7 @@ onUnmounted(() => {
//
const initData = () => {
//
httpGet("/api/model/list")
httpGet("/api/model/list?type=chat")
.then((res) => {
models.value = res.data;
if (!modelID.value) {
@ -751,12 +751,13 @@ const sendMessage = function () {
}
//
let content = prompt.value;
if (files.value.length === 1) {
if (files.value.length > 0) {
content += files.value.map((file) => file.url).join(" ");
} else if (files.value.length > 1) {
showMessageError("当前只支持上传一个文件!");
return false;
}
// else if (files.value.length > 1) {
// showMessageError("");
// return false;
// }
//
chatData.value.push({
type: "prompt",

View File

@ -11,7 +11,12 @@
<el-form-item label="生图模型">
<template #default>
<div class="form-item-inner">
<el-select v-model="selectedModel" style="width: 150px" placeholder="请选择模型" @change="changeModel">
<el-select
v-model="selectedModel"
style="width: 150px"
placeholder="请选择模型"
@change="changeModel"
>
<el-option v-for="v in models" :label="v.name" :value="v" :key="v.value" />
</el-select>
</div>
@ -24,7 +29,12 @@
<template #default>
<div class="form-item-inner">
<el-select v-model="params.quality" style="width: 150px">
<el-option v-for="v in qualities" :label="v.name" :value="v.value" :key="v.value" />
<el-option
v-for="v in qualities"
:label="v.name"
:value="v.value"
:key="v.value"
/>
</el-select>
</div>
</template>
@ -48,9 +58,18 @@
<template #default>
<div class="form-item-inner">
<el-select v-model="params.style" style="width: 150px">
<el-option v-for="v in styles" :label="v.name" :value="v.value" :key="v.value" />
<el-option
v-for="v in styles"
:label="v.name"
:value="v.value"
:key="v.value"
/>
</el-select>
<el-tooltip content="生动使模型倾向于生成超真实和戏剧性的图像" raw-content placement="right">
<el-tooltip
content="生动使模型倾向于生成超真实和戏剧性的图像"
raw-content
placement="right"
>
<el-icon class="info-icon">
<InfoFilled />
</el-icon>
@ -73,7 +92,13 @@
</div>
<el-row class="text-info">
<el-button class="generate-btn" size="small" @click="generatePrompt" color="#5865f2" :disabled="isGenerating">
<el-button
class="generate-btn"
size="small"
@click="generatePrompt"
color="#5865f2"
:disabled="isGenerating"
>
<i class="iconfont icon-chuangzuo" style="margin-right: 5px"></i>
<span>生成专业绘画指令</span>
</el-button>
@ -97,111 +122,148 @@
</div>
</div>
<div class="task-list-box pl-6 pr-6 pb-4 pt-4 h-dvh">
<div class="task-list-inner" :style="{ height: listBoxHeight + 'px' }">
<div class="task-list-inner">
<div class="job-list-box">
<h2 class="text-xl">任务列表</h2>
<task-list :list="runningJobs" />
<template v-if="finishedJobs.length > 0">
<h2 class="text-xl">创作记录</h2>
<div class="finish-job-list">
<div class="finish-job-list mt-3">
<div v-if="finishedJobs.length > 0">
<v3-waterfall
id="waterfall"
<Waterfall
:list="finishedJobs"
srcKey="img_thumb"
:gap="20"
:bottomGap="-10"
:colWidth="colWidth"
:distanceToScroll="100"
:isLoading="loading"
:isOver="isOver"
@scrollReachBottom="fetchFinishJobs()"
:row-key="waterfallOptions.rowKey"
:gutter="waterfallOptions.gutter"
:has-around-gutter="waterfallOptions.hasAroundGutter"
:width="waterfallOptions.width"
:breakpoints="waterfallOptions.breakpoints"
:img-selector="waterfallOptions.imgSelector"
:background-color="waterfallOptions.backgroundColor"
:animation-effect="waterfallOptions.animationEffect"
:animation-duration="waterfallOptions.animationDuration"
:animation-delay="waterfallOptions.animationDelay"
:animation-cancel="waterfallOptions.animationCancel"
:lazyload="waterfallOptions.lazyload"
:load-props="waterfallOptions.loadProps"
:cross-origin="waterfallOptions.crossOrigin"
:align="waterfallOptions.align"
:is-loading="loading"
:is-over="isOver"
@afterRender="loading = false"
>
<template #default="slotProp">
<div class="job-item">
<el-image
v-if="slotProp.item.img_url !== ''"
@click="previewImg(slotProp.item)"
:src="slotProp.item['img_thumb']"
fit="cover"
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-if="slotProp.item.progress === 101">
<template #error>
<div class="image-slot">
<div class="err-msg-container">
<div class="title">任务失败</div>
<div class="opt">
<el-popover title="错误详情" trigger="click" :width="250" :content="slotProp.item['err_msg']" placement="top">
<template #reference>
<el-button type="info">详情</el-button>
</template>
</el-popover>
<el-button type="danger" @click="removeImage(slotProp.item)">删除</el-button>
<template #default="{ item, url }">
<div
class="bg-gray-900 rounded-lg shadow-md overflow-hidden transition-all duration-300 ease-linear hover:shadow-md hover:shadow-purple-800 group"
>
<div class="overflow-hidden rounded-lg">
<LazyImg
:url="url"
v-if="item.progress === 100"
class="cursor-pointer transition-all duration-300 ease-linear group-hover:scale-105"
@click="previewImg(item)"
/>
<el-image v-else-if="item.progress === 101">
<template #error>
<div class="image-slot">
<div class="err-msg-container">
<div class="title">任务失败</div>
<div class="opt">
<el-popover
title="错误详情"
trigger="click"
:width="250"
:content="item['err_msg']"
placement="top"
>
<template #reference>
<el-button type="info">详情</el-button>
</template>
</el-popover>
<el-button type="danger" @click="removeImage(item)"
>删除</el-button
>
</div>
</div>
</div>
</template>
</el-image>
</div>
<div
class="px-4 pt-2 pb-4 border-t border-t-gray-800"
v-if="item.progress === 100"
>
<div
class="pt-3 flex justify-center items-center border-t border-t-gray-600 border-opacity-50"
>
<div class="flex">
<el-tooltip content="取消分享" placement="top" v-if="item.publish">
<el-button
type="warning"
@click="publishImage(item, false)"
circle
>
<i class="iconfont icon-cancel-share"></i>
</el-button>
</el-tooltip>
<el-tooltip content="分享" placement="top" v-else>
<el-button
type="success"
@click="publishImage(item, true)"
circle
>
<i class="iconfont icon-share-bold"></i>
</el-button>
</el-tooltip>
<el-tooltip content="复制提示词" placement="top">
<el-button
type="info"
circle
class="copy-prompt"
:data-clipboard-text="item.prompt"
>
<i class="iconfont icon-file"></i>
</el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button
type="danger"
:icon="Delete"
@click="removeImage(item)"
circle
/>
</el-tooltip>
</div>
</template>
</el-image>
<el-image v-else>
<template #error>
<div class="image-slot">
<i class="iconfont icon-loading"></i>
<span>正在下载图片</span>
</div>
</template>
</el-image>
<div class="remove">
<el-tooltip content="删除" placement="top">
<el-button type="danger" :icon="Delete" @click="removeImage(slotProp.item)" circle />
</el-tooltip>
<el-tooltip content="取消分享" placement="top" v-if="slotProp.item.publish">
<el-button type="warning" @click="publishImage(slotProp.item, false)" circle>
<i class="iconfont icon-cancel-share"></i>
</el-button>
</el-tooltip>
<el-tooltip content="分享" placement="top" v-else>
<el-button type="success" @click="publishImage(slotProp.item, true)" circle>
<i class="iconfont icon-share-bold"></i>
</el-button>
</el-tooltip>
<el-tooltip content="复制提示词" placement="top">
<el-button type="info" circle class="copy-prompt" :data-clipboard-text="slotProp.item.prompt">
<i class="iconfont icon-file"></i>
</el-button>
</el-tooltip>
</div>
</div>
</div>
</template>
</Waterfall>
<template #footer>
<div class="no-more-data">
<span>没有更多数据了</span>
<div class="flex justify-center py-10">
<img
:src="waterfallOptions.loadProps.loading"
class="max-w-[50px] max-h-[50px]"
v-if="loading"
/>
<div v-else>
<button
class="px-5 py-2 rounded-full bg-purple-700 text-md text-white cursor-pointer hover:bg-purple-800 transition-all duration-300"
@click="fetchFinishJobs"
v-if="!isOver"
>
加载更多
</button>
<div class="no-more-data" v-else>
<span class="text-gray-500 mr-2">没有更多数据了</span>
<i class="iconfont icon-face"></i>
</div>
</template>
</v3-waterfall>
</div>
</div>
</div>
<el-empty :image-size="100" :image="nodata" description="暂无记录" v-else />
</div>
</template>
<!-- end finish job list-->
</div>
</div>
@ -214,7 +276,7 @@
<el-image-viewer
@close="
() => {
previewURL = '';
previewURL = ''
}
"
v-if="previewURL !== ''"
@ -224,280 +286,306 @@
</template>
<script setup>
import nodata from "@/assets/img/no-data.png";
import nodata from '@/assets/img/no-data.png'
import { nextTick, onMounted, onUnmounted, ref } from "vue";
import { Delete, InfoFilled, Picture } from "@element-plus/icons-vue";
import { httpGet, httpPost } from "@/utils/http";
import { ElMessage, ElMessageBox } from "element-plus";
import Clipboard from "clipboard";
import { checkSession, getSystemInfo } from "@/store/cache";
import { useSharedStore } from "@/store/sharedata";
import TaskList from "@/components/TaskList.vue";
import BackTop from "@/components/BackTop.vue";
import { showMessageError, showMessageOK } from "@/utils/dialog";
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
import { Delete, InfoFilled, Picture } from '@element-plus/icons-vue'
import { httpGet, httpPost } from '@/utils/http'
import { ElMessage, ElMessageBox } from 'element-plus'
import Clipboard from 'clipboard'
import { checkSession, getSystemInfo } from '@/store/cache'
import { useSharedStore } from '@/store/sharedata'
import TaskList from '@/components/TaskList.vue'
import BackTop from '@/components/BackTop.vue'
import { showMessageError, showMessageOK } from '@/utils/dialog'
import { LazyImg, Waterfall } from 'vue-waterfall-plugin-next'
import 'vue-waterfall-plugin-next/dist/style.css'
const listBoxHeight = ref(0);
const listBoxHeight = ref(0)
// const paramBoxHeight = ref(0)
const isLogin = ref(false);
const loading = ref(true);
const colWidth = ref(220);
const isOver = ref(false);
const previewURL = ref("");
const store = useSharedStore();
const models = ref([]);
const isLogin = ref(false)
const loading = ref(true)
const isOver = ref(false)
const previewURL = ref('')
const store = useSharedStore()
const models = ref([])
const waterfallOptions = store.waterfallOptions
const resizeElement = function () {
listBoxHeight.value = window.innerHeight - 58;
// paramBoxHeight.value = window.innerHeight - 110
};
resizeElement();
window.onresize = () => {
resizeElement();
};
const qualities = [
{ name: "标准", value: "standard" },
{ name: "高清", value: "hd" },
];
const dalleSizes = ["1024x1024", "1792x1024", "1024x1792"];
const fluxSizes = ["1024x1024", "1024x768", "768x1024", "1280x960", "960x1280", "1366x768", "768x1366"];
const sizes = ref(dalleSizes);
const styles = [
{ name: "生动", value: "vivid" },
{ name: "自然", value: "natural" },
];
const params = ref({
quality: "standard",
size: "1024x1024",
style: "vivid",
prompt: "",
});
listBoxHeight.value = window.innerHeight - 58
}
const finishedJobs = ref([]);
const runningJobs = ref([]);
const allowPulling = ref(true); //
const tastPullHandler = ref(null);
const power = ref(0);
const dallPower = ref(0); // SD
const clipboard = ref(null);
const userId = ref(0);
const selectedModel = ref(null);
resizeElement()
window.onresize = () => {
resizeElement()
}
const qualities = [
{ name: '标准', value: 'standard' },
{ name: '高清', value: 'hd' },
]
const dalleSizes = ['1024x1024', '1792x1024', '1024x1792']
const fluxSizes = ['1024x1024', '1152x896', '896x1152', '1280x960', '1024x576']
const sizes = ref(dalleSizes)
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 allowPulling = ref(true) //
const downloadPulling = ref(false) //
const tastPullHandler = ref(null)
const downloadPullHandler = ref(null)
const power = ref(0)
const dallPower = ref(0) // SD
const clipboard = ref(null)
const userId = ref(0)
const selectedModel = ref(null)
onMounted(() => {
initData();
clipboard.value = new Clipboard(".copy-prompt");
clipboard.value.on("success", () => {
showMessageOK("复制成功!");
});
initData()
clipboard.value = new Clipboard('.copy-prompt')
clipboard.value.on('success', () => {
showMessageOK('复制成功!')
})
clipboard.value.on("error", () => {
showMessageError("复制失败!");
});
clipboard.value.on('error', () => {
showMessageError('复制失败!')
})
getSystemInfo()
.then((res) => {
dallPower.value = res.data["dall_power"];
dallPower.value = res.data['dall_power']
})
.catch((e) => {
showMessageError("获取系统配置失败:" + e.message);
});
showMessageError('获取系统配置失败:' + e.message)
})
//
httpGet("/api/dall/models")
httpGet('/api/dall/models')
.then((res) => {
models.value = res.data;
selectedModel.value = models.value[0];
params.value.model_id = selectedModel.value.id;
changeModel(selectedModel.value);
models.value = res.data
selectedModel.value = models.value[0]
params.value.model_id = selectedModel.value.id
changeModel(selectedModel.value)
})
.catch((e) => {
showMessageError("获取模型列表失败:" + e.message);
});
});
showMessageError('获取模型列表失败:' + e.message)
})
})
onUnmounted(() => {
clipboard.value.destroy();
clipboard.value.destroy()
if (tastPullHandler.value) {
clearInterval(tastPullHandler.value);
clearInterval(tastPullHandler.value)
}
});
if (downloadPullHandler.value) {
clearInterval(downloadPullHandler.value)
}
})
const initData = () => {
checkSession()
.then((user) => {
power.value = user["power"];
userId.value = user.id;
isLogin.value = true;
page.value = 0;
fetchRunningJobs();
fetchFinishJobs();
power.value = user['power']
userId.value = user.id
isLogin.value = true
page.value = 0
fetchRunningJobs()
fetchFinishJobs()
//
tastPullHandler.value = setInterval(() => {
if (allowPulling.value) {
fetchRunningJobs();
fetchRunningJobs()
}
}, 5000);
}, 5000)
//
downloadPullHandler.value = setInterval(() => {
if (downloadPulling.value) {
page.value = 0
fetchFinishJobs()
}
}, 5000)
})
.catch(() => {});
};
.catch(() => {})
}
const fetchRunningJobs = () => {
if (!isLogin.value) {
return;
return
}
//
httpGet(`/api/dall/jobs?finish=false`)
.then((res) => {
//
if (res.data.items && res.data.items.length !== runningJobs.value.length) {
page.value = 0;
fetchFinishJobs();
page.value = 0
fetchFinishJobs()
}
if (res.data.items.length > 0) {
runningJobs.value = res.data.items;
runningJobs.value = res.data.items
} else {
allowPulling.value = false;
runningJobs.value = [];
allowPulling.value = false
runningJobs.value = []
}
})
.catch((e) => {
ElMessage.error("获取任务失败:" + e.message);
});
};
ElMessage.error('获取任务失败:' + e.message)
})
}
const page = ref(1);
const pageSize = ref(15);
const page = ref(1)
const pageSize = ref(15)
//
const fetchFinishJobs = () => {
if (!isLogin.value) {
return;
return
}
loading.value = true;
page.value = page.value + 1;
loading.value = true
page.value = page.value + 1
httpGet(`/api/dall/jobs?finish=true&page=${page.value}&page_size=${pageSize.value}`)
.then((res) => {
if (res.data.items.length < pageSize.value) {
isOver.value = true;
isOver.value = true
loading.value = false
}
const imageList = res.data.items;
const imageList = res.data.items
let needPulling = false
for (let i = 0; i < imageList.length; i++) {
imageList[i]["img_thumb"] = imageList[i]["img_url"] + "?imageView2/4/w/300/h/0/q/75";
if (imageList[i]['img_url']) {
imageList[i]['img_thumb'] = imageList[i]['img_url'] + '?imageView2/4/w/300/h/0/q/75'
} else if (imageList[i].progress === 100) {
needPulling = true
imageList[i]['img_thumb'] = waterfallOptions.loadProps.loading
}
}
//
if (page.value === 1) {
finishedJobs.value = imageList;
} else {
finishedJobs.value = finishedJobs.value.concat(imageList);
downloadPulling.value = needPulling
}
loading.value = false;
if (page.value === 1) {
finishedJobs.value = imageList
} else {
finishedJobs.value = finishedJobs.value.concat(imageList)
}
})
.catch((e) => {
ElMessage.error("获取任务失败:" + e.message);
});
};
ElMessage.error('获取任务失败:' + e.message)
loading.value = false
})
}
//
const promptRef = ref(null);
const promptRef = ref(null)
const generate = () => {
if (params.value.prompt === "") {
promptRef.value.focus();
return ElMessage.error("请输入绘画提示词!");
if (params.value.prompt === '') {
promptRef.value.focus()
return ElMessage.error('请输入绘画提示词!')
}
if (!isLogin.value) {
store.setShowLoginDialog(true);
return;
store.setShowLoginDialog(true)
return
}
httpPost("/api/dall/image", params.value)
httpPost('/api/dall/image', params.value)
.then(() => {
ElMessage.success("任务执行成功!");
power.value -= dallPower.value;
ElMessage.success('任务执行成功!')
power.value -= dallPower.value
//
runningJobs.value.push({
prompt: params.value.prompt,
progress: 0,
});
allowPulling.value = true;
})
allowPulling.value = true
isOver.value = false
})
.catch((e) => {
ElMessage.error("任务执行失败:" + e.message);
});
};
ElMessage.error('任务执行失败:' + e.message)
})
}
const removeImage = (item) => {
ElMessageBox.confirm("此操作将会删除任务和图片,继续操作码?", "删除提示", {
confirmButtonText: "确认",
cancelButtonText: "取消",
type: "warning",
ElMessageBox.confirm('此操作将会删除任务和图片,继续操作码?', '删除提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
httpGet("/api/dall/remove", { id: item.id })
httpGet('/api/dall/remove', { id: item.id })
.then(() => {
ElMessage.success("任务删除成功");
page.value = 0;
isOver.value = false;
fetchFinishJobs();
ElMessage.success('任务删除成功')
page.value = 0
isOver.value = false
fetchFinishJobs()
})
.catch((e) => {
ElMessage.error("任务删除失败:" + e.message);
});
ElMessage.error('任务删除失败:' + e.message)
})
})
.catch(() => {});
};
.catch(() => {})
}
const previewImg = (item) => {
previewURL.value = item.img_url;
};
previewURL.value = item.img_url
}
//
const publishImage = (item, action) => {
let text = "图片发布";
let text = '图片发布'
if (action === false) {
text = "取消发布";
text = '取消发布'
}
httpGet("/api/dall/publish", { id: item.id, action: action })
httpGet('/api/dall/publish', { id: item.id, action: action })
.then(() => {
ElMessage.success(text + "成功");
item.publish = action;
page.value = 0;
isOver.value = false;
ElMessage.success(text + '成功')
item.publish = action
page.value = 0
isOver.value = false
})
.catch((e) => {
ElMessage.error(text + "失败:" + e.message);
});
};
ElMessage.error(text + '失败:' + e.message)
})
}
const isGenerating = ref(false);
const isGenerating = ref(false)
const generatePrompt = () => {
if (params.value.prompt === "") {
return showMessageError("请输入原始提示词");
if (params.value.prompt === '') {
return showMessageError('请输入原始提示词')
}
isGenerating.value = true;
httpPost("/api/prompt/image", { prompt: params.value.prompt })
isGenerating.value = true
httpPost('/api/prompt/image', { prompt: params.value.prompt })
.then((res) => {
params.value.prompt = res.data;
isGenerating.value = false;
params.value.prompt = res.data
isGenerating.value = false
})
.catch((e) => {
showMessageError("生成提示词失败:" + e.message);
isGenerating.value = false;
});
};
showMessageError('生成提示词失败:' + e.message)
isGenerating.value = false
})
}
const changeModel = (model) => {
if (model.value.startsWith("dall")) {
sizes.value = dalleSizes;
if (model.value.startsWith('dall')) {
sizes.value = dalleSizes
} else {
sizes.value = fluxSizes;
sizes.value = fluxSizes
}
params.value.model_id = selectedModel.value.id;
};
params.value.model_id = selectedModel.value.id
}
</script>
<style lang="stylus">
@import "@/assets/css/image-dall.styl"
@import "@/assets/css/custom-scroll.styl"
@import '@/assets/css/image-dall.styl';
@import '@/assets/css/custom-scroll.styl';
</style>

View File

@ -58,7 +58,9 @@
<i class="iconfont" :class="item.icon"></i>
</span>
<el-image :src="item.icon" style="width: 20px; height: 20px" v-else />
<span :class="item.url === curPath ? 'title active' : 'title'">{{ item.name }}</span>
<span :class="item.url === curPath ? 'title active' : 'title'">{{
item.name
}}</span>
</a>
</li>
</ul>
@ -77,7 +79,7 @@
<el-icon>
<UserFilled />
</el-icon>
<span class="username title">{{ loginUser.nickname }}</span>
<span class="username title">账户信息</span>
</div>
</li>
<li v-if="!license.de_copy">
@ -108,7 +110,12 @@
</div>
<el-scrollbar class="right-main">
<div class="topheader" v-if="loginUser.id === undefined || !loginUser.id">
<el-button @click="router.push('/login')" class="btn-go animate__animated animate__pulse animate__infinite" round>登录</el-button>
<el-button
@click="router.push('/login')"
class="btn-go animate__animated animate__pulse animate__infinite"
round
>登录</el-button
>
</div>
<div class="content custom-scroll">
<router-view :key="routerViewKey" v-slot="{ Component }">
@ -123,7 +130,9 @@
<el-dialog v-model="showLoginDialog" width="500px" @close="store.setShowLoginDialog(false)">
<template #header>
<div class="text-center text-xl" style="color: var(--theme-text-color-primary)">登录后解锁功能</div>
<div class="text-center text-xl" style="color: var(--theme-text-color-primary)">
登录后解锁功能
</div>
</template>
<div class="p-4 pt-2 pb-2">
<LoginDialog @success="loginSuccess" @hide="store.setShowLoginDialog(false)" />
@ -133,33 +142,33 @@
</template>
<script setup>
import { UserFilled } from "@element-plus/icons-vue";
import ThemeChange from "@/components/ThemeChange.vue";
import { useRouter } from "vue-router";
import { computed, onMounted, ref, watch } from "vue";
import { httpGet } from "@/utils/http";
import { ElMessage } from "element-plus";
import { checkSession, getLicenseInfo, getSystemInfo } from "@/store/cache";
import { removeUserToken } from "@/store/session";
import { useSharedStore } from "@/store/sharedata";
import ConfigDialog from "@/components/UserInfoDialog.vue";
import { showMessageError } from "@/utils/dialog";
import LoginDialog from "@/components/LoginDialog.vue";
import { UserFilled } from '@element-plus/icons-vue'
import ThemeChange from '@/components/ThemeChange.vue'
import { useRouter } from 'vue-router'
import { computed, onMounted, ref, watch } from 'vue'
import { httpGet } from '@/utils/http'
import { ElMessage } from 'element-plus'
import { checkSession, getLicenseInfo, getSystemInfo } from '@/store/cache'
import { removeUserToken } from '@/store/session'
import { useSharedStore } from '@/store/sharedata'
import ConfigDialog from '@/components/UserInfoDialog.vue'
import { showMessageError } from '@/utils/dialog'
import LoginDialog from '@/components/LoginDialog.vue'
const router = useRouter();
const logo = ref("");
const mainNavs = ref([]);
const moreNavs = ref([]);
const curPath = ref();
const router = useRouter()
const logo = ref('')
const mainNavs = ref([])
const moreNavs = ref([])
const curPath = ref()
const title = ref("");
const store = useSharedStore();
const loginUser = ref({});
const routerViewKey = ref(0);
const showConfigDialog = ref(false);
const license = ref({ de_copy: true });
const showLoginDialog = ref(false);
const githubURL = ref(process.env.VUE_APP_GITHUB_URL);
const title = ref('')
const store = useSharedStore()
const loginUser = ref({})
const routerViewKey = ref(0)
const showConfigDialog = ref(false)
const license = ref({ de_copy: true })
const showLoginDialog = ref(false)
const githubURL = ref(process.env.VUE_APP_GITHUB_URL)
/**
* 从路径名中提取第一个路径段
@ -167,123 +176,123 @@ const githubURL = ref(process.env.VUE_APP_GITHUB_URL);
* @returns 第一个路径段不含斜杠例如 'chat'如果不存在则返回 null
*/
const extractFirstSegment = (pathname) => {
const segments = pathname.split("/").filter((segment) => segment.length > 0);
return segments.length > 0 ? segments[0] : null;
};
const segments = pathname.split('/').filter((segment) => segment.length > 0)
return segments.length > 0 ? segments[0] : null
}
const getFirstPathSegment = (url) => {
try {
// 使 URL URL
const parsedUrl = new URL(url);
return extractFirstSegment(parsedUrl.pathname);
const parsedUrl = new URL(url)
return extractFirstSegment(parsedUrl.pathname)
} catch (error) {
// 使
if (typeof window !== "undefined") {
const parsedUrl = new URL(url, window.location.origin);
return extractFirstSegment(parsedUrl.pathname);
if (typeof window !== 'undefined') {
const parsedUrl = new URL(url, window.location.origin)
return extractFirstSegment(parsedUrl.pathname)
}
// null
return null;
return null
}
};
}
const stars = computed(() => {
return 1000;
});
return 1000
})
watch(
() => store.showLoginDialog,
(newValue) => {
showLoginDialog.value = newValue;
showLoginDialog.value = newValue
}
);
)
// ;
router.beforeEach((to, from, next) => {
curPath.value = to.path;
next();
});
curPath.value = to.path
next()
})
if (curPath.value === "/external") {
curPath.value = router.currentRoute.value.query.url;
if (curPath.value === '/external') {
curPath.value = router.currentRoute.value.query.url
}
const changeNav = (item) => {
curPath.value = item.url;
if (item.url.indexOf("http") !== -1) {
curPath.value = item.url
if (item.url.indexOf('http') !== -1) {
//
router.push({ path: "/external", query: { url: item.url, title: item.name } });
router.push({ path: '/external', query: { url: item.url, title: item.name } })
} else {
//
if (router.currentRoute.value.path !== item.url) {
router.push(item.url).then(() => {
// `routerViewKey`
routerViewKey.value += 1;
});
routerViewKey.value += 1
})
}
}
};
}
onMounted(() => {
curPath.value = router.currentRoute.value.path;
curPath.value = router.currentRoute.value.path
getSystemInfo()
.then((res) => {
logo.value = res.data.logo;
title.value = res.data.title;
logo.value = res.data.logo
title.value = res.data.title
})
.catch((e) => {
ElMessage.error("获取系统配置失败:" + e.message);
});
ElMessage.error('获取系统配置失败:' + e.message)
})
//
httpGet("/api/menu/list")
httpGet('/api/menu/list')
.then((res) => {
mainNavs.value = res.data;
mainNavs.value = res.data
//
const rows = Math.floor((window.innerHeight - 100) / 90);
const rows = Math.floor((window.innerHeight - 100) / 90)
if (res.data.length > rows) {
mainNavs.value = res.data.slice(0, rows);
moreNavs.value = res.data.slice(rows);
mainNavs.value = res.data.slice(0, rows)
moreNavs.value = res.data.slice(rows)
}
})
.catch((e) => {
ElMessage.error("获取系统菜单失败:" + e.message);
});
ElMessage.error('获取系统菜单失败:' + e.message)
})
getLicenseInfo()
.then((res) => {
license.value = res.data;
license.value = res.data
})
.catch((e) => {
license.value = { de_copy: false };
showMessageError("获取 License 配置:" + e.message);
});
curPath.value = "/" + getFirstPathSegment(window.location.href);
init();
});
license.value = { de_copy: false }
showMessageError('获取 License 配置:' + e.message)
})
curPath.value = '/' + getFirstPathSegment(window.location.href)
init()
})
const init = () => {
checkSession()
.then((user) => {
loginUser.value = user;
loginUser.value = user
})
.catch(() => {});
};
.catch(() => {})
}
const logout = function () {
httpGet("/api/user/logout")
httpGet('/api/user/logout')
.then(() => {
removeUserToken();
router.push("/login");
removeUserToken()
router.push('/login')
})
.catch(() => {
ElMessage.error("注销失败!");
});
};
ElMessage.error('注销失败!')
})
}
const loginSuccess = () => {
init();
store.setShowLoginDialog(false);
init()
store.setShowLoginDialog(false)
//
routerViewKey.value += 1;
};
routerViewKey.value += 1
}
</script>
<style lang="stylus" scoped>

File diff suppressed because it is too large Load Diff

View File

@ -12,9 +12,18 @@
<template #default>
<div class="form-item-inner">
<el-select v-model="params.sampler" style="width: 150px">
<el-option v-for="item in samplers" :label="item" :value="item" :key="item" />
<el-option
v-for="item in samplers"
:label="item"
:value="item"
:key="item"
/>
</el-select>
<el-tooltip content="出图效果比较好的一般是 Euler 和 DPM 系列算法" raw-content placement="right">
<el-tooltip
content="出图效果比较好的一般是 Euler 和 DPM 系列算法"
raw-content
placement="right"
>
<el-icon class="info-icon">
<InfoFilled />
</el-icon>
@ -29,7 +38,12 @@
<template #default>
<div class="form-item-inner">
<el-select v-model="params.scheduler" style="width: 150px">
<el-option v-for="item in schedulers" :label="item" :value="item" :key="item" />
<el-option
v-for="item in schedulers"
:label="item"
:value="item"
:key="item"
/>
</el-select>
<el-tooltip content="推荐自动或者 Karras" raw-content placement="right">
<el-icon class="info-icon">
@ -63,7 +77,11 @@
<template #default>
<div class="form-item-inner">
<el-input v-model.number="params.steps" />
<el-tooltip content="值越大则代表细节越多,同时也意味着出图速度越慢" raw-content placement="right">
<el-tooltip
content="值越大则代表细节越多,同时也意味着出图速度越慢"
raw-content
placement="right"
>
<el-icon class="info-icon">
<InfoFilled />
</el-icon>
@ -78,7 +96,11 @@
<template #default>
<div class="form-item-inner">
<el-input v-model.number="params.cfg_scale" />
<el-tooltip content="提示词引导系数,图像在多大程度上服从提示词<br/> 较低值会产生更有创意的结果" raw-content placement="right">
<el-tooltip
content="提示词引导系数,图像在多大程度上服从提示词<br/> 较低值会产生更有创意的结果"
raw-content
placement="right"
>
<el-icon class="info-icon">
<InfoFilled />
</el-icon>
@ -93,7 +115,11 @@
<template #default>
<div class="form-item-inner">
<el-input v-model.number="params.seed" />
<el-tooltip content="随机数种子,相同的种子会得到相同的结果<br/> 设置为 -1 则每次随机生成种子" raw-content placement="right">
<el-tooltip
content="随机数种子,相同的种子会得到相同的结果<br/> 设置为 -1 则每次随机生成种子"
raw-content
placement="right"
>
<el-icon class="info-icon">
<InfoFilled />
</el-icon>
@ -113,8 +139,16 @@
<el-form-item label="高清修复">
<template #default>
<div class="form-item-inner">
<el-switch v-model="params.hd_fix" style="--el-switch-on-color: #47fff1" size="large" />
<el-tooltip content="先以较小的分辨率生成图像,接着方法图像<br />然后在不更改构图的情况下再修改细节" raw-content placement="right">
<el-switch
v-model="params.hd_fix"
style="--el-switch-on-color: #47fff1"
size="large"
/>
<el-tooltip
content="先以较小的分辨率生成图像,接着方法图像<br />然后在不更改构图的情况下再修改细节"
raw-content
placement="right"
>
<el-icon style="margin-left: 10px; top: 12px">
<InfoFilled />
</el-icon>
@ -129,8 +163,17 @@
<el-form-item label="重绘幅度">
<template #default>
<div class="form-item-inner">
<el-slider v-model.number="params.hd_redraw_rate" :max="1" :step="0.1" style="width: 180px; --el-slider-main-bg-color: #47fff1" />
<el-tooltip content="决定算法对图像内容的影响程度<br />较大的值将得到越有创意的图像" raw-content placement="right">
<el-slider
v-model.number="params.hd_redraw_rate"
:max="1"
:step="0.1"
style="width: 180px; --el-slider-main-bg-color: #47fff1"
/>
<el-tooltip
content="决定算法对图像内容的影响程度<br />较大的值将得到越有创意的图像"
raw-content
placement="right"
>
<el-icon class="info-icon">
<InfoFilled />
</el-icon>
@ -145,9 +188,18 @@
<template #default>
<div class="form-item-inner">
<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-option
v-for="item in scaleAlg"
:label="item"
:value="item"
:key="item"
/>
</el-select>
<el-tooltip content="高清修复放大算法主流算法有Latent和ESRGAN_4x" raw-content placement="right">
<el-tooltip
content="高清修复放大算法主流算法有Latent和ESRGAN_4x"
raw-content
placement="right"
>
<el-icon class="info-icon">
<InfoFilled />
</el-icon>
@ -162,7 +214,11 @@
<template #default>
<div class="form-item-inner">
<el-input v-model.number="params.hd_scale" />
<el-tooltip content="随机数种子,相同的种子会得到相同的结果<br/> 设置为 -1 则每次随机生成种子" raw-content placement="right">
<el-tooltip
content="随机数种子,相同的种子会得到相同的结果<br/> 设置为 -1 则每次随机生成种子"
raw-content
placement="right"
>
<el-icon class="info-icon">
<InfoFilled />
</el-icon>
@ -177,7 +233,11 @@
<template #default>
<div class="form-item-inner">
<el-input v-model.number="params.hd_steps" />
<el-tooltip content="重绘迭代步数如果设置为0则设置跟原图相同的迭代步数" raw-content placement="right">
<el-tooltip
content="重绘迭代步数如果设置为0则设置跟原图相同的迭代步数"
raw-content
placement="right"
>
<el-icon class="info-icon">
<InfoFilled />
</el-icon>
@ -201,7 +261,13 @@
</div>
<el-row class="text-info">
<el-button class="generate-btn" size="small" @click="generatePrompt" color="#5865f2" :disabled="isGenerating">
<el-button
class="generate-btn"
size="small"
@click="generatePrompt"
color="#5865f2"
:disabled="isGenerating"
>
<i class="iconfont icon-chuangzuo" style="margin-right: 5px"></i>
<span>生成专业绘画指令</span>
</el-button>
@ -216,7 +282,12 @@
</el-tooltip>
</div>
<div class="param-line">
<el-input v-model="params.neg_prompt" :autosize="{ minRows: 4, maxRows: 6 }" type="textarea" placeholder="反向提示词" />
<el-input
v-model="params.neg_prompt"
:autosize="{ minRows: 4, maxRows: 6 }"
type="textarea"
placeholder="反向提示词"
/>
</div>
<div class="text-info">
@ -243,61 +314,138 @@
<task-list :list="runningJobs" />
<template v-if="finishedJobs.length > 0">
<h2 class="text-xl">创作记录</h2>
<div class="finish-job-list">
<div class="finish-job-list mt-3">
<div v-if="finishedJobs.length > 0">
<v3-waterfall
id="waterfall"
<Waterfall
:list="finishedJobs"
srcKey="img_thumb"
:gap="20"
:bottomGap="-10"
:colWidth="colWidth"
:distanceToScroll="100"
:isLoading="loading"
:isOver="isOver"
@scrollReachBottom="fetchFinishJobs()"
:row-key="waterfallOptions.rowKey"
:gutter="waterfallOptions.gutter"
:has-around-gutter="waterfallOptions.hasAroundGutter"
:width="waterfallOptions.width"
:breakpoints="waterfallOptions.breakpoints"
:img-selector="waterfallOptions.imgSelector"
:background-color="waterfallOptions.backgroundColor"
:animation-effect="waterfallOptions.animationEffect"
:animation-duration="waterfallOptions.animationDuration"
:animation-delay="waterfallOptions.animationDelay"
:animation-cancel="waterfallOptions.animationCancel"
:lazyload="waterfallOptions.lazyload"
:load-props="waterfallOptions.loadProps"
:cross-origin="waterfallOptions.crossOrigin"
:align="waterfallOptions.align"
:is-loading="loading"
:is-over="isOver"
@afterRender="loading = false"
>
<template #default="slotProp">
<div class="job-item animate">
<el-image v-if="slotProp.item.progress === 101">
<template #error>
<div class="image-slot">
<div class="err-msg-container">
<div class="title">任务失败</div>
<div class="opt">
<el-popover title="错误详情" trigger="click" :width="250" :content="slotProp.item['err_msg']" placement="top">
<template #reference>
<el-button type="info">详情</el-button>
</template>
</el-popover>
<el-button type="danger" @click="removeImage(slotProp.item)">删除</el-button>
<template #default="{ item, url }">
<div
class="bg-gray-900 rounded-lg shadow-md overflow-hidden transition-all duration-300 ease-linear hover:shadow-md hover:shadow-purple-800 group"
>
<div class="overflow-hidden rounded-lg">
<LazyImg
:url="url"
v-if="item.progress === 100"
class="cursor-pointer transition-all duration-300 ease-linear group-hover:scale-105"
@click="showTask(item)"
/>
<el-image v-else-if="item.progress === 101">
<template #error>
<div class="image-slot">
<div class="err-msg-container">
<div class="title">任务失败</div>
<div class="opt">
<el-popover
title="错误详情"
trigger="click"
:width="250"
:content="item['err_msg']"
placement="top"
>
<template #reference>
<el-button type="info">详情</el-button>
</template>
</el-popover>
<el-button type="danger" @click="removeImage(item)"
>删除</el-button
>
</div>
</div>
</div>
</template>
</el-image>
</div>
<div
class="px-4 pt-2 pb-4 border-t border-t-gray-800"
v-if="item.progress === 100"
>
<div
class="pt-3 flex justify-center items-center border-t border-t-gray-600 border-opacity-50"
>
<div class="flex">
<el-tooltip content="取消分享" placement="top" v-if="item.publish">
<el-button
type="warning"
@click="publishImage(item, false)"
circle
>
<i class="iconfont icon-cancel-share"></i>
</el-button>
</el-tooltip>
<el-tooltip content="分享" placement="top" v-else>
<el-button
type="success"
@click="publishImage(item, true)"
circle
>
<i class="iconfont icon-share-bold"></i>
</el-button>
</el-tooltip>
<el-tooltip content="复制提示词" placement="top">
<el-button
type="info"
circle
class="copy-prompt"
:data-clipboard-text="item.prompt"
>
<i class="iconfont icon-file"></i>
</el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button
type="danger"
:icon="Delete"
@click="removeImage(item)"
circle
/>
</el-tooltip>
</div>
</template>
</el-image>
<div v-else>
<el-image :src="slotProp.item['img_thumb']" @click="showTask(slotProp.item)" fit="cover" loading="lazy" />
<div class="remove">
<el-button type="danger" :icon="Delete" @click="removeImage(slotProp.item)" circle />
<el-button type="warning" v-if="slotProp.item.publish" @click="publishImage(slotProp.item, false)" circle>
<i class="iconfont icon-cancel-share"></i>
</el-button>
<el-button type="success" v-else @click="publishImage(slotProp.item, true)" circle>
<i class="iconfont icon-share-bold"></i>
</el-button>
</div>
</div>
</div>
</template>
</Waterfall>
<template #footer>
<div class="no-more-data">
<span>没有更多数据了</span>
<div class="flex justify-center py-10">
<img
:src="waterfallOptions.loadProps.loading"
class="max-w-[50px] max-h-[50px]"
v-if="loading"
/>
<div v-else>
<button
class="px-5 py-2 rounded-full bg-purple-700 text-md text-white cursor-pointer hover:bg-purple-800 transition-all duration-300"
@click="fetchFinishJobs"
v-if="!isOver"
>
加载更多
</button>
<div class="no-more-data" v-else>
<span class="text-gray-500 mr-2">没有更多数据了</span>
<i class="iconfont icon-face"></i>
</div>
</template>
</v3-waterfall>
</div>
</div>
</div>
<el-empty :image-size="100" v-else :image="nodata" description="暂无记录" />
</div>
@ -312,48 +460,63 @@
</div>
<!-- 任务详情弹框 -->
<sd-task-view v-model="showTaskDialog" :data="item" @drawSame="copyParams" @close="showTaskDialog = false" />
<sd-task-view
v-model="showTaskDialog"
:data="item"
@drawSame="copyParams"
@close="showTaskDialog = false"
/>
</div>
</div>
</template>
<script setup>
import { nextTick, onMounted, onUnmounted, ref } from "vue";
import { Delete, DocumentCopy, InfoFilled, Orange } from "@element-plus/icons-vue";
import nodata from "@/assets/img/no-data.png";
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
import { Delete, DocumentCopy, InfoFilled, Orange } from '@element-plus/icons-vue'
import nodata from '@/assets/img/no-data.png'
import { httpGet, httpPost } from "@/utils/http";
import { ElMessage, ElMessageBox } from "element-plus";
import Clipboard from "clipboard";
import { checkSession, getSystemInfo } from "@/store/cache";
import { useRouter } from "vue-router";
import { getSessionId } from "@/store/session";
import { useSharedStore } from "@/store/sharedata";
import TaskList from "@/components/TaskList.vue";
import BackTop from "@/components/BackTop.vue";
import { showMessageError } from "@/utils/dialog";
import SdTaskView from "@/components/SdTaskView.vue";
const listBoxHeight = ref(0);
import { httpGet, httpPost } from '@/utils/http'
import { ElMessage, ElMessageBox } from 'element-plus'
import Clipboard from 'clipboard'
import { checkSession, getSystemInfo } from '@/store/cache'
import { useRouter } from 'vue-router'
import { getSessionId } from '@/store/session'
import { useSharedStore } from '@/store/sharedata'
import TaskList from '@/components/TaskList.vue'
import BackTop from '@/components/BackTop.vue'
import { showMessageError } from '@/utils/dialog'
import SdTaskView from '@/components/SdTaskView.vue'
import { LazyImg, Waterfall } from 'vue-waterfall-plugin-next'
import 'vue-waterfall-plugin-next/dist/style.css'
const listBoxHeight = ref(0)
// const paramBoxHeight = ref(0)
const fullImgHeight = ref(window.innerHeight - 60);
const showTaskDialog = ref(false);
const item = ref({});
const isLogin = ref(false);
const loading = ref(true);
const colWidth = ref(220);
const store = useSharedStore();
const showTaskDialog = ref(false)
const item = ref({})
const isLogin = ref(false)
const loading = ref(true)
const store = useSharedStore()
const waterfallOptions = store.waterfallOptions
const resizeElement = function () {
listBoxHeight.value = window.innerHeight - 80;
listBoxHeight.value = window.innerHeight - 80
// paramBoxHeight.value = window.innerHeight - 200
};
resizeElement();
}
resizeElement()
window.onresize = () => {
resizeElement();
};
const samplers = ["Euler a", "DPM++ 2S a", "DPM++ 2M", "DPM++ SDE", "DPM++ 2M SDE", "UniPC", "Restart"];
const schedulers = ["Automatic", "Karras", "Exponential", "Uniform"];
const scaleAlg = ["Latent", "ESRGAN_4x", "R-ESRGAN 4x+", "SwinIR_4x", "LDSR"];
resizeElement()
}
const samplers = [
'Euler a',
'DPM++ 2S a',
'DPM++ 2M',
'DPM++ SDE',
'DPM++ 2M SDE',
'UniPC',
'Restart',
]
const schedulers = ['Automatic', 'Karras', 'Exponential', 'Uniform']
const scaleAlg = ['Latent', 'ESRGAN_4x', 'R-ESRGAN 4x+', 'SwinIR_4x', 'LDSR']
const params = ref({
width: 1024,
height: 1024,
@ -367,227 +530,229 @@ const params = ref({
hd_scale: 2,
hd_scale_alg: scaleAlg[0],
hd_steps: 0,
prompt: "",
neg_prompt: "nsfw, paintings,low quality,easynegative,ng_deepnegative ,lowres,bad anatomy,bad hands,bad feet",
});
prompt: '',
neg_prompt:
'nsfw, paintings,low quality,easynegative,ng_deepnegative ,lowres,bad anatomy,bad hands,bad feet',
})
const runningJobs = ref([]);
const finishedJobs = ref([]);
const allowPulling = ref(true); //
const tastPullHandler = ref(null);
const router = useRouter();
const runningJobs = ref([])
const finishedJobs = ref([])
const allowPulling = ref(true) //
const tastPullHandler = ref(null)
const router = useRouter()
//
const _params = router.currentRoute.value.params["copyParams"];
const _params = router.currentRoute.value.params['copyParams']
if (_params) {
params.value = JSON.parse(_params);
params.value = JSON.parse(_params)
}
const power = ref(0);
const sdPower = ref(0); // SD
const power = ref(0)
const sdPower = ref(0) // SD
const userId = ref(0);
const clipboard = ref(null);
const userId = ref(0)
const clipboard = ref(null)
onMounted(() => {
initData();
clipboard.value = new Clipboard(".copy-prompt-sd");
clipboard.value.on("success", () => {
ElMessage.success("复制成功!");
});
initData()
clipboard.value = new Clipboard('.copy-prompt-sd')
clipboard.value.on('success', () => {
ElMessage.success('复制成功!')
})
clipboard.value.on("error", () => {
ElMessage.error("复制失败!");
});
clipboard.value.on('error', () => {
ElMessage.error('复制失败!')
})
getSystemInfo()
.then((res) => {
sdPower.value = res.data.sd_power;
params.value.neg_prompt = res.data.sd_neg_prompt;
sdPower.value = res.data.sd_power
params.value.neg_prompt = res.data.sd_neg_prompt
})
.catch((e) => {
ElMessage.error("获取系统配置失败:" + e.message);
});
});
ElMessage.error('获取系统配置失败:' + e.message)
})
})
onUnmounted(() => {
clipboard.value.destroy();
clipboard.value.destroy()
if (tastPullHandler.value) {
clearInterval(tastPullHandler.value);
clearInterval(tastPullHandler.value)
}
});
})
const initData = () => {
checkSession()
.then((user) => {
power.value = user["power"];
userId.value = user.id;
isLogin.value = true;
page.value = 0;
fetchRunningJobs();
fetchFinishJobs();
power.value = user['power']
userId.value = user.id
isLogin.value = true
page.value = 0
fetchRunningJobs()
fetchFinishJobs()
tastPullHandler.value = setInterval(() => {
if (allowPulling.value) {
fetchRunningJobs();
fetchRunningJobs()
}
}, 5000);
}, 5000)
})
.catch(() => {});
};
.catch(() => {})
}
const fetchRunningJobs = () => {
if (!isLogin.value) {
return;
return
}
//
httpGet(`/api/sd/jobs?finish=0`)
.then((res) => {
if (runningJobs.value.length !== res.data.items.length) {
page.value = 0;
fetchFinishJobs();
page.value = 0
fetchFinishJobs()
}
if (runningJobs.value.length === 0) {
allowPulling.value = false;
allowPulling.value = false
}
runningJobs.value = res.data.items;
runningJobs.value = res.data.items
})
.catch((e) => {
ElMessage.error("获取任务失败:" + e.message);
});
};
ElMessage.error('获取任务失败:' + e.message)
})
}
const page = ref(0);
const pageSize = ref(20);
const isOver = ref(false);
const page = ref(0)
const pageSize = ref(20)
const isOver = ref(false)
//
const fetchFinishJobs = () => {
if (!isLogin.value || isOver.value === true) {
return;
return
}
loading.value = true;
page.value = page.value + 1;
loading.value = true
page.value = page.value + 1
httpGet(`/api/sd/jobs?finish=1&page=${page.value}&page_size=${pageSize.value}`)
.then((res) => {
if (res.data.items.length < pageSize.value) {
isOver.value = true;
isOver.value = true
loading.value = false
}
const imageList = res.data.items;
const imageList = res.data.items
for (let i = 0; i < imageList.length; i++) {
imageList[i]["img_thumb"] = imageList[i]["img_url"] + "?imageView2/4/w/300/h/0/q/75";
imageList[i]['img_thumb'] = imageList[i]['img_url'] + '?imageView2/4/w/300/h/0/q/75'
}
if (page.value === 1) {
finishedJobs.value = imageList;
finishedJobs.value = imageList
} else {
finishedJobs.value = finishedJobs.value.concat(imageList);
finishedJobs.value = finishedJobs.value.concat(imageList)
}
loading.value = false;
})
.catch((e) => {
ElMessage.error("获取任务失败:" + e.message);
});
};
ElMessage.error('获取任务失败:' + e.message)
loading.value = false
})
}
//
const promptRef = ref(null);
const promptRef = ref(null)
const generate = () => {
if (params.value.prompt === "") {
promptRef.value.focus();
return ElMessage.error("请输入绘画提示词!");
if (params.value.prompt === '') {
promptRef.value.focus()
return ElMessage.error('请输入绘画提示词!')
}
if (!isLogin.value) {
store.setShowLoginDialog(true);
return;
store.setShowLoginDialog(true)
return
}
if (!params.value.seed) {
params.value.seed = -1;
params.value.seed = -1
}
params.value.session_id = getSessionId();
httpPost("/api/sd/image", params.value)
params.value.session_id = getSessionId()
httpPost('/api/sd/image', params.value)
.then(() => {
ElMessage.success("绘画任务推送成功,请耐心等待任务执行...");
power.value -= sdPower.value;
allowPulling.value = true;
ElMessage.success('绘画任务推送成功,请耐心等待任务执行...')
power.value -= sdPower.value
allowPulling.value = true
runningJobs.value.push({
progress: 0,
});
})
isOver.value = false
})
.catch((e) => {
ElMessage.error("任务推送失败:" + e.message);
});
};
ElMessage.error('任务推送失败:' + e.message)
})
}
const showTask = (row) => {
item.value = row;
showTaskDialog.value = true;
};
item.value = row
showTaskDialog.value = true
}
const copyParams = (row) => {
params.value = row.params;
showTaskDialog.value = false;
};
params.value = row.params
showTaskDialog.value = false
}
const removeImage = (item) => {
ElMessageBox.confirm("此操作将会删除任务和图片,继续操作码?", "删除提示", {
confirmButtonText: "确认",
cancelButtonText: "取消",
type: "warning",
ElMessageBox.confirm('此操作将会删除任务和图片,继续操作码?', '删除提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
httpGet("/api/sd/remove", { id: item.id })
httpGet('/api/sd/remove', { id: item.id })
.then(() => {
ElMessage.success("任务删除成功");
page.value = 0;
isOver.value = false;
fetchFinishJobs();
ElMessage.success('任务删除成功')
page.value = 0
isOver.value = false
fetchFinishJobs()
})
.catch((e) => {
ElMessage.error("任务删除失败:" + e.message);
});
ElMessage.error('任务删除失败:' + e.message)
})
})
.catch(() => {});
};
.catch(() => {})
}
//
const publishImage = (item, action) => {
let text = "图片发布";
let text = '图片发布'
if (action === false) {
text = "取消发布";
text = '取消发布'
}
httpGet("/api/sd/publish", { id: item.id, action: action })
httpGet('/api/sd/publish', { id: item.id, action: action })
.then(() => {
ElMessage.success(text + "成功");
item.publish = action;
page.value = 0;
isOver.value = false;
item.publish = action;
ElMessage.success(text + '成功')
item.publish = action
page.value = 0
isOver.value = false
item.publish = action
})
.catch((e) => {
ElMessage.error(text + "失败:" + e.message);
});
};
ElMessage.error(text + '失败:' + e.message)
})
}
const isGenerating = ref(false);
const isGenerating = ref(false)
const generatePrompt = () => {
if (params.value.prompt === "") {
return showMessageError("请输入原始提示词");
if (params.value.prompt === '') {
return showMessageError('请输入原始提示词')
}
isGenerating.value = true;
httpPost("/api/prompt/image", { prompt: params.value.prompt })
isGenerating.value = true
httpPost('/api/prompt/image', { prompt: params.value.prompt })
.then((res) => {
params.value.prompt = res.data;
isGenerating.value = false;
params.value.prompt = res.data
isGenerating.value = false
})
.catch((e) => {
showMessageError("生成提示词失败:" + e.message);
isGenerating.value = false;
});
};
showMessageError('生成提示词失败:' + e.message)
isGenerating.value = false
})
}
</script>
<style lang="stylus">
@import "@/assets/css/image-sd.styl"
@import "@/assets/css/custom-scroll.styl"
@import '@/assets/css/image-sd.styl';
@import '@/assets/css/custom-scroll.styl';
</style>

View File

@ -11,149 +11,238 @@
</el-radio-group>
</div>
</div>
<div class="waterfall" :style="{ height: listBoxHeight + 'px' }" id="waterfall-box">
<v3-waterfall
<div
class="waterfall"
:style="{ height: listBoxHeight + 'px' }"
id="waterfall-box"
>
<Waterfall
v-if="imgType === 'mj'"
id="waterfall"
id="waterfall-mj"
:list="data['mj']"
srcKey="img_thumb"
:gap="12"
:bottomGap="-5"
:colWidth="colWidth"
:distanceToScroll="100"
:isLoading="loading"
:isOver="isOver"
@scrollReachBottom="getNext"
:row-key="waterfallOptions.rowKey"
:gutter="waterfallOptions.gutter"
:has-around-gutter="waterfallOptions.hasAroundGutter"
:width="waterfallOptions.width"
:breakpoints="waterfallOptions.breakpoints"
:img-selector="waterfallOptions.imgSelector"
:background-color="waterfallOptions.backgroundColor"
:animation-effect="waterfallOptions.animationEffect"
:animation-duration="waterfallOptions.animationDuration"
:animation-delay="waterfallOptions.animationDelay"
:animation-cancel="waterfallOptions.animationCancel"
:lazyload="waterfallOptions.lazyload"
:load-props="waterfallOptions.loadProps"
:cross-origin="waterfallOptions.crossOrigin"
:align="waterfallOptions.align"
:is-loading="loading"
:is-over="isOver"
@afterRender="loading = false"
>
<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>
<template #default="{ item, url }">
<div
class="bg-gray-900 rounded-lg shadow-md overflow-hidden transition-all duration-300 ease-linear hover:shadow-md hover:shadow-purple-800 group"
>
<div class="overflow-hidden rounded-lg">
<LazyImg
:url="url"
class="cursor-pointer transition-all duration-300 ease-linear group-hover:scale-105"
@click="previewImg(item)"
/>
</div>
<div class="opt">
<el-tooltip class="box-item" content="复制提示词" placement="top">
<el-icon class="copy-prompt-wall" :data-clipboard-text="slotProp.item.prompt">
<DocumentCopy />
</el-icon>
</el-tooltip>
<div class="px-4 pt-2 pb-4 border-t border-t-gray-800">
<div
class="pt-3 flex justify-center items-center border-t border-t-gray-600 border-opacity-50"
>
<div class="opt">
<el-tooltip
class="box-item"
content="复制提示词"
placement="top"
>
<el-button
type="info"
circle
class="copy-prompt-wall"
:data-clipboard-text="item.prompt"
>
<i class="iconfont icon-file"></i>
</el-button>
</el-tooltip>
<el-tooltip class="box-item" content="画同款" placement="top">
<i class="iconfont icon-palette-pen" @click="drawSameMj(slotProp.item)"></i>
</el-tooltip>
<el-tooltip
class="box-item"
content="画同款"
placement="top"
>
<el-button
type="primary"
circle
@click="drawSameMj(item)"
>
<i class="iconfont icon-palette"></i>
</el-button>
</el-tooltip>
</div>
</div>
</div>
</div>
</template>
</v3-waterfall>
</Waterfall>
<v3-waterfall
v-else-if="imgType === 'dall'"
id="waterfall"
:list="data['dall']"
srcKey="img_thumb"
:gap="12"
:bottomGap="-5"
:colWidth="colWidth"
:distanceToScroll="100"
:isLoading="loading"
:isOver="isOver"
@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" 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"
<Waterfall
v-if="imgType === 'sd'"
id="waterfall-sd"
:list="data['sd']"
srcKey="img_thumb"
:gap="12"
:bottomGap="-5"
:colWidth="colWidth"
:distanceToScroll="100"
:isLoading="loading"
:isOver="isOver"
@scrollReachBottom="getNext"
:row-key="waterfallOptions.rowKey"
:gutter="waterfallOptions.gutter"
:has-around-gutter="waterfallOptions.hasAroundGutter"
:width="waterfallOptions.width"
:breakpoints="waterfallOptions.breakpoints"
:img-selector="waterfallOptions.imgSelector"
:background-color="waterfallOptions.backgroundColor"
:animation-effect="waterfallOptions.animationEffect"
:animation-duration="waterfallOptions.animationDuration"
:animation-delay="waterfallOptions.animationDelay"
:animation-cancel="waterfallOptions.animationCancel"
:lazyload="waterfallOptions.lazyload"
:load-props="waterfallOptions.loadProps"
:cross-origin="waterfallOptions.crossOrigin"
:align="waterfallOptions.align"
:is-loading="loading"
:is-over="isOver"
@afterRender="loading = false"
>
<template #default="slotProp">
<div class="list-item">
<div class="image">
<el-image :src="slotProp.item['img_thumb']" loading="lazy" @click="showTask(slotProp.item)">
<template #placeholder>
<div class="image-slot">正在加载图片</div>
</template>
<template #default="{ item, url }">
<div
class="bg-gray-900 rounded-lg shadow-md overflow-hidden transition-all duration-300 ease-linear hover:shadow-md hover:shadow-purple-800 group"
>
<div class="overflow-hidden rounded-lg">
<LazyImg
:url="url"
class="cursor-pointer transition-all duration-300 ease-linear group-hover:scale-105"
@click="showTask(item)"
/>
</div>
<div class="px-4 pt-2 pb-4 border-t border-t-gray-800">
<div
class="pt-3 flex justify-center items-center border-t border-t-gray-600 border-opacity-50"
>
<div class="opt">
<el-tooltip
class="box-item"
content="复制提示词"
placement="top"
>
<el-button
type="info"
circle
class="copy-prompt-wall"
:data-clipboard-text="item.prompt"
>
<i class="iconfont icon-file"></i>
</el-button>
</el-tooltip>
<template #error>
<div class="image-slot">
<el-icon>
<Picture />
</el-icon>
</div>
</template>
</el-image>
<el-tooltip
class="box-item"
content="画同款"
placement="top"
>
<el-button
type="primary"
circle
@click="drawSameSd(item)"
>
<i class="iconfont icon-palette"></i>
</el-button>
</el-tooltip>
</div>
</div>
</div>
</div>
</template>
</v3-waterfall>
</Waterfall>
<div class="footer" v-if="isOver">
<!-- <el-empty
:image-size="100"
:image="nodata"
description="没有更多数据了"
/> -->
<span>没有更多数据了</span>
<i class="iconfont icon-face"></i>
<Waterfall
v-if="imgType === 'dall'"
id="waterfall-dall"
:list="data['dall']"
:row-key="waterfallOptions.rowKey"
:gutter="waterfallOptions.gutter"
:has-around-gutter="waterfallOptions.hasAroundGutter"
:width="waterfallOptions.width"
:breakpoints="waterfallOptions.breakpoints"
:img-selector="waterfallOptions.imgSelector"
:background-color="waterfallOptions.backgroundColor"
:animation-effect="waterfallOptions.animationEffect"
:animation-duration="waterfallOptions.animationDuration"
:animation-delay="waterfallOptions.animationDelay"
:animation-cancel="waterfallOptions.animationCancel"
:lazyload="waterfallOptions.lazyload"
:load-props="waterfallOptions.loadProps"
:cross-origin="waterfallOptions.crossOrigin"
:align="waterfallOptions.align"
:is-loading="loading"
:is-over="isOver"
@afterRender="loading = false"
>
<template #default="{ item, url }">
<div
class="bg-gray-900 rounded-lg shadow-md overflow-hidden transition-all duration-300 ease-linear hover:shadow-md hover:shadow-purple-800 group"
>
<div class="overflow-hidden rounded-lg">
<LazyImg
:url="url"
class="cursor-pointer transition-all duration-300 ease-linear group-hover:scale-105"
@click="previewImg(item)"
/>
</div>
<div class="px-4 pt-2 pb-4 border-t border-t-gray-800">
<div
class="pt-3 flex justify-center items-center border-t border-t-gray-600 border-opacity-50"
>
<div class="opt">
<el-tooltip
class="box-item"
content="复制提示词"
placement="top"
>
<el-button
type="info"
circle
class="copy-prompt-wall"
:data-clipboard-text="item.prompt"
>
<i class="iconfont icon-file"></i>
</el-button>
</el-tooltip>
</div>
</div>
</div>
</div>
</template>
</Waterfall>
<div class="flex flex-col items-center justify-center py-10">
<img
:src="waterfallOptions.loadProps.loading"
class="max-w-[50px] max-h-[50px]"
v-if="loading"
/>
<div v-else>
<button
class="px-5 py-2 rounded-full bg-purple-700 text-md text-white cursor-pointer hover:bg-purple-800 transition-all duration-300"
@click="getNext"
v-if="!isOver"
>
加载更多
</button>
<div class="no-more-data" v-else>
<span class="text-gray-500 mr-2">没有更多数据了</span>
<i class="iconfont icon-face"></i>
</div>
</div>
</div>
<back-top :right="30" :bottom="30" />
@ -161,7 +250,23 @@
<!-- end of waterfall -->
</div>
<!-- 任务详情弹框 -->
<sd-task-view v-model="showTaskDialog" :data="item" @drawSame="drawSameSd" @close="showTaskDialog = false" />
<sd-task-view
v-model="showTaskDialog"
:data="item"
@drawSame="drawSameSd"
@close="showTaskDialog = false"
/>
<!-- 图片预览 -->
<el-image-viewer
@close="
() => {
previewURL = '';
}
"
v-if="previewURL !== ''"
:url-list="[previewURL]"
/>
</div>
</template>
@ -175,6 +280,13 @@ import Clipboard from "clipboard";
import { useRouter } from "vue-router";
import BackTop from "@/components/BackTop.vue";
import SdTaskView from "@/components/SdTaskView.vue";
import { LazyImg, Waterfall } from "vue-waterfall-plugin-next";
import "vue-waterfall-plugin-next/dist/style.css";
import { useSharedStore } from "@/store/sharedata";
const store = useSharedStore();
const waterfallOptions = store.waterfallOptions;
const data = ref({
mj: [],
sd: [],
@ -184,19 +296,12 @@ const loading = ref(true);
const isOver = ref(false);
const imgType = ref("mj"); //
const listBoxHeight = window.innerHeight - 124;
const colWidth = ref(220);
const showTaskDialog = ref(false);
const item = ref({});
const previewURL = ref("");
//
const calcColWidth = () => {
const listBoxWidth = window.innerWidth - 60 - 80;
const rows = Math.floor(listBoxWidth / colWidth.value);
colWidth.value = Math.floor((listBoxWidth - (rows - 1) * 12) / rows);
};
calcColWidth();
window.onresize = () => {
calcColWidth();
const previewImg = (item) => {
previewURL.value = item.img_url;
};
const page = ref(0);
@ -223,16 +328,17 @@ const getNext = () => {
}
httpGet(`${url}?page=${page.value}&page_size=${pageSize.value}`)
.then((res) => {
loading.value = false;
if (!res.data.items || res.data.items.length === 0) {
isOver.value = true;
loading.value = false;
return;
}
//
const imageList = res.data.items;
for (let i = 0; i < imageList.length; i++) {
imageList[i]["img_thumb"] = imageList[i]["img_url"] + "?imageView2/4/w/300/h/0/q/75";
imageList[i]["img_thumb"] =
imageList[i]["img_url"] + "?imageView2/4/w/300/h/0/q/75";
}
if (data.value[imgType.value].length === 0) {
data.value[imgType.value] = imageList;
@ -246,6 +352,7 @@ const getNext = () => {
})
.catch((e) => {
ElMessage.error("获取图片失败:" + e.message);
loading.value = false;
});
};
@ -300,6 +407,6 @@ const drawSameMj = (row) => {
</script>
<style lang="stylus">
@import "@/assets/css/images-wall.styl"
@import "@/assets/css/custom-scroll.styl"
@import '@/assets/css/images-wall.styl';
@import '@/assets/css/custom-scroll.styl';
</style>

View File

@ -5,7 +5,9 @@
<AccountTop>
<template #default>
<div class="wechatLog flex-center" v-if="wechatLoginURL !== ''">
<a :href="wechatLoginURL" @click="setRoute(router.currentRoute.value.path)"> <i class="iconfont icon-wechat"></i>使用微信登录 </a>
<a :href="wechatLoginURL" @click="setRoute(router.currentRoute.value.path)">
<i class="iconfont icon-wechat"></i>使用微信登录
</a>
</div>
</template>
</AccountTop>
@ -14,18 +16,34 @@
<el-form ref="ruleFormRef" :model="ruleForm" :rules="rules">
<el-form-item label="" prop="username">
<div class="form-title">账号</div>
<el-input v-model="ruleForm.username" size="large" placeholder="请输入账号" @keyup="handleKeyup" />
<el-input
v-model="ruleForm.username"
size="large"
placeholder="请输入账号"
@keyup="handleKeyup"
/>
</el-form-item>
<el-form-item label="" prop="password">
<div class="flex-between w100">
<div class="form-title">密码</div>
<div class="form-forget text-color-primary" @click="router.push('/resetpassword')">忘记密码</div>
<div class="form-forget text-color-primary" @click="router.push('/resetpassword')">
忘记密码
</div>
</div>
<el-input size="large" v-model="ruleForm.password" placeholder="请输入密码" show-password autocomplete="off" @keyup="handleKeyup" />
<el-input
size="large"
v-model="ruleForm.password"
placeholder="请输入密码"
show-password
autocomplete="off"
@keyup="handleKeyup"
/>
</el-form-item>
<el-form-item>
<el-button class="login-btn" size="large" type="primary" @click="login">登录</el-button>
<el-button class="login-btn" size="large" type="primary" @click="login"
>登录</el-button
>
</el-form-item>
</el-form>
</div>
@ -38,96 +56,105 @@
</template>
<script setup>
import { onMounted, ref, reactive } from "vue";
import { httpGet, httpPost } from "@/utils/http";
import { useRouter } from "vue-router";
import AccountBg from "@/components/AccountBg.vue";
import { isMobile } from "@/utils/libs";
import { checkSession, getLicenseInfo, getSystemInfo } from "@/store/cache";
import { setUserToken } from "@/store/session";
import { showMessageError } from "@/utils/dialog";
import { setRoute } from "@/store/system";
import { useSharedStore } from "@/store/sharedata";
import AccountBg from '@/components/AccountBg.vue'
import { checkSession, getLicenseInfo, getSystemInfo } from '@/store/cache'
import { setUserToken } from '@/store/session'
import { useSharedStore } from '@/store/sharedata'
import { setRoute } from '@/store/system'
import { showMessageError } from '@/utils/dialog'
import { httpGet, httpPost } from '@/utils/http'
import { onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import AccountTop from "@/components/AccountTop.vue";
import Captcha from "@/components/Captcha.vue";
import AccountTop from '@/components/AccountTop.vue'
import Captcha from '@/components/Captcha.vue'
const router = useRouter();
const title = ref("");
const router = useRouter()
const title = ref('')
const logo = ref("");
const licenseConfig = ref({});
const wechatLoginURL = ref("");
const enableVerify = ref(false);
const captchaRef = ref(null);
const ruleFormRef = ref(null);
const logo = ref('')
const licenseConfig = ref({})
const wechatLoginURL = ref('')
const enableVerify = ref(false)
const captchaRef = ref(null)
const ruleFormRef = ref(null)
const ruleForm = reactive({
username: process.env.VUE_APP_USER,
password: process.env.VUE_APP_PASS,
});
})
const rules = {
username: [{ required: true, trigger: "blur", message: "请输入账号" }],
password: [{ required: true, trigger: "blur", message: "请输入密码" }],
};
username: [{ required: true, trigger: 'blur', message: '请输入账号' }],
password: [{ required: true, trigger: 'blur', message: '请输入密码' }],
}
onMounted(() => {
// URLtoken
const urlParams = new URLSearchParams(window.location.search)
const token = urlParams.get('token')
if (token) {
setUserToken(token)
store.setIsLogin(true)
router.push('/chat')
return
}
//
getSystemInfo()
.then((res) => {
logo.value = res.data.logo;
title.value = res.data.title;
enableVerify.value = res.data["enabled_verify"];
logo.value = res.data.logo
title.value = res.data.title
enableVerify.value = res.data['enabled_verify']
})
.catch((e) => {
showMessageError("获取系统配置失败:" + e.message);
title.value = "Geek-AI";
});
showMessageError('获取系统配置失败:' + e.message)
title.value = 'Geek-AI'
})
getLicenseInfo()
.then((res) => {
licenseConfig.value = res.data;
licenseConfig.value = res.data
})
.catch((e) => {
showMessageError("获取 License 配置:" + e.message);
});
showMessageError('获取 License 配置:' + e.message)
})
checkSession()
.then(() => {
router.back();
router.back()
})
.catch(() => {});
.catch(() => {})
const returnURL = `${location.protocol}//${location.host}/login/callback?action=login`;
httpGet("/api/user/clogin?return_url=" + returnURL)
const returnURL = `${location.protocol}//${location.host}/login/callback?action=login`
httpGet('/api/user/clogin?return_url=' + returnURL)
.then((res) => {
wechatLoginURL.value = res.data.url;
wechatLoginURL.value = res.data.url
})
.catch((e) => {
console.error(e);
});
});
console.error(e)
})
})
const handleKeyup = (e) => {
if (e.key === "Enter") {
login();
if (e.key === 'Enter') {
login()
}
};
}
const login = async function () {
await ruleFormRef.value.validate(async (valid) => {
if (valid) {
if (enableVerify.value) {
captchaRef.value.loadCaptcha();
captchaRef.value.loadCaptcha()
} else {
doLogin({});
doLogin({})
}
}
});
};
})
}
const store = useSharedStore();
const store = useSharedStore()
const doLogin = (verifyData) => {
httpPost("/api/user/login", {
httpPost('/api/user/login', {
username: ruleForm.username,
password: ruleForm.password,
key: verifyData.key,
@ -135,14 +162,14 @@ const doLogin = (verifyData) => {
x: verifyData.x,
})
.then((res) => {
setUserToken(res.data.token);
store.setIsLogin(true);
router.back();
setUserToken(res.data.token)
store.setIsLogin(true)
router.back()
})
.catch((e) => {
showMessageError("登录失败," + e.message);
});
};
showMessageError('登录失败,' + e.message)
})
}
</script>
<style lang="stylus" scoped>

View File

@ -19,11 +19,7 @@
<div class="param-line">请选择生成思维导图的AI模型</div>
<div class="param-line">
<el-select
v-model="modelID"
placeholder="请选择模型"
style="width: 100%"
>
<el-select v-model="modelID" placeholder="请选择模型" style="width: 100%">
<el-option
v-for="item in models"
:key="item.id"
@ -43,9 +39,7 @@
<div class="text-info">
<el-text type="primary"
>当前可用算力<el-text type="warning">{{
loginUser.power
}}</el-text></el-text
>当前可用算力<el-text type="warning">{{ loginUser.power }}</el-text></el-text
>
</div>
@ -115,179 +109,186 @@
</template>
<script setup>
import { nextTick, ref } from "vue";
import { Markmap } from "markmap-view";
import { Transformer } from "markmap-lib";
import { checkSession, getSystemInfo } from "@/store/cache";
import { httpGet, httpPost } from "@/utils/http";
import { ElMessage } from "element-plus";
import { Download } from "@element-plus/icons-vue";
import { Toolbar } from "markmap-toolbar";
import { useSharedStore } from "@/store/sharedata";
import { nextTick, onMounted, ref } from 'vue'
import { Markmap } from 'markmap-view'
import { Transformer } from 'markmap-lib'
import { checkSession, getSystemInfo } from '@/store/cache'
import { httpGet, httpPost } from '@/utils/http'
import { ElMessage } from 'element-plus'
import { Download } from '@element-plus/icons-vue'
import { Toolbar } from 'markmap-toolbar'
import { useSharedStore } from '@/store/sharedata'
const leftBoxHeight = ref(window.innerHeight - 105);
const leftBoxHeight = ref(window.innerHeight - 105)
//const rightBoxHeight = ref(window.innerHeight - 115);
const rightBoxHeight = ref(window.innerHeight);
const rightBoxHeight = ref(window.innerHeight)
const prompt = ref("");
const text = ref("");
const content = ref(text.value);
const html = ref("");
const prompt = ref('')
const text = ref('')
const content = ref(text.value)
const html = ref('')
const isLogin = ref(false);
const loginUser = ref({ power: 0 });
const transformer = new Transformer();
const store = useSharedStore();
const loading = ref(false);
const isLogin = ref(false)
const loginUser = ref({ power: 0 })
const transformer = new Transformer()
const store = useSharedStore()
const loading = ref(false)
const svgRef = ref(null);
const markMap = ref(null);
const models = ref([]);
const modelID = ref(0);
const svgRef = ref(null)
const markMap = ref(null)
const models = ref([])
const modelID = ref(0)
const cacheKey = ref('MarkMapCache')
getSystemInfo()
.then((res) => {
text.value = res.data["mark_map_text"];
content.value = text.value;
initData();
nextTick(() => {
try {
markMap.value = Markmap.create(svgRef.value);
const { el } = Toolbar.create(markMap.value);
document.getElementById("toolbar").append(el);
update();
} catch (e) {
console.error(e);
}
});
onMounted(async () => {
const cache = localStorage.getItem(cacheKey.value)
if (cache) {
text.value = cache
} else {
const res = await getSystemInfo().catch((e) => {
ElMessage.error('获取系统配置失败:' + e.message)
})
text.value = res.data['mark_map_text']
content.value = text.value
}
initData()
nextTick(() => {
try {
markMap.value = Markmap.create(svgRef.value)
const { el } = Toolbar.create(markMap.value)
document.getElementById('toolbar').append(el)
update()
} catch (e) {
console.error(e)
}
})
.catch((e) => {
ElMessage.error("获取系统配置失败:" + e.message);
});
})
const initData = () => {
httpGet("/api/model/list")
httpGet('/api/model/list')
.then((res) => {
for (let v of res.data) {
models.value.push(v);
models.value.push(v)
}
modelID.value = models.value[0].id;
modelID.value = models.value[0].id
})
.catch((e) => {
ElMessage.error("获取模型失败:" + e.message);
});
ElMessage.error('获取模型失败:' + e.message)
})
checkSession()
.then((user) => {
loginUser.value = user;
isLogin.value = true;
loginUser.value = user
isLogin.value = true
})
.catch(() => {});
};
.catch(() => {})
}
const update = () => {
try {
const { root } = transformer.transform(processContent(text.value));
markMap.value.setData(root);
markMap.value.fit();
const { root } = transformer.transform(processContent(text.value))
markMap.value.setData(root)
markMap.value.fit()
} catch (e) {
console.error(e);
console.error(e)
}
};
}
const processContent = (text) => {
if (!text) {
return text;
return text
}
const arr = [];
const lines = text.split("\n");
const arr = []
const lines = text.split('\n')
for (let line of lines) {
if (line.indexOf("```") !== -1) {
continue;
if (line.indexOf('```') !== -1) {
continue
}
line = line.replace(/([*_~`>])|(\d+\.)\s/g, "");
arr.push(line);
line = line.replace(/([*_~`>])|(\d+\.)\s/g, '')
arr.push(line)
}
return arr.join("\n");
};
return arr.join('\n')
}
window.onresize = () => {
leftBoxHeight.value = window.innerHeight - 145;
rightBoxHeight.value = window.innerHeight - 85;
};
leftBoxHeight.value = window.innerHeight - 145
rightBoxHeight.value = window.innerHeight - 85
}
const generate = () => {
text.value = content.value;
update();
};
text.value = content.value
update()
}
// 使 AI
const generateAI = () => {
html.value = "";
text.value = "";
if (prompt.value === "") {
return ElMessage.error("请输入你的需求");
html.value = ''
text.value = ''
if (prompt.value === '') {
return ElMessage.error('请输入你的需求')
}
if (!isLogin.value) {
store.setShowLoginDialog(true);
return;
store.setShowLoginDialog(true)
return
}
loading.value = true;
httpPost("/api/markMap/gen", {
loading.value = true
httpPost('/api/markMap/gen', {
prompt: prompt.value,
model_id: modelID.value
model_id: modelID.value,
})
.then((res) => {
text.value = res.data;
content.value = processContent(text.value);
const model = getModelById(modelID.value);
loginUser.value.power -= model.power;
nextTick(() => update());
loading.value = false;
text.value = res.data
content.value = processContent(text.value)
const model = getModelById(modelID.value)
loginUser.value.power -= model.power
nextTick(() => update())
loading.value = false
//
localStorage.setItem(cacheKey.value, text.value)
})
.catch((e) => {
ElMessage.error("生成思维导图失败:" + e.message);
loading.value = false;
});
};
ElMessage.error('生成思维导图失败:' + e.message)
loading.value = false
})
}
const getModelById = (modelId) => {
for (let m of models.value) {
if (m.id === modelId) {
return m;
return m
}
}
};
}
// download SVG to png file
const downloadImage = () => {
const svgElement = document.getElementById("markmap");
const svgElement = document.getElementById('markmap')
// SVG
const serializer = new XMLSerializer();
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);
'<?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);
context.fillStyle = "white";
context.fillRect(0, 0, canvas.width, canvas.height);
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)
context.fillStyle = 'white'
context.fillRect(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();
};
};
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">

View File

@ -1,471 +0,0 @@
<template>
<div data-component="ConsolePage">
<div class="content-top">
<div class="content-title">
<img src="/openai-logomark.svg" alt="OpenAI Logo" />
<span>realtime console</span>
</div>
</div>
<div class="content-main">
<div class="content-logs">
<div class="content-block events">
<div class="visualization">
<div class="visualization-entry client">
<canvas ref="clientCanvasRef" />
</div>
<div class="visualization-entry server">
<canvas ref="serverCanvasRef" />
</div>
</div>
<div class="content-block-title">events</div>
<div class="content-block-body" ref="eventsScrollRef">
<template v-if="!realtimeEvents.length">
awaiting connection...
</template>
<template v-else>
<div v-for="(realtimeEvent, i) in realtimeEvents" :key="realtimeEvent.event.event_id" class="event">
<div class="event-timestamp">
{{ formatTime(realtimeEvent.time) }}
</div>
<div class="event-details">
<div
class="event-summary"
@click="toggleEventDetails(realtimeEvent.event.event_id)"
>
<div
:class="[
'event-source',
realtimeEvent.event.type === 'error'
? 'error'
: realtimeEvent.source,
]"
>
<component :is="realtimeEvent.source === 'client' ? ArrowUp : ArrowDown" />
<span>
{{ realtimeEvent.event.type === 'error'
? 'error!'
: realtimeEvent.source }}
</span>
</div>
<div class="event-type">
{{ realtimeEvent.event.type }}
{{ realtimeEvent.count ? `(${realtimeEvent.count})` : '' }}
</div>
</div>
<div
v-if="expandedEvents[realtimeEvent.event.event_id]"
class="event-payload"
>
{{ JSON.stringify(realtimeEvent.event, null, 2) }}
</div>
</div>
</div>
</template>
</div>
</div>
<div class="content-block conversation">
<div class="content-block-title">conversation</div>
<div class="content-block-body" data-conversation-content>
<template v-if="!items.length">
awaiting connection...
</template>
<template v-else>
<div
v-for="(conversationItem, i) in items"
:key="conversationItem.id"
class="conversation-item"
>
<div :class="['speaker', conversationItem.role || '']">
<div>
{{
(conversationItem.role || conversationItem.type).replaceAll(
'_',
' '
)
}}
</div>
<div class="close" @click="deleteConversationItem(conversationItem.id)">
<X />
</div>
</div>
<div class="speaker-content">
<!-- tool response -->
<div v-if="conversationItem.type === 'function_call_output'">
{{ conversationItem.formatted.output }}
</div>
<!-- tool call -->
<div v-if="conversationItem.formatted.tool">
{{ conversationItem.formatted.tool.name }}(
{{ conversationItem.formatted.tool.arguments }})
</div>
<div
v-if="
!conversationItem.formatted.tool &&
conversationItem.role === 'user'
"
>
{{
conversationItem.formatted.transcript ||
(conversationItem.formatted.audio?.length
? '(awaiting transcript)'
: conversationItem.formatted.text || '(item sent)')
}}
</div>
<div
v-if="
!conversationItem.formatted.tool &&
conversationItem.role === 'assistant'
"
>
{{
conversationItem.formatted.transcript ||
conversationItem.formatted.text ||
'(truncated)'
}}
</div>
<audio
v-if="conversationItem.formatted.file"
:src="conversationItem.formatted.file.url"
controls
/>
</div>
</div>
</template>
</div>
</div>
<div class="content-actions" style="position:absolute; top: 0; left: 0">
<el-button
:type="isConnected ? '' : 'primary'"
@click="connectConversation"
>
{{isConnected ? '断开连接' : '连接对话'}}
</el-button>
<el-button @mousedown="startRecording" @mouseup="stopRecording">开始讲话</el-button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted, watch } from 'vue';
import { RealtimeClient } from '@openai/realtime-api-beta';
import { WavRecorder, WavStreamPlayer } from '@/lib/wavtools/index.js';
import { instructions } from '@/utils/conversation_config.js';
import { WavRenderer } from '@/utils/wav_renderer';
// Constants
const LOCAL_RELAY_SERVER_URL = process.env.REACT_APP_LOCAL_RELAY_SERVER_URL || '';
// Reactive state
const apiKey = ref(
LOCAL_RELAY_SERVER_URL
? ''
: localStorage.getItem('tmp::voice_api_key') || prompt('OpenAI API Key') || ''
);
const wavRecorder = ref(new WavRecorder({ sampleRate: 24000 }));
const wavStreamPlayer = ref(new WavStreamPlayer({ sampleRate: 24000 }));
const client = ref(
new RealtimeClient({
url: "ws://localhost:5678/api/realtime",
apiKey: "sk-Gc5cEzDzGQLIqxWA9d62089350F3454bB359C4A3Fa21B3E4",
dangerouslyAllowAPIKeyInBrowser: true,
})
);
const clientCanvasRef = ref(null);
const serverCanvasRef = ref(null);
const eventsScrollRef = ref(null);
const startTime = ref(new Date().toISOString());
const items = ref([]);
const realtimeEvents = ref([]);
const expandedEvents = reactive({});
const isConnected = ref(false);
const canPushToTalk = ref(true);
const isRecording = ref(false);
const memoryKv = ref({});
const coords = ref({ lat: 37.775593, lng: -122.418137 });
const marker = ref(null);
// Methods
const formatTime = (timestamp) => {
const t0 = new Date(startTime.value).valueOf();
const t1 = new Date(timestamp).valueOf();
const delta = t1 - t0;
const hs = Math.floor(delta / 10) % 100;
const s = Math.floor(delta / 1000) % 60;
const m = Math.floor(delta / 60_000) % 60;
const pad = (n) => {
let s = n + '';
while (s.length < 2) {
s = '0' + s;
}
return s;
};
return `${pad(m)}:${pad(s)}.${pad(hs)}`;
};
const connectConversation = async () => {
alert(123)
startTime.value = new Date().toISOString();
isConnected.value = true;
realtimeEvents.value = [];
items.value = client.value.conversation.getItems();
await wavRecorder.value.begin();
await wavStreamPlayer.value.connect();
await client.value.connect();
client.value.sendUserMessageContent([
{
type: 'input_text',
text: '你好,我是老阳!',
},
]);
if (client.value.getTurnDetectionType() === 'server_vad') {
await wavRecorder.value.record((data) => client.value.appendInputAudio(data.mono));
}
};
const disconnectConversation = async () => {
isConnected.value = false;
realtimeEvents.value = [];
items.value = [];
memoryKv.value = {};
coords.value = { lat: 37.775593, lng: -122.418137 };
marker.value = null;
client.value.disconnect();
await wavRecorder.value.end();
await wavStreamPlayer.value.interrupt();
};
const deleteConversationItem = async (id) => {
client.value.deleteItem(id);
};
const startRecording = async () => {
isRecording.value = true;
const trackSampleOffset = await wavStreamPlayer.value.interrupt();
if (trackSampleOffset?.trackId) {
const { trackId, offset } = trackSampleOffset;
await client.value.cancelResponse(trackId, offset);
}
await wavRecorder.value.record((data) => client.value.appendInputAudio(data.mono));
};
const stopRecording = async () => {
isRecording.value = false;
await wavRecorder.value.pause();
client.value.createResponse();
};
const changeTurnEndType = async (value) => {
if (value === 'none' && wavRecorder.value.getStatus() === 'recording') {
await wavRecorder.value.pause();
}
client.value.updateSession({
turn_detection: value === 'none' ? null : { type: 'server_vad' },
});
if (value === 'server_vad' && client.value.isConnected()) {
await wavRecorder.value.record((data) => client.value.appendInputAudio(data.mono));
}
canPushToTalk.value = value === 'none';
};
const toggleEventDetails = (eventId) => {
if (expandedEvents[eventId]) {
delete expandedEvents[eventId];
} else {
expandedEvents[eventId] = true;
}
};
// Lifecycle hooks and watchers
onMounted(() => {
if (apiKey.value !== '') {
localStorage.setItem('tmp::voice_api_key', apiKey.value);
}
// Set up render loops for the visualization canvas
let isLoaded = true;
const render = () => {
if (isLoaded) {
if (clientCanvasRef.value) {
const canvas = clientCanvasRef.value;
if (!canvas.width || !canvas.height) {
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
}
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const result = wavRecorder.value.recording
? wavRecorder.value.getFrequencies('voice')
: { values: new Float32Array([0]) };
WavRenderer.drawBars(canvas, ctx, result.values, '#0099ff', 10, 0, 8);
}
}
if (serverCanvasRef.value) {
const canvas = serverCanvasRef.value;
if (!canvas.width || !canvas.height) {
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
}
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const result = wavStreamPlayer.value.analyser
? wavStreamPlayer.value.getFrequencies('voice')
: { values: new Float32Array([0]) };
WavRenderer.drawBars(canvas, ctx, result.values, '#009900', 10, 0, 8);
}
}
requestAnimationFrame(render);
}
};
render();
// Set up client event listeners
client.value.on('realtime.event', (realtimeEvent) => {
realtimeEvents.value = realtimeEvents.value.slice();
const lastEvent = realtimeEvents.value[realtimeEvents.value.length - 1];
if (lastEvent?.event.type === realtimeEvent.event.type) {
lastEvent.count = (lastEvent.count || 0) + 1;
realtimeEvents.value.splice(-1, 1, lastEvent);
} else {
realtimeEvents.value.push(realtimeEvent);
}
});
client.value.on('error', (event) => console.error(event));
client.value.on('conversation.interrupted', async () => {
const trackSampleOffset = await wavStreamPlayer.value.interrupt();
if (trackSampleOffset?.trackId) {
const { trackId, offset } = trackSampleOffset;
await client.value.cancelResponse(trackId, offset);
}
});
client.value.on('conversation.updated', async ({ item, delta }) => {
items.value = client.value.conversation.getItems();
if (delta?.audio) {
wavStreamPlayer.value.add16BitPCM(delta.audio, item.id);
}
if (item.status === 'completed' && item.formatted.audio?.length) {
const wavFile = await WavRecorder.decode(
item.formatted.audio,
24000,
24000
);
item.formatted.file = wavFile;
}
});
// Set up client instructions and tools
client.value.updateSession({ instructions: instructions });
client.value.updateSession({ input_audio_transcription: { model: 'whisper-1' } });
client.value.addTool(
{
name: 'set_memory',
description: 'Saves important data about the user into memory.',
parameters: {
type: 'object',
properties: {
key: {
type: 'string',
description:
'The key of the memory value. Always use lowercase and underscores, no other characters.',
},
value: {
type: 'string',
description: 'Value can be anything represented as a string',
},
},
required: ['key', 'value'],
},
},
async ({ key, value }) => {
memoryKv.value = { ...memoryKv.value, [key]: value };
return { ok: true };
}
);
client.value.addTool(
{
name: 'get_weather',
description:
'Retrieves the weather for a given lat, lng coordinate pair. Specify a label for the location.',
parameters: {
type: 'object',
properties: {
lat: {
type: 'number',
description: 'Latitude',
},
lng: {
type: 'number',
description: 'Longitude',
},
location: {
type: 'string',
description: 'Name of the location',
},
},
required: ['lat', 'lng', 'location'],
},
},
async ({ lat, lng, location }) => {
marker.value = { lat, lng, location };
coords.value = { lat, lng, location };
const result = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lng}&current=temperature_2m,wind_speed_10m`
);
const json = await result.json();
const temperature = {
value: json.current.temperature_2m,
units: json.current_units.temperature_2m,
};
const wind_speed = {
value: json.current.wind_speed_10m,
units: json.current_units.wind_speed_10m,
};
marker.value = { lat, lng, location, temperature, wind_speed };
return json;
}
);
items.value = client.value.conversation.getItems();
});
onUnmounted(() => {
client.value.reset();
});
// Watchers
watch(realtimeEvents, () => {
if (eventsScrollRef.value) {
const eventsEl = eventsScrollRef.value;
eventsEl.scrollTop = eventsEl.scrollHeight;
}
});
watch(items, () => {
const conversationEls = document.querySelectorAll('[data-conversation-content]');
conversationEls.forEach((el) => {
el.scrollTop = el.scrollHeight;
});
});
</script>
<style scoped>
/* You can add your component-specific styles here */
/* If you're using SCSS, you might want to import your existing SCSS file */
/* @import './ConsolePage.scss'; */
</style>

View File

@ -142,6 +142,7 @@ const types = ref([
{ label: "Luma视频", value: "luma" },
{ label: "可灵视频", value: "keling" },
{ label: "Realtime API", value: "realtime" },
{ label: "语音合成", value: "tts" },
{ label: "其他", value: "other" },
]);
const isEdit = ref(false);

View File

@ -126,6 +126,16 @@
</el-form-item>
</div>
<div v-if="item.type === 'tts'">
<el-form-item label="音色" prop="voice">
<el-select v-model="item.options.voice" placeholder="请选择音色">
<el-option v-for="v in voices" :value="v.value" :label="v.label" :key="v.value">
{{ v.label }}
</el-option>
</el-select>
</el-form-item>
</div>
<el-form-item label="绑定API-KEY" prop="apikey">
<el-select v-model="item.key_id" placeholder="请选择 API KEY" filterable clearable>
<el-option v-for="v in apiKeys" :value="v.id" :label="v.name" :key="v.id">
@ -191,6 +201,15 @@ const formRef = ref(null);
const type = ref([
{ label: "聊天", value: "chat" },
{ label: "绘图", value: "img" },
{ label: "语音", value: "tts" },
]);
const voices = ref([
{ label: "Echo", value: "echo" },
{ label: "Fable", value: "fable" },
{ label: "Onyx", value: "onyx" },
{ label: "Nova", value: "nova" },
{ label: "Shimmer", value: "shimmer" },
]);
// API KEY
@ -270,7 +289,7 @@ onUnmounted(() => {
const add = function () {
title.value = "新增模型";
showDialog.value = true;
item.value = { enabled: true, power: 1, open: true, max_tokens: 1024, max_context: 8192, temperature: 0.9 };
item.value = { enabled: true, power: 1, open: true, max_tokens: 1024, max_context: 8192, temperature: 0.9, options: {} };
};
const edit = function (row) {
@ -282,9 +301,6 @@ 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;
item.value.key_id = parseInt(item.value.key_id);

View File

@ -10,13 +10,24 @@
</div>
<el-row>
<el-table :data="users.items" border class="table" :row-key="(row) => row.id" @selection-change="handleSelectionChange" table-layout="auto">
<el-table
:data="users.items"
border
class="table"
:row-key="(row) => row.id"
@selection-change="handleSelectionChange"
table-layout="auto"
>
<el-table-column type="selection" width="38"></el-table-column>
<el-table-column prop="id" label="ID" />
<el-table-column label="账号">
<template #default="scope">
<span>{{ scope.row.username }}</span>
<el-image v-if="scope.row.vip" :src="vipImg" style="height: 20px; position: relative; top: 5px; left: 5px" />
<el-image
v-if="scope.row.vip"
:src="vipImg"
style="height: 20px; position: relative; top: 5px; left: 5px"
/>
</template>
</el-table-column>
<el-table-column prop="mobile" label="手机" />
@ -31,24 +42,42 @@
</el-table-column>
<el-table-column label="过期时间">
<template #default="scope">
<span v-if="scope.row['expired_time']">{{ scope.row["expired_time"] }}</span>
<span v-if="scope.row['expired_time']">{{ scope.row['expired_time'] }}</span>
<el-tag v-else>长期有效</el-tag>
</template>
</el-table-column>
<el-table-column label="注册时间">
<template #default="scope">
<span>{{ dateFormat(scope.row["created_at"]) }}</span>
<span>{{ dateFormat(scope.row['created_at']) }}</span>
</template>
</el-table-column>
<el-table-column fixed="right" label="操作" width="220">
<el-table-column fixed="right" label="操作">
<template #default="scope">
<el-button-group class="ml-4">
<el-button size="small" type="primary" @click="userEdit(scope.row)">编辑</el-button>
<el-button size="small" type="danger" @click="removeUser(scope.row)">删除</el-button>
<el-button size="small" type="success" @click="resetPass(scope.row)">重置密码</el-button>
</el-button-group>
<el-dropdown>
<button
class="px-2 py-1 bg-purple-200 hover:bg-purple-300 rounded text-purple-800 text-sm"
>
<i class="iconfont icon-more-horizontal"></i>
</button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="userEdit(scope.row)">
<i class="iconfont icon-edit mr-1"></i>编辑
</el-dropdown-item>
<el-dropdown-item @click="removeUser(scope.row)" class="text-red-500">
<i class="iconfont icon-remove mr-1"></i>删除
</el-dropdown-item>
<el-dropdown-item @click="resetPass(scope.row)">
<i class="iconfont icon-password mr-1"></i>重置密码
</el-dropdown-item>
<el-dropdown-item @click="genLoginLink(scope.row)">
<i class="iconfont icon-link mr-1"></i>生成登录链接
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</el-table-column>
</el-table>
@ -66,7 +95,12 @@
</div>
</el-row>
<el-dialog v-model="showUserEditDialog" :title="title" :close-on-click-modal="false" width="50%">
<el-dialog
v-model="showUserEditDialog"
:title="title"
:close-on-click-modal="false"
width="50%"
>
<el-form :model="user" label-width="100px" ref="userEditFormRef" :rules="rules">
<el-form-item label="账号:" prop="username">
<el-input v-model="user.username" autocomplete="off" />
@ -96,13 +130,23 @@
</el-form-item>
<el-form-item label="聊天角色" prop="chat_roles">
<el-select v-model="user.chat_roles" multiple :filterable="true" placeholder="选择聊天角色,多选">
<el-select
v-model="user.chat_roles"
multiple
:filterable="true"
placeholder="选择聊天角色,多选"
>
<el-option v-for="item in roles" :key="item.key" :label="item.name" :value="item.key" />
</el-select>
</el-form-item>
<el-form-item label="模型权限" prop="chat_models">
<el-select v-model="user.chat_models" multiple :filterable="true" placeholder="选择AI模型多选">
<el-select
v-model="user.chat_models"
multiple
:filterable="true"
placeholder="选择AI模型多选"
>
<el-option v-for="item in models" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
@ -141,204 +185,241 @@
</span>
</template>
</el-dialog>
<el-dialog v-model="showGenLoginLinkDialog" title="自动登录链接" width="50%">
<el-input v-model="loginLink" readonly disabled />
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="copyLoginLink">复制链接</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { onMounted, reactive, ref } from "vue";
import { httpGet, httpPost } from "@/utils/http";
import { ElMessage, ElMessageBox } from "element-plus";
import { dateFormat, disabledDate } from "@/utils/libs";
import { Delete, Plus, Search } from "@element-plus/icons-vue";
import { onMounted, reactive, ref } from 'vue'
import { httpGet, httpPost } from '@/utils/http'
import { ElMessage, ElMessageBox } from 'element-plus'
import { dateFormat, disabledDate } from '@/utils/libs'
import { Delete, Plus, Search } from '@element-plus/icons-vue'
import { showLoading, closeLoading, showMessageError, showMessageOK } from '@/utils/dialog'
//
const users = ref({ page: 1, page_size: 15, items: [] });
const query = ref({ username: "", page: 1, page_size: 15 });
const users = ref({ page: 1, page_size: 15, items: [] })
const query = ref({ username: '', page: 1, page_size: 15 })
const title = ref("添加用户");
const vipImg = ref("/images/menu/member.png");
const add = ref(true);
const user = ref({ chat_roles: [], chat_models: [] });
const pass = ref({ username: "", password: "", id: 0 });
const roles = ref([]);
const models = ref([]);
const showUserEditDialog = ref(false);
const showResetPassDialog = ref(false);
const title = ref('添加用户')
const vipImg = ref('/images/menu/member.png')
const add = ref(true)
const user = ref({ chat_roles: [], chat_models: [] })
const pass = ref({ username: '', password: '', id: 0 })
const roles = ref([])
const models = ref([])
const showUserEditDialog = ref(false)
const showResetPassDialog = ref(false)
const rules = reactive({
username: [{ required: true, message: "请输入账号", trigger: "blur" }],
username: [{ required: true, message: '请输入账号', trigger: 'blur' }],
password: [
{
required: true,
validator: (rule, value) => {
return !(value.length > 16 || value.length < 8);
return !(value.length > 16 || value.length < 8)
},
message: "密码必须为8-16",
trigger: "blur",
message: '密码必须为8-16',
trigger: 'blur',
},
],
calls: [
{ required: true, message: "请输入提问次数" },
{ type: "number", message: "请输入有效数字" },
{ required: true, message: '请输入提问次数' },
{ type: 'number', message: '请输入有效数字' },
],
chat_roles: [{ required: true, message: "请选择聊天角色", trigger: "change" }],
chat_models: [{ required: true, message: "请选择AI模型", trigger: "change" }],
});
const loading = ref(true);
chat_roles: [{ required: true, message: '请选择聊天角色', trigger: 'change' }],
chat_models: [{ required: true, message: '请选择AI模型', trigger: 'change' }],
})
const loading = ref(true)
const userEditFormRef = ref(null);
const userEditFormRef = ref(null)
onMounted(() => {
fetchUserList(users.value.page, users.value.page_size);
fetchUserList(users.value.page, users.value.page_size)
//
httpGet("/api/admin/role/list")
httpGet('/api/admin/role/list')
.then((res) => {
roles.value = res.data;
roles.value = res.data
})
.catch(() => {
ElMessage.error("获取聊天角色失败");
});
ElMessage.error('获取聊天角色失败')
})
httpGet("/api/admin/model/list")
httpGet('/api/admin/model/list')
.then((res) => {
models.value = res.data;
models.value = res.data
})
.catch((e) => {
ElMessage.error("获取模型失败:" + e.message);
});
});
ElMessage.error('获取模型失败:' + e.message)
})
})
const fetchUserList = function (page, pageSize) {
query.value.page = page;
query.value.page_size = pageSize;
httpGet("/api/admin/user/list", query.value)
query.value.page = page
query.value.page_size = pageSize
httpGet('/api/admin/user/list', query.value)
.then((res) => {
if (res.data) {
//
const arr = res.data.items;
const arr = res.data.items
for (let i = 0; i < arr.length; i++) {
arr[i].expired_time = dateFormat(arr[i].expired_time);
arr[i].expired_time = dateFormat(arr[i].expired_time)
}
users.value.items = arr;
users.value.total = res.data.total;
users.value.page = res.data.page;
user.value.page_size = res.data.page_size;
users.value.items = arr
users.value.total = res.data.total
users.value.page = res.data.page
user.value.page_size = res.data.page_size
}
loading.value = false;
loading.value = false
})
.catch(() => {
ElMessage.error("加载用户列表失败");
});
};
ElMessage.error('加载用户列表失败')
})
}
const handleSearch = () => {
fetchUserList(users.value.page, users.value.page_size);
};
fetchUserList(users.value.page, users.value.page_size)
}
//
const removeUser = function (user) {
ElMessageBox.confirm("此操作将会永久删除用户信息和聊天记录,确认操作吗?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
ElMessageBox.confirm('此操作将会永久删除用户信息和聊天记录,确认操作吗?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
httpGet("/api/admin/user/remove", { id: user.id })
httpGet('/api/admin/user/remove', { id: user.id })
.then(() => {
ElMessage.success("操作成功!");
fetchUserList(users.value.page, users.value.page_size);
ElMessage.success('操作成功!')
fetchUserList(users.value.page, users.value.page_size)
})
.catch((e) => {
ElMessage.error("操作失败," + e.message);
});
ElMessage.error('操作失败,' + e.message)
})
})
.catch(() => {
ElMessage.info("操作被取消");
});
};
ElMessage.info('操作被取消')
})
}
const userEdit = function (row) {
user.value = row;
title.value = "编辑用户";
showUserEditDialog.value = true;
add.value = false;
};
user.value = row
title.value = '编辑用户'
showUserEditDialog.value = true
add.value = false
}
const addUser = () => {
user.value = { chat_id: 0, chat_roles: [], chat_models: [] };
title.value = "添加用户";
showUserEditDialog.value = true;
add.value = true;
};
user.value = { chat_id: 0, chat_roles: [], chat_models: [] }
title.value = '添加用户'
showUserEditDialog.value = true
add.value = true
}
const saveUser = function () {
userEditFormRef.value.validate((valid) => {
if (valid) {
showUserEditDialog.value = false;
console.log(user.value);
httpPost("/api/admin/user/save", user.value)
showUserEditDialog.value = false
console.log(user.value)
httpPost('/api/admin/user/save', user.value)
.then((res) => {
ElMessage.success("操作成功!");
ElMessage.success('操作成功!')
if (add.value) {
users.value.items.push(res.data);
users.value.items.push(res.data)
}
})
.catch((e) => {
ElMessage.error("操作失败," + e.message);
});
ElMessage.error('操作失败,' + e.message)
})
} else {
return false;
return false
}
});
};
})
}
const userIds = ref([]);
const userIds = ref([])
const handleSelectionChange = function (rows) {
userIds.value = [];
userIds.value = []
rows.forEach((row) => {
userIds.value.push(row.id);
});
};
userIds.value.push(row.id)
})
}
const multipleDelete = function () {
ElMessageBox.confirm("此操作将会永久删除用户信息和聊天记录,确认操作吗?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
ElMessageBox.confirm('此操作将会永久删除用户信息和聊天记录,确认操作吗?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
loading.value = true;
httpGet("/api/admin/user/remove", { ids: userIds.value })
loading.value = true
httpGet('/api/admin/user/remove', { ids: userIds.value })
.then(() => {
ElMessage.success("操作成功!");
fetchUserList(users.value.page, users.value.page_size);
loading.value = false;
ElMessage.success('操作成功!')
fetchUserList(users.value.page, users.value.page_size)
loading.value = false
})
.catch((e) => {
ElMessage.error("操作失败," + e.message);
loading.value = false;
});
ElMessage.error('操作失败,' + e.message)
loading.value = false
})
})
.catch(() => {
ElMessage.info("操作被取消");
});
};
ElMessage.info('操作被取消')
})
}
//
const resetPass = (row) => {
showResetPassDialog.value = true;
pass.value.id = row.id;
pass.value.username = row.username;
};
showResetPassDialog.value = true
pass.value.id = row.id
pass.value.username = row.username
}
const doResetPass = () => {
httpPost("/api/admin/user/resetPass", pass.value)
httpPost('/api/admin/user/resetPass', pass.value)
.then(() => {
ElMessage.success("操作成功!");
showResetPassDialog.value = false;
ElMessage.success('操作成功!')
showResetPassDialog.value = false
})
.catch((e) => {
ElMessage.error("操作失败," + e.message);
});
};
ElMessage.error('操作失败,' + e.message)
})
}
//
const showGenLoginLinkDialog = ref(false)
const loginLink = ref('')
const genLoginLink = (row) => {
showLoading()
httpGet('/api/admin/user/genLoginLink', { id: row.id })
.then((res) => {
loginLink.value = `${window.location.origin}/login?token=${res.data}`
showGenLoginLinkDialog.value = true
closeLoading()
})
.catch((e) => {
ElMessage.error('操作失败,' + e.message)
closeLoading()
})
}
const copyLoginLink = () => {
try {
navigator.clipboard.writeText(loginLink.value)
showMessageOK('复制成功!')
} catch (e) {
showMessageError('复制失败')
}
}
</script>
<style lang="stylus" scoped>

View File

@ -3,8 +3,18 @@
<el-tabs v-model="activeName" @tab-change="handleChange">
<el-tab-pane label="对话列表" name="chat" v-loading="data.chat.loading">
<div class="handle-box">
<el-input v-model.number="data.chat.query.user_id" placeholder="账户ID" class="handle-input mr10" @keyup="searchChat($event)"></el-input>
<el-input v-model="data.chat.query.title" placeholder="对话标题" class="handle-input mr10" @keyup="searchChat($event)"></el-input>
<el-input
v-model.number="data.chat.query.user_id"
placeholder="账户ID"
class="handle-input mr10"
@keyup="searchChat($event)"
></el-input>
<el-input
v-model="data.chat.query.title"
placeholder="对话标题"
class="handle-input mr10"
@keyup="searchChat($event)"
></el-input>
<el-date-picker
v-model="data.chat.query.created_at"
type="daterange"
@ -38,13 +48,15 @@
<el-table-column label="创建时间">
<template #default="scope">
<span>{{ dateFormat(scope.row["created_at"]) }}</span>
<span>{{ dateFormat(scope.row['created_at']) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="180">
<template #default="scope">
<el-button size="small" type="primary" @click="showMessages(scope.row)">查看</el-button>
<el-button size="small" type="primary" @click="showMessages(scope.row)"
>查看</el-button
>
<el-popconfirm title="确定要删除当前记录吗?" @confirm="removeChat(scope.row)">
<template #reference>
<el-button size="small" type="danger">删除</el-button>
@ -70,9 +82,24 @@
</el-tab-pane>
<el-tab-pane label="消息记录" name="message">
<div class="handle-box">
<el-input v-model.number="data.message.query.user_id" placeholder="账户ID" class="handle-input mr10" @keyup="searchMessage($event)"></el-input>
<el-input v-model="data.message.query.content" placeholder="消息内容" class="handle-input mr10" @keyup="searchMessage($event)"></el-input>
<el-input v-model="data.message.query.model" placeholder="模型" class="handle-input mr10" @keyup="searchMessage($event)"></el-input>
<el-input
v-model.number="data.message.query.user_id"
placeholder="账户ID"
class="handle-input mr10"
@keyup="searchMessage($event)"
></el-input>
<el-input
v-model="data.message.query.content"
placeholder="消息内容"
class="handle-input mr10"
@keyup="searchMessage($event)"
></el-input>
<el-input
v-model="data.message.query.model"
placeholder="模型"
class="handle-input mr10"
@keyup="searchMessage($event)"
></el-input>
<el-date-picker
v-model="data.message.query.created_at"
type="daterange"
@ -108,13 +135,15 @@
<el-table-column label="创建时间">
<template #default="scope">
<span>{{ dateFormat(scope.row["created_at"]) }}</span>
<span>{{ dateFormat(scope.row['created_at']) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="180">
<template #default="scope">
<el-button size="small" type="primary" @click="showContent(scope.row.content)">查看</el-button>
<el-button size="small" type="primary" @click="showContent(scope.row.content)"
>查看</el-button
>
<el-popconfirm title="确定要删除当前记录吗?" @confirm="removeMessage(scope.row)">
<template #reference>
<el-button size="small" type="danger">删除</el-button>
@ -140,13 +169,23 @@
</el-tab-pane>
</el-tabs>
<el-dialog v-model="showContentDialog" title="消息详情" class="chat-dialog" style="--el-dialog-width: 60%">
<el-dialog
v-model="showContentDialog"
title="消息详情"
class="chat-dialog"
style="--el-dialog-width: 60%"
>
<div class="chat-detail">
<div class="chat-line" v-html="dialogContent"></div>
</div>
</el-dialog>
<el-dialog v-model="showChatItemDialog" title="对话详情" class="chat-dialog" style="--el-dialog-width: 60%">
<el-dialog
v-model="showChatItemDialog"
title="对话详情"
class="chat-dialog"
style="--el-dialog-width: 60%"
>
<div class="chat-box chat-page">
<div v-for="item in messages" :key="item.id">
<chat-prompt v-if="item.type === 'prompt'" :data="item" />
@ -159,21 +198,21 @@
</template>
<script setup>
import { onMounted, ref } from "vue";
import { httpGet, httpPost } from "@/utils/http";
import { ElMessage } from "element-plus";
import { dateFormat, processContent } from "@/utils/libs";
import { Search } from "@element-plus/icons-vue";
import "highlight.js/styles/a11y-dark.css";
import hl from "highlight.js";
import ChatPrompt from "@/components/ChatPrompt.vue";
import ChatReply from "@/components/ChatReply.vue";
import ChatPrompt from '@/components/ChatPrompt.vue'
import ChatReply from '@/components/ChatReply.vue'
import { httpGet, httpPost } from '@/utils/http'
import { dateFormat, processContent } from '@/utils/libs'
import { Search } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import hl from 'highlight.js'
import 'highlight.js/styles/a11y-dark.css'
import { onMounted, ref } from 'vue'
//
const data = ref({
chat: {
items: [],
query: { title: "", created_at: [], page: 1, page_size: 15 },
query: { title: '', created_at: [], page: 1, page_size: 15 },
total: 0,
page: 1,
pageSize: 15,
@ -181,104 +220,104 @@ const data = ref({
},
message: {
items: [],
query: { title: "", created_at: [], page: 1, page_size: 15 },
query: { title: '', created_at: [], page: 1, page_size: 15 },
total: 0,
page: 1,
pageSize: 15,
loading: true,
},
});
const activeName = ref("chat");
})
const activeName = ref('chat')
onMounted(() => {
fetchChatData();
});
fetchChatData()
})
const handleChange = (tab) => {
if (tab === "chat") {
fetchChatData();
} else if (tab === "message") {
fetchMessageData();
if (tab === 'chat') {
fetchChatData()
} else if (tab === 'message') {
fetchMessageData()
}
};
}
//
const searchChat = (evt) => {
if (evt.keyCode === 13) {
fetchChatData();
fetchChatData()
}
};
}
//
const searchMessage = (evt) => {
if (evt.keyCode === 13) {
fetchMessageData();
fetchMessageData()
}
};
}
//
const fetchChatData = () => {
const d = data.value.chat;
d.query.page = d.page;
d.query.page_size = d.pageSize;
httpPost("/api/admin/chat/list", d.query)
const d = data.value.chat
d.query.page = d.page
d.query.page_size = d.pageSize
httpPost('/api/admin/chat/list', d.query)
.then((res) => {
if (res.data) {
d.items = res.data.items;
d.total = res.data.total;
d.page = res.data.page;
d.pageSize = res.data.page_size;
d.items = res.data.items
d.total = res.data.total
d.page = res.data.page
d.pageSize = res.data.page_size
}
d.loading = false;
d.loading = false
})
.catch((e) => {
ElMessage.error("获取数据失败:" + e.message);
});
};
ElMessage.error('获取数据失败:' + e.message)
})
}
const fetchMessageData = () => {
const d = data.value.message;
d.query.page = d.page;
d.query.page_size = d.pageSize;
httpPost("/api/admin/chat/message", d.query)
const d = data.value.message
d.query.page = d.page
d.query.page_size = d.pageSize
httpPost('/api/admin/chat/message', d.query)
.then((res) => {
if (res.data) {
d.items = res.data.items;
d.total = res.data.total;
d.page = res.data.page;
d.pageSize = res.data.page_size;
d.items = res.data.items
d.total = res.data.total
d.page = res.data.page
d.pageSize = res.data.page_size
}
d.loading = false;
d.loading = false
})
.catch((e) => {
ElMessage.error("获取数据失败:" + e.message);
});
};
ElMessage.error('获取数据失败:' + e.message)
})
}
const removeChat = function (row) {
httpGet("/api/admin/chat/remove?chat_id=" + row.chat_id)
httpGet('/api/admin/chat/remove?chat_id=' + row.chat_id)
.then(() => {
ElMessage.success("删除成功!");
fetchChatData();
ElMessage.success('删除成功!')
fetchChatData()
})
.catch((e) => {
ElMessage.error("删除失败:" + e.message);
});
};
ElMessage.error('删除失败:' + e.message)
})
}
const removeMessage = function (row) {
httpGet("/api/admin/chat/message/remove?id=" + row.id)
httpGet('/api/admin/chat/message/remove?id=' + row.id)
.then(() => {
ElMessage.success("删除成功!");
fetchMessageData();
ElMessage.success('删除成功!')
fetchMessageData()
})
.catch((e) => {
ElMessage.error("删除失败:" + e.message);
});
};
ElMessage.error('删除失败:' + e.message)
})
}
const mathjaxPlugin = require("markdown-it-mathjax3");
const md = require("markdown-it")({
const mathjaxPlugin = require('markdown-it-mathjax3')
const md = require('markdown-it')({
breaks: true,
html: true,
linkify: true,
@ -286,43 +325,43 @@ const md = require("markdown-it")({
highlight: function (str, lang) {
if (lang && hl.getLanguage(lang)) {
//
const preCode = hl.highlight(lang, str, true).value;
const preCode = hl.highlight(lang, str, true).value
// pre
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code></pre>`;
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code></pre>`
}
//
const preCode = md.utils.escapeHtml(str);
const preCode = md.utils.escapeHtml(str)
// pre
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code></pre>`;
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code></pre>`
},
});
md.use(mathjaxPlugin);
})
md.use(mathjaxPlugin)
const showContentDialog = ref(false);
const dialogContent = ref("");
const showContentDialog = ref(false)
const dialogContent = ref('')
const showContent = (content) => {
showContentDialog.value = true;
dialogContent.value = md.render(processContent(content));
};
showContentDialog.value = true
dialogContent.value = md.render(processContent(content))
}
const showChatItemDialog = ref(false);
const messages = ref([]);
const showChatItemDialog = ref(false)
const messages = ref([])
const showMessages = (row) => {
showChatItemDialog.value = true;
messages.value = [];
httpGet("/api/admin/chat/history?chat_id=" + row.chat_id)
showChatItemDialog.value = true
messages.value = []
httpGet('/api/admin/chat/history?chat_id=' + row.chat_id)
.then((res) => {
const data = res.data;
const data = res.data
for (let i = 0; i < data.length; i++) {
messages.value.push(data[i]);
messages.value.push(data[i])
}
})
.catch((e) => {
// TODO:
ElMessage.error("加载聊天记录失败:" + e.message);
});
};
ElMessage.error('加载聊天记录失败:' + e.message)
})
}
</script>
<style lang="stylus" scoped>
@ -380,7 +419,7 @@ const showMessages = (row) => {
font-size: 14px;
display: flex;
align-items: flex-start;
justify-content: flex-start;
}
}

View File

@ -0,0 +1,268 @@
<template>
<div class="apps-page">
<van-nav-bar title="全部应用" left-arrow @click-left="router.back()" />
<div class="apps-filter mb-8 pt-8" style="border: 1px solid #ccc">
<van-tabs v-model="activeTab" animated swipeable>
<van-tab title="全部分类">
<div class="app-list">
<van-list v-model="loading" :finished="true" finished-text="" @load="fetchApps()">
<van-cell v-for="item in apps" :key="item.id" class="app-cell">
<div class="app-card">
<div class="app-info">
<div class="app-image">
<van-image :src="item.icon" round />
</div>
<div class="app-detail">
<div class="app-title">{{ item.name }}</div>
<div class="app-desc">{{ item.hello_msg }}</div>
</div>
</div>
<div class="app-actions">
<van-button
size="small"
type="primary"
class="action-btn"
@click="useRole(item.id)"
>对话</van-button
>
<van-button
size="small"
:type="hasRole(item.key) ? 'danger' : 'success'"
class="action-btn"
@click="updateRole(item, hasRole(item.key) ? 'remove' : 'add')"
>
{{ hasRole(item.key) ? '移除' : '添加' }}
</van-button>
</div>
</div>
</van-cell>
</van-list>
</div>
</van-tab>
<van-tab v-for="type in appTypes" :key="type.id" :title="type.name">
<div class="app-list">
<van-list
v-model="loading"
:finished="true"
finished-text=""
@load="fetchApps(type.id)"
>
<van-cell v-for="item in typeApps" :key="item.id" class="app-cell">
<div class="app-card">
<div class="app-info">
<div class="app-image">
<van-image :src="item.icon" round />
</div>
<div class="app-detail">
<div class="app-title">{{ item.name }}</div>
<div class="app-desc">{{ item.hello_msg }}</div>
</div>
</div>
<div class="app-actions">
<van-button
size="small"
type="primary"
class="action-btn"
@click="useRole(item.id)"
>对话</van-button
>
<van-button
size="small"
:type="hasRole(item.key) ? 'danger' : 'success'"
class="action-btn"
@click="updateRole(item, hasRole(item.key) ? 'remove' : 'add')"
>
{{ hasRole(item.key) ? '移除' : '添加' }}
</van-button>
</div>
</div>
</van-cell>
</van-list>
</div>
</van-tab>
</van-tabs>
</div>
</div>
</template>
<script setup>
import { checkSession } from '@/store/cache'
import { httpGet, httpPost } from '@/utils/http'
import { arrayContains, removeArrayItem, showLoginDialog, substr } from '@/utils/libs'
import { showNotify } from 'vant'
import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const isLogin = ref(false)
const apps = ref([])
const typeApps = ref([])
const appTypes = ref([])
const loading = ref(false)
const roles = ref([])
const activeTab = ref(0)
onMounted(() => {
checkSession()
.then((user) => {
isLogin.value = true
roles.value = user.chat_roles
})
.catch(() => {})
fetchAppTypes()
fetchApps()
})
const fetchAppTypes = () => {
httpGet('/api/app/type/list')
.then((res) => {
appTypes.value = res.data
})
.catch((e) => {
showNotify({ type: 'danger', message: '获取应用分类失败:' + e.message })
})
}
const fetchApps = (typeId = '') => {
httpGet('/api/app/list', { tid: typeId })
.then((res) => {
const items = res.data
// hello message
for (let i = 0; i < items.length; i++) {
items[i].intro = substr(items[i].hello_msg, 80)
}
if (typeId) {
typeApps.value = items
} else {
apps.value = items
}
})
.catch((e) => {
showNotify({ type: 'danger', message: '获取应用失败:' + e.message })
})
}
const updateRole = (row, opt) => {
if (!isLogin.value) {
return showLoginDialog(router)
}
const title = ref('')
if (opt === 'add') {
title.value = '添加应用'
const exists = arrayContains(roles.value, row.key)
if (exists) {
return
}
roles.value.push(row.key)
} else {
title.value = '移除应用'
const exists = arrayContains(roles.value, row.key)
if (!exists) {
return
}
roles.value = removeArrayItem(roles.value, row.key)
}
httpPost('/api/app/update', { keys: roles.value })
.then(() => {
showNotify({ type: 'success', message: title.value + '成功!' })
})
.catch((e) => {
showNotify({ type: 'danger', message: title.value + '失败:' + e.message })
})
}
const hasRole = (roleKey) => {
return arrayContains(roles.value, roleKey, (v1, v2) => v1 === v2)
}
const useRole = (roleId) => {
if (!isLogin.value) {
return showLoginDialog(router)
}
router.push(`/mobile/chat/session?role_id=${roleId}`)
}
</script>
<style scoped lang="stylus">
.apps-page {
min-height 100vh
background-color var(--van-background)
.apps-filter {
padding 10px 0
:deep(.van-tabs__nav) {
background var(--van-background-2)
}
}
.app-list {
padding 0 15px
.app-cell {
padding 0
margin-bottom 15px
.app-card {
background var(--van-cell-background)
border-radius 12px
padding 15px
box-shadow 0 2px 12px rgba(0, 0, 0, 0.05)
.app-info {
display flex
align-items center
margin-bottom 15px
.app-image {
width 60px
height 60px
margin-right 15px
:deep(.van-image) {
width 100%
height 100%
}
}
.app-detail {
flex 1
.app-title {
font-size 16px
font-weight 600
margin-bottom 5px
color var(--van-text-color)
}
.app-desc {
font-size 13px
color var(--van-gray-6)
display -webkit-box
-webkit-box-orient vertical
-webkit-line-clamp 2
overflow hidden
}
}
}
.app-actions {
display flex
gap 10px
.action-btn {
flex 1
border-radius 20px
padding 0 10px
}
}
}
}
}
}
</style>

View File

@ -1,60 +1,79 @@
<template>
<div class="index container">
<h2 class="title">{{ title }}</h2>
<van-notice-bar left-icon="info-o" :scrollable="true">{{ slogan }}}</van-notice-bar>
<div class="header">
<h2 class="title">{{ title }}</h2>
</div>
<div class="content">
<van-grid :column-num="3" :gutter="10" border>
<van-grid-item @click="router.push('chat')">
<template #icon>
<i class="iconfont icon-chat"></i>
</template>
<template #text>
<div class="text">AI 对话</div>
</template>
</van-grid-item>
<div class="content mb-8">
<div class="feature-grid">
<van-grid :column-num="3" :gutter="15" border>
<van-grid-item @click="router.push('chat')" class="feature-item">
<template #icon>
<div class="feature-icon">
<i class="iconfont icon-chat"></i>
</div>
</template>
<template #text>
<div class="text">AI 对话</div>
</template>
</van-grid-item>
<van-grid-item @click="router.push('image')">
<template #icon>
<i class="iconfont icon-mj"></i>
</template>
<template #text>
<div class="text">AI 绘画</div>
</template>
</van-grid-item>
<van-grid-item @click="router.push('image')" class="feature-item">
<template #icon>
<div class="feature-icon">
<i class="iconfont icon-mj"></i>
</div>
</template>
<template #text>
<div class="text">AI 绘画</div>
</template>
</van-grid-item>
<van-grid-item @click="router.push('imgWall')">
<template #icon>
<van-icon name="photo-o" />
</template>
<template #text>
<div class="text">AI 画廊</div>
</template>
</van-grid-item>
</van-grid>
<van-grid-item @click="router.push('imgWall')" class="feature-item">
<template #icon>
<div class="feature-icon">
<van-icon name="photo-o" />
</div>
</template>
<template #text>
<div class="text">AI 画廊</div>
</template>
</van-grid-item>
</van-grid>
</div>
<div class="section-header">
<h3 class="section-title">推荐应用</h3>
<van-button class="more-btn" size="small" icon="arrow" @click="router.push('apps')"
>更多</van-button
>
</div>
<div class="app-list">
<van-list v-model:loading="loading" :finished="true" finished-text="" @load="fetchApps">
<van-cell v-for="item in apps" :key="item.id">
<div>
<div class="item">
<div class="image flex justify-center items-center">
<van-image :src="item.icon" />
<van-list v-model="loading" :finished="true" finished-text="" @load="fetchApps">
<van-cell v-for="item in displayApps" :key="item.id" class="app-cell">
<div class="app-card">
<div class="app-info">
<div class="app-image">
<van-image :src="item.icon" round />
</div>
<div class="info">
<div class="info-title">{{ item.name }}</div>
<div class="info-text">{{ item.hello_msg }}</div>
<div class="app-detail">
<div class="app-title">{{ item.name }}</div>
<div class="app-desc">{{ item.hello_msg }}</div>
</div>
</div>
<div class="btn">
<div v-if="hasRole(item.key)">
<van-button size="small" type="success" @click="useRole(item.id)">使用</van-button>
<van-button size="small" type="danger" @click="updateRole(item, 'remove')">移除</van-button>
</div>
<van-button v-else size="small" style="--el-color-primary: #009999" @click="updateRole(item, 'add')">
<van-icon name="add-o" />
<span>添加应用</span>
<div class="app-actions">
<van-button size="small" type="primary" class="action-btn" @click="useRole(item.id)"
>对话</van-button
>
<van-button
size="small"
:type="hasRole(item.key) ? 'danger' : 'success'"
class="action-btn"
@click="updateRole(item, hasRole(item.key) ? 'remove' : 'add')"
>
{{ hasRole(item.key) ? '移除' : '添加' }}
</van-button>
</div>
</div>
@ -66,173 +85,245 @@
</template>
<script setup>
import { onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import { checkSession, getSystemInfo } from "@/store/cache";
import { httpGet, httpPost } from "@/utils/http";
import { arrayContains, removeArrayItem, showLoginDialog, substr } from "@/utils/libs";
import { showNotify } from "vant";
import { ElMessage } from "element-plus";
import { checkSession, getSystemInfo } from '@/store/cache'
import { httpGet, httpPost } from '@/utils/http'
import { arrayContains, removeArrayItem, showLoginDialog, substr } from '@/utils/libs'
import { ElMessage } from 'element-plus'
import { showNotify } from 'vant'
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
const title = ref(process.env.VUE_APP_TITLE);
const router = useRouter();
const isLogin = ref(false);
const apps = ref([]);
const loading = ref(false);
const roles = ref([]);
const slogan = ref("你有多大想象力AI就有多大创造力");
const title = ref(process.env.VUE_APP_TITLE)
const router = useRouter()
const isLogin = ref(false)
const apps = ref([])
const loading = ref(false)
const roles = ref([])
const slogan = ref('你有多大想象力AI就有多大创造力')
// 5
const displayApps = computed(() => {
return apps.value.slice(0, 8)
})
onMounted(() => {
getSystemInfo()
.then((res) => {
title.value = res.data.title;
title.value = res.data.title
if (res.data.slogan) {
slogan.value = res.data.slogan;
slogan.value = res.data.slogan
}
})
.catch((e) => {
ElMessage.error("获取系统配置失败:" + e.message);
});
ElMessage.error('获取系统配置失败:' + e.message)
})
checkSession()
.then((user) => {
isLogin.value = true;
roles.value = user.chat_roles;
isLogin.value = true
roles.value = user.chat_roles
})
.catch(() => {});
fetchApps();
});
.catch(() => {})
fetchApps()
})
const fetchApps = () => {
httpGet("/api/app/list")
httpGet('/api/app/list')
.then((res) => {
const items = res.data;
const items = res.data
// hello message
for (let i = 0; i < items.length; i++) {
items[i].intro = substr(items[i].hello_msg, 80);
items[i].intro = substr(items[i].hello_msg, 80)
}
apps.value = items;
apps.value = items
})
.catch((e) => {
showNotify({ type: "danger", message: "获取应用失败:" + e.message });
});
};
showNotify({ type: 'danger', message: '获取应用失败:' + e.message })
})
}
const updateRole = (row, opt) => {
if (!isLogin.value) {
return showLoginDialog(router);
return showLoginDialog(router)
}
const title = ref("");
if (opt === "add") {
title.value = "添加应用";
const exists = arrayContains(roles.value, row.key);
const title = ref('')
if (opt === 'add') {
title.value = '添加应用'
const exists = arrayContains(roles.value, row.key)
if (exists) {
return;
return
}
roles.value.push(row.key);
roles.value.push(row.key)
} else {
title.value = "移除应用";
const exists = arrayContains(roles.value, row.key);
title.value = '移除应用'
const exists = arrayContains(roles.value, row.key)
if (!exists) {
return;
return
}
roles.value = removeArrayItem(roles.value, row.key);
roles.value = removeArrayItem(roles.value, row.key)
}
httpPost("/api/role/update", { keys: roles.value })
httpPost('/api/app/update', { keys: roles.value })
.then(() => {
ElMessage.success({ message: title.value + "成功!", duration: 1000 });
ElMessage.success({ message: title.value + '成功!', duration: 1000 })
})
.catch((e) => {
ElMessage.error(title.value + "失败:" + e.message);
});
};
ElMessage.error(title.value + '失败:' + e.message)
})
}
const hasRole = (roleKey) => {
return arrayContains(roles.value, roleKey, (v1, v2) => v1 === v2);
};
return arrayContains(roles.value, roleKey, (v1, v2) => v1 === v2)
}
const useRole = (roleId) => {
if (!isLogin.value) {
return showLoginDialog(router);
return showLoginDialog(router)
}
router.push(`/mobile/chat/session?role_id=${roleId}`);
};
router.push(`/mobile/chat/session?role_id=${roleId}`)
}
</script>
<style scoped lang="stylus">
.index {
color var(--van-text-color)
.title {
display flex
justify-content center
background-color var(--van-background)
min-height 100vh
display flex
flex-direction column
.header {
flex-shrink 0
padding 10px 15px
text-align center
background var(--van-background)
position sticky
top 0
z-index 1
.title {
font-size 24px
font-weight 600
color var(--van-text-color)
}
.slogan {
font-size 14px
color var(--van-gray-6)
}
}
--van-notice-bar-font-size: 16px
.content {
padding 15px 0 60px 0
.van-grid-item {
.iconfont {
font-size 20px
flex 1
overflow-y auto
padding 15px
-webkit-overflow-scrolling touch
.feature-grid {
margin-bottom 30px
.feature-item {
padding 15px 0
.feature-icon {
width 50px
height 50px
border-radius 50%
background var(--van-primary-color)
display flex
align-items center
justify-content center
margin-bottom 10px
i, .van-icon {
font-size 24px
color white
}
}
.text {
font-size 14px
font-weight 500
}
}
.text {
display flex
width 100%
padding 10px
justify-content center
font-size 14px
}
.section-header {
display flex
justify-content space-between
align-items center
margin-bottom 15px
.section-title {
font-size 18px
font-weight 600
color var(--van-text-color)
}
.more-btn {
padding 0 10px
font-size 12px
border-radius 15px
}
}
.app-list {
padding-top 10px
.app-cell {
padding 0
margin-bottom 15px
.item {
display flex
.image {
width 80px
height 80px
min-width 80px
border-radius 5px
overflow hidden
}
.app-card {
background var(--van-cell-background)
border-radius 12px
padding 15px
box-shadow 0 2px 12px rgba(0, 0, 0, 0.05)
.info {
text-align left
padding 0 10px
.info-title {
color var(--van-text-color)
font-size 1.25rem
line-height 1.75rem
letter-spacing: .025em;
font-weight: 600;
word-break: break-all;
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
.app-info {
display flex
align-items center
margin-bottom 15px
.app-image {
width 60px
height 60px
margin-right 15px
:deep(.van-image) {
width 100%
height 100%
}
}
.app-detail {
flex 1
.app-title {
font-size 16px
font-weight 600
margin-bottom 5px
color var(--van-text-color)
}
.app-desc {
font-size 13px
color var(--van-gray-6)
display -webkit-box
-webkit-box-orient vertical
-webkit-line-clamp 2
overflow hidden
}
}
}
.info-text {
padding 5px 0
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
word-break: break-all;
font-size: .875rem;
}
}
}
.app-actions {
display flex
gap 10px
.btn {
padding 5px 0
.van-button {
margin-right 10px
.van-icon {
margin-right 5px
.action-btn {
flex 1
border-radius 20px
padding 0 10px
}
}
}
}

View File

@ -24,6 +24,9 @@ module.exports = defineConfig({
outputDir: "dist",
crossorigin: "anonymous",
devServer: {
client: {
overlay: false // 关闭错误覆盖层
},
allowedHosts: "all",
port: 8888,
proxy: {