Compare commits
16 Commits
v3.1.4
...
image-wall
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7e815d2bb | ||
|
|
f58b0a65f0 | ||
|
|
b59ad521ca | ||
|
|
b47ff975b0 | ||
|
|
d043a87b30 | ||
|
|
4cae7525d9 | ||
|
|
76966d2ce7 | ||
|
|
5a740aecb0 | ||
|
|
1ae79331e7 | ||
|
|
8b14e141d0 | ||
|
|
9cbc6c91c4 | ||
|
|
21c3a419a5 | ||
|
|
287fac3a89 | ||
|
|
ba206bb387 | ||
|
|
4fc01f3f7b | ||
|
|
f5ed71bcc6 |
23
CHANGELOG.md
@@ -1,15 +1,28 @@
|
||||
# 更新日志
|
||||
|
||||
## v3.1.5
|
||||
|
||||
1. 功能新增:新增百度文心一言大模型 API 接入支持
|
||||
2. 功能新增:新增科大讯飞星火大模型 API 接入支持
|
||||
3. 功能重构:将 chat_handler 的所有功能实现放入单独的包中
|
||||
4. 功能新增:新增系统配置 `enabled_function` 用于启用和关闭函数功能
|
||||
5. Bug修复:修复管理后台更新 API Key 失败的 Bug
|
||||
6. Bug修复:修复新建的对话无法更新对话标题的 Bug
|
||||
7. 功能优化:其他一些小的体验优化工作
|
||||
|
||||
## v3.1.4
|
||||
|
||||
1. 功能新增:新增阿里云 OSS 图片上传实现,目前已支持本地存储,七牛云,Minio和阿里云 OSS 四种存储介质。
|
||||
2. 功能新增:**增加 Stable Diffusion 绘画功能页面**。
|
||||
3. 功能重构:将 [chatgpt-plus-exts](https://github.com/yangjian102621/chatgpt-plus-exts) 合并到本项目,部署更加简单,无需部署两个项目了。
|
||||
4. Bug修复:修复[用户注册报错BUG #37](https://github.com/yangjian102621/chatgpt-plus/issues/37)。
|
||||
5. Bug修复:修复 MidJourney API 接口升级导致图片文保存失败的 Bug。
|
||||
6. 功能优化:增加阿里云短信服务配置项 `Sign` 和 `CodeTempId` 用来配置自己的短信签名和短信验证码模版 ID。
|
||||
6. 功能优化:增加阿里云短信服务配置项 `Sign` 和 `CodeTempId` 用来配置自己的短信签名和短信验证码模版 ID。
|
||||
7. 功能优化:添加系统配置用来设置自定义的众筹微信收款二维码。
|
||||
8. 功能优化:优化绘画页面的弹窗样式和页面布局。
|
||||
|
||||
## v3.1.3
|
||||
|
||||
1. 页面重构:重后 Home 页面,拆分成聊天,MJ绘画,SD 绘画,应用广场等多个功能菜单。
|
||||
2. 功能新增:新增 MidJourney 专业绘画页面,开放更高级的 MJ 绘画姿势。
|
||||
3. 功能优化:采用队列的方式控制绘画任务并发,简化任务回调通知逻辑,给任务回调加锁。
|
||||
@@ -18,6 +31,7 @@
|
||||
6. Bug修复:修复 JWT token 有效期计算错误的 Bug。
|
||||
|
||||
## v3.1.2
|
||||
|
||||
1. 功能新增:新增七牛云 OSS 实现,目前已支持三种文件上传服务:Local, Minio, QiNiu OSS。
|
||||
2. 功能新增:新增桌面版,使用 electron 套壳网页版。
|
||||
3. Bug修复:自动去除众筹核销时候转账单号中的空格,防止复制的时候多复制了空格。
|
||||
@@ -26,17 +40,20 @@
|
||||
6. 功能优化:所有路由跳转都使用绝对路径
|
||||
|
||||
## v3.1.1
|
||||
|
||||
紧急修复版本,采用弹窗的方式显示验证码,解决验证码在低分辨率下被掩盖的Bug
|
||||
|
||||
## v3.1.0(大版本更新)
|
||||
1. 功能重构:将聊天模型独立拆分,以便支持多平台模型,目前已经内置支持 OPenAI,Azure 以及 ChatGLM,用户可以在这两个平台的模型中随意切换,体验不同的模型聊天。
|
||||
|
||||
1. 功能重构:将聊天模型独立拆分,以便支持多平台模型,目前已经内置支持 OPenAI,Azure 以及
|
||||
ChatGLM,用户可以在这两个平台的模型中随意切换,体验不同的模型聊天。
|
||||
2. 功能重构:重写系统 API 授权机制,使用 JWT 替换传统的 session 会话授权,使得 API 授权变得更加灵活。
|
||||
3. 功能重构:重构文件夹上传服务,支持多种文件上传存储handler,目前已经实现本地存储和 minio oss 存储。
|
||||
4. 功能优化:更新头像自动删除旧的图片资源。
|
||||
5. 功能优化:将应用日志在终端输出的同时存盘,方便 docker 部署查看日志。
|
||||
6. 功能新增:允许用户配置自己的 OPenAI,Azure 以及 ChatGLM API KEY。
|
||||
7. 功能优化:优化移动版的行为验证码样式,修复低分辨率显示器验证码被遮挡的 Bug
|
||||
8. 升级 gin, element-plus,redis 组件到最新版本。
|
||||
8. 升级 gin, element-plus,redis 组件到最新版本。
|
||||
9. Bug修复:修复若干已知的的 Bug
|
||||
|
||||
## v3.0.7
|
||||
|
||||
60
README.md
@@ -1,12 +1,12 @@
|
||||
# ChatGPT-Plus
|
||||
|
||||
**ChatGPT-PLUS** 基于 AI 大语言模型 API 实现的 AI 助手全套开源解决方案,自带运营管理后台,开箱即用。集成了 OpenAI, Azure,
|
||||
ChatGLM,讯飞星火,文心一言等多个平台的大语言模型。主要有如下特性:
|
||||
ChatGLM,讯飞星火,文心一言等多个平台的大语言模型。集成了 MidJourney 和 Stable Diffusion AI绘画功能。主要有如下特性:
|
||||
|
||||
* 完整的开源系统,前端应用和后台管理系统皆可开箱即用。
|
||||
* 聊天体验跟 ChatGPT 官方版本完全一致。
|
||||
* 内置了各种预训练好的角色,比如小红书写手,英语翻译大师,苏格拉底,孔子,乔布斯,周报助手等。轻松满足你的各种聊天和应用需求。
|
||||
* 支持 MidJourney AI 绘画集成,开箱即用。
|
||||
* 支持 MidJourney / Stable Diffusion AI 绘画集成,开箱即用。
|
||||
* 支持使用个人微信二维码作为充值收费的支付渠道,无需企业支付通道。(可定制开发其他支付通道支持)
|
||||
* 集成插件 API 功能,可结合 GPT 开发各种强大的插件,已内置实现了微博热搜,今日头条,今日早报和 AI 绘画函数插件。
|
||||
|
||||
@@ -30,9 +30,9 @@ ChatGLM,讯飞星火,文心一言等多个平台的大语言模型。主要有
|
||||
|
||||

|
||||
|
||||
### 用户设置
|
||||
### 绘图作品展
|
||||
|
||||

|
||||

|
||||
|
||||
### 登录页面
|
||||
|
||||
@@ -50,7 +50,8 @@ ChatGLM,讯飞星火,文心一言等多个平台的大语言模型。主要有
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
### 7. 体验地址
|
||||
|
||||
@@ -101,6 +102,10 @@ ChatGPT 的服务。
|
||||
* Github 地址:https://github.com/yangjian102621/chatgpt-plus
|
||||
* 码云地址:https://gitee.com/blackfox/chatgpt-plus
|
||||
|
||||
## 客户端下载
|
||||
|
||||
目前已经支持 Win/Linux/Mac/Android 客户端,下载地址为:https://github.com/yangjian102621/chatgpt-plus/releases/tag/v3.1.2
|
||||
|
||||
## TODOLIST
|
||||
|
||||
* [x] 整合 Midjourney AI 绘画 API
|
||||
@@ -128,7 +133,7 @@ cd docker/mysql
|
||||
# 创建 mysql 容器
|
||||
docker-compose up -d
|
||||
# 导入数据库
|
||||
docker exec -i chatgpt-plus-mysql sh -c 'exec mysql -uroot -p12345678' < ../../database/chatgpt_plus-v3.1.4.sql
|
||||
docker exec -i chatgpt-plus-mysql sh -c 'exec mysql -uroot -p12345678' < ../../database/chatgpt_plus-v3.1.5.sql
|
||||
```
|
||||
|
||||
如果你本地已经安装了 MySQL 服务,那么你只需手动导入数据库即可。
|
||||
@@ -216,8 +221,11 @@ WeChatBot = false # 是否启动微信机器人
|
||||
```
|
||||
|
||||
> 1. 如果你不知道如何获取 Discord 用户 Token 和 Bot Token
|
||||
请查参考 [Midjourney|如何集成到自己的平台](https://zhuanlan.zhihu.com/p/631079476)。
|
||||
> 2. `Txt2ImgJsonPath` 的默认用的是使用最广泛的 [stable-diffusion-webui](https://github.com/AUTOMATIC1111/stable-diffusion-webui) 项目的 API,如果你用的是其他版本,比如秋叶的懒人包部署的,那么请将对应的 text2img 的参数报文复制放在 `res/text2img.json` 文件中即可。
|
||||
请查参考 [Midjourney|如何集成到自己的平台](https://zhuanlan.zhihu.com/p/631079476)。
|
||||
> 2. `Txt2ImgJsonPath`
|
||||
的默认用的是使用最广泛的 [stable-diffusion-webui](https://github.com/AUTOMATIC1111/stable-diffusion-webui) 项目的
|
||||
API,如果你用的是其他版本,比如秋叶的懒人包部署的,那么请将对应的 text2img 的参数报文复制放在 `res/text2img.json`
|
||||
文件中即可。
|
||||
|
||||
修改 nginx 配置文档 `docker/conf/nginx/conf.d/chatgpt-plus.conf`,把后端转发的地址改成当前主机的内网 IP 地址。
|
||||
|
||||
@@ -244,6 +252,42 @@ location /static/ {
|
||||
|
||||
### 3. 启动应用
|
||||
|
||||
先修改 `docker/docker-compose.yaml` 文件中的镜像地址,改成最新的版本:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
services:
|
||||
# 后端 API 镜像
|
||||
chatgpt-plus-api:
|
||||
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-api:v3.1.5 #这里改成最新的 release 版本地址
|
||||
container_name: chatgpt-plus-api
|
||||
restart: always
|
||||
environment:
|
||||
- DEBUG=false
|
||||
- LOG_LEVEL=info
|
||||
- CONFIG_FILE=config.toml
|
||||
ports:
|
||||
- "5678:5678"
|
||||
volumes:
|
||||
- /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime
|
||||
- ./conf/config.toml:/var/www/app/config.toml
|
||||
- ./logs:/var/www/app/logs
|
||||
- ./static:/var/www/app/static
|
||||
|
||||
# 前端应用镜像
|
||||
chatgpt-plus-web:
|
||||
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-web:v3.1.5 #这里改成最新的 release 版本地址
|
||||
container_name: chatgpt-plus-web
|
||||
restart: always
|
||||
ports:
|
||||
- "8080:8080" # 这边是对外的端口,支持 8080,80和443
|
||||
volumes:
|
||||
- ./logs/nginx:/var/log/nginx
|
||||
- ./conf/nginx/conf.d:/etc/nginx/conf.d
|
||||
- ./conf/nginx/nginx.conf:/etc/nginx/nginx.conf
|
||||
- ./ssl:/etc/nginx/ssl
|
||||
```
|
||||
|
||||
```shell
|
||||
cd docker
|
||||
docker-compose up -d
|
||||
|
||||
@@ -2,9 +2,9 @@ package types
|
||||
|
||||
// ApiRequest API 请求实体
|
||||
type ApiRequest struct {
|
||||
Model string `json:"model"`
|
||||
Model string `json:"model,omitempty"` // 兼容百度文心一言
|
||||
Temperature float32 `json:"temperature"`
|
||||
MaxTokens int `json:"max_tokens"`
|
||||
MaxTokens int `json:"max_tokens,omitempty"` // 兼容百度文心一言
|
||||
Stream bool `json:"stream"`
|
||||
Messages []interface{} `json:"messages,omitempty"`
|
||||
Prompt []interface{} `json:"prompt,omitempty"` // 兼容 ChatGLM
|
||||
@@ -67,4 +67,10 @@ var ModelToTokens = map[string]int{
|
||||
"gpt-3.5-turbo-16k": 16384,
|
||||
"gpt-4": 8192,
|
||||
"gpt-4-32k": 32768,
|
||||
"chatglm_pro": 32768, // 清华智普
|
||||
"chatglm_std": 16384,
|
||||
"chatglm_lite": 4096,
|
||||
"ernie_bot_turbo": 8192, // 文心一言
|
||||
"general": 8192, // 科大讯飞
|
||||
"general2": 8192,
|
||||
}
|
||||
|
||||
@@ -36,6 +36,16 @@ func (wc *WsClient) Send(message []byte) error {
|
||||
return wc.Conn.WriteMessage(wc.mt, message)
|
||||
}
|
||||
|
||||
func (wc *WsClient) SendJson(value interface{}) error {
|
||||
wc.lock.Lock()
|
||||
defer wc.lock.Unlock()
|
||||
|
||||
if wc.Closed {
|
||||
return ErrConClosed
|
||||
}
|
||||
return wc.Conn.WriteJSON(value)
|
||||
}
|
||||
|
||||
func (wc *WsClient) Receive() (int, []byte, error) {
|
||||
if wc.Closed {
|
||||
return 0, nil, ErrConClosed
|
||||
|
||||
@@ -79,6 +79,8 @@ type ChatConfig struct {
|
||||
OpenAI ModelAPIConfig `json:"open_ai"`
|
||||
Azure ModelAPIConfig `json:"azure"`
|
||||
ChatGML ModelAPIConfig `json:"chat_gml"`
|
||||
Baidu ModelAPIConfig `json:"baidu"`
|
||||
XunFei ModelAPIConfig `json:"xun_fei"`
|
||||
|
||||
EnableContext bool `json:"enable_context"` // 是否开启聊天上下文
|
||||
EnableHistory bool `json:"enable_history"` // 是否允许保存聊天记录
|
||||
@@ -90,6 +92,8 @@ type Platform string
|
||||
const OpenAI = Platform("OpenAI")
|
||||
const Azure = Platform("Azure")
|
||||
const ChatGLM = Platform("ChatGLM")
|
||||
const Baidu = Platform("Baidu")
|
||||
const XunFei = Platform("XunFei")
|
||||
|
||||
// UserChatConfig 用户的聊天配置
|
||||
type UserChatConfig struct {
|
||||
@@ -111,7 +115,8 @@ type SystemConfig struct {
|
||||
InitImgCalls int `json:"init_img_calls"`
|
||||
VipMonthCalls int `json:"vip_month_calls"` // 会员每个赠送的调用次数
|
||||
EnabledRegister bool `json:"enabled_register"`
|
||||
EnabledMsg bool `json:"enabled_msg"` // 启用短信验证码服务
|
||||
EnabledDraw bool `json:"enabled_draw"` // 启动 AI 绘画功能
|
||||
RewardImg string `json:"reward_img"` // 众筹收款二维码地址
|
||||
EnabledMsg bool `json:"enabled_msg"` // 启用短信验证码服务
|
||||
EnabledDraw bool `json:"enabled_draw"` // 启动 AI 绘画功能
|
||||
RewardImg string `json:"reward_img"` // 众筹收款二维码地址
|
||||
EnabledFunction bool `json:"enabled_function"` // 启用 API 函数功能
|
||||
}
|
||||
|
||||
@@ -36,11 +36,11 @@ func (h *ApiKeyHandler) Save(c *gin.Context) {
|
||||
|
||||
apiKey := model.ApiKey{}
|
||||
if data.Id > 0 {
|
||||
h.db.Find(&apiKey)
|
||||
h.db.Find(&apiKey, data.Id)
|
||||
}
|
||||
apiKey.Platform = data.Platform
|
||||
apiKey.Value = data.Value
|
||||
res := h.db.Save(&apiKey)
|
||||
res := h.db.Debug().Save(&apiKey)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "更新数据库失败!")
|
||||
return
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"chatplus/core/types"
|
||||
"chatplus/store/model"
|
||||
"chatplus/store/vo"
|
||||
"chatplus/utils"
|
||||
"chatplus/utils/resp"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Update 更新会话标题
|
||||
func (h *ChatHandler) Update(c *gin.Context) {
|
||||
var data struct {
|
||||
Id uint `json:"id"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
var m = model.ChatItem{}
|
||||
m.Id = data.Id
|
||||
res := h.db.Model(&m).UpdateColumn("title", data.Title)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "Failed to update database")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, types.OkMsg)
|
||||
}
|
||||
|
||||
// History 获取聊天历史记录
|
||||
func (h *ChatHandler) History(c *gin.Context) {
|
||||
chatId := c.Query("chat_id") // 会话 ID
|
||||
var items []model.HistoryMessage
|
||||
var messages = make([]vo.HistoryMessage, 0)
|
||||
res := h.db.Where("chat_id = ?", chatId).Find(&items)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "No history message")
|
||||
return
|
||||
} else {
|
||||
for _, item := range items {
|
||||
var v vo.HistoryMessage
|
||||
err := utils.CopyObject(item, &v)
|
||||
v.CreatedAt = item.CreatedAt.Unix()
|
||||
v.UpdatedAt = item.UpdatedAt.Unix()
|
||||
if err == nil {
|
||||
messages = append(messages, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, messages)
|
||||
}
|
||||
|
||||
// Clear 清空所有聊天记录
|
||||
func (h *ChatHandler) Clear(c *gin.Context) {
|
||||
// 获取当前登录用户所有的聊天会话
|
||||
user, err := utils.GetLoginUser(c, h.db)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
var chats []model.ChatItem
|
||||
res := h.db.Where("user_id = ?", user.Id).Find(&chats)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "No chats found")
|
||||
return
|
||||
}
|
||||
|
||||
var chatIds = make([]string, 0)
|
||||
for _, chat := range chats {
|
||||
chatIds = append(chatIds, chat.ChatId)
|
||||
// 清空会话上下文
|
||||
h.App.ChatContexts.Delete(chat.ChatId)
|
||||
}
|
||||
err = h.db.Transaction(func(tx *gorm.DB) error {
|
||||
res := h.db.Where("user_id =?", user.Id).Delete(&model.ChatItem{})
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
|
||||
res = h.db.Where("user_id = ? AND chat_id IN ?", user.Id, chatIds).Delete(&model.HistoryMessage{})
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
|
||||
// TODO: 是否要删除 MidJourney 绘画记录和图片文件?
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("Error with delete chats: %+v", err)
|
||||
resp.ERROR(c, "Failed to remove chat from database.")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, types.OkMsg)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package handler
|
||||
package chatimpl
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -16,7 +16,8 @@ import (
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// 将消息发送给 Azure API 并获取结果,通过 WebSocket 推送到客户端
|
||||
// 微软 Azure 模型消息发送实现
|
||||
|
||||
func (h *ChatHandler) sendAzureMessage(
|
||||
chatCtx []interface{},
|
||||
req types.ApiRequest,
|
||||
273
api/handler/chatimpl/baidu_handler.go
Normal file
@@ -0,0 +1,273 @@
|
||||
package chatimpl
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"chatplus/core/types"
|
||||
"chatplus/store/model"
|
||||
"chatplus/store/vo"
|
||||
"chatplus/utils"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"gorm.io/gorm"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type baiduResp struct {
|
||||
Id string `json:"id"`
|
||||
Object string `json:"object"`
|
||||
Created int `json:"created"`
|
||||
SentenceId int `json:"sentence_id"`
|
||||
IsEnd bool `json:"is_end"`
|
||||
IsTruncated bool `json:"is_truncated"`
|
||||
Result string `json:"result"`
|
||||
NeedClearHistory bool `json:"need_clear_history"`
|
||||
Usage struct {
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
} `json:"usage"`
|
||||
}
|
||||
|
||||
// 百度文心一言消息发送实现
|
||||
|
||||
func (h *ChatHandler) sendBaiduMessage(
|
||||
chatCtx []interface{},
|
||||
req types.ApiRequest,
|
||||
userVo vo.User,
|
||||
ctx context.Context,
|
||||
session *types.ChatSession,
|
||||
role model.ChatRole,
|
||||
prompt string,
|
||||
ws *types.WsClient) error {
|
||||
promptCreatedAt := time.Now() // 记录提问时间
|
||||
start := time.Now()
|
||||
var apiKey = userVo.ChatConfig.ApiKeys[session.Model.Platform]
|
||||
response, err := h.doRequest(ctx, req, session.Model.Platform, &apiKey)
|
||||
logger.Info("HTTP请求完成,耗时:", time.Now().Sub(start))
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "context canceled") {
|
||||
logger.Info("用户取消了请求:", prompt)
|
||||
return nil
|
||||
} else if strings.Contains(err.Error(), "no available key") {
|
||||
utils.ReplyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
|
||||
return nil
|
||||
} else {
|
||||
logger.Error(err)
|
||||
}
|
||||
|
||||
utils.ReplyMessage(ws, ErrorMsg)
|
||||
utils.ReplyMessage(ws, "")
|
||||
return err
|
||||
} else {
|
||||
defer response.Body.Close()
|
||||
}
|
||||
|
||||
contentType := response.Header.Get("Content-Type")
|
||||
if strings.Contains(contentType, "text/event-stream") {
|
||||
replyCreatedAt := time.Now() // 记录回复时间
|
||||
// 循环读取 Chunk 消息
|
||||
var message = types.Message{}
|
||||
var contents = make([]string, 0)
|
||||
var content string
|
||||
scanner := bufio.NewScanner(response.Body)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if len(line) < 5 || strings.HasPrefix(line, "id:") {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "data:") {
|
||||
content = line[5:]
|
||||
}
|
||||
|
||||
var resp baiduResp
|
||||
err := utils.JsonDecode(content, &resp)
|
||||
if err != nil {
|
||||
logger.Error("error with parse data line: ", err)
|
||||
utils.ReplyMessage(ws, fmt.Sprintf("**解析数据行失败:%s**", err))
|
||||
break
|
||||
}
|
||||
|
||||
if len(contents) == 0 {
|
||||
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
|
||||
}
|
||||
utils.ReplyChunkMessage(ws, types.WsMessage{
|
||||
Type: types.WsMiddle,
|
||||
Content: utils.InterfaceToString(resp.Result),
|
||||
})
|
||||
contents = append(contents, resp.Result)
|
||||
|
||||
if resp.IsTruncated {
|
||||
utils.ReplyMessage(ws, "AI 输出异常中断")
|
||||
break
|
||||
}
|
||||
|
||||
if resp.IsEnd {
|
||||
break
|
||||
}
|
||||
|
||||
} // end for
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
if strings.Contains(err.Error(), "context canceled") {
|
||||
logger.Info("用户取消了请求:", prompt)
|
||||
} else {
|
||||
logger.Error("信息读取出错:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 消息发送成功
|
||||
if len(contents) > 0 {
|
||||
// 更新用户的对话次数
|
||||
if userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" {
|
||||
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("calls", gorm.Expr("calls - ?", 1))
|
||||
}
|
||||
|
||||
if message.Role == "" {
|
||||
message.Role = "assistant"
|
||||
}
|
||||
message.Content = strings.Join(contents, "")
|
||||
useMsg := types.Message{Role: "user", Content: prompt}
|
||||
|
||||
// 更新上下文消息,如果是调用函数则不需要更新上下文
|
||||
if h.App.ChatConfig.EnableContext {
|
||||
chatCtx = append(chatCtx, useMsg) // 提问消息
|
||||
chatCtx = append(chatCtx, message) // 回复消息
|
||||
h.App.ChatContexts.Put(session.ChatId, chatCtx)
|
||||
}
|
||||
|
||||
// 追加聊天记录
|
||||
if h.App.ChatConfig.EnableHistory {
|
||||
// for prompt
|
||||
promptToken, err := utils.CalcTokens(prompt, req.Model)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
historyUserMsg := model.HistoryMessage{
|
||||
UserId: userVo.Id,
|
||||
ChatId: session.ChatId,
|
||||
RoleId: role.Id,
|
||||
Type: types.PromptMsg,
|
||||
Icon: userVo.Avatar,
|
||||
Content: prompt,
|
||||
Tokens: promptToken,
|
||||
UseContext: true,
|
||||
}
|
||||
historyUserMsg.CreatedAt = promptCreatedAt
|
||||
historyUserMsg.UpdatedAt = promptCreatedAt
|
||||
res := h.db.Save(&historyUserMsg)
|
||||
if res.Error != nil {
|
||||
logger.Error("failed to save prompt history message: ", res.Error)
|
||||
}
|
||||
|
||||
// for reply
|
||||
// 计算本次对话消耗的总 token 数量
|
||||
replyToken, _ := utils.CalcTokens(message.Content, req.Model)
|
||||
totalTokens := replyToken + getTotalTokens(req)
|
||||
historyReplyMsg := model.HistoryMessage{
|
||||
UserId: userVo.Id,
|
||||
ChatId: session.ChatId,
|
||||
RoleId: role.Id,
|
||||
Type: types.ReplyMsg,
|
||||
Icon: role.Icon,
|
||||
Content: message.Content,
|
||||
Tokens: totalTokens,
|
||||
UseContext: true,
|
||||
}
|
||||
historyReplyMsg.CreatedAt = replyCreatedAt
|
||||
historyReplyMsg.UpdatedAt = replyCreatedAt
|
||||
res = h.db.Create(&historyReplyMsg)
|
||||
if res.Error != nil {
|
||||
logger.Error("failed to save reply history message: ", res.Error)
|
||||
}
|
||||
// 更新用户信息
|
||||
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).
|
||||
UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", totalTokens))
|
||||
}
|
||||
|
||||
// 保存当前会话
|
||||
var chatItem model.ChatItem
|
||||
res := h.db.Where("chat_id = ?", session.ChatId).First(&chatItem)
|
||||
if res.Error != nil {
|
||||
chatItem.ChatId = session.ChatId
|
||||
chatItem.UserId = session.UserId
|
||||
chatItem.RoleId = role.Id
|
||||
chatItem.ModelId = session.Model.Id
|
||||
if utf8.RuneCountInString(prompt) > 30 {
|
||||
chatItem.Title = string([]rune(prompt)[:30]) + "..."
|
||||
} else {
|
||||
chatItem.Title = prompt
|
||||
}
|
||||
h.db.Create(&chatItem)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error with reading response: %v", err)
|
||||
}
|
||||
|
||||
var res struct {
|
||||
Code int `json:"error_code"`
|
||||
Msg string `json:"error_msg"`
|
||||
}
|
||||
err = json.Unmarshal(body, &res)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error with decode response: %v", err)
|
||||
}
|
||||
utils.ReplyMessage(ws, "请求百度文心大模型 API 失败:"+res.Msg)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ChatHandler) getBaiduToken(apiKey string) (string, error) {
|
||||
ctx := context.Background()
|
||||
tokenString, err := h.redis.Get(ctx, apiKey).Result()
|
||||
if err == nil {
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
expr := time.Hour * 24 * 20 // access_token 有效期
|
||||
key := strings.Split(apiKey, "|")
|
||||
if len(key) != 2 {
|
||||
return "", fmt.Errorf("invalid api key: %s", apiKey)
|
||||
}
|
||||
url := fmt.Sprintf("https://aip.baidubce.com/oauth/2.0/token?client_id=%s&client_secret=%s&grant_type=client_credentials", key[0], key[1])
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("POST", url, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
req.Header.Add("Accept", "application/json")
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with send request: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with read response: %w", err)
|
||||
}
|
||||
var r map[string]interface{}
|
||||
err = json.Unmarshal(body, &r)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with parse response: %w", err)
|
||||
}
|
||||
|
||||
if r["error"] != nil {
|
||||
return "", fmt.Errorf("error with api response: %s", r["error_description"])
|
||||
}
|
||||
|
||||
tokenString = fmt.Sprintf("%s", r["access_token"])
|
||||
h.redis.Set(ctx, apiKey, tokenString, expr)
|
||||
return tokenString, nil
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package handler
|
||||
package chatimpl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"chatplus/core"
|
||||
"chatplus/core/types"
|
||||
"chatplus/handler"
|
||||
logger2 "chatplus/logger"
|
||||
"chatplus/service/mj"
|
||||
"chatplus/store"
|
||||
"chatplus/store/model"
|
||||
@@ -26,8 +28,10 @@ import (
|
||||
|
||||
const ErrorMsg = "抱歉,AI 助手开小差了,请稍后再试。"
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
type ChatHandler struct {
|
||||
BaseHandler
|
||||
handler.BaseHandler
|
||||
db *gorm.DB
|
||||
leveldb *store.LevelDB
|
||||
redis *redis.Client
|
||||
@@ -35,9 +39,14 @@ type ChatHandler struct {
|
||||
}
|
||||
|
||||
func NewChatHandler(app *core.AppServer, db *gorm.DB, levelDB *store.LevelDB, redis *redis.Client, service *mj.Service) *ChatHandler {
|
||||
handler := ChatHandler{db: db, leveldb: levelDB, redis: redis, mjService: service}
|
||||
handler.App = app
|
||||
return &handler
|
||||
h := ChatHandler{
|
||||
db: db,
|
||||
leveldb: levelDB,
|
||||
redis: redis,
|
||||
mjService: service,
|
||||
}
|
||||
h.App = app
|
||||
return &h
|
||||
}
|
||||
|
||||
var chatConfig types.ChatConfig
|
||||
@@ -123,7 +132,11 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) {
|
||||
logger.Error(err)
|
||||
client.Close()
|
||||
h.App.ChatClients.Delete(sessionId)
|
||||
h.App.ReqCancelFunc.Delete(sessionId)
|
||||
cancelFunc := h.App.ReqCancelFunc.Get(sessionId)
|
||||
if cancelFunc != nil {
|
||||
cancelFunc()
|
||||
h.App.ReqCancelFunc.Delete(sessionId)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -196,17 +209,30 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
|
||||
req.Temperature = h.App.ChatConfig.ChatGML.Temperature
|
||||
req.MaxTokens = h.App.ChatConfig.ChatGML.MaxTokens
|
||||
break
|
||||
default:
|
||||
case types.Baidu:
|
||||
req.Temperature = h.App.ChatConfig.OpenAI.Temperature
|
||||
// TODO: 目前只支持 ERNIE-Bot-turbo 模型,如果是 ERNIE-Bot 模型则需要增加函数支持
|
||||
case types.OpenAI:
|
||||
req.Temperature = h.App.ChatConfig.OpenAI.Temperature
|
||||
req.MaxTokens = h.App.ChatConfig.OpenAI.MaxTokens
|
||||
var functions = make([]types.Function, 0)
|
||||
for _, f := range types.InnerFunctions {
|
||||
if !h.App.SysConfig.EnabledDraw && f.Name == types.FuncMidJourney {
|
||||
continue
|
||||
// OpenAI 支持函数功能
|
||||
if h.App.SysConfig.EnabledFunction {
|
||||
var functions = make([]types.Function, 0)
|
||||
for _, f := range types.InnerFunctions {
|
||||
if !h.App.SysConfig.EnabledDraw && f.Name == types.FuncMidJourney {
|
||||
continue
|
||||
}
|
||||
functions = append(functions, f)
|
||||
}
|
||||
functions = append(functions, f)
|
||||
req.Functions = functions
|
||||
}
|
||||
req.Functions = functions
|
||||
case types.XunFei:
|
||||
req.Temperature = h.App.ChatConfig.XunFei.Temperature
|
||||
req.MaxTokens = h.App.ChatConfig.XunFei.MaxTokens
|
||||
default:
|
||||
utils.ReplyMessage(ws, "不支持的平台:"+session.Model.Platform+",请联系管理员!")
|
||||
utils.ReplyMessage(ws, "")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 加载聊天上下文
|
||||
@@ -239,9 +265,10 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
|
||||
// loading recent chat history as chat context
|
||||
if chatConfig.ContextDeep > 0 {
|
||||
var historyMessages []model.HistoryMessage
|
||||
res := h.db.Where("chat_id = ? and use_context = 1", session.ChatId).Limit(chatConfig.ContextDeep).Order("created_at desc").Find(&historyMessages)
|
||||
res := h.db.Debug().Where("chat_id = ? and use_context = 1", session.ChatId).Limit(chatConfig.ContextDeep).Order("id desc").Find(&historyMessages)
|
||||
if res.Error == nil {
|
||||
for _, msg := range historyMessages {
|
||||
for i := len(historyMessages) - 1; i >= 0; i-- {
|
||||
msg := historyMessages[i]
|
||||
if tokens+msg.Tokens >= types.ModelToTokens[session.Model.Value] {
|
||||
break
|
||||
}
|
||||
@@ -274,6 +301,11 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
|
||||
return h.sendOpenAiMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
|
||||
case types.ChatGLM:
|
||||
return h.sendChatGLMMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
|
||||
case types.Baidu:
|
||||
return h.sendBaiduMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
|
||||
case types.XunFei:
|
||||
return h.sendXunFeiMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
|
||||
|
||||
}
|
||||
utils.ReplyChunkMessage(ws, types.WsMessage{
|
||||
Type: types.WsMiddle,
|
||||
@@ -357,12 +389,36 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platf
|
||||
break
|
||||
case types.ChatGLM:
|
||||
apiURL = strings.Replace(h.App.ChatConfig.ChatGML.ApiURL, "{model}", req.Model, 1)
|
||||
req.Prompt = req.Messages
|
||||
req.Prompt = req.Messages // 使用 prompt 字段替代 message 字段
|
||||
req.Messages = nil
|
||||
break
|
||||
case types.Baidu:
|
||||
apiURL = h.App.ChatConfig.Baidu.ApiURL
|
||||
break
|
||||
default:
|
||||
apiURL = h.App.ChatConfig.OpenAI.ApiURL
|
||||
}
|
||||
if *apiKey == "" {
|
||||
var key model.ApiKey
|
||||
res := h.db.Where("platform = ?", platform).Order("last_used_at ASC").First(&key)
|
||||
if res.Error != nil {
|
||||
return nil, errors.New("no available key, please import key")
|
||||
}
|
||||
// 更新 API KEY 的最后使用时间
|
||||
h.db.Model(&key).UpdateColumn("last_used_at", time.Now().Unix())
|
||||
*apiKey = key.Value
|
||||
}
|
||||
|
||||
// 百度文心,需要串接 access_token
|
||||
if platform == types.Baidu {
|
||||
token, err := h.getBaiduToken(*apiKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logger.Info("百度文心 Access_Token:", token)
|
||||
apiURL = fmt.Sprintf("%s?access_token=%s", apiURL, token)
|
||||
}
|
||||
|
||||
// 创建 HttpClient 请求对象
|
||||
var client *http.Client
|
||||
requestBody, err := json.Marshal(req)
|
||||
@@ -387,17 +443,6 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platf
|
||||
} else {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
if *apiKey == "" {
|
||||
var key model.ApiKey
|
||||
res := h.db.Where("platform = ?", platform).Order("last_used_at ASC").First(&key)
|
||||
if res.Error != nil {
|
||||
return nil, errors.New("no available key, please import key")
|
||||
}
|
||||
// 更新 API KEY 的最后使用时间
|
||||
h.db.Model(&key).UpdateColumn("last_used_at", time.Now().Unix())
|
||||
*apiKey = key.Value
|
||||
}
|
||||
|
||||
logger.Infof("Sending %s request, KEY: %s, PROXY: %s, Model: %s", platform, *apiKey, proxyURL, req.Model)
|
||||
switch platform {
|
||||
case types.Azure:
|
||||
@@ -411,7 +456,9 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platf
|
||||
logger.Info(token)
|
||||
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
break
|
||||
default:
|
||||
case types.Baidu:
|
||||
request.RequestURI = ""
|
||||
case types.OpenAI:
|
||||
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiKey))
|
||||
}
|
||||
return client.Do(request)
|
||||
@@ -1,4 +1,4 @@
|
||||
package handler
|
||||
package chatimpl
|
||||
|
||||
import (
|
||||
"chatplus/core/types"
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"chatplus/utils"
|
||||
"chatplus/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// List 获取会话列表
|
||||
@@ -47,6 +48,95 @@ func (h *ChatHandler) List(c *gin.Context) {
|
||||
resp.SUCCESS(c, items)
|
||||
}
|
||||
|
||||
// Update 更新会话标题
|
||||
func (h *ChatHandler) Update(c *gin.Context) {
|
||||
var data struct {
|
||||
ChatId string `json:"chat_id"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
res := h.db.Model(&model.ChatItem{}).Where("chat_id = ?", data.ChatId).UpdateColumn("title", data.Title)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "Failed to update database")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, types.OkMsg)
|
||||
}
|
||||
|
||||
// Clear 清空所有聊天记录
|
||||
func (h *ChatHandler) Clear(c *gin.Context) {
|
||||
// 获取当前登录用户所有的聊天会话
|
||||
user, err := utils.GetLoginUser(c, h.db)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
var chats []model.ChatItem
|
||||
res := h.db.Where("user_id = ?", user.Id).Find(&chats)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "No chats found")
|
||||
return
|
||||
}
|
||||
|
||||
var chatIds = make([]string, 0)
|
||||
for _, chat := range chats {
|
||||
chatIds = append(chatIds, chat.ChatId)
|
||||
// 清空会话上下文
|
||||
h.App.ChatContexts.Delete(chat.ChatId)
|
||||
}
|
||||
err = h.db.Transaction(func(tx *gorm.DB) error {
|
||||
res := h.db.Where("user_id =?", user.Id).Delete(&model.ChatItem{})
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
|
||||
res = h.db.Where("user_id = ? AND chat_id IN ?", user.Id, chatIds).Delete(&model.HistoryMessage{})
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
|
||||
// TODO: 是否要删除 MidJourney 绘画记录和图片文件?
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("Error with delete chats: %+v", err)
|
||||
resp.ERROR(c, "Failed to remove chat from database.")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, types.OkMsg)
|
||||
}
|
||||
|
||||
// History 获取聊天历史记录
|
||||
func (h *ChatHandler) History(c *gin.Context) {
|
||||
chatId := c.Query("chat_id") // 会话 ID
|
||||
var items []model.HistoryMessage
|
||||
var messages = make([]vo.HistoryMessage, 0)
|
||||
res := h.db.Where("chat_id = ?", chatId).Find(&items)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "No history message")
|
||||
return
|
||||
} else {
|
||||
for _, item := range items {
|
||||
var v vo.HistoryMessage
|
||||
err := utils.CopyObject(item, &v)
|
||||
v.CreatedAt = item.CreatedAt.Unix()
|
||||
v.UpdatedAt = item.UpdatedAt.Unix()
|
||||
if err == nil {
|
||||
messages = append(messages, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, messages)
|
||||
}
|
||||
|
||||
// Remove 删除会话
|
||||
func (h *ChatHandler) Remove(c *gin.Context) {
|
||||
chatId := h.GetTrim(c, "chat_id")
|
||||
@@ -80,6 +170,7 @@ func (h *ChatHandler) Remove(c *gin.Context) {
|
||||
resp.SUCCESS(c, types.OkMsg)
|
||||
}
|
||||
|
||||
// Detail 对话详情,用户导出对话
|
||||
func (h *ChatHandler) Detail(c *gin.Context) {
|
||||
chatId := h.GetTrim(c, "chat_id")
|
||||
if utils.IsEmptyValue(chatId) {
|
||||
@@ -1,4 +1,4 @@
|
||||
package handler
|
||||
package chatimpl
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -17,7 +17,8 @@ import (
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// 将消息发送给 ChatGLM API 并获取结果,通过 WebSocket 推送到客户端
|
||||
// 清华大学 ChatGML 消息发送实现
|
||||
|
||||
func (h *ChatHandler) sendChatGLMMessage(
|
||||
chatCtx []interface{},
|
||||
req types.ApiRequest,
|
||||
@@ -1,4 +1,4 @@
|
||||
package handler
|
||||
package chatimpl
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// 将消息发送给 OpenAI API 并获取结果,通过 WebSocket 推送到客户端
|
||||
// OPenAI 消息发送实现
|
||||
func (h *ChatHandler) sendOpenAiMessage(
|
||||
chatCtx []interface{},
|
||||
req types.ApiRequest,
|
||||
322
api/handler/chatimpl/xunfei_handler.go
Normal file
@@ -0,0 +1,322 @@
|
||||
package chatimpl
|
||||
|
||||
import (
|
||||
"chatplus/core/types"
|
||||
"chatplus/store/model"
|
||||
"chatplus/store/vo"
|
||||
"chatplus/utils"
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gorilla/websocket"
|
||||
"gorm.io/gorm"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type xunFeiResp struct {
|
||||
Header struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Sid string `json:"sid"`
|
||||
Status int `json:"status"`
|
||||
} `json:"header"`
|
||||
Payload struct {
|
||||
Choices struct {
|
||||
Status int `json:"status"`
|
||||
Seq int `json:"seq"`
|
||||
Text []struct {
|
||||
Content string `json:"content"`
|
||||
Role string `json:"role"`
|
||||
Index int `json:"index"`
|
||||
} `json:"text"`
|
||||
} `json:"choices"`
|
||||
Usage struct {
|
||||
Text struct {
|
||||
QuestionTokens int `json:"question_tokens"`
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
} `json:"text"`
|
||||
} `json:"usage"`
|
||||
} `json:"payload"`
|
||||
}
|
||||
|
||||
// 科大讯飞消息发送实现
|
||||
|
||||
func (h *ChatHandler) sendXunFeiMessage(
|
||||
chatCtx []interface{},
|
||||
req types.ApiRequest,
|
||||
userVo vo.User,
|
||||
ctx context.Context,
|
||||
session *types.ChatSession,
|
||||
role model.ChatRole,
|
||||
prompt string,
|
||||
ws *types.WsClient) error {
|
||||
promptCreatedAt := time.Now() // 记录提问时间
|
||||
var apiKey = userVo.ChatConfig.ApiKeys[session.Model.Platform]
|
||||
if apiKey == "" {
|
||||
var key model.ApiKey
|
||||
res := h.db.Where("platform = ?", session.Model.Platform).Order("last_used_at ASC").First(&key)
|
||||
if res.Error != nil {
|
||||
utils.ReplyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
|
||||
return nil
|
||||
}
|
||||
// 更新 API KEY 的最后使用时间
|
||||
h.db.Model(&key).UpdateColumn("last_used_at", time.Now().Unix())
|
||||
apiKey = key.Value
|
||||
}
|
||||
|
||||
d := websocket.Dialer{
|
||||
HandshakeTimeout: 5 * time.Second,
|
||||
}
|
||||
key := strings.Split(apiKey, "|")
|
||||
if len(key) != 3 {
|
||||
utils.ReplyMessage(ws, "非法的 API KEY!")
|
||||
return nil
|
||||
}
|
||||
|
||||
var apiURL string
|
||||
if req.Model == "generalv2" {
|
||||
apiURL = strings.Replace(h.App.ChatConfig.XunFei.ApiURL, "{version}", "v2.1", 1)
|
||||
} else {
|
||||
apiURL = strings.Replace(h.App.ChatConfig.XunFei.ApiURL, "{version}", "v1.1", 1)
|
||||
}
|
||||
|
||||
wsURL, err := assembleAuthUrl(apiURL, key[1], key[2])
|
||||
//握手并建立websocket 连接
|
||||
conn, resp, err := d.Dial(wsURL, nil)
|
||||
if err != nil {
|
||||
logger.Error(readResp(resp) + err.Error())
|
||||
utils.ReplyMessage(ws, "请求讯飞星火模型 API 失败:"+readResp(resp)+err.Error())
|
||||
return nil
|
||||
} else if resp.StatusCode != 101 {
|
||||
utils.ReplyMessage(ws, "请求讯飞星火模型 API 失败:"+readResp(resp)+err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
data := buildRequest(key[0], req)
|
||||
fmt.Printf("%+v", data)
|
||||
fmt.Println(apiURL)
|
||||
err = conn.WriteJSON(data)
|
||||
if err != nil {
|
||||
utils.ReplyMessage(ws, "发送消息失败:"+err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
replyCreatedAt := time.Now() // 记录回复时间
|
||||
// 循环读取 Chunk 消息
|
||||
var message = types.Message{}
|
||||
var contents = make([]string, 0)
|
||||
var content string
|
||||
for {
|
||||
_, msg, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
logger.Error("error with read message:", err)
|
||||
utils.ReplyMessage(ws, fmt.Sprintf("**数据读取失败:%s**", err))
|
||||
break
|
||||
}
|
||||
|
||||
// 解析数据
|
||||
var result xunFeiResp
|
||||
err = json.Unmarshal(msg, &result)
|
||||
if err != nil {
|
||||
logger.Error("error with parsing JSON:", err)
|
||||
utils.ReplyMessage(ws, fmt.Sprintf("**解析数据行失败:%s**", err))
|
||||
return nil
|
||||
}
|
||||
|
||||
if result.Header.Code != 0 {
|
||||
utils.ReplyMessage(ws, fmt.Sprintf("**请求 API 返回错误:%s**", result.Header.Message))
|
||||
return nil
|
||||
}
|
||||
|
||||
content = result.Payload.Choices.Text[0].Content
|
||||
contents = append(contents, content)
|
||||
// 第一个结果
|
||||
if result.Payload.Choices.Status == 0 {
|
||||
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
|
||||
}
|
||||
utils.ReplyChunkMessage(ws, types.WsMessage{
|
||||
Type: types.WsMiddle,
|
||||
Content: utils.InterfaceToString(content),
|
||||
})
|
||||
|
||||
if result.Payload.Choices.Status == 2 { // 最终结果
|
||||
_ = conn.Close() // 关闭连接
|
||||
break
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
utils.ReplyMessage(ws, "**用户取消了生成指令!**")
|
||||
return nil
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 消息发送成功
|
||||
if len(contents) > 0 {
|
||||
// 更新用户的对话次数
|
||||
if userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" {
|
||||
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("calls", gorm.Expr("calls - ?", 1))
|
||||
}
|
||||
|
||||
if message.Role == "" {
|
||||
message.Role = "assistant"
|
||||
}
|
||||
message.Content = strings.Join(contents, "")
|
||||
useMsg := types.Message{Role: "user", Content: prompt}
|
||||
|
||||
// 更新上下文消息,如果是调用函数则不需要更新上下文
|
||||
if h.App.ChatConfig.EnableContext {
|
||||
chatCtx = append(chatCtx, useMsg) // 提问消息
|
||||
chatCtx = append(chatCtx, message) // 回复消息
|
||||
h.App.ChatContexts.Put(session.ChatId, chatCtx)
|
||||
}
|
||||
|
||||
// 追加聊天记录
|
||||
if h.App.ChatConfig.EnableHistory {
|
||||
// for prompt
|
||||
promptToken, err := utils.CalcTokens(prompt, req.Model)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
historyUserMsg := model.HistoryMessage{
|
||||
UserId: userVo.Id,
|
||||
ChatId: session.ChatId,
|
||||
RoleId: role.Id,
|
||||
Type: types.PromptMsg,
|
||||
Icon: userVo.Avatar,
|
||||
Content: prompt,
|
||||
Tokens: promptToken,
|
||||
UseContext: true,
|
||||
}
|
||||
historyUserMsg.CreatedAt = promptCreatedAt
|
||||
historyUserMsg.UpdatedAt = promptCreatedAt
|
||||
res := h.db.Save(&historyUserMsg)
|
||||
if res.Error != nil {
|
||||
logger.Error("failed to save prompt history message: ", res.Error)
|
||||
}
|
||||
|
||||
// for reply
|
||||
// 计算本次对话消耗的总 token 数量
|
||||
replyToken, _ := utils.CalcTokens(message.Content, req.Model)
|
||||
totalTokens := replyToken + getTotalTokens(req)
|
||||
historyReplyMsg := model.HistoryMessage{
|
||||
UserId: userVo.Id,
|
||||
ChatId: session.ChatId,
|
||||
RoleId: role.Id,
|
||||
Type: types.ReplyMsg,
|
||||
Icon: role.Icon,
|
||||
Content: message.Content,
|
||||
Tokens: totalTokens,
|
||||
UseContext: true,
|
||||
}
|
||||
historyReplyMsg.CreatedAt = replyCreatedAt
|
||||
historyReplyMsg.UpdatedAt = replyCreatedAt
|
||||
res = h.db.Create(&historyReplyMsg)
|
||||
if res.Error != nil {
|
||||
logger.Error("failed to save reply history message: ", res.Error)
|
||||
}
|
||||
// 更新用户信息
|
||||
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).
|
||||
UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", totalTokens))
|
||||
}
|
||||
|
||||
// 保存当前会话
|
||||
var chatItem model.ChatItem
|
||||
res := h.db.Where("chat_id = ?", session.ChatId).First(&chatItem)
|
||||
if res.Error != nil {
|
||||
chatItem.ChatId = session.ChatId
|
||||
chatItem.UserId = session.UserId
|
||||
chatItem.RoleId = role.Id
|
||||
chatItem.ModelId = session.Model.Id
|
||||
if utf8.RuneCountInString(prompt) > 30 {
|
||||
chatItem.Title = string([]rune(prompt)[:30]) + "..."
|
||||
} else {
|
||||
chatItem.Title = prompt
|
||||
}
|
||||
h.db.Create(&chatItem)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 构建 websocket 请求实体
|
||||
func buildRequest(appid string, req types.ApiRequest) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"header": map[string]interface{}{
|
||||
"app_id": appid,
|
||||
},
|
||||
"parameter": map[string]interface{}{
|
||||
"chat": map[string]interface{}{
|
||||
"domain": req.Model,
|
||||
"temperature": float64(req.Temperature),
|
||||
"top_k": int64(6),
|
||||
"max_tokens": int64(req.MaxTokens),
|
||||
"auditing": "default",
|
||||
},
|
||||
},
|
||||
"payload": map[string]interface{}{
|
||||
"message": map[string]interface{}{
|
||||
"text": req.Messages,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 创建鉴权 URL
|
||||
func assembleAuthUrl(hostURL string, apiKey, apiSecret string) (string, error) {
|
||||
ul, err := url.Parse(hostURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
date := time.Now().UTC().Format(time.RFC1123)
|
||||
signString := []string{"host: " + ul.Host, "date: " + date, "GET " + ul.Path + " HTTP/1.1"}
|
||||
//拼接签名字符串
|
||||
signStr := strings.Join(signString, "\n")
|
||||
sha := hmacWithSha256(signStr, apiSecret)
|
||||
|
||||
authUrl := fmt.Sprintf("hmac username=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"", apiKey,
|
||||
"hmac-sha256", "host date request-line", sha)
|
||||
//将请求参数使用base64编码
|
||||
authorization := base64.StdEncoding.EncodeToString([]byte(authUrl))
|
||||
v := url.Values{}
|
||||
v.Add("host", ul.Host)
|
||||
v.Add("date", date)
|
||||
v.Add("authorization", authorization)
|
||||
//将编码后的字符串url encode后添加到url后面
|
||||
return hostURL + "?" + v.Encode(), nil
|
||||
}
|
||||
|
||||
// 使用 sha256 签名
|
||||
func hmacWithSha256(data, key string) string {
|
||||
mac := hmac.New(sha256.New, []byte(key))
|
||||
mac.Write([]byte(data))
|
||||
encodeData := mac.Sum(nil)
|
||||
return base64.StdEncoding.EncodeToString(encodeData)
|
||||
}
|
||||
|
||||
// 读取响应
|
||||
func readResp(resp *http.Response) string {
|
||||
if resp == nil {
|
||||
return ""
|
||||
}
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return fmt.Sprintf("code=%d,body=%s", resp.StatusCode, string(b))
|
||||
}
|
||||
@@ -315,14 +315,26 @@ func (h *MidJourneyHandler) Variation(c *gin.Context) {
|
||||
// JobList 获取 MJ 任务列表
|
||||
func (h *MidJourneyHandler) JobList(c *gin.Context) {
|
||||
status := h.GetInt(c, "status", 0)
|
||||
var items []model.MidJourneyJob
|
||||
var res *gorm.DB
|
||||
userId, _ := c.Get(types.LoginUserID)
|
||||
userId := h.GetInt(c, "user_id", 0)
|
||||
page := h.GetInt(c, "page", 0)
|
||||
pageSize := h.GetInt(c, "page_size", 0)
|
||||
|
||||
session := h.db.Session(&gorm.Session{})
|
||||
if status == 1 {
|
||||
res = h.db.Where("user_id = ? AND progress = 100", userId).Order("id DESC").Find(&items)
|
||||
session = session.Where("progress = ?", 100).Order("id DESC")
|
||||
} else {
|
||||
res = h.db.Where("user_id = ? AND progress < 100", userId).Order("id ASC").Find(&items)
|
||||
session = session.Where("progress < ?", 100).Order("id ASC")
|
||||
}
|
||||
if userId > 0 {
|
||||
session = session.Where("user_id = ?", userId)
|
||||
}
|
||||
if page > 0 && pageSize > 0 {
|
||||
offset := (page - 1) * pageSize
|
||||
session = session.Offset(offset).Limit(pageSize)
|
||||
}
|
||||
|
||||
var items []model.MidJourneyJob
|
||||
res := session.Find(&items)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, types.NoData)
|
||||
return
|
||||
|
||||
@@ -163,14 +163,26 @@ func (h *SdJobHandler) Image(c *gin.Context) {
|
||||
// JobList 获取 MJ 任务列表
|
||||
func (h *SdJobHandler) JobList(c *gin.Context) {
|
||||
status := h.GetInt(c, "status", 0)
|
||||
var items []model.SdJob
|
||||
var res *gorm.DB
|
||||
userId, _ := c.Get(types.LoginUserID)
|
||||
userId := h.GetInt(c, "user_id", 0)
|
||||
page := h.GetInt(c, "page", 0)
|
||||
pageSize := h.GetInt(c, "page_size", 0)
|
||||
|
||||
session := h.db.Session(&gorm.Session{})
|
||||
if status == 1 {
|
||||
res = h.db.Where("user_id = ? AND progress = 100", userId).Order("id DESC").Find(&items)
|
||||
session = session.Where("progress = ?", 100).Order("id DESC")
|
||||
} else {
|
||||
res = h.db.Where("user_id = ? AND progress < 100", userId).Order("id ASC").Find(&items)
|
||||
session = session.Where("progress < ?", 100).Order("id ASC")
|
||||
}
|
||||
if userId > 0 {
|
||||
session = session.Where("user_id = ?", userId)
|
||||
}
|
||||
if page > 0 && pageSize > 0 {
|
||||
offset := (page - 1) * pageSize
|
||||
session = session.Offset(offset).Limit(pageSize)
|
||||
}
|
||||
|
||||
var items []model.SdJob
|
||||
res := session.Find(&items)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, types.NoData)
|
||||
return
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"chatplus/core/types"
|
||||
"chatplus/handler"
|
||||
"chatplus/handler/admin"
|
||||
"chatplus/handler/chatimpl"
|
||||
logger2 "chatplus/logger"
|
||||
"chatplus/service"
|
||||
"chatplus/service/fun"
|
||||
@@ -115,7 +116,7 @@ func main() {
|
||||
// 创建控制器
|
||||
fx.Provide(handler.NewChatRoleHandler),
|
||||
fx.Provide(handler.NewUserHandler),
|
||||
fx.Provide(handler.NewChatHandler),
|
||||
fx.Provide(chatimpl.NewChatHandler),
|
||||
fx.Provide(handler.NewUploadHandler),
|
||||
fx.Provide(handler.NewSmsHandler),
|
||||
fx.Provide(handler.NewRewardHandler),
|
||||
@@ -196,7 +197,7 @@ func main() {
|
||||
group.POST("password", h.Password)
|
||||
group.POST("bind/mobile", h.BindMobile)
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.ChatHandler) {
|
||||
fx.Invoke(func(s *core.AppServer, h *chatimpl.ChatHandler) {
|
||||
group := s.Engine.Group("/api/chat/")
|
||||
group.Any("new", h.ChatHandle)
|
||||
group.GET("list", h.List)
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
bytes, err := os.ReadFile("res/text2img.json")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(string(bytes))
|
||||
|
||||
}
|
||||
|
||||
1176
database/chatgpt_plus-v3.1.5.sql
Normal file
@@ -2,7 +2,7 @@ version: '3'
|
||||
services:
|
||||
# 后端 API 程序
|
||||
chatgpt-plus-api:
|
||||
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-api:v3.1.3
|
||||
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-api:v3.1.5
|
||||
container_name: chatgpt-plus-api
|
||||
restart: always
|
||||
environment:
|
||||
@@ -19,7 +19,7 @@ services:
|
||||
|
||||
# 前端应用
|
||||
chatgpt-plus-web:
|
||||
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-web:v3.1.3
|
||||
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-web:v3.1.5
|
||||
container_name: chatgpt-plus-web
|
||||
restart: always
|
||||
ports:
|
||||
|
||||
|
Before Width: | Height: | Size: 322 KiB After Width: | Height: | Size: 201 KiB |
BIN
docs/imgs/image-list.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 13 KiB |
BIN
docs/imgs/mobile_pay.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
docs/imgs/mobile_user_profile.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 454 KiB |
11
web/package-lock.json
generated
@@ -23,6 +23,7 @@
|
||||
"pinia": "^2.1.4",
|
||||
"qs": "^6.11.1",
|
||||
"sortablejs": "^1.15.0",
|
||||
"v3-waterfall": "^1.2.1",
|
||||
"vant": "^4.5.0",
|
||||
"vue": "^3.2.13",
|
||||
"vue-router": "^4.0.15"
|
||||
@@ -10459,6 +10460,11 @@
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/v3-waterfall": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/v3-waterfall/-/v3-waterfall-1.2.1.tgz",
|
||||
"integrity": "sha512-zjfT1FuHupsAahvS4mr3Yb8k2SHB8srW6st+/cBXwrsyhbCcj8Qhb1QtNUuEIx/tbpLQrMpxtJunZXkaKBfAEA=="
|
||||
},
|
||||
"node_modules/v8-compile-cache": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
|
||||
@@ -19518,6 +19524,11 @@
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"dev": true
|
||||
},
|
||||
"v3-waterfall": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/v3-waterfall/-/v3-waterfall-1.2.1.tgz",
|
||||
"integrity": "sha512-zjfT1FuHupsAahvS4mr3Yb8k2SHB8srW6st+/cBXwrsyhbCcj8Qhb1QtNUuEIx/tbpLQrMpxtJunZXkaKBfAEA=="
|
||||
},
|
||||
"v8-compile-cache": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
|
||||
|
||||
@@ -17,14 +17,15 @@
|
||||
"good-storage": "^1.1.1",
|
||||
"highlight.js": "^11.7.0",
|
||||
"json-bigint": "^1.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "^13.0.1",
|
||||
"md-editor-v3": "^2.2.1",
|
||||
"pinia": "^2.1.4",
|
||||
"qs": "^6.11.1",
|
||||
"sortablejs": "^1.15.0",
|
||||
"v3-waterfall": "^1.2.1",
|
||||
"vant": "^4.5.0",
|
||||
"vue": "^3.2.13",
|
||||
"lodash": "^4.17.21",
|
||||
"vue-router": "^4.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
width: 210px;
|
||||
width: 190px;
|
||||
}
|
||||
#app .common-layout .el-aside .chat-list .content .chat-list-item .chat-title {
|
||||
color: #c1c1c1;
|
||||
|
||||
@@ -17,6 +17,7 @@ $borderColor = #4676d0;
|
||||
display: flex;
|
||||
color: #ffffff;
|
||||
font-size: 20px;
|
||||
|
||||
span {
|
||||
padding-top: 5px;
|
||||
padding-left: 10px;
|
||||
@@ -81,7 +82,7 @@ $borderColor = #4676d0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
width: 210px;
|
||||
width: 190px;
|
||||
}
|
||||
|
||||
.chat-title {
|
||||
|
||||
13
web/src/assets/css/custom-scroll.css
Normal file
@@ -0,0 +1,13 @@
|
||||
.custom-scroll ::-webkit-scrollbar {
|
||||
width: 8px; /* 滚动条宽度 */
|
||||
}
|
||||
.custom-scroll ::-webkit-scrollbar-track {
|
||||
background-color: #282c34;
|
||||
}
|
||||
.custom-scroll ::-webkit-scrollbar-thumb {
|
||||
background-color: #444;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.custom-scroll ::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #666;
|
||||
}
|
||||
26
web/src/assets/css/custom-scroll.styl
Normal file
@@ -0,0 +1,26 @@
|
||||
.custom-scroll {
|
||||
/* 修改滚动条的颜色 */
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 8px; /* 滚动条宽度 */
|
||||
}
|
||||
|
||||
/* 修改滚动条轨道的背景颜色 */
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: #282C34;
|
||||
}
|
||||
|
||||
/* 修改滚动条的滑块颜色 */
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #444444;
|
||||
border-radius 8px
|
||||
}
|
||||
|
||||
/* 修改滚动条的滑块的悬停颜色 */
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #666666;
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,6 @@
|
||||
}
|
||||
.page-mj .inner {
|
||||
display: flex;
|
||||
/* 修改滚动条的颜色 */
|
||||
/* 修改滚动条轨道的背景颜色 */
|
||||
/* 修改滚动条的滑块颜色 */
|
||||
/* 修改滚动条的滑块的悬停颜色 */
|
||||
}
|
||||
.page-mj .inner .mj-box {
|
||||
margin: 10px;
|
||||
@@ -147,19 +143,6 @@
|
||||
.page-mj .inner .el-form .el-slider {
|
||||
width: 180px;
|
||||
}
|
||||
.page-mj .inner ::-webkit-scrollbar {
|
||||
width: 10px; /* 滚动条宽度 */
|
||||
}
|
||||
.page-mj .inner ::-webkit-scrollbar-track {
|
||||
background-color: #282c34;
|
||||
}
|
||||
.page-mj .inner ::-webkit-scrollbar-thumb {
|
||||
background-color: #444;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.page-mj .inner ::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #666;
|
||||
}
|
||||
.page-mj .inner .task-list-box {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
|
||||
@@ -182,160 +182,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* 修改滚动条的颜色 */
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px; /* 滚动条宽度 */
|
||||
}
|
||||
|
||||
/* 修改滚动条轨道的背景颜色 */
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: #282C34;
|
||||
}
|
||||
|
||||
/* 修改滚动条的滑块颜色 */
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #444444;
|
||||
border-radius 10px
|
||||
}
|
||||
|
||||
/* 修改滚动条的滑块的悬停颜色 */
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #666666;
|
||||
}
|
||||
|
||||
.task-list-box {
|
||||
width 100%
|
||||
padding 10px
|
||||
color #ffffff
|
||||
overflow-x hidden
|
||||
|
||||
.running-job-list {
|
||||
.job-item {
|
||||
//border: 1px solid #454545;
|
||||
width: 100%;
|
||||
padding 2px
|
||||
background-color #555555
|
||||
|
||||
.job-item-inner {
|
||||
position relative
|
||||
height 100%
|
||||
overflow hidden
|
||||
|
||||
.progress {
|
||||
position absolute
|
||||
width 100%
|
||||
height 100%
|
||||
top 0
|
||||
left 0
|
||||
display flex
|
||||
justify-content center
|
||||
align-items center
|
||||
|
||||
span {
|
||||
font-size 20px
|
||||
color #ffffff
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.finish-job-list {
|
||||
.job-item {
|
||||
width 100%
|
||||
height 100%
|
||||
|
||||
.opt {
|
||||
.opt-line {
|
||||
margin 6px 0
|
||||
|
||||
ul {
|
||||
display flex
|
||||
flex-flow row
|
||||
|
||||
li {
|
||||
margin-right 10px
|
||||
|
||||
a {
|
||||
padding 3px 0
|
||||
width 44px
|
||||
text-align center
|
||||
border-radius 5px
|
||||
display block
|
||||
cursor pointer
|
||||
background-color #4E5058
|
||||
color #ffffff
|
||||
|
||||
&:hover {
|
||||
background-color #6D6F78
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.show-prompt {
|
||||
font-size 20px
|
||||
cursor pointer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.el-image {
|
||||
width 100%
|
||||
height 100%
|
||||
max-height 240px
|
||||
|
||||
img {
|
||||
height 240px
|
||||
}
|
||||
|
||||
.el-image-viewer__wrapper {
|
||||
img {
|
||||
width auto
|
||||
height auto
|
||||
}
|
||||
}
|
||||
|
||||
.image-slot {
|
||||
display flex
|
||||
flex-flow column
|
||||
justify-content center
|
||||
align-items center
|
||||
height 100%
|
||||
min-height 200px
|
||||
color #ffffff
|
||||
|
||||
.iconfont {
|
||||
font-size 50px
|
||||
margin-bottom 10px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-image.upscale {
|
||||
max-height 304px
|
||||
|
||||
img {
|
||||
height 304px
|
||||
}
|
||||
|
||||
.el-image-viewer__wrapper {
|
||||
img {
|
||||
width auto
|
||||
height auto
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@import "task-list.styl"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -231,16 +231,3 @@
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
.custom-scroll ::-webkit-scrollbar {
|
||||
width: 10px; /* 滚动条宽度 */
|
||||
}
|
||||
.custom-scroll ::-webkit-scrollbar-track {
|
||||
background-color: #282c34;
|
||||
}
|
||||
.custom-scroll ::-webkit-scrollbar-thumb {
|
||||
background-color: #444;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.custom-scroll ::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #666;
|
||||
}
|
||||
|
||||
@@ -87,222 +87,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.task-list-box {
|
||||
width 100%
|
||||
padding 10px
|
||||
color #ffffff
|
||||
overflow-x hidden
|
||||
|
||||
.running-job-list {
|
||||
.job-item {
|
||||
//border: 1px solid #454545;
|
||||
width: 100%;
|
||||
padding 2px
|
||||
background-color #555555
|
||||
|
||||
.job-item-inner {
|
||||
position relative
|
||||
height 100%
|
||||
overflow hidden
|
||||
|
||||
.progress {
|
||||
position absolute
|
||||
width 100%
|
||||
height 100%
|
||||
top 0
|
||||
left 0
|
||||
display flex
|
||||
justify-content center
|
||||
align-items center
|
||||
|
||||
span {
|
||||
font-size 20px
|
||||
color #ffffff
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.finish-job-list {
|
||||
.job-item {
|
||||
width 100%
|
||||
height 100%
|
||||
|
||||
.opt {
|
||||
.opt-line {
|
||||
margin 6px 0
|
||||
|
||||
ul {
|
||||
display flex
|
||||
flex-flow row
|
||||
|
||||
li {
|
||||
margin-right 10px
|
||||
|
||||
a {
|
||||
padding 3px 0
|
||||
width 44px
|
||||
text-align center
|
||||
border-radius 5px
|
||||
display block
|
||||
cursor pointer
|
||||
background-color #4E5058
|
||||
color #ffffff
|
||||
|
||||
&:hover {
|
||||
background-color #6D6F78
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.show-prompt {
|
||||
font-size 20px
|
||||
cursor pointer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.el-image {
|
||||
width 100%
|
||||
height 100%
|
||||
max-height 240px
|
||||
|
||||
img {
|
||||
height 240px
|
||||
}
|
||||
|
||||
.el-image-viewer__wrapper {
|
||||
img {
|
||||
width auto
|
||||
height auto
|
||||
}
|
||||
}
|
||||
|
||||
.image-slot {
|
||||
display flex
|
||||
flex-flow column
|
||||
justify-content center
|
||||
align-items center
|
||||
height 100%
|
||||
min-height 200px
|
||||
color #ffffff
|
||||
|
||||
.iconfont {
|
||||
font-size 50px
|
||||
margin-bottom 10px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-image.upscale {
|
||||
max-height 304px
|
||||
|
||||
img {
|
||||
height 304px
|
||||
}
|
||||
|
||||
.el-image-viewer__wrapper {
|
||||
img {
|
||||
width auto
|
||||
height auto
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@import "task-list.styl"
|
||||
}
|
||||
|
||||
.el-overlay-dialog {
|
||||
.el-dialog {
|
||||
background-color #1a1b1e
|
||||
@import "sd-task-dialog.styl"
|
||||
|
||||
.el-dialog__header {
|
||||
.el-dialog__title {
|
||||
color #F5F5F5
|
||||
}
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding 0 0 0 15px !important
|
||||
display flex
|
||||
height 100%
|
||||
|
||||
.el-row {
|
||||
width 100%
|
||||
|
||||
.img-container {
|
||||
display flex
|
||||
justify-content center
|
||||
}
|
||||
|
||||
.task-info {
|
||||
background-color #25262b
|
||||
padding 1rem 1.5rem
|
||||
|
||||
.info-line {
|
||||
width 100%
|
||||
|
||||
.prompt {
|
||||
background-color #35363b
|
||||
padding 10px
|
||||
color #999999
|
||||
overflow auto
|
||||
max-height 100px
|
||||
min-height 50px
|
||||
|
||||
position relative
|
||||
|
||||
.el-icon {
|
||||
position absolute
|
||||
right 10px
|
||||
bottom 10px
|
||||
cursor pointer
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
margin-top 10px
|
||||
display flex
|
||||
|
||||
label {
|
||||
display flex
|
||||
width 100px
|
||||
color #a5a5a5
|
||||
}
|
||||
|
||||
.item-value {
|
||||
display flex
|
||||
width 100%
|
||||
background-color #35363b
|
||||
padding 2px 5px
|
||||
border-radius 5px
|
||||
color #F5F5F5
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.copy-params {
|
||||
padding 20px 0 10px 0
|
||||
|
||||
.el-button {
|
||||
width 100%
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// end el-row
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mj-list-item-prompt {
|
||||
.el-icon {
|
||||
@@ -315,30 +104,3 @@
|
||||
|
||||
}
|
||||
|
||||
.custom-scroll {
|
||||
/* 修改滚动条的颜色 */
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px; /* 滚动条宽度 */
|
||||
}
|
||||
|
||||
/* 修改滚动条轨道的背景颜色 */
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: #282C34;
|
||||
}
|
||||
|
||||
/* 修改滚动条的滑块颜色 */
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #444444;
|
||||
border-radius 10px
|
||||
}
|
||||
|
||||
/* 修改滚动条的滑块的悬停颜色 */
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #666666;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
175
web/src/assets/css/images-wall.css
Normal file
@@ -0,0 +1,175 @@
|
||||
.page-images-wall {
|
||||
display: flex;
|
||||
background-color: #282c34;
|
||||
}
|
||||
.page-images-wall .inner {
|
||||
width: 100%;
|
||||
color: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
.page-images-wall .inner .header {
|
||||
display: flex;
|
||||
padding: 0 40px;
|
||||
}
|
||||
.page-images-wall .inner .header h2 {
|
||||
width: 300px;
|
||||
}
|
||||
.page-images-wall .inner .header .settings {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
}
|
||||
.page-images-wall .inner .header .settings .el-radio-group {
|
||||
font-size: 16px;
|
||||
}
|
||||
.page-images-wall .inner .header .settings .el-radio-group .el-radio {
|
||||
color: #fff;
|
||||
}
|
||||
.page-images-wall .inner .waterfall {
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.page-images-wall .inner .waterfall .list-item .image {
|
||||
overflow: hidden;
|
||||
}
|
||||
.page-images-wall .inner .waterfall .list-item .image .el-image {
|
||||
transition: transform 0.3s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.page-images-wall .inner .waterfall .list-item .prompt {
|
||||
display: none;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
color: #fff;
|
||||
padding: 10px 10px 20px 10px;
|
||||
line-height: 1.2;
|
||||
background-color: rgba(10,10,10,0.7);
|
||||
}
|
||||
.page-images-wall .inner .waterfall .list-item .prompt .el-icon {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
cursor: pointer;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 5px;
|
||||
padding: 2px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.page-images-wall .inner .waterfall .list-item .prompt .el-icon:hover {
|
||||
background-color: #999;
|
||||
}
|
||||
.page-images-wall .inner .waterfall .list-item:hover .prompt {
|
||||
display: block;
|
||||
animation: expandUp 0.3s ease-in-out forwards;
|
||||
transform-origin: bottom center;
|
||||
transform: scaleY(0); /* 初始状态,元素高度为0 */
|
||||
}
|
||||
.page-images-wall .inner .waterfall .list-item:hover .image .el-image {
|
||||
transform: scale(1.2); /* 放大图像到1.2倍大小 */
|
||||
}
|
||||
.page-images-wall .inner .footer {
|
||||
display: flex;
|
||||
padding: 20px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.page-images-wall .inner .footer .iconfont {
|
||||
margin-left: 6px;
|
||||
}
|
||||
.page-images-wall .el-overlay-dialog .el-dialog {
|
||||
background-color: #1a1b1e;
|
||||
}
|
||||
.page-images-wall .el-overlay-dialog .el-dialog .el-dialog__header .el-dialog__title {
|
||||
color: #f5f5f5;
|
||||
}
|
||||
.page-images-wall .el-overlay-dialog .el-dialog .el-dialog__body {
|
||||
padding: 0 0 0 15px !important;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
.page-images-wall .el-overlay-dialog .el-dialog .el-dialog__body .el-row {
|
||||
width: 100%;
|
||||
}
|
||||
.page-images-wall .el-overlay-dialog .el-dialog .el-dialog__body .el-row .img-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.page-images-wall .el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info {
|
||||
background-color: #25262b;
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
.page-images-wall .el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .info-line {
|
||||
width: 100%;
|
||||
}
|
||||
.page-images-wall .el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .info-line .prompt {
|
||||
background-color: #35363b;
|
||||
padding: 10px;
|
||||
color: #999;
|
||||
overflow: auto;
|
||||
max-height: 100px;
|
||||
min-height: 50px;
|
||||
position: relative;
|
||||
}
|
||||
.page-images-wall .el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .info-line .prompt .el-icon {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.page-images-wall .el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .info-line .wrapper {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
}
|
||||
.page-images-wall .el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .info-line .wrapper label {
|
||||
display: flex;
|
||||
width: 100px;
|
||||
color: #a5a5a5;
|
||||
}
|
||||
.page-images-wall .el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .info-line .wrapper .item-value {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
background-color: #35363b;
|
||||
padding: 2px 5px;
|
||||
border-radius: 5px;
|
||||
color: #f5f5f5;
|
||||
}
|
||||
.page-images-wall .el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .copy-params {
|
||||
padding: 20px 0 10px 0;
|
||||
}
|
||||
.page-images-wall .el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .copy-params .el-button {
|
||||
width: 100%;
|
||||
}
|
||||
@-moz-keyframes expandUp {
|
||||
0% {
|
||||
transform: scaleY(0);
|
||||
}
|
||||
100% {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes expandUp {
|
||||
0% {
|
||||
transform: scaleY(0);
|
||||
}
|
||||
100% {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
@-o-keyframes expandUp {
|
||||
0% {
|
||||
transform: scaleY(0);
|
||||
}
|
||||
100% {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
@keyframes expandUp {
|
||||
0% {
|
||||
transform: scaleY(0);
|
||||
}
|
||||
100% {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
117
web/src/assets/css/images-wall.styl
Normal file
@@ -0,0 +1,117 @@
|
||||
@keyframes expandUp {
|
||||
0% {
|
||||
transform: scaleY(0);
|
||||
}
|
||||
100% {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
|
||||
.page-images-wall {
|
||||
display: flex;
|
||||
background-color: #282c34;
|
||||
|
||||
.inner {
|
||||
width 100%
|
||||
color #ffffff
|
||||
overflow hidden
|
||||
|
||||
.header {
|
||||
display flex
|
||||
padding 0 40px
|
||||
|
||||
h2 {
|
||||
width 300px
|
||||
}
|
||||
|
||||
.settings {
|
||||
width 100%
|
||||
display flex
|
||||
justify-content right
|
||||
|
||||
.el-radio-group {
|
||||
font-size 16px
|
||||
|
||||
.el-radio {
|
||||
color #ffffff
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.waterfall {
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
overflow-y auto
|
||||
overflow-x hidden
|
||||
|
||||
.list-item {
|
||||
|
||||
.image {
|
||||
overflow hidden
|
||||
|
||||
.el-image {
|
||||
transition: transform 0.3s;
|
||||
cursor pointer
|
||||
}
|
||||
}
|
||||
|
||||
.prompt {
|
||||
display none
|
||||
position absolute
|
||||
bottom 0
|
||||
color #ffffff
|
||||
padding 10px 10px 20px 10px
|
||||
line-height 1.2
|
||||
background-color rgba(10, 10, 10, 0.7)
|
||||
|
||||
.el-icon {
|
||||
position absolute
|
||||
bottom 10px
|
||||
right 10px
|
||||
cursor pointer
|
||||
border 1px solid #ffffff
|
||||
border-radius 5px
|
||||
padding 2px
|
||||
font-size 12px;
|
||||
|
||||
&:hover {
|
||||
background-color #999999
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.prompt {
|
||||
display block
|
||||
animation: expandUp 0.3s ease-in-out forwards;
|
||||
transform-origin: bottom center;
|
||||
transform: scaleY(0); /* 初始状态,元素高度为0 */
|
||||
}
|
||||
|
||||
.image {
|
||||
.el-image {
|
||||
transform: scale(1.2); /* 放大图像到1.2倍大小 */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
.footer {
|
||||
display flex
|
||||
padding 20px
|
||||
align-items center
|
||||
justify-content center
|
||||
|
||||
.iconfont {
|
||||
margin-left 6px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@import "sd-task-dialog.styl"
|
||||
}
|
||||
@@ -2,115 +2,137 @@ html,
|
||||
body,
|
||||
#app,
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
.admin-home a {
|
||||
text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.admin-home .content-box {
|
||||
position: absolute;
|
||||
left: 250px;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
padding-bottom: 30px;
|
||||
-webkit-transition: left 0.3s ease-in-out;
|
||||
transition: left 0.3s ease-in-out;
|
||||
background: #f0f0f0;
|
||||
position: absolute;
|
||||
left: 250px;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
/*padding-bottom: 30px;*/
|
||||
-webkit-transition: left 0.3s ease-in-out;
|
||||
transition: left 0.3s ease-in-out;
|
||||
background: #f0f0f0;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.admin-home .content-box .content {
|
||||
width: auto;
|
||||
height: 100%;
|
||||
padding: 10px;
|
||||
overflow-y: scroll;
|
||||
box-sizing: border-box;
|
||||
/*BaseForm*/
|
||||
width: auto;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
/*BaseForm*/
|
||||
}
|
||||
|
||||
.admin-home .content-box .content .container {
|
||||
padding: 30px;
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
padding: 30px;
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.admin-home .content-box .content .container .handle-box {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.admin-home .content-box .content .crumbs {
|
||||
margin: 10px 0;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.admin-home .content-box .content .el-table th {
|
||||
background-color: #f5f7fa !important;
|
||||
background-color: #f5f7fa !important;
|
||||
}
|
||||
|
||||
.admin-home .content-box .content .pagination {
|
||||
margin: 20px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
margin: 20px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.admin-home .content-box .content .plugins-tips {
|
||||
padding: 20px 10px;
|
||||
margin-bottom: 20px;
|
||||
padding: 20px 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.admin-home .content-box .content .el-button + .el-tooltip {
|
||||
margin-left: 10px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.admin-home .content-box .content .el-table tr:hover {
|
||||
background: #f6faff;
|
||||
background: #f6faff;
|
||||
}
|
||||
|
||||
.admin-home .content-box .content .mgb20 {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.admin-home .content-box .content .move-enter-active,
|
||||
.admin-home .content-box .content .move-leave-active {
|
||||
transition: opacity 0.1s ease;
|
||||
transition: opacity 0.1s ease;
|
||||
}
|
||||
|
||||
.admin-home .content-box .content .move-enter-from,
|
||||
.admin-home .content-box .content .move-leave-to {
|
||||
opacity: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.admin-home .content-box .content .form-box {
|
||||
width: 600px;
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
.admin-home .content-box .content .form-box .line {
|
||||
text-align: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.admin-home .content-box .content .el-time-panel__content::after,
|
||||
.admin-home .content-box .content .el-time-panel__content::before {
|
||||
margin-top: -7px;
|
||||
margin-top: -7px;
|
||||
}
|
||||
|
||||
.admin-home .content-box .content .el-time-spinner__wrapper .el-scrollbar__wrap:not(.el-scrollbar__wrap--hidden-default) {
|
||||
padding-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.admin-home .content-box .content [class*=" el-icon-"],
|
||||
.admin-home .content-box .content [class^=el-icon-] {
|
||||
speak: none;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
vertical-align: baseline;
|
||||
display: inline-block;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
speak: none;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
vertical-align: baseline;
|
||||
display: inline-block;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.admin-home .content-box .content .el-sub-menu [class^=el-icon-] {
|
||||
vertical-align: middle;
|
||||
margin-right: 5px;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
vertical-align: middle;
|
||||
margin-right: 5px;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.admin-home .content-box .content [hidden] {
|
||||
display: none !important;
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.admin-home .content-collapse {
|
||||
left: 65px;
|
||||
left: 65px;
|
||||
}
|
||||
|
||||
63
web/src/assets/css/sd-task-dialog.css
Normal file
@@ -0,0 +1,63 @@
|
||||
.el-overlay-dialog .el-dialog {
|
||||
background-color: #1a1b1e;
|
||||
}
|
||||
.el-overlay-dialog .el-dialog .el-dialog__header .el-dialog__title {
|
||||
color: #f5f5f5;
|
||||
}
|
||||
.el-overlay-dialog .el-dialog .el-dialog__body {
|
||||
padding: 0 0 0 15px !important;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
.el-overlay-dialog .el-dialog .el-dialog__body .el-row {
|
||||
width: 100%;
|
||||
}
|
||||
.el-overlay-dialog .el-dialog .el-dialog__body .el-row .img-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info {
|
||||
background-color: #25262b;
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
.el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .info-line {
|
||||
width: 100%;
|
||||
}
|
||||
.el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .info-line .prompt {
|
||||
background-color: #35363b;
|
||||
padding: 10px;
|
||||
color: #999;
|
||||
overflow: auto;
|
||||
max-height: 100px;
|
||||
min-height: 50px;
|
||||
position: relative;
|
||||
}
|
||||
.el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .info-line .prompt .el-icon {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .info-line .wrapper {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
}
|
||||
.el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .info-line .wrapper label {
|
||||
display: flex;
|
||||
width: 100px;
|
||||
color: #a5a5a5;
|
||||
}
|
||||
.el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .info-line .wrapper .item-value {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
background-color: #35363b;
|
||||
padding: 2px 5px;
|
||||
border-radius: 5px;
|
||||
color: #f5f5f5;
|
||||
}
|
||||
.el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .copy-params {
|
||||
padding: 20px 0 10px 0;
|
||||
}
|
||||
.el-overlay-dialog .el-dialog .el-dialog__body .el-row .task-info .copy-params .el-button {
|
||||
width: 100%;
|
||||
}
|
||||
85
web/src/assets/css/sd-task-dialog.styl
Normal file
@@ -0,0 +1,85 @@
|
||||
.el-overlay-dialog {
|
||||
.el-dialog {
|
||||
background-color #1a1b1e
|
||||
|
||||
.el-dialog__header {
|
||||
.el-dialog__title {
|
||||
color #F5F5F5
|
||||
}
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding 0 0 0 15px !important
|
||||
display flex
|
||||
height 100%
|
||||
|
||||
.el-row {
|
||||
width 100%
|
||||
|
||||
.img-container {
|
||||
display flex
|
||||
justify-content center
|
||||
}
|
||||
|
||||
.task-info {
|
||||
background-color #25262b
|
||||
padding 1rem 1.5rem
|
||||
|
||||
.info-line {
|
||||
width 100%
|
||||
|
||||
.prompt {
|
||||
background-color #35363b
|
||||
padding 10px
|
||||
color #999999
|
||||
overflow auto
|
||||
max-height 100px
|
||||
min-height 50px
|
||||
|
||||
position relative
|
||||
|
||||
.el-icon {
|
||||
position absolute
|
||||
right 10px
|
||||
bottom 10px
|
||||
cursor pointer
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
margin-top 10px
|
||||
display flex
|
||||
|
||||
label {
|
||||
display flex
|
||||
width 100px
|
||||
color #a5a5a5
|
||||
}
|
||||
|
||||
.item-value {
|
||||
display flex
|
||||
width 100%
|
||||
background-color #35363b
|
||||
padding 2px 5px
|
||||
border-radius 5px
|
||||
color #F5F5F5
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.copy-params {
|
||||
padding 20px 0 10px 0
|
||||
|
||||
.el-button {
|
||||
width 100%
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// end el-row
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
96
web/src/assets/css/task-list.css
Normal file
@@ -0,0 +1,96 @@
|
||||
.task-list-box {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
color: #fff;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.task-list-box .running-job-list .job-item {
|
||||
width: 100%;
|
||||
padding: 2px;
|
||||
background-color: #555;
|
||||
}
|
||||
.task-list-box .running-job-list .job-item .job-item-inner {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.task-list-box .running-job-list .job-item .job-item-inner .progress {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.task-list-box .running-job-list .job-item .job-item-inner .progress span {
|
||||
font-size: 20px;
|
||||
color: #fff;
|
||||
}
|
||||
.task-list-box .finish-job-list .job-item {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.task-list-box .finish-job-list .job-item .opt .opt-line {
|
||||
margin: 6px 0;
|
||||
}
|
||||
.task-list-box .finish-job-list .job-item .opt .opt-line ul {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
}
|
||||
.task-list-box .finish-job-list .job-item .opt .opt-line ul li {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.task-list-box .finish-job-list .job-item .opt .opt-line ul li a {
|
||||
padding: 3px 0;
|
||||
width: 44px;
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
background-color: #4e5058;
|
||||
color: #fff;
|
||||
}
|
||||
.task-list-box .finish-job-list .job-item .opt .opt-line ul li a:hover {
|
||||
background-color: #6d6f78;
|
||||
}
|
||||
.task-list-box .finish-job-list .job-item .opt .opt-line ul .show-prompt {
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.task-list-box .el-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 240px;
|
||||
}
|
||||
.task-list-box .el-image img {
|
||||
height: 240px;
|
||||
}
|
||||
.task-list-box .el-image .el-image-viewer__wrapper img {
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
.task-list-box .el-image .image-slot {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
min-height: 200px;
|
||||
color: #fff;
|
||||
}
|
||||
.task-list-box .el-image .image-slot .iconfont {
|
||||
font-size: 50px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.task-list-box .el-image.upscale {
|
||||
max-height: 304px;
|
||||
}
|
||||
.task-list-box .el-image.upscale img {
|
||||
height: 304px;
|
||||
}
|
||||
.task-list-box .el-image.upscale .el-image-viewer__wrapper img {
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
129
web/src/assets/css/task-list.styl
Normal file
@@ -0,0 +1,129 @@
|
||||
.task-list-box {
|
||||
width 100%
|
||||
padding 10px
|
||||
color #ffffff
|
||||
overflow-x hidden
|
||||
|
||||
.running-job-list {
|
||||
.job-item {
|
||||
//border: 1px solid #454545;
|
||||
width: 100%;
|
||||
padding 2px
|
||||
background-color #555555
|
||||
|
||||
.job-item-inner {
|
||||
position relative
|
||||
height 100%
|
||||
overflow hidden
|
||||
|
||||
.progress {
|
||||
position absolute
|
||||
width 100%
|
||||
height 100%
|
||||
top 0
|
||||
left 0
|
||||
display flex
|
||||
justify-content center
|
||||
align-items center
|
||||
|
||||
span {
|
||||
font-size 20px
|
||||
color #ffffff
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.finish-job-list {
|
||||
.job-item {
|
||||
width 100%
|
||||
height 100%
|
||||
|
||||
.opt {
|
||||
.opt-line {
|
||||
margin 6px 0
|
||||
|
||||
ul {
|
||||
display flex
|
||||
flex-flow row
|
||||
|
||||
li {
|
||||
margin-right 10px
|
||||
|
||||
a {
|
||||
padding 3px 0
|
||||
width 44px
|
||||
text-align center
|
||||
border-radius 5px
|
||||
display block
|
||||
cursor pointer
|
||||
background-color #4E5058
|
||||
color #ffffff
|
||||
|
||||
&:hover {
|
||||
background-color #6D6F78
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.show-prompt {
|
||||
font-size 20px
|
||||
cursor pointer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.el-image {
|
||||
width 100%
|
||||
height 100%
|
||||
max-height 240px
|
||||
|
||||
img {
|
||||
height 240px
|
||||
}
|
||||
|
||||
.el-image-viewer__wrapper {
|
||||
img {
|
||||
width auto
|
||||
height auto
|
||||
}
|
||||
}
|
||||
|
||||
.image-slot {
|
||||
display flex
|
||||
flex-flow column
|
||||
justify-content center
|
||||
align-items center
|
||||
height 100%
|
||||
min-height 200px
|
||||
color #ffffff
|
||||
|
||||
.iconfont {
|
||||
font-size 50px
|
||||
margin-bottom 10px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-image.upscale {
|
||||
max-height 304px
|
||||
|
||||
img {
|
||||
height 304px
|
||||
}
|
||||
|
||||
.el-image-viewer__wrapper {
|
||||
img {
|
||||
width auto
|
||||
height auto
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 4125778 */
|
||||
src: url('iconfont.woff2?t=1694420182193') format('woff2'),
|
||||
url('iconfont.woff?t=1694420182193') format('woff'),
|
||||
url('iconfont.ttf?t=1694420182193') format('truetype');
|
||||
src: url('iconfont.woff2?t=1697164072791') format('woff2'),
|
||||
url('iconfont.woff?t=1697164072791') format('woff'),
|
||||
url('iconfont.ttf?t=1697164072791') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
@@ -13,6 +13,10 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-face:before {
|
||||
content: "\e64b";
|
||||
}
|
||||
|
||||
.icon-book:before {
|
||||
content: "\e622";
|
||||
}
|
||||
|
||||
@@ -5,6 +5,13 @@
|
||||
"css_prefix_text": "icon-",
|
||||
"description": "",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "845789",
|
||||
"name": "笑脸",
|
||||
"font_class": "face",
|
||||
"unicode": "e64b",
|
||||
"unicode_decimal": 58955
|
||||
},
|
||||
{
|
||||
"icon_id": "11836501",
|
||||
"name": "知识库",
|
||||
|
||||
@@ -33,6 +33,8 @@ import {
|
||||
Uploader
|
||||
} from "vant";
|
||||
import router from "@/router";
|
||||
import 'v3-waterfall/dist/style.css'
|
||||
import V3waterfall from "v3-waterfall";
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(createPinia())
|
||||
@@ -62,6 +64,7 @@ app.use(ShareSheet);
|
||||
app.use(Switch);
|
||||
app.use(Uploader);
|
||||
app.use(Tag);
|
||||
app.use(V3waterfall)
|
||||
app.use(router).use(ElementPlus).mount('#app')
|
||||
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ const routes = [
|
||||
},
|
||||
{
|
||||
name: 'image-sd',
|
||||
path: '/sd',
|
||||
path: '/sd/',
|
||||
meta: {title: 'Stable Diffusion 绘画中心'},
|
||||
component: () => import('@/views/ImageSd.vue'),
|
||||
},
|
||||
@@ -40,9 +40,9 @@ const routes = [
|
||||
},
|
||||
{
|
||||
name: 'images',
|
||||
path: '/images',
|
||||
meta: {title: '绘画社区'},
|
||||
component: () => import('@/views/Images.vue'),
|
||||
path: '/images-wall',
|
||||
meta: {title: '作品展示'},
|
||||
component: () => import('@/views/ImagesWall.vue'),
|
||||
},
|
||||
{
|
||||
name: 'user-invitation',
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
@click="changeChat(chat)">
|
||||
<el-image :src="chat.icon" class="avatar"/>
|
||||
<span class="chat-title-input" v-if="chat.edit">
|
||||
<el-input v-model="tmpChatTitle" size="small" placeholder="请输入会话标题"/>
|
||||
<el-input v-model="tmpChatTitle" size="small" @keydown="titleKeydown($event, chat)"
|
||||
placeholder="请输入会话标题"/>
|
||||
</span>
|
||||
<span v-else class="chat-title">{{ chat.title }}</span>
|
||||
<span class="btn btn-check" v-if="chat.edit || chat.removing">
|
||||
@@ -451,16 +452,24 @@ const editChatTitle = function (event, chat) {
|
||||
tmpChatTitle.value = chat.title;
|
||||
};
|
||||
|
||||
|
||||
const titleKeydown = (e, chat) => {
|
||||
if (e.keyCode === 13) {
|
||||
e.stopPropagation();
|
||||
confirm(e, chat)
|
||||
}
|
||||
}
|
||||
// 确认修改
|
||||
const confirm = function (event, chat) {
|
||||
event.stopPropagation();
|
||||
if (curOpt.value === 'edit') {
|
||||
if (tmpChatTitle.value === '') {
|
||||
ElMessage.error("请输入会话标题!");
|
||||
return;
|
||||
return ElMessage.error("请输入会话标题!");
|
||||
}
|
||||
|
||||
httpPost('/api/chat/update', {id: chat.id, title: tmpChatTitle.value}).then(() => {
|
||||
if (!chat.chat_id) {
|
||||
return ElMessage.error("对话 ID 为空,请刷新页面再试!");
|
||||
}
|
||||
httpPost('/api/chat/update', {chat_id: chat.chat_id, title: tmpChatTitle.value}).then(() => {
|
||||
chat.title = tmpChatTitle.value;
|
||||
chat.edit = false;
|
||||
}).catch(e => {
|
||||
|
||||
@@ -45,7 +45,7 @@ const navs = ref([
|
||||
{path: "/mj", icon: "image", title: "MJ 绘画"},
|
||||
{path: "/sd", icon: "palette", title: "SD 绘画"},
|
||||
{path: "/apps", icon: "menu", title: "应用中心"},
|
||||
{path: "/images", icon: "image-list", title: "绘画社区"},
|
||||
{path: "/images-wall", icon: "image-list", title: "作品展示"},
|
||||
{path: "/knowledge", icon: "book", title: "我的知识库"},
|
||||
{path: "/member", icon: "vip-user", title: "会员计划"},
|
||||
{path: "/invite", icon: "share", title: "推广计划"},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="page-mj">
|
||||
<div class="inner">
|
||||
<div class="inner custom-scroll">
|
||||
<div class="mj-box">
|
||||
<h2>MidJourney 创作中心</h2>
|
||||
|
||||
@@ -379,7 +379,6 @@ import {getSessionId, getUserToken} from "@/store/session";
|
||||
|
||||
const listBoxHeight = ref(window.innerHeight - 40)
|
||||
const mjBoxHeight = ref(window.innerHeight - 150)
|
||||
|
||||
window.onresize = () => {
|
||||
listBoxHeight.value = window.innerHeight - 40
|
||||
mjBoxHeight.value = window.innerHeight - 150
|
||||
@@ -476,14 +475,14 @@ onMounted(() => {
|
||||
checkSession().then(user => {
|
||||
imgCalls.value = user['img_calls']
|
||||
// 获取运行中的任务
|
||||
httpGet("/api/mj/jobs?status=0").then(res => {
|
||||
httpGet(`/api/mj/jobs?status=0&user_id=${user['id']}`).then(res => {
|
||||
runningJobs.value = res.data
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取任务失败:" + e.message)
|
||||
})
|
||||
|
||||
// 获取运行中的任务
|
||||
httpGet("/api/mj/jobs?status=1").then(res => {
|
||||
httpGet(`/api/mj/jobs?status=1&user_id=${user['id']}`).then(res => {
|
||||
finishedJobs.value = res.data
|
||||
previewImgList.value = []
|
||||
for (let index in finishedJobs.value) {
|
||||
@@ -601,4 +600,5 @@ const send = (url, index, item) => {
|
||||
|
||||
<style lang="stylus">
|
||||
@import "@/assets/css/image-mj.styl"
|
||||
@import "@/assets/css/custom-scroll.styl"
|
||||
</style>
|
||||
|
||||
@@ -539,6 +539,11 @@ const runningJobs = ref([])
|
||||
const finishedJobs = ref([])
|
||||
const previewImgList = ref([])
|
||||
const router = useRouter()
|
||||
// 检查是否有画同款的参数
|
||||
const _params = router.currentRoute.value.params["copyParams"]
|
||||
if (_params) {
|
||||
params.value = JSON.parse(_params)
|
||||
}
|
||||
|
||||
const socket = ref(null)
|
||||
const imgCalls = ref(0)
|
||||
@@ -681,4 +686,5 @@ const copyParams = (row) => {
|
||||
|
||||
<style lang="stylus">
|
||||
@import "@/assets/css/image-sd.styl"
|
||||
@import "@/assets/css/custom-scroll.styl"
|
||||
</style>
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
<template>
|
||||
<div class="page-images" :style="{ height: winHeight + 'px' }">
|
||||
<div class="inner">
|
||||
<h1>绘画作品广场</h1>
|
||||
<h2>页面正在紧锣密鼓开发中,敬请期待!</h2>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from "vue"
|
||||
|
||||
const winHeight = ref(window.innerHeight)
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.page-images {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items center
|
||||
background-color: #282c34;
|
||||
|
||||
.inner {
|
||||
text-align center
|
||||
|
||||
h1 {
|
||||
color: #202020;
|
||||
font-size: 80px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 0.1em;
|
||||
text-shadow: -1px -1px 1px #111111, 2px 2px 1px #363636;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color #ffffff;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
292
web/src/views/ImagesWall.vue
Normal file
@@ -0,0 +1,292 @@
|
||||
<template>
|
||||
<div class="page-images-wall">
|
||||
<div class="inner custom-scroll">
|
||||
<div class="header">
|
||||
<h2>AI 绘画作品墙</h2>
|
||||
<div class="settings">
|
||||
<el-radio-group v-model="imgType" @change="changeImgType">
|
||||
<el-radio label="mj" size="large">MidJourney</el-radio>
|
||||
<el-radio label="sd" size="large">Stable Diffusion</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
<div class="waterfall" :style="{ height:listBoxHeight + 'px' }" id="waterfall-box">
|
||||
<v3-waterfall id="waterfall" :list="list" srcKey="img_url"
|
||||
:gap="12"
|
||||
:bottomGap="-5"
|
||||
:colWidth="colWidth"
|
||||
:distanceToScroll="100"
|
||||
:isLoading="loading"
|
||||
:isOver="false"
|
||||
@scrollReachBottom="getNext">
|
||||
<template #default="slotProp">
|
||||
<div class="list-item">
|
||||
<div class="image" v-if="imgType === 'mj'">
|
||||
<el-image :src="slotProp.item['img_url']+'?imageView2/4/w/300/q/75'"
|
||||
: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="image" v-else>
|
||||
<el-image :src="slotProp.item['img_url']+'?imageView2/4/w/300/q/75'" loading="lazy"
|
||||
@click="showTask(slotProp.item)">
|
||||
<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="prompt">
|
||||
<span>{{ slotProp.item.prompt }}</span>
|
||||
<el-icon class="copy-prompt" :data-clipboard-text="slotProp.item.prompt">
|
||||
<DocumentCopy/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</v3-waterfall>
|
||||
|
||||
<div class="footer" v-if="isOver">
|
||||
<span>没有更多数据了</span>
|
||||
<i class="iconfont icon-face"></i>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- 任务详情弹框 -->
|
||||
<el-dialog v-model="showTaskDialog" title="绘画任务详情" :fullscreen="true">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="16">
|
||||
<div class="img-container" :style="{maxHeight: fullImgHeight+'px'}">
|
||||
<el-image :src="item['img_url']" fit="contain"/>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div class="task-info">
|
||||
<div class="info-line">
|
||||
<el-divider>
|
||||
正向提示词
|
||||
</el-divider>
|
||||
<div class="prompt">
|
||||
<span>{{ item.prompt }}</span>
|
||||
<el-icon class="copy-prompt" :data-clipboard-text="item.prompt">
|
||||
<DocumentCopy/>
|
||||
</el-icon>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="info-line">
|
||||
<el-divider>
|
||||
反向提示词
|
||||
</el-divider>
|
||||
<div class="prompt">
|
||||
<span>{{ item.params.negative_prompt }}</span>
|
||||
<el-icon class="copy-prompt" :data-clipboard-text="item.params.negative_prompt">
|
||||
<DocumentCopy/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-line">
|
||||
<div class="wrapper">
|
||||
<label>采样方法:</label>
|
||||
<div class="item-value">{{ item.params.sampler }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-line">
|
||||
<div class="wrapper">
|
||||
<label>图片尺寸:</label>
|
||||
<div class="item-value">{{ item.params.width }} x {{ item.params.height }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-line">
|
||||
<div class="wrapper">
|
||||
<label>迭代步数:</label>
|
||||
<div class="item-value">{{ item.params.steps }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-line">
|
||||
<div class="wrapper">
|
||||
<label>引导系数:</label>
|
||||
<div class="item-value">{{ item.params.cfg_scale }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-line">
|
||||
<div class="wrapper">
|
||||
<label>随机因子:</label>
|
||||
<div class="item-value">{{ item.params.seed }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="item.params.hd_fix">
|
||||
<el-divider>
|
||||
高清修复
|
||||
</el-divider>
|
||||
<div class="info-line">
|
||||
<div class="wrapper">
|
||||
<label>重绘幅度:</label>
|
||||
<div class="item-value">{{ item.params.hd_redraw_rate }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-line">
|
||||
<div class="wrapper">
|
||||
<label>放大算法:</label>
|
||||
<div class="item-value">{{ item.params.hd_scale_alg }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-line">
|
||||
<div class="wrapper">
|
||||
<label>放大倍数:</label>
|
||||
<div class="item-value">{{ item.params.hd_scale }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-line">
|
||||
<div class="wrapper">
|
||||
<label>迭代步数:</label>
|
||||
<div class="item-value">{{ item.params.hd_steps }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="copy-params">
|
||||
<el-button type="primary" round @click="copyParams(item)">画一张同款的</el-button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {nextTick, onMounted, ref} from "vue"
|
||||
import {DocumentCopy, Picture} from "@element-plus/icons-vue";
|
||||
import {httpGet} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import Clipboard from "clipboard";
|
||||
import {useRouter} from "vue-router";
|
||||
|
||||
const list = ref([])
|
||||
const loading = ref(true)
|
||||
const isOver = ref(false)
|
||||
const imgType = ref("mj") // 图片类别
|
||||
const listBoxHeight = window.innerHeight - 71
|
||||
const colWidth = ref(240)
|
||||
const fullImgHeight = ref(window.innerHeight - 60)
|
||||
const showTaskDialog = ref(false)
|
||||
const item = 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 page = ref(0)
|
||||
const pageSize = ref(20)
|
||||
// 获取下一页数据
|
||||
const getNext = () => {
|
||||
if (isOver.value) {
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
page.value = page.value + 1
|
||||
const url = imgType.value === "mj" ? "/api/mj/jobs" : "/api/sd/jobs"
|
||||
// 获取运行中的任务
|
||||
httpGet(`${url}?status=1&page=${page.value}&page_size=${pageSize.value}`).then(res => {
|
||||
loading.value = false
|
||||
if (list.value.length === 0) {
|
||||
list.value = res.data
|
||||
return
|
||||
}
|
||||
|
||||
if (res.data.length < pageSize.value) {
|
||||
isOver.value = true
|
||||
}
|
||||
list.value = list.value.concat(res.data)
|
||||
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取图片失败:" + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
getNext()
|
||||
|
||||
onMounted(() => {
|
||||
const clipboard = new Clipboard('.copy-prompt');
|
||||
clipboard.on('success', () => {
|
||||
ElMessage.success({message: "复制成功!", duration: 500});
|
||||
})
|
||||
|
||||
clipboard.on('error', () => {
|
||||
ElMessage.error('复制失败!');
|
||||
})
|
||||
})
|
||||
|
||||
const changeImgType = () => {
|
||||
document.getElementById('waterfall-box').scrollTo(0, 0)
|
||||
page.value = 0
|
||||
list.value = []
|
||||
isOver.value = false
|
||||
nextTick(() => getNext())
|
||||
}
|
||||
|
||||
const showTask = (row) => {
|
||||
item.value = row
|
||||
showTaskDialog.value = true
|
||||
}
|
||||
|
||||
|
||||
const router = useRouter()
|
||||
const copyParams = (row) => {
|
||||
router.push({name: "image-sd", params: {copyParams: JSON.stringify(row.params)}})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@import "@/assets/css/images-wall.styl"
|
||||
@import "@/assets/css/custom-scroll.styl"
|
||||
</style>
|
||||
@@ -39,12 +39,19 @@
|
||||
<el-dialog
|
||||
v-model="showDialog"
|
||||
:title="title"
|
||||
style="width: 90%; max-width: 600px;"
|
||||
>
|
||||
<el-alert
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
style="margin-bottom: 10px; font-size:14px;">
|
||||
<p><b>注意:</b>如果是百度文心一言平台,需要用竖线(|)将 API Key 和 Secret Key 串接起来填入!</p>
|
||||
<p><b>注意:</b>如果是讯飞星火大模型,需要用竖线(|)将 APPID, APIKey 和 APISecret 按照顺序串接起来填入!</p>
|
||||
</el-alert>
|
||||
<el-form :model="item" label-width="120px" ref="formRef" :rules="rules">
|
||||
<el-form-item label="所属平台:" prop="platform">
|
||||
<el-select v-model="item.platform" placeholder="请选择平台">
|
||||
<el-option v-for="item in platforms" :value="item" :key="item">{{ item }}</el-option>
|
||||
<el-option v-for="item in platforms" :value="item.value" :key="item.value">{{ item.name }}</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
@@ -82,7 +89,13 @@ const rules = reactive({
|
||||
const loading = ref(true)
|
||||
const formRef = ref(null)
|
||||
const title = ref("")
|
||||
const platforms = ref(["Azure", "OpenAI", "ChatGLM"])
|
||||
const platforms = ref([
|
||||
{name: "【OpenAI】ChatGPT", value: "OpenAI"},
|
||||
{name: "【讯飞】星火大模型", value: "XunFei"},
|
||||
{name: "【清华智普】ChatGLM", value: "ChatGLM"},
|
||||
{name: "【百度】文心一言", value: "Baidu"},
|
||||
{name: "【微软】Azure", value: "Azure"},
|
||||
])
|
||||
|
||||
// 获取数据
|
||||
httpGet('/api/admin/apikey/list').then((res) => {
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
<el-form :model="item" label-width="120px" ref="formRef" :rules="rules">
|
||||
<el-form-item label="所属平台:" prop="platform">
|
||||
<el-select v-model="item.platform" placeholder="请选择平台">
|
||||
<el-option v-for="item in platforms" :value="item" :key="item">{{ item }}</el-option>
|
||||
<el-option v-for="item in platforms" :value="item.value" :key="item.value">{{ item.name }}</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
@@ -94,7 +94,13 @@ const rules = reactive({
|
||||
})
|
||||
const loading = ref(true)
|
||||
const formRef = ref(null)
|
||||
const platforms = ref(["Azure", "OpenAI", "ChatGLM"])
|
||||
const platforms = ref([
|
||||
{name: "【OpenAI】ChatGPT", value: "OpenAI"},
|
||||
{name: "【讯飞】星火大模型", value: "XunFei"},
|
||||
{name: "【清华智普】ChatGLM", value: "ChatGLM"},
|
||||
{name: "【百度】文心一言", value: "Baidu"},
|
||||
{name: "【微软】Azure", value: "Azure"},
|
||||
])
|
||||
|
||||
// 获取数据
|
||||
httpGet('/api/admin/model/list').then((res) => {
|
||||
|
||||
@@ -18,12 +18,46 @@
|
||||
<el-form-item label="开放注册服务" prop="enabled_register">
|
||||
<el-switch v-model="system['enabled_register']"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="短信验证服务" prop="enabled_msg">
|
||||
<el-form-item label="短信服务" prop="enabled_msg">
|
||||
<el-switch v-model="system['enabled_msg']"/>
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content="是否在注册时候开启短信验证码服务"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
<el-form-item label="开放AI绘画" prop="enabled_draw">
|
||||
<el-form-item label="启用函数功能" prop="enabled_function">
|
||||
<el-switch v-model="system['enabled_function']"/>
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content="是否在AI对话时启用函数功能"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
<el-form-item label="启用AI绘画" prop="enabled_draw">
|
||||
<el-switch v-model="system['enabled_draw']"/>
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content="需要开启函数功能此配置才会生效"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="收款二维码" prop="reward_img">
|
||||
<el-input v-model="system['reward_img']" placeholder="众筹收款二维码地址">
|
||||
<template #append>
|
||||
@@ -57,7 +91,7 @@
|
||||
<el-input-number v-model="chat['context_deep']" :min="0" :max="10"/>
|
||||
<div class="tip" style="margin-top: 10px;">会话上下文深度:在老会话中继续会话,默认加载多少条聊天记录作为上下文。如果设置为
|
||||
0
|
||||
则不加载聊天记录,仅仅使用当前角色的上下文。该配置参数最好设置为 2 的整数倍。
|
||||
则不加载聊天记录,仅仅使用当前角色的上下文。该配置参数最好设置需要为偶数,否则将无法兼容百度的 API。
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
@@ -90,13 +124,37 @@
|
||||
<el-input v-model="chat['chat_gml']['api_url']" placeholder="支持变量,{model} => 模型名称"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="模型创意度">
|
||||
<el-slider v-model="chat['chat_gml']['temperature']" :max="2" :step="0.1"/>
|
||||
<el-slider v-model="chat['chat_gml']['temperature']" :max="1" :step="0.01"/>
|
||||
<div class="tip">值越大 AI 回答越发散,值越小回答越保守,建议保持默认值</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="最大响应长度">
|
||||
<el-input v-model.number="chat['chat_gml']['max_tokens']" placeholder="回复的最大字数,最大4096"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="center">文心一言</el-divider>
|
||||
<el-form-item label="API 地址" prop="baidu.api_url">
|
||||
<el-input v-model="chat['baidu']['api_url']" placeholder="支持变量,{model} => 模型名称"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="模型创意度">
|
||||
<el-slider v-model="chat['baidu']['temperature']" :max="1" :step="0.01"/>
|
||||
<div class="tip">值越大 AI 回答越发散,值越小回答越保守,建议保持默认值</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="最大响应长度">
|
||||
<el-input v-model.number="chat['baidu']['max_tokens']" placeholder="回复的最大字数,最大4096"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="center">讯飞星火</el-divider>
|
||||
<el-form-item label="API 地址" prop="xun_fei.api_url">
|
||||
<el-input v-model="chat['xun_fei']['api_url']" placeholder="支持变量,{model} => 模型名称"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="模型创意度">
|
||||
<el-slider v-model="chat['xun_fei']['temperature']" :max="1" :step="0.1"/>
|
||||
<div class="tip">值越大 AI 回答越发散,值越小回答越保守,建议保持默认值</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="最大响应长度">
|
||||
<el-input v-model.number="chat['xun_fei']['max_tokens']" placeholder="回复的最大字数,最大4096"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item style="text-align: right">
|
||||
<el-button type="primary" @click="save('chat')">保存</el-button>
|
||||
</el-form-item>
|
||||
@@ -110,13 +168,15 @@ import {onMounted, reactive, ref} from "vue";
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import Compressor from "compressorjs";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {UploadFilled} from "@element-plus/icons-vue";
|
||||
import {InfoFilled, UploadFilled} from "@element-plus/icons-vue";
|
||||
|
||||
const system = ref({models: []})
|
||||
const chat = ref({
|
||||
open_ai: {api_url: "", temperature: 1, max_tokens: 1024},
|
||||
azure: {api_url: "", temperature: 1, max_tokens: 1024},
|
||||
chat_gml: {api_url: "", temperature: 1, max_tokens: 1024},
|
||||
chat_gml: {api_url: "", temperature: 0.95, max_tokens: 1024},
|
||||
baidu: {api_url: "", temperature: 0.95, max_tokens: 1024},
|
||||
xun_fei: {api_url: "", temperature: 0.5, max_tokens: 1024},
|
||||
context_deep: 0,
|
||||
enable_context: true,
|
||||
enable_history: true,
|
||||
@@ -145,6 +205,12 @@ onMounted(() => {
|
||||
if (res.data.chat_gml) {
|
||||
chat.value.chat_gml = res.data.chat_gml
|
||||
}
|
||||
if (res.data.baidu) {
|
||||
chat.value.baidu = res.data.baidu
|
||||
}
|
||||
if (res.data.xun_fei) {
|
||||
chat.value.xun_fei = res.data.xun_fei
|
||||
}
|
||||
chat.value.context_deep = res.data.context_deep
|
||||
chat.value.enable_context = res.data.enable_context
|
||||
chat.value.enable_history = res.data.enable_history
|
||||
@@ -160,9 +226,6 @@ const rules = reactive({
|
||||
admin_title: [{required: true, message: '请输入控制台标题', trigger: 'blur',}],
|
||||
user_init_calls: [{required: true, message: '请输入赠送对话次数', trigger: 'blur'}],
|
||||
user_img_calls: [{required: true, message: '请输入赠送绘图次数', trigger: 'blur'}],
|
||||
open_ai: {api_url: [{required: true, message: '请输入 API URL', trigger: 'blur'}]},
|
||||
azure: {api_url: [{required: true, message: '请输入 API URL', trigger: 'blur'}]},
|
||||
chat_gml: {api_url: [{required: true, message: '请输入 API URL', trigger: 'blur'}]},
|
||||
})
|
||||
const save = function (key) {
|
||||
if (key === 'system') {
|
||||
@@ -176,6 +239,9 @@ const save = function (key) {
|
||||
}
|
||||
})
|
||||
} else if (key === 'chat') {
|
||||
if (chat.value.context_deep % 2 !== 0) {
|
||||
return ElMessage.error("会话上下文深度必须为偶数!")
|
||||
}
|
||||
chatFormRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
httpPost('/api/admin/config/update', {key: key, config: chat.value}).then(() => {
|
||||
@@ -234,6 +300,12 @@ const uploadRewardImg = (file) => {
|
||||
line-height 1.5;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size 16px
|
||||
margin-left 10px
|
||||
cursor pointer
|
||||
}
|
||||
|
||||
.uploader-icon {
|
||||
font-size 24px
|
||||
position relative
|
||||
|
||||