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

|

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

|

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

|

|
||||||

|

|
||||||

|

|
||||||
|

|
||||||
|
|
||||||
### 7. 体验地址
|
### 7. 体验地址
|
||||||
|
|
||||||
@@ -101,6 +102,10 @@ ChatGPT 的服务。
|
|||||||
* Github 地址:https://github.com/yangjian102621/chatgpt-plus
|
* Github 地址:https://github.com/yangjian102621/chatgpt-plus
|
||||||
* 码云地址:https://gitee.com/blackfox/chatgpt-plus
|
* 码云地址:https://gitee.com/blackfox/chatgpt-plus
|
||||||
|
|
||||||
|
## 客户端下载
|
||||||
|
|
||||||
|
目前已经支持 Win/Linux/Mac/Android 客户端,下载地址为:https://github.com/yangjian102621/chatgpt-plus/releases/tag/v3.1.2
|
||||||
|
|
||||||
## TODOLIST
|
## TODOLIST
|
||||||
|
|
||||||
* [x] 整合 Midjourney AI 绘画 API
|
* [x] 整合 Midjourney AI 绘画 API
|
||||||
@@ -128,7 +133,7 @@ cd docker/mysql
|
|||||||
# 创建 mysql 容器
|
# 创建 mysql 容器
|
||||||
docker-compose up -d
|
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 服务,那么你只需手动导入数据库即可。
|
如果你本地已经安装了 MySQL 服务,那么你只需手动导入数据库即可。
|
||||||
@@ -216,8 +221,11 @@ WeChatBot = false # 是否启动微信机器人
|
|||||||
```
|
```
|
||||||
|
|
||||||
> 1. 如果你不知道如何获取 Discord 用户 Token 和 Bot Token
|
> 1. 如果你不知道如何获取 Discord 用户 Token 和 Bot Token
|
||||||
请查参考 [Midjourney|如何集成到自己的平台](https://zhuanlan.zhihu.com/p/631079476)。
|
请查参考 [Midjourney|如何集成到自己的平台](https://zhuanlan.zhihu.com/p/631079476)。
|
||||||
> 2. `Txt2ImgJsonPath` 的默认用的是使用最广泛的 [stable-diffusion-webui](https://github.com/AUTOMATIC1111/stable-diffusion-webui) 项目的 API,如果你用的是其他版本,比如秋叶的懒人包部署的,那么请将对应的 text2img 的参数报文复制放在 `res/text2img.json` 文件中即可。
|
> 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 地址。
|
修改 nginx 配置文档 `docker/conf/nginx/conf.d/chatgpt-plus.conf`,把后端转发的地址改成当前主机的内网 IP 地址。
|
||||||
|
|
||||||
@@ -244,6 +252,42 @@ location /static/ {
|
|||||||
|
|
||||||
### 3. 启动应用
|
### 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
|
```shell
|
||||||
cd docker
|
cd docker
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ package types
|
|||||||
|
|
||||||
// ApiRequest API 请求实体
|
// ApiRequest API 请求实体
|
||||||
type ApiRequest struct {
|
type ApiRequest struct {
|
||||||
Model string `json:"model"`
|
Model string `json:"model,omitempty"` // 兼容百度文心一言
|
||||||
Temperature float32 `json:"temperature"`
|
Temperature float32 `json:"temperature"`
|
||||||
MaxTokens int `json:"max_tokens"`
|
MaxTokens int `json:"max_tokens,omitempty"` // 兼容百度文心一言
|
||||||
Stream bool `json:"stream"`
|
Stream bool `json:"stream"`
|
||||||
Messages []interface{} `json:"messages,omitempty"`
|
Messages []interface{} `json:"messages,omitempty"`
|
||||||
Prompt []interface{} `json:"prompt,omitempty"` // 兼容 ChatGLM
|
Prompt []interface{} `json:"prompt,omitempty"` // 兼容 ChatGLM
|
||||||
@@ -67,4 +67,10 @@ var ModelToTokens = map[string]int{
|
|||||||
"gpt-3.5-turbo-16k": 16384,
|
"gpt-3.5-turbo-16k": 16384,
|
||||||
"gpt-4": 8192,
|
"gpt-4": 8192,
|
||||||
"gpt-4-32k": 32768,
|
"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)
|
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) {
|
func (wc *WsClient) Receive() (int, []byte, error) {
|
||||||
if wc.Closed {
|
if wc.Closed {
|
||||||
return 0, nil, ErrConClosed
|
return 0, nil, ErrConClosed
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ type ChatConfig struct {
|
|||||||
OpenAI ModelAPIConfig `json:"open_ai"`
|
OpenAI ModelAPIConfig `json:"open_ai"`
|
||||||
Azure ModelAPIConfig `json:"azure"`
|
Azure ModelAPIConfig `json:"azure"`
|
||||||
ChatGML ModelAPIConfig `json:"chat_gml"`
|
ChatGML ModelAPIConfig `json:"chat_gml"`
|
||||||
|
Baidu ModelAPIConfig `json:"baidu"`
|
||||||
|
XunFei ModelAPIConfig `json:"xun_fei"`
|
||||||
|
|
||||||
EnableContext bool `json:"enable_context"` // 是否开启聊天上下文
|
EnableContext bool `json:"enable_context"` // 是否开启聊天上下文
|
||||||
EnableHistory bool `json:"enable_history"` // 是否允许保存聊天记录
|
EnableHistory bool `json:"enable_history"` // 是否允许保存聊天记录
|
||||||
@@ -90,6 +92,8 @@ type Platform string
|
|||||||
const OpenAI = Platform("OpenAI")
|
const OpenAI = Platform("OpenAI")
|
||||||
const Azure = Platform("Azure")
|
const Azure = Platform("Azure")
|
||||||
const ChatGLM = Platform("ChatGLM")
|
const ChatGLM = Platform("ChatGLM")
|
||||||
|
const Baidu = Platform("Baidu")
|
||||||
|
const XunFei = Platform("XunFei")
|
||||||
|
|
||||||
// UserChatConfig 用户的聊天配置
|
// UserChatConfig 用户的聊天配置
|
||||||
type UserChatConfig struct {
|
type UserChatConfig struct {
|
||||||
@@ -111,7 +115,8 @@ type SystemConfig struct {
|
|||||||
InitImgCalls int `json:"init_img_calls"`
|
InitImgCalls int `json:"init_img_calls"`
|
||||||
VipMonthCalls int `json:"vip_month_calls"` // 会员每个赠送的调用次数
|
VipMonthCalls int `json:"vip_month_calls"` // 会员每个赠送的调用次数
|
||||||
EnabledRegister bool `json:"enabled_register"`
|
EnabledRegister bool `json:"enabled_register"`
|
||||||
EnabledMsg bool `json:"enabled_msg"` // 启用短信验证码服务
|
EnabledMsg bool `json:"enabled_msg"` // 启用短信验证码服务
|
||||||
EnabledDraw bool `json:"enabled_draw"` // 启动 AI 绘画功能
|
EnabledDraw bool `json:"enabled_draw"` // 启动 AI 绘画功能
|
||||||
RewardImg string `json:"reward_img"` // 众筹收款二维码地址
|
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{}
|
apiKey := model.ApiKey{}
|
||||||
if data.Id > 0 {
|
if data.Id > 0 {
|
||||||
h.db.Find(&apiKey)
|
h.db.Find(&apiKey, data.Id)
|
||||||
}
|
}
|
||||||
apiKey.Platform = data.Platform
|
apiKey.Platform = data.Platform
|
||||||
apiKey.Value = data.Value
|
apiKey.Value = data.Value
|
||||||
res := h.db.Save(&apiKey)
|
res := h.db.Debug().Save(&apiKey)
|
||||||
if res.Error != nil {
|
if res.Error != nil {
|
||||||
resp.ERROR(c, "更新数据库失败!")
|
resp.ERROR(c, "更新数据库失败!")
|
||||||
return
|
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 (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
@@ -16,7 +16,8 @@ import (
|
|||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 将消息发送给 Azure API 并获取结果,通过 WebSocket 推送到客户端
|
// 微软 Azure 模型消息发送实现
|
||||||
|
|
||||||
func (h *ChatHandler) sendAzureMessage(
|
func (h *ChatHandler) sendAzureMessage(
|
||||||
chatCtx []interface{},
|
chatCtx []interface{},
|
||||||
req types.ApiRequest,
|
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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"chatplus/core"
|
"chatplus/core"
|
||||||
"chatplus/core/types"
|
"chatplus/core/types"
|
||||||
|
"chatplus/handler"
|
||||||
|
logger2 "chatplus/logger"
|
||||||
"chatplus/service/mj"
|
"chatplus/service/mj"
|
||||||
"chatplus/store"
|
"chatplus/store"
|
||||||
"chatplus/store/model"
|
"chatplus/store/model"
|
||||||
@@ -26,8 +28,10 @@ import (
|
|||||||
|
|
||||||
const ErrorMsg = "抱歉,AI 助手开小差了,请稍后再试。"
|
const ErrorMsg = "抱歉,AI 助手开小差了,请稍后再试。"
|
||||||
|
|
||||||
|
var logger = logger2.GetLogger()
|
||||||
|
|
||||||
type ChatHandler struct {
|
type ChatHandler struct {
|
||||||
BaseHandler
|
handler.BaseHandler
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
leveldb *store.LevelDB
|
leveldb *store.LevelDB
|
||||||
redis *redis.Client
|
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 {
|
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}
|
h := ChatHandler{
|
||||||
handler.App = app
|
db: db,
|
||||||
return &handler
|
leveldb: levelDB,
|
||||||
|
redis: redis,
|
||||||
|
mjService: service,
|
||||||
|
}
|
||||||
|
h.App = app
|
||||||
|
return &h
|
||||||
}
|
}
|
||||||
|
|
||||||
var chatConfig types.ChatConfig
|
var chatConfig types.ChatConfig
|
||||||
@@ -123,7 +132,11 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) {
|
|||||||
logger.Error(err)
|
logger.Error(err)
|
||||||
client.Close()
|
client.Close()
|
||||||
h.App.ChatClients.Delete(sessionId)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,17 +209,30 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
|
|||||||
req.Temperature = h.App.ChatConfig.ChatGML.Temperature
|
req.Temperature = h.App.ChatConfig.ChatGML.Temperature
|
||||||
req.MaxTokens = h.App.ChatConfig.ChatGML.MaxTokens
|
req.MaxTokens = h.App.ChatConfig.ChatGML.MaxTokens
|
||||||
break
|
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.Temperature = h.App.ChatConfig.OpenAI.Temperature
|
||||||
req.MaxTokens = h.App.ChatConfig.OpenAI.MaxTokens
|
req.MaxTokens = h.App.ChatConfig.OpenAI.MaxTokens
|
||||||
var functions = make([]types.Function, 0)
|
// OpenAI 支持函数功能
|
||||||
for _, f := range types.InnerFunctions {
|
if h.App.SysConfig.EnabledFunction {
|
||||||
if !h.App.SysConfig.EnabledDraw && f.Name == types.FuncMidJourney {
|
var functions = make([]types.Function, 0)
|
||||||
continue
|
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
|
// loading recent chat history as chat context
|
||||||
if chatConfig.ContextDeep > 0 {
|
if chatConfig.ContextDeep > 0 {
|
||||||
var historyMessages []model.HistoryMessage
|
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 {
|
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] {
|
if tokens+msg.Tokens >= types.ModelToTokens[session.Model.Value] {
|
||||||
break
|
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)
|
return h.sendOpenAiMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
|
||||||
case types.ChatGLM:
|
case types.ChatGLM:
|
||||||
return h.sendChatGLMMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
|
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{
|
utils.ReplyChunkMessage(ws, types.WsMessage{
|
||||||
Type: types.WsMiddle,
|
Type: types.WsMiddle,
|
||||||
@@ -357,12 +389,36 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platf
|
|||||||
break
|
break
|
||||||
case types.ChatGLM:
|
case types.ChatGLM:
|
||||||
apiURL = strings.Replace(h.App.ChatConfig.ChatGML.ApiURL, "{model}", req.Model, 1)
|
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
|
req.Messages = nil
|
||||||
break
|
break
|
||||||
|
case types.Baidu:
|
||||||
|
apiURL = h.App.ChatConfig.Baidu.ApiURL
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
apiURL = h.App.ChatConfig.OpenAI.ApiURL
|
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 请求对象
|
// 创建 HttpClient 请求对象
|
||||||
var client *http.Client
|
var client *http.Client
|
||||||
requestBody, err := json.Marshal(req)
|
requestBody, err := json.Marshal(req)
|
||||||
@@ -387,17 +443,6 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platf
|
|||||||
} else {
|
} else {
|
||||||
client = http.DefaultClient
|
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)
|
logger.Infof("Sending %s request, KEY: %s, PROXY: %s, Model: %s", platform, *apiKey, proxyURL, req.Model)
|
||||||
switch platform {
|
switch platform {
|
||||||
case types.Azure:
|
case types.Azure:
|
||||||
@@ -411,7 +456,9 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platf
|
|||||||
logger.Info(token)
|
logger.Info(token)
|
||||||
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||||
break
|
break
|
||||||
default:
|
case types.Baidu:
|
||||||
|
request.RequestURI = ""
|
||||||
|
case types.OpenAI:
|
||||||
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiKey))
|
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiKey))
|
||||||
}
|
}
|
||||||
return client.Do(request)
|
return client.Do(request)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package handler
|
package chatimpl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"chatplus/core/types"
|
"chatplus/core/types"
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"chatplus/utils"
|
"chatplus/utils"
|
||||||
"chatplus/utils/resp"
|
"chatplus/utils/resp"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// List 获取会话列表
|
// List 获取会话列表
|
||||||
@@ -47,6 +48,95 @@ func (h *ChatHandler) List(c *gin.Context) {
|
|||||||
resp.SUCCESS(c, items)
|
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 删除会话
|
// Remove 删除会话
|
||||||
func (h *ChatHandler) Remove(c *gin.Context) {
|
func (h *ChatHandler) Remove(c *gin.Context) {
|
||||||
chatId := h.GetTrim(c, "chat_id")
|
chatId := h.GetTrim(c, "chat_id")
|
||||||
@@ -80,6 +170,7 @@ func (h *ChatHandler) Remove(c *gin.Context) {
|
|||||||
resp.SUCCESS(c, types.OkMsg)
|
resp.SUCCESS(c, types.OkMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detail 对话详情,用户导出对话
|
||||||
func (h *ChatHandler) Detail(c *gin.Context) {
|
func (h *ChatHandler) Detail(c *gin.Context) {
|
||||||
chatId := h.GetTrim(c, "chat_id")
|
chatId := h.GetTrim(c, "chat_id")
|
||||||
if utils.IsEmptyValue(chatId) {
|
if utils.IsEmptyValue(chatId) {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package handler
|
package chatimpl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
@@ -17,7 +17,8 @@ import (
|
|||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 将消息发送给 ChatGLM API 并获取结果,通过 WebSocket 推送到客户端
|
// 清华大学 ChatGML 消息发送实现
|
||||||
|
|
||||||
func (h *ChatHandler) sendChatGLMMessage(
|
func (h *ChatHandler) sendChatGLMMessage(
|
||||||
chatCtx []interface{},
|
chatCtx []interface{},
|
||||||
req types.ApiRequest,
|
req types.ApiRequest,
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package handler
|
package chatimpl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 将消息发送给 OpenAI API 并获取结果,通过 WebSocket 推送到客户端
|
// OPenAI 消息发送实现
|
||||||
func (h *ChatHandler) sendOpenAiMessage(
|
func (h *ChatHandler) sendOpenAiMessage(
|
||||||
chatCtx []interface{},
|
chatCtx []interface{},
|
||||||
req types.ApiRequest,
|
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 任务列表
|
// JobList 获取 MJ 任务列表
|
||||||
func (h *MidJourneyHandler) JobList(c *gin.Context) {
|
func (h *MidJourneyHandler) JobList(c *gin.Context) {
|
||||||
status := h.GetInt(c, "status", 0)
|
status := h.GetInt(c, "status", 0)
|
||||||
var items []model.MidJourneyJob
|
userId := h.GetInt(c, "user_id", 0)
|
||||||
var res *gorm.DB
|
page := h.GetInt(c, "page", 0)
|
||||||
userId, _ := c.Get(types.LoginUserID)
|
pageSize := h.GetInt(c, "page_size", 0)
|
||||||
|
|
||||||
|
session := h.db.Session(&gorm.Session{})
|
||||||
if status == 1 {
|
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 {
|
} 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 {
|
if res.Error != nil {
|
||||||
resp.ERROR(c, types.NoData)
|
resp.ERROR(c, types.NoData)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -163,14 +163,26 @@ func (h *SdJobHandler) Image(c *gin.Context) {
|
|||||||
// JobList 获取 MJ 任务列表
|
// JobList 获取 MJ 任务列表
|
||||||
func (h *SdJobHandler) JobList(c *gin.Context) {
|
func (h *SdJobHandler) JobList(c *gin.Context) {
|
||||||
status := h.GetInt(c, "status", 0)
|
status := h.GetInt(c, "status", 0)
|
||||||
var items []model.SdJob
|
userId := h.GetInt(c, "user_id", 0)
|
||||||
var res *gorm.DB
|
page := h.GetInt(c, "page", 0)
|
||||||
userId, _ := c.Get(types.LoginUserID)
|
pageSize := h.GetInt(c, "page_size", 0)
|
||||||
|
|
||||||
|
session := h.db.Session(&gorm.Session{})
|
||||||
if status == 1 {
|
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 {
|
} 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 {
|
if res.Error != nil {
|
||||||
resp.ERROR(c, types.NoData)
|
resp.ERROR(c, types.NoData)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"chatplus/core/types"
|
"chatplus/core/types"
|
||||||
"chatplus/handler"
|
"chatplus/handler"
|
||||||
"chatplus/handler/admin"
|
"chatplus/handler/admin"
|
||||||
|
"chatplus/handler/chatimpl"
|
||||||
logger2 "chatplus/logger"
|
logger2 "chatplus/logger"
|
||||||
"chatplus/service"
|
"chatplus/service"
|
||||||
"chatplus/service/fun"
|
"chatplus/service/fun"
|
||||||
@@ -115,7 +116,7 @@ func main() {
|
|||||||
// 创建控制器
|
// 创建控制器
|
||||||
fx.Provide(handler.NewChatRoleHandler),
|
fx.Provide(handler.NewChatRoleHandler),
|
||||||
fx.Provide(handler.NewUserHandler),
|
fx.Provide(handler.NewUserHandler),
|
||||||
fx.Provide(handler.NewChatHandler),
|
fx.Provide(chatimpl.NewChatHandler),
|
||||||
fx.Provide(handler.NewUploadHandler),
|
fx.Provide(handler.NewUploadHandler),
|
||||||
fx.Provide(handler.NewSmsHandler),
|
fx.Provide(handler.NewSmsHandler),
|
||||||
fx.Provide(handler.NewRewardHandler),
|
fx.Provide(handler.NewRewardHandler),
|
||||||
@@ -196,7 +197,7 @@ func main() {
|
|||||||
group.POST("password", h.Password)
|
group.POST("password", h.Password)
|
||||||
group.POST("bind/mobile", h.BindMobile)
|
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 := s.Engine.Group("/api/chat/")
|
||||||
group.Any("new", h.ChatHandle)
|
group.Any("new", h.ChatHandle)
|
||||||
group.GET("list", h.List)
|
group.GET("list", h.List)
|
||||||
|
|||||||
@@ -1,14 +1,5 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
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:
|
services:
|
||||||
# 后端 API 程序
|
# 后端 API 程序
|
||||||
chatgpt-plus-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
|
container_name: chatgpt-plus-api
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
@@ -19,7 +19,7 @@ services:
|
|||||||
|
|
||||||
# 前端应用
|
# 前端应用
|
||||||
chatgpt-plus-web:
|
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
|
container_name: chatgpt-plus-web
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
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",
|
"pinia": "^2.1.4",
|
||||||
"qs": "^6.11.1",
|
"qs": "^6.11.1",
|
||||||
"sortablejs": "^1.15.0",
|
"sortablejs": "^1.15.0",
|
||||||
|
"v3-waterfall": "^1.2.1",
|
||||||
"vant": "^4.5.0",
|
"vant": "^4.5.0",
|
||||||
"vue": "^3.2.13",
|
"vue": "^3.2.13",
|
||||||
"vue-router": "^4.0.15"
|
"vue-router": "^4.0.15"
|
||||||
@@ -10459,6 +10460,11 @@
|
|||||||
"uuid": "dist/bin/uuid"
|
"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": {
|
"node_modules/v8-compile-cache": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmmirror.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
|
"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==",
|
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||||
"dev": true
|
"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": {
|
"v8-compile-cache": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmmirror.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
|
"resolved": "https://registry.npmmirror.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
|
||||||
|
|||||||
@@ -17,14 +17,15 @@
|
|||||||
"good-storage": "^1.1.1",
|
"good-storage": "^1.1.1",
|
||||||
"highlight.js": "^11.7.0",
|
"highlight.js": "^11.7.0",
|
||||||
"json-bigint": "^1.0.0",
|
"json-bigint": "^1.0.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"markdown-it": "^13.0.1",
|
"markdown-it": "^13.0.1",
|
||||||
"md-editor-v3": "^2.2.1",
|
"md-editor-v3": "^2.2.1",
|
||||||
"pinia": "^2.1.4",
|
"pinia": "^2.1.4",
|
||||||
"qs": "^6.11.1",
|
"qs": "^6.11.1",
|
||||||
"sortablejs": "^1.15.0",
|
"sortablejs": "^1.15.0",
|
||||||
|
"v3-waterfall": "^1.2.1",
|
||||||
"vant": "^4.5.0",
|
"vant": "^4.5.0",
|
||||||
"vue": "^3.2.13",
|
"vue": "^3.2.13",
|
||||||
"lodash": "^4.17.21",
|
|
||||||
"vue-router": "^4.0.15"
|
"vue-router": "^4.0.15"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
width: 210px;
|
width: 190px;
|
||||||
}
|
}
|
||||||
#app .common-layout .el-aside .chat-list .content .chat-list-item .chat-title {
|
#app .common-layout .el-aside .chat-list .content .chat-list-item .chat-title {
|
||||||
color: #c1c1c1;
|
color: #c1c1c1;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ $borderColor = #4676d0;
|
|||||||
display: flex;
|
display: flex;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
padding-top: 5px;
|
padding-top: 5px;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
@@ -81,7 +82,7 @@ $borderColor = #4676d0;
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
width: 210px;
|
width: 190px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-title {
|
.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 {
|
.page-mj .inner {
|
||||||
display: flex;
|
display: flex;
|
||||||
/* 修改滚动条的颜色 */
|
|
||||||
/* 修改滚动条轨道的背景颜色 */
|
|
||||||
/* 修改滚动条的滑块颜色 */
|
|
||||||
/* 修改滚动条的滑块的悬停颜色 */
|
|
||||||
}
|
}
|
||||||
.page-mj .inner .mj-box {
|
.page-mj .inner .mj-box {
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
@@ -147,19 +143,6 @@
|
|||||||
.page-mj .inner .el-form .el-slider {
|
.page-mj .inner .el-form .el-slider {
|
||||||
width: 180px;
|
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 {
|
.page-mj .inner .task-list-box {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
|||||||
@@ -182,160 +182,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 修改滚动条的颜色 */
|
@import "task-list.styl"
|
||||||
|
|
||||||
::-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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -231,16 +231,3 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
top: 2px;
|
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 {
|
@import "task-list.styl"
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-overlay-dialog {
|
@import "sd-task-dialog.styl"
|
||||||
.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
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mj-list-item-prompt {
|
.mj-list-item-prompt {
|
||||||
.el-icon {
|
.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,
|
body,
|
||||||
#app,
|
#app,
|
||||||
.wrapper {
|
.wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
|
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-home a {
|
.admin-home a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-home .content-box {
|
.admin-home .content-box {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 250px;
|
left: 250px;
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
padding-bottom: 30px;
|
/*padding-bottom: 30px;*/
|
||||||
-webkit-transition: left 0.3s ease-in-out;
|
-webkit-transition: left 0.3s ease-in-out;
|
||||||
transition: left 0.3s ease-in-out;
|
transition: left 0.3s ease-in-out;
|
||||||
background: #f0f0f0;
|
background: #f0f0f0;
|
||||||
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-home .content-box .content {
|
.admin-home .content-box .content {
|
||||||
width: auto;
|
width: auto;
|
||||||
height: 100%;
|
padding: 10px;
|
||||||
padding: 10px;
|
box-sizing: border-box;
|
||||||
overflow-y: scroll;
|
/*BaseForm*/
|
||||||
box-sizing: border-box;
|
|
||||||
/*BaseForm*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-home .content-box .content .container {
|
.admin-home .content-box .content .container {
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-home .content-box .content .container .handle-box {
|
.admin-home .content-box .content .container .handle-box {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-home .content-box .content .crumbs {
|
.admin-home .content-box .content .crumbs {
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-home .content-box .content .el-table th {
|
.admin-home .content-box .content .el-table th {
|
||||||
background-color: #f5f7fa !important;
|
background-color: #f5f7fa !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-home .content-box .content .pagination {
|
.admin-home .content-box .content .pagination {
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-home .content-box .content .plugins-tips {
|
.admin-home .content-box .content .plugins-tips {
|
||||||
padding: 20px 10px;
|
padding: 20px 10px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-home .content-box .content .el-button + .el-tooltip {
|
.admin-home .content-box .content .el-button + .el-tooltip {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-home .content-box .content .el-table tr:hover {
|
.admin-home .content-box .content .el-table tr:hover {
|
||||||
background: #f6faff;
|
background: #f6faff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-home .content-box .content .mgb20 {
|
.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-enter-active,
|
||||||
.admin-home .content-box .content .move-leave-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-enter-from,
|
||||||
.admin-home .content-box .content .move-leave-to {
|
.admin-home .content-box .content .move-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-home .content-box .content .form-box {
|
.admin-home .content-box .content .form-box {
|
||||||
width: 600px;
|
width: 600px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-home .content-box .content .form-box .line {
|
.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::after,
|
||||||
.admin-home .content-box .content .el-time-panel__content::before {
|
.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) {
|
.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-"],
|
||||||
.admin-home .content-box .content [class^=el-icon-] {
|
.admin-home .content-box .content [class^=el-icon-] {
|
||||||
speak: none;
|
speak: none;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-variant: normal;
|
font-variant: normal;
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
vertical-align: baseline;
|
vertical-align: baseline;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-home .content-box .content .el-sub-menu [class^=el-icon-] {
|
.admin-home .content-box .content .el-sub-menu [class^=el-icon-] {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-home .content-box .content [hidden] {
|
.admin-home .content-box .content [hidden] {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-home .content-collapse {
|
.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-face {
|
||||||
font-family: "iconfont"; /* Project id 4125778 */
|
font-family: "iconfont"; /* Project id 4125778 */
|
||||||
src: url('iconfont.woff2?t=1694420182193') format('woff2'),
|
src: url('iconfont.woff2?t=1697164072791') format('woff2'),
|
||||||
url('iconfont.woff?t=1694420182193') format('woff'),
|
url('iconfont.woff?t=1697164072791') format('woff'),
|
||||||
url('iconfont.ttf?t=1694420182193') format('truetype');
|
url('iconfont.ttf?t=1697164072791') format('truetype');
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconfont {
|
.iconfont {
|
||||||
@@ -13,6 +13,10 @@
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-face:before {
|
||||||
|
content: "\e64b";
|
||||||
|
}
|
||||||
|
|
||||||
.icon-book:before {
|
.icon-book:before {
|
||||||
content: "\e622";
|
content: "\e622";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,13 @@
|
|||||||
"css_prefix_text": "icon-",
|
"css_prefix_text": "icon-",
|
||||||
"description": "",
|
"description": "",
|
||||||
"glyphs": [
|
"glyphs": [
|
||||||
|
{
|
||||||
|
"icon_id": "845789",
|
||||||
|
"name": "笑脸",
|
||||||
|
"font_class": "face",
|
||||||
|
"unicode": "e64b",
|
||||||
|
"unicode_decimal": 58955
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"icon_id": "11836501",
|
"icon_id": "11836501",
|
||||||
"name": "知识库",
|
"name": "知识库",
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ import {
|
|||||||
Uploader
|
Uploader
|
||||||
} from "vant";
|
} from "vant";
|
||||||
import router from "@/router";
|
import router from "@/router";
|
||||||
|
import 'v3-waterfall/dist/style.css'
|
||||||
|
import V3waterfall from "v3-waterfall";
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
app.use(createPinia())
|
app.use(createPinia())
|
||||||
@@ -62,6 +64,7 @@ app.use(ShareSheet);
|
|||||||
app.use(Switch);
|
app.use(Switch);
|
||||||
app.use(Uploader);
|
app.use(Uploader);
|
||||||
app.use(Tag);
|
app.use(Tag);
|
||||||
|
app.use(V3waterfall)
|
||||||
app.use(router).use(ElementPlus).mount('#app')
|
app.use(router).use(ElementPlus).mount('#app')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'image-sd',
|
name: 'image-sd',
|
||||||
path: '/sd',
|
path: '/sd/',
|
||||||
meta: {title: 'Stable Diffusion 绘画中心'},
|
meta: {title: 'Stable Diffusion 绘画中心'},
|
||||||
component: () => import('@/views/ImageSd.vue'),
|
component: () => import('@/views/ImageSd.vue'),
|
||||||
},
|
},
|
||||||
@@ -40,9 +40,9 @@ const routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'images',
|
name: 'images',
|
||||||
path: '/images',
|
path: '/images-wall',
|
||||||
meta: {title: '绘画社区'},
|
meta: {title: '作品展示'},
|
||||||
component: () => import('@/views/Images.vue'),
|
component: () => import('@/views/ImagesWall.vue'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'user-invitation',
|
name: 'user-invitation',
|
||||||
|
|||||||
@@ -22,7 +22,8 @@
|
|||||||
@click="changeChat(chat)">
|
@click="changeChat(chat)">
|
||||||
<el-image :src="chat.icon" class="avatar"/>
|
<el-image :src="chat.icon" class="avatar"/>
|
||||||
<span class="chat-title-input" v-if="chat.edit">
|
<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>
|
||||||
<span v-else class="chat-title">{{ chat.title }}</span>
|
<span v-else class="chat-title">{{ chat.title }}</span>
|
||||||
<span class="btn btn-check" v-if="chat.edit || chat.removing">
|
<span class="btn btn-check" v-if="chat.edit || chat.removing">
|
||||||
@@ -451,16 +452,24 @@ const editChatTitle = function (event, chat) {
|
|||||||
tmpChatTitle.value = chat.title;
|
tmpChatTitle.value = chat.title;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const titleKeydown = (e, chat) => {
|
||||||
|
if (e.keyCode === 13) {
|
||||||
|
e.stopPropagation();
|
||||||
|
confirm(e, chat)
|
||||||
|
}
|
||||||
|
}
|
||||||
// 确认修改
|
// 确认修改
|
||||||
const confirm = function (event, chat) {
|
const confirm = function (event, chat) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
if (curOpt.value === 'edit') {
|
if (curOpt.value === 'edit') {
|
||||||
if (tmpChatTitle.value === '') {
|
if (tmpChatTitle.value === '') {
|
||||||
ElMessage.error("请输入会话标题!");
|
return ElMessage.error("请输入会话标题!");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
if (!chat.chat_id) {
|
||||||
httpPost('/api/chat/update', {id: chat.id, title: tmpChatTitle.value}).then(() => {
|
return ElMessage.error("对话 ID 为空,请刷新页面再试!");
|
||||||
|
}
|
||||||
|
httpPost('/api/chat/update', {chat_id: chat.chat_id, title: tmpChatTitle.value}).then(() => {
|
||||||
chat.title = tmpChatTitle.value;
|
chat.title = tmpChatTitle.value;
|
||||||
chat.edit = false;
|
chat.edit = false;
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const navs = ref([
|
|||||||
{path: "/mj", icon: "image", title: "MJ 绘画"},
|
{path: "/mj", icon: "image", title: "MJ 绘画"},
|
||||||
{path: "/sd", icon: "palette", title: "SD 绘画"},
|
{path: "/sd", icon: "palette", title: "SD 绘画"},
|
||||||
{path: "/apps", icon: "menu", title: "应用中心"},
|
{path: "/apps", icon: "menu", title: "应用中心"},
|
||||||
{path: "/images", icon: "image-list", title: "绘画社区"},
|
{path: "/images-wall", icon: "image-list", title: "作品展示"},
|
||||||
{path: "/knowledge", icon: "book", title: "我的知识库"},
|
{path: "/knowledge", icon: "book", title: "我的知识库"},
|
||||||
{path: "/member", icon: "vip-user", title: "会员计划"},
|
{path: "/member", icon: "vip-user", title: "会员计划"},
|
||||||
{path: "/invite", icon: "share", title: "推广计划"},
|
{path: "/invite", icon: "share", title: "推广计划"},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page-mj">
|
<div class="page-mj">
|
||||||
<div class="inner">
|
<div class="inner custom-scroll">
|
||||||
<div class="mj-box">
|
<div class="mj-box">
|
||||||
<h2>MidJourney 创作中心</h2>
|
<h2>MidJourney 创作中心</h2>
|
||||||
|
|
||||||
@@ -379,7 +379,6 @@ import {getSessionId, getUserToken} from "@/store/session";
|
|||||||
|
|
||||||
const listBoxHeight = ref(window.innerHeight - 40)
|
const listBoxHeight = ref(window.innerHeight - 40)
|
||||||
const mjBoxHeight = ref(window.innerHeight - 150)
|
const mjBoxHeight = ref(window.innerHeight - 150)
|
||||||
|
|
||||||
window.onresize = () => {
|
window.onresize = () => {
|
||||||
listBoxHeight.value = window.innerHeight - 40
|
listBoxHeight.value = window.innerHeight - 40
|
||||||
mjBoxHeight.value = window.innerHeight - 150
|
mjBoxHeight.value = window.innerHeight - 150
|
||||||
@@ -476,14 +475,14 @@ onMounted(() => {
|
|||||||
checkSession().then(user => {
|
checkSession().then(user => {
|
||||||
imgCalls.value = user['img_calls']
|
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
|
runningJobs.value = res.data
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
ElMessage.error("获取任务失败:" + e.message)
|
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
|
finishedJobs.value = res.data
|
||||||
previewImgList.value = []
|
previewImgList.value = []
|
||||||
for (let index in finishedJobs.value) {
|
for (let index in finishedJobs.value) {
|
||||||
@@ -601,4 +600,5 @@ const send = (url, index, item) => {
|
|||||||
|
|
||||||
<style lang="stylus">
|
<style lang="stylus">
|
||||||
@import "@/assets/css/image-mj.styl"
|
@import "@/assets/css/image-mj.styl"
|
||||||
|
@import "@/assets/css/custom-scroll.styl"
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -539,6 +539,11 @@ const runningJobs = ref([])
|
|||||||
const finishedJobs = ref([])
|
const finishedJobs = ref([])
|
||||||
const previewImgList = ref([])
|
const previewImgList = ref([])
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
// 检查是否有画同款的参数
|
||||||
|
const _params = router.currentRoute.value.params["copyParams"]
|
||||||
|
if (_params) {
|
||||||
|
params.value = JSON.parse(_params)
|
||||||
|
}
|
||||||
|
|
||||||
const socket = ref(null)
|
const socket = ref(null)
|
||||||
const imgCalls = ref(0)
|
const imgCalls = ref(0)
|
||||||
@@ -681,4 +686,5 @@ const copyParams = (row) => {
|
|||||||
|
|
||||||
<style lang="stylus">
|
<style lang="stylus">
|
||||||
@import "@/assets/css/image-sd.styl"
|
@import "@/assets/css/image-sd.styl"
|
||||||
|
@import "@/assets/css/custom-scroll.styl"
|
||||||
</style>
|
</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
|
<el-dialog
|
||||||
v-model="showDialog"
|
v-model="showDialog"
|
||||||
:title="title"
|
: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 :model="item" label-width="120px" ref="formRef" :rules="rules">
|
||||||
<el-form-item label="所属平台:" prop="platform">
|
<el-form-item label="所属平台:" prop="platform">
|
||||||
<el-select v-model="item.platform" placeholder="请选择平台">
|
<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-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
@@ -82,7 +89,13 @@ const rules = reactive({
|
|||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const formRef = ref(null)
|
const formRef = ref(null)
|
||||||
const title = ref("")
|
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) => {
|
httpGet('/api/admin/apikey/list').then((res) => {
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
<el-form :model="item" label-width="120px" ref="formRef" :rules="rules">
|
<el-form :model="item" label-width="120px" ref="formRef" :rules="rules">
|
||||||
<el-form-item label="所属平台:" prop="platform">
|
<el-form-item label="所属平台:" prop="platform">
|
||||||
<el-select v-model="item.platform" placeholder="请选择平台">
|
<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-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
@@ -94,7 +94,13 @@ const rules = reactive({
|
|||||||
})
|
})
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const formRef = ref(null)
|
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) => {
|
httpGet('/api/admin/model/list').then((res) => {
|
||||||
|
|||||||
@@ -18,12 +18,46 @@
|
|||||||
<el-form-item label="开放注册服务" prop="enabled_register">
|
<el-form-item label="开放注册服务" prop="enabled_register">
|
||||||
<el-switch v-model="system['enabled_register']"/>
|
<el-switch v-model="system['enabled_register']"/>
|
||||||
</el-form-item>
|
</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-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>
|
||||||
<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-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>
|
||||||
|
|
||||||
<el-form-item label="收款二维码" prop="reward_img">
|
<el-form-item label="收款二维码" prop="reward_img">
|
||||||
<el-input v-model="system['reward_img']" placeholder="众筹收款二维码地址">
|
<el-input v-model="system['reward_img']" placeholder="众筹收款二维码地址">
|
||||||
<template #append>
|
<template #append>
|
||||||
@@ -57,7 +91,7 @@
|
|||||||
<el-input-number v-model="chat['context_deep']" :min="0" :max="10"/>
|
<el-input-number v-model="chat['context_deep']" :min="0" :max="10"/>
|
||||||
<div class="tip" style="margin-top: 10px;">会话上下文深度:在老会话中继续会话,默认加载多少条聊天记录作为上下文。如果设置为
|
<div class="tip" style="margin-top: 10px;">会话上下文深度:在老会话中继续会话,默认加载多少条聊天记录作为上下文。如果设置为
|
||||||
0
|
0
|
||||||
则不加载聊天记录,仅仅使用当前角色的上下文。该配置参数最好设置为 2 的整数倍。
|
则不加载聊天记录,仅仅使用当前角色的上下文。该配置参数最好设置需要为偶数,否则将无法兼容百度的 API。
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
@@ -90,13 +124,37 @@
|
|||||||
<el-input v-model="chat['chat_gml']['api_url']" placeholder="支持变量,{model} => 模型名称"/>
|
<el-input v-model="chat['chat_gml']['api_url']" placeholder="支持变量,{model} => 模型名称"/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="模型创意度">
|
<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>
|
<div class="tip">值越大 AI 回答越发散,值越小回答越保守,建议保持默认值</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="最大响应长度">
|
<el-form-item label="最大响应长度">
|
||||||
<el-input v-model.number="chat['chat_gml']['max_tokens']" placeholder="回复的最大字数,最大4096"/>
|
<el-input v-model.number="chat['chat_gml']['max_tokens']" placeholder="回复的最大字数,最大4096"/>
|
||||||
</el-form-item>
|
</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-form-item style="text-align: right">
|
||||||
<el-button type="primary" @click="save('chat')">保存</el-button>
|
<el-button type="primary" @click="save('chat')">保存</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -110,13 +168,15 @@ import {onMounted, reactive, ref} from "vue";
|
|||||||
import {httpGet, httpPost} from "@/utils/http";
|
import {httpGet, httpPost} from "@/utils/http";
|
||||||
import Compressor from "compressorjs";
|
import Compressor from "compressorjs";
|
||||||
import {ElMessage} from "element-plus";
|
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 system = ref({models: []})
|
||||||
const chat = ref({
|
const chat = ref({
|
||||||
open_ai: {api_url: "", temperature: 1, max_tokens: 1024},
|
open_ai: {api_url: "", temperature: 1, max_tokens: 1024},
|
||||||
azure: {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,
|
context_deep: 0,
|
||||||
enable_context: true,
|
enable_context: true,
|
||||||
enable_history: true,
|
enable_history: true,
|
||||||
@@ -145,6 +205,12 @@ onMounted(() => {
|
|||||||
if (res.data.chat_gml) {
|
if (res.data.chat_gml) {
|
||||||
chat.value.chat_gml = 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.context_deep = res.data.context_deep
|
||||||
chat.value.enable_context = res.data.enable_context
|
chat.value.enable_context = res.data.enable_context
|
||||||
chat.value.enable_history = res.data.enable_history
|
chat.value.enable_history = res.data.enable_history
|
||||||
@@ -160,9 +226,6 @@ const rules = reactive({
|
|||||||
admin_title: [{required: true, message: '请输入控制台标题', trigger: 'blur',}],
|
admin_title: [{required: true, message: '请输入控制台标题', trigger: 'blur',}],
|
||||||
user_init_calls: [{required: true, message: '请输入赠送对话次数', trigger: 'blur'}],
|
user_init_calls: [{required: true, message: '请输入赠送对话次数', trigger: 'blur'}],
|
||||||
user_img_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) {
|
const save = function (key) {
|
||||||
if (key === 'system') {
|
if (key === 'system') {
|
||||||
@@ -176,6 +239,9 @@ const save = function (key) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else if (key === 'chat') {
|
} else if (key === 'chat') {
|
||||||
|
if (chat.value.context_deep % 2 !== 0) {
|
||||||
|
return ElMessage.error("会话上下文深度必须为偶数!")
|
||||||
|
}
|
||||||
chatFormRef.value.validate((valid) => {
|
chatFormRef.value.validate((valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
httpPost('/api/admin/config/update', {key: key, config: chat.value}).then(() => {
|
httpPost('/api/admin/config/update', {key: key, config: chat.value}).then(() => {
|
||||||
@@ -234,6 +300,12 @@ const uploadRewardImg = (file) => {
|
|||||||
line-height 1.5;
|
line-height 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
font-size 16px
|
||||||
|
margin-left 10px
|
||||||
|
cursor pointer
|
||||||
|
}
|
||||||
|
|
||||||
.uploader-icon {
|
.uploader-icon {
|
||||||
font-size 24px
|
font-size 24px
|
||||||
position relative
|
position relative
|
||||||
|
|||||||