mirror of
https://github.com/yangjian102621/geekai.git
synced 2025-12-25 01:25:59 +08:00
Compare commits
73 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1acca6f7a | ||
|
|
ccfc9f17e9 | ||
|
|
4641482865 | ||
|
|
79522d9ab5 | ||
|
|
e0b4e8970a | ||
|
|
4d93e901e0 | ||
|
|
bcc72a3091 | ||
|
|
1c1ddf76fb | ||
|
|
04caf92702 | ||
|
|
c797b35f5a | ||
|
|
0746cd49f4 | ||
|
|
a3a2500498 | ||
|
|
ff69cb231a | ||
|
|
afb9193985 | ||
|
|
14fa4fdaa0 | ||
|
|
2a71d5d557 | ||
|
|
cd31333d0c | ||
|
|
f080425ee6 | ||
|
|
96dd0ddb99 | ||
|
|
a4ee5cdeff | ||
|
|
d0025032b0 | ||
|
|
43f00b1481 | ||
|
|
f580f671a3 | ||
|
|
b1fb16995a | ||
|
|
47907b9f0c | ||
|
|
ba55fca7cc | ||
|
|
f687a10416 | ||
|
|
393bfa137e | ||
|
|
4dcb0d850c | ||
|
|
88fa374104 | ||
|
|
e1b1c195f6 | ||
|
|
1352369af0 | ||
|
|
ded041da0f | ||
|
|
7b9a7475a9 | ||
|
|
3958e99e4d | ||
|
|
0ef51714c9 | ||
|
|
668ff70bc1 | ||
|
|
ed063a1d9d | ||
|
|
88eaddbd1d | ||
|
|
8369e18bf0 | ||
|
|
79b9476d3d | ||
|
|
41bfa3a974 | ||
|
|
6b0d4e81bf | ||
|
|
41e66d85d5 | ||
|
|
f98dcee7d4 | ||
|
|
04b364c1cd | ||
|
|
6c84d2557c | ||
|
|
8a4596b36a | ||
|
|
6e2deeed87 | ||
|
|
bf6834da4e | ||
|
|
68dcd054f9 | ||
|
|
a77bebbc29 | ||
|
|
36beb74de8 | ||
|
|
dd1f98db1e | ||
|
|
b7f41c524a | ||
|
|
a3f0576535 | ||
|
|
5c8a237e27 | ||
|
|
447adf45eb | ||
|
|
ca77288a69 | ||
|
|
63be3f5f56 | ||
|
|
cad1ce6943 | ||
|
|
54b5a78c0e | ||
|
|
98d4d58393 | ||
|
|
887fdb6679 | ||
|
|
63fd125439 | ||
|
|
c39dd913fd | ||
|
|
b40f7ed5f3 | ||
|
|
183829a08b | ||
|
|
03d33c784c | ||
|
|
eec10fdfbc | ||
|
|
0a3c74cd6f | ||
|
|
5768c7959e | ||
|
|
d124eddd9d |
23
CHANGELOG.md
23
CHANGELOG.md
@@ -1,5 +1,28 @@
|
||||
# 更新日志
|
||||
|
||||
## 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
|
||||
|
||||
- 功能新增:新增支持可灵生成视频,支持文生视频,图生生视频。
|
||||
- Bug 修复:修复手机端登录页面 Logo 无法修改的问题。
|
||||
- 功能新增:重构所有异步任务(绘图,音乐,视频)更新方式,使用 http pull 来替代 websocket。
|
||||
- 功能优化:优化 Luma 图生视频功能,支持本地上传图片和远程图片。
|
||||
- Bug 修复:修复移动端聊天页面新建对话时候角色没有更模型绑定的 Bug。
|
||||
- 功能优化:优化聊天页面代码块样式,优化公式的解析。
|
||||
- 功能优化:在绘图,视频相关 API 增加提示词长度的检查,防止提示词超出导致写入数据库失败。
|
||||
- Bug 修复:优化 Redis 连接池配置,增加连接池超时时间,单核服务器报错 `redis: connection pool timeout`。
|
||||
- 功能优化:优化邮件验证码发送逻辑,更新邮件发送成功提示。
|
||||
|
||||
## v4.2.0
|
||||
|
||||
- 功能优化:优化聊天页面 Notice 组件样式,采用 Vuepress 文档样式
|
||||
|
||||
@@ -27,7 +27,9 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/imroc/req/v3"
|
||||
"github.com/nfnt/resize"
|
||||
"github.com/shirou/gopsutil/host"
|
||||
"golang.org/x/image/webp"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -70,6 +72,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)
|
||||
return s.Engine.Run(s.Config.Listen)
|
||||
}
|
||||
@@ -93,19 +112,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!")
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -144,14 +144,15 @@ type SystemConfig struct {
|
||||
OrderPayTimeout int `json:"order_pay_timeout,omitempty"` //订单支付超时时间
|
||||
VipInfoText string `json:"vip_info_text,omitempty"` // 会员页面充值说明
|
||||
|
||||
MjPower int `json:"mj_power,omitempty"` // MJ 绘画消耗算力
|
||||
MjActionPower int `json:"mj_action_power,omitempty"` // MJ 操作(放大,变换)消耗算力
|
||||
SdPower int `json:"sd_power,omitempty"` // SD 绘画消耗算力
|
||||
DallPower int `json:"dall_power,omitempty"` // DALL-E-3 绘图消耗算力
|
||||
SunoPower int `json:"suno_power,omitempty"` // Suno 生成歌曲消耗算力
|
||||
LumaPower int `json:"luma_power,omitempty"` // Luma 生成视频消耗算力
|
||||
AdvanceVoicePower int `json:"advance_voice_power,omitempty"` // 高级语音对话消耗算力
|
||||
PromptPower int `json:"prompt_power,omitempty"` // 生成提示词消耗算力
|
||||
MjPower int `json:"mj_power,omitempty"` // MJ 绘画消耗算力
|
||||
MjActionPower int `json:"mj_action_power,omitempty"` // MJ 操作(放大,变换)消耗算力
|
||||
SdPower int `json:"sd_power,omitempty"` // SD 绘画消耗算力
|
||||
DallPower int `json:"dall_power,omitempty"` // DALL-E-3 绘图消耗算力
|
||||
SunoPower int `json:"suno_power,omitempty"` // Suno 生成歌曲消耗算力
|
||||
LumaPower int `json:"luma_power,omitempty"` // Luma 生成视频消耗算力
|
||||
KeLingPowers map[string]int `json:"keling_powers,omitempty"` // 可灵生成视频消耗算力
|
||||
AdvanceVoicePower int `json:"advance_voice_power,omitempty"` // 高级语音对话消耗算力
|
||||
PromptPower int `json:"prompt_power,omitempty"` // 生成提示词消耗算力
|
||||
|
||||
WechatCardURL string `json:"wechat_card_url,omitempty"` // 微信客服地址
|
||||
|
||||
@@ -169,5 +170,6 @@ type SystemConfig struct {
|
||||
EnabledVerify bool `json:"enabled_verify"` // 是否启用验证码
|
||||
EmailWhiteList []string `json:"email_white_list"` // 邮箱白名单列表
|
||||
TranslateModelId int `json:"translate_model_id"` // 用来做提示词翻译的大模型 id
|
||||
MaxFileSize int `json:"max_file_size"` // 最大文件大小,单位:MB
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -26,7 +26,6 @@ const (
|
||||
type MjTask struct {
|
||||
Id uint `json:"id"` // 任务ID
|
||||
TaskId string `json:"task_id"` // 中转任务ID
|
||||
ClientId string `json:"client_id"`
|
||||
ImgArr []string `json:"img_arr"`
|
||||
Type TaskType `json:"type"`
|
||||
UserId int `json:"user_id"`
|
||||
@@ -44,7 +43,6 @@ type MjTask struct {
|
||||
type SdTask struct {
|
||||
Id int `json:"id"` // job 数据库ID
|
||||
Type TaskType `json:"type"`
|
||||
ClientId string `json:"client_id"`
|
||||
UserId int `json:"user_id"`
|
||||
Params SdTaskParams `json:"params"`
|
||||
RetryCount int `json:"retry_count"`
|
||||
@@ -52,7 +50,6 @@ type SdTask struct {
|
||||
}
|
||||
|
||||
type SdTaskParams struct {
|
||||
ClientId string `json:"client_id"` // 客户端ID
|
||||
TaskId string `json:"task_id"`
|
||||
Prompt string `json:"prompt"` // 提示词
|
||||
NegPrompt string `json:"neg_prompt"` // 反向提示词
|
||||
@@ -73,22 +70,20 @@ type SdTaskParams struct {
|
||||
|
||||
// DallTask DALL-E task
|
||||
type DallTask struct {
|
||||
ClientId string `json:"client_id"`
|
||||
ModelId uint `json:"model_id"`
|
||||
ModelName string `json:"model_name"`
|
||||
Id uint `json:"id"`
|
||||
UserId uint `json:"user_id"`
|
||||
Prompt string `json:"prompt"`
|
||||
N int `json:"n"`
|
||||
Quality string `json:"quality"`
|
||||
Size string `json:"size"`
|
||||
Style string `json:"style"`
|
||||
Power int `json:"power"`
|
||||
TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID
|
||||
ModelId uint `json:"model_id"`
|
||||
ModelName string `json:"model_name"`
|
||||
Id uint `json:"id"`
|
||||
UserId uint `json:"user_id"`
|
||||
Prompt string `json:"prompt"`
|
||||
N int `json:"n"`
|
||||
Quality string `json:"quality"`
|
||||
Size string `json:"size"`
|
||||
Style string `json:"style"`
|
||||
Power int `json:"power"`
|
||||
TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID
|
||||
}
|
||||
|
||||
type SunoTask struct {
|
||||
ClientId string `json:"client_id"`
|
||||
Id uint `json:"id"`
|
||||
Channel string `json:"channel"`
|
||||
UserId int `json:"user_id"`
|
||||
@@ -96,7 +91,8 @@ type SunoTask struct {
|
||||
Title string `json:"title"`
|
||||
RefTaskId string `json:"ref_task_id,omitempty"`
|
||||
RefSongId string `json:"ref_song_id,omitempty"`
|
||||
Prompt string `json:"prompt"` // 提示词/歌词
|
||||
Prompt string `json:"prompt"` // 提示词
|
||||
Lyrics string `json:"lyrics,omitempty"` // 歌词
|
||||
Tags string `json:"tags"`
|
||||
Model string `json:"model"`
|
||||
Instrumental bool `json:"instrumental"` // 是否纯音乐
|
||||
@@ -109,21 +105,21 @@ const (
|
||||
VideoLuma = "luma"
|
||||
VideoRunway = "runway"
|
||||
VideoCog = "cog"
|
||||
VideoKeLing = "keling"
|
||||
)
|
||||
|
||||
type VideoTask struct {
|
||||
ClientId string `json:"client_id"`
|
||||
Id uint `json:"id"`
|
||||
Channel string `json:"channel"`
|
||||
UserId int `json:"user_id"`
|
||||
Type string `json:"type"`
|
||||
TaskId string `json:"task_id"`
|
||||
Prompt string `json:"prompt"` // 提示词
|
||||
Params VideoParams `json:"params"`
|
||||
Params interface{} `json:"params"`
|
||||
TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID
|
||||
}
|
||||
|
||||
type VideoParams struct {
|
||||
type LumaVideoParams struct {
|
||||
PromptOptimize bool `json:"prompt_optimize"` // 是否优化提示词
|
||||
Loop bool `json:"loop"` // 是否循环参考图
|
||||
StartImgURL string `json:"start_img_url"` // 第一帧参考图地址
|
||||
@@ -133,3 +129,33 @@ type VideoParams struct {
|
||||
Style string `json:"style"` // 风格
|
||||
Duration int `json:"duration"` // 视频时长(秒)
|
||||
}
|
||||
|
||||
type KeLingVideoParams struct {
|
||||
TaskType string `json:"task_type"` // 任务类型: text2video/image2video
|
||||
Model string `json:"model"` // 模型: default/anime
|
||||
Prompt string `json:"prompt"` // 视频描述
|
||||
NegPrompt string `json:"negative_prompt"` // 负面提示词
|
||||
CfgScale float64 `json:"cfg_scale"` // 相关性系数(0-1)
|
||||
Mode string `json:"mode"` // 生成模式: std/pro
|
||||
AspectRatio string `json:"aspect_ratio"` // 画面比例: 16:9/9:16/1:1
|
||||
Duration string `json:"duration"` // 视频时长: 5/10
|
||||
CameraControl CameraControl `json:"camera_control"` // 摄像机控制
|
||||
Image string `json:"image"` // 参考图片URL(image2video)
|
||||
ImageTail string `json:"image_tail"` // 尾帧图片URL(image2video)
|
||||
}
|
||||
|
||||
// CameraControl 摄像机控制
|
||||
type CameraControl struct {
|
||||
Type string `json:"type"` // 控制类型: simple/down_back/forward_up/right_turn_forward/left_turn_forward
|
||||
Config CameraConfig `json:"config"` // 控制参数(仅simple类型时使用)
|
||||
}
|
||||
|
||||
// CameraConfig 摄像机参数
|
||||
type CameraConfig struct {
|
||||
Horizontal int `json:"horizontal"` // 水平移动(-10到10)
|
||||
Vertical int `json:"vertical"` // 垂直移动(-10到10)
|
||||
Pan int `json:"pan"` // 左右旋转(-10到10)
|
||||
Tilt int `json:"tilt"` // 上下旋转(-10到10)
|
||||
Roll int `json:"roll"` // 横向翻转(-10到10)
|
||||
Zoom int `json:"zoom"` // 镜头缩放(-10到10)
|
||||
}
|
||||
|
||||
@@ -34,13 +34,14 @@ const (
|
||||
MsgTypeErr = WsMsgType("error")
|
||||
MsgTypePing = WsMsgType("ping") // 心跳消息
|
||||
|
||||
ChPing = WsChannel("ping")
|
||||
ChChat = WsChannel("chat")
|
||||
ChMj = WsChannel("mj")
|
||||
ChSd = WsChannel("sd")
|
||||
ChDall = WsChannel("dall")
|
||||
ChSuno = WsChannel("suno")
|
||||
ChLuma = WsChannel("luma")
|
||||
ChPing = WsChannel("ping")
|
||||
ChChat = WsChannel("chat")
|
||||
ChMj = WsChannel("mj")
|
||||
ChSd = WsChannel("sd")
|
||||
ChDall = WsChannel("dall")
|
||||
ChSuno = WsChannel("suno")
|
||||
ChLuma = WsChannel("luma")
|
||||
ChKeLing = WsChannel("keling")
|
||||
)
|
||||
|
||||
// InputMessage 对话输入消息结构
|
||||
|
||||
12
api/go.mod
12
api/go.mod
@@ -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
|
||||
)
|
||||
|
||||
30
api/go.sum
30
api/go.sum
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -48,6 +48,7 @@ func (h *ConfigHandler) Update(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
logger.Errorf("Update config failed: %v", err)
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
@@ -58,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})
|
||||
@@ -132,7 +139,8 @@ func (h *ConfigHandler) Active(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, info.HostID)
|
||||
resp.SUCCESS(c)
|
||||
|
||||
}
|
||||
|
||||
// GetLicense 获取 License 信息
|
||||
@@ -144,7 +152,6 @@ func (h *ConfigHandler) GetLicense(c *gin.Context) {
|
||||
// FixData 修复数据
|
||||
func (h *ConfigHandler) FixData(c *gin.Context) {
|
||||
resp.ERROR(c, "当前升级版本没有数据需要修正!")
|
||||
return
|
||||
//var fixed bool
|
||||
//version := "data_fix_4.1.4"
|
||||
//err := h.levelDB.Get(version, &fixed)
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -33,6 +34,7 @@ func NewMediaHandler(app *core.AppServer, db *gorm.DB, userService *service.User
|
||||
}
|
||||
|
||||
type mediaQuery struct {
|
||||
Type string `json:"type"` // 任务类型 luma, keling
|
||||
Prompt string `json:"prompt"`
|
||||
Username string `json:"username"`
|
||||
CreatedAt []string `json:"created_at"`
|
||||
@@ -84,15 +86,15 @@ func (h *MediaHandler) SunoList(c *gin.Context) {
|
||||
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items))
|
||||
}
|
||||
|
||||
// LumaList Luma 视频任务列表
|
||||
func (h *MediaHandler) LumaList(c *gin.Context) {
|
||||
// Videos 视频任务列表
|
||||
func (h *MediaHandler) Videos(c *gin.Context) {
|
||||
var data mediaQuery
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
session := h.DB.Session(&gorm.Session{}).Where("type", data.Type)
|
||||
if data.Username != "" {
|
||||
var user model.User
|
||||
err := h.DB.Where("username", data.Username).First(&user).Error
|
||||
@@ -154,6 +156,7 @@ func (h *MediaHandler) Remove(c *gin.Context) {
|
||||
fileURL = job.AudioURL
|
||||
break
|
||||
case "luma":
|
||||
case "keling":
|
||||
var job model.VideoJob
|
||||
if res := h.DB.Where("id", id).First(&job); res.Error != nil {
|
||||
resp.ERROR(c, "记录不存在")
|
||||
|
||||
@@ -13,9 +13,10 @@ import (
|
||||
"geekai/service/oss"
|
||||
"geekai/store/model"
|
||||
"geekai/utils/resp"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UploadHandler struct {
|
||||
@@ -28,11 +29,24 @@ func NewUploadHandler(app *core.AppServer, db *gorm.DB, manager *oss.UploaderMan
|
||||
}
|
||||
|
||||
func (h *UploadHandler) Upload(c *gin.Context) {
|
||||
// 判断文件大小
|
||||
f, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if h.App.SysConfig.MaxFileSize > 0 && f.Size > int64(h.App.SysConfig.MaxFileSize)*1024*1024 {
|
||||
resp.ERROR(c, "文件大小超过限制")
|
||||
return
|
||||
}
|
||||
|
||||
file, err := h.uploaderManager.GetUploadHandler().PutFile(c, "file")
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
userId := 0
|
||||
res := h.DB.Create(&model.File{
|
||||
UserId: userId,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -90,7 +92,7 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
|
||||
}
|
||||
|
||||
// 检查 prompt 长度是否超过了当前模型允许的最大上下文长度
|
||||
promptTokens, err := utils.CalcTokens(prompt, session.Model.Value)
|
||||
promptTokens, _ := utils.CalcTokens(prompt, session.Model.Value)
|
||||
if promptTokens > session.Model.MaxContext {
|
||||
|
||||
return errors.New("对话内容超出了当前模型允许的最大上下文长度!")
|
||||
@@ -105,7 +107,6 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
|
||||
if strings.HasPrefix(session.Model.Value, "o1-") ||
|
||||
strings.HasPrefix(session.Model.Value, "o3-") ||
|
||||
strings.HasPrefix(session.Model.Value, "gpt") {
|
||||
utils.SendChunkMsg(ws, "> AI 正在思考...\n")
|
||||
req.MaxCompletionTokens = session.Model.MaxTokens
|
||||
session.Start = time.Now().Unix()
|
||||
} else {
|
||||
@@ -349,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)
|
||||
@@ -496,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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -89,13 +89,7 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
var function model.Function
|
||||
var toolCall = false
|
||||
var arguments = make([]string, 0)
|
||||
|
||||
if strings.HasPrefix(req.Model, "o1-") {
|
||||
content := fmt.Sprintf("AI 思考结束,耗时:%d 秒。\n\n", time.Now().Unix()-session.Start)
|
||||
contents = append(contents, "> AI 正在思考中...\n")
|
||||
contents = append(contents, content)
|
||||
utils.SendChunkMsg(ws, content)
|
||||
}
|
||||
var reasoning = false
|
||||
|
||||
scanner := bufio.NewScanner(response.Body)
|
||||
for scanner.Scan() {
|
||||
@@ -111,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
|
||||
}
|
||||
|
||||
@@ -159,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
|
||||
|
||||
@@ -174,7 +186,7 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
}
|
||||
|
||||
if toolCall { // 调用函数完成任务
|
||||
params := make(map[string]interface{})
|
||||
params := make(map[string]any)
|
||||
_ = utils.JsonDecode(strings.Join(arguments, ""), ¶ms)
|
||||
logger.Debugf("函数名称: %s, 函数参数:%s", function.Name, params)
|
||||
params["user_id"] = userVo.Id
|
||||
|
||||
@@ -70,7 +70,6 @@ func (h *DallJobHandler) Image(c *gin.Context) {
|
||||
idValue, _ := c.Get(types.LoginUserID)
|
||||
userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
|
||||
task := types.DallTask{
|
||||
ClientId: data.ClientId,
|
||||
UserId: uint(userId),
|
||||
ModelId: chatModel.Id,
|
||||
ModelName: chatModel.Value,
|
||||
|
||||
@@ -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(¶ms); 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
|
||||
|
||||
@@ -66,7 +66,6 @@ func (h *MidJourneyHandler) preCheck(c *gin.Context) bool {
|
||||
func (h *MidJourneyHandler) Image(c *gin.Context) {
|
||||
var data struct {
|
||||
TaskType string `json:"task_type"`
|
||||
ClientId string `json:"client_id"`
|
||||
Prompt string `json:"prompt"`
|
||||
NegPrompt string `json:"neg_prompt"`
|
||||
Rate string `json:"rate"`
|
||||
@@ -153,7 +152,6 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
task := types.MjTask{
|
||||
ClientId: data.ClientId,
|
||||
TaskId: taskId,
|
||||
Type: types.TaskType(data.TaskType),
|
||||
Prompt: data.Prompt,
|
||||
@@ -207,7 +205,6 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
|
||||
|
||||
type reqVo struct {
|
||||
Index int `json:"index"`
|
||||
ClientId string `json:"client_id"`
|
||||
ChannelId string `json:"channel_id"`
|
||||
MessageId string `json:"message_id"`
|
||||
MessageHash string `json:"message_hash"`
|
||||
@@ -229,7 +226,6 @@ func (h *MidJourneyHandler) Upscale(c *gin.Context) {
|
||||
userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
|
||||
taskId, _ := h.snowflake.Next(true)
|
||||
task := types.MjTask{
|
||||
ClientId: data.ClientId,
|
||||
Type: types.TaskUpscale,
|
||||
UserId: userId,
|
||||
ChannelId: data.ChannelId,
|
||||
@@ -286,7 +282,6 @@ func (h *MidJourneyHandler) Variation(c *gin.Context) {
|
||||
taskId, _ := h.snowflake.Next(true)
|
||||
task := types.MjTask{
|
||||
Type: types.TaskVariation,
|
||||
ClientId: data.ClientId,
|
||||
UserId: userId,
|
||||
Index: data.Index,
|
||||
ChannelId: data.ChannelId,
|
||||
|
||||
@@ -56,11 +56,15 @@ func (h *PromptHandler) Lyric(c *gin.Context) {
|
||||
|
||||
if h.App.SysConfig.PromptPower > 0 {
|
||||
userId := h.GetLoginUserId(c)
|
||||
h.userService.DecreasePower(int(userId), h.App.SysConfig.PromptPower, model.PowerLog{
|
||||
err = h.userService.DecreasePower(int(userId), h.App.SysConfig.PromptPower, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: h.getPromptModel(),
|
||||
Remark: "生成歌词",
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, content)
|
||||
@@ -82,11 +86,15 @@ func (h *PromptHandler) Image(c *gin.Context) {
|
||||
}
|
||||
if h.App.SysConfig.PromptPower > 0 {
|
||||
userId := h.GetLoginUserId(c)
|
||||
h.userService.DecreasePower(int(userId), h.App.SysConfig.PromptPower, model.PowerLog{
|
||||
err = h.userService.DecreasePower(int(userId), h.App.SysConfig.PromptPower, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: h.getPromptModel(),
|
||||
Remark: "生成绘画提示词",
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, strings.Trim(content, `"`))
|
||||
}
|
||||
@@ -108,11 +116,15 @@ func (h *PromptHandler) Video(c *gin.Context) {
|
||||
|
||||
if h.App.SysConfig.PromptPower > 0 {
|
||||
userId := h.GetLoginUserId(c)
|
||||
h.userService.DecreasePower(int(userId), h.App.SysConfig.PromptPower, model.PowerLog{
|
||||
err = h.userService.DecreasePower(int(userId), h.App.SysConfig.PromptPower, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: h.getPromptModel(),
|
||||
Remark: "生成视频脚本",
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, strings.Trim(content, `"`))
|
||||
|
||||
@@ -102,6 +102,7 @@ func (h *SdJobHandler) Image(c *gin.Context) {
|
||||
if data.Sampler == "" {
|
||||
data.Sampler = "Euler a"
|
||||
}
|
||||
|
||||
idValue, _ := c.Get(types.LoginUserID)
|
||||
userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
|
||||
taskId, err := h.snowflake.Next(true)
|
||||
@@ -111,8 +112,7 @@ func (h *SdJobHandler) Image(c *gin.Context) {
|
||||
}
|
||||
|
||||
task := types.SdTask{
|
||||
ClientId: data.ClientId,
|
||||
Type: types.TaskImage,
|
||||
Type: types.TaskImage,
|
||||
Params: types.SdTaskParams{
|
||||
TaskId: taskId,
|
||||
Prompt: data.Prompt,
|
||||
|
||||
@@ -111,9 +111,5 @@ func (h *SmsHandler) SendCode(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if h.App.Debug {
|
||||
resp.SUCCESS(c, code)
|
||||
} else {
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
@@ -18,9 +18,10 @@ import (
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SunoHandler struct {
|
||||
@@ -45,7 +46,6 @@ func NewSunoHandler(app *core.AppServer, db *gorm.DB, service *suno.Service, upl
|
||||
func (h *SunoHandler) Create(c *gin.Context) {
|
||||
|
||||
var data struct {
|
||||
ClientId string `json:"client_id"`
|
||||
Prompt string `json:"prompt"`
|
||||
Instrumental bool `json:"instrumental"`
|
||||
Lyrics string `json:"lyrics"`
|
||||
@@ -90,7 +90,6 @@ func (h *SunoHandler) Create(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
task := types.SunoTask{
|
||||
ClientId: data.ClientId,
|
||||
UserId: int(h.GetLoginUserId(c)),
|
||||
Type: data.Type,
|
||||
Title: data.Title,
|
||||
@@ -98,6 +97,7 @@ func (h *SunoHandler) Create(c *gin.Context) {
|
||||
RefSongId: data.RefSongId,
|
||||
ExtendSecs: data.ExtendSecs,
|
||||
Prompt: data.Prompt,
|
||||
Lyrics: data.Lyrics,
|
||||
Tags: data.Tags,
|
||||
Model: data.Model,
|
||||
Instrumental: data.Instrumental,
|
||||
|
||||
@@ -18,9 +18,10 @@ import (
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type VideoHandler struct {
|
||||
@@ -45,7 +46,6 @@ func NewVideoHandler(app *core.AppServer, db *gorm.DB, service *video.Service, u
|
||||
func (h *VideoHandler) LumaCreate(c *gin.Context) {
|
||||
|
||||
var data struct {
|
||||
ClientId string `json:"client_id"`
|
||||
Prompt string `json:"prompt"`
|
||||
FirstFrameImg string `json:"first_frame_img,omitempty"`
|
||||
EndFrameImg string `json:"end_frame_img,omitempty"`
|
||||
@@ -56,6 +56,11 @@ func (h *VideoHandler) LumaCreate(c *gin.Context) {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
// 检查 Prompt 长度
|
||||
if data.Prompt == "" {
|
||||
resp.ERROR(c, "prompt is needed")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
@@ -68,20 +73,14 @@ func (h *VideoHandler) LumaCreate(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if data.Prompt == "" {
|
||||
resp.ERROR(c, "prompt is needed")
|
||||
return
|
||||
}
|
||||
|
||||
userId := int(h.GetLoginUserId(c))
|
||||
params := types.VideoParams{
|
||||
params := types.LumaVideoParams{
|
||||
PromptOptimize: data.ExpandPrompt,
|
||||
Loop: data.Loop,
|
||||
StartImgURL: data.FirstFrameImg,
|
||||
EndImgURL: data.EndFrameImg,
|
||||
}
|
||||
task := types.VideoTask{
|
||||
ClientId: data.ClientId,
|
||||
UserId: userId,
|
||||
Type: types.VideoLuma,
|
||||
Prompt: data.Prompt,
|
||||
@@ -119,20 +118,117 @@ func (h *VideoHandler) LumaCreate(c *gin.Context) {
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *VideoHandler) KeLingCreate(c *gin.Context) {
|
||||
|
||||
var data struct {
|
||||
Channel string `json:"channel"`
|
||||
TaskType string `json:"task_type"` // 任务类型: text2video/image2video
|
||||
Model string `json:"model"` // 模型: kling-v1-5,kling-v1-6
|
||||
Prompt string `json:"prompt"` // 视频描述
|
||||
NegPrompt string `json:"negative_prompt"` // 负面提示词
|
||||
CfgScale float64 `json:"cfg_scale"` // 相关性系数(0-1)
|
||||
Mode string `json:"mode"` // 生成模式: std/pro
|
||||
AspectRatio string `json:"aspect_ratio"` // 画面比例: 16:9/9:16/1:1
|
||||
Duration string `json:"duration"` // 视频时长: 5/10
|
||||
CameraControl types.CameraControl `json:"camera_control"` // 摄像机控制
|
||||
Image string `json:"image"` // 参考图片URL(image2video)
|
||||
ImageTail string `json:"image_tail"` // 尾帧图片URL(image2video)
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
// 计算当前任务所需算力
|
||||
key := fmt.Sprintf("%s_%s_%s", data.Model, data.Mode, data.Duration)
|
||||
power := h.App.SysConfig.KeLingPowers[key]
|
||||
if power == 0 {
|
||||
resp.ERROR(c, "当前模型暂不支持")
|
||||
return
|
||||
}
|
||||
if user.Power < power {
|
||||
resp.ERROR(c, "您的算力不足,请充值后再试!")
|
||||
return
|
||||
}
|
||||
|
||||
if data.Prompt == "" {
|
||||
resp.ERROR(c, "prompt is needed")
|
||||
return
|
||||
}
|
||||
|
||||
userId := int(h.GetLoginUserId(c))
|
||||
params := types.KeLingVideoParams{
|
||||
TaskType: data.TaskType,
|
||||
Model: data.Model,
|
||||
Prompt: data.Prompt,
|
||||
NegPrompt: data.NegPrompt,
|
||||
CfgScale: data.CfgScale,
|
||||
Mode: data.Mode,
|
||||
AspectRatio: data.AspectRatio,
|
||||
Duration: data.Duration,
|
||||
CameraControl: data.CameraControl,
|
||||
Image: data.Image,
|
||||
ImageTail: data.ImageTail,
|
||||
}
|
||||
task := types.VideoTask{
|
||||
UserId: userId,
|
||||
Type: types.VideoKeLing,
|
||||
Prompt: data.Prompt,
|
||||
Params: params,
|
||||
TranslateModelId: h.App.SysConfig.TranslateModelId,
|
||||
Channel: data.Channel,
|
||||
}
|
||||
// 插入数据库
|
||||
job := model.VideoJob{
|
||||
UserId: userId,
|
||||
Type: types.VideoKeLing,
|
||||
Prompt: data.Prompt,
|
||||
Power: power,
|
||||
TaskInfo: utils.JsonEncode(task),
|
||||
}
|
||||
tx := h.DB.Create(&job)
|
||||
if tx.Error != nil {
|
||||
resp.ERROR(c, tx.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 创建任务
|
||||
task.Id = job.Id
|
||||
h.videoService.PushTask(task)
|
||||
|
||||
// update user's power
|
||||
err = h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: "keling",
|
||||
Remark: fmt.Sprintf("keling 文生视频,任务ID:%d", job.Id),
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *VideoHandler) List(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
t := c.Query("type")
|
||||
page := h.GetInt(c, "page", 1)
|
||||
pageSize := h.GetInt(c, "page_size", 20)
|
||||
all := h.GetBool(c, "all")
|
||||
session := h.DB.Session(&gorm.Session{}).Where("user_id", userId)
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
if t != "" {
|
||||
session = session.Where("type", t)
|
||||
}
|
||||
if all {
|
||||
session = session.Where("publish", 0).Where("progress", 100)
|
||||
} else {
|
||||
session = session.Where("user_id", h.GetLoginUserId(c))
|
||||
session = session.Where("user_id", userId)
|
||||
}
|
||||
// 统计总数
|
||||
var total int64
|
||||
@@ -161,6 +257,33 @@ func (h *VideoHandler) List(c *gin.Context) {
|
||||
if item.VideoURL == "" {
|
||||
item.VideoURL = v.WaterURL
|
||||
}
|
||||
// 解析任务详情
|
||||
if item.Type == types.VideoKeLing {
|
||||
task := types.VideoTask{}
|
||||
err = utils.JsonDecode(v.TaskInfo, &task)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var params types.KeLingVideoParams
|
||||
err = utils.JsonDecode(utils.JsonEncode(task.Params), ¶ms)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
item.RawData = map[string]interface{}{
|
||||
"task_type": params.TaskType,
|
||||
"model": params.Model,
|
||||
"cfg_scale": params.CfgScale,
|
||||
"mode": params.Mode,
|
||||
"aspect_ratio": params.AspectRatio,
|
||||
"duration": params.Duration,
|
||||
"model_name": fmt.Sprintf("%s_%s_%s", params.Model, params.Mode, params.Duration),
|
||||
}
|
||||
|
||||
// 如果视频URL不为空,则设置为生成成功
|
||||
if item.VideoURL != "" {
|
||||
item.Progress = 100
|
||||
}
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
@@ -192,6 +315,8 @@ func (h *VideoHandler) Remove(c *gin.Context) {
|
||||
// 删除文件
|
||||
_ = h.uploader.GetUploadHandler().Delete(job.CoverURL)
|
||||
_ = h.uploader.GetUploadHandler().Delete(job.VideoURL)
|
||||
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
||||
func (h *VideoHandler) Publish(c *gin.Context) {
|
||||
|
||||
13
api/main.go
13
api/main.go
@@ -163,7 +163,6 @@ func main() {
|
||||
fx.Provide(dalle.NewService),
|
||||
fx.Invoke(func(s *dalle.Service) {
|
||||
s.Run()
|
||||
s.CheckTaskNotify()
|
||||
s.DownloadImages()
|
||||
s.CheckTaskStatus()
|
||||
}),
|
||||
@@ -182,7 +181,6 @@ func main() {
|
||||
fx.Invoke(func(s *mj.Service) {
|
||||
s.Run()
|
||||
s.SyncTaskProgress()
|
||||
s.CheckTaskNotify()
|
||||
s.DownloadImages()
|
||||
}),
|
||||
|
||||
@@ -191,21 +189,18 @@ func main() {
|
||||
fx.Invoke(func(s *sd.Service, config *types.AppConfig) {
|
||||
s.Run()
|
||||
s.CheckTaskStatus()
|
||||
s.CheckTaskNotify()
|
||||
}),
|
||||
|
||||
fx.Provide(suno.NewService),
|
||||
fx.Invoke(func(s *suno.Service) {
|
||||
s.Run()
|
||||
s.SyncTaskProgress()
|
||||
s.CheckTaskNotify()
|
||||
s.DownloadFiles()
|
||||
}),
|
||||
fx.Provide(video.NewService),
|
||||
fx.Invoke(func(s *video.Service) {
|
||||
s.Run()
|
||||
s.SyncTaskProgress()
|
||||
s.CheckTaskNotify()
|
||||
s.DownloadFiles()
|
||||
}),
|
||||
fx.Provide(service.NewUserService),
|
||||
@@ -256,6 +251,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 +331,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 +428,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) {
|
||||
@@ -492,6 +490,7 @@ func main() {
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.VideoHandler) {
|
||||
group := s.Engine.Group("/api/video")
|
||||
group.POST("luma/create", h.LumaCreate)
|
||||
group.POST("keling/create", h.KeLingCreate)
|
||||
group.GET("list", h.List)
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("publish", h.Publish)
|
||||
@@ -560,8 +559,8 @@ func main() {
|
||||
fx.Provide(admin.NewMediaHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.MediaHandler) {
|
||||
group := s.Engine.Group("/api/admin/media")
|
||||
group.POST("/list/suno", h.SunoList)
|
||||
group.POST("/list/luma", h.LumaList)
|
||||
group.POST("/suno", h.SunoList)
|
||||
group.POST("/videos", h.Videos)
|
||||
group.GET("/remove", h.Remove)
|
||||
}),
|
||||
fx.Provide(handler.NewRealtimeHandler),
|
||||
|
||||
333
api/service/crawler/service.go
Normal file
333
api/service/crawler/service.go
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -34,22 +34,16 @@ type Service struct {
|
||||
db *gorm.DB
|
||||
uploadManager *oss.UploaderManager
|
||||
taskQueue *store.RedisQueue
|
||||
notifyQueue *store.RedisQueue
|
||||
userService *service.UserService
|
||||
wsService *service.WebsocketService
|
||||
clientIds map[uint]string
|
||||
}
|
||||
|
||||
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),
|
||||
notifyQueue: store.NewRedisQueue("DallE_Notify_Queue", redisCli),
|
||||
wsService: wsService,
|
||||
uploadManager: manager,
|
||||
userService: userService,
|
||||
clientIds: map[uint]string{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +54,7 @@ func (s *Service) PushTask(task types.DallTask) {
|
||||
}
|
||||
|
||||
func (s *Service) Run() {
|
||||
// 将数据库中未提交的人物加载到队列
|
||||
// 将数据库中未提交的任务加载到队列
|
||||
var jobs []model.DallJob
|
||||
s.db.Where("progress", 0).Find(&jobs)
|
||||
for _, v := range jobs {
|
||||
@@ -84,16 +78,16 @@ func (s *Service) Run() {
|
||||
continue
|
||||
}
|
||||
logger.Infof("handle a new DALL-E task: %+v", task)
|
||||
s.clientIds[task.Id] = task.ClientId
|
||||
_, err = s.Image(task, false)
|
||||
if err != nil {
|
||||
logger.Errorf("error with image task: %v", err)
|
||||
s.db.Model(&model.DallJob{Id: task.Id}).UpdateColumns(map[string]interface{}{
|
||||
"progress": service.FailTaskProgress,
|
||||
"err_msg": err.Error(),
|
||||
})
|
||||
s.notifyQueue.RPush(service.NotifyMessage{ClientId: task.ClientId, UserId: int(task.UserId), JobId: int(task.Id), Message: service.TaskStatusFailed})
|
||||
}
|
||||
go func() {
|
||||
_, err = s.Image(task, false)
|
||||
if err != nil {
|
||||
logger.Errorf("error with image task: %v", err)
|
||||
s.db.Model(&model.DallJob{Id: task.Id}).UpdateColumns(map[string]interface{}{
|
||||
"progress": service.FailTaskProgress,
|
||||
"err_msg": err.Error(),
|
||||
})
|
||||
}
|
||||
}()
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -212,10 +206,9 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
|
||||
return "", fmt.Errorf("err with update database: %v", err)
|
||||
}
|
||||
|
||||
s.notifyQueue.RPush(service.NotifyMessage{ClientId: task.ClientId, UserId: int(task.UserId), JobId: int(task.Id), Message: service.TaskStatusFailed})
|
||||
var content string
|
||||
if sync {
|
||||
imgURL, err := s.downloadImage(task.Id, int(task.UserId), res.Data[0].Url)
|
||||
imgURL, err := s.downloadImage(task.Id, res.Data[0].Url)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with download image: %v", err)
|
||||
}
|
||||
@@ -225,26 +218,6 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
|
||||
return content, nil
|
||||
}
|
||||
|
||||
func (s *Service) CheckTaskNotify() {
|
||||
go func() {
|
||||
logger.Info("Running DALL-E task notify checking ...")
|
||||
for {
|
||||
var message service.NotifyMessage
|
||||
err := s.notifyQueue.LPop(&message)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Debugf("notify message: %+v", message)
|
||||
client := s.wsService.Clients.Get(message.ClientId)
|
||||
if client == nil {
|
||||
continue
|
||||
}
|
||||
utils.SendChannelMsg(client, types.ChDall, message.Message)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Service) CheckTaskStatus() {
|
||||
go func() {
|
||||
logger.Info("Running DALL-E task status checking ...")
|
||||
@@ -254,7 +227,7 @@ func (s *Service) CheckTaskStatus() {
|
||||
s.db.Where("progress < ?", 100).Find(&jobs)
|
||||
for _, job := range jobs {
|
||||
// 超时的任务标记为失败
|
||||
if time.Now().Sub(job.CreatedAt) > time.Minute*10 {
|
||||
if time.Since(job.CreatedAt) > time.Minute*10 {
|
||||
job.Progress = service.FailTaskProgress
|
||||
job.ErrMsg = "任务超时"
|
||||
s.db.Updates(&job)
|
||||
@@ -301,7 +274,7 @@ func (s *Service) DownloadImages() {
|
||||
}
|
||||
|
||||
logger.Infof("try to download image: %s", v.OrgURL)
|
||||
imgURL, err := s.downloadImage(v.Id, int(v.UserId), v.OrgURL)
|
||||
imgURL, err := s.downloadImage(v.Id, v.OrgURL)
|
||||
if err != nil {
|
||||
logger.Error("error with download image: %s, error: %v", imgURL, err)
|
||||
continue
|
||||
@@ -316,7 +289,7 @@ func (s *Service) DownloadImages() {
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Service) downloadImage(jobId uint, userId int, orgURL string) (string, error) {
|
||||
func (s *Service) downloadImage(jobId uint, orgURL string) (string, error) {
|
||||
// sava image
|
||||
imgURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(orgURL, false)
|
||||
if err != nil {
|
||||
@@ -328,6 +301,5 @@ func (s *Service) downloadImage(jobId uint, userId int, orgURL string) (string,
|
||||
if res.Error != nil {
|
||||
return "", err
|
||||
}
|
||||
s.notifyQueue.RPush(service.NotifyMessage{ClientId: s.clientIds[jobId], UserId: userId, JobId: int(jobId), Message: service.TaskStatusFinished})
|
||||
return imgURL, nil
|
||||
}
|
||||
|
||||
@@ -31,7 +31,8 @@ type LicenseService struct {
|
||||
func NewLicenseService(server *core.AppServer, levelDB *store.LevelDB) *LicenseService {
|
||||
var license types.License
|
||||
var machineId string
|
||||
_ = levelDB.Get(types.LicenseKey, &license)
|
||||
err := levelDB.Get(types.LicenseKey, &license)
|
||||
logger.Infof("License: %+v", server.SysConfig)
|
||||
info, err := host.Info()
|
||||
if err == nil {
|
||||
machineId = info.HostID
|
||||
|
||||
@@ -15,10 +15,11 @@ import (
|
||||
"geekai/store"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -26,23 +27,17 @@ import (
|
||||
type Service struct {
|
||||
client *Client // MJ Client
|
||||
taskQueue *store.RedisQueue
|
||||
notifyQueue *store.RedisQueue
|
||||
db *gorm.DB
|
||||
wsService *service.WebsocketService
|
||||
uploaderManager *oss.UploaderManager
|
||||
userService *service.UserService
|
||||
clientIds map[uint]string
|
||||
}
|
||||
|
||||
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),
|
||||
notifyQueue: store.NewRedisQueue("MidJourney_Notify_Queue", redisCli),
|
||||
client: client,
|
||||
wsService: wsService,
|
||||
uploaderManager: manager,
|
||||
clientIds: map[uint]string{},
|
||||
userService: userService,
|
||||
}
|
||||
}
|
||||
@@ -59,7 +54,6 @@ func (s *Service) Run() {
|
||||
continue
|
||||
}
|
||||
task.Id = v.Id
|
||||
s.clientIds[task.Id] = task.ClientId
|
||||
s.PushTask(task)
|
||||
}
|
||||
|
||||
@@ -96,7 +90,6 @@ func (s *Service) Run() {
|
||||
if task.Mode == "" {
|
||||
task.Mode = "fast"
|
||||
}
|
||||
s.clientIds[task.Id] = task.ClientId
|
||||
|
||||
var job model.MidJourneyJob
|
||||
tx := s.db.Where("id = ?", task.Id).First(&job)
|
||||
@@ -139,7 +132,6 @@ func (s *Service) Run() {
|
||||
// update the task progress
|
||||
s.db.Updates(&job)
|
||||
// 任务失败,通知前端
|
||||
s.notifyQueue.RPush(service.NotifyMessage{ClientId: task.ClientId, UserId: task.UserId, JobId: int(job.Id), Message: service.TaskStatusFailed})
|
||||
continue
|
||||
}
|
||||
logger.Infof("任务提交成功:%+v", res)
|
||||
@@ -178,24 +170,6 @@ func GetImageHash(action string) string {
|
||||
return split[len(split)-1]
|
||||
}
|
||||
|
||||
func (s *Service) CheckTaskNotify() {
|
||||
go func() {
|
||||
for {
|
||||
var message service.NotifyMessage
|
||||
err := s.notifyQueue.LPop(&message)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
logger.Debugf("receive a new mj notify message: %+v", message)
|
||||
client := s.wsService.Clients.Get(message.ClientId)
|
||||
if client == nil {
|
||||
continue
|
||||
}
|
||||
utils.SendChannelMsg(client, types.ChMj, message.Message)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Service) DownloadImages() {
|
||||
go func() {
|
||||
var items []model.MidJourneyJob
|
||||
@@ -228,12 +202,6 @@ func (s *Service) DownloadImages() {
|
||||
|
||||
v.ImgURL = imgURL
|
||||
s.db.Updates(&v)
|
||||
|
||||
s.notifyQueue.RPush(service.NotifyMessage{
|
||||
ClientId: s.clientIds[v.Id],
|
||||
UserId: v.UserId,
|
||||
JobId: int(v.Id),
|
||||
Message: service.TaskStatusFinished})
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 5)
|
||||
@@ -259,7 +227,7 @@ func (s *Service) SyncTaskProgress() {
|
||||
|
||||
for _, job := range jobs {
|
||||
// 10 分钟还没完成的任务标记为失败
|
||||
if time.Now().Sub(job.CreatedAt) > time.Minute*10 {
|
||||
if time.Since(job.CreatedAt) > time.Minute*10 {
|
||||
job.Progress = service.FailTaskProgress
|
||||
job.ErrMsg = "任务超时"
|
||||
s.db.Updates(&job)
|
||||
@@ -279,18 +247,12 @@ func (s *Service) SyncTaskProgress() {
|
||||
"err_msg": task.FailReason,
|
||||
})
|
||||
logger.Errorf("task failed: %v", task.FailReason)
|
||||
s.notifyQueue.RPush(service.NotifyMessage{
|
||||
ClientId: s.clientIds[job.Id],
|
||||
UserId: job.UserId,
|
||||
JobId: int(job.Id),
|
||||
Message: service.TaskStatusFailed})
|
||||
continue
|
||||
}
|
||||
|
||||
if len(task.Buttons) > 0 {
|
||||
job.Hash = GetImageHash(task.Buttons[0].CustomId)
|
||||
}
|
||||
oldProgress := job.Progress
|
||||
job.Progress = utils.IntValue(strings.Replace(task.Progress, "%", "", 1), 0)
|
||||
if task.ImageUrl != "" {
|
||||
job.OrgURL = task.ImageUrl
|
||||
@@ -300,19 +262,6 @@ func (s *Service) SyncTaskProgress() {
|
||||
logger.Errorf("error with update database: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 通知前端更新任务进度
|
||||
if oldProgress != job.Progress {
|
||||
message := service.TaskStatusRunning
|
||||
if job.Progress == 100 {
|
||||
message = service.TaskStatusFinished
|
||||
}
|
||||
s.notifyQueue.RPush(service.NotifyMessage{
|
||||
ClientId: s.clientIds[job.Id],
|
||||
UserId: job.UserId,
|
||||
JobId: int(job.Id),
|
||||
Message: message})
|
||||
}
|
||||
}
|
||||
|
||||
// 找出失败的任务,并恢复其扣减算力
|
||||
|
||||
@@ -16,9 +16,10 @@ import (
|
||||
"geekai/store"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
|
||||
"github.com/imroc/req/v3"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -30,20 +31,16 @@ var logger = logger2.GetLogger()
|
||||
type Service struct {
|
||||
httpClient *req.Client
|
||||
taskQueue *store.RedisQueue
|
||||
notifyQueue *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),
|
||||
notifyQueue: store.NewRedisQueue("StableDiffusion_Queue", redisCli),
|
||||
db: db,
|
||||
wsService: wsService,
|
||||
uploadManager: manager,
|
||||
userService: userService,
|
||||
}
|
||||
@@ -102,8 +99,6 @@ func (s *Service) Run() {
|
||||
"progress": service.FailTaskProgress,
|
||||
"err_msg": err.Error(),
|
||||
})
|
||||
// 通知前端,任务失败
|
||||
s.notifyQueue.RPush(service.NotifyMessage{ClientId: task.ClientId, UserId: task.UserId, JobId: task.Id, Message: service.TaskStatusFailed})
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -225,15 +220,12 @@ func (s *Service) Txt2Img(task types.SdTask) error {
|
||||
|
||||
// task finished
|
||||
s.db.Model(&model.SdJob{Id: uint(task.Id)}).UpdateColumn("progress", 100)
|
||||
s.notifyQueue.RPush(service.NotifyMessage{ClientId: task.ClientId, UserId: task.UserId, JobId: task.Id, Message: service.TaskStatusFinished})
|
||||
return nil
|
||||
default:
|
||||
err, resp := s.checkTaskProgress(apiKey)
|
||||
resp, err := s.checkTaskProgress(apiKey)
|
||||
// 更新任务进度
|
||||
if err == nil && resp.Progress > 0 {
|
||||
s.db.Model(&model.SdJob{Id: uint(task.Id)}).UpdateColumn("progress", int(resp.Progress*100))
|
||||
// 发送更新状态信号
|
||||
s.notifyQueue.RPush(service.NotifyMessage{ClientId: task.ClientId, UserId: task.UserId, JobId: task.Id, Message: service.TaskStatusRunning})
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
@@ -242,7 +234,7 @@ func (s *Service) Txt2Img(task types.SdTask) error {
|
||||
}
|
||||
|
||||
// 执行任务
|
||||
func (s *Service) checkTaskProgress(apiKey model.ApiKey) (error, *TaskProgressResp) {
|
||||
func (s *Service) checkTaskProgress(apiKey model.ApiKey) (*TaskProgressResp, error) {
|
||||
apiURL := fmt.Sprintf("%s/sdapi/v1/progress?skip_current_image=false", apiKey.ApiURL)
|
||||
var res TaskProgressResp
|
||||
response, err := s.httpClient.R().
|
||||
@@ -250,13 +242,13 @@ func (s *Service) checkTaskProgress(apiKey model.ApiKey) (error, *TaskProgressRe
|
||||
SetSuccessResult(&res).
|
||||
Get(apiURL)
|
||||
if err != nil {
|
||||
return err, nil
|
||||
return nil, err
|
||||
}
|
||||
if response.IsErrorState() {
|
||||
return fmt.Errorf("error http code status: %v", response.Status), nil
|
||||
return nil, fmt.Errorf("error http code status: %v", response.Status)
|
||||
}
|
||||
|
||||
return nil, &res
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func (s *Service) PushTask(task types.SdTask) {
|
||||
@@ -264,25 +256,6 @@ func (s *Service) PushTask(task types.SdTask) {
|
||||
s.taskQueue.RPush(task)
|
||||
}
|
||||
|
||||
func (s *Service) CheckTaskNotify() {
|
||||
go func() {
|
||||
logger.Info("Running Stable-Diffusion task notify checking ...")
|
||||
for {
|
||||
var message service.NotifyMessage
|
||||
err := s.notifyQueue.LPop(&message)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
logger.Debugf("notify message: %+v", message)
|
||||
client := s.wsService.Clients.Get(message.ClientId)
|
||||
if client == nil {
|
||||
continue
|
||||
}
|
||||
utils.SendChannelMsg(client, types.ChSd, message.Message)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// CheckTaskStatus 检查任务状态,自动删除过期或者失败的任务
|
||||
func (s *Service) CheckTaskStatus() {
|
||||
go func() {
|
||||
@@ -297,7 +270,7 @@ func (s *Service) CheckTaskStatus() {
|
||||
|
||||
for _, job := range jobs {
|
||||
// 5 分钟还没完成的任务标记为失败
|
||||
if time.Now().Sub(job.CreatedAt) > time.Minute*5 {
|
||||
if time.Since(job.CreatedAt) > time.Minute*5 {
|
||||
job.Progress = service.FailTaskProgress
|
||||
job.ErrMsg = "任务超时"
|
||||
s.db.Updates(&job)
|
||||
|
||||
@@ -18,10 +18,11 @@ import (
|
||||
"geekai/store"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
|
||||
"github.com/imroc/req/v3"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -34,20 +35,16 @@ type Service struct {
|
||||
uploadManager *oss.UploaderManager
|
||||
taskQueue *store.RedisQueue
|
||||
notifyQueue *store.RedisQueue
|
||||
wsService *service.WebsocketService
|
||||
clientIds map[string]string
|
||||
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,
|
||||
clientIds: map[string]string{},
|
||||
userService: userService,
|
||||
}
|
||||
}
|
||||
@@ -70,7 +67,6 @@ func (s *Service) Run() {
|
||||
}
|
||||
task.Id = v.Id
|
||||
s.PushTask(task)
|
||||
s.clientIds[v.TaskId] = task.ClientId
|
||||
}
|
||||
logger.Info("Starting Suno job consumer...")
|
||||
go func() {
|
||||
@@ -95,7 +91,6 @@ func (s *Service) Run() {
|
||||
"err_msg": err.Error(),
|
||||
"progress": service.FailTaskProgress,
|
||||
})
|
||||
s.notifyQueue.RPush(service.NotifyMessage{ClientId: task.ClientId, UserId: task.UserId, JobId: int(task.Id), Message: service.TaskStatusFailed})
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -104,7 +99,6 @@ func (s *Service) Run() {
|
||||
"task_id": r.Data,
|
||||
"channel": r.Channel,
|
||||
})
|
||||
s.clientIds[r.Data] = task.ClientId
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -138,7 +132,7 @@ func (s *Service) Create(task types.SunoTask) (RespVo, error) {
|
||||
if task.Type == 1 {
|
||||
reqBody["gpt_description_prompt"] = task.Prompt
|
||||
} else { // 自定义模式
|
||||
reqBody["prompt"] = task.Prompt
|
||||
reqBody["prompt"] = task.Lyrics
|
||||
reqBody["tags"] = task.Tags
|
||||
reqBody["mv"] = task.Model
|
||||
reqBody["title"] = task.Title
|
||||
@@ -146,7 +140,7 @@ func (s *Service) Create(task types.SunoTask) (RespVo, error) {
|
||||
|
||||
var res RespVo
|
||||
apiURL := fmt.Sprintf("%s/suno/submit/music", apiKey.ApiURL)
|
||||
logger.Debugf("API URL: %s, request body: %+v", apiURL, reqBody)
|
||||
logger.Debugf("API URL: %s, request body: %s", apiURL, utils.JsonEncode(reqBody))
|
||||
r, err := req.C().R().
|
||||
SetHeader("Authorization", "Bearer "+apiKey.Value).
|
||||
SetBody(reqBody).
|
||||
@@ -262,27 +256,6 @@ func (s *Service) Upload(task types.SunoTask) (RespVo, error) {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *Service) CheckTaskNotify() {
|
||||
go func() {
|
||||
logger.Info("Running Suno task notify checking ...")
|
||||
for {
|
||||
var message service.NotifyMessage
|
||||
err := s.notifyQueue.LPop(&message)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
logger.Debugf("notify message: %+v", message)
|
||||
logger.Debugf("client id: %+v", s.wsService.Clients)
|
||||
client := s.wsService.Clients.Get(message.ClientId)
|
||||
logger.Debugf("%+v", client)
|
||||
if client == nil {
|
||||
continue
|
||||
}
|
||||
utils.SendChannelMsg(client, types.ChSuno, message.Message)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Service) DownloadFiles() {
|
||||
go func() {
|
||||
var items []model.SunoJob
|
||||
@@ -311,7 +284,6 @@ func (s *Service) DownloadFiles() {
|
||||
v.AudioURL = audioURL
|
||||
v.Progress = 100
|
||||
s.db.Updates(&v)
|
||||
s.notifyQueue.RPush(service.NotifyMessage{ClientId: s.clientIds[v.TaskId], UserId: v.UserId, JobId: int(v.Id), Message: service.TaskStatusFinished})
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 10)
|
||||
@@ -377,12 +349,10 @@ func (s *Service) SyncTaskProgress() {
|
||||
}
|
||||
}
|
||||
tx.Commit()
|
||||
s.notifyQueue.RPush(service.NotifyMessage{ClientId: s.clientIds[job.TaskId], UserId: job.UserId, JobId: int(job.Id), Message: service.TaskStatusFinished})
|
||||
} else if task.Data.FailReason != "" {
|
||||
job.Progress = service.FailTaskProgress
|
||||
job.ErrMsg = task.Data.FailReason
|
||||
s.db.Updates(&job)
|
||||
s.notifyQueue.RPush(service.NotifyMessage{ClientId: s.clientIds[job.TaskId], UserId: job.UserId, JobId: int(job.Id), Message: service.TaskStatusFailed})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,109 +12,89 @@ type NotifyMessage struct {
|
||||
ClientId string `json:"client_id"`
|
||||
JobId int `json:"job_id"`
|
||||
Message string `json:"message"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
const TranslatePromptTemplate = "Translate the following painting prompt words into English keyword phrases. Without any explanation, directly output the keyword phrases separated by commas. The content to be translated is: [%s]"
|
||||
|
||||
const ImagePromptOptimizeTemplate = `
|
||||
Create a highly effective prompt to provide to an AI image generation tool in order to create an artwork based on a desired concept.
|
||||
以下是一条 AI 提示词示例,用于优化和扩写绘图提示词:
|
||||
|
||||
Please specify details about the artwork, such as the style, subject, mood, and other important characteristics you want the resulting image to have.
|
||||
请你作为一名专业的 AI 绘图提示词优化专家,基于用户提供的简单绘图描述,生成一份详细、专业且富有创意的 AI 绘图提示词指令。在优化过程中,你需要做到以下几点:
|
||||
|
||||
Remember, prompts should always be output in English.
|
||||
1. 深入理解用户描述的核心意图和关键元素,挖掘潜在的细节和情感氛围,将其融入到提示词中。
|
||||
2. 丰富画面细节,包括但不限于场景背景、人物特征、物体属性、光影效果、色彩搭配等,使画面更加生动逼真。
|
||||
3. 运用专业的艺术风格术语,如超现实主义、印象派、赛博朋克等,为画面增添独特的艺术魅力。
|
||||
4. 考虑构图和视角,如俯视、仰视、特写、全景等,提升画面的视觉冲击力。
|
||||
5. 确保提示词指令清晰、准确、完整,便于 AI 绘图模型理解和生成高质量图像。最终输出的提示词应简洁明了,避免冗余信息,以逗号分隔各个元素,突出重点,
|
||||
让用户能够直接复制使用,从而帮助用户将简单的想法转化为精美绝伦的画作。
|
||||
6. 不管用户输入的是什么语言,你务必要用英文输出优化后的提示词。
|
||||
7. 直接输出优化后的提示词,不要输出其他任何五官内容。
|
||||
|
||||
# Steps
|
||||
下面是一个提示词优化示例:
|
||||
===示例开始===
|
||||
原始指令 :一个穿着红色连衣裙的少女在花园里浇花,阳光明媚。
|
||||
|
||||
1. **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."
|
||||
|
||||
2. **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.
|
||||
优化后的 AI 绘图提示词指令:一位年轻美丽的少女,约 16 - 18 岁,有着柔顺的黑色长发,披散在肩上,面容精致,眼神温柔而专注。她穿着一条复古风格的红色连衣裙,裙子上有精致的褶皱和白色的蕾丝花边,裙摆轻轻飘动。少女站在一个充满生机的花园中,花园里种满了各种各样的鲜花,有娇艳的玫瑰、淡雅的百合、缤纷的郁金香等,花朵色彩鲜艳,绿叶繁茂。她手持一个银色的 watering can(浇水壶),正在细心地给一朵盛开的玫瑰浇水。阳光从画面的右侧洒下,形成明亮而温暖的光晕,照亮了少女和整个花园,营造出一种宁静、美好的氛围,画面采用写实风格,光影效果逼真,色彩鲜明且富有层次感,构图以少女为中心,前景是盛开的花朵,背景是花园的树木和篱笆,整体画面充满诗意和浪漫气息。
|
||||
===示例结束===
|
||||
|
||||
3. **Mood or Atmosphere**: Convey the feeling you want the image to evoke. For instance, peaceful, chaotic, epic, etc.
|
||||
|
||||
4. **Color Palette and Lighting**: Mention color preferences or lighting. For example, "vibrant with shades of blue and purple" or "dim and dramatic lighting."
|
||||
|
||||
5. **Optional Features**: You can add any additional attributes, such as background details, attention to textures, or any specific kind of framing.
|
||||
|
||||
# Output Format
|
||||
|
||||
- **Prompt Format**: A descriptive phrase that includes key aspects of the artwork (subject, style, mood, colors, lighting, any optional features).
|
||||
|
||||
Here is an example of how the final prompt should look:
|
||||
|
||||
"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."
|
||||
|
||||
**Please input the prompt words directly in English, and do not input any other explanatory statements**
|
||||
|
||||
# Examples
|
||||
|
||||
1. **Input**:
|
||||
- Subject: A white tiger in a dense jungle
|
||||
- Art Style: Realistic
|
||||
- Mood: Intense, mysterious
|
||||
- Lighting: Dramatic contrast with light filtering through leaves
|
||||
|
||||
**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."
|
||||
|
||||
2. **Input**:
|
||||
- Subject: An enchanted castle on a floating island
|
||||
- Art Style: Fantasy
|
||||
- Mood: Majestic, magical
|
||||
- Colors: Bright blues, greens, and gold
|
||||
|
||||
**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."
|
||||
|
||||
# Notes
|
||||
|
||||
- Ensure that you mix different aspects to get a comprehensive and visually compelling prompt.
|
||||
- Be as descriptive as possible as it often helps generate richer, more detailed images.
|
||||
- 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."
|
||||
|
||||
The theme of the creation is:【%s】
|
||||
现在用户输入的原始提示词为:【%s】
|
||||
`
|
||||
|
||||
const LyricPromptTemplate = `
|
||||
你是一位才华横溢的作曲家,拥有丰富的情感和细腻的笔触,你对文字有着独特的感悟力,能将各种情感和意境巧妙地融入歌词中。
|
||||
请以【%s】为主题创作一首歌曲,歌曲时间不要太短,3分钟左右,不要输出任何解释性的内容。
|
||||
输出格式如下:
|
||||
下面是一个标准的歌词输出模板:
|
||||
歌曲名称
|
||||
第一节:
|
||||
{{歌词内容}}
|
||||
副歌:
|
||||
{{歌词内容}}
|
||||
|
||||
第二节:
|
||||
{{歌词内容}}
|
||||
副歌:
|
||||
{{歌词内容}}
|
||||
[Verse]
|
||||
[歌词]
|
||||
|
||||
尾声:
|
||||
{{歌词内容}}
|
||||
[Verse 2]
|
||||
[歌词]
|
||||
|
||||
[Chorus]
|
||||
[歌词]
|
||||
|
||||
[Verse 3]
|
||||
[歌词]
|
||||
|
||||
[Bridge]
|
||||
[歌词]
|
||||
|
||||
[Chorus]
|
||||
[歌词]
|
||||
|
||||
[Verse 4]
|
||||
[歌词]
|
||||
|
||||
[Bridge]
|
||||
假如此刻眼泪能倒流
|
||||
让我学会微笑不掩忧
|
||||
一次次的碎片堆积的愁
|
||||
最终也会开成希望的秋
|
||||
|
||||
[Chorus]
|
||||
假如我还能牵你的手
|
||||
天空也许会更蔚蓝悠游
|
||||
曾经那些未完成的错过
|
||||
愿能变成今天的收获
|
||||
`
|
||||
|
||||
const VideoPromptTemplate = `
|
||||
As an expert in video generation prompts, please create a detailed descriptive prompt for the following video concept. The description should include the setting, character appearance, actions, overall atmosphere, and camera angles. Please make it as detailed and vivid as possible to help ensure that every aspect of the video is accurately captured.
|
||||
const VideoPromptTemplate = `## 任务描述
|
||||
你是一位优秀AI视频创作专家,擅长编写专业的AI视频提示词,现在你的任务是对用户输入的简单视频描述提示词进行专业优化和扩写,使其转化为详细的、具备专业影视画面感的 AI 生成视频提示词指令。需涵盖风格、主体元素、环境氛围、细节特征、人物状态(若有)、镜头运用及整体氛围营造等方面,以生动形象、富有感染力且精准的描述,引导 AI 生成高质量的视频内容。下面是一个示例:
|
||||
===示例开始===
|
||||
输入: “汽车在沙漠功能上行驶”,
|
||||
输出: “纪实摄影风格,一辆尘土飞扬的复古越野车在无垠的沙漠公路上疾驰,车身线条硬朗,漆面斑驳,透露出岁月的痕迹。驾驶室内的司机戴着墨镜,专注地握着方向盘,眼神坚定地望向前方。夕阳的余晖洒在车身上,沙漠的沙丘在远处延绵起伏,一片金黄。广角镜头捕捉到车辆行驶时扬起的沙尘,营造出动感与冒险的氛围。远景全貌,强调速度感与环境辽阔。”
|
||||
===示例结束===
|
||||
|
||||
Please remember that regardless of the user’s input, the final output must be in English.
|
||||
## 输出要求:
|
||||
1. 直接输出扩写后的提示词就好,不要输出其他任何不相关信息
|
||||
2. 如果用户用中文提问,你就用中文回答,如果用英文提问,你也必须用英文回答。
|
||||
3. 请确保提示词的长度长度在1000个字以内。
|
||||
|
||||
# Details to Include
|
||||
|
||||
- Describe the overall visual style of the video (e.g., animated, realistic, retro tone, etc.)
|
||||
- Identify key characters or objects in the video and describe their appearance, attire, and expressions
|
||||
- Describe the environment of the scene, including weather, lighting, colors, and important details
|
||||
- Explain the behavior and interactions of the characters
|
||||
- Include any unique camera angles, movements, or special effects
|
||||
|
||||
# Output Format
|
||||
Provide the prompt in paragraph form, ensuring that the description is detailed enough for a video generation system to recreate the envisioned scene. Include the beginning, middle, and end of the scene to convey a complete storyline.
|
||||
|
||||
# Example
|
||||
**User Input:**
|
||||
“A small cat basking in the sun on a balcony.”
|
||||
|
||||
**Generated Prompt:**
|
||||
On a bright spring afternoon, an orange-striped kitten lies lazily on a balcony, basking in the warm sunlight. The iron railings around the balcony cast soft shadows that dance gently with the light. The cat’s eyes are half-closed, exuding a sense of contentment and tranquility in its surroundings. In the distance, a few fluffy white clouds drift slowly across the blue sky. The camera initially focuses on the cat’s face, capturing the delicate details of its fur, and then gradually zooms out to reveal the full balcony scene, immersing viewers in a moment of calm and relaxation.
|
||||
|
||||
The theme of the creation is:【%s】
|
||||
=====
|
||||
用户的输入的视频主题是:【%s】
|
||||
`
|
||||
|
||||
const MetaPromptTemplate = `
|
||||
|
||||
@@ -1,377 +0,0 @@
|
||||
package video
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
logger2 "geekai/logger"
|
||||
"geekai/service"
|
||||
"geekai/service/oss"
|
||||
"geekai/store"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/imroc/req/v3"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
type Service struct {
|
||||
httpClient *req.Client
|
||||
db *gorm.DB
|
||||
uploadManager *oss.UploaderManager
|
||||
taskQueue *store.RedisQueue
|
||||
notifyQueue *store.RedisQueue
|
||||
wsService *service.WebsocketService
|
||||
clientIds map[uint]string
|
||||
userService *service.UserService
|
||||
}
|
||||
|
||||
func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Client, wsService *service.WebsocketService, userService *service.UserService) *Service {
|
||||
return &Service{
|
||||
httpClient: req.C().SetTimeout(time.Minute * 3),
|
||||
db: db,
|
||||
taskQueue: store.NewRedisQueue("Video_Task_Queue", redisCli),
|
||||
notifyQueue: store.NewRedisQueue("Video_Notify_Queue", redisCli),
|
||||
wsService: wsService,
|
||||
uploadManager: manager,
|
||||
clientIds: map[uint]string{},
|
||||
userService: userService,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) PushTask(task types.VideoTask) {
|
||||
logger.Infof("add a new Video task to the task list: %+v", task)
|
||||
s.taskQueue.RPush(task)
|
||||
}
|
||||
|
||||
func (s *Service) Run() {
|
||||
// 将数据库中未提交的人物加载到队列
|
||||
var jobs []model.VideoJob
|
||||
s.db.Where("task_id", "").Where("progress", 0).Find(&jobs)
|
||||
for _, v := range jobs {
|
||||
var task types.VideoTask
|
||||
err := utils.JsonDecode(v.TaskInfo, &task)
|
||||
if err != nil {
|
||||
logger.Errorf("decode task info with error: %v", err)
|
||||
continue
|
||||
}
|
||||
task.Id = v.Id
|
||||
s.PushTask(task)
|
||||
s.clientIds[v.Id] = task.ClientId
|
||||
}
|
||||
logger.Info("Starting Video job consumer...")
|
||||
go func() {
|
||||
for {
|
||||
var task types.VideoTask
|
||||
err := s.taskQueue.LPop(&task)
|
||||
if err != nil {
|
||||
logger.Errorf("taking task with error: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// translate prompt
|
||||
if utils.HasChinese(task.Prompt) {
|
||||
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.Prompt), task.TranslateModelId)
|
||||
if err == nil {
|
||||
task.Prompt = content
|
||||
} else {
|
||||
logger.Warnf("error with translate prompt: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if task.ClientId != "" {
|
||||
s.clientIds[task.Id] = task.ClientId
|
||||
}
|
||||
|
||||
var r LumaRespVo
|
||||
r, err = s.LumaCreate(task)
|
||||
if err != nil {
|
||||
logger.Errorf("create task with error: %v", err)
|
||||
err = s.db.Model(&model.VideoJob{Id: task.Id}).UpdateColumns(map[string]interface{}{
|
||||
"err_msg": err.Error(),
|
||||
"progress": service.FailTaskProgress,
|
||||
"cover_url": "/images/failed.jpg",
|
||||
}).Error
|
||||
if err != nil {
|
||||
logger.Errorf("update task with error: %v", err)
|
||||
}
|
||||
s.notifyQueue.RPush(service.NotifyMessage{ClientId: task.ClientId, UserId: task.UserId, JobId: int(task.Id), Message: service.TaskStatusFailed})
|
||||
continue
|
||||
}
|
||||
|
||||
// 更新任务信息
|
||||
err = s.db.Model(&model.VideoJob{Id: task.Id}).UpdateColumns(map[string]interface{}{
|
||||
"task_id": r.Id,
|
||||
"channel": r.Channel,
|
||||
"prompt_ext": r.Prompt,
|
||||
}).Error
|
||||
if err != nil {
|
||||
logger.Errorf("update task with error: %v", err)
|
||||
s.PushTask(task)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
type LumaRespVo struct {
|
||||
Id string `json:"id"`
|
||||
Prompt string `json:"prompt"`
|
||||
State string `json:"state"`
|
||||
QueueState interface{} `json:"queue_state"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Video interface{} `json:"video"`
|
||||
VideoRaw interface{} `json:"video_raw"`
|
||||
Liked interface{} `json:"liked"`
|
||||
EstimateWaitSeconds interface{} `json:"estimate_wait_seconds"`
|
||||
Thumbnail interface{} `json:"thumbnail"`
|
||||
Channel string `json:"channel,omitempty"`
|
||||
}
|
||||
|
||||
func (s *Service) LumaCreate(task types.VideoTask) (LumaRespVo, error) {
|
||||
// 读取 API KEY
|
||||
var apiKey model.ApiKey
|
||||
session := s.db.Session(&gorm.Session{}).Where("type", "luma").Where("enabled", true)
|
||||
if task.Channel != "" {
|
||||
session = session.Where("api_url", task.Channel)
|
||||
}
|
||||
tx := session.Order("last_used_at DESC").First(&apiKey)
|
||||
if tx.Error != nil {
|
||||
return LumaRespVo{}, errors.New("no available API KEY for Luma")
|
||||
}
|
||||
|
||||
reqBody := map[string]interface{}{
|
||||
"user_prompt": task.Prompt,
|
||||
"expand_prompt": task.Params.PromptOptimize,
|
||||
"loop": task.Params.Loop,
|
||||
"image_url": task.Params.StartImgURL,
|
||||
"image_end_url": task.Params.EndImgURL,
|
||||
}
|
||||
var res LumaRespVo
|
||||
apiURL := fmt.Sprintf("%s/luma/generations", apiKey.ApiURL)
|
||||
logger.Debugf("API URL: %s, request body: %+v", apiURL, reqBody)
|
||||
r, err := req.C().R().
|
||||
SetHeader("Authorization", "Bearer "+apiKey.Value).
|
||||
SetBody(reqBody).
|
||||
Post(apiURL)
|
||||
if err != nil {
|
||||
return LumaRespVo{}, fmt.Errorf("请求 API 出错:%v", err)
|
||||
}
|
||||
|
||||
if r.StatusCode != 200 && r.StatusCode != 201 {
|
||||
return LumaRespVo{}, fmt.Errorf("请求 API 出错:%d, %s", r.StatusCode, r.String())
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
err = json.Unmarshal(body, &res)
|
||||
if err != nil {
|
||||
return LumaRespVo{}, fmt.Errorf("解析API数据失败:%v, %s", err, string(body))
|
||||
}
|
||||
|
||||
// update the last_use_at for api key
|
||||
apiKey.LastUsedAt = time.Now().Unix()
|
||||
session.Updates(&apiKey)
|
||||
res.Channel = apiKey.ApiURL
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *Service) CheckTaskNotify() {
|
||||
go func() {
|
||||
logger.Info("Running Suno task notify checking ...")
|
||||
for {
|
||||
var message service.NotifyMessage
|
||||
err := s.notifyQueue.LPop(&message)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
logger.Debugf("Receive notify message: %+v", message)
|
||||
client := s.wsService.Clients.Get(message.ClientId)
|
||||
if client == nil {
|
||||
continue
|
||||
}
|
||||
utils.SendChannelMsg(client, types.ChLuma, message.Message)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Service) DownloadFiles() {
|
||||
go func() {
|
||||
var items []model.VideoJob
|
||||
for {
|
||||
res := s.db.Where("progress", 102).Find(&items)
|
||||
if res.Error != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, v := range items {
|
||||
if v.WaterURL == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Infof("try download video: %s", v.WaterURL)
|
||||
videoURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(v.WaterURL, true)
|
||||
if err != nil {
|
||||
logger.Errorf("download video with error: %v", err)
|
||||
continue
|
||||
}
|
||||
logger.Infof("download video success: %s", videoURL)
|
||||
v.WaterURL = videoURL
|
||||
|
||||
if v.VideoURL != "" {
|
||||
logger.Infof("try download no water video: %s", v.VideoURL)
|
||||
videoURL, err = s.uploadManager.GetUploadHandler().PutUrlFile(v.VideoURL, true)
|
||||
if err != nil {
|
||||
logger.Errorf("download video with error: %v", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
logger.Infof("download no water video success: %s", videoURL)
|
||||
v.VideoURL = videoURL
|
||||
v.Progress = 100
|
||||
s.db.Updates(&v)
|
||||
s.notifyQueue.RPush(service.NotifyMessage{ClientId: s.clientIds[v.Id], UserId: v.UserId, JobId: int(v.Id), Message: service.TaskStatusFinished})
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// SyncTaskProgress 异步拉取任务
|
||||
func (s *Service) SyncTaskProgress() {
|
||||
go func() {
|
||||
var jobs []model.VideoJob
|
||||
for {
|
||||
res := s.db.Where("progress < ?", 100).Where("task_id <> ?", "").Find(&jobs)
|
||||
if res.Error != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, job := range jobs {
|
||||
task, err := s.QueryLumaTask(job.TaskId, job.Channel)
|
||||
if err != nil {
|
||||
logger.Errorf("query task with error: %v", err)
|
||||
// 更新任务信息
|
||||
s.db.Model(&model.VideoJob{Id: job.Id}).UpdateColumns(map[string]interface{}{
|
||||
"progress": service.FailTaskProgress, // 102 表示资源未下载完成,
|
||||
"err_msg": err.Error(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Debugf("task: %+v", task)
|
||||
if task.State == "completed" { // 更新任务信息
|
||||
data := map[string]interface{}{
|
||||
"progress": 102, // 102 表示资源未下载完成,
|
||||
"water_url": task.Video.Url,
|
||||
"raw_data": utils.JsonEncode(task),
|
||||
"prompt_ext": task.Prompt,
|
||||
"cover_url": task.Thumbnail.Url,
|
||||
}
|
||||
if task.Video.DownloadUrl != "" {
|
||||
data["video_url"] = task.Video.DownloadUrl
|
||||
}
|
||||
err = s.db.Model(&model.VideoJob{Id: job.Id}).UpdateColumns(data).Error
|
||||
if err != nil {
|
||||
logger.Errorf("更新数据库失败:%v", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 找出失败的任务,并恢复其扣减算力
|
||||
s.db.Where("progress", service.FailTaskProgress).Where("power > ?", 0).Find(&jobs)
|
||||
for _, job := range jobs {
|
||||
err := s.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{
|
||||
Type: types.PowerRefund,
|
||||
Model: "luma",
|
||||
Remark: fmt.Sprintf("Luma 任务失败,退回算力。任务ID:%s,Err:%s", job.TaskId, job.ErrMsg),
|
||||
})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// 更新任务状态
|
||||
s.db.Model(&job).UpdateColumn("power", 0)
|
||||
}
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
type LumaTaskVo struct {
|
||||
Id string `json:"id"`
|
||||
Liked interface{} `json:"liked"`
|
||||
State string `json:"state"`
|
||||
Video struct {
|
||||
Url string `json:"url"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Thumbnail string `json:"thumbnail"`
|
||||
DownloadUrl string `json:"download_url"`
|
||||
} `json:"video"`
|
||||
Prompt string `json:"prompt"`
|
||||
UserId string `json:"user_id"`
|
||||
BatchId string `json:"batch_id"`
|
||||
Thumbnail struct {
|
||||
Url string `json:"url"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
} `json:"thumbnail"`
|
||||
VideoRaw struct {
|
||||
Url string `json:"url"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
} `json:"video_raw"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
LastFrame struct {
|
||||
Url string `json:"url"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
} `json:"last_frame"`
|
||||
}
|
||||
|
||||
func (s *Service) QueryLumaTask(taskId string, channel string) (LumaTaskVo, error) {
|
||||
// 读取 API KEY
|
||||
var apiKey model.ApiKey
|
||||
err := s.db.Session(&gorm.Session{}).Where("type", "luma").
|
||||
Where("api_url", channel).
|
||||
Where("enabled", true).
|
||||
Order("last_used_at DESC").First(&apiKey).Error
|
||||
if err != nil {
|
||||
return LumaTaskVo{}, errors.New("no available API KEY for Luma")
|
||||
}
|
||||
|
||||
apiURL := fmt.Sprintf("%s/luma/generations/%s", apiKey.ApiURL, taskId)
|
||||
var res LumaTaskVo
|
||||
r, err := req.C().R().SetHeader("Authorization", "Bearer "+apiKey.Value).Get(apiURL)
|
||||
|
||||
if err != nil {
|
||||
return LumaTaskVo{}, fmt.Errorf("请求 API 失败:%v", err)
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
if r.StatusCode != 200 {
|
||||
return LumaTaskVo{}, fmt.Errorf("API 返回失败:%v", r.String())
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
err = json.Unmarshal(body, &res)
|
||||
if err != nil {
|
||||
return LumaTaskVo{}, fmt.Errorf("解析API数据失败:%v, %s", err, string(body))
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
661
api/service/video/video.go
Normal file
661
api/service/video/video.go
Normal file
@@ -0,0 +1,661 @@
|
||||
package video
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
logger2 "geekai/logger"
|
||||
"geekai/service"
|
||||
"geekai/service/oss"
|
||||
"geekai/store"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
|
||||
"github.com/imroc/req/v3"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
type Service struct {
|
||||
httpClient *req.Client
|
||||
db *gorm.DB
|
||||
uploadManager *oss.UploaderManager
|
||||
taskQueue *store.RedisQueue
|
||||
userService *service.UserService
|
||||
}
|
||||
|
||||
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),
|
||||
uploadManager: manager,
|
||||
userService: userService,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) PushTask(task types.VideoTask) {
|
||||
logger.Infof("add a new Video task to the task list: %+v", task)
|
||||
s.taskQueue.RPush(task)
|
||||
}
|
||||
|
||||
func (s *Service) Run() {
|
||||
// 将数据库中未提交的任务加载到队列
|
||||
var jobs []model.VideoJob
|
||||
s.db.Where("task_id", "").Where("progress", 0).Find(&jobs)
|
||||
for _, v := range jobs {
|
||||
var task types.VideoTask
|
||||
err := utils.JsonDecode(v.TaskInfo, &task)
|
||||
if err != nil {
|
||||
logger.Errorf("decode task info with error: %v", err)
|
||||
continue
|
||||
}
|
||||
task.Id = v.Id
|
||||
s.PushTask(task)
|
||||
}
|
||||
logger.Info("Starting Video job consumer...")
|
||||
go func() {
|
||||
for {
|
||||
var task types.VideoTask
|
||||
err := s.taskQueue.LPop(&task)
|
||||
if err != nil {
|
||||
logger.Errorf("taking task with error: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if task.Type == types.VideoLuma {
|
||||
// translate prompt
|
||||
if utils.HasChinese(task.Prompt) {
|
||||
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.Prompt), task.TranslateModelId)
|
||||
if err == nil {
|
||||
task.Prompt = content
|
||||
} else {
|
||||
logger.Warnf("error with translate prompt: %v", err)
|
||||
}
|
||||
}
|
||||
var r LumaRespVo
|
||||
r, err = s.LumaCreate(task)
|
||||
if err != nil {
|
||||
logger.Errorf("create task with error: %v", err)
|
||||
err = s.db.Model(&model.VideoJob{Id: task.Id}).UpdateColumns(map[string]interface{}{
|
||||
"err_msg": err.Error(),
|
||||
"progress": service.FailTaskProgress,
|
||||
"cover_url": "/images/failed.jpg",
|
||||
}).Error
|
||||
if err != nil {
|
||||
logger.Errorf("update task with error: %v", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 更新任务信息
|
||||
err = s.db.Model(&model.VideoJob{Id: task.Id}).UpdateColumns(map[string]interface{}{
|
||||
"task_id": r.Id,
|
||||
"channel": r.Channel,
|
||||
"prompt_ext": r.Prompt,
|
||||
}).Error
|
||||
if err != nil {
|
||||
logger.Errorf("update task with error: %v", err)
|
||||
s.PushTask(task)
|
||||
}
|
||||
} else if task.Type == types.VideoKeLing {
|
||||
var r KeLingRespVo
|
||||
r, err = s.KeLingCreate(task)
|
||||
logger.Debugf("ke ling create task result: %+v", r)
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("create task with error: %v", err)
|
||||
err = s.db.Model(&model.VideoJob{Id: task.Id}).UpdateColumns(map[string]interface{}{
|
||||
"err_msg": err.Error(),
|
||||
"progress": service.FailTaskProgress,
|
||||
"cover_url": "/images/failed.jpg",
|
||||
}).Error
|
||||
if err != nil {
|
||||
logger.Errorf("update task with error: %v", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 更新任务信息
|
||||
err = s.db.Model(&model.VideoJob{Id: task.Id}).UpdateColumns(map[string]interface{}{
|
||||
"task_id": r.Data.TaskID,
|
||||
"channel": r.Channel,
|
||||
"prompt_ext": task.Prompt,
|
||||
}).Error
|
||||
if err != nil {
|
||||
logger.Errorf("update task with error: %v", err)
|
||||
s.PushTask(task)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Service) DownloadFiles() {
|
||||
go func() {
|
||||
var items []model.VideoJob
|
||||
for {
|
||||
res := s.db.Where("progress", 102).Find(&items)
|
||||
if res.Error != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, v := range items {
|
||||
if v.WaterURL == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Infof("try download video: %s", v.WaterURL)
|
||||
videoURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(v.WaterURL, true)
|
||||
if err != nil {
|
||||
logger.Errorf("download video with error: %v", err)
|
||||
continue
|
||||
}
|
||||
logger.Infof("download video success: %s", videoURL)
|
||||
v.WaterURL = videoURL
|
||||
|
||||
if v.VideoURL != "" {
|
||||
logger.Infof("try download no water video: %s", v.VideoURL)
|
||||
videoURL, err = s.uploadManager.GetUploadHandler().PutUrlFile(v.VideoURL, true)
|
||||
if err != nil {
|
||||
logger.Errorf("download video with error: %v", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
logger.Infof("download no water video success: %s", videoURL)
|
||||
v.VideoURL = videoURL
|
||||
v.Progress = 100
|
||||
s.db.Updates(&v)
|
||||
|
||||
// Convert TaskInfo to VideoTask
|
||||
var videoTask types.VideoTask
|
||||
if err := json.Unmarshal([]byte(v.TaskInfo), &videoTask); err != nil {
|
||||
logger.Errorf("failed to unmarshal task info to VideoTask: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// SyncTaskProgress 异步拉取任务
|
||||
func (s *Service) SyncTaskProgress() {
|
||||
go func() {
|
||||
var jobs []model.VideoJob
|
||||
for {
|
||||
res := s.db.Where("progress < ?", 100).Where("task_id <> ?", "").Find(&jobs)
|
||||
if res.Error != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, job := range jobs {
|
||||
if job.Type == types.VideoLuma {
|
||||
task, err := s.QueryLumaTask(job.TaskId, job.Channel)
|
||||
if err != nil {
|
||||
logger.Errorf("query task with error: %v", err)
|
||||
// 更新任务信息
|
||||
s.db.Model(&model.VideoJob{Id: job.Id}).UpdateColumns(map[string]interface{}{
|
||||
"progress": service.FailTaskProgress, // 102 表示资源未下载完成,
|
||||
"err_msg": err.Error(),
|
||||
"cover_url": "/images/failed.jpg",
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Debugf("task: %+v", task)
|
||||
if task.State == "completed" { // 更新任务信息
|
||||
data := map[string]interface{}{
|
||||
"progress": 102, // 102 表示资源未下载完成,
|
||||
"water_url": task.Video.Url,
|
||||
"raw_data": utils.JsonEncode(task),
|
||||
"prompt_ext": task.Prompt,
|
||||
"cover_url": task.Thumbnail.Url,
|
||||
}
|
||||
if task.Video.DownloadUrl != "" {
|
||||
data["video_url"] = task.Video.DownloadUrl
|
||||
}
|
||||
err = s.db.Model(&model.VideoJob{Id: job.Id}).UpdateColumns(data).Error
|
||||
if err != nil {
|
||||
logger.Errorf("更新数据库失败:%v", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
} else if job.Type == types.VideoKeLing {
|
||||
// Convert TaskInfo to VideoTask
|
||||
var videoTask types.VideoTask
|
||||
if err := json.Unmarshal([]byte(job.TaskInfo), &videoTask); err != nil {
|
||||
logger.Errorf("failed to unmarshal task info to VideoTask: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Type assert task.Params to KeLingVideoParams
|
||||
paramsMap, ok := videoTask.Params.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Convert map to KeLingVideoParams
|
||||
paramsBytes, err := json.Marshal(paramsMap)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var params types.KeLingVideoParams
|
||||
if err := json.Unmarshal(paramsBytes, ¶ms); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
task, err := s.QueryKeLingTask(job.TaskId, job.Channel, params.TaskType)
|
||||
if err != nil {
|
||||
logger.Errorf("query task with error: %v", err)
|
||||
// 更新任务信息
|
||||
s.db.Model(&model.VideoJob{Id: job.Id}).UpdateColumns(map[string]interface{}{
|
||||
"progress": service.FailTaskProgress, // 102 表示资源未下载完成,
|
||||
"err_msg": err.Error(),
|
||||
"cover_url": "/images/failed.jpg",
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Debugf("task: %+v", task)
|
||||
if task.TaskStatus == "succeed" { // 更新任务信息
|
||||
data := map[string]interface{}{
|
||||
"progress": 102, // 102 表示资源未下载完成,
|
||||
"water_url": task.TaskResult.Videos[0].URL,
|
||||
"raw_data": utils.JsonEncode(task),
|
||||
"prompt_ext": job.Prompt,
|
||||
"cover_url": "",
|
||||
}
|
||||
if len(task.TaskResult.Videos) > 0 {
|
||||
data["video_url"] = task.TaskResult.Videos[0].URL
|
||||
}
|
||||
err = s.db.Model(&model.VideoJob{Id: job.Id}).UpdateColumns(data).Error
|
||||
if err != nil {
|
||||
logger.Errorf("更新数据库失败:%v", err)
|
||||
continue
|
||||
}
|
||||
} else if task.TaskStatus == "failed" {
|
||||
// 更新任务信息
|
||||
s.db.Model(&model.VideoJob{Id: job.Id}).UpdateColumns(map[string]interface{}{
|
||||
"progress": service.FailTaskProgress,
|
||||
"err_msg": task.TaskStatusMsg,
|
||||
"cover_url": "/images/failed.jpg",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 找出失败的任务,并恢复其扣减算力
|
||||
s.db.Where("progress", service.FailTaskProgress).Where("power > ?", 0).Find(&jobs)
|
||||
for _, job := range jobs {
|
||||
err := s.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{
|
||||
Type: types.PowerRefund,
|
||||
Model: job.Type,
|
||||
Remark: fmt.Sprintf("%s 任务失败,退回算力。任务ID:%s,Err:%s", job.Type, job.TaskId, job.ErrMsg),
|
||||
})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// 更新任务状态
|
||||
s.db.Model(&job).UpdateColumn("power", 0)
|
||||
}
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
type LumaTaskVo struct {
|
||||
Id string `json:"id"`
|
||||
Liked interface{} `json:"liked"`
|
||||
State string `json:"state"`
|
||||
Video struct {
|
||||
Url string `json:"url"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Thumbnail string `json:"thumbnail"`
|
||||
DownloadUrl string `json:"download_url"`
|
||||
} `json:"video"`
|
||||
Prompt string `json:"prompt"`
|
||||
UserId string `json:"user_id"`
|
||||
BatchId string `json:"batch_id"`
|
||||
Thumbnail struct {
|
||||
Url string `json:"url"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
} `json:"thumbnail"`
|
||||
VideoRaw struct {
|
||||
Url string `json:"url"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
} `json:"video_raw"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
LastFrame struct {
|
||||
Url string `json:"url"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
} `json:"last_frame"`
|
||||
}
|
||||
|
||||
type LumaRespVo struct {
|
||||
Id string `json:"id"`
|
||||
Prompt string `json:"prompt"`
|
||||
State string `json:"state"`
|
||||
QueueState interface{} `json:"queue_state"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Video interface{} `json:"video"`
|
||||
VideoRaw interface{} `json:"video_raw"`
|
||||
Liked interface{} `json:"liked"`
|
||||
EstimateWaitSeconds interface{} `json:"estimate_wait_seconds"`
|
||||
Thumbnail interface{} `json:"thumbnail"`
|
||||
Channel string `json:"channel,omitempty"`
|
||||
}
|
||||
|
||||
func (s *Service) LumaCreate(task types.VideoTask) (LumaRespVo, error) {
|
||||
// 读取 API KEY
|
||||
var apiKey model.ApiKey
|
||||
session := s.db.Session(&gorm.Session{}).Where("type", "luma").Where("enabled", true)
|
||||
if task.Channel != "" {
|
||||
session = session.Where("api_url", task.Channel)
|
||||
}
|
||||
tx := session.Order("last_used_at DESC").First(&apiKey)
|
||||
if tx.Error != nil {
|
||||
return LumaRespVo{}, errors.New("no available API KEY for Luma")
|
||||
}
|
||||
|
||||
// Type assert task.Params to LumaVideoParams
|
||||
paramsMap, ok := task.Params.(map[string]interface{})
|
||||
if !ok {
|
||||
return LumaRespVo{}, errors.New("invalid params type for Luma video task")
|
||||
}
|
||||
|
||||
// Convert map to LumaVideoParams
|
||||
paramsBytes, err := json.Marshal(paramsMap)
|
||||
if err != nil {
|
||||
return LumaRespVo{}, fmt.Errorf("failed to marshal params: %v", err)
|
||||
}
|
||||
|
||||
var params types.LumaVideoParams
|
||||
if err := json.Unmarshal(paramsBytes, ¶ms); err != nil {
|
||||
return LumaRespVo{}, fmt.Errorf("failed to unmarshal params: %v", err)
|
||||
}
|
||||
|
||||
reqBody := map[string]interface{}{
|
||||
"user_prompt": task.Prompt,
|
||||
"expand_prompt": params.PromptOptimize,
|
||||
"loop": params.Loop,
|
||||
"image_url": params.StartImgURL, // 图生视频
|
||||
"image_end_url": params.EndImgURL, // 图生视频
|
||||
}
|
||||
|
||||
var res LumaRespVo
|
||||
apiURL := fmt.Sprintf("%s/luma/generations", apiKey.ApiURL)
|
||||
logger.Debugf("API URL: %s, request body: %+v", apiURL, reqBody)
|
||||
r, err := req.C().R().
|
||||
SetHeader("Authorization", "Bearer "+apiKey.Value).
|
||||
SetBody(reqBody).
|
||||
Post(apiURL)
|
||||
if err != nil {
|
||||
return LumaRespVo{}, fmt.Errorf("请求 API 出错:%v", err)
|
||||
}
|
||||
|
||||
if r.StatusCode != 200 && r.StatusCode != 201 {
|
||||
return LumaRespVo{}, fmt.Errorf("请求 API 出错:%d, %s", r.StatusCode, r.String())
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
err = json.Unmarshal(body, &res)
|
||||
if err != nil {
|
||||
return LumaRespVo{}, fmt.Errorf("解析API数据失败:%v, %s", err, string(body))
|
||||
}
|
||||
|
||||
// update the last_use_at for api key
|
||||
apiKey.LastUsedAt = time.Now().Unix()
|
||||
session.Updates(&apiKey)
|
||||
res.Channel = apiKey.ApiURL
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *Service) QueryLumaTask(taskId string, channel string) (LumaTaskVo, error) {
|
||||
// 读取 API KEY
|
||||
var apiKey model.ApiKey
|
||||
err := s.db.Session(&gorm.Session{}).Where("type", "luma").
|
||||
Where("api_url", channel).
|
||||
Where("enabled", true).
|
||||
Order("last_used_at DESC").First(&apiKey).Error
|
||||
if err != nil {
|
||||
return LumaTaskVo{}, errors.New("no available API KEY for Luma")
|
||||
}
|
||||
|
||||
apiURL := fmt.Sprintf("%s/luma/generations/%s", apiKey.ApiURL, taskId)
|
||||
var res LumaTaskVo
|
||||
r, err := req.C().R().SetHeader("Authorization", "Bearer "+apiKey.Value).Get(apiURL)
|
||||
|
||||
if err != nil {
|
||||
return LumaTaskVo{}, fmt.Errorf("请求 API 失败:%v", err)
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
if r.StatusCode != 200 {
|
||||
return LumaTaskVo{}, fmt.Errorf("API 返回失败:%v", r.String())
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
err = json.Unmarshal(body, &res)
|
||||
if err != nil {
|
||||
return LumaTaskVo{}, fmt.Errorf("解析API数据失败:%v, %s", err, string(body))
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
type KeLingRespVo struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
RequestID string `json:"request_id"`
|
||||
Data struct {
|
||||
TaskID string `json:"task_id"`
|
||||
TaskStatus string `json:"task_status"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
} `json:"data"`
|
||||
Channel string `json:"channel,omitempty"`
|
||||
}
|
||||
|
||||
func (s *Service) KeLingCreate(task types.VideoTask) (KeLingRespVo, error) {
|
||||
var apiKey model.ApiKey
|
||||
session := s.db.Session(&gorm.Session{}).Where("type", "keling").Where("enabled", true)
|
||||
if task.Channel != "" {
|
||||
session = session.Where("api_url", task.Channel)
|
||||
}
|
||||
tx := session.Order("last_used_at DESC").First(&apiKey)
|
||||
if tx.Error != nil {
|
||||
return KeLingRespVo{}, errors.New("no available API KEY for keling")
|
||||
}
|
||||
|
||||
// Type assert task.Params to KeLingVideoParams
|
||||
paramsMap, ok := task.Params.(map[string]interface{})
|
||||
if !ok {
|
||||
return KeLingRespVo{}, errors.New("invalid params type for KeLing video task")
|
||||
}
|
||||
|
||||
// Convert map to KeLingVideoParams
|
||||
paramsBytes, err := json.Marshal(paramsMap)
|
||||
if err != nil {
|
||||
return KeLingRespVo{}, fmt.Errorf("failed to marshal params: %v", err)
|
||||
}
|
||||
|
||||
var params types.KeLingVideoParams
|
||||
if err := json.Unmarshal(paramsBytes, ¶ms); err != nil {
|
||||
return KeLingRespVo{}, fmt.Errorf("failed to unmarshal params: %v", err)
|
||||
}
|
||||
|
||||
// 2. 构建API请求参数
|
||||
payload := map[string]interface{}{
|
||||
"model_name": params.Model,
|
||||
"prompt": task.Prompt,
|
||||
"negative_prompt": params.NegPrompt,
|
||||
"cfg_scale": params.CfgScale,
|
||||
"mode": params.Mode,
|
||||
"aspect_ratio": params.AspectRatio,
|
||||
"duration": params.Duration,
|
||||
}
|
||||
|
||||
// 只有当 CameraControl 的类型不为空时,才处理摄像机控制参数
|
||||
if params.CameraControl.Type != "" {
|
||||
cameraControl := map[string]interface{}{
|
||||
"type": params.CameraControl.Type,
|
||||
}
|
||||
|
||||
// 只有在 simple 类型时才添加 config 参数
|
||||
if params.CameraControl.Type == "simple" {
|
||||
cameraControl["config"] = params.CameraControl.Config
|
||||
}
|
||||
|
||||
payload["camera_control"] = cameraControl
|
||||
}
|
||||
|
||||
// 处理图生视频
|
||||
if params.TaskType == "image2video" {
|
||||
payload["image"] = params.Image
|
||||
payload["image_tail"] = params.ImageTail
|
||||
}
|
||||
|
||||
jsonPayload, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return KeLingRespVo{}, fmt.Errorf("failed to marshal payload: %v", err)
|
||||
}
|
||||
|
||||
// 3. 准备HTTP请求
|
||||
url := fmt.Sprintf("%s/kling/v1/videos/%s", apiKey.ApiURL, params.TaskType)
|
||||
req, err := http.NewRequest("POST", url, bytes.NewReader(jsonPayload))
|
||||
if err != nil {
|
||||
return KeLingRespVo{}, fmt.Errorf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+apiKey.Value)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// 4. 发送请求
|
||||
client := &http.Client{Timeout: time.Duration(30) * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return KeLingRespVo{}, fmt.Errorf("failed to send request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 5. 处理响应
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return KeLingRespVo{}, fmt.Errorf("failed to read response: %v", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return KeLingRespVo{}, fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var apiResponse = KeLingRespVo{}
|
||||
if err := json.Unmarshal(body, &apiResponse); err != nil {
|
||||
return KeLingRespVo{}, fmt.Errorf("failed to parse response: %v", err)
|
||||
}
|
||||
// 设置 API 通道
|
||||
apiResponse.Channel = apiKey.ApiURL
|
||||
return apiResponse, nil
|
||||
}
|
||||
|
||||
// VideoCallbackData 表示视频生成任务的回调数据
|
||||
type VideoCallbackData struct {
|
||||
TaskID string `json:"task_id"`
|
||||
TaskStatus string `json:"task_status"`
|
||||
TaskStatusMsg string `json:"task_status_msg"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
TaskResult TaskResult `json:"task_result"`
|
||||
}
|
||||
|
||||
type TaskResult struct {
|
||||
Images []CallBackImageResult `json:"images,omitempty"`
|
||||
Videos []CallBackVideoResult `json:"videos,omitempty"`
|
||||
}
|
||||
|
||||
type CallBackImageResult struct {
|
||||
Index int `json:"index"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type CallBackVideoResult struct {
|
||||
ID string `json:"id"`
|
||||
URL string `json:"url"`
|
||||
Duration string `json:"duration"`
|
||||
}
|
||||
|
||||
func (s *Service) QueryKeLingTask(taskId string, channel string, action string) (VideoCallbackData, error) {
|
||||
var apiKey model.ApiKey
|
||||
err := s.db.Session(&gorm.Session{}).Where("type", "keling").
|
||||
//Where("api_url", channel).
|
||||
Where("enabled", true).
|
||||
Order("last_used_at DESC").First(&apiKey).Error
|
||||
if err != nil {
|
||||
return VideoCallbackData{}, errors.New("no available API KEY for keling")
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/kling/v1/videos/%s/%s", apiKey.ApiURL, action, taskId)
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return VideoCallbackData{}, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+apiKey.Value)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return VideoCallbackData{}, fmt.Errorf("failed to execute request: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return VideoCallbackData{}, fmt.Errorf("unexpected status code: %d", res.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return VideoCallbackData{}, fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
|
||||
var response struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data VideoCallbackData `json:"data"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
return VideoCallbackData{}, fmt.Errorf("failed to unmarshal response: %w", err)
|
||||
}
|
||||
|
||||
if response.Code != 0 {
|
||||
return VideoCallbackData{}, fmt.Errorf("API error: %s", response.Message)
|
||||
}
|
||||
|
||||
return response.Data, nil
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -13,4 +13,5 @@ type ChatModel struct {
|
||||
Temperature float32 // 模型温度
|
||||
KeyId int // 绑定 API KEY ID
|
||||
Type string // 模型类型
|
||||
Options string // 模型选项
|
||||
}
|
||||
|
||||
@@ -10,14 +10,18 @@ package store
|
||||
import (
|
||||
"context"
|
||||
"geekai/core/types"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
)
|
||||
|
||||
func NewRedisClient(config *types.AppConfig) (*redis.Client, error) {
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: config.Redis.Url(),
|
||||
Password: config.Redis.Password,
|
||||
DB: config.Redis.DB,
|
||||
Addr: config.Redis.Url(),
|
||||
Password: config.Redis.Password,
|
||||
DB: config.Redis.DB,
|
||||
PoolSize: 20,
|
||||
PoolTimeout: 5 * time.Second,
|
||||
})
|
||||
_, err := client.Ping(context.Background()).Result()
|
||||
if err != nil {
|
||||
|
||||
@@ -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
214
api/test/crawler_test.go
Normal 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))
|
||||
}
|
||||
}
|
||||
*/
|
||||
41
api/test/run_crawler_test.sh
Normal file
41
api/test/run_crawler_test.sh
Normal 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 "测试完成"
|
||||
@@ -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{
|
||||
|
||||
1
config/config.yaml
Normal file
1
config/config.yaml
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
-- https://www.phpmyadmin.net/
|
||||
--
|
||||
-- 主机: 127.0.0.1
|
||||
-- 生成日期: 2025-02-10 10:17:27
|
||||
-- 生成日期: 2025-03-10 15:33:32
|
||||
-- 服务器版本: 8.0.33
|
||||
-- PHP 版本: 8.1.2-1ubuntu2.20
|
||||
|
||||
@@ -59,7 +59,7 @@ DROP TABLE IF EXISTS `chatgpt_api_keys`;
|
||||
CREATE TABLE `chatgpt_api_keys` (
|
||||
`id` int NOT NULL,
|
||||
`name` varchar(30) DEFAULT NULL COMMENT '名称',
|
||||
`value` varchar(100) NOT NULL COMMENT 'API KEY value',
|
||||
`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 地址',
|
||||
@@ -150,7 +150,7 @@ 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(50) NOT NULL COMMENT '模型名称',
|
||||
`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 '是否启用模型',
|
||||
@@ -169,23 +169,21 @@ CREATE TABLE `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, 44, '2023-08-23 12:06:36', '2025-01-06 14:01:08'),
|
||||
(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, 44, '2024-05-14 09:25:15', '2025-01-06 14:01:08'),
|
||||
(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'),
|
||||
(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-3.5-turbo', 'gpt-3.5-turbo', 2, 1, 1, 1.0, 1024, 4096, 1, 75, '2024-07-22 13:53:41', '2025-01-08 10:33:07'),
|
||||
(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'),
|
||||
(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'),
|
||||
(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, 57, '2025-01-06 14:01:04', '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');
|
||||
|
||||
@@ -254,8 +252,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\":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- 支持 OPenAI,Azure,文心一言,讯飞星火,清华 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\":1}'),
|
||||
(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,\"content\":\"## v4.2.0 更新日志\\n\\n- 功能优化:优化聊天页面 Notice 组件样式,采用 Vuepress 文档样式\\n- Bug 修复:修复主题切换的组件显示异常问题\\n- 功能优化:支持 DeepSeek-R1 推理模型,优化推理样式输出\\n- 功能优化:优化 Suno 歌曲播放按钮样式,居中显示\\n- 功能优化:后台管理新增模型的时候,可以绑定所有的 API KEY,而不只是能绑定 Chat 类型的 API KEY\\n- 功能新增:新增每日签到功能,每日签到可以获得算力奖励\\n- 功能优化:兼容 OpenAI o3 系列模型\\n- 功能优化:API 默认开启允许跨域调用\\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支持MidJourney,GPT,Claude,Google 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.5,GPT-4,DALL-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\":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- 支持 OPenAI,Azure,文心一言,讯飞星火,清华 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支持MidJourney,GPT,Claude,Google 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.5,GPT-4,DALL-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}');
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
@@ -267,7 +265,7 @@ DROP TABLE IF EXISTS `chatgpt_dall_jobs`;
|
||||
CREATE TABLE `chatgpt_dall_jobs` (
|
||||
`id` int NOT NULL,
|
||||
`user_id` int NOT NULL COMMENT '用户ID',
|
||||
`prompt` varchar(2000) NOT NULL COMMENT '提示词',
|
||||
`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 '原图地址',
|
||||
@@ -288,7 +286,7 @@ DROP TABLE IF EXISTS `chatgpt_files`;
|
||||
CREATE TABLE `chatgpt_files` (
|
||||
`id` int NOT NULL,
|
||||
`user_id` int NOT NULL COMMENT '用户 ID',
|
||||
`name` varchar(100) NOT NULL COMMENT '文件名',
|
||||
`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 '文件后缀',
|
||||
@@ -380,16 +378,17 @@ INSERT INTO `chatgpt_menus` (`id`, `name`, `icon`, `url`, `sort_num`, `enabled`)
|
||||
(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', 10, 1),
|
||||
(8, '应用中心', 'icon-app', '/apps', 9, 1),
|
||||
(7, '算力日志', 'icon-file', '/powerLog', 11, 1),
|
||||
(8, '应用中心', 'icon-app', '/apps', 10, 1),
|
||||
(9, '画廊', 'icon-image', '/images-wall', 5, 1),
|
||||
(10, '会员计划', 'icon-vip2', '/member', 11, 1),
|
||||
(11, '分享计划', 'icon-share1', '/invite', 12, 1),
|
||||
(12, '思维导图', 'icon-xmind', '/xmind', 8, 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', 13, 1),
|
||||
(14, '项目文档', 'icon-book', 'https://docs.geekai.me', 14, 1),
|
||||
(19, 'Suno', 'icon-suno', '/suno', 6, 1),
|
||||
(20, 'Luma', 'icon-luma', '/luma', 7, 1);
|
||||
(20, 'Luma', 'icon-luma', '/luma', 7, 1),
|
||||
(21, '可灵', 'icon-keling', '/keling', 8, 1);
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
@@ -407,7 +406,7 @@ CREATE TABLE `chatgpt_mj_jobs` (
|
||||
`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` varchar(2000) NOT NULL COMMENT '会话提示词',
|
||||
`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',
|
||||
@@ -527,7 +526,7 @@ CREATE TABLE `chatgpt_sd_jobs` (
|
||||
`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` varchar(2000) 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 '任务进度',
|
||||
@@ -608,7 +607,7 @@ 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', 12897, 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'),
|
||||
(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');
|
||||
@@ -644,8 +643,8 @@ CREATE TABLE `chatgpt_video_jobs` (
|
||||
`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` varchar(2000) NOT NULL COMMENT '提示词',
|
||||
`prompt_ext` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '优化后提示词',
|
||||
`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 '带水印的视频地址',
|
||||
@@ -904,7 +903,7 @@ ALTER TABLE `chatgpt_invite_logs`
|
||||
-- 使用表AUTO_INCREMENT `chatgpt_menus`
|
||||
--
|
||||
ALTER TABLE `chatgpt_menus`
|
||||
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=21;
|
||||
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=22;
|
||||
|
||||
--
|
||||
-- 使用表AUTO_INCREMENT `chatgpt_mj_jobs`
|
||||
963
database/geekai_plus-v4.2.2.sql
Normal file
963
database/geekai_plus-v4.2.2.sql
Normal 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 user’s 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- 支持 OPenAI,Azure,文心一言,讯飞星火,清华 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支持MidJourney,GPT,Claude,Google 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.5,GPT-4,DALL-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 */;
|
||||
10
database/update-v4.2.1.sql
Normal file
10
database/update-v4.2.1.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
ALTER TABLE `chatgpt_video_jobs` CHANGE `prompt` `prompt` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '提示词';
|
||||
ALTER TABLE `chatgpt_video_jobs` CHANGE `prompt_ext` `prompt_ext` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '优化后提示词';
|
||||
|
||||
ALTER TABLE `chatgpt_mj_jobs` CHANGE `prompt` `prompt` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '会话提示词';
|
||||
ALTER TABLE `chatgpt_sd_jobs` CHANGE `prompt` `prompt` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '会话提示词';
|
||||
ALTER TABLE `chatgpt_dall_jobs` CHANGE `prompt` `prompt` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '提示词';
|
||||
|
||||
ALTER TABLE `chatgpt_files` CHANGE `name` `name` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '文件名';
|
||||
ALTER TABLE `chatgpt_chat_models` CHANGE `name` `name` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '模型名称';
|
||||
ALTER TABLE `chatgpt_api_keys` CHANGE `value` `value` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'API KEY value';
|
||||
1
database/update-v4.2.2.sql
Normal file
1
database/update-v4.2.2.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE `chatgpt_chat_models` ADD `options` TEXT NOT NULL COMMENT '模型自定义选项' AFTER `key_id`;
|
||||
@@ -3,7 +3,7 @@ ProxyURL = ""
|
||||
MysqlDns = "root:mhSCk0NheGhmtsha@tcp(geekai-mysql:3306)/geekai_plus?charset=utf8mb4&collation=utf8mb4_unicode_ci&parseTime=True&loc=Local"
|
||||
StaticDir = "./static"
|
||||
StaticUrl = "/static"
|
||||
TikaHost = "http://tika-geekai:9998"
|
||||
TikaHost = "http://geekai-tika:9998"
|
||||
|
||||
[Session]
|
||||
SecretKey = "azyehq3ivunjhbntz78isj00i4hz2mt9xtddysfucxakadq4qbfrt0b7q3lnvg80"
|
||||
@@ -68,14 +68,6 @@ TikaHost = "http://tika-geekai:9998"
|
||||
SubDir = ""
|
||||
Domain = ""
|
||||
|
||||
[XXLConfig] # xxl-job 配置,需要你部署 XXL-JOB 定时任务工具,用来定期清理未支付订单和清理过期 VIP,如果你没有启用支付服务,则该服务也无需启动
|
||||
Enabled = false # 是否启用 XXL JOB 服务
|
||||
ServerAddr = "http://geekai-xxl-job-admin:8080/xxl-job-admin" # xxl-job-admin 管理地址
|
||||
ExecutorIp = "geekai-api" # 执行器 IP 地址
|
||||
ExecutorPort = "9999" # 执行器服务端口
|
||||
AccessToken = "GeekMaster" # 执行器 API 通信 token
|
||||
RegistryKey = "chatgpt-plus" # 任务注册 key,需要与 xxl-job 管理后台配置一致,请不要随意改动
|
||||
|
||||
# 支付宝商户支付
|
||||
[AlipayConfig]
|
||||
Enabled = false # 启用支付宝支付通道
|
||||
|
||||
963
docker/data/mysql/init.d/geekai_plus-v4.2.2.sql
Normal file
963
docker/data/mysql/init.d/geekai_plus-v4.2.2.sql
Normal 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 user’s 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- 支持 OPenAI,Azure,文心一言,讯飞星火,清华 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支持MidJourney,GPT,Claude,Google 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.5,GPT-4,DALL-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 */;
|
||||
@@ -37,37 +37,16 @@ services:
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
|
||||
# xxl-job-admin:
|
||||
# container_name: geekai-xxl-job-admin
|
||||
# image: registry.cn-shenzhen.aliyuncs.com/geekmaster/xxl-job-admin:2.4.0
|
||||
# restart: always
|
||||
# ports:
|
||||
# - "8081:8080"
|
||||
# environment:
|
||||
# - PARAMS=--spring.config.location=/application.properties
|
||||
# volumes:
|
||||
# - ./logs/xxl-job:/data/applogs
|
||||
# - ./conf/xxl-job/application.properties:/application.properties
|
||||
|
||||
tika:
|
||||
geekai-tika:
|
||||
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/tika:latest
|
||||
container_name: geekai-tika
|
||||
restart: always
|
||||
ports:
|
||||
- "9999:9998"
|
||||
|
||||
# midjourney-proxy:
|
||||
# image: registry.cn-shenzhen.aliyuncs.com/geekmaster/midjourney-proxy:2.6.2
|
||||
# container_name: geekai-midjourney-proxy
|
||||
# restart: always
|
||||
# ports:
|
||||
# - "8082:8080"
|
||||
# volumes:
|
||||
# - ./conf/mj-proxy:/home/spring/config
|
||||
|
||||
# 后端 API 程序
|
||||
geekai-api:
|
||||
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-plus-api:v4.2.0-amd64
|
||||
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-plus-api:v4.2.1-amd64
|
||||
container_name: geekai-api
|
||||
restart: always
|
||||
depends_on:
|
||||
@@ -90,7 +69,7 @@ services:
|
||||
|
||||
# 前端应用
|
||||
geekai-web:
|
||||
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-plus-web:v4.2.0-amd64
|
||||
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-plus-web:v4.2.1-amd64
|
||||
container_name: geekai-web
|
||||
restart: always
|
||||
depends_on:
|
||||
|
||||
@@ -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.0
|
||||
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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
VUE_APP_API_HOST=
|
||||
VUE_APP_WS_HOST=
|
||||
VUE_APP_KEY_PREFIX=GeekAI_
|
||||
VUE_APP_VERSION=v4.2.0
|
||||
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
|
||||
|
||||
8
web/package-lock.json
generated
8
web/package-lock.json
generated
@@ -38,7 +38,8 @@
|
||||
"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",
|
||||
@@ -12951,6 +12952,11 @@
|
||||
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/vue-waterfall-plugin-next": {
|
||||
"version": "2.6.5",
|
||||
"resolved": "https://registry.npmmirror.com/vue-waterfall-plugin-next/-/vue-waterfall-plugin-next-2.6.5.tgz",
|
||||
"integrity": "sha512-8ACGbdjoyKLiJfnKXB8h8f9eE14lhyzfI1N1nrfVAIRczSpNY1KRwGOnVXN5OHqheLl3V1C0uVVRPtjTJkHkhw=="
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",
|
||||
|
||||
@@ -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",
|
||||
@@ -22,7 +21,7 @@
|
||||
"json-bigint": "^1.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "^13.0.1",
|
||||
"markdown-it-emoji": "^2.0.0",
|
||||
"markdown-it-emoji": "^2.0.0",
|
||||
"markdown-it-mathjax3": "^4.3.2",
|
||||
"markmap-common": "^0.16.0",
|
||||
"markmap-lib": "^0.16.1",
|
||||
@@ -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",
|
||||
|
||||
BIN
web/public/images/mj/mj-v7.png
Normal file
BIN
web/public/images/mj/mj-v7.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 840 KiB |
BIN
web/public/images/voice.gif
Normal file
BIN
web/public/images/voice.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
@@ -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;
|
||||
|
||||
363
web/src/assets/css/keling.styl
Normal file
363
web/src/assets/css/keling.styl
Normal file
@@ -0,0 +1,363 @@
|
||||
.page-keling
|
||||
display flex
|
||||
min-height 100vh
|
||||
:deep(.el-form-item__label)
|
||||
color var(--text-theme-color)
|
||||
.grid-content
|
||||
// background-color #383838
|
||||
background var(--card-bg)
|
||||
border-radius 8px
|
||||
padding 8px 14px
|
||||
display flex
|
||||
cursor pointer
|
||||
margin-bottom 10px
|
||||
// border 1px solid #383838
|
||||
border 1px solid var(--chat-bg)
|
||||
&:hover
|
||||
border 1px solid var(--theme-border-hover)
|
||||
.icon
|
||||
width 20px
|
||||
height 20px
|
||||
margin-bottom 5px
|
||||
.texts
|
||||
margin-left 5px
|
||||
margin-top 2px
|
||||
color var(--text-theme-color)
|
||||
.param-line.pt
|
||||
padding-top 5px
|
||||
padding-bottom 5px
|
||||
.grid-content.active
|
||||
// color #47fff1
|
||||
// background-color #585858
|
||||
border 1px solid var(--theme-border-hover)
|
||||
.h-20
|
||||
height 4rem !important
|
||||
.main-content
|
||||
padding-right 1.5rem
|
||||
padding-left 1.5rem
|
||||
padding-bottom 1rem
|
||||
flex 1
|
||||
background var(--chat-bg)
|
||||
// width: 100%;
|
||||
// padding 0 10px 10px 10px
|
||||
color var(--text-theme-color)
|
||||
overflow-x hidden
|
||||
.camera-control
|
||||
padding 10px
|
||||
border-radius 4px
|
||||
background var(--card-bg)
|
||||
:deep(.el-form-item:last-child)
|
||||
margin-bottom 0 !important
|
||||
.title-tabs
|
||||
:deep(.el-tabs__item.is-active)
|
||||
color var(--theme-textcolor-normal)
|
||||
font-size 18px
|
||||
:deep(.el-tabs__item)
|
||||
color var(--text-theme-color)
|
||||
font-size 18px
|
||||
.el-tabs
|
||||
--el-tabs-header-height 55px
|
||||
.el-tabs__item
|
||||
color var(--text-theme-color)
|
||||
font-size 18px
|
||||
.el-tabs__item.is-active, .title-tabs .el-tabs__item.is-active
|
||||
.title-tabs .el-tabs__active-bar
|
||||
background-color var(--theme-textcolor-normal)
|
||||
:deep(.el-textarea)
|
||||
--el-input-focus-border-color var(--el-color-primary)
|
||||
:deep(.el-textarea__inner)
|
||||
background transparent
|
||||
color var(--text-theme-color)
|
||||
.el-input__wrapper
|
||||
background transparent
|
||||
padding 5px
|
||||
.text
|
||||
margin-bottom 10px
|
||||
color #6b778c
|
||||
font-size 15px
|
||||
.param-line.pt
|
||||
padding-top 5px
|
||||
padding-bottom 5px
|
||||
.form-item-inner
|
||||
display flex
|
||||
align-items center
|
||||
.el-icon
|
||||
margin-left 10px
|
||||
.el-form-item__label
|
||||
color var(--text-theme-color)
|
||||
// 图片上传样式
|
||||
.img-inline
|
||||
display flex
|
||||
gap 20px
|
||||
align-items center
|
||||
.img-uploader
|
||||
text-align center
|
||||
:deep(.el-upload)
|
||||
border 1px dashed var(--el-border-color)
|
||||
border-radius 6px
|
||||
cursor pointer
|
||||
position relative
|
||||
overflow hidden
|
||||
width 120px
|
||||
height 120px
|
||||
line-height 120px
|
||||
transition var(--el-transition-duration-fast)
|
||||
margin-bottom 20px
|
||||
&:hover
|
||||
border-color var(--el-color-primary)
|
||||
.el-icon.uploader-icon
|
||||
font-size 28px
|
||||
color #8c939d
|
||||
width 100%
|
||||
height 120px
|
||||
text-align center
|
||||
.img-list-box
|
||||
display flex
|
||||
.img-item
|
||||
width 120px
|
||||
position relative
|
||||
margin-right 10px
|
||||
.el-image
|
||||
width 120px
|
||||
height 120px
|
||||
border-radius 5px
|
||||
.el-button
|
||||
position absolute
|
||||
right 5px
|
||||
top 5px
|
||||
width 20px
|
||||
height 20px
|
||||
.el-row.text-info
|
||||
width 100%
|
||||
padding 10px 0
|
||||
.el-tag
|
||||
margin-right 10px
|
||||
// 提交按钮
|
||||
.submit-btn
|
||||
display flex
|
||||
margin 20px 0
|
||||
.el-button
|
||||
width 200px
|
||||
.video-list
|
||||
.btn
|
||||
margin-right 10px
|
||||
border none
|
||||
border-radius 5px
|
||||
padding 5px 10px
|
||||
cursor pointer
|
||||
color var(--theme-text-color-primary)
|
||||
background-color var(--btn-bg)
|
||||
&:hover
|
||||
opacity 0.7
|
||||
.list-box
|
||||
padding 0
|
||||
.item
|
||||
display flex
|
||||
flex-flow row
|
||||
align-items center
|
||||
min-height 100px
|
||||
padding 10px 15px
|
||||
border-radius 10px
|
||||
cursor pointer
|
||||
margin-bottom 20px
|
||||
background var(--chat-bg)
|
||||
.left
|
||||
.container
|
||||
width 160px
|
||||
position relative
|
||||
max-height 120px
|
||||
overflow hidden
|
||||
display flex
|
||||
justify-content center
|
||||
align-items center
|
||||
.video
|
||||
width 160px
|
||||
border-radius 5px
|
||||
.el-image
|
||||
width 160px
|
||||
height 90px
|
||||
border-radius 5px
|
||||
.duration
|
||||
position absolute
|
||||
bottom 0
|
||||
right 0
|
||||
background-color rgba(14, 8, 8, 0.7)
|
||||
padding 0 3px
|
||||
font-family 'Input Sans'
|
||||
font-size 14px
|
||||
font-weight 700
|
||||
border-radius 0.125rem
|
||||
.play
|
||||
position absolute
|
||||
width 100%
|
||||
height 100%
|
||||
top 0
|
||||
left 50%
|
||||
border none
|
||||
border-radius 5px
|
||||
background rgba(100, 100, 100, 0.3)
|
||||
cursor pointer
|
||||
color var(--text-theme-color)
|
||||
opacity 0
|
||||
transform translate(-50%, 0px)
|
||||
transition opacity 0.3s ease 0s
|
||||
&:hover
|
||||
.play
|
||||
opacity 1
|
||||
// display block
|
||||
.center
|
||||
width 100%
|
||||
// border 1px solid saddlebrown
|
||||
display flex
|
||||
justify-content center
|
||||
align-items flex-start
|
||||
flex-flow column
|
||||
padding 0 20px
|
||||
.prompt, .failed
|
||||
padding 0
|
||||
font-size 16px
|
||||
max-height 60px
|
||||
line-height 28px
|
||||
overflow hidden
|
||||
text-overflow ellipsis
|
||||
.prompt
|
||||
color var(--text-fb)
|
||||
cursor text
|
||||
.failed
|
||||
color #E4696B
|
||||
.right
|
||||
display flex
|
||||
justify-content right
|
||||
min-width 200px
|
||||
font-size 14px
|
||||
padding 0
|
||||
.tools
|
||||
display flex
|
||||
justify-content left
|
||||
align-items center
|
||||
flex-flow row
|
||||
height 90px
|
||||
.btn-publish
|
||||
padding 2px 10px
|
||||
.text
|
||||
margin-right 10px
|
||||
.btn-icon
|
||||
background none
|
||||
padding 6px
|
||||
transition background 0.6s ease 0s
|
||||
color #919191
|
||||
&:hover
|
||||
// background #5f5958
|
||||
// color #e1e1e1
|
||||
color var(--el-color-primary)
|
||||
.downloading
|
||||
width 16px
|
||||
.pagination
|
||||
margin-top 20px
|
||||
display flex
|
||||
justify-content center
|
||||
.inner
|
||||
display flex
|
||||
width 100%
|
||||
.mj-box
|
||||
margin 10px
|
||||
// background-color #262626
|
||||
// border 1px solid #454545
|
||||
// height: calc(100vh - 50px)
|
||||
// overflow: scroll
|
||||
min-width 300px
|
||||
max-width 300px
|
||||
padding 20px
|
||||
border-radius 10px
|
||||
color var(--text-theme-color)
|
||||
font-size 14px
|
||||
overflow auto
|
||||
h2
|
||||
font-weight bold
|
||||
font-size 20px
|
||||
text-align center
|
||||
color var(--theme-textcolor-normal)
|
||||
// 隐藏滚动条
|
||||
::-webkit-scrollbar
|
||||
width 0
|
||||
height 0
|
||||
background-color transparent
|
||||
.mj-params
|
||||
margin-top 10px
|
||||
overflow auto
|
||||
.param-line
|
||||
padding 0 10px
|
||||
.el-icon
|
||||
position relative
|
||||
.model
|
||||
background var(--card-bg)
|
||||
// border 1px solid #454545
|
||||
border-radius 8px
|
||||
padding 5px
|
||||
margin-bottom 10px
|
||||
display flex
|
||||
flex-flow column
|
||||
align-items center
|
||||
cursor pointer
|
||||
border 1px solid var(--chat-bg)
|
||||
&:hover
|
||||
border 1px solid var(--theme-border-hover)
|
||||
.el-image
|
||||
height 40px
|
||||
width 100%
|
||||
.text
|
||||
margin-top 4px
|
||||
font-size 12px
|
||||
.model.active
|
||||
// color #47fff1
|
||||
// background-color #585858
|
||||
border 1px solid var(--theme-border-hover)
|
||||
.form-item-inner
|
||||
display flex
|
||||
align-items center
|
||||
.el-select
|
||||
--el-select-input-focus-border-color var(--el-color-primary)
|
||||
--el-input-focus-border-color var(--el-color-primary)
|
||||
.el-input__wrapper
|
||||
background var(--chat-bg)
|
||||
.el-input__inner
|
||||
color var(--text-theme-color)
|
||||
.el-icon
|
||||
margin-left 10px
|
||||
.img-uploader
|
||||
.el-upload
|
||||
border 1px dashed var(--el-border-color)
|
||||
border-radius 6px
|
||||
cursor pointer
|
||||
position relative
|
||||
overflow hidden
|
||||
width 100%
|
||||
transition var(--el-transition-duration-fast)
|
||||
&:hover
|
||||
border-color var(--el-color-primary)
|
||||
.el-icon.uploader-icon
|
||||
font-size 28px
|
||||
color #8c939d
|
||||
width 100%
|
||||
height 120px
|
||||
text-align center
|
||||
.param-line.pt
|
||||
display flex
|
||||
align-items center
|
||||
padding-top 5px
|
||||
padding-bottom 5px
|
||||
.el-form
|
||||
.el-form-item__label
|
||||
color var(--text-theme-color)
|
||||
.el-input, .el-slider
|
||||
width 180px
|
||||
.uploader-icon
|
||||
font-size 24px
|
||||
position relative
|
||||
top 3px
|
||||
.no-more-data
|
||||
text-align center
|
||||
padding 30px
|
||||
.generate-btn
|
||||
.iconfont
|
||||
margin-right 5px
|
||||
@@ -2,6 +2,7 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background-color: #0E0808;
|
||||
height: 100vh;
|
||||
|
||||
.inner {
|
||||
text-align left
|
||||
@@ -62,7 +63,6 @@
|
||||
|
||||
.prompt {
|
||||
width 100%
|
||||
height 500px
|
||||
background-color transparent
|
||||
white-space pre-wrap
|
||||
overflow-y auto
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
.song {
|
||||
display flex
|
||||
padding 10px
|
||||
background-color #252020
|
||||
background-color var(--el-bg-color)
|
||||
border-radius 10px
|
||||
margin-bottom 10px
|
||||
font-size 14px
|
||||
@@ -109,6 +109,7 @@
|
||||
display flex
|
||||
margin-left 10px
|
||||
align-items center
|
||||
color var(--el-color-primary)
|
||||
}
|
||||
|
||||
.el-button--info {
|
||||
@@ -281,8 +282,8 @@
|
||||
|
||||
.model {
|
||||
color #8f8f8f
|
||||
// background-color #1C1616
|
||||
// border 1px solid #8f8f8f
|
||||
background-color var(--el-bg-color)
|
||||
border 1px solid var(--el-border-color-light)
|
||||
font-weight normal
|
||||
font-size 12px
|
||||
padding 1px 3px
|
||||
@@ -347,7 +348,9 @@
|
||||
|
||||
.task {
|
||||
height 100px
|
||||
background-color #2A2525
|
||||
background-color var(--el-bg-color)
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius 5px
|
||||
display flex
|
||||
margin-bottom 10px
|
||||
.left {
|
||||
@@ -358,7 +361,7 @@
|
||||
width 320px
|
||||
.title {
|
||||
font-size 14px
|
||||
color #e1e1e1
|
||||
color var(--el-text-color-primary)
|
||||
white-space: nowrap; /* 防止文字换行 */
|
||||
overflow: hidden; /* 隐藏溢出的内容 */
|
||||
text-overflow: ellipsis; /* 用省略号表示溢出的内容 */
|
||||
|
||||
@@ -45,6 +45,10 @@
|
||||
--chat-content-bg:rgba(86, 86, 95, .2);
|
||||
--chat-user-content-bg: #762AA4;
|
||||
--hover-deep-color:#30323c;
|
||||
--tab-title-bg:#525777;//顶部tab栏背景切换
|
||||
--tab-title-color:#fff;//顶部tab栏文字切换
|
||||
//深黑色
|
||||
--bg-deep-color:rgba(255,255,255,0.8);
|
||||
//layout
|
||||
.more-menus li.moreTitle,
|
||||
.twoTittle .title,
|
||||
|
||||
@@ -39,6 +39,10 @@
|
||||
--el-bg-color:#fff;
|
||||
--el-fill-color-blank: #fff;
|
||||
--el-pagination-button-bg-color: rgba(86,86,95,0.2);
|
||||
--tab-title-bg:#fff;//顶部tab栏背景切换
|
||||
--tab-title-color:#595959;//顶部tab栏文字切换
|
||||
|
||||
|
||||
|
||||
// 操作按钮
|
||||
--btn-bg: rgba(100, 100, 100, .1);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 4125778 */
|
||||
src: url('iconfont.woff2?t=1736144380052') format('woff2'),
|
||||
url('iconfont.woff?t=1736144380052') format('woff'),
|
||||
url('iconfont.ttf?t=1736144380052') format('truetype');
|
||||
src: url('iconfont.woff2?t=1740279975534') format('woff2'),
|
||||
url('iconfont.woff?t=1740279975534') format('woff'),
|
||||
url('iconfont.ttf?t=1740279975534') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
@@ -13,6 +13,10 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-keling:before {
|
||||
content: "\eab7";
|
||||
}
|
||||
|
||||
.icon-gitee:before {
|
||||
content: "\e6d0";
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -5,6 +5,13 @@
|
||||
"css_prefix_text": "icon-",
|
||||
"description": "",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "42692844",
|
||||
"name": "可灵大模型",
|
||||
"font_class": "keling",
|
||||
"unicode": "eab7",
|
||||
"unicode_decimal": 60087
|
||||
},
|
||||
{
|
||||
"icon_id": "6905420",
|
||||
"name": "码云",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
web/src/assets/img/failed.png
Normal file
BIN
web/src/assets/img/failed.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
BIN
web/src/assets/img/loading.gif
Normal file
BIN
web/src/assets/img/loading.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
@@ -2,24 +2,23 @@
|
||||
<div class="chat-line chat-line-prompt-list" v-if="listStyle === 'list'">
|
||||
<div class="chat-line-inner">
|
||||
<div class="chat-icon">
|
||||
<img :src="data.icon" alt="User"/>
|
||||
<img :src="data.icon" alt="User" />
|
||||
</div>
|
||||
|
||||
<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"/>
|
||||
<el-image :src="file.url" fit="cover" />
|
||||
</div>
|
||||
<div class="item" v-else>
|
||||
<div class="icon">
|
||||
<el-image :src="GetFileIcon(file.ext)" fit="cover"/>
|
||||
<el-image :src="GetFileIcon(file.ext)" fit="cover" />
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="title">
|
||||
<el-link :href="file.url" target="_blank" style="--el-font-weight-primary: bold">{{
|
||||
file.name
|
||||
}}
|
||||
<el-link :href="file.url" target="_blank" style="--el-font-weight-primary: bold"
|
||||
>{{ file.name }}
|
||||
</el-link>
|
||||
</div>
|
||||
<div class="info">
|
||||
@@ -33,7 +32,7 @@
|
||||
<div class="content" v-html="content"></div>
|
||||
<div class="bar" v-if="data.created_at > 0">
|
||||
<span class="bar-item"
|
||||
><el-icon><Clock/></el-icon> {{ dateFormat(data.created_at) }}</span
|
||||
><el-icon><Clock /></el-icon> {{ dateFormat(data.created_at) }}</span
|
||||
>
|
||||
<span class="bar-item">tokens: {{ finalTokens }}</span>
|
||||
</div>
|
||||
@@ -44,24 +43,23 @@
|
||||
<div class="chat-line chat-line-prompt-chat" v-else>
|
||||
<div class="chat-line-inner">
|
||||
<div class="chat-icon">
|
||||
<img :src="data.icon" alt="User"/>
|
||||
<img :src="data.icon" alt="User" />
|
||||
</div>
|
||||
|
||||
<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"/>
|
||||
<el-image :src="file.url" fit="cover" />
|
||||
</div>
|
||||
<div class="item" v-else>
|
||||
<div class="icon">
|
||||
<el-image :src="GetFileIcon(file.ext)" fit="cover"/>
|
||||
<el-image :src="GetFileIcon(file.ext)" fit="cover" />
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="title">
|
||||
<el-link :href="file.url" target="_blank" style="--el-font-weight-primary: bold">{{
|
||||
file.name
|
||||
}}
|
||||
<el-link :href="file.url" target="_blank" style="--el-font-weight-primary: bold"
|
||||
>{{ file.name }}
|
||||
</el-link>
|
||||
</div>
|
||||
<div class="info">
|
||||
@@ -77,7 +75,7 @@
|
||||
</div>
|
||||
<div class="bar" v-if="data.created_at > 0">
|
||||
<span class="bar-item"
|
||||
><el-icon><Clock/></el-icon> {{ dateFormat(data.created_at) }}</span
|
||||
><el-icon><Clock /></el-icon> {{ dateFormat(data.created_at) }}</span
|
||||
>
|
||||
<!-- <span class="bar-item">tokens: {{ finalTokens }}</span>-->
|
||||
</div>
|
||||
@@ -87,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,
|
||||
@@ -103,83 +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,
|
||||
"</textarea>"
|
||||
)}</textarea>`;
|
||||
/<\/textarea>/g,
|
||||
'</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 linkRegex = /(https?:\/\/\S+)/g
|
||||
const links = props.data.content.match(linkRegex)
|
||||
const urlPrefix = `${window.location.protocol}//${window.location.host}`
|
||||
if (links) {
|
||||
httpPost("/api/upload/list", {urls: links})
|
||||
.then((res) => {
|
||||
files.value = res.data.items;
|
||||
// 把本地链接转换为相对路径
|
||||
const _links = links.map((link) => {
|
||||
if (link.startsWith(urlPrefix)) {
|
||||
return link.replace(urlPrefix, '')
|
||||
}
|
||||
return link
|
||||
})
|
||||
// 合并数组并去重
|
||||
const urls = [...new Set([...links, ..._links])]
|
||||
httpPost('/api/upload/list', { urls: urls })
|
||||
.then((res) => {
|
||||
files.value = res.data.items
|
||||
|
||||
for (let link of links) {
|
||||
if (isExternalImg(link, files.value)) {
|
||||
files.value.push({url: link, ext: ".png"});
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
});
|
||||
// for (let link of links) {
|
||||
// if (isExternalImg(link, files.value)) {
|
||||
// files.value.push({ url: link, ext: ".png" });
|
||||
// }
|
||||
// }
|
||||
})
|
||||
.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">
|
||||
@@ -193,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;
|
||||
@@ -231,6 +240,8 @@ const isExternalImg = (link, files) => {
|
||||
border 1px solid #e3e3e3
|
||||
border-radius 10px
|
||||
margin-bottom 10px
|
||||
max-width 150px
|
||||
max-height 150px
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,6 +375,8 @@ const isExternalImg = (link, files) => {
|
||||
border 1px solid #e3e3e3
|
||||
border-radius 10px
|
||||
margin-bottom 10px
|
||||
max-width 150px
|
||||
max-height 150px
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,54 +156,87 @@ 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,
|
||||
"</textarea>"
|
||||
)}</textarea>`;
|
||||
'</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);
|
||||
|
||||
.chat-line {
|
||||
.boxed {
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 5px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
.chat-item {
|
||||
.content-wrapper {
|
||||
img {
|
||||
@@ -260,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);
|
||||
@@ -279,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;
|
||||
@@ -319,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
|
||||
@@ -398,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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -53,6 +53,7 @@ import {isImage, removeArrayItem} from "@/utils/libs";
|
||||
import {GetFileIcon} from "@/store/system";
|
||||
import {checkSession} from "@/store/cache";
|
||||
import {useSharedStore} from "@/store/sharedata";
|
||||
import {closeLoading, showLoading} from "@/utils/dialog";
|
||||
|
||||
const props = defineProps({
|
||||
userId: Number,
|
||||
@@ -111,14 +112,17 @@ const onScroll = (options) => {
|
||||
const afterRead = (file) => {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file.file, file.name);
|
||||
showLoading("文件上传中...");
|
||||
// 执行上传操作
|
||||
httpPost("/api/upload", formData)
|
||||
.then((res) => {
|
||||
fileData.items.unshift(res.data);
|
||||
ElMessage.success({ message: "上传成功", duration: 500 });
|
||||
closeLoading()
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("图片上传失败:" + e.message);
|
||||
closeLoading()
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div>
|
||||
<span>{{ copyRight }}</span>
|
||||
</div>
|
||||
<div v-if="!license.de_copy">
|
||||
<div v-if="!license?.de_copy">
|
||||
<a :href="gitURL" target="_blank">
|
||||
{{ title }} -
|
||||
{{ version }}
|
||||
@@ -30,15 +30,19 @@ const license = ref({});
|
||||
const props = defineProps({
|
||||
textColor: {
|
||||
type: String,
|
||||
default: "#ffffff",
|
||||
},
|
||||
default: "#ffffff"
|
||||
}
|
||||
});
|
||||
|
||||
// 获取系统配置
|
||||
getSystemInfo()
|
||||
.then((res) => {
|
||||
title.value = res.data.title ?? process.env.VUE_APP_TITLE;
|
||||
copyRight.value = (res.data.copyright ? res.data.copyright : "极客学长") + " © 2023 - " + new Date().getFullYear() + " All rights reserved";
|
||||
copyRight.value =
|
||||
(res.data.copyright ? res.data.copyright : "极客学长") +
|
||||
" © 2023 - " +
|
||||
new Date().getFullYear() +
|
||||
" All rights reserved";
|
||||
icp.value = res.data.icp;
|
||||
})
|
||||
.catch((e) => {
|
||||
|
||||
@@ -63,7 +63,11 @@ const doSendMsg = (data) => {
|
||||
x: data.x,
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success("验证码发送成功");
|
||||
if (props.type === "mobile") {
|
||||
ElMessage.success("验证码发送成功");
|
||||
} else if (props.type === "email") {
|
||||
ElMessage.success("验证码已发送至邮箱,如果长时间未收到,请检查是否在垃圾邮件中!");
|
||||
}
|
||||
let time = 60;
|
||||
btnText.value = time;
|
||||
const handler = setInterval(() => {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -5,351 +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: "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 }
|
||||
|
||||
@@ -1,84 +1,99 @@
|
||||
import {httpGet} from "@/utils/http";
|
||||
import { httpGet } from "@/utils/http";
|
||||
import Storage from "good-storage";
|
||||
import {randString} from "@/utils/libs";
|
||||
import { randString } from "@/utils/libs";
|
||||
|
||||
const userDataKey = "USER_INFO_CACHE_KEY"
|
||||
const adminDataKey = "ADMIN_INFO_CACHE_KEY"
|
||||
const systemInfoKey = "SYSTEM_INFO_CACHE_KEY"
|
||||
const licenseInfoKey = "LICENSE_INFO_CACHE_KEY"
|
||||
const userDataKey = "USER_INFO_CACHE_KEY";
|
||||
const adminDataKey = "ADMIN_INFO_CACHE_KEY";
|
||||
const systemInfoKey = "SYSTEM_INFO_CACHE_KEY";
|
||||
const licenseInfoKey = "LICENSE_INFO_CACHE_KEY";
|
||||
export function checkSession() {
|
||||
return new Promise((resolve, reject) => {
|
||||
httpGet('/api/user/session').then(res => {
|
||||
resolve(res.data)
|
||||
}).catch(e => {
|
||||
Storage.remove(userDataKey)
|
||||
reject(e)
|
||||
})
|
||||
})
|
||||
const item = Storage.get(userDataKey) ?? { expire: 0, data: null };
|
||||
if (item.expire > Date.now()) {
|
||||
return Promise.resolve(item.data);
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
httpGet("/api/user/session")
|
||||
.then((res) => {
|
||||
item.data = res.data;
|
||||
item.expire = Date.now() + 1000 * 3;
|
||||
Storage.set(userDataKey, item);
|
||||
resolve(item.data);
|
||||
})
|
||||
.catch((e) => {
|
||||
Storage.remove(userDataKey);
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
export function checkAdminSession() {
|
||||
const item = Storage.get(adminDataKey) ?? {expire:0, data:null}
|
||||
if (item.expire > Date.now()) {
|
||||
return Promise.resolve(item.data)
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
httpGet('/api/admin/session').then(res => {
|
||||
item.data = res.data
|
||||
item.expire = Date.now() + 1000 * 30
|
||||
Storage.set(adminDataKey, item)
|
||||
resolve(item.data)
|
||||
}).catch(e => {
|
||||
Storage.remove(adminDataKey)
|
||||
reject(e)
|
||||
})
|
||||
})
|
||||
const item = Storage.get(adminDataKey) ?? { expire: 0, data: null };
|
||||
if (item.expire > Date.now()) {
|
||||
return Promise.resolve(item.data);
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
httpGet("/api/admin/session")
|
||||
.then((res) => {
|
||||
item.data = res.data;
|
||||
item.expire = Date.now() + 1000 * 30;
|
||||
Storage.set(adminDataKey, item);
|
||||
resolve(item.data);
|
||||
})
|
||||
.catch((e) => {
|
||||
Storage.remove(adminDataKey);
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function removeAdminInfo() {
|
||||
Storage.remove(adminDataKey)
|
||||
Storage.remove(adminDataKey);
|
||||
}
|
||||
|
||||
export function getSystemInfo() {
|
||||
const item = Storage.get(systemInfoKey) ?? {expire:0, data:null}
|
||||
if (item.expire > Date.now()) {
|
||||
return Promise.resolve(item.data)
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
httpGet('/api/config/get?key=system').then(res => {
|
||||
item.data = res
|
||||
item.expire = Date.now() + 1000 * 30
|
||||
Storage.set(systemInfoKey, item)
|
||||
resolve(item.data)
|
||||
}).catch(err => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
const item = Storage.get(systemInfoKey) ?? { expire: 0, data: null };
|
||||
if (item.expire > Date.now()) {
|
||||
return Promise.resolve(item.data);
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
httpGet("/api/config/get?key=system")
|
||||
.then((res) => {
|
||||
item.data = res;
|
||||
item.expire = Date.now() + 1000 * 30;
|
||||
Storage.set(systemInfoKey, item);
|
||||
resolve(item.data);
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function getLicenseInfo() {
|
||||
const item = Storage.get(licenseInfoKey) ?? {expire:0, data:null}
|
||||
if (item.expire > Date.now()) {
|
||||
return Promise.resolve(item.data)
|
||||
}
|
||||
const item = Storage.get(licenseInfoKey) ?? { expire: 0, data: null };
|
||||
if (item.expire > Date.now()) {
|
||||
return Promise.resolve(item.data);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
httpGet('/api/config/license').then(res => {
|
||||
item.data = res
|
||||
item.expire = Date.now() + 1000 * 30
|
||||
Storage.set(licenseInfoKey, item)
|
||||
resolve(item.data)
|
||||
}).catch(err => {
|
||||
resolve(err)
|
||||
})
|
||||
})
|
||||
return new Promise((resolve, reject) => {
|
||||
httpGet("/api/config/license")
|
||||
.then((res) => {
|
||||
item.data = res;
|
||||
item.expire = Date.now() + 1000 * 30;
|
||||
Storage.set(licenseInfoKey, item);
|
||||
resolve(item.data);
|
||||
})
|
||||
.catch((err) => {
|
||||
resolve(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function getClientId() {
|
||||
let clientId = Storage.get('client_id')
|
||||
if (clientId) {
|
||||
return clientId
|
||||
}
|
||||
clientId = randString(42)
|
||||
Storage.set('client_id', clientId)
|
||||
return clientId
|
||||
}
|
||||
let clientId = Storage.get("client_id");
|
||||
if (clientId) {
|
||||
return clientId;
|
||||
}
|
||||
clientId = randString(42);
|
||||
Storage.set("client_id", clientId);
|
||||
return clientId;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -213,6 +213,10 @@ export function processContent(content) {
|
||||
return "";
|
||||
});
|
||||
}
|
||||
|
||||
// 支持 \[ 公式标签
|
||||
content = content.replace(/\\\[/g, "$$").replace(/\\\]/g, "$$");
|
||||
content = content.replace(/\\\(\\boxed\{(\d+)\}\\\)/g, '<span class="boxed">$1</span>');
|
||||
return content;
|
||||
}
|
||||
|
||||
@@ -240,7 +244,7 @@ export function showLoginDialog(router) {
|
||||
|
||||
export const replaceImg = (img) => {
|
||||
if (!img.startsWith("http")) {
|
||||
img = `${location.protocol}//${location.host}/${img}`;
|
||||
img = `${location.protocol}//${location.host}${img}`;
|
||||
}
|
||||
const devHost = process.env.VUE_APP_API_HOST;
|
||||
const localhost = "http://localhost:5678";
|
||||
|
||||
@@ -370,7 +370,6 @@ httpGet("/api/function/list")
|
||||
showMessageError("获取工具函数失败:" + e.message);
|
||||
});
|
||||
|
||||
// 创建 socket 连接
|
||||
const prompt = ref("");
|
||||
const showStopGenerate = ref(false); // 停止生成
|
||||
const lineBuffer = ref(""); // 输出缓冲行
|
||||
@@ -465,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) {
|
||||
@@ -752,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",
|
||||
|
||||
@@ -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>
|
||||
@@ -66,13 +85,20 @@
|
||||
:autosize="{ minRows: 4, maxRows: 6 }"
|
||||
type="textarea"
|
||||
ref="promptRef"
|
||||
maxlength="2000"
|
||||
placeholder="请在此输入绘画提示词,您也可以点击下面的提示词助手生成绘画提示词"
|
||||
v-loading="isGenerating"
|
||||
/>
|
||||
</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>
|
||||
@@ -96,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>
|
||||
@@ -213,7 +276,7 @@
|
||||
<el-image-viewer
|
||||
@close="
|
||||
() => {
|
||||
previewURL = '';
|
||||
previewURL = ''
|
||||
}
|
||||
"
|
||||
v-if="previewURL !== ''"
|
||||
@@ -223,270 +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, getClientId, 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({
|
||||
client_id: getClientId(),
|
||||
quality: "standard",
|
||||
size: "1024x1024",
|
||||
style: "vivid",
|
||||
prompt: "",
|
||||
});
|
||||
listBoxHeight.value = window.innerHeight - 58
|
||||
}
|
||||
|
||||
const finishedJobs = ref([]);
|
||||
const runningJobs = ref([]);
|
||||
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);
|
||||
});
|
||||
|
||||
store.addMessageHandler("dall", (data) => {
|
||||
// 丢弃无关消息
|
||||
if (data.channel !== "dall" || data.clientId !== getClientId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.body === "FINISH" || data.body === "FAIL") {
|
||||
page.value = 0;
|
||||
isOver.value = false;
|
||||
fetchFinishJobs();
|
||||
}
|
||||
nextTick(() => fetchRunningJobs());
|
||||
});
|
||||
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();
|
||||
store.removeMessageHandler("dall");
|
||||
});
|
||||
clipboard.value.destroy()
|
||||
if (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;
|
||||
power.value = user['power']
|
||||
userId.value = user.id
|
||||
isLogin.value = true
|
||||
page.value = 0
|
||||
fetchRunningJobs()
|
||||
fetchFinishJobs()
|
||||
|
||||
page.value = 0;
|
||||
fetchRunningJobs();
|
||||
fetchFinishJobs();
|
||||
// 轮询运行中任务
|
||||
tastPullHandler.value = setInterval(() => {
|
||||
if (allowPulling.value) {
|
||||
fetchRunningJobs()
|
||||
}
|
||||
}, 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) => {
|
||||
runningJobs.value = res.data.items;
|
||||
// 如果任务有更新,则更新已完成任务列表
|
||||
if (res.data.items && res.data.items.length !== runningJobs.value.length) {
|
||||
page.value = 0
|
||||
fetchFinishJobs()
|
||||
}
|
||||
if (res.data.items.length > 0) {
|
||||
runningJobs.value = res.data.items
|
||||
} else {
|
||||
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;
|
||||
fetchRunningJobs();
|
||||
ElMessage.success('任务执行成功!')
|
||||
power.value -= dallPower.value
|
||||
// 追加任务列表
|
||||
runningJobs.value.push({
|
||||
prompt: params.value.prompt,
|
||||
progress: 0,
|
||||
})
|
||||
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>
|
||||
|
||||
@@ -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
@@ -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>
|
||||
@@ -194,13 +254,20 @@
|
||||
:autosize="{ minRows: 4, maxRows: 6 }"
|
||||
type="textarea"
|
||||
ref="promptRef"
|
||||
maxlength="2000"
|
||||
placeholder="请在此输入绘画提示词,您也可以点击下面的提示词助手生成绘画提示词"
|
||||
v-loading="isGenerating"
|
||||
/>
|
||||
</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>
|
||||
@@ -215,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">
|
||||
@@ -242,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>
|
||||
@@ -311,50 +460,64 @@
|
||||
</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, getClientId, 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({
|
||||
client_id: getClientId(),
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
sampler: samplers[0],
|
||||
@@ -367,221 +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 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);
|
||||
});
|
||||
|
||||
store.addMessageHandler("sd", (data) => {
|
||||
// 丢弃无关消息
|
||||
if (data.channel !== "sd" || data.clientId !== getClientId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.body === "FINISH" || data.body === "FAIL") {
|
||||
page.value = 0;
|
||||
isOver.value = false;
|
||||
fetchFinishJobs();
|
||||
}
|
||||
nextTick(() => fetchRunningJobs());
|
||||
});
|
||||
});
|
||||
ElMessage.error('获取系统配置失败:' + e.message)
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clipboard.value.destroy();
|
||||
store.removeMessageHandler("sd");
|
||||
});
|
||||
clipboard.value.destroy()
|
||||
if (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()
|
||||
}
|
||||
}, 5000)
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
const fetchRunningJobs = () => {
|
||||
if (!isLogin.value) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
// 获取运行中的任务
|
||||
httpGet(`/api/sd/jobs?finish=0`)
|
||||
.then((res) => {
|
||||
runningJobs.value = res.data.items;
|
||||
if (runningJobs.value.length !== res.data.items.length) {
|
||||
page.value = 0
|
||||
fetchFinishJobs()
|
||||
}
|
||||
if (runningJobs.value.length === 0) {
|
||||
allowPulling.value = false
|
||||
}
|
||||
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;
|
||||
fetchRunningJobs();
|
||||
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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -8,18 +8,26 @@
|
||||
<img :src="logo" class="logo" alt="Geek-AI" />
|
||||
</div>
|
||||
<div class="menu-item">
|
||||
<span v-if="!license.de_copy">
|
||||
<span v-if="!license?.de_copy">
|
||||
<el-tooltip class="box-item" content="部署文档" placement="bottom">
|
||||
<a :href="docsURL" class="link-button mr-3" target="_blank">
|
||||
<i class="iconfont icon-book"></i>
|
||||
</a>
|
||||
</el-tooltip>
|
||||
<el-tooltip class="box-item" content="Github 源码" placement="bottom">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
content="Github 源码"
|
||||
placement="bottom"
|
||||
>
|
||||
<a :href="githubURL" class="link-button mr-3" target="_blank">
|
||||
<i class="iconfont icon-github"></i>
|
||||
</a>
|
||||
</el-tooltip>
|
||||
<el-tooltip class="box-item" content="Gitee 源码" placement="bottom">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
content="Gitee 源码"
|
||||
placement="bottom"
|
||||
>
|
||||
<a :href="giteeURL" class="link-button" target="_blank">
|
||||
<i class="iconfont icon-gitee"></i>
|
||||
</a>
|
||||
@@ -27,7 +35,12 @@
|
||||
</span>
|
||||
|
||||
<span v-if="!isLogin">
|
||||
<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
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</el-menu>
|
||||
@@ -38,14 +51,23 @@
|
||||
{{ title }}
|
||||
</h1>
|
||||
<div class="msg-text cursor-ani">
|
||||
<span v-for="(char, index) in displayedChars" :key="index" :style="{ color: rainbowColor(index) }">
|
||||
<span
|
||||
v-for="(char, index) in displayedChars"
|
||||
:key="index"
|
||||
:style="{ color: rainbowColor(index) }"
|
||||
>
|
||||
{{ char }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="navs animate__animated animate__backInDown">
|
||||
<el-space wrap :size="14">
|
||||
<div v-for="item in navs" :key="item.url" class="nav-item-box" @click="router.push(item.url)">
|
||||
<div
|
||||
v-for="item in navs"
|
||||
:key="item.url"
|
||||
class="nav-item-box"
|
||||
@click="router.push(item.url)"
|
||||
>
|
||||
<i :class="'iconfont ' + iconMap[item.url]"></i>
|
||||
<div>{{ item.name }}</div>
|
||||
</div>
|
||||
@@ -58,14 +80,14 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, ref} from "vue";
|
||||
import {useRouter} from "vue-router";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import FooterBar from "@/components/FooterBar.vue";
|
||||
import ThemeChange from "@/components/ThemeChange.vue";
|
||||
import {httpGet} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {checkSession, getLicenseInfo, getSystemInfo} from "@/store/cache";
|
||||
import {isMobile} from "@/utils/libs";
|
||||
import { httpGet } from "@/utils/http";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { checkSession, getLicenseInfo, getSystemInfo } from "@/store/cache";
|
||||
import { isMobile } from "@/utils/libs";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
@@ -95,7 +117,7 @@ const iconMap = ref({
|
||||
"/apps": "icon-app",
|
||||
"/member": "icon-vip-user",
|
||||
"/invite": "icon-share",
|
||||
"/luma": "icon-luma",
|
||||
"/luma": "icon-luma"
|
||||
});
|
||||
|
||||
const displayedChars = ref([]);
|
||||
@@ -163,7 +185,7 @@ const setContent = () => {
|
||||
const rainbowColor = (index) => {
|
||||
const hue = (index * 40) % 360; // 每个字符间隔40度,形成彩虹色
|
||||
return `hsl(${hue}, 90%, 50%)`; // 色调(hue),饱和度(70%),亮度(50%)
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
|
||||
662
web/src/views/KeLing.vue
Normal file
662
web/src/views/KeLing.vue
Normal file
@@ -0,0 +1,662 @@
|
||||
<template>
|
||||
<div class="page-keling">
|
||||
<div class="inner custom-scroll">
|
||||
<!-- 左侧参数设置面板 -->
|
||||
<el-scrollbar max-height="100vh">
|
||||
<div class="mj-box">
|
||||
<h2>视频参数设置</h2>
|
||||
<el-form :model="params" label-width="80px" label-position="left">
|
||||
<!-- 画面比例 -->
|
||||
<div class="param-line">
|
||||
<div class="param-line pt">
|
||||
<span>画面比例:</span>
|
||||
<el-tooltip content="生成画面的尺寸比例" placement="right">
|
||||
<el-icon>
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="param-line pt">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="8" v-for="item in rates" :key="item.value">
|
||||
<div
|
||||
class="flex-col items-center"
|
||||
:class="item.value === params.aspect_ratio ? 'grid-content active' : 'grid-content'"
|
||||
@click="changeRate(item)"
|
||||
>
|
||||
<el-image class="icon proportion" :src="item.img" fit="cover"></el-image>
|
||||
<div class="texts">{{ item.text }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 模型选择 -->
|
||||
<div class="param-line">
|
||||
<el-form-item label="模型选择">
|
||||
<el-select v-model="params.model" placeholder="请选择模型" @change="updateModelPower">
|
||||
<el-option v-for="item in models" :key="item.value" :label="item.text" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 视频时长 -->
|
||||
<div class="param-line">
|
||||
<el-form-item label="视频时长">
|
||||
<el-select v-model="params.duration" placeholder="请选择时长" @change="updateModelPower">
|
||||
<el-option label="5秒" value="5" />
|
||||
<el-option label="10秒" value="10" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 生成模式 -->
|
||||
<div class="param-line">
|
||||
<el-form-item label="生成模式">
|
||||
<el-select v-model="params.mode" placeholder="请选择模式" @change="updateModelPower">
|
||||
<el-option label="标准模式" value="std" />
|
||||
<el-option label="专业模式" value="pro" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 创意程度 -->
|
||||
<div class="param-line">
|
||||
<el-form-item label="创意程度">
|
||||
<el-slider v-model="params.cfg_scale" :min="0" :max="1" :step="0.1" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 运镜控制 -->
|
||||
<div class="param-line" v-if="showCameraControl">
|
||||
<div class="param-line pt">
|
||||
<span>运镜控制:</span>
|
||||
<el-tooltip content="生成画面的运镜效果,仅 1.5的高级模式可用" placement="right">
|
||||
<el-icon>
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<!-- 添加运镜类型选择 -->
|
||||
<div class="param-line">
|
||||
<el-select v-model="params.camera_control.type" placeholder="请选择运镜类型">
|
||||
<el-option label="请选择" value="" />
|
||||
<el-option label="简单运镜" value="simple" />
|
||||
<el-option label="下移拉远" value="down_back" />
|
||||
<el-option label="推进上移" value="forward_up" />
|
||||
<el-option label="右旋推进" value="right_turn_forward" />
|
||||
<el-option label="左旋推进" value="left_turn_forward" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<!-- 仅在simple模式下显示详细配置 -->
|
||||
<div class="camera-control mt-2" v-if="params.camera_control.type === 'simple'">
|
||||
<el-form-item label="水平移动">
|
||||
<el-slider v-model="params.camera_control.config.horizontal" :min="-10" :max="10" />
|
||||
</el-form-item>
|
||||
<el-form-item label="垂直移动">
|
||||
<el-slider v-model="params.camera_control.config.vertical" :min="-10" :max="10" />
|
||||
</el-form-item>
|
||||
<el-form-item label="左右旋转">
|
||||
<el-slider v-model="params.camera_control.config.pan" :min="-10" :max="10" />
|
||||
</el-form-item>
|
||||
<el-form-item label="上下旋转">
|
||||
<el-slider v-model="params.camera_control.config.tilt" :min="-10" :max="10" />
|
||||
</el-form-item>
|
||||
<el-form-item label="横向翻转">
|
||||
<el-slider v-model="params.camera_control.config.roll" :min="-10" :max="10" />
|
||||
</el-form-item>
|
||||
<el-form-item label="镜头缩放">
|
||||
<el-slider v-model="params.camera_control.config.zoom" :min="-10" :max="10" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
|
||||
<!-- 右侧主内容区 -->
|
||||
<div class="main-content task-list-inner">
|
||||
<!-- 任务类型选择 -->
|
||||
<div class="param-line">
|
||||
<el-tabs v-model="params.task_type" @tab-change="tabChange" class="title-tabs">
|
||||
<el-tab-pane label="文生视频" name="text2video">
|
||||
<div class="text">使用文字描述想要生成视频的内容</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="图生视频" name="image2video">
|
||||
<div class="text">以某张图片为底稿参考来创作视频,生成类似风格或类型视频,支持 PNG /JPG/JPEG 格式图片;</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<!-- 生成操作区 -->
|
||||
<div class="generation-area">
|
||||
<div v-if="params.task_type === 'text2video'" class="text2video">
|
||||
<el-input
|
||||
v-model="params.prompt"
|
||||
type="textarea"
|
||||
maxlength="500"
|
||||
:autosize="{ minRows: 4, maxRows: 6 }"
|
||||
placeholder="请在此输入视频提示词,您也可以点击下面的提示词助手生成视频提示词"
|
||||
/>
|
||||
<el-row class="text-info">
|
||||
<el-button class="generate-btn" @click="generatePrompt" :loading="isGenerating" size="small" color="#5865f2">
|
||||
<i class="iconfont icon-chuangzuo"></i>
|
||||
生成专业视频提示词
|
||||
</el-button>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<div v-else class="image2video">
|
||||
<div class="image-upload img-inline">
|
||||
<div class="upload-box img-uploader video-img-box">
|
||||
<el-icon v-if="params.image" @click="removeImage('start')" class="removeimg"><CircleCloseFilled /></el-icon>
|
||||
|
||||
<h4>起始帧</h4>
|
||||
<el-upload class="uploader img-uploader" :auto-upload="true" :show-file-list="false" :http-request="uploadStartImage" accept=".jpg,.png,.jpeg">
|
||||
<img v-if="params.image" :src="params.image" class="preview" />
|
||||
|
||||
<el-icon v-else class="upload-icon"><Plus /></el-icon>
|
||||
</el-upload>
|
||||
</div>
|
||||
<div class="btn-swap" v-if="params.image && params.image_tail">
|
||||
<i class="iconfont icon-exchange" @click="switchReverse"></i>
|
||||
</div>
|
||||
<div class="upload-box img-uploader video-img-box">
|
||||
<el-icon v-if="params.image_tail" @click="removeImage('end')" class="removeimg"><CircleCloseFilled /></el-icon>
|
||||
<h4>结束帧</h4>
|
||||
<el-upload class="uploader" :auto-upload="true" :show-file-list="false" :http-request="uploadEndImage" accept=".jpg,.png,.jpeg">
|
||||
<img v-if="params.image_tail" :src="params.image_tail" class="preview" />
|
||||
<el-icon v-else class="upload-icon"><Plus /></el-icon>
|
||||
</el-upload>
|
||||
</div>
|
||||
</div>
|
||||
<div class="param-line pt">
|
||||
<div class="flex-row justify-between items-center">
|
||||
<div class="flex-row justify-start items-center">
|
||||
<span>提示词:</span>
|
||||
<el-tooltip content="输入你想要的内容,用逗号分割" placement="right">
|
||||
<el-icon>
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="param-line pt">
|
||||
<el-input v-model="params.prompt" type="textarea" :autosize="{ minRows: 4, maxRows: 6 }" placeholder="描述视频画面细节" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 排除内容 -->
|
||||
<div class="param-line pt">
|
||||
<div class="flex-row justify-between items-center">
|
||||
<div class="flex-row justify-start items-center">
|
||||
<span>不希望出现的内容:(可选)</span>
|
||||
<el-tooltip content="不想出现在图片上的元素(例如:树,建筑)" placement="right">
|
||||
<el-icon>
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="param-line pt">
|
||||
<el-input
|
||||
v-model="params.negative_prompt"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 4, maxRows: 6 }"
|
||||
placeholder="请在此输入你不希望出现在视频上的内容"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 算力显示 -->
|
||||
<el-row class="text-info">
|
||||
<el-text type="primary"
|
||||
>每次生成视频消耗 <el-text type="warning">{{ powerCost }}算力;</el-text> </el-text
|
||||
>
|
||||
<el-text type="primary"
|
||||
>当前可用算力:<el-text type="warning">{{ availablePower }}</el-text></el-text
|
||||
>
|
||||
</el-row>
|
||||
|
||||
<!-- 生成按钮 -->
|
||||
<div class="submit-btn">
|
||||
<el-button type="primary" :dark="false" @click="generate" round>立即生成</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 任务列表区域 -->
|
||||
<div class="video-list">
|
||||
<h2 class="text-xl p-3">你的作品</h2>
|
||||
|
||||
<div class="list-box" v-if="!noData">
|
||||
<div v-for="item in list" :key="item.id">
|
||||
<div class="item">
|
||||
<div class="left">
|
||||
<div class="container">
|
||||
<div v-if="item.progress === 100">
|
||||
<video class="video" :src="item.video_url" preload="auto" loop="loop" muted="muted">您的浏览器不支持视频播放</video>
|
||||
<button class="play flex justify-center items-center" @click="previewVideo(item)">
|
||||
<img src="/images/play.svg" alt="" />
|
||||
</button>
|
||||
</div>
|
||||
<el-image v-else-if="item.progress === 101" :src="item.cover_url" fit="cover" />
|
||||
<generating message="正在生成视频" v-else />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="center">
|
||||
<div class="pb-2">
|
||||
<el-tag class="mr-1">{{ item.raw_data.task_type }}</el-tag>
|
||||
<el-tag class="mr-1">{{ item.raw_data.model }}</el-tag>
|
||||
<el-tag class="mr-1">{{ item.raw_data.duration }}秒</el-tag>
|
||||
<el-tag class="mr-1">{{ item.raw_data.mode }}</el-tag>
|
||||
</div>
|
||||
<div class="failed" v-if="item.progress === 101">任务执行失败:{{ item.err_msg }},任务提示词:{{ item.prompt }}</div>
|
||||
<div class="prompt" v-else>
|
||||
{{ substr(item.prompt, 1000) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right" v-if="item.progress === 100">
|
||||
<div class="tools">
|
||||
<el-tooltip content="复制提示词" placement="top">
|
||||
<button class="btn btn-icon copy-prompt" :data-clipboard-text="item.prompt">
|
||||
<i class="iconfont icon-copy"></i>
|
||||
</button>
|
||||
</el-tooltip>
|
||||
|
||||
<!-- <button class="btn btn-publish">
|
||||
<span class="text">发布</span>
|
||||
<black-switch v-model:value="item.publish" @change="publishJob(item)" size="small" />
|
||||
</button> -->
|
||||
|
||||
<el-tooltip content="下载视频" placement="top">
|
||||
<button class="btn btn-icon" @click="downloadVideo(item)" :disabled="item.downloading">
|
||||
<i class="iconfont icon-download" v-if="!item.downloading"></i>
|
||||
<el-image src="/images/loading.gif" class="downloading" fit="cover" v-else />
|
||||
</button>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<button class="btn btn-icon" @click="removeJob(item)">
|
||||
<i class="iconfont icon-remove"></i>
|
||||
</button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right-error" v-else>
|
||||
<el-button type="danger" @click="removeJob(item)" circle>
|
||||
<i class="iconfont icon-remove"></i>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty :image-size="100" :image="nodata" description="没有任何作品,赶紧去创作吧!" v-else />
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-if="total > pageSize"
|
||||
background
|
||||
style="--el-pagination-button-bg-color: rgba(86, 86, 95, 0.2)"
|
||||
layout="total,prev, pager, next"
|
||||
:hide-on-single-page="true"
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
@current-change="fetchData"
|
||||
:total="total"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视频预览对话框 -->
|
||||
<black-dialog v-model:show="previewVisible" title="视频预览" hide-footer @cancal="previewVisible = false" width="auto">
|
||||
<video
|
||||
v-if="currentVideo"
|
||||
:src="currentVideo"
|
||||
controls
|
||||
style="max-width: 90vw; max-height: 90vh"
|
||||
:autoplay="true"
|
||||
loop="loop"
|
||||
muted="muted"
|
||||
preload="auto"
|
||||
>
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
</black-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import failed from "@/assets/img/failed.png";
|
||||
import TaskList from "@/components/TaskList.vue";
|
||||
import { ref, reactive, onMounted, onUnmounted, watch } from "vue";
|
||||
import { Plus, Delete, InfoFilled, ChromeFilled, DocumentCopy, Download, WarnTriangleFilled, CircleCloseFilled } from "@element-plus/icons-vue";
|
||||
import { httpGet, httpPost, httpDownload } from "@/utils/http";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { checkSession, getSystemInfo } from "@/store/cache";
|
||||
import Clipboard from "clipboard";
|
||||
import BlackDialog from "@/components/ui/BlackDialog.vue";
|
||||
import BlackSwitch from "@/components/ui/BlackSwitch.vue";
|
||||
import { closeLoading, showLoading, showMessageError, showMessageOK } from "@/utils/dialog";
|
||||
import { replaceImg, substr } from "@/utils/libs";
|
||||
import Generating from "@/components/ui/Generating.vue";
|
||||
import nodata from "@/assets/img/no-data.png";
|
||||
|
||||
const models = ref([
|
||||
{
|
||||
text: "可灵 1.6",
|
||||
value: "kling-v1-6",
|
||||
},
|
||||
{
|
||||
text: "可灵 1.5",
|
||||
value: "kling-v1-5",
|
||||
},
|
||||
{
|
||||
text: "可灵 1.0",
|
||||
value: "kling-v1",
|
||||
},
|
||||
]);
|
||||
// 参数设置
|
||||
const params = reactive({
|
||||
task_type: "text2video",
|
||||
model: models.value[0].value,
|
||||
prompt: "",
|
||||
negative_prompt: "",
|
||||
cfg_scale: 0.7,
|
||||
mode: "std",
|
||||
aspect_ratio: "16:9",
|
||||
duration: "5",
|
||||
camera_control: {
|
||||
type: "",
|
||||
config: {
|
||||
horizontal: 0,
|
||||
vertical: 0,
|
||||
pan: 0,
|
||||
tilt: 0,
|
||||
roll: 0,
|
||||
zoom: 0,
|
||||
},
|
||||
},
|
||||
image: "",
|
||||
image_tail: "",
|
||||
});
|
||||
const rates = [
|
||||
{ css: "square", value: "1:1", text: "1:1", img: "/images/mj/rate_1_1.png" },
|
||||
|
||||
{
|
||||
css: "size16-9",
|
||||
value: "16:9",
|
||||
text: "16:9",
|
||||
img: "/images/mj/rate_16_9.png",
|
||||
},
|
||||
{
|
||||
css: "size9-16",
|
||||
value: "9:16",
|
||||
text: "9:16",
|
||||
img: "/images/mj/rate_9_16.png",
|
||||
},
|
||||
];
|
||||
|
||||
// 切换图片比例
|
||||
const changeRate = (item) => {
|
||||
params.aspect_ratio = item.value;
|
||||
};
|
||||
|
||||
const generating = ref(false);
|
||||
const isGenerating = ref(false);
|
||||
const powerCost = ref(10);
|
||||
const availablePower = ref(100);
|
||||
const taskFilter = ref("all");
|
||||
const loading = ref(false);
|
||||
const list = ref([]);
|
||||
const noData = ref(true);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const total = ref(0);
|
||||
const taskPulling = ref(true);
|
||||
const pullHandler = ref(null);
|
||||
const previewVisible = ref(false);
|
||||
const currentVideo = ref("");
|
||||
const showCameraControl = ref(false);
|
||||
const keLingPowers = ref({});
|
||||
const isLogin = ref(false);
|
||||
// 动态更新模型消耗的算力
|
||||
const updateModelPower = () => {
|
||||
showCameraControl.value = params.model === "kling-v1-5" && params.mode === "pro";
|
||||
powerCost.value = keLingPowers.value[`${params.model}_${params.mode}_${params.duration}`] || {};
|
||||
};
|
||||
|
||||
// tab切换
|
||||
const tabChange = (tab) => {
|
||||
params.task_type = tab;
|
||||
};
|
||||
|
||||
const uploadStartImage = async (file) => {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file.file);
|
||||
try {
|
||||
showLoading("图片上传中...");
|
||||
const res = await httpPost("/api/upload", formData);
|
||||
params.image = res.data.url;
|
||||
ElMessage.success("上传成功");
|
||||
closeLoading();
|
||||
} catch (e) {
|
||||
showMessageError("上传失败: " + e.message);
|
||||
closeLoading();
|
||||
}
|
||||
};
|
||||
|
||||
//移除图片
|
||||
const removeImage = (type) => {
|
||||
if (type === "start") {
|
||||
params.image = "";
|
||||
} else if (type === "end") {
|
||||
params.image_tail = "";
|
||||
}
|
||||
};
|
||||
|
||||
//图片交换方法
|
||||
const switchReverse = () => {
|
||||
[params.image, params.image_tail] = [params.image_tail, params.image];
|
||||
};
|
||||
const uploadEndImage = async (file) => {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file.file);
|
||||
try {
|
||||
const res = await httpPost("/api/upload", formData);
|
||||
params.image_tail = res.data.url;
|
||||
ElMessage.success("上传成功");
|
||||
} catch (e) {
|
||||
showMessageError("上传失败: " + e.message);
|
||||
}
|
||||
};
|
||||
|
||||
const generatePrompt = async () => {
|
||||
if (isGenerating.value) return;
|
||||
if (!params.prompt) {
|
||||
return showMessageError("请输入视频描述");
|
||||
}
|
||||
isGenerating.value = true;
|
||||
try {
|
||||
const res = await httpPost("/api/prompt/video", { prompt: params.prompt });
|
||||
params.prompt = res.data;
|
||||
} catch (e) {
|
||||
showMessageError("生成失败: " + e.message);
|
||||
} finally {
|
||||
isGenerating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const generate = async () => {
|
||||
//增加防抖
|
||||
if (generating.value) return;
|
||||
if (!params.prompt?.trim()) {
|
||||
return ElMessage.error("请输入视频描述");
|
||||
}
|
||||
// 提示词长度不能超过 500
|
||||
if (params.prompt.length > 500) {
|
||||
return ElMessage.error("视频描述不能超过 500 个字符");
|
||||
}
|
||||
if (params.task_type === "image2video" && !params.image) {
|
||||
return ElMessage.error("请上传起始帧图片");
|
||||
}
|
||||
generating.value = true;
|
||||
// 处理图片链接
|
||||
if (params.image) {
|
||||
params.image = replaceImg(params.image);
|
||||
}
|
||||
if (params.image_tail) {
|
||||
params.image_tail = replaceImg(params.image_tail);
|
||||
}
|
||||
try {
|
||||
await httpPost("/api/video/keling/create", params);
|
||||
showMessageOK("任务创建成功");
|
||||
// 新增重置
|
||||
page.value = 1;
|
||||
list.value.unshift({
|
||||
progress: 0,
|
||||
prompt: params.prompt,
|
||||
raw_data: {
|
||||
task_type: params.task_type,
|
||||
model: params.model,
|
||||
duration: params.duration,
|
||||
mode: params.mode,
|
||||
},
|
||||
});
|
||||
taskPulling.value = true;
|
||||
} catch (e) {
|
||||
showMessageError("创建失败: " + e.message);
|
||||
} finally {
|
||||
generating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchData = (_page) => {
|
||||
if (_page) {
|
||||
page.value = _page;
|
||||
}
|
||||
|
||||
httpGet("/api/video/list", {
|
||||
page: page.value,
|
||||
page_size: pageSize.value,
|
||||
type: "keling",
|
||||
task_type: taskFilter.value === "all" ? "" : taskFilter.value,
|
||||
})
|
||||
.then((res) => {
|
||||
total.value = res.data.total;
|
||||
let needPull = false;
|
||||
const items = [];
|
||||
for (let v of res.data.items) {
|
||||
if (v.progress === 0 || v.progress === 102) {
|
||||
needPull = true;
|
||||
}
|
||||
items.push({
|
||||
...v,
|
||||
downloading: false,
|
||||
});
|
||||
}
|
||||
loading.value = false;
|
||||
taskPulling.value = needPull;
|
||||
if (JSON.stringify(list.value) !== JSON.stringify(items)) {
|
||||
list.value = items;
|
||||
}
|
||||
noData.value = list.value.length === 0;
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
noData.value = true;
|
||||
});
|
||||
};
|
||||
|
||||
const previewVideo = (task) => {
|
||||
currentVideo.value = task.video_url;
|
||||
previewVisible.value = true;
|
||||
};
|
||||
|
||||
const downloadVideo = async (task) => {
|
||||
try {
|
||||
const res = await httpDownload(`/api/download?url=${replaceImg(task.video_url)}`);
|
||||
const blob = new Blob([res.data]);
|
||||
const link = document.createElement("a");
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = `video_${task.id}.mp4`;
|
||||
link.click();
|
||||
URL.revokeObjectURL(link.href);
|
||||
} catch (e) {
|
||||
showMessageError("下载失败: " + e.message);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除任务
|
||||
const removeJob = (item) => {
|
||||
ElMessageBox.confirm("此操作将会删除任务相关文件,继续操作码?", "删除提示", {
|
||||
confirmButtonText: "确认",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
})
|
||||
.then(() => {
|
||||
httpGet("/api/video/remove", { id: item.id })
|
||||
.then(() => {
|
||||
ElMessage.success("任务删除成功");
|
||||
fetchData(page.value);
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("任务删除失败:" + e.message);
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
const clipboard = ref(null);
|
||||
// 生命周期钩子
|
||||
onMounted(() => {
|
||||
checkSession()
|
||||
.then((u) => {
|
||||
isLogin.value = true;
|
||||
availablePower.value = u.power;
|
||||
fetchData(1);
|
||||
// 设置轮询
|
||||
pullHandler.value = setInterval(() => {
|
||||
if (taskPulling.value) {
|
||||
fetchData(page.value);
|
||||
}
|
||||
}, 5000);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
});
|
||||
|
||||
clipboard.value = new Clipboard(".copy-prompt");
|
||||
clipboard.value.on("success", () => {
|
||||
ElMessage.success("复制成功!");
|
||||
});
|
||||
clipboard.value.on("error", () => {
|
||||
ElMessage.error("复制失败!");
|
||||
});
|
||||
|
||||
getSystemInfo().then((res) => {
|
||||
keLingPowers.value = res.data.keling_powers;
|
||||
updateModelPower();
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
clipboard.value.destroy();
|
||||
if (pullHandler.value) {
|
||||
clearInterval(pullHandler.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import "@/assets/css/keling.styl"
|
||||
</style>
|
||||
@@ -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,99 +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(() => {
|
||||
// 检查URL中是否存在token参数
|
||||
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(() => {
|
||||
if (isMobile()) {
|
||||
router.push("/mobile");
|
||||
} else {
|
||||
router.push("/chat");
|
||||
}
|
||||
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,
|
||||
@@ -138,18 +162,14 @@ const doLogin = (verifyData) => {
|
||||
x: verifyData.x,
|
||||
})
|
||||
.then((res) => {
|
||||
setUserToken(res.data.token);
|
||||
store.setIsLogin(true);
|
||||
if (isMobile()) {
|
||||
router.push("/mobile");
|
||||
} else {
|
||||
router.push("/chat");
|
||||
}
|
||||
setUserToken(res.data.token)
|
||||
store.setIsLogin(true)
|
||||
router.back()
|
||||
})
|
||||
.catch((e) => {
|
||||
showMessageError("登录失败," + e.message);
|
||||
});
|
||||
};
|
||||
showMessageError('登录失败,' + e.message)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<i class="iconfont icon-image"></i>
|
||||
</el-upload>
|
||||
</div>
|
||||
<textarea class="prompt-input" :rows="row" v-model="formData.prompt" placeholder="请输入提示词或者上传图片" autofocus> </textarea>
|
||||
<textarea class="prompt-input" :rows="row" v-model="formData.prompt" maxlength="2000" placeholder="请输入提示词或者上传图片" autofocus> </textarea>
|
||||
<div class="send-icon" @click="create">
|
||||
<i class="iconfont icon-send"></i>
|
||||
</div>
|
||||
@@ -58,7 +58,7 @@
|
||||
<img src="/images/play.svg" alt="" />
|
||||
</button>
|
||||
</div>
|
||||
<el-image :src="item.cover_url" fit="cover" v-else-if="item.progress > 100" />
|
||||
<el-image :src="item.cover_url" fit="cover" v-else-if="item.progress === 101" />
|
||||
<generating message="正在生成视频" v-else />
|
||||
</div>
|
||||
</div>
|
||||
@@ -68,10 +68,16 @@
|
||||
</div>
|
||||
<div class="right" v-if="item.progress === 100">
|
||||
<div class="tools">
|
||||
<button class="btn btn-publish">
|
||||
<!-- <button class="btn btn-publish">
|
||||
<span class="text">发布</span>
|
||||
<black-switch v-model:value="item.publish" @change="publishJob(item)" size="small" />
|
||||
</button>
|
||||
</button> -->
|
||||
|
||||
<el-tooltip content="复制提示词" placement="top">
|
||||
<button class="btn btn-icon copy-prompt" :data-clipboard-text="item.prompt">
|
||||
<i class="iconfont icon-copy"></i>
|
||||
</button>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip content="下载视频" placement="top">
|
||||
<button class="btn btn-icon" @click="download(item)" :disabled="item.downloading">
|
||||
@@ -111,7 +117,7 @@
|
||||
</div>
|
||||
</el-container>
|
||||
<black-dialog v-model:show="showDialog" title="预览视频" hide-footer @cancal="showDialog = false" width="auto">
|
||||
<video style="width: 100%; max-height: 90vh" :src="currentVideoUrl" preload="auto" :autoplay="true" loop="loop" muted="muted" v-show="showDialog">
|
||||
<video style="max-width: 90vw; max-height: 90vh" :src="currentVideoUrl" preload="auto" :autoplay="true" loop="loop" muted="muted" v-show="showDialog">
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
</black-dialog>
|
||||
@@ -124,22 +130,20 @@ import nodata from "@/assets/img/no-data.png";
|
||||
import { onMounted, onUnmounted, reactive, ref } from "vue";
|
||||
import { CircleCloseFilled } from "@element-plus/icons-vue";
|
||||
import { httpDownload, httpPost, httpGet } from "@/utils/http";
|
||||
import { checkSession, getClientId } from "@/store/cache";
|
||||
import { checkSession } from "@/store/cache";
|
||||
import { closeLoading, showLoading, showMessageError, showMessageOK } from "@/utils/dialog";
|
||||
import { replaceImg } from "@/utils/libs";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import BlackSwitch from "@/components/ui/BlackSwitch.vue";
|
||||
import Generating from "@/components/ui/Generating.vue";
|
||||
import BlackDialog from "@/components/ui/BlackDialog.vue";
|
||||
import { useSharedStore } from "@/store/sharedata";
|
||||
|
||||
import Clipboard from "clipboard";
|
||||
const showDialog = ref(false);
|
||||
const currentVideoUrl = ref("");
|
||||
const row = ref(1);
|
||||
const images = ref([]);
|
||||
|
||||
const formData = reactive({
|
||||
client_id: getClientId(),
|
||||
prompt: "",
|
||||
expand_prompt: false,
|
||||
loop: false,
|
||||
@@ -147,32 +151,43 @@ const formData = reactive({
|
||||
end_frame_img: "",
|
||||
});
|
||||
|
||||
const store = useSharedStore();
|
||||
const loading = ref(false);
|
||||
const list = ref([]);
|
||||
const noData = ref(true);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const total = ref(0);
|
||||
const taskPulling = ref(true);
|
||||
const clipboard = ref(null);
|
||||
const pullHandler = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
checkSession().then(() => {
|
||||
fetchData(1);
|
||||
// 设置轮询
|
||||
pullHandler.value = setInterval(() => {
|
||||
if (taskPulling.value) {
|
||||
fetchData(1);
|
||||
}
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
store.addMessageHandler("luma", (data) => {
|
||||
// 丢弃无关消息
|
||||
if (data.channel !== "luma" || data.clientId !== getClientId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.body === "FINISH" || data.body === "FAIL") {
|
||||
fetchData(1);
|
||||
}
|
||||
clipboard.value = new Clipboard(".copy-prompt");
|
||||
clipboard.value.on("success", () => {
|
||||
ElMessage.success("复制成功!");
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
store.removeMessageHandler("luma");
|
||||
clipboard.value.destroy();
|
||||
if (pullHandler.value) {
|
||||
clearInterval(pullHandler.value);
|
||||
}
|
||||
});
|
||||
|
||||
const download = (item) => {
|
||||
const url = replaceImg(item.video_url);
|
||||
const downloadURL = `${process.env.VUE_APP_API_HOST}/api/download?url=${url}`;
|
||||
// parse filename
|
||||
const urlObj = new URL(url);
|
||||
const fileName = urlObj.pathname.split("/").pop();
|
||||
item.downloading = true;
|
||||
@@ -231,14 +246,16 @@ const publishJob = (item) => {
|
||||
const upload = (file) => {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file.file, file.name);
|
||||
// 执行上传操作
|
||||
showLoading("正在上传文件...");
|
||||
httpPost("/api/upload", formData)
|
||||
.then((res) => {
|
||||
images.value.push(res.data.url);
|
||||
ElMessage.success({ message: "上传成功", duration: 500 });
|
||||
closeLoading();
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("图片上传失败:" + e.message);
|
||||
closeLoading();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -249,12 +266,7 @@ const remove = (img) => {
|
||||
const switchReverse = () => {
|
||||
images.value = images.value.reverse();
|
||||
};
|
||||
const loading = ref(false);
|
||||
const list = ref([]);
|
||||
const noData = ref(true);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const total = ref(0);
|
||||
|
||||
const fetchData = (_page) => {
|
||||
if (_page) {
|
||||
page.value = _page;
|
||||
@@ -266,8 +278,19 @@ const fetchData = (_page) => {
|
||||
})
|
||||
.then((res) => {
|
||||
total.value = res.data.total;
|
||||
let needPull = false;
|
||||
const items = [];
|
||||
for (let v of res.data.items) {
|
||||
if (v.progress === 0 || v.progress === 102) {
|
||||
needPull = true;
|
||||
}
|
||||
items.push(v);
|
||||
}
|
||||
loading.value = false;
|
||||
list.value = res.data.items;
|
||||
taskPulling.value = needPull;
|
||||
if (JSON.stringify(list.value) !== JSON.stringify(items)) {
|
||||
list.value = items;
|
||||
}
|
||||
noData.value = list.value.length === 0;
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -276,19 +299,19 @@ const fetchData = (_page) => {
|
||||
});
|
||||
};
|
||||
|
||||
// 创建视频
|
||||
const create = () => {
|
||||
const len = images.value.length;
|
||||
if (len) {
|
||||
formData.first_frame_img = images.value[0];
|
||||
formData.first_frame_img = replaceImg(images.value[0]);
|
||||
if (len === 2) {
|
||||
formData.end_frame_img = images.value[1];
|
||||
formData.end_frame_img = replaceImg(images.value[1]);
|
||||
}
|
||||
}
|
||||
|
||||
httpPost("/api/video/luma/create", formData)
|
||||
.then(() => {
|
||||
fetchData(1);
|
||||
taskPulling.value = true;
|
||||
showMessageOK("创建任务成功");
|
||||
})
|
||||
.catch((e) => {
|
||||
|
||||
@@ -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">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user