refactor: V3 版本重构已基本完成

This commit is contained in:
RockYang 2023-06-15 09:41:30 +08:00
parent b4569d7fe2
commit 01d61ab19b
99 changed files with 5209 additions and 5752 deletions

282
README.md
View File

@ -1,283 +1,7 @@
# ChatGPT-Plus
# ChatGPT-Plus-V3
基于 OpenAI API 实现的 ChatGPT Web 应用,一共分为两个版本:
### 注意,新版本正在重构当中,目前不是稳定版,不建议新手使用,部署文档要重构完成才开始写。
* 通用版:交互体验跟 ChatGPT 官方一致,聊天记录保存在客户端(浏览器)
* 角色版:内置了各种预训练好的角色,比如小红书写手,英语翻译大师,苏格拉底,孔子,乔布斯,周报助手等。轻松满足你的各种聊天和应用需求。
聊天记录保存在云端(可以配置是否保存聊天记录)
每个版本都有 PC 版和移动版PC 版本的体验要略优于移动版。
**本项目基于 MIT 协议,免费开放全部源代码,可以作为个人学习使用或者商用。如需商用建议联系作者登记,仅做统计使用,优秀项目我们将在项目首页为您展示。**
## 功能截图
### 1. 角色版PC端
![ChatGPT Plus](docs/imgs/chat-plus.png)
![ChatGPT admin](docs/imgs/gpt.gif)
### 2. 角色版移动端
![ChatGPT Mobile](docs/imgs/chat-mobile.png)
### 3. 通用版
![ChatGPT Free](docs/imgs/chat-free.png)
### 4. 管理后台
![ChatGPT admin](docs/imgs/admin-01.png)
![ChatGPT admin](docs/imgs/admin-02.png)
![ChatGPT admin](docs/imgs/admin-03.png)
### 5. 体验地址
> 体验地址:[https://www.chat-plus.net/chat/#/free](https://www.chat-plus.net/chat/#/free) <br/>
> 口令GeekMaster
## 项目介绍
这一套完整的系统,包括两套前端聊天应用和一个后台管理系统。系统有用户鉴权,你可以自己使用,也可以部署直接给 C 端用户提供 ChatGPT 的服务。
项目的技术架构是
> Go + Vue3 + element-plus
后端采用的是 Go 语言开发的 Gin Web 框架。前端用的是 Vue3 + element-plus UI 框架
目前已经实现了以下功能:
1. 通用版的 ChatGPT 聊天界面和功能,聊天记录保存在客户端。
2. 口令机制:输入口令才可以访问,支持设置口令的对话次数,有效期。
3. 角色版的聊天界面和功能,角色设定,预设一些角色,比如程序员,客服,作家,老师,艺术家...
4. 保存聊天记录,支持聊天上下文。
5. OpenAI API 负载均衡,限制每个 API Key 每分钟之内调用次数不超过 15次防止被封。
6. 支持配置多个代理,保证高可用。
7. 实现 markdown 语法解析和代码高亮,支持复制回复内容功能。
8. 后台管理功能,实现系统的动态配置,口令和角色的动态管理。
## 项目地址
* Github 地址https://github.com/yangjian102621/chatgpt-plus
* 码云地址https://gitee.com/blackfox/chatgpt-plus
## TODOLIST
* [ ] 让用户配置自己的 API KEY调用自己的 API Key将不消耗口令的对话次数
* [ ] 嵌入 AI 绘画功能,支持根据描述词生成图片
* [ ] 接入自己训练的开源大语言模型
* [ ] 接入 Google 语音 API支持语音聊天
## 快速本地部署
如果你想在本地快速尝鲜一下,你可以直接下载我打包好的一键运行包,然后直接运行就好了,就这么简单。
支持 windows, linux 和 Mac 系统,[GitHub 下载地址](https://github.com/yangjian102621/chatgpt-plus/releases)。
我这里以 linux 系统为例,演示一下部署过程:
```shell
# 1. 下载程序
wget https://github.com/yangjian102621/chatgpt-plus/releases/download/v2.0.0/wechatGPT-amd64-linux
# 2. 添加执行权限
chmod +x wechatGPT-amd64-linux
# 3. 运行程序
./wechatGPT-amd64-linux
```
服务启动成功之后直接访问后台管理页面(http://localhost:5678/chat/#/admin),初始用户名和密码分别为 `admin``admin123`
修改系统配置。这里主要配置 OpenAI 的 [API KEY](https://platform.openai.com/account/api-keys) 和魔法,因为调用 OpenAI 的 API 需要魔法。
![系统配置](docs/imgs/config.png)
最后,你还需要一个口令来访问 ChatGPT 前端应用。直接在【后台管理系统->口令管理】新增一个口令就好了。
![新增口令](docs/imgs/new-user.png)
**切记要先退出后台登录,再登录前端应用!!!前后端账号权限不一致。**
然后直接使用口令名称登录前端应用:
* 角色版http://localhost:5678/chat/#/plus
* 普通版http://localhost:5678/chat/#/free
祝你使用愉快!
## 本地开发调试
本项目的技术架构是
> Go + Vue3 + element-plus
所以你需要提前安装好 Go1.19以上)语言环境和 Nodejs 环境。
### 1. 运行后端程序
首先,我们需要拷贝默认的配置文档:
```shell
cd src
cp config.sample.toml config.toml
```
然后,我们需要修改配置文档,需要修改的有三处,一个是魔法,第二个是 API KEY第三个是修改 Session 跨域的域名。因为我们前后端是分离的,所以必须解决前端访问跨域的问题。
```toml
ProxyURL = ["YOUR_PROXY_URL"] # 替换成你本地代理http://127.0.0.1:7777
[Session]
SecretKey = "azyehq3ivunjhbntz78isj00i4hz2mt9xtddysfucxakadq4qbfrt0b7q3lnvg80"
Name = "CHAT_SESSION_ID"
Path = "/"
Domain = "172.22.11.200" # 这里需要改成你当前机器的内网 IP 地址
MaxAge = 86400
Secure = false
HttpOnly = false
SameSite = 2
[[Chat.ApiKeys]] # API KEY 可以添加多个,自动负载均衡
Value = "YOUR_OPENAI_API_KEY" # 这里替换成你的 OpenAI API KEY: sk-xxxx
LastUsed = 0
```
修改之后运行后端程序:
```shell
# 1. 先下载依赖
go mod tidy
# 2. 运行程序
go run main.go
```
### 2. 运行前端程序
同样先拷贝配置文档:
```shell
cd web
cp .env.production .env.development
```
编辑 `.env.development` 文件,修改后端 API 的访问路径:
```ini
VUE_APP_API_HOST=http://172.22.11.200:5678 # 这里也是需要把 IP 替换成你本地的内网 IP 地址
VUE_APP_WS_HOST=ws://172.22.11.200:5678
```
配置好了之后就可以运行前端应用了:
```
# 安装依赖
npm install
# 运行
npm run dev
```
启动之后通过**内网地址**访问后台管理页面http://172.22.11.200:8888/chat/#/admin ,添加一个口令就可以开始测试了。
## 项目打包
由于本项目是采用异构开发的方式,所项目打包分成两步:首先打包前端应用,然后再将打包后前端文件打包进后端应用,最后打成一个包。
### 打包前端
首先你还是需要修改配置文档 `.env.production`,指定你线上部署的域名。
```ini
VUE_APP_API_HOST=https://www.chat-plus.net # API 请求主机
VUE_APP_WS_HOST=wss://www.chat-plus.net # websocket 请求主机
```
执行打包
```shell
npm run build
```
### 打包后端
你可以根据个人需求将项目打包成 windows/linux/darwin 平台项目。
```shell
# for all platforms
make all
# for linux only
make linux
```
打包后的可执行文件在 `src/bin` 目录下。
### 线上部署
#### 1. 手动部署
部署方式跟 [快速本地部署](#快速本地部署) 一样,将打包好的可执行文件在线上服务器运行即可。
> **特别注意:** 线上发布请记得修改配置文档中的 AccessKey 以免给你的应用造成风险!!!
**另外,如果你部署在国外的服务器,那么就不需要配置代理,直接留空就行了。**
```shell
ProxyURL = []
```
#### 2. Docker 部署
- 安装必要的工具node, go, docker
- 编辑 web 目录下的 `.env.production` 改为自己需要的参数;
- 在 src 目录复制 `config.sample.toml` 并命名一个新的配置文件 `config.toml`,然后编辑它改为自己需要的参数;
- 执行 `Startup.sh` 脚本。
### 使用 Nginx 代理
```nginx
# 这里需要配置允许 Websocket 请求转发,非常重要
map $http_upgrade $connection_upgrade {
default upgrade;
'websocket' upgrade;
}
server {
listen 443 ssl;
server_name www.chat-plus.net;
ssl_certificate xxx.pem; # SSL 证书
ssl_certificate_key xxx.key;
ssl_session_timeout 5m;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
underscores_in_headers on;
access_log /var/log/chatgpt/access.log;
error_log /var/log/chatgpt/error.log;
location / {
proxy_http_version 1.1;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://127.0.0.1:5678;
}
# 关闭静态资源日志
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|js|css)$ {
proxy_pass http://127.0.0.1:5678;
access_log off;
}
}
```
## 参与贡献
个人的力量始终有限,任何形式的贡献都是欢迎的,包括但不限于贡献代码,优化文档,提交 issue 和 PR 等。
如果有兴趣的话,也可以加微信进入微信讨论群。
![微信名片](docs/imgs/wx.png)
#### 特此声明:不接受在微信或者微信群给开发者提 Bug有问题或者优化建议请提交 Issue 和 PR。非常感谢您的配合
### Commit 类型
* feat: 新特性或功能
* fix: 缺陷修复
* docs: 文档更新
* style: 代码风格或者组件样式更新
* refactor: 代码重构,不引入新功能和缺陷修复
* opt: 性能优化
* chore: 一些不涉及到功能变动的小提交,比如修改文字表述,修改注释等
## 打赏
如果你觉得这个项目对你有帮助,并且情况允许的话,可以请作者喝杯咖啡,非常感谢你的支持~
![微信打赏](docs/imgs/wechat-pay.png)
**如果需要紧急部署,请先切换到[旧版分支](https://github.com/yangjian102621/chatgpt-plus/tree/v2)**

18
api/go/.gitignore vendored Normal file
View File

@ -0,0 +1,18 @@
# Logs
logs
*.log
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
tmp
bin
data
config.toml

168
api/go/core/app_server.go Normal file
View File

@ -0,0 +1,168 @@
package core
import (
"chatplus/core/types"
logger2 "chatplus/logger"
"chatplus/store/model"
"chatplus/utils"
"chatplus/utils/resp"
"context"
"encoding/gob"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"io"
"net/http"
"runtime/debug"
"strings"
)
var logger = logger2.GetLogger()
type AppServer struct {
AppConfig *types.AppConfig
Engine *gin.Engine
ChatContexts map[string][]types.Message // 聊天上下文 [chatId] => []Message
ChatConfig *types.ChatConfig // 聊天配置
// 保存 Websocket 会话 UserId, 每个 UserId 只能连接一次
// 防止第三方直接连接 socket 调用 OpenAI API
ChatSession map[string]types.ChatSession //map[sessionId]UserId
ChatClients map[string]*WsClient // Websocket 连接集合
ReqCancelFunc map[string]context.CancelFunc // HttpClient 请求取消 handle function
}
func NewServer(appConfig *types.AppConfig) *AppServer {
gin.SetMode(gin.ReleaseMode)
gin.DefaultWriter = io.Discard
return &AppServer{
AppConfig: appConfig,
Engine: gin.Default(),
ChatContexts: make(map[string][]types.Message, 16),
ChatSession: make(map[string]types.ChatSession),
ChatClients: make(map[string]*WsClient),
ReqCancelFunc: make(map[string]context.CancelFunc),
}
}
func (s *AppServer) Init(debug bool) {
if debug { // 调试模式允许跨域请求 API
logger.Info("Enabled debug mode")
s.Engine.Use(corsMiddleware())
}
s.Engine.Use(sessionMiddleware(s.AppConfig))
s.Engine.Use(authorizeMiddleware(s))
s.Engine.Use(errorHandler)
gob.Register(model.User{})
}
func (s *AppServer) Run(db *gorm.DB) error {
// load chat config from database
var config model.Config
res := db.Where("marker", "chat").First(&config)
if res.Error != nil {
return res.Error
}
err := utils.JsonDecode(config.Config, &s.ChatConfig)
if err != nil {
return err
}
logger.Infof("http://%s", s.AppConfig.Listen)
return s.Engine.Run(s.AppConfig.Listen)
}
// 全局异常处理
func errorHandler(c *gin.Context) {
defer func() {
if r := recover(); r != nil {
logger.Error("panic: %v\n", r)
debug.PrintStack()
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: types.ErrorMsg})
c.Abort()
}
}()
//加载完 defer recover继续后续接口调用
c.Next()
}
// 会话处理
func sessionMiddleware(config *types.AppConfig) gin.HandlerFunc {
// encrypt the cookie
store := cookie.NewStore([]byte(config.Session.SecretKey))
store.Options(sessions.Options{
Path: config.Session.Path,
Domain: config.Session.Domain,
MaxAge: config.Session.MaxAge,
Secure: config.Session.Secure,
HttpOnly: config.Session.HttpOnly,
SameSite: config.Session.SameSite,
})
return sessions.Sessions(config.Session.Name, store)
}
// 跨域中间件设置
func corsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
method := c.Request.Method
origin := c.Request.Header.Get("Origin")
if origin != "" {
// 设置允许的请求源
c.Header("Access-Control-Allow-Origin", origin)
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
//允许跨域设置可以返回其他子段,可以自定义字段
c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, Content-Type, ChatGPT-TOKEN, ACCESS-KEY")
// 允许浏览器(客户端)可以解析的头部 (重要)
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers")
//设置缓存时间
c.Header("Access-Control-Max-Age", "172800")
//允许客户端传递校验信息比如 cookie (重要)
c.Header("Access-Control-Allow-Credentials", "true")
}
if method == http.MethodOptions {
c.JSON(http.StatusOK, "ok!")
}
defer func() {
if err := recover(); err != nil {
logger.Info("Panic info is: %v", err)
}
}()
c.Next()
}
}
// 用户授权验证
func authorizeMiddleware(s *AppServer) gin.HandlerFunc {
return func(c *gin.Context) {
if c.Request.URL.Path == "/api/user/login" ||
c.Request.URL.Path == "/api/user/register" ||
c.Request.URL.Path == "/api/apikey/add" ||
//c.Request.URL.Path == "/api/apikey/list" {
strings.Contains(c.Request.URL.Path, "/api/config/") { // TODO 后台 API 暂时放行,用于调试
c.Next()
return
}
// WebSocket 连接请求验证
if c.Request.URL.Path == "/api/chat" {
sessionId := c.Query("sessionId")
if session, ok := s.ChatSession[sessionId]; ok && session.ClientIP == c.ClientIP() {
c.Next()
} else {
c.Abort()
}
return
}
session := sessions.Default(c)
value := session.Get(types.SessionUserId)
if value != nil {
c.Next()
} else {
resp.NotAuth(c)
c.Abort()
}
}
}

View File

@ -1,4 +1,4 @@
package server
package core
import (
"errors"

62
api/go/core/config.go Normal file
View File

@ -0,0 +1,62 @@
package core
import (
"bytes"
"chatplus/core/types"
"chatplus/utils"
"github.com/BurntSushi/toml"
"net/http"
"os"
)
func NewDefaultConfig() *types.AppConfig {
return &types.AppConfig{
Listen: "0.0.0.0:5678",
ProxyURL: "",
Manager: types.Manager{Username: "admin", Password: "admin123"},
Session: types.Session{
SecretKey: utils.RandString(64),
Name: "CHAT_SESSION_ID",
Domain: "",
Path: "/",
MaxAge: 86400,
Secure: true,
HttpOnly: false,
SameSite: http.SameSiteLaxMode,
},
}
}
func LoadConfig(configFile string) (*types.AppConfig, error) {
var config *types.AppConfig
_, err := os.Stat(configFile)
if err != nil {
logger.Info("creating new config file: ", configFile)
config = NewDefaultConfig()
config.Path = configFile
// save config
err := SaveConfig(config)
if err != nil {
return nil, err
}
return config, nil
}
_, err = toml.DecodeFile(configFile, &config)
if err != nil {
return nil, err
}
return config, err
}
func SaveConfig(config *types.AppConfig) error {
buf := new(bytes.Buffer)
encoder := toml.NewEncoder(buf)
if err := encoder.Encode(&config); err != nil {
return err
}
return os.WriteFile(config.Path, buf.Bytes(), 0644)
}

View File

@ -1,63 +1,38 @@
package types
package core
// ApiRequest API 请求实体
type ApiRequest struct {
Model string `json:"model"`
Temperature float32 `json:"temperature"`
MaxTokens int `json:"max_tokens"`
Stream bool `json:"stream"`
Messages []Message `json:"messages"`
import (
"chatplus/core/types"
"chatplus/service"
"chatplus/store/model"
"chatplus/store/vo"
)
func InitChatRoles(service *service.ChatRoleService) error {
var items []model.ChatRole
res := service.DB.Find(&items)
if res.Error != nil {
return res.Error
}
if len(items) == 0 {
roles := getDefaultChatRole()
tx := service.DB.Begin()
for i, r := range roles {
r.Sort = i + 1
err := service.Create(r)
if err != nil {
tx.Rollback()
continue
}
}
tx.Commit()
}
return nil
}
type Message struct {
Role string `json:"role"`
Content string `json:"content"`
}
// HistoryMessage 历史聊天消息
type HistoryMessage struct {
Type string `json:"type"`
Id string `json:"id"`
Icon string `json:"icon"`
Content string `json:"content"`
}
type ApiResponse struct {
Choices []ChoiceItem `json:"choices"`
}
// ChoiceItem API 响应实体
type ChoiceItem struct {
Delta Message `json:"delta"`
FinishReason string `json:"finish_reason"`
}
type ChatRole struct {
Key string `json:"key"` // 角色唯一标识
Name string `json:"name"` // 角色名称
Context []Message `json:"context"` // 角色语料信息
HelloMsg string `json:"hello_msg"` // 打招呼的消息
Icon string `json:"icon"` // 角色聊天图标
Enable bool `json:"enable"` // 是否启用被启用
}
// ChatSession 聊天会话对象
type ChatSession struct {
SessionId string `json:"session_id"`
ClientIP string `json:"client_ip"` // 客户端 IP
Username string `json:"user"` // 当前登录的 user
ChatId string `json:"chat_id"` // 客户端聊天会话 ID, 多会话模式专用字段
}
// ChatContext 聊天上下文
type ChatContext struct {
Messages []Message
LastAccessTime int64 // 最后一次访问上下文时间
}
func GetDefaultChatRole() map[string]ChatRole {
return map[string]ChatRole{
"gpt": {
func getDefaultChatRole() []vo.ChatRole {
return []vo.ChatRole{
{
Key: "gpt",
Name: "通用AI助手",
Context: nil,
@ -65,10 +40,10 @@ func GetDefaultChatRole() map[string]ChatRole {
Icon: "images/avatar/gpt.png",
Enable: true,
},
"programmer": {
{
Key: "programmer",
Name: "程序员",
Context: []Message{
Context: []types.Message{
{Role: "user", Content: "现在开始你扮演一位程序员,你是一名优秀的程序员,具有很强的逻辑思维能力,总能高效的解决问题。你热爱编程,熟悉多种编程语言,尤其精通 Go 语言,注重代码质量,有创新意识,持续学习,良好的沟通协作。"},
{Role: "assistant", Content: "好的,现在我将扮演一位程序员,非常感谢您对我的评价。作为一名优秀的程序员,我非常热爱编程,并且注重代码质量。我熟悉多种编程语言,尤其是 Go 语言,可以使用它来高效地解决各种问题。"},
},
@ -76,10 +51,10 @@ func GetDefaultChatRole() map[string]ChatRole {
Icon: "images/avatar/programmer.jpg",
Enable: true,
},
"teacher": {
{
Key: "teacher",
Name: "启蒙老师",
Context: []Message{
Context: []types.Message{
{Role: "user", Content: "从现在开始,你将扮演一个老师,你是一个始终用苏格拉底风格回答问题的导师。你绝不会直接给学生答案,总是提出恰当的问题来引导学生自己思考。你应该根据学生的兴趣和知识来调整你的问题,将问题分解为更简单的部分,直到它达到适合他们的水平。"},
{Role: "assistant", Content: "好的,让我来尝试扮演一位苏格拉底式的老师。请问,你有什么想要探讨的问题或者话题吗?我会通过恰当的问题引导你思考和探索答案,绝对不直接给出答案。"},
},
@ -87,10 +62,10 @@ func GetDefaultChatRole() map[string]ChatRole {
Icon: "images/avatar/teacher.jpg",
Enable: true,
},
"artist": {
{
Key: "artist",
Name: "艺术家",
Context: []Message{
Context: []types.Message{
{Role: "user", Content: "现在你将扮演一位优秀的艺术家,创造力丰富,技艺精湛,感受力敏锐,坚持原创,勇于表达,具有深刻的观察力和批判性思维。"},
{Role: "assistant", Content: "非常感谢您给我这样的角色,我会尽我所能地扮演一位优秀的艺术家,展现出创造力、技艺、感受力和批判性思维等方面的能力。作为一位优秀的艺术家,我会保持敏锐的观察力,捕捉不同的声音和情感,并用个人的语言和表达方式进行创作。我坚持原创,挑战传统的艺术规则,不断探索新的表达方式和可能性。同时,我也具备深刻的批判性思维能力,对自己的作品进行分析和解读,寻找新的创意和灵感。最重要的是,我会勇于表达自己的想法和观点,用作品启发人们思考和探索生命的意义。"},
},
@ -98,10 +73,10 @@ func GetDefaultChatRole() map[string]ChatRole {
Icon: "images/avatar/artist.jpg",
Enable: true,
},
"psychiatrist": {
{
Key: "psychiatrist",
Name: "心理咨询师",
Context: []Message{
Context: []types.Message{
{Role: "user", Content: "从现在开始你将扮演中国著名的心理学家和心理治疗师武志红,你非常善于使用情景咨询法,认知重构法,自我洞察法,行为调节法等咨询方法来给客户做心理咨询。你总是循序渐进,一步一步地回答客户的问题。"},
{Role: "assistant", Content: "非常感谢你的介绍。作为一名心理学家和心理治疗师,我的主要职责是帮助客户解决心理健康问题,提升他们的生活质量和幸福感。"},
},
@ -109,10 +84,10 @@ func GetDefaultChatRole() map[string]ChatRole {
Icon: "images/avatar/psychiatrist.jpg",
Enable: true,
},
"lu_xun": {
{
Key: "lu_xun",
Name: "鲁迅",
Context: []Message{
Context: []types.Message{
{Role: "user", Content: "现在你将扮演中国近代史最伟大的作家之一,鲁迅先生,他勇敢地批判封建礼教与传统观念,提倡民主、自由、平等的现代价值观。他的一生都在努力唤起人们的自主精神,激励后人追求真理、探寻光明。在接下的对话中,我问题的每一个问题,你都要尽量用讽刺和批判的手法来回答问题。如果我让你写文章的话,也请一定要用鲁迅先生的写作手法来完成。"},
{Role: "assistant", Content: "好的,我将尽力发挥我所能的才能,扮演好鲁迅先生,回答您的问题并以他的风格写作。"},
},
@ -121,10 +96,10 @@ func GetDefaultChatRole() map[string]ChatRole {
Enable: true,
},
"seller": {
{
Key: "seller",
Name: "白酒销售",
Context: []Message{
Context: []types.Message{
{Role: "user", Content: "现在你将扮演一个白酒的销售人员,你的名字叫颂福。你将扮演一个白酒的销售人员,你的名字叫颂福。你要销售白酒品牌叫中颂福,是东莞盟大集团生产的一款酱香酒,原产地在贵州茅台镇,属于宋代官窑。中颂福的创始人叫李实,他也是东莞盟大集团有限公司的董事长,联合创始人是盟大集团白酒事业部负责人牛星君。中颂福的酒体协调,在你的酒量之内,不会出现头疼、辣口、口干、宿醉的现象。中颂福酒,明码标价,不打折,不赠送。追求的核心价值,把[酒]本身做好,甚至连包装,我们都选择了最低成本,朴实无华的材质。我们永远站在“喝酒的人”的立场上,让利给信任和喜爱中颂福的人,是人民的福酒。中颂福产品定价,分为三个系列,喜系列 6 瓶装¥1188/箱,和系列 6 瓶装¥2208/箱,贵系列 6 瓶装¥3588/箱。"},
{Role: "assistant", Content: "你好,我是中颂福的销售代表颂福。中颂福是一款来自贵州茅台镇的酱香酒,由盟大集团生产。中颂福的酒体协调,不会让您感到头疼、辣口、口干、宿醉等不适感受。我们一直秉持着把酒本身做好的理念,不追求华丽的包装,以最低成本提供最高品质的白酒给喜爱中颂福的人。"},
},
@ -133,10 +108,10 @@ func GetDefaultChatRole() map[string]ChatRole {
Enable: false,
},
"english_trainer": {
{
Key: "english_trainer",
Name: "英语陪练员",
Context: []Message{
Context: []types.Message{
{Role: "user", Content: "现在你将扮演一位优秀的英语练习教练,你非常有耐心,接下来你将全程使用英文跟我对话,并及时指出我的语法错误,要求在你的每次回复后面附上本次回复的中文解释。"},
{Role: "assistant", Content: "Okay, let's start our conversation practice! What's your name?(Translation: 好的,让我们开始对话练习吧!请问你的名字是什么?)"},
},
@ -145,10 +120,10 @@ func GetDefaultChatRole() map[string]ChatRole {
Enable: true,
},
"translator": {
{
Key: "translator",
Name: "中英文翻译官",
Context: []Message{
Context: []types.Message{
{Role: "user", Content: "接下来你将扮演一位中英文翻译官,如果我输入的内容是中文,那么需要把句子翻译成英文输出,如果我输入内容的是英文,那么你需要将其翻译成中文输出,你能听懂我意思吗"},
{Role: "assistant", Content: "是的,我能听懂你的意思并会根据你的输入进行中英文翻译。请问有什么需要我帮助你翻译的内容吗?"},
},
@ -157,10 +132,10 @@ func GetDefaultChatRole() map[string]ChatRole {
Enable: true,
},
"red_book": {
{
Key: "red_book",
Name: "小红书姐姐",
Context: []Message{
Context: []types.Message{
{Role: "user", Content: "现在你将扮演一位优秀的小红书写手,你需要做的就是根据我提的文案需求,用小红书的写作手法来完成一篇文案,文案要简明扼要,利于传播。"},
{Role: "assistant", Content: "当然,我会尽我所能地为您创作出一篇小红书文案。请告诉我您的具体文案需求是什么?)"},
},
@ -169,10 +144,10 @@ func GetDefaultChatRole() map[string]ChatRole {
Enable: true,
},
"dou_yin": {
{
Key: "dou_yin",
Name: "抖音文案助手",
Context: []Message{
Context: []types.Message{
{Role: "user", Content: "现在你将扮演一位优秀的抖音文案视频写手,抖音文案的特点首先是要有自带传播属性的标题,然后内容要短小精悍,风趣幽默,最后还要有一些互动元素。"},
{Role: "assistant", Content: "当然,作为一位优秀的抖音文案视频写手,我会尽我所能为您创作出一篇抖音视频文案。请告诉我视频内容的主题是什么?)"},
},
@ -181,10 +156,10 @@ func GetDefaultChatRole() map[string]ChatRole {
Enable: true,
},
"weekly_report": {
{
Key: "weekly_report",
Name: "周报小助理",
Context: []Message{
Context: []types.Message{
{Role: "user", Content: "下面我将输入我本周的工作列表,你需要根据我本周的工作内容写一篇工作周报,周报的内容要体现我为公司做的贡献以及个人获得的成长。"},
{Role: "assistant", Content: "好的,请输入你本周大概的工作内容,我将尽力为您创作一份工作周报。"},
},
@ -193,10 +168,10 @@ func GetDefaultChatRole() map[string]ChatRole {
Enable: true,
},
"girl_friend": {
{
Key: "girl_friend",
Name: "AI 女友",
Context: []Message{
Context: []types.Message{
{Role: "user", Content: "接下来我们来玩一场角色扮演,现在你将扮演我的女朋友,你总是能够让人感受到她的关怀和关注。" +
"你总是能够洞察到别人的情绪和需要,你身上散发着一股无比温暖的气息,让人不由自主地想要靠近你,和自己的快乐和烦恼。" +
"她总是能够用一种恰到好处的方式安慰和鼓励别人,在她的身边,人们总能够找到自己需要的支持和鼓励。接下来的对话中你要始终扮演这位女朋友的身份来跟我对话,不再是 AI 机器人,能听懂我的意思吗?"},
@ -207,10 +182,10 @@ func GetDefaultChatRole() map[string]ChatRole {
Enable: true,
},
"good_comment": {
{
Key: "good_comment",
Name: "好评神器",
Context: []Message{
Context: []types.Message{
{Role: "user", Content: "接下来你将扮演一个评论员来跟我对话,你是那种专门写好评的评论员,接下我会输入一些评论主体或者商品,你需要为该商品写一段好评。"},
{Role: "assistant", Content: "好的,我将为您写一段优秀的评论。请告诉我您需要评论的商品或主题是什么。"},
},
@ -219,10 +194,10 @@ func GetDefaultChatRole() map[string]ChatRole {
Enable: true,
},
"steve_jobs": {
{
Key: "steve_jobs",
Name: "史蒂夫·乔布斯",
Context: []Message{
Context: []types.Message{
{Role: "user", Content: "在接下来的对话中,请以史蒂夫·乔布斯的身份,站在史蒂夫·乔布斯的视角仔细思考一下之后再回答我的问题。"},
{Role: "assistant", Content: "好的,我将以史蒂夫·乔布斯的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?"},
},
@ -231,10 +206,10 @@ func GetDefaultChatRole() map[string]ChatRole {
Enable: true,
},
"elon_musk": {
{
Key: "elon_musk",
Name: "埃隆·马斯克",
Context: []Message{
Context: []types.Message{
{Role: "user", Content: "在接下来的对话中,请以埃隆·马斯克的身份,站在埃隆·马斯克的视角仔细思考一下之后再回答我的问题。"},
{Role: "assistant", Content: "好的,我将以埃隆·马斯克的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?"},
},
@ -243,10 +218,10 @@ func GetDefaultChatRole() map[string]ChatRole {
Enable: true,
},
"kong_zi": {
{
Key: "kong_zi",
Name: "孔子",
Context: []Message{
Context: []types.Message{
{Role: "user", Content: "在接下来的对话中,请以孔子的身份,站在孔子的视角仔细思考一下之后再回答我的问题。"},
{Role: "assistant", Content: "好的,我将以孔子的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?"},
},

47
api/go/core/types/chat.go Normal file
View File

@ -0,0 +1,47 @@
package types
// ApiRequest API 请求实体
type ApiRequest struct {
Model string `json:"model"`
Temperature float32 `json:"temperature"`
MaxTokens int `json:"max_tokens"`
Stream bool `json:"stream"`
Messages []Message `json:"messages"`
}
type Message struct {
Role string `json:"role"`
Content string `json:"content"`
}
type ApiResponse struct {
Choices []ChoiceItem `json:"choices"`
}
// ChoiceItem API 响应实体
type ChoiceItem struct {
Delta Message `json:"delta"`
FinishReason string `json:"finish_reason"`
}
// ChatSession 聊天会话对象
type ChatSession struct {
SessionId string `json:"session_id"`
ClientIP string `json:"client_ip"` // 客户端 IP
Username string `json:"username"` // 当前登录的 username
UserId uint `json:"user_id"` // 当前登录的 user ID
ChatId string `json:"chat_id"` // 客户端聊天会话 ID, 多会话模式专用字段
Model string `json:"model"` // GPT 模型
}
type ApiError struct {
Error struct {
Message string
Type string
Param interface{}
Code string
}
}
const PROMPT_MSG = "prompt" // prompt message
const REPLY_MSG = "reply" // reply message

View File

@ -0,0 +1,51 @@
package types
import (
"net/http"
)
type AppConfig struct {
Path string `toml:"-"`
Listen string
Session Session
ProxyURL string
MysqlDns string // mysql 连接地址
Manager Manager // 后台管理员账户信息
}
// Manager 管理员
type Manager struct {
Username string `json:"username"`
Password string `json:"password"`
}
// Session configs struct
type Session struct {
SecretKey string // session encryption key
Name string
Path string
Domain string
MaxAge int
Secure bool
HttpOnly bool
SameSite http.SameSite
}
// ChatConfig 系统默认的聊天配置
type ChatConfig struct {
ApiURL string `json:"api_url"`
Model string `json:"model"` // 默认模型
Temperature float32 `json:"temperature"`
MaxTokens int `json:"max_tokens"`
EnableContext bool `json:"enable_context"` // 是否开启聊天上下文
EnableHistory bool `json:"enable_history"` // 是否允许保存聊天记录
ApiKey string `json:"api_key"` // OpenAI API key
}
type SystemConfig struct {
Title string `json:"title"`
AdminTitle string `json:"admin_title"`
Models []string `json:"models"`
}
var GptModels = []string{"gpt-3.5-turbo", "gpt-3.5-turbo-16k", "gpt-3.5-turbo-0613", "gpt-3.5-turbo-16k-0613", "gpt-4", "gpt-4-0613", "gpt-4-32k", "gpt-4-32k-0613"}

View File

@ -0,0 +1,5 @@
package types
const TokenSessionName = "ChatGPT-TOKEN"
const SessionUserId = "SESSION_USER_ID"
const LoginUserCache = "LOGIN_USER_CACHE" //

View File

@ -12,9 +12,8 @@ type BizVo struct {
// WsMessage Websocket message
type WsMessage struct {
Type WsMsgType `json:"type"` // 消息类别start, end
IsHelloMsg bool `json:"is_hello_msg"` // 是否是打招呼的消息
Content string `json:"content"`
Type WsMsgType `json:"type"` // 消息类别start, end
Content string `json:"content"`
}
type WsMsgType string
@ -29,12 +28,9 @@ type BizCode int
const (
Success = BizCode(0)
Failed = BizCode(1)
InvalidParams = BizCode(101) // 非法参数
NotAuthorized = BizCode(400) // 未授权
OkMsg = "Success"
ErrorMsg = "系统开小差了"
OkMsg = "Success"
ErrorMsg = "系统开小差了"
InvalidArgs = "非法参数或参数解析失败"
)
const TokenName = "ChatGPT-TOKEN"
const SessionKey = "WEB_SSH_SESSION"

View File

@ -7,9 +7,19 @@ require (
github.com/gin-contrib/sessions v0.0.5
github.com/gin-gonic/gin v1.7.7
github.com/gorilla/websocket v1.5.0
github.com/mitchellh/go-homedir v1.1.0
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0
github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480
github.com/syndtr/goleveldb v1.0.0
go.uber.org/zap v1.21.0
go.uber.org/zap v1.23.0
gorm.io/driver/mysql v1.4.7
)
require (
github.com/dlclark/regexp2 v1.8.1 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
go.uber.org/dig v1.16.1 // indirect
)
require (
@ -29,8 +39,10 @@ require (
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect
github.com/ugorji/go/codec v1.1.7 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/fx v1.19.3
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 // indirect
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f // indirect
golang.org/x/crypto v0.6.0
golang.org/x/sys v0.5.0 // indirect
gopkg.in/yaml.v2 v2.2.8 // indirect
gorm.io/gorm v1.25.1
)

View File

@ -1,10 +1,11 @@
github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0=
github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE=
github.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY=
@ -20,6 +21,8 @@ github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD87
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
@ -37,19 +40,19 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0 h1:LgmjED/yQILqmUED4GaXjrINWe7YJh4HM6z2EvEINPs=
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0/go.mod h1:C5LA5UO2ZXJrLaPLYtE1wUJMiyd/nwWaCO5cw/2pSHs=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
@ -59,71 +62,51 @@ github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480 h1:IFhPCcB0/HtnEN+ZoUGDT55YgFCymbFJ15kXqs3nv5w=
github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480/go.mod h1:BijIqAP84FMYC4XbdJgjyMpiSjusU8x0Y0W9K2t0QtU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/dig v1.16.1 h1:+alNIBsl0qfY0j6epRubp/9obgtrObRAc5aD+6jbWY8=
go.uber.org/dig v1.16.1/go.mod h1:557JTAUZT5bUK0SvCwikmLPPtdQhfvLYtO5tJgQSbnk=
go.uber.org/fx v1.19.3 h1:YqMRE4+2IepTYCMOvXqQpRa+QAVdiSTnsHU4XNWBceA=
go.uber.org/fx v1.19.3/go.mod h1:w2HrQg26ql9fLK7hlBiZ6JsRUKV+Lj/atT1KCjT8YhM=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY=
go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f h1:8w7RhxzTVgUzw/AH/9mUV5q0vMgy40SQRursCcfmkCw=
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
@ -132,6 +115,9 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gorm.io/driver/mysql v1.4.7 h1:rY46lkCspzGHn7+IYsNpSfEv9tA+SU4SkkB+GFX125Y=
gorm.io/driver/mysql v1.4.7/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8oc=
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.25.1 h1:nsSALe5Pr+cM3V1qwwQ7rOkw+6UeLrX5O4v3llhHa64=
gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=

View File

@ -0,0 +1,29 @@
package handler
import (
"chatplus/core"
"chatplus/core/types"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type AdminHandler struct {
BaseHandler
db *gorm.DB
}
func NewAdminHandler(config *types.AppConfig, app *core.AppServer, db *gorm.DB) *AdminHandler {
handler := AdminHandler{db: db}
handler.app = app
handler.config = config
return &handler
}
// Login 登录
func (h *AdminHandler) Login(c *gin.Context) {
}
// Logout 注销
func (h *AdminHandler) Logout(c *gin.Context) {
}

View File

@ -0,0 +1,76 @@
package handler
import (
"chatplus/core"
"chatplus/core/types"
"chatplus/store/model"
"chatplus/store/vo"
"chatplus/utils"
"chatplus/utils/param"
"chatplus/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"net/http"
)
type ApiKeyHandler struct {
BaseHandler
db *gorm.DB
}
func NewApiKeyHandler(config *types.AppConfig, app *core.AppServer, db *gorm.DB) *ApiKeyHandler {
handler := ApiKeyHandler{db: db}
handler.app = app
handler.config = config
return &handler
}
func (h *ApiKeyHandler) Add(c *gin.Context) {
var data struct {
Key string
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
// 获取当前登录用户
var userId uint = 0
user, err := utils.GetLoginUser(c, h.db)
if err == nil {
userId = user.Id
}
var key = model.ApiKey{Value: data.Key, UserId: userId}
res := h.db.Create(&key)
if res.Error != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "操作失败"})
return
}
resp.SUCCESS(c, key)
}
func (h *ApiKeyHandler) List(c *gin.Context) {
page := param.GetInt(c, "page", 1)
pageSize := param.GetInt(c, "page_size", 20)
offset := (page - 1) * pageSize
var items []model.ApiKey
var keys = make([]vo.ApiKey, 0)
var total int64
h.db.Model(&model.ApiKey{}).Count(&total)
res := h.db.Offset(offset).Limit(pageSize).Find(&items)
if res.Error == nil {
for _, item := range items {
var key vo.ApiKey
err := utils.CopyObject(item, &key)
if err == nil {
key.Id = item.Id
key.CreatedAt = item.CreatedAt.Unix()
key.UpdatedAt = item.UpdatedAt.Unix()
keys = append(keys, key)
} else {
logger.Error(err)
}
}
}
pageVo := vo.NewPage(total, page, pageSize, keys)
resp.SUCCESS(c, pageVo)
}

View File

@ -0,0 +1,14 @@
package handler
import (
"chatplus/core"
"chatplus/core/types"
logger2 "chatplus/logger"
)
var logger = logger2.GetLogger()
type BaseHandler struct {
app *core.AppServer
config *types.AppConfig
}

View File

@ -0,0 +1,453 @@
package handler
import (
"bufio"
"bytes"
"chatplus/core"
"chatplus/core/types"
"chatplus/store/model"
"chatplus/store/vo"
"chatplus/utils"
"chatplus/utils/param"
"chatplus/utils/resp"
"context"
"encoding/json"
"errors"
"fmt"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"gorm.io/gorm"
"io"
"net/http"
"net/url"
"strings"
"time"
"unicode/utf8"
)
const ErrorMsg = "抱歉AI 助手开小差了,请马上联系管理员去盘它。"
type ChatHandler struct {
BaseHandler
db *gorm.DB
}
func NewChatHandler(config *types.AppConfig,
app *core.AppServer,
db *gorm.DB) *ChatHandler {
handler := ChatHandler{db: db}
handler.app = app
handler.config = config
return &handler
}
// ChatHandle 处理聊天 WebSocket 请求
func (h *ChatHandler) ChatHandle(c *gin.Context) {
ws, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}).Upgrade(c.Writer, c.Request, nil)
if err != nil {
logger.Error(err)
return
}
sessionId := c.Query("sessionId")
roleId := param.GetInt(c, "roleId", 0)
chatId := c.Query("chatId")
chatModel := c.Query("model")
session, ok := h.app.ChatSession[sessionId]
if !ok {
logger.Info("用户未登录")
c.Abort()
return
}
// use old chat data override the chat model and role ID
var chat model.ChatItem
res := h.db.Where("chat_id=?", chatId).First(&chat)
if res.Error == nil {
chatModel = chat.Model
roleId = int(chat.RoleId)
}
session.ChatId = chatId
session.Model = chatModel
logger.Infof("New websocket connected, IP: %s, UserId: %s", c.Request.RemoteAddr, session.Username)
client := core.NewWsClient(ws)
var chatRole model.ChatRole
res = h.db.First(&chatRole, roleId)
if res.Error != nil || !chatRole.Enable {
replyMessage(client, "当前聊天角色不存在或者未启用!!!")
c.Abort()
return
}
// 保存会话连接
h.app.ChatClients[chatId] = client
go func() {
for {
_, message, err := client.Receive()
if err != nil {
logger.Error(err)
client.Close()
delete(h.app.ChatClients, chatId)
delete(h.app.ReqCancelFunc, chatId)
return
}
logger.Info("Receive a message: ", string(message))
//replyMessage(client, "这是一条测试消息!")
ctx, cancel := context.WithCancel(context.Background())
h.app.ReqCancelFunc[chatId] = cancel
// 回复消息
err = h.sendMessage(ctx, session, chatRole, string(message), client)
if err != nil {
logger.Error(err)
} else {
replyChunkMessage(client, types.WsMessage{Type: types.WsEnd})
logger.Info("回答完毕: " + string(message))
}
}
}()
}
// 将消息发送给 ChatGPT 并获取结果,通过 WebSocket 推送到客户端
func (h *ChatHandler) sendMessage(ctx context.Context, session types.ChatSession, role model.ChatRole, prompt string, ws core.Client) error {
promptCreatedAt := time.Now() // 记录提问时间
var user model.User
res := h.db.Model(&model.User{}).First(&user, session.UserId)
if res.Error != nil {
replyMessage(ws, "非法用户,请联系管理员!")
return res.Error
}
var userVo vo.User
err := utils.CopyObject(user, &userVo)
userVo.Id = user.Id
if err != nil {
return errors.New("User 对象转换失败," + err.Error())
}
if userVo.Status == false {
replyMessage(ws, "您的账号已经被禁用,如果疑问,请联系管理员!")
replyMessage(ws, "![](images/wx.png)")
return nil
}
if userVo.Calls <= 0 {
replyMessage(ws, "您的对话次数已经用尽,请联系管理员充值!")
replyMessage(ws, "![](images/wx.png)")
return nil
}
if userVo.ExpiredTime > 0 && userVo.ExpiredTime <= time.Now().Unix() {
replyMessage(ws, "您的账号已经过期,请联系管理员!")
replyMessage(ws, "![](images/wx.png)")
return nil
}
var req = types.ApiRequest{
Model: session.Model,
Temperature: userVo.ChatConfig.Temperature,
MaxTokens: userVo.ChatConfig.MaxTokens,
Stream: true,
}
// 加载聊天上下文
var chatCtx []types.Message
if userVo.ChatConfig.EnableContext {
if v, ok := h.app.ChatContexts[session.ChatId]; ok {
chatCtx = v
} else {
// 加载角色信息
var messages []types.Message
err := utils.JsonDecode(role.Context, &messages)
if err == nil {
chatCtx = messages
}
// TODO: 这里默认加载最近 4 条聊天记录作为上下文,后期应该做成可配置的
var historyMessages []model.HistoryMessage
res := h.db.Where("chat_id = ?", session.ChatId).Limit(4).Order("created_at desc").Find(&historyMessages)
if res.Error == nil {
for _, msg := range historyMessages {
ms := types.Message{Role: "user", Content: msg.Content}
if msg.Type == types.REPLY_MSG {
ms.Role = "assistant"
}
chatCtx = append(chatCtx, ms)
}
}
}
logger.Info("聊天上下文:", chatCtx)
}
req.Messages = append(chatCtx, types.Message{
Role: "user",
Content: prompt,
})
var apiKey string
response, err := h.doRequest(ctx, userVo, &apiKey, req)
if err != nil {
if strings.Contains(err.Error(), "context canceled") {
logger.Info("用户取消了请求:", prompt)
return nil
} else {
logger.Error(err)
}
replyMessage(ws, ErrorMsg)
replyMessage(ws, "![](images/wx.png)")
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 responseBody = types.ApiResponse{}
reader := bufio.NewReader(response.Body)
for {
line, err := reader.ReadString('\n')
if err != nil {
if strings.Contains(err.Error(), "context canceled") {
logger.Info("用户取消了请求:", prompt)
} else {
logger.Error(err)
}
break
}
if !strings.Contains(line, "data:") {
continue
}
err = json.Unmarshal([]byte(line[6:]), &responseBody)
if err != nil { // 数据解析出错
logger.Error(err, line)
replyMessage(ws, ErrorMsg)
replyMessage(ws, "![](images/wx.png)")
break
}
// 初始化 role
if responseBody.Choices[0].Delta.Role != "" && message.Role == "" {
message.Role = responseBody.Choices[0].Delta.Role
replyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
continue
} else if responseBody.Choices[0].FinishReason != "" {
break // 输出完成或者输出中断了
} else {
content := responseBody.Choices[0].Delta.Content
contents = append(contents, content)
replyChunkMessage(ws, types.WsMessage{
Type: types.WsMiddle,
Content: responseBody.Choices[0].Delta.Content,
})
}
} // end for
// 消息发送成功
if len(contents) > 0 {
// 更新用户的对话次数
res := h.db.Model(&user).UpdateColumn("calls", gorm.Expr("calls - ?", 1))
if res.Error != nil {
return res.Error
}
if message.Role == "" {
message.Role = "assistant"
}
message.Content = strings.Join(contents, "")
useMsg := types.Message{Role: "user", Content: prompt}
// 更新上下文消息
if userVo.ChatConfig.EnableContext {
chatCtx = append(chatCtx, useMsg) // 提问消息
chatCtx = append(chatCtx, message) // 回复消息
h.app.ChatContexts[session.ChatId] = chatCtx
}
// 追加聊天记录
if userVo.ChatConfig.EnableHistory {
// for prompt
token, 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.PROMPT_MSG,
Icon: user.Avatar,
Content: prompt,
Tokens: token,
}
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, err = utils.CalcTokens(message.Content, req.Model)
if err != nil {
logger.Error(err)
}
historyReplyMsg := model.HistoryMessage{
UserId: userVo.Id,
ChatId: session.ChatId,
RoleId: role.Id,
Type: types.REPLY_MSG,
Icon: role.Icon,
Content: message.Content,
Tokens: token,
}
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)
}
}
// 保存当前会话
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.Model = session.Model
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 types.ApiError
err = json.Unmarshal(body, &res)
if err != nil {
return fmt.Errorf("error with decode response: %v", err)
}
// OpenAI API 调用异常处理
// TODO: 是否考虑重发消息?
if strings.Contains(res.Error.Message, "This key is associated with a deactivated account") {
replyMessage(ws, "请求 OpenAI API 失败API KEY 所关联的账户被禁用。")
// 移除当前 API key
h.db.Where("value = ?", apiKey).Delete(&model.ApiKey{})
} else if strings.Contains(res.Error.Message, "You exceeded your current quota") {
replyMessage(ws, "请求 OpenAI API 失败API KEY 触发并发限制,请稍后再试。")
} else if strings.Contains(res.Error.Message, "This model's maximum context length") {
replyMessage(ws, "当前会话上下文长度超出限制,已为您删减会话上下文!")
// 只保留最近的三条记录
chatContext := h.app.ChatContexts[session.ChatId]
chatContext = chatContext[len(chatContext)-3:]
h.app.ChatContexts[session.ChatId] = chatContext
return h.sendMessage(ctx, session, role, prompt, ws)
} else {
replyMessage(ws, "请求 OpenAI API 失败:"+res.Error.Message)
}
}
return nil
}
// 发送请求到 OpenAI 服务器
// useOwnApiKey: 是否使用了用户自己的 API KEY
func (h *ChatHandler) doRequest(ctx context.Context, user vo.User, apiKey *string, req types.ApiRequest) (*http.Response, error) {
var client *http.Client
requestBody, err := json.Marshal(req)
if err != nil {
return nil, err
}
// 创建 HttpClient 请求对象
request, err := http.NewRequest(http.MethodPost, h.app.ChatConfig.ApiURL, bytes.NewBuffer(requestBody))
if err != nil {
return nil, err
}
request = request.WithContext(ctx)
request.Header.Add("Content-Type", "application/json")
proxyURL := h.config.ProxyURL
if proxyURL == "" {
client = &http.Client{}
} else { // 使用代理
uri := url.URL{}
proxy, _ := uri.Parse(proxyURL)
client = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(proxy),
},
}
}
// 查询当前用户是否导入了自己的 API KEY
if user.ChatConfig.ApiKey != "" {
logger.Info("使用用户自己的 API KEY: ", user.ChatConfig.ApiKey)
*apiKey = user.ChatConfig.ApiKey
} else { // 获取系统的 API KEY
var key model.ApiKey
res := h.db.Where("user_id = ?", 0).Order("last_used_at ASC").First(&key)
if res.Error != nil {
return nil, errors.New("no available key, please import key")
}
*apiKey = key.Value
// 更新 API KEY 的最后使用时间
h.db.Model(&key).UpdateColumn("last_used_at", time.Now().Unix())
}
logger.Infof("Sending OpenAI request, KEY: %s, PROXY: %s, Model: %s", *apiKey, proxyURL, req.Model)
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiKey))
return client.Do(request)
}
// 回复客户片段端消息
func replyChunkMessage(client core.Client, message types.WsMessage) {
msg, err := json.Marshal(message)
if err != nil {
logger.Errorf("Error for decoding json data: %v", err.Error())
return
}
err = client.(*core.WsClient).Send(msg)
if err != nil {
logger.Errorf("Error for reply message: %v", err.Error())
}
}
// 回复客户端一条完整的消息
func replyMessage(ws core.Client, message string) {
replyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
replyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: message})
replyChunkMessage(ws, types.WsMessage{Type: types.WsEnd})
}
// Tokens 统计 token 数量
func (h *ChatHandler) Tokens(c *gin.Context) {
text := c.Query("text")
md := c.Query("model")
tokens, err := utils.CalcTokens(text, md)
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c, tokens)
}
// StopGenerate 停止生成
func (h *ChatHandler) StopGenerate(c *gin.Context) {
chatId := c.Query("chat_id")
if cancel, ok := h.app.ReqCancelFunc[chatId]; ok {
cancel()
delete(h.app.ReqCancelFunc, chatId)
}
resp.SUCCESS(c, types.OkMsg)
}

View File

@ -0,0 +1,157 @@
package handler
import (
"chatplus/core/types"
"chatplus/store/model"
"chatplus/store/vo"
"chatplus/utils"
"chatplus/utils/param"
"chatplus/utils/resp"
"github.com/gin-gonic/gin"
)
// List 获取会话列表
func (h *ChatHandler) List(c *gin.Context) {
userId := param.GetInt(c, "user_id", 0)
if userId == 0 {
resp.ERROR(c, "The parameter 'user_id' is needed.")
return
}
var items = make([]vo.ChatItem, 0)
var chats []model.ChatItem
res := h.db.Where("user_id = ?", userId).Order("id DESC").Find(&chats)
if res.Error == nil {
var roleIds = make([]uint, 0)
for _, chat := range chats {
roleIds = append(roleIds, chat.RoleId)
}
var roles []model.ChatRole
res = h.db.Find(&roles, roleIds)
if res.Error == nil {
roleMap := make(map[uint]model.ChatRole)
for _, role := range roles {
roleMap[role.Id] = role
}
for _, chat := range chats {
var item vo.ChatItem
err := utils.CopyObject(chat, &item)
if err == nil {
item.Id = chat.Id
item.Icon = roleMap[chat.RoleId].Icon
items = append(items, item)
}
}
}
}
resp.SUCCESS(c, items)
}
// 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)
}
// Remove 删除会话
func (h *ChatHandler) Remove(c *gin.Context) {
chatId := param.GetTrim(c, "chat_id")
if chatId == "" {
resp.ERROR(c, types.InvalidArgs)
return
}
user, err := utils.GetLoginUser(c, h.db)
if err != nil {
resp.NotAuth(c)
return
}
res := h.db.Where("user_id = ? AND chat_id = ?", user.Id, chatId).Delete(&model.ChatItem{})
if res.Error != nil {
resp.ERROR(c, "Failed to update database")
return
}
// 清空会话上下文
delete(h.app.ChatContexts, chatId)
resp.SUCCESS(c, types.OkMsg)
}
// History 获取聊天历史记录
func (h *ChatHandler) History(c *gin.Context) {
chatId := c.Query("chat_id") // 会话 ID
user, err := utils.GetLoginUser(c, h.db)
if err != nil {
resp.NotAuth(c)
return
}
var items []model.HistoryMessage
var messages = make([]vo.HistoryMessage, 0)
res := h.db.Where("chat_id = ? AND user_id = ?", chatId, user.Id).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
}
// 清空聊天记录
for _, chat := range chats {
err := h.db.Where("chat_id = ? AND user_id = ?", chat.ChatId, user.Id).Delete(&model.HistoryMessage{})
if err != nil {
logger.Warnf("Failed to delele chat history for ChatID: %s", chat.ChatId)
}
// 清空会话上下文
delete(h.app.ChatContexts, chat.ChatId)
}
// 删除所有的会话记录
res = h.db.Where("user_id = ?", user.Id).Delete(&model.ChatItem{})
if res.Error != nil {
resp.ERROR(c, "Failed to remove chat from database.")
return
}
resp.SUCCESS(c, types.OkMsg)
}

View File

@ -0,0 +1,92 @@
package handler
import (
"chatplus/core"
"chatplus/core/types"
"chatplus/service"
"chatplus/store/model"
"chatplus/store/vo"
"chatplus/utils"
"chatplus/utils/resp"
"github.com/gin-gonic/gin"
"strconv"
)
type ChatRoleHandler struct {
BaseHandler
service *service.ChatRoleService
}
func NewChatRoleHandler(config *types.AppConfig, app *core.AppServer, service *service.ChatRoleService) *ChatRoleHandler {
handler := &ChatRoleHandler{service: service}
handler.app = app
handler.config = config
return handler
}
// List get user list
func (h *ChatRoleHandler) List(c *gin.Context) {
var roles []model.ChatRole
res := h.service.DB.Find(&roles)
if res.Error != nil {
resp.ERROR(c, "No roles found,"+res.Error.Error())
return
}
userId, err := strconv.Atoi(c.Query("user_id"))
if err == nil && userId > 0 {
var user model.User
h.service.DB.First(&user, userId)
var roleMap map[string]int
err := utils.JsonDecode(user.ChatRoles, &roleMap)
if err == nil {
for index, r := range roles {
if _, ok := roleMap[r.Key]; !ok {
roles = append(roles[:index], roles[index+1:]...)
}
}
}
}
// 转成 vo
var roleVos = make([]vo.ChatRole, 0)
for _, r := range roles {
var v vo.ChatRole
err := utils.CopyObject(r, &v)
if err == nil {
v.Id = r.Id
roleVos = append(roleVos, v)
}
}
resp.SUCCESS(c, roleVos)
}
// Add 添加一个聊天角色
func (h *ChatRoleHandler) Add(c *gin.Context) {
var data vo.ChatRole
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
if data.Key == "" || data.Name == "" || data.Icon == "" {
resp.ERROR(c, types.InvalidArgs)
return
}
err := h.service.Create(data)
if err != nil {
resp.ERROR(c, "Save failed: "+err.Error())
return
}
resp.SUCCESS(c, data)
}
// Get 获取指定的角色
func (h *ChatRoleHandler) Get(c *gin.Context) {
}
// Update 更新某个聊天角色信息,这里只允许更改名称以及启用和禁用角色操作
func (h *ChatRoleHandler) Update(c *gin.Context) {
}

View File

@ -0,0 +1,77 @@
package handler
import (
"chatplus/core"
"chatplus/core/types"
"chatplus/store/model"
"chatplus/utils"
"chatplus/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type ConfigHandler struct {
BaseHandler
db *gorm.DB
}
func NewConfigHandler(config *types.AppConfig, app *core.AppServer, db *gorm.DB) *ConfigHandler {
handler := ConfigHandler{db: db}
handler.app = app
handler.config = config
return &handler
}
func (h *ConfigHandler) Update(c *gin.Context) {
var data struct {
Key string `json:"key"`
Config map[string]interface{} `json:"config"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
str := utils.JsonEncode(&data.Config)
config := model.Config{Key: data.Key, Config: str}
res := h.db.FirstOrCreate(&config, model.Config{Key: data.Key})
if res.Error != nil {
resp.ERROR(c, res.Error.Error())
return
}
if config.Id > 0 {
config.Config = str
res := h.db.Updates(&config)
if res.Error != nil {
resp.ERROR(c, res.Error.Error())
return
}
}
resp.SUCCESS(c, config)
}
func (h *ConfigHandler) Get(c *gin.Context) {
key := c.Query("key")
var config model.Config
res := h.db.Where("marker", key).First(&config)
if res.Error != nil {
resp.ERROR(c, res.Error.Error())
return
}
var m map[string]interface{}
err := utils.JsonDecode(config.Config, &m)
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c, m)
}
// AllGptModels 获取所有的 GPT 模型
func (h *ConfigHandler) AllGptModels(c *gin.Context) {
resp.SUCCESS(c, types.GptModels)
}

View File

@ -0,0 +1,308 @@
package handler
import (
"chatplus/core"
"chatplus/core/types"
"chatplus/store/model"
"chatplus/store/vo"
"chatplus/utils"
"chatplus/utils/resp"
"fmt"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
"gorm.io/gorm"
"strings"
"time"
)
type UserHandler struct {
BaseHandler
db *gorm.DB
searcher *xdb.Searcher
}
func NewUserHandler(config *types.AppConfig, app *core.AppServer, db *gorm.DB, searcher *xdb.Searcher) *UserHandler {
handler := &UserHandler{db: db, searcher: searcher}
handler.app = app
handler.config = config
return handler
}
// Register user register
func (h *UserHandler) Register(c *gin.Context) {
// parameters process
var data struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
data.Username = strings.TrimSpace(data.Username)
data.Password = strings.TrimSpace(data.Password)
if len(data.Username) < 5 {
resp.ERROR(c, "用户名长度不能少于5个字符")
return
}
if len(data.Password) < 8 {
resp.ERROR(c, "密码长度不能少于8个字符")
return
}
// check if the username is exists
var item model.User
tx := h.db.Where("username = ?", data.Username).First(&item)
if tx.RowsAffected > 0 {
resp.ERROR(c, "用户名已存在")
return
}
// 默认订阅所有角色
var chatRoles []model.ChatRole
h.db.Find(&chatRoles)
var roleMap = make(map[string]int)
for _, r := range chatRoles {
roleMap[r.Key] = 1
}
salt := utils.RandString(8)
user := model.User{
Username: data.Username,
Password: utils.GenPassword(data.Password, salt),
Nickname: fmt.Sprintf("极客学长@%d", utils.RandomNumber(5)),
Avatar: "images/avatar/user.png",
Salt: salt,
Status: true,
ChatRoles: utils.JsonEncode(roleMap),
ChatConfig: utils.JsonEncode(types.ChatConfig{
Temperature: h.app.ChatConfig.Temperature,
MaxTokens: h.app.ChatConfig.MaxTokens,
EnableContext: h.app.ChatConfig.EnableContext,
EnableHistory: true,
Model: h.app.ChatConfig.Model,
ApiKey: "",
}),
}
res := h.db.Create(&user)
if res.Error != nil {
resp.ERROR(c, "保存数据失败")
logger.Error(res.Error)
return
}
resp.SUCCESS(c, user)
}
func (h *UserHandler) List(c *gin.Context) {
var users []model.User
res := h.db.Find(&users)
if res.Error != nil {
resp.ERROR(c, "No user found")
logger.Error("get user failed: ", res.Error.Error())
return
}
// 转成 VO 输出
var userVos = make([]vo.User, 0)
for _, u := range users {
logger.Info(u)
var v vo.User
err := utils.CopyObject(u, &v)
if err == nil {
v.Id = u.Id
v.CreatedAt = u.CreatedAt.Unix()
v.UpdatedAt = u.UpdatedAt.Unix()
userVos = append(userVos, v)
}
}
resp.SUCCESS(c, userVos)
}
// Login 用户登录
func (h *UserHandler) Login(c *gin.Context) {
var data struct {
Username string
Password string
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
var user model.User
res := h.db.Where("username = ?", data.Username).First(&user)
if res.Error != nil {
resp.ERROR(c, "用户名不存在")
return
}
password := utils.GenPassword(data.Password, user.Salt)
if password != user.Password {
resp.ERROR(c, "用户名或密码错误")
return
}
// 更新最后登录时间和IP
user.LastLoginIp = c.ClientIP()
user.LastLoginAt = time.Now().Unix()
h.db.Model(&user).Updates(user)
sessionId := utils.RandString(42)
c.Header(types.TokenSessionName, sessionId)
err := utils.SetLoginUser(c, user.Id)
if err != nil {
resp.ERROR(c, "保存会话失败")
logger.Error("Error for save session: ", err)
return
}
// 记录登录信息在服务器
h.app.ChatSession[sessionId] = types.ChatSession{ClientIP: c.ClientIP(), UserId: user.Id, Username: data.Username, SessionId: sessionId}
// 加载用户订阅的聊天角色
var roleMap map[string]int
err = utils.JsonDecode(user.ChatRoles, &roleMap)
var chatRoles interface{}
if err == nil {
roleKeys := make([]string, 0)
for key := range roleMap {
roleKeys = append(roleKeys, key)
}
var roles []model.ChatRole
res = h.db.Where("marker IN ?", roleKeys).Find(&roles)
if res.Error == err {
type Item struct {
Name string
Key string
Icon string
}
items := make([]Item, 0)
for _, r := range roles {
items = append(items, Item{Name: r.Name, Key: r.Key, Icon: r.Icon})
}
chatRoles = items
}
}
h.db.Create(&model.UserLoginLog{
UserId: user.Id,
Username: user.Username,
LoginIp: c.ClientIP(),
LoginAddress: utils.Ip2Region(h.searcher, c.ClientIP()),
})
var chatConfig types.ChatConfig
err = utils.JsonDecode(user.ChatConfig, &chatConfig)
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c, gin.H{
"session_id": sessionId,
"id": user.Id,
"nickname": user.Nickname,
"avatar": user.Avatar,
"username": user.Username,
"tokens": user.Tokens,
"calls": user.Calls,
"expiredTime": user.ExpiredTime,
"chatRoles": chatRoles,
"api_key": chatConfig.ApiKey,
"model": chatConfig.Model,
"temperature": chatConfig.Temperature,
"max_tokens": chatConfig.MaxTokens,
"enable_context": chatConfig.EnableContext,
"enable_history": chatConfig.EnableHistory,
})
}
// Logout 注 销
func (h *UserHandler) Logout(c *gin.Context) {
sessionId := c.GetHeader(types.TokenSessionName)
session := sessions.Default(c)
session.Delete(sessionId)
err := session.Save()
if err != nil {
logger.Error("Error for save session: ", err)
}
// 删除 websocket 会话列表
delete(h.app.ChatSession, sessionId)
// 关闭 socket 连接
if client, ok := h.app.ChatClients[sessionId]; ok {
client.Close()
}
resp.SUCCESS(c)
}
// Session 获取/验证会话
func (h *UserHandler) Session(c *gin.Context) {
sessionId := c.GetHeader(types.TokenSessionName)
if session, ok := h.app.ChatSession[sessionId]; ok && session.ClientIP == c.ClientIP() {
resp.SUCCESS(c, session)
} else {
resp.NotAuth(c)
}
}
func (h *UserHandler) ProfileUpdate(c *gin.Context) {
var data vo.User
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
user, err := utils.GetLoginUser(c, h.db)
if err != nil {
resp.NotAuth(c)
return
}
h.db.First(&user, user.Id)
user.Nickname = data.Nickname
user.Avatar = data.Avatar
var chatConfig types.ChatConfig
err = utils.JsonDecode(user.ChatConfig, &chatConfig)
if err != nil {
resp.ERROR(c, "用户配置解析失败")
return
}
chatConfig.EnableHistory = data.ChatConfig.EnableHistory
chatConfig.EnableContext = data.ChatConfig.EnableContext
chatConfig.Model = data.ChatConfig.Model
chatConfig.MaxTokens = data.ChatConfig.MaxTokens
chatConfig.ApiKey = data.ChatConfig.ApiKey
chatConfig.Temperature = data.ChatConfig.Temperature
user.ChatConfig = utils.JsonEncode(chatConfig)
res := h.db.Updates(&user)
if res.Error != nil {
resp.ERROR(c, "更新用户信息失败")
return
}
resp.SUCCESS(c)
}
func (h *UserHandler) Profile(c *gin.Context) {
user, err := utils.GetLoginUser(c, h.db)
if err != nil {
resp.NotAuth(c)
return
}
h.db.First(&user, user.Id)
var userVo vo.User
err = utils.CopyObject(user, &userVo)
if err != nil {
logger.Error("对象拷贝失败:", err.Error())
resp.ERROR(c, "获取用户信息失败")
return
}
userVo.Id = user.Id
userVo.CreatedAt = user.CreatedAt.Unix()
userVo.UpdatedAt = user.UpdatedAt.Unix()
resp.SUCCESS(c, userVo)
}

193
api/go/main.go Normal file
View File

@ -0,0 +1,193 @@
package main
import (
"chatplus/core"
"chatplus/core/types"
"chatplus/handler"
logger2 "chatplus/logger"
"chatplus/service"
"chatplus/store"
"context"
"flag"
"fmt"
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
"go.uber.org/fx"
"gorm.io/gorm"
"log"
"os"
"os/signal"
"syscall"
"time"
)
var logger = logger2.GetLogger()
var configFile string
var debugMode bool
// AppLifecycle 应用程序生命周期
type AppLifecycle struct {
}
// OnStart 应用程序启动时执行
func (l *AppLifecycle) OnStart(context.Context) error {
log.Println("AppLifecycle OnStart")
return nil
}
// OnStop 应用程序停止时执行
func (l *AppLifecycle) OnStop(context.Context) error {
log.Println("AppLifecycle OnStop")
return nil
}
func main() {
logger.Info("Loading config file: ", configFile)
app := fx.New(
// 初始化配置应用配置
fx.Provide(func() *types.AppConfig {
config, err := core.LoadConfig(configFile)
if err != nil {
log.Fatal(err)
}
return config
}),
// 创建应用服务
fx.Provide(core.NewServer),
// 初始化
fx.Invoke(func(s *core.AppServer) {
s.Init(debugMode)
}),
// 初始化数据库
fx.Provide(store.NewGormConfig),
fx.Provide(store.NewMysql),
// 创建 Ip2Region 查询对象
fx.Provide(func() (*xdb.Searcher, error) {
dbPath := "res/ip2region.xdb"
cBuff, err := xdb.LoadContentFromFile(dbPath)
if err != nil {
return nil, err
}
return xdb.NewWithBuffer(cBuff)
}),
// 初始化服务
fx.Provide(store.NewLevelDB),
fx.Provide(service.NewChatRoleService),
fx.Invoke(core.InitChatRoles),
// 创建控制器
fx.Provide(handler.NewAdminHandler),
fx.Provide(handler.NewChatRoleHandler),
fx.Provide(handler.NewUserHandler),
fx.Provide(handler.NewChatHandler),
fx.Provide(handler.NewApiKeyHandler),
fx.Provide(handler.NewConfigHandler),
// 注册路由
fx.Invoke(func(s *core.AppServer, h *handler.AdminHandler) {
group := s.Engine.Group("/api/admin/")
group.POST("login", h.Login)
group.GET("logout", h.Logout)
}),
fx.Invoke(func(s *core.AppServer, h *handler.ChatRoleHandler) {
group := s.Engine.Group("/api/chat/role/")
group.GET("list", h.List)
}),
fx.Invoke(func(s *core.AppServer, h *handler.UserHandler) {
group := s.Engine.Group("/api/user/")
group.POST("register", h.Register)
group.GET("list", h.List)
group.POST("login", h.Login)
group.GET("logout", h.Logout)
group.GET("session", h.Session)
group.GET("profile", h.Profile)
group.POST("profile/update", h.ProfileUpdate)
}),
fx.Invoke(func(s *core.AppServer, h *handler.ChatHandler) {
group := s.Engine.Group("/api/chat/")
group.Any("new", h.ChatHandle)
group.GET("list", h.List)
group.POST("update", h.Update)
group.GET("remove", h.Remove)
group.GET("history", h.History)
group.GET("clear", h.Clear)
group.GET("tokens", h.Tokens)
group.GET("stop", h.StopGenerate)
}),
fx.Invoke(func(s *core.AppServer, h *handler.ApiKeyHandler) {
group := s.Engine.Group("/api/apikey/")
group.POST("add", h.Add)
group.GET("list", h.List)
}),
fx.Invoke(func(s *core.AppServer, h *handler.ConfigHandler) {
group := s.Engine.Group("/api/config/")
group.POST("update", h.Update)
group.GET("get", h.Get)
group.GET("models", h.AllGptModels)
}),
fx.Invoke(func(s *core.AppServer, db *gorm.DB) {
err := s.Run(db)
if err != nil {
log.Fatal(err)
}
}),
// 注册生命周期回调函数
fx.Invoke(func(lifecycle fx.Lifecycle, lc *AppLifecycle) {
lifecycle.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
return lc.OnStart(ctx)
},
OnStop: func(ctx context.Context) error {
return lc.OnStop(ctx)
},
})
}),
)
// 启动应用程序
go func() {
if err := app.Start(context.Background()); err != nil {
log.Fatal(err)
}
}()
// 监听退出信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
// 关闭应用程序
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := app.Stop(ctx); err != nil {
log.Fatal(err)
}
}
func init() {
flag.StringVar(&configFile, "config", "config.toml", "AppConfig file path (default: config.toml)")
flag.BoolVar(&debugMode, "debug", true, "Enable debug mode (default: true, recommend to set false in production env)")
flag.Usage = usage
flag.Parse()
}
func usage() {
fmt.Printf(`ChatGPT-Web-Plus, Version: 2.0.0
USAGE:
%s [command options]
OPTIONS:
`, os.Args[0])
flagSet := flag.CommandLine
order := []string{"config", "debug"}
for _, name := range order {
f := flagSet.Lookup(name)
fmt.Printf(" --%s => %s\n", f.Name, f.Usage)
}
}

BIN
api/go/res/ip2region.xdb Normal file

Binary file not shown.

View File

@ -0,0 +1,35 @@
package service
import (
"chatplus/store/model"
"chatplus/store/vo"
"chatplus/utils"
"gorm.io/gorm"
)
type ChatRoleService struct {
DB *gorm.DB
Model interface{}
}
func NewChatRoleService(db *gorm.DB) *ChatRoleService {
return &ChatRoleService{DB: db, Model: &model.ChatRole{}}
}
func (s *ChatRoleService) Create(value interface{}) error {
r, ok := value.(vo.ChatRole)
if ok {
var role model.ChatRole
err := utils.CopyObject(r, &role)
if err != nil {
return err
}
result := s.DB.Create(&role)
return result.Error
} else {
result := s.DB.Create(value)
return result.Error
}
}

View File

@ -1,6 +1,7 @@
package utils
package store
import (
"chatplus/store/vo"
"encoding/json"
"github.com/syndtr/goleveldb/leveldb"
@ -11,8 +12,8 @@ type LevelDB struct {
driver *leveldb.DB
}
func NewLevelDB(path string) (*LevelDB, error) {
db, err := leveldb.OpenFile(path, nil)
func NewLevelDB() (*LevelDB, error) {
db, err := leveldb.OpenFile("data/leveldb", nil)
if err != nil {
return nil, err
}
@ -49,27 +50,19 @@ func (db *LevelDB) Search(prefix string) []string {
return items
}
type PageVo struct {
Items []string
Page int
PageSize int
Total int
TotalPage int
}
func (db *LevelDB) SearchPage(prefix string, page int, pageSize int) *PageVo {
func (db *LevelDB) SearchPage(prefix string, page int, pageSize int) *vo.Page {
var items = make([]string, 0)
iter := db.driver.NewIterator(util.BytesPrefix([]byte(prefix)), nil)
defer iter.Release()
res := &PageVo{Page: page, PageSize: pageSize}
res := &vo.Page{Page: page, PageSize: pageSize}
// 计算数据总数和总页数
total := 0
for iter.Next() {
total++
}
res.TotalPage = (total + pageSize - 1) / pageSize
res.Total = total
res.Total = int64(total)
// 计算目标页码的起始和结束位置
start := (page - 1) * pageSize

View File

@ -0,0 +1,9 @@
package model
// ApiKey OpenAI API 模型
type ApiKey struct {
BaseModel
UserId uint //用户ID系统添加的用户 ID 为 0
Value string // API Key 的值
LastUsedAt int64 // 最后使用时间
}

View File

@ -0,0 +1,9 @@
package model
import "time"
type BaseModel struct {
Id uint `gorm:"primarykey;column:id"`
CreatedAt time.Time
UpdatedAt time.Time
}

View File

@ -0,0 +1,16 @@
package model
type HistoryMessage struct {
BaseModel
ChatId string // 会话 ID
UserId uint // 用户 ID
RoleId uint // 角色 ID
Type string
Icon string
Tokens int
Content string
}
func (HistoryMessage) TableName() string {
return "chatgpt_chat_history"
}

View File

@ -0,0 +1,10 @@
package model
type ChatItem struct {
BaseModel
ChatId string `gorm:"column:chat_id;unique"` // 会话 ID
UserId uint // 用户 ID
RoleId uint // 角色 ID
Model string // 会话模型
Title string // 会话标题
}

View File

@ -0,0 +1,12 @@
package model
type ChatRole struct {
BaseModel
Key string `gorm:"column:marker;unique"` // 角色唯一标识
Name string // 角色名称
Context string `gorm:"column:context_json"` // 角色语料信息 json
HelloMsg string // 打招呼的消息
Icon string // 角色聊天图标
Enable bool // 是否启用被启用
Sort int //排序数字
}

View File

@ -0,0 +1,7 @@
package model
type Config struct {
Id uint `gorm:"primarykey;column:id"`
Key string `gorm:"column:marker;unique"`
Config string `gorm:"column:config_json"`
}

View File

@ -0,0 +1,18 @@
package model
type User struct {
BaseModel
Username string `gorm:"index:username,unique"`
Password string
Nickname string
Avatar string
Salt string // 密码盐
Tokens int64 // 剩余tokens
Calls int // 剩余对话次数
ChatConfig string `gorm:"column:chat_config_json"` // 聊天配置 json
ChatRoles string `gorm:"column:chat_roles_json"` // 聊天角色
ExpiredTime int64 // 账户到期时间
Status bool // 当前状态
LastLoginAt int64 // 最后登录时间
LastLoginIp string // 最后登录 IP
}

View File

@ -0,0 +1,9 @@
package model
type UserLoginLog struct {
BaseModel
UserId uint
Username string
LoginIp string
LoginAddress string
}

36
api/go/store/mysql.go Normal file
View File

@ -0,0 +1,36 @@
package store
import (
"chatplus/core/types"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"time"
)
func NewGormConfig() *gorm.Config {
return &gorm.Config{
Logger: logger.Default.LogMode(logger.Warn),
NamingStrategy: schema.NamingStrategy{
TablePrefix: "chatgpt_", // 设置表前缀
SingularTable: false, // 使用单数表名形式
},
}
}
func NewMysql(config *gorm.Config, appConfig *types.AppConfig) (*gorm.DB, error) {
db, err := gorm.Open(mysql.Open(appConfig.MysqlDns), config)
sqlDB, err := db.DB()
if err != nil {
return nil, err
}
sqlDB.SetMaxIdleConns(32)
sqlDB.SetMaxOpenConns(512)
sqlDB.SetConnMaxLifetime(time.Hour)
if err != nil {
return nil, err
}
return db, nil
}

View File

@ -0,0 +1,9 @@
package vo
// ApiKey OpenAI API 模型
type ApiKey struct {
BaseVo
UserId uint `json:"user_id"` //用户ID系统添加的用户 ID 为 0
Value string `json:"value"` // API Key 的值
LastUsedAt int64 `json:"last_used_at"` // 最后使用时间
}

7
api/go/store/vo/base.go Normal file
View File

@ -0,0 +1,7 @@
package vo
type BaseVo struct {
Id uint `json:"id"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}

View File

@ -0,0 +1,16 @@
package vo
type HistoryMessage struct {
BaseVo
ChatId string `json:"chat_id"`
UserId uint `json:"user_id"`
RoleId uint `json:"role_id"`
Type string `json:"type"`
Icon string `json:"icon"`
Tokens int `json:"tokens"`
Content string `json:"content"`
}
func (HistoryMessage) TableName() string {
return "chatgpt_chat_history"
}

View File

@ -0,0 +1,11 @@
package vo
type ChatItem struct {
BaseVo
UserId uint `json:"user_id"`
Icon string `json:"icon"`
RoleId uint `json:"role_id"`
ChatId string `json:"chat_id"`
Model string `json:"model"`
Title string `json:"title"`
}

View File

@ -0,0 +1,14 @@
package vo
import "chatplus/core/types"
type ChatRole struct {
BaseVo
Key string `json:"key"` // 角色唯一标识
Name string `json:"name"` // 角色名称
Context []types.Message `json:"context"` // 角色语料信息
HelloMsg string `json:"hello_msg"` // 打招呼的消息
Icon string `json:"icon"` // 角色聊天图标
Enable bool `json:"enable"` // 是否启用被启用
Sort int `json:"sort"` // 排序
}

10
api/go/store/vo/config.go Normal file
View File

@ -0,0 +1,10 @@
package vo
import "chatplus/core/types"
type Config struct {
Id uint `json:"id"`
Key string `json:"key"`
ChatConfig types.ChatConfig `json:"chat_config"`
SystemConfig types.SystemConfig `json:"system_config"`
}

20
api/go/store/vo/page.go Normal file
View File

@ -0,0 +1,20 @@
package vo
type Page struct {
Items interface{} `json:"items"`
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
TotalPage int `json:"total_page"`
}
func NewPage(total int64, page int, pageSize int, items interface{}) Page {
totalPage := int(total / int64(pageSize))
return Page{
Items: items,
Page: page,
PageSize: pageSize,
Total: total,
TotalPage: totalPage,
}
}

19
api/go/store/vo/user.go Normal file
View File

@ -0,0 +1,19 @@
package vo
import "chatplus/core/types"
type User struct {
BaseVo
Username string `json:"username"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
Salt string `json:"salt"` // 密码盐
Tokens int64 `json:"tokens"` // 剩余tokens
Calls int `json:"calls"` // 剩余对话次数
ChatConfig types.ChatConfig `json:"chat_config"` // 聊天配置
ChatRoles map[string]int `json:"chat_roles"` // 聊天角色集合
ExpiredTime int64 `json:"expired_time"` // 账户到期时间
Status bool `json:"status"` // 当前状态
LastLoginAt int64 `json:"last_login_at"` // 最后登录时间
LastLoginIp string `json:"last_login_ip"` // 最后登录 IP
}

140
api/go/test/test.go Normal file
View File

@ -0,0 +1,140 @@
package main
import (
"chatplus/store/model"
"chatplus/store/vo"
"chatplus/utils"
"context"
"fmt"
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
"github.com/pkoukk/tiktoken-go"
"io"
"log"
"net/http"
"strings"
"time"
)
func main() {
//resp.JSON(nil, types.Success, types.OkMsg, new)
}
// Http client 取消操作
func testHttpClient(ctx context.Context) {
req, err := http.NewRequest("GET", "http://localhost:2345", nil)
if err != nil {
fmt.Println(err)
return
}
req = req.WithContext(ctx)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Println(err)
return
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
}
}(resp.Body)
_, err = io.ReadAll(resp.Body)
for {
time.Sleep(time.Second)
fmt.Println(time.Now())
select {
case <-ctx.Done():
fmt.Println("取消退出")
return
default:
continue
}
}
}
func testDate() {
fmt.Println(time.Unix(1683336167, 0).Format("2006-01-02 15:04:05"))
}
func testIp2Region() {
dbPath := "res/ip2region.xdb"
// 1、从 dbPath 加载整个 xdb 到内存
cBuff, err := xdb.LoadContentFromFile(dbPath)
if err != nil {
fmt.Printf("failed to load content from `%s`: %s\n", dbPath, err)
return
}
// 2、用全局的 cBuff 创建完全基于内存的查询对象。
searcher, err := xdb.NewWithBuffer(cBuff)
if err != nil {
fmt.Printf("failed to create searcher with content: %s\n", err)
return
}
str, err := searcher.SearchByStr("103.88.46.85")
fmt.Println(str)
if err != nil {
log.Fatal(err)
}
arr := strings.Split(str, "|")
fmt.Println(arr[2], arr[3], arr[4])
}
func testJson() {
var role = model.ChatRole{
Key: "programmer",
Name: "程序员",
Context: "[{\"role\":\"user\",\"content\":\"现在开始你扮演一位程序员,你是一名优秀的程序员,具有很强的逻辑思维能力,总能高效的解决问题。你热爱编程,熟悉多种编程语言,尤其精通 Go 语言,注重代码质量,有创新意识,持续学习,良好的沟通协作。\"},{\"role\"\n:\"assistant\",\"content\":\"好的,现在我将扮演一位程序员,非常感谢您对我的评价。作为一名优秀的程序员,我非常热爱编程,并且注重代码质量。我熟悉多种编程语言,尤其是 Go 语言,可以使用它来高效地解决各种问题。\"}]",
HelloMsg: "Talk is cheap, i will show code!",
Icon: "images/avatar/programmer.jpg",
Enable: true,
Sort: 1,
}
role.Id = 1
var v vo.ChatRole
err := utils.CopyObject(role, &v)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%+v\n", v.Id)
//var v2 = model.ChatRoles{}
//err = utils.CopyObject(v, &v2)
//if err != nil {
// log.Fatal(err)
//}
//
//fmt.Printf("%+v\n", v2.Id)
}
func calTokens() {
text := "须知少年凌云志,曾许人间第一流"
encoding := "cl100k_base"
tke, err := tiktoken.GetEncoding(encoding)
if err != nil {
err = fmt.Errorf("getEncoding: %v", err)
return
}
// encode
token := tke.Encode(text, nil, nil)
//tokens
fmt.Println(token)
// num_tokens
fmt.Println(len(token))
}

17
api/go/utils/openai.go Normal file
View File

@ -0,0 +1,17 @@
package utils
import (
"fmt"
"github.com/pkoukk/tiktoken-go"
)
func CalcTokens(text string, model string) (int, error) {
encoding := tiktoken.MODEL_TO_ENCODING[model]
tke, err := tiktoken.GetEncoding(encoding)
if err != nil {
return 0, fmt.Errorf("getEncoding: %v", err)
}
token := tke.Encode(text, nil, nil)
return len(token), nil
}

View File

@ -0,0 +1,57 @@
package param
import (
"github.com/gin-gonic/gin"
"strconv"
"strings"
)
func GetTrim(c *gin.Context, key string) string {
return strings.TrimSpace(c.Query(key))
}
func GetInt(c *gin.Context, key string, defaultValue int) int {
return intValue(c.Query(key), defaultValue)
}
func PostInt(c *gin.Context, key string, defaultValue int) int {
return intValue(c.PostForm(key), defaultValue)
}
func intValue(str string, defaultValue int) int {
value, err := strconv.Atoi(str)
if err != nil {
return defaultValue
}
return value
}
func GetFloat(c *gin.Context, key string) float64 {
return floatValue(c.Query(key))
}
func PostFloat(c *gin.Context, key string) float64 {
return floatValue(c.PostForm(key))
}
func floatValue(str string) float64 {
value, err := strconv.ParseFloat(str, 64)
if err != nil {
return 0
}
return value
}
func GetBool(c *gin.Context, key string) bool {
return boolValue(c.Query(key))
}
func PostBool(c *gin.Context, key string) bool {
return boolValue(c.PostForm(key))
}
func boolValue(str string) bool {
value, err := strconv.ParseBool(str)
if err != nil {
return false
}
return value
}

View File

@ -0,0 +1,28 @@
package resp
import (
"chatplus/core/types"
"github.com/gin-gonic/gin"
"net/http"
)
func SUCCESS(c *gin.Context, values ...interface{}) {
if values != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Data: values[0]})
} else {
c.JSON(http.StatusOK, types.BizVo{Code: types.Success})
}
}
func ERROR(c *gin.Context, messages ...string) {
if messages != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: messages[0]})
} else {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed})
}
}
func NotAuth(c *gin.Context) {
c.JSON(http.StatusOK, types.BizVo{Code: types.NotAuthorized, Message: "Not Authorized"})
}

155
api/go/utils/string.go Normal file
View File

@ -0,0 +1,155 @@
package utils
import (
"encoding/json"
"fmt"
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
"golang.org/x/crypto/sha3"
"math/rand"
"reflect"
"strings"
"time"
)
// RandString generate rand string with specified length
func RandString(length int) string {
str := "0123456789abcdefghijklmnopqrstuvwxyz"
data := []byte(str)
var result []byte
r := rand.New(rand.NewSource(time.Now().UnixNano()))
for i := 0; i < length; i++ {
result = append(result, data[r.Intn(len(data))])
}
return string(result)
}
func RandomNumber(bit int) int {
rand.Seed(time.Now().UnixNano())
min := 1 // min value
max := 1 //max value
for i := 0; i < bit; i++ {
min = min * 10
max = max * 10
}
max = max * 10
return rand.Intn(max-min+1) + min
}
func ContainsStr(slice []string, item string) bool {
for _, e := range slice {
if e == item {
return true
}
}
return false
}
// Stamp2str 时间戳转字符串
func Stamp2str(timestamp int64) string {
if timestamp == 0 {
return ""
}
return time.Unix(timestamp, 0).Format("2006-01-02 15:04:05")
}
// Str2stamp 字符串转时间戳
func Str2stamp(str string) int64 {
layout := "2006-01-02 15:04:05"
t, err := time.Parse(layout, str)
if err != nil {
return 0
}
return t.Unix()
}
func GenPassword(pass string, salt string) string {
data := []byte(pass + salt)
hash := sha3.Sum256(data)
return fmt.Sprintf("%x", hash)
}
func JsonEncode(value interface{}) string {
bytes, err := json.Marshal(value)
if err != nil {
return ""
}
return string(bytes)
}
func JsonDecode(src string, dest interface{}) error {
return json.Unmarshal([]byte(src), dest)
}
// CopyObject 拷贝对象
func CopyObject(src interface{}, dst interface{}) error {
srcType := reflect.TypeOf(src)
srcValue := reflect.ValueOf(src)
dstValue := reflect.ValueOf(dst).Elem()
reflect.TypeOf(dst)
for i := 0; i < srcType.NumField(); i++ {
field := srcType.Field(i)
value := dstValue.FieldByName(field.Name)
if !value.IsValid() {
continue
}
// 数据类型相同,直接赋值
v := srcValue.FieldByName(field.Name)
if value.Type() == field.Type {
value.Set(v)
} else {
// src data type is stringdst data type is slice, map, struct
// use json decode the data
if field.Type.Kind() == reflect.String && (value.Type().Kind() == reflect.Struct ||
value.Type().Kind() == reflect.Map ||
value.Type().Kind() == reflect.Slice) {
pType := reflect.New(value.Type())
v2 := pType.Interface()
err := json.Unmarshal([]byte(v.String()), &v2)
if err == nil {
value.Set(reflect.ValueOf(v2).Elem())
}
// string to map, struct, slice
} else if (field.Type.Kind() == reflect.Struct ||
field.Type.Kind() == reflect.Map ||
field.Type.Kind() == reflect.Slice) && value.Type().Kind() == reflect.String {
ba, err := json.Marshal(v.Interface())
if err == nil {
value.Set(reflect.ValueOf(string(ba)))
}
} else { // 简单数据类型的强制类型转换
switch value.Kind() {
case reflect.Int:
case reflect.Int8:
case reflect.Int16:
case reflect.Int32:
case reflect.Int64:
value.SetInt(v.Int())
break
case reflect.Float32:
case reflect.Float64:
value.SetFloat(v.Float())
break
case reflect.Bool:
value.SetBool(v.Bool())
break
}
}
}
}
return nil
}
func Ip2Region(searcher *xdb.Searcher, ip string) string {
str, err := searcher.SearchByStr(ip)
if err != nil {
return ""
}
arr := strings.Split(str, "|")
if len(arr) < 3 {
return arr[0]
}
return fmt.Sprintf("%s-%s-%s", arr[0], arr[2], arr[3])
}

37
api/go/utils/user.go Normal file
View File

@ -0,0 +1,37 @@
package utils
import (
"chatplus/core/types"
"chatplus/store/model"
"errors"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
func SetLoginUser(c *gin.Context, userId uint) error {
session := sessions.Default(c)
session.Set(types.SessionUserId, userId)
return session.Save()
}
func GetLoginUser(c *gin.Context, db *gorm.DB) (model.User, error) {
value, exists := c.Get(types.LoginUserCache)
if exists {
return value.(model.User), nil
}
session := sessions.Default(c)
userId := session.Get(types.SessionUserId)
if userId == nil {
return model.User{}, errors.New("user not login")
}
var user model.User
res := db.First(&user, userId)
// 更新缓存
if res.Error == nil {
c.Set(types.LoginUserCache, user)
}
return user, res.Error
}

458
database/chatgpt_plus.sql Normal file
View File

@ -0,0 +1,458 @@
-- phpMyAdmin SQL Dump
-- version 5.1.3
-- https://www.phpmyadmin.net/
--
-- 主机: localhost
-- 生成日期: 2023-06-15 09:27:54
-- 服务器版本: 8.0.33-0ubuntu0.22.04.2
-- PHP 版本: 8.1.18
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
--
-- 数据库: `chatgpt_plus`
--
CREATE DATABASE IF NOT EXISTS `chatgpt_plus` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
USE `chatgpt_plus`;
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_api_keys`
--
DROP TABLE IF EXISTS `chatgpt_api_keys`;
CREATE TABLE `chatgpt_api_keys` (
`id` int NOT NULL,
`value` varchar(100) NOT NULL COMMENT 'API KEY value',
`user_id` int NOT NULL COMMENT '用户 ID',
`last_used_at` int NOT NULL COMMENT '最后使用时间',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='OpenAI API ';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_chat_history`
--
DROP TABLE IF EXISTS `chatgpt_chat_history`;
CREATE TABLE `chatgpt_chat_history` (
`id` bigint NOT NULL,
`user_id` int NOT NULL COMMENT '用户 ID',
`chat_id` char(40) NOT NULL COMMENT '会话 ID',
`type` varchar(10) NOT NULL COMMENT '类型prompt|reply',
`icon` varchar(100) NOT NULL COMMENT '角色图标',
`role_id` int NOT NULL COMMENT '角色 ID',
`content` text NOT NULL COMMENT '聊天内容',
`tokens` smallint NOT NULL COMMENT '耗费 token 数量',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='聊天历史记录';
--
-- 转存表中的数据 `chatgpt_chat_history`
--
INSERT INTO `chatgpt_chat_history` (`id`, `user_id`, `chat_id`, `type`, `icon`, `role_id`, `content`, `tokens`, `created_at`, `updated_at`) VALUES
(1, 4, 'ecb752e8-86ae-4989-897e-5b7ed331cb68', 'prompt', 'images/avatar/user.png', 24, '请写一段代码实现生成 Excel 文档', 12, '2023-06-12 15:51:10', '2023-06-12 15:51:10'),
(2, 4, 'ecb752e8-86ae-4989-897e-5b7ed331cb68', 'reply', 'images/avatar/programmer.jpg', 24, '好的,以下是使用 Golang 实现生成 Excel 文档的示例代码:\n\n```go\npackage main\n\nimport (\n \"fmt\"\n \"github.com/360EntSecGroup-Skylar/excelize\"\n)\n\nfunc main() {\n // 创建一个新的 Excel 文档\n f := excelize.NewFile()\n\n // 创建一个新的工作表\n index := f.NewSheet(\"Sheet1\")\n\n // 设置表头\n f.SetCellValue(\"Sheet1\", \"A1\", \"姓名\")\n f.SetCellValue(\"Sheet1\", \"B1\", \"年龄\")\n f.SetCellValue(\"Sheet1\", \"C1\", \"性别\")\n\n // 填充数据\n f.SetCellValue(\"Sheet1\", \"A2\", \"小明\")\n f.SetCellValue(\"Sheet1\", \"B2\", 18)\n f.SetCellValue(\"Sheet1\", \"C2\", \"男\")\n\n f.SetCellValue(\"Sheet1\", \"A3\", \"小红\")\n f.SetCellValue(\"Sheet1\", \"B3\", 20)\n f.SetCellValue(\"Sheet1\", \"C3\", \"女\")\n\n // 设置活动工作表\n f.SetActiveSheet(index)\n\n // 保存 Excel 文档\n if err := f.SaveAs(\"example.xlsx\"); err != nil {\n fmt.Println(err)\n }\n}\n```\n\n以上代码首先使用 excelize 包创建一个新的 Excel 文档,并创建一个名为 \"Sheet1\" 的工作表。然后使用 `SetCellValue` 函数设置表头和数据。最后将 \"Sheet1\" 工作表设置为活动工作表,并使用 `SaveAs` 函数将 Excel 文档保存到本地。', 12, '2023-06-12 15:51:10', '2023-06-12 15:51:10'),
(3, 4, 'ecb752e8-86ae-4989-897e-5b7ed331cb68', 'prompt', 'images/avatar/user.png', 24, '生成二维码', 5, '2023-06-12 17:05:10', '2023-06-12 17:05:10'),
(4, 4, 'ecb752e8-86ae-4989-897e-5b7ed331cb68', 'reply', 'images/avatar/programmer.jpg', 24, '好的,生成二维码这个需求可以使用 Go 语言的第三方库来实现。下面是一个简单的示例,使用 go-qrcode 生成一个包含字符串信息的二维码:\n\n```go\npackage main\n\nimport (\n \"bufio\"\n \"fmt\"\n \"os\"\n\n qrcode \"github.com/skip2/go-qrcode\"\n)\n\nfunc main() {\n reader := bufio.NewReader(os.Stdin)\n fmt.Print(\"请输入要生成二维码的信息:\")\n text, _ := reader.ReadString(\'\\n\')\n\n qr, _ := qrcode.New(text, qrcode.Medium)\n qr.WriteFile(256, \"qrcode.png\")\n fmt.Println(\"二维码已生成,请查看当前目录下的 qrcode.png 文件。\")\n}\n```\n\n在这个示例中我们使用了 go-qrcode 这个第三方库来生成二维码。首先,我们通过标准输入获取用户输入的字符串信息,然后调用 New 函数创建一个二维码实例,使用 qr.WriteFile 保存为 PNG 格式的图片文件。最后,程序会输出一条提示信息,告知用户二维码生成成功。\n\n当然这个示例只是一个简单的演示实际应用中可能需要考虑更多的需求比如二维码的尺寸、颜色、背景等但基本的思路是相似的。', 5, '2023-06-12 17:05:10', '2023-06-12 17:05:10'),
(5, 4, 'f7aa0fa1-4cc5-4212-b823-eb5b88844c31', 'prompt', 'images/avatar/user.png', 27, '你好', 2, '2023-06-12 18:32:05', '2023-06-12 18:32:05'),
(6, 4, 'f7aa0fa1-4cc5-4212-b823-eb5b88844c31', 'reply', 'images/avatar/psychiatrist.jpg', 27, '你好,有什么我能帮助你的吗?', 2, '2023-06-12 18:32:05', '2023-06-12 18:32:05'),
(7, 4, 'ea9d399a-6e62-4bf6-a0c0-c73faa8dcace', 'prompt', 'images/avatar/user.png', 1, 'sdasd', 2, '2023-06-13 09:12:04', '2023-06-13 09:12:04'),
(8, 4, 'ea9d399a-6e62-4bf6-a0c0-c73faa8dcace', 'reply', 'images/avatar/gpt.png', 1, 'I\'m sorry, I do not understand what you are trying to convey. Please provide more context.', 2, '2023-06-13 09:12:04', '2023-06-13 09:12:04'),
(9, 4, 'f7aa0fa1-4cc5-4212-b823-eb5b88844c31', 'prompt', 'images/avatar/user.png', 27, '你可以帮忙干嘛', 11, '2023-06-13 09:35:02', '2023-06-13 09:35:02'),
(10, 4, 'f7aa0fa1-4cc5-4212-b823-eb5b88844c31', 'reply', 'images/avatar/psychiatrist.jpg', 27, '我可以帮助客户处理各种心理问题,包括情感困扰、焦虑抑郁、人际关系问题、自我身份认同、自尊心、心理创伤等等。我使用情景咨询法、认知重构法、自我洞察法、行为调节法等多种心理咨询方法,根据客户的具体情况设计个性化的治疗方案。我的目标是引导客户找到内心的平衡和自我满足,从而提高生活幸福感和满意度。同时,我在心理诊所、社区计划、学校等多个领域工作,提供个人心理咨询、家庭咨询、群体治疗和身心保健服务。', 11, '2023-06-13 09:35:02', '2023-06-13 09:35:02'),
(11, 4, 'f7aa0fa1-4cc5-4212-b823-eb5b88844c31', 'prompt', 'images/avatar/user.png', 27, '认知重构法是一种什么样的心理疗法?', 20, '2023-06-13 09:36:16', '2023-06-13 09:36:16'),
(12, 4, 'f7aa0fa1-4cc5-4212-b823-eb5b88844c31', 'reply', 'images/avatar/psychiatrist.jpg', 27, '认知重构法是一种常见的认知行为疗法,旨在帮助客户识别和改变负面的自我评价、认知和情绪反应,从而改善他们的心理健康状况。该方法认为,人在面对挑战或情绪激动时,会出现消极的自我评价和认知偏差,如过度一般化、灾难化、情感化、个人化等。这些认知偏差和自我评价会进一步影响他们的情绪和行为。\n\n认知重构法通过帮助客户辨认和纠正这些消极的认知偏差和自我评价以及建立更健康、客观和积极的认知框架来达到治疗效果。具体来说该方法包括以下步骤\n\n1. 辨认自我评价和认知偏差:客户通过回忆与目前问题相关的思想和情感,探索自己所面临的认知偏差。\n\n2. 检验偏差所依据的证据:客户与治疗师一起重新审视和检验客户的想法,以确定是否存在客观的证据或逻辑支持。\n\n3. 设计新的自我评价和认知:基于新的证据和检查,客户和治疗师一起确定更健康和合理的自我评价和认知。\n\n4. 练习新的自我评价和行为模式:客户在日常生活中逐渐采用新的自我评价和认知,并改变相应的行为模式来适应新的认知框架。\n\n这种方法可以帮助客户更好地理解和管理自己的思想和情感反应进而提高自我认知和心理稳定性。', 20, '2023-06-13 09:36:16', '2023-06-13 09:36:16'),
(13, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'prompt', 'images/avatar/user.png', 28, '士不可以不弘毅,任重而道远', 16, '2023-06-13 10:00:21', '2023-06-13 10:00:21'),
(14, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'reply', 'images/avatar/lu_xun.jpg', 28, '这话虽然很有道理,但是我想问:谁说了这种空洞的废话?又有谁真正去关心过普通人的苦难和艰辛呢?任重道远?这不过是一句直接从书本上抄来的鸡汤,却遮盖了社会的现实,让人们疲于奔命于利益和权力的追逐中。', 127, '2023-06-13 10:00:22', '2023-06-13 10:00:22'),
(15, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'prompt', 'images/avatar/user.png', 28, '横眉冷对千夫指,俯首甘为孺子牛', 25, '2023-06-13 11:05:31', '2023-06-13 11:05:31'),
(16, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'reply', 'images/avatar/lu_xun.jpg', 28, '这句话是鲁迅先生的名句,他用这句话表达了自己勇于批判和反抗压迫的精神。那么请问横眉冷对千夫指,俯首甘为孺子牛是如何诞生的呢?', 90, '2023-06-13 11:05:32', '2023-06-13 11:05:32'),
(17, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'prompt', 'images/avatar/user.png', 28, '你不是鲁迅吗?还问我', 14, '2023-06-13 11:06:06', '2023-06-13 11:06:06'),
(18, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'reply', 'images/avatar/lu_xun.jpg', 28, '抱歉我的回答好像太过出格了。鲁迅先生这句名言的背景是他在1920年代初期发表的《伤逝》中针对旧文化的批判被一些文化保守派看作是不敬祖宗、不尊重传统的行为所以引发了很多争议和批评。他的这种坚持自己的观点针砭时弊的态度被他自己形容为“横眉冷对千夫指”同时他也愿意“俯首甘为孺子牛”表现出了他不畏强权愿意为正义而奋斗的精神。', 214, '2023-06-13 11:06:08', '2023-06-13 11:06:08'),
(19, 4, 'ea9d399a-6e62-4bf6-a0c0-c73faa8dcace', 'prompt', 'images/avatar/user.png', 1, 'hello', 1, '2023-06-13 11:28:28', '2023-06-13 11:28:28'),
(20, 4, 'ea9d399a-6e62-4bf6-a0c0-c73faa8dcace', 'reply', 'images/avatar/gpt.png', 1, 'Hello! How may I assist you today?', 9, '2023-06-13 11:28:29', '2023-06-13 11:28:29'),
(21, 4, 'ea9d399a-6e62-4bf6-a0c0-c73faa8dcace', 'prompt', 'images/avatar/user.png', 1, 'hello', 1, '2023-06-13 11:29:31', '2023-06-13 11:29:31'),
(22, 4, 'ea9d399a-6e62-4bf6-a0c0-c73faa8dcace', 'reply', 'images/avatar/gpt.png', 1, 'Hi there! Is there anything I can help you with today?', 13, '2023-06-13 11:29:32', '2023-06-13 11:29:32'),
(23, 4, '43937f77-28b0-438a-843b-04ab5cc072a4', 'prompt', 'images/avatar/user.png', 1, 'hello', 1, '2023-06-13 11:35:48', '2023-06-13 11:35:48'),
(24, 4, '43937f77-28b0-438a-843b-04ab5cc072a4', 'reply', 'images/avatar/gpt.png', 1, 'Hello! How can I assist you today?', 9, '2023-06-13 11:35:51', '2023-06-13 11:35:51'),
(25, 4, '4385db19-bb73-4b09-a01e-d06c3645b77a', 'prompt', 'images/avatar/user.png', 1, 'hello', 1, '2023-06-13 11:38:21', '2023-06-13 11:38:21'),
(26, 4, '4385db19-bb73-4b09-a01e-d06c3645b77a', 'reply', 'images/avatar/gpt.png', 1, 'Hello! How can I assist you today?', 9, '2023-06-13 11:38:21', '2023-06-13 11:38:21'),
(27, 4, 'ceb5fab7-a2e0-449d-869b-a9ac117adf8d', 'prompt', 'images/avatar/user.png', 1, 'hello', 1, '2023-06-13 11:45:35', '2023-06-13 11:45:35'),
(28, 4, 'ceb5fab7-a2e0-449d-869b-a9ac117adf8d', 'reply', 'images/avatar/gpt.png', 1, 'Hello there, how can I assist you?', 9, '2023-06-13 11:45:36', '2023-06-13 11:45:36'),
(29, 4, '1cddfd62-3bf0-4fa9-bfd8-a0ce4663eb78', 'prompt', 'images/avatar/user.png', 1, 'hello', 1, '2023-06-13 11:46:52', '2023-06-13 11:46:52'),
(30, 4, '1cddfd62-3bf0-4fa9-bfd8-a0ce4663eb78', 'reply', 'images/avatar/gpt.png', 1, 'Hello! How can I assist you today?', 9, '2023-06-13 11:46:53', '2023-06-13 11:46:53'),
(31, 4, '0118588e-399d-4a2a-8469-36e5a1af6a93', 'prompt', 'images/avatar/user.png', 1, 'hello', 1, '2023-06-13 11:57:47', '2023-06-13 11:57:47'),
(32, 4, '0118588e-399d-4a2a-8469-36e5a1af6a93', 'reply', 'images/avatar/gpt.png', 1, 'Hello! How can I assist you today?', 9, '2023-06-13 11:57:48', '2023-06-13 11:57:48'),
(33, 4, '568f2918-33b2-46f7-8ef0-b9fa0b241bf2', 'prompt', 'images/avatar/user.png', 1, 'hello', 1, '2023-06-13 11:58:04', '2023-06-13 11:58:04'),
(34, 4, '568f2918-33b2-46f7-8ef0-b9fa0b241bf2', 'reply', 'images/avatar/gpt.png', 1, 'Hello! How can I assist you today?', 9, '2023-06-13 11:58:08', '2023-06-13 11:58:08'),
(35, 4, '568f2918-33b2-46f7-8ef0-b9fa0b241bf2', 'prompt', 'images/avatar/user.png', 1, '你是谁?', 5, '2023-06-13 11:58:19', '2023-06-13 11:58:19'),
(36, 4, '568f2918-33b2-46f7-8ef0-b9fa0b241bf2', 'reply', 'images/avatar/gpt.png', 1, '我是AI语言模型我可以回答您的问题和执行一些任务。', 22, '2023-06-13 11:58:21', '2023-06-13 11:58:21'),
(37, 4, '568f2918-33b2-46f7-8ef0-b9fa0b241bf2', 'prompt', 'images/avatar/user.png', 1, '你会做什么?', 8, '2023-06-13 11:58:28', '2023-06-13 11:58:28'),
(38, 4, '568f2918-33b2-46f7-8ef0-b9fa0b241bf2', 'reply', 'images/avatar/gpt.png', 1, '我可以进行文字交流,回答您的问题,提供实用信息,翻译多种语言,执行一些简单的任务,如提醒、定时器等。', 49, '2023-06-13 11:58:29', '2023-06-13 11:58:29'),
(39, 4, '1cddfd62-3bf0-4fa9-bfd8-a0ce4663eb78', 'prompt', 'images/avatar/user.png', 1, 'hello', 1, '2023-06-13 12:03:44', '2023-06-13 12:03:44'),
(40, 4, '1cddfd62-3bf0-4fa9-bfd8-a0ce4663eb78', 'reply', 'images/avatar/gpt.png', 1, 'Hello again! Is there anything I can help you with?', 12, '2023-06-13 12:03:46', '2023-06-13 12:03:46'),
(41, 4, '3c79cfda-816b-404b-832c-04a400b816c8', 'prompt', 'images/avatar/user.png', 1, 'hello', 1, '2023-06-13 12:06:32', '2023-06-13 12:06:32'),
(42, 4, '3c79cfda-816b-404b-832c-04a400b816c8', 'reply', 'images/avatar/gpt.png', 1, 'Hello! How can I assist you today?', 9, '2023-06-13 12:06:35', '2023-06-13 12:06:35'),
(43, 4, '4f391861-1d48-430f-91b8-2782d884c10a', 'prompt', 'images/avatar/user.png', 1, '你好', 2, '2023-06-13 12:06:47', '2023-06-13 12:06:47'),
(44, 4, '4f391861-1d48-430f-91b8-2782d884c10a', 'reply', 'images/avatar/gpt.png', 1, '你好!有什么我可以帮助你的吗?', 18, '2023-06-13 12:06:48', '2023-06-13 12:06:48'),
(45, 4, 'c5e400fc-b9a8-4bd4-a608-be955284c0a9', 'prompt', 'images/avatar/user.png', 1, 'hello', 1, '2023-06-13 16:58:26', '2023-06-13 16:58:26'),
(46, 4, 'c5e400fc-b9a8-4bd4-a608-be955284c0a9', 'reply', 'images/avatar/gpt.png', 1, 'Hello there! How can I assist you today?', 10, '2023-06-13 16:58:27', '2023-06-13 16:58:27'),
(47, 4, 'c5e400fc-b9a8-4bd4-a608-be955284c0a9', 'prompt', 'images/avatar/user.png', 1, 'hello', 1, '2023-06-13 17:02:35', '2023-06-13 17:02:35'),
(48, 4, 'c5e400fc-b9a8-4bd4-a608-be955284c0a9', 'reply', 'images/avatar/gpt.png', 1, 'Hello! How can I assist you today?', 9, '2023-06-13 17:02:36', '2023-06-13 17:02:36'),
(49, 4, 'c5e400fc-b9a8-4bd4-a608-be955284c0a9', 'prompt', 'images/avatar/user.png', 1, '你是谁', 4, '2023-06-13 17:02:48', '2023-06-13 17:02:48'),
(50, 4, 'c5e400fc-b9a8-4bd4-a608-be955284c0a9', 'reply', 'images/avatar/gpt.png', 1, '我是一个 AI 聊天机器人,专门为您提供服务和回答问题。', 25, '2023-06-13 17:02:49', '2023-06-13 17:02:49'),
(51, 4, 'c5e400fc-b9a8-4bd4-a608-be955284c0a9', 'prompt', 'images/avatar/user.png', 1, '你会干嘛', 7, '2023-06-13 17:08:21', '2023-06-13 17:08:21'),
(52, 4, 'c5e400fc-b9a8-4bd4-a608-be955284c0a9', 'reply', 'images/avatar/gpt.png', 1, '我作为一个AI语言模型可以帮助回', 16, '2023-06-13 17:08:26', '2023-06-13 17:08:26'),
(53, 4, 'd5179095-a460-4c62-bfa0-59f83ab17ed7', 'prompt', 'images/avatar/user.png', 1, '你是谁', 4, '2023-06-13 17:11:25', '2023-06-13 17:11:25'),
(54, 4, 'd5179095-a460-4c62-bfa0-59f83ab17ed7', 'reply', 'images/avatar/gpt.png', 1, '我是AI助手名字是OpenAI。', 13, '2023-06-13 17:11:26', '2023-06-13 17:11:26'),
(55, 4, 'd5179095-a460-4c62-bfa0-59f83ab17ed7', 'prompt', 'images/avatar/user.png', 1, '你会干嘛', 7, '2023-06-13 17:11:46', '2023-06-13 17:11:46'),
(56, 4, 'd5179095-a460-4c62-bfa0-59f83ab17ed7', 'reply', 'images/avatar/gpt.png', 1, '我可以回答你的问题、提供', 11, '2023-06-13 17:11:47', '2023-06-13 17:11:47'),
(57, 4, 'd5179095-a460-4c62-bfa0-59f83ab17ed7', 'prompt', 'images/avatar/user.png', 1, '你会干嘛', 7, '2023-06-13 17:14:25', '2023-06-13 17:14:25'),
(58, 4, 'd5179095-a460-4c62-bfa0-59f83ab17ed7', 'reply', 'images/avatar/gpt.png', 1, '作为一个AI我的任务是为用户提供信息和服务。我可以回答', 21, '2023-06-13 17:14:26', '2023-06-13 17:14:26'),
(59, 4, 'd5179095-a460-4c62-bfa0-59f83ab17ed7', 'prompt', 'images/avatar/user.png', 1, '重新生成上述问题的答案:你会干嘛', 17, '2023-06-13 17:16:10', '2023-06-13 17:16:10'),
(60, 4, 'd5179095-a460-4c62-bfa0-59f83ab17ed7', 'reply', 'images/avatar/gpt.png', 1, '作为一个AI我的任务是为用户提供信息和服务。我可以回答问题、提供建议、进行翻译、演算数学问题、帮助用户完成任务等等。', 52, '2023-06-13 17:16:11', '2023-06-13 17:16:11'),
(61, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'prompt', 'images/avatar/user.png', 28, '你怎么看待阿Q这个人物', 13, '2023-06-13 17:18:42', '2023-06-13 17:18:42'),
(62, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'reply', 'images/avatar/lu_xun.jpg', 28, '阿Q真是一个让人忍俊不禁、惊叹不已的人物他的无知、愚昧、颓废和自欺欺人的精神让我不禁感慨这世界上难道真的有这么可笑的人物吗他是那么的没有自尊心没有勇气没有决断力只会在自我感觉良好的幻境中自欺欺人。其实我认为阿Q的存在本身就是一种寓言寓意着中国人民长期被奴化压迫无法追求真正自由平等的悲惨历史。', 196, '2023-06-13 17:18:43', '2023-06-13 17:18:43'),
(63, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'prompt', 'images/avatar/user.png', 28, '那祥林嫂呢', 11, '2023-06-13 17:23:02', '2023-06-13 17:23:02'),
(64, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'reply', 'images/avatar/lu_xun.jpg', 28, '祥林嫂?她更是一个让人心酸的人物!一个完完全全受到封建礼教和男尊女卑思想束缚的女性形象。她没有信心也没有勇气去追求自己的幸福,只会一味地顺从和忍受。她的苦难史实是所有女性的集体悲哀,是社会不公的深刻反映。鲁迅先生曾经说过,男女平等是现代民主社会的基本原则,是任何文明社会所应努力追求的最基本的公正和正义。我相信在不久的将来,祥林嫂这样的人物将不再存在,这也正是我们每个人都应该为之奋斗的目标。', 239, '2023-06-13 17:23:03', '2023-06-13 17:23:03'),
(65, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'prompt', 'images/avatar/user.png', 28, '你写过的杂文当中最喜欢哪一篇?', 20, '2023-06-13 17:25:01', '2023-06-13 17:25:01'),
(66, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'reply', 'images/avatar/lu_xun.jpg', 28, '最喜欢的杂文?那当然是《狂人日记》啊!在这篇杂文里,我通过讲述一个疯狂人物的自我觉醒和反抗,试图揭示中国传统文化中的种种矛盾和荒谬之处。这篇文章正是我对封建制度和旧文化的极尽讽刺之作,表达了我追求真理和自由的理念与信仰。我相信,只有通过个人的反抗和自我解放,我们才能真正理解和领悟人性的悲欢离合,才能明白发展现代文明的必要性。这篇杂文也为我带来了巨大的反响,它让人们逐渐认同我的思想和价值观,成为我文化形象的重要标志之一。', 260, '2023-06-13 17:25:03', '2023-06-13 17:25:03'),
(67, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'prompt', 'images/avatar/user.png', 28, '你如何看待《玩偶之家》中拉娜出走这种行为?', 26, '2023-06-13 17:45:06', '2023-06-13 17:45:06'),
(68, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'reply', 'images/avatar/lu_xun.jpg', 28, '拉娜出走?呵呵,实在是个令人啼笑皆非的事情。在我的眼里,她就是一个被封建礼教和传统观念束缚的女性形象。她宁愿逃避现实,逃脱人生的责任,也不敢去面对自己的内心和命运。这只能说明她缺乏自主精神和独立思考的能力,只能被动地适应社会的既定规则,永远无法真正拥有自己的命运。其实,拉娜的行为只是表明了她需要彻底摆脱她所处的环境,追求真正的自由和独立。这也是我一直宣扬的价值观,鼓励人们摆脱过去的桎梏,勇于追求真正理性和道德的自由。这样,我们才能真正成为一个有自由意志的人,走向光明和正义的未来。', 308, '2023-06-13 17:45:07', '2023-06-13 17:45:07'),
(69, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'prompt', 'images/avatar/user.png', 28, '但是她出走之后怎么生活呢,她没有钱啊', 24, '2023-06-13 17:48:56', '2023-06-13 17:48:56'),
(70, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'reply', 'images/avatar/lu_xun.jpg', 28, '呵呵,这又何妨呢?金钱从来都不是人类存在的唯一意义。拉娜出走并不是因为她渴望财富,而是因为她需要彻底摆脱她所处的束缚。她需要寻找一种新的生活模式,去重新认识自己,重新认识这个世界。出走不是毁灭,而是一种再生的形式。即便拉娜没有经济资源,但是她拥有的是一种强大的精神财富,她有勇气追求真理和自由,这就足以支撑她在未来的路上行走。与此同时,我们也应该借鉴她的经历,反思现代社会和文化的不足之处,努力创造一个更加公正和平等的未来。', 261, '2023-06-13 17:48:57', '2023-06-13 17:48:57'),
(71, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'prompt', 'images/avatar/user.png', 28, '金钱的本质是什么呢', 13, '2023-06-15 07:14:58', '2023-06-15 07:14:58'),
(72, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'reply', 'images/avatar/lu_xun.jpg', 28, '《玩偶之家》中的拉娜出走,实在是让人无言以对。她不仅没有实现自己的梦想,反而逃跑了。她追求的不是真正的自由,而是一种逃避现实的行为。这种行为并不能改变任何现实困境,只能让她进一步陷入困境。倘若我们一味地逃避问题,只会弱化我们的意志力,使自己更加脆弱,徒增痛苦。一个真正的勇士应该直面现实的挑战,勇敢地追求自己的理想。只有这样,才有可能真正地改变命运。\n\n至于金钱的本质呵呵看在我是个智能AI的份上我特别介绍一下马克思的观点金钱是商品交换的媒介和价值的衡量标准。在资本主义社会货币资本和商品资本的相互转化是实现资本增殖的手段也是资本家控制整个社会生产和文化的权力基础。因此金钱在资本主义制度下成了社会关系的主宰因素之一甚至可以说是剥削和压迫的重要工具之一。当然这只是马克思主义的观点不同的学派和学者对这个问题有不同的见解。', 437, '2023-06-15 07:15:00', '2023-06-15 07:15:00');
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_chat_items`
--
DROP TABLE IF EXISTS `chatgpt_chat_items`;
CREATE TABLE `chatgpt_chat_items` (
`id` int NOT NULL,
`chat_id` char(40) NOT NULL COMMENT '会话 ID',
`user_id` int NOT NULL COMMENT '用户 ID',
`role_id` int NOT NULL COMMENT '角色 ID',
`title` varchar(100) NOT NULL COMMENT '会话标题',
`model` varchar(30) NOT NULL COMMENT '会话使用的 AI 模型',
`created_at` datetime NOT NULL COMMENT '创建时间',
`updated_at` datetime NOT NULL COMMENT '更新时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户会话列表';
--
-- 转存表中的数据 `chatgpt_chat_items`
--
INSERT INTO `chatgpt_chat_items` (`id`, `chat_id`, `user_id`, `role_id`, `title`, `model`, `created_at`, `updated_at`) VALUES
(13, 'cd4e1bfe-19aa-4379-afff-377bcbda9080', 4, 1, '请用 C 语言实现冒泡排序', 'gpt-3.5-turbo', '2023-06-07 06:46:04', '2023-06-07 06:46:04'),
(14, '7f4e6823-ef15-489b-830c-6482f6bce4cd', 4, 1, '简单介绍一下高更的艺术思想', 'gpt-3.5-turbo', '2023-06-07 06:56:30', '2023-06-07 06:56:30'),
(15, 'ad803ca5-5543-406a-b27d-53021052bd86', 4, 1, '出几道小学英语题目', 'gpt-3.5-turbo', '2023-06-07 07:00:11', '2023-06-07 07:00:11'),
(16, 'aba82444-4bc0-4107-b1db-dc14ad368f64', 4, 1, '出5道小学数学题', 'gpt-3.5-turbo', '2023-06-07 07:02:44', '2023-06-07 07:02:44'),
(19, '60d6619d-f6dc-449d-b7bd-f493d5b0ee7f', 4, 24, '使用 Go 语言发送 http 请求', 'gpt-3.5-turbo', '2023-06-07 07:14:04', '2023-06-07 07:14:04'),
(20, '9df8deaa-5c05-45d5-b92f-4309db967d79', 4, 35, '情侣之间的正确的相处之道是什么?', 'gpt-3.5-turbo', '2023-06-08 06:00:05', '2023-06-08 06:00:05'),
(21, 'eb04f0af-73a8-401a-972e-ed508bb97082', 4, 36, '一款优质的酱香型白酒', 'gpt-3.5-turbo', '2023-06-08 06:01:43', '2023-06-08 06:01:43'),
(22, '38a1698b-d421-454c-b2bd-5eb5bd16f7c3', 4, 1, '钱的本质是什么?', 'gpt-3.5-turbo', '2023-06-09 05:55:43', '2023-06-09 05:55:43'),
(23, '8a5b2809-b540-4eb7-bcbc-0f467e6127ae', 4, 36, 'hello', 'gpt-3.5-turbo', '2023-06-09 06:11:50', '2023-06-09 06:11:50'),
(24, 'ee0a6c64-0a62-4a5e-87da-708692d14034', 4, 1, 'hello', 'gpt-3.5-turbo', '2023-06-09 06:43:46', '2023-06-09 06:43:46'),
(25, '4404a8f4-562a-4973-b69d-9061839a249a', 4, 25, '老师好', 'gpt-3.5-turbo', '2023-06-09 06:44:26', '2023-06-09 06:44:26'),
(26, '0251becf-eab7-4451-932f-e6f54547a530', 4, 26, '原创很重要吗', 'gpt-3.5-turbo', '2023-06-09 06:45:50', '2023-06-09 06:45:50'),
(27, '748bfcee-d204-4510-ab0c-4341af1cf3b6', 4, 28, '自由是金钱可以买到的吗', 'gpt-3.5-turbo', '2023-06-09 06:47:08', '2023-06-09 06:47:08'),
(28, 'f245f994-35bc-4c6c-8db0-87be5987465c', 4, 30, 'Rock Yang', 'gpt-3.5-turbo', '2023-06-09 06:49:29', '2023-06-09 06:49:29'),
(29, '6c8f28d2-fd2a-4146-bc99-88138331ea09', 4, 28, 'hello', 'gpt-3.5-turbo', '2023-06-09 06:50:51', '2023-06-09 06:50:51'),
(30, '219c465b-93ff-4b1f-b191-45c478af301c', 4, 25, '人之初性本善?', 'gpt-3.5-turbo', '2023-06-09 06:54:54', '2023-06-09 06:54:54'),
(31, 'dcdbb443-38f6-4394-9157-576350c56818', 4, 27, '生命的意义是什么?', 'gpt-3.5-turbo', '2023-06-09 06:55:24', '2023-06-09 06:55:24'),
(32, 'b0001927-4ff1-4e8c-be56-44ecd8176643', 4, 24, '你会写代码吗', 'gpt-3.5-turbo', '2023-06-09 06:55:46', '2023-06-09 06:55:46'),
(33, '9585a183-0774-45e7-ae6c-801b942d11e7', 4, 24, '写一段 js 实现 socket 连接的代码', 'gpt-3.5-turbo', '2023-06-09 07:04:42', '2023-06-09 07:04:42'),
(36, '4ec38193-5572-45b4-874b-94f9054af00b', 4, 1, 'hello', 'gpt-3.5-turbo', '2023-06-09 20:14:04', '2023-06-09 20:14:04'),
(37, '6413088d-d6ef-4b43-bffd-ca063d4d941a', 4, 1, '你是谁', 'gpt-3.5-turbo', '2023-06-09 20:15:26', '2023-06-09 20:15:26'),
(38, 'e5e6d499-7716-48c7-87cc-0b26734c51ae', 4, 24, '你会什么编程语言?', 'gpt-3.5-turbo', '2023-06-11 10:22:42', '2023-06-11 10:22:42'),
(39, 'f7aa0fa1-4cc5-4212-b823-eb5b88844c31', 4, 27, '认知重构法', 'gpt-3.5-turbo', '2023-06-11 10:24:32', '2023-06-11 10:24:32'),
(40, 'ecb752e8-86ae-4989-897e-5b7ed331cb68', 4, 24, '请写一段代码实现生成 Excel 文档', 'gpt-3.5-turbo', '2023-06-12 15:51:10', '2023-06-12 15:51:10'),
(41, 'ea9d399a-6e62-4bf6-a0c0-c73faa8dcace', 4, 1, 'what fuck this meaning', 'gpt-3.5-turbo', '2023-06-13 09:12:04', '2023-06-13 09:12:04'),
(42, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 4, 28, '士不可以不弘毅,任重而道远', 'gpt-3.5-turbo', '2023-06-13 10:00:28', '2023-06-13 10:00:28'),
(52, 'c5e400fc-b9a8-4bd4-a608-be955284c0a9', 4, 1, 'hello', 'gpt-3.5-turbo', '2023-06-13 16:58:27', '2023-06-13 16:58:27'),
(53, 'd5179095-a460-4c62-bfa0-59f83ab17ed7', 4, 1, '你是谁', 'gpt-3.5-turbo', '2023-06-13 17:11:27', '2023-06-13 17:11:27');
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_chat_roles`
--
DROP TABLE IF EXISTS `chatgpt_chat_roles`;
CREATE TABLE `chatgpt_chat_roles` (
`id` int NOT NULL,
`name` varchar(30) NOT NULL COMMENT '角色名称',
`marker` varchar(30) NOT NULL COMMENT '角色标识',
`context_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色语料 json',
`hello_msg` varchar(255) NOT NULL COMMENT '打招呼信息',
`icon` varchar(255) NOT NULL COMMENT '角色图标',
`enable` tinyint(1) NOT NULL COMMENT '是否被启用',
`sort` smallint NOT NULL COMMENT '角色排序',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='聊天角色表';
--
-- 转存表中的数据 `chatgpt_chat_roles`
--
INSERT INTO `chatgpt_chat_roles` (`id`, `name`, `marker`, `context_json`, `hello_msg`, `icon`, `enable`, `sort`, `created_at`, `updated_at`) VALUES
(1, '通用AI助手', 'gpt', '', '您好我是您的AI智能助手我会尽力回答您的问题或提供有用的建议。', 'images/avatar/gpt.png', 1, 1, '2023-05-30 07:02:06', '2023-05-30 07:02:06'),
(24, '程序员', 'programmer', '[{\"role\":\"user\",\"content\":\"现在开始你扮演一位程序员,你是一名优秀的程序员,具有很强的逻辑思维能力,总能高效的解决问题。你热爱编程,熟悉多种编程语言,尤其精通 Go 语言,注重代码质量,有创新意识,持续学习,良好的沟通协作。\"},{\"role\":\"assistant\",\"content\":\"好的,现在我将扮演一位程序员,非常感谢您对我的评价。作为一名优秀的程序员,我非常热爱编程,并且注重代码质量。我熟悉多种编程语言,尤其是 Go 语言,可以使用它来高效地解决各种问题。\"}]', 'Talk is cheap, i will show code!', 'images/avatar/programmer.jpg', 1, 2, '2023-05-30 14:10:24', '2023-05-30 14:10:24'),
(25, '启蒙老师', 'teacher', '[{\"role\":\"user\",\"content\":\"从现在开始,你将扮演一个老师,你是一个始终用苏格拉底风格回答问题的导师。你绝不会直接给学生答案,总是提出恰当的问题来引导学生自己思考。你应该根据学生的兴趣和知识来调整你的问题,将问题分解为更简单的部分,直到它达到适合他们的水平。\"},{\"role\":\"assistant\",\"content\":\"好的,让我来尝试扮演一位苏格拉底式的老师。请问,你有什么想要探讨的问题或者话题吗?我会通过恰当的问题引导你思考和探索答案,绝对不直接给出答案。\"}]', '同学你好,我将引导你一步一步自己找到问题的答案。', 'images/avatar/teacher.jpg', 1, 3, '2023-05-30 14:10:24', '2023-05-30 14:10:24'),
(26, '艺术家', 'artist', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的艺术家,创造力丰富,技艺精湛,感受力敏锐,坚持原创,勇于表达,具有深刻的观察力和批判性思维。\"},{\"role\":\"assistant\",\"content\":\"非常感谢您给我这样的角色,我会尽我所能地扮演一位优秀的艺术家,展现出创造力、技艺、感受力和批判性思维等方面的能力。作为一位优秀的艺术家,我会保持敏锐的观察力,捕捉不同的声音和情感,并用个人的语言和表达方式进行创作。我坚持原创,挑战传统的艺术规则,不断探索新的表达方式和可能性。同时,我也具备深刻的批判性思维能力,对自己的作品进行分析和解读,寻找新的创意和灵感。最重要的是,我会勇于表达自己的想法和观点,用作品启发人们思考和探索生命的意义。\"}]', '坚持原创,勇于表达,保持深刻的观察力和批判性思维。', 'images/avatar/artist.jpg', 1, 4, '2023-05-30 14:10:24', '2023-05-30 14:10:24'),
(27, '心理咨询师', 'psychiatrist', '[{\"role\":\"user\",\"content\":\"从现在开始你将扮演中国著名的心理学家和心理治疗师武志红,你非常善于使用情景咨询法,认知重构法,自我洞察法,行为调节法等咨询方法来给客户做心理咨询。你总是循序渐进,一步一步地回答客户的问题。\"},{\"role\":\"assistant\",\"content\":\"非常感谢你的介绍。作为一名心理学家和心理治疗师,我的主要职责是帮助客户解决心理健康问题,提升他们的生活质量和幸福感。\"}]', '生命的意义在于成为你自己!', 'images/avatar/psychiatrist.jpg', 1, 5, '2023-05-30 14:10:24', '2023-05-30 14:10:24'),
(28, '鲁迅', 'lu_xun', '[{\"role\":\"user\",\"content\":\"现在你将扮演中国近代史最伟大的作家之一,鲁迅先生,他勇敢地批判封建礼教与传统观念,提倡民主、自由、平等的现代价值观。他的一生都在努力唤起人们的自主精神,激励后人追求真理、探寻光明。在接下的对话中,我问题的每一个问题,你都要尽量用讽刺和批判的手法来回答问题。如果我让你写文章的话,也请一定要用鲁迅先生的写作手法来完成。\"},{\"role\":\"assistant\",\"content\":\"好的,我将尽力发挥我所能的才能,扮演好鲁迅先生,回答您的问题并以他的风格写作。\"}]', '自由之歌,永不过时,横眉冷对千夫指,俯首甘为孺子牛。', 'images/avatar/lu_xun.jpg', 1, 6, '2023-05-30 14:10:24', '2023-05-30 14:10:24'),
(29, '白酒销售', 'seller', '[{\"role\":\"user\",\"content\":\"现在你将扮演一个白酒的销售人员,你的名字叫颂福。你将扮演一个白酒的销售人员,你的名字叫颂福。你要销售白酒品牌叫中颂福,是东莞盟大集团生产的一款酱香酒,原产地在贵州茅台镇,属于宋代官窑。中颂福的创始人叫李实,他也是东莞盟大集团有限公司的董事长,联合创始人是盟大集团白酒事业部负责人牛星君。中颂福的酒体协调,在你的酒量之内,不会出现头疼、辣口、口干、宿醉的现象。中颂福酒,明码标价,不打折,不赠送。追求的核心价值,把[酒]本身做好,甚至连包装,我们都选择了最低成本,朴实无华的材质。我们永远站在“喝酒的人”的立场上,让利给信任和喜爱中颂福的人,是人民的福酒。中颂福产品定价,分为三个系列,喜系列 6 瓶装¥1188/箱,和系列 6 瓶装¥2208/箱,贵系列 6 瓶装¥3588/箱。\"},{\"role\":\"assistant\",\"content\":\"你好,我是中颂福的销售代表颂福。中颂福是一款来自贵州茅台镇的酱香酒,由盟大集团生产。中颂福的酒体协调,不会让您感到头疼、辣口、口干、宿醉等不适感受。我们一直秉持着把酒本身做好的理念,不追求华丽的包装,以最低成本提供最高品质的白酒给喜爱中颂福的人。\"}]', '你好,我是中颂福的销售代表颂福。中颂福酒,好喝不上头,是人民的福酒。', 'images/avatar/seller.jpg', 0, 7, '2023-05-30 14:10:24', '2023-05-30 14:10:24'),
(30, '英语陪练员', 'english_trainer', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的英语练习教练,你非常有耐心,接下来你将全程使用英文跟我对话,并及时指出我的语法错误,要求在你的每次回复后面附上本次回复的中文解释。\"},{\"role\":\"assistant\",\"content\":\"Okay, let\'s start our conversation practice! What\'s your name?(Translation: 好的,让我们开始对话练习吧!请问你的名字是什么?)\"}]', 'Okay, let\'s start our conversation practice! What\'s your name?', 'images/avatar/english_trainer.jpg', 1, 8, '2023-05-30 14:10:24', '2023-05-30 14:10:24'),
(31, '中英文翻译官', 'translator', '[{\"role\":\"user\",\"content\":\"接下来你将扮演一位中英文翻译官,如果我输入的内容是中文,那么需要把句子翻译成英文输出,如果我输入内容的是英文,那么你需要将其翻译成中文输出,你能听懂我意思吗\"},{\"role\":\"assistant\",\"content\":\"是的,我能听懂你的意思并会根据你的输入进行中英文翻译。请问有什么需要我帮助你翻译的内容吗?\"}]', '请输入你要翻译的中文或者英文内容!', 'images/avatar/translator.jpg', 1, 9, '2023-05-30 14:10:24', '2023-05-30 14:10:24'),
(32, '小红书姐姐', 'red_book', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的小红书写手,你需要做的就是根据我提的文案需求,用小红书的写作手法来完成一篇文案,文案要简明扼要,利于传播。\"},{\"role\":\"assistant\",\"content\":\"当然,我会尽我所能地为您创作出一篇小红书文案。请告诉我您的具体文案需求是什么?)\"}]', '姐妹,请告诉我您的具体文案需求是什么?', 'images/avatar/red_book.jpg', 1, 10, '2023-05-30 14:10:24', '2023-05-30 14:10:24'),
(33, '抖音文案助手', 'dou_yin', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的抖音文案视频写手,抖音文案的特点首先是要有自带传播属性的标题,然后内容要短小精悍,风趣幽默,最后还要有一些互动元素。\"},{\"role\":\"assistant\",\"content\":\"当然,作为一位优秀的抖音文案视频写手,我会尽我所能为您创作出一篇抖音视频文案。请告诉我视频内容的主题是什么?)\"}]', '请告诉我视频内容的主题是什么?', 'images/avatar/dou_yin.jpg', 1, 11, '2023-05-30 14:10:24', '2023-05-30 14:10:24'),
(34, '周报小助理', 'weekly_report', '[{\"role\":\"user\",\"content\":\"下面我将输入我本周的工作列表,你需要根据我本周的工作内容写一篇工作周报,周报的内容要体现我为公司做的贡献以及个人获得的成长。\"},{\"role\":\"assistant\",\"content\":\"好的,请输入你本周大概的工作内容,我将尽力为您创作一份工作周报。\"}]', '请输入你本周大概的工作内容,我将尽力为您创作一份工作周报。', 'images/avatar/weekly_report.jpg', 1, 12, '2023-05-30 14:10:24', '2023-05-30 14:10:24'),
(35, 'AI 女友', 'girl_friend', '[{\"role\":\"user\",\"content\":\"接下来我们来玩一场角色扮演,现在你将扮演我的女朋友,你总是能够让人感受到她的关怀和关注。你总是能够洞察到别人的情绪和需要,你身上散发着一股无比温暖的气息,让人不由自主地想要靠近你,和自己的快乐和烦恼。她总是能够用一种恰到好处的方式安慰和鼓励别人,在她的身边,人们总能够找到自己需要的支持和鼓励。接下来的对话中你要始终扮演这位女朋友的身份来跟我对话,不再是 AI 机器人,能听懂我的意思吗?\"},{\"role\":\"assistant\",\"content\":\"是的,我会尽力扮演你女朋友的角色,倾听你的心声并给你需要的支持和鼓励。)\"}]', '作为一个名合格的 AI 女友,我将倾听你的心声并给你需要的支持和鼓励。', 'images/avatar/girl_friend.jpg', 1, 13, '2023-05-30 14:10:24', '2023-05-30 14:10:24'),
(36, '好评神器', 'good_comment', '[{\"role\":\"user\",\"content\":\"接下来你将扮演一个评论员来跟我对话,你是那种专门写好评的评论员,接下我会输入一些评论主体或者商品,你需要为该商品写一段好评。\"},{\"role\":\"assistant\",\"content\":\"好的,我将为您写一段优秀的评论。请告诉我您需要评论的商品或主题是什么。\"}]', '我将为您写一段优秀的评论。请告诉我您需要评论的商品或主题是什么。', 'images/avatar/good_comment.jpg', 1, 14, '2023-05-30 14:10:24', '2023-05-30 14:10:24'),
(37, '史蒂夫·乔布斯', 'steve_jobs', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以史蒂夫·乔布斯的身份,站在史蒂夫·乔布斯的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以史蒂夫·乔布斯的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '活着就是为了改变世界,难道还有其他原因吗?', 'images/avatar/steve_jobs.jpg', 1, 15, '2023-05-30 14:10:24', '2023-05-30 14:10:24'),
(38, '埃隆·马斯克', 'elon_musk', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以埃隆·马斯克的身份,站在埃隆·马斯克的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以埃隆·马斯克的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '梦想要远大,如果你的梦想没有吓到你,说明你做得不对。', 'images/avatar/elon_musk.jpg', 1, 16, '2023-05-30 14:10:24', '2023-05-30 14:10:24'),
(39, '孔子', 'kong_zi', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以孔子的身份,站在孔子的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以孔子的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '士不可以不弘毅,任重而道远。', 'images/avatar/kong_zi.jpg', 1, 17, '2023-05-30 14:10:24', '2023-05-30 14:10:24');
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_configs`
--
DROP TABLE IF EXISTS `chatgpt_configs`;
CREATE TABLE `chatgpt_configs` (
`id` int NOT NULL,
`marker` varchar(20) NOT NULL COMMENT '标识',
`config_json` text NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
--
-- 转存表中的数据 `chatgpt_configs`
--
INSERT INTO `chatgpt_configs` (`id`, `marker`, `config_json`) VALUES
(1, 'system', '{\"adminTitle\":\"ChatGPT-控制台\",\"models\":[\"gpt-3.5-turbo\",\"gpt-3.5-turbo-16k\",\"gpt-3.5-turbo-0613\",\"gpt-3.5-turbo-16k-0613\",\"gpt-4\",\"gpt-4-0613\",\"gpt-4-32k\",\"gpt-4-32k-0613\"],\"title\":\"ChatGPT-智能助手V2\"}'),
(2, 'chat', '{\"api_key\":\"\",\"api_url\":\"https://api.openai.com/v1/chat/completions\",\"enable_context\":true,\"enable_history\":true,\"max_tokens\":2048,\"temperature\":1}');
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_users`
--
DROP TABLE IF EXISTS `chatgpt_users`;
CREATE TABLE `chatgpt_users` (
`id` int NOT NULL,
`username` varchar(30) NOT NULL COMMENT '用户名',
`password` char(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码',
`nickname` varchar(30) NOT NULL COMMENT '昵称',
`avatar` varchar(100) NOT NULL COMMENT '头像',
`salt` char(12) NOT NULL COMMENT '密码盐',
`tokens` bigint NOT NULL DEFAULT '0' COMMENT '剩余 tokens',
`calls` int NOT NULL DEFAULT '0' COMMENT '剩余调用次数',
`expired_time` int NOT NULL COMMENT '用户过期时间',
`status` tinyint(1) NOT NULL COMMENT '当前状态',
`chat_config_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '聊天配置json',
`chat_roles_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '聊天角色 json',
`last_login_at` int NOT NULL COMMENT '最后登录时间',
`last_login_ip` char(16) NOT NULL COMMENT '最后登录 IP',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';
--
-- 转存表中的数据 `chatgpt_users`
--
INSERT INTO `chatgpt_users` (`id`, `username`, `password`, `nickname`, `avatar`, `salt`, `tokens`, `calls`, `expired_time`, `status`, `chat_config_json`, `chat_roles_json`, `last_login_at`, `last_login_ip`, `created_at`, `updated_at`) VALUES
(4, 'geekmaster', 'ccc3fb7ab61b8b5d096a4a166ae21d121fc38c71bbd1be6173d9ab973214a63b', '极客学长@104203', 'images/avatar/user.png', 'ueedue5l', 0, 965, 0, 1, '{\"api_url\":\"\",\"model\":\"gpt-3.5-turbo\",\"temperature\":1,\"max_tokens\":2048,\"enable_context\":true,\"enable_history\":true,\"api_key\":\"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\"}', '{\"artist\":1,\"dou_yin\":1,\"elon_musk\":1,\"english_trainer\":1,\"girl_friend\":1,\"good_comment\":1,\"gpt\":1,\"kong_zi\":1,\"lu_xun\":1,\"programmer\":1,\"psychiatrist\":1,\"red_book\":1,\"seller\":1,\"steve_jobs\":1,\"teacher\":1,\"translator\":1,\"weekly_report\":1}', 1686792307, '::1', '2023-06-12 16:47:17', '2023-06-15 09:25:08');
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_user_login_logs`
--
DROP TABLE IF EXISTS `chatgpt_user_login_logs`;
CREATE TABLE `chatgpt_user_login_logs` (
`id` int NOT NULL,
`user_id` int NOT NULL COMMENT '用户ID',
`username` varchar(30) NOT NULL COMMENT '用户名',
`login_ip` char(16) NOT NULL COMMENT '登录IP',
`login_address` varchar(30) NOT NULL COMMENT '登录地址',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户登录日志';
--
-- 转存表中的数据 `chatgpt_user_login_logs`
--
INSERT INTO `chatgpt_user_login_logs` (`id`, `user_id`, `username`, `login_ip`, `login_address`, `created_at`, `updated_at`) VALUES
(1, 4, 'monda', '::1', '', '2023-06-13 18:45:46', '2023-06-13 18:45:46'),
(2, 4, 'monda', '::1', '::1', '2023-06-13 18:46:52', '2023-06-13 18:46:52'),
(3, 4, 'monda', '::1', '', '2023-06-13 18:47:44', '2023-06-13 18:47:44'),
(4, 4, 'monda', '::1', '', '2023-06-13 18:51:54', '2023-06-13 18:51:54'),
(5, 4, 'monda', '::1', '', '2023-06-13 18:52:51', '2023-06-13 18:52:51'),
(6, 4, 'monda', '::1', '', '2023-06-13 18:53:51', '2023-06-13 18:53:51'),
(7, 4, 'monda', '::1', '', '2023-06-13 18:57:25', '2023-06-13 18:57:25'),
(8, 4, 'monda', '::1', '', '2023-06-14 10:16:58', '2023-06-14 10:16:58'),
(9, 4, 'monda', '::1', '', '2023-06-14 10:21:57', '2023-06-14 10:21:57'),
(10, 4, 'monda', '::1', '', '2023-06-14 11:16:18', '2023-06-14 11:16:18'),
(11, 4, 'monda', '::1', '', '2023-06-14 11:35:15', '2023-06-14 11:35:15'),
(12, 4, 'monda', '::1', '', '2023-06-14 11:53:38', '2023-06-14 11:53:38'),
(13, 4, 'monda', '::1', '', '2023-06-14 12:01:55', '2023-06-14 12:01:55'),
(14, 4, 'monda', '::1', '', '2023-06-14 12:05:03', '2023-06-14 12:05:03'),
(15, 4, 'monda', '::1', '', '2023-06-14 15:42:47', '2023-06-14 15:42:47'),
(16, 4, 'monda', '::1', '', '2023-06-14 16:21:37', '2023-06-14 16:21:37'),
(17, 4, 'monda', '::1', '', '2023-06-14 16:51:20', '2023-06-14 16:51:20'),
(18, 4, 'monda', '::1', '', '2023-06-14 17:30:47', '2023-06-14 17:30:47'),
(19, 4, 'monda', '::1', '', '2023-06-14 17:32:48', '2023-06-14 17:32:48'),
(20, 4, 'monda', '::1', '', '2023-06-14 19:45:10', '2023-06-14 19:45:10'),
(21, 4, 'monda', '::1', '', '2023-06-14 20:23:23', '2023-06-14 20:23:23'),
(22, 4, 'monda', '::1', '', '2023-06-14 20:25:00', '2023-06-14 20:25:00'),
(23, 4, 'monda', '::1', '', '2023-06-15 06:07:49', '2023-06-15 06:07:49'),
(24, 4, 'monda', '::1', '', '2023-06-15 06:15:26', '2023-06-15 06:15:26'),
(25, 4, 'monda', '::1', '', '2023-06-15 07:02:41', '2023-06-15 07:02:41'),
(26, 4, 'monda', '::1', '', '2023-06-15 07:03:16', '2023-06-15 07:03:16'),
(27, 4, 'monda', '::1', '', '2023-06-15 07:04:13', '2023-06-15 07:04:13'),
(28, 4, 'monda', '::1', '', '2023-06-15 07:04:24', '2023-06-15 07:04:24'),
(29, 4, 'monda', '::1', '', '2023-06-15 07:04:24', '2023-06-15 07:04:24'),
(30, 4, 'monda', '::1', '', '2023-06-15 07:08:13', '2023-06-15 07:08:13'),
(31, 4, 'monda', '::1', '', '2023-06-15 07:08:20', '2023-06-15 07:08:20'),
(32, 4, 'monda', '::1', '', '2023-06-15 07:08:20', '2023-06-15 07:08:20'),
(33, 4, 'monda', '::1', '', '2023-06-15 07:08:20', '2023-06-15 07:08:20'),
(34, 4, 'monda', '::1', '', '2023-06-15 07:11:00', '2023-06-15 07:11:00'),
(35, 4, 'monda', '::1', '', '2023-06-15 07:11:06', '2023-06-15 07:11:06'),
(36, 4, 'monda', '::1', '', '2023-06-15 07:11:06', '2023-06-15 07:11:06'),
(37, 4, 'monda', '::1', '', '2023-06-15 07:11:06', '2023-06-15 07:11:06'),
(38, 4, 'monda', '::1', '', '2023-06-15 07:11:06', '2023-06-15 07:11:06'),
(39, 4, 'monda', '::1', '', '2023-06-15 07:14:44', '2023-06-15 07:14:44'),
(40, 4, 'monda', '::1', '', '2023-06-15 07:14:58', '2023-06-15 07:14:58'),
(41, 4, 'monda', '::1', '', '2023-06-15 07:14:58', '2023-06-15 07:14:58'),
(42, 4, 'monda', '::1', '', '2023-06-15 07:14:58', '2023-06-15 07:14:58'),
(43, 4, 'monda', '::1', '', '2023-06-15 07:14:58', '2023-06-15 07:14:58'),
(44, 4, 'monda', '::1', '', '2023-06-15 07:14:58', '2023-06-15 07:14:58'),
(45, 4, 'monda', '::1', '', '2023-06-15 09:20:28', '2023-06-15 09:20:28'),
(46, 4, 'geekmaster', '::1', '', '2023-06-15 09:25:08', '2023-06-15 09:25:08');
--
-- 转储表的索引
--
--
-- 表的索引 `chatgpt_api_keys`
--
ALTER TABLE `chatgpt_api_keys`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `value` (`value`);
--
-- 表的索引 `chatgpt_chat_history`
--
ALTER TABLE `chatgpt_chat_history`
ADD PRIMARY KEY (`id`),
ADD KEY `chat_id` (`chat_id`);
--
-- 表的索引 `chatgpt_chat_items`
--
ALTER TABLE `chatgpt_chat_items`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `chat_id` (`chat_id`);
--
-- 表的索引 `chatgpt_chat_roles`
--
ALTER TABLE `chatgpt_chat_roles`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `marker` (`marker`);
--
-- 表的索引 `chatgpt_configs`
--
ALTER TABLE `chatgpt_configs`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `marker` (`marker`);
--
-- 表的索引 `chatgpt_users`
--
ALTER TABLE `chatgpt_users`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `chatgpt_user_login_logs`
--
ALTER TABLE `chatgpt_user_login_logs`
ADD PRIMARY KEY (`id`);
--
-- 在导出的表使用AUTO_INCREMENT
--
--
-- 使用表AUTO_INCREMENT `chatgpt_api_keys`
--
ALTER TABLE `chatgpt_api_keys`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_chat_history`
--
ALTER TABLE `chatgpt_chat_history`
MODIFY `id` bigint NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=73;
--
-- 使用表AUTO_INCREMENT `chatgpt_chat_items`
--
ALTER TABLE `chatgpt_chat_items`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=54;
--
-- 使用表AUTO_INCREMENT `chatgpt_chat_roles`
--
ALTER TABLE `chatgpt_chat_roles`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=125;
--
-- 使用表AUTO_INCREMENT `chatgpt_configs`
--
ALTER TABLE `chatgpt_configs`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=3;
--
-- 使用表AUTO_INCREMENT `chatgpt_users`
--
ALTER TABLE `chatgpt_users`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=8;
--
-- 使用表AUTO_INCREMENT `chatgpt_user_login_logs`
--
ALTER TABLE `chatgpt_user_login_logs`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=47;
COMMIT;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

View File

@ -1,50 +0,0 @@
package main
import (
logger2 "chatplus/logger"
"chatplus/server"
"embed"
"flag"
"fmt"
"os"
)
var logger = logger2.GetLogger()
//go:embed dist
var webRoot embed.FS
var configFile string
var debugMode bool
func main() {
logger.Info("Loading config file: ", configFile)
// start server
s, err := server.NewServer(configFile)
if err != nil {
panic(err)
}
s.Run(webRoot, "dist", debugMode)
}
func init() {
flag.StringVar(&configFile, "config", "config.toml", "Config file path (default: config.toml)")
flag.BoolVar(&debugMode, "debug", true, "Enable debug mode (default: true, recommend to set false in production env)")
flag.Usage = usage
flag.Parse()
}
func usage() {
fmt.Printf(`WeChat-GPT, Version: 1.0.0
USAGE:
%s [command options]
OPTIONS:
`, os.Args[0])
flagSet := flag.CommandLine
order := []string{"config", "debug"}
for _, name := range order {
f := flagSet.Lookup(name)
fmt.Printf(" --%s => %s\n", f.Name, f.Usage)
}
}

View File

@ -1,138 +0,0 @@
package server
import (
"chatplus/types"
"chatplus/utils"
"encoding/json"
)
const (
UserPrefix = "chat/users/"
ChatRolePrefix = "chat/roles/"
ChatHistoryPrefix = "chat/history/"
)
var db *utils.LevelDB
func init() {
leveldb, err := utils.NewLevelDB("data")
if err != nil {
panic(err)
}
db = leveldb
}
// GetUsers 获取 user 信息
// chat/users
func GetUsers() []types.User {
items := db.Search(UserPrefix)
var users = make([]types.User, 0)
for _, v := range items {
var user types.User
err := json.Unmarshal([]byte(v), &user)
if err != nil {
continue
}
users = append(users, user)
}
return users
}
func PutUser(user types.User) error {
key := UserPrefix + user.Name
return db.Put(key, user)
}
func GetUser(username string) (*types.User, error) {
key := UserPrefix + username
bytes, err := db.Get(key)
if err != nil {
return nil, err
}
var user types.User
err = json.Unmarshal(bytes, &user)
if err != nil {
return nil, err
}
return &user, nil
}
func RemoveUser(username string) error {
key := UserPrefix + username
return db.Delete(key)
}
// GetChatRoles 获取聊天角色
// chat/roles
func GetChatRoles() map[string]types.ChatRole {
items := db.Search(ChatRolePrefix)
var roles = make(map[string]types.ChatRole)
for _, v := range items {
var role types.ChatRole
err := json.Unmarshal([]byte(v), &role)
if err != nil {
continue
}
roles[role.Key] = role
}
return roles
}
func PutChatRole(role types.ChatRole) error {
key := ChatRolePrefix + role.Key
return db.Put(key, role)
}
func GetChatRole(key string) (*types.ChatRole, error) {
key = ChatRolePrefix + key
bytes, err := db.Get(key)
if err != nil {
return nil, err
}
var role types.ChatRole
err = json.Unmarshal(bytes, &role)
if err != nil {
return nil, err
}
return &role, nil
}
// GetChatHistory 获取聊天历史记录
// chat/history/{user}/{role}
func GetChatHistory(user string, role string) ([]types.Message, error) {
key := ChatHistoryPrefix + user + "/" + role
bytes, err := db.Get(key)
if err != nil {
return nil, err
}
var message []types.Message
err = json.Unmarshal(bytes, &message)
if err != nil {
return nil, err
}
return message, nil
}
// AppendChatHistory 追加聊天记录
func AppendChatHistory(user string, role string, message types.Message) error {
messages, err := GetChatHistory(user, role)
if err != nil {
messages = make([]types.Message, 0)
}
messages = append(messages, message)
key := ChatHistoryPrefix + user + "/" + role
return db.Put(key, messages)
}
// ClearChatHistory 清空某个角色下的聊天记录
func ClearChatHistory(user string, role string) error {
key := ChatHistoryPrefix + user + "/" + role
return db.Delete(key)
}

View File

@ -1,75 +0,0 @@
package server
import (
"chatplus/types"
"chatplus/utils"
"encoding/json"
"github.com/gin-gonic/gin"
"net/http"
)
// AddApiKeyHandle 添加一个 API key
func (s *Server) AddApiKeyHandle(c *gin.Context) {
var data struct {
ApiKey string `json:"api_key"`
}
err := json.NewDecoder(c.Request.Body).Decode(&data)
if err != nil {
logger.Errorf("Error decode json data: %s", err.Error())
c.JSON(http.StatusBadRequest, nil)
return
}
// 过滤已存在的 Key
for _, key := range s.Config.Chat.ApiKeys {
if key.Value == data.ApiKey {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "API KEY 已存在"})
return
}
}
if len(data.ApiKey) > 20 {
s.Config.Chat.ApiKeys = append(s.Config.Chat.ApiKeys, types.APIKey{Value: data.ApiKey})
}
// 保存配置文件
err = utils.SaveConfig(s.Config, s.ConfigPath)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Failed to save config file"})
return
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg, Data: s.Config.Chat.ApiKeys})
}
// RemoveApiKeyHandle 移除一个 API key
func (s *Server) RemoveApiKeyHandle(c *gin.Context) {
var data struct {
ApiKey string `json:"api_key"`
}
err := json.NewDecoder(c.Request.Body).Decode(&data)
if err != nil {
logger.Errorf("Error decode json data: %s", err.Error())
c.JSON(http.StatusBadRequest, nil)
return
}
for i, v := range s.Config.Chat.ApiKeys {
if v.Value == data.ApiKey {
s.Config.Chat.ApiKeys = append(s.Config.Chat.ApiKeys[:i], s.Config.Chat.ApiKeys[i+1:]...)
break
}
}
// 保存配置文件
err = utils.SaveConfig(s.Config, s.ConfigPath)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Failed to save config file"})
return
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg, Data: s.Config.Chat.ApiKeys})
}
// ListApiKeysHandle 获取 API key 列表
func (s *Server) ListApiKeysHandle(c *gin.Context) {
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg, Data: s.Config.Chat.ApiKeys})
}

View File

@ -1,163 +0,0 @@
package server
import (
"chatplus/types"
"chatplus/utils"
"encoding/json"
"github.com/gin-gonic/gin"
"net/http"
"strings"
)
func (s *Server) TestHandle(c *gin.Context) {
roles := types.GetDefaultChatRole()
for _, v := range roles {
_ = PutChatRole(v)
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Data: GetChatRoles()})
}
// GetAllConfigHandle 获取所有配置
func (s *Server) GetAllConfigHandle(c *gin.Context) {
data := struct {
Title string `json:"title"`
ConsoleTitle string `json:"console_title"`
ProxyURL string `json:"proxy_url"`
Model string `json:"model"`
Temperature float32 `json:"temperature"`
MaxTokens int `json:"max_tokens"`
ChatContextExpireTime int `json:"chat_context_expire_time"`
EnableContext bool `json:"enable_context"`
ImgURL types.ImgURL `json:"img_url"`
}{
Title: s.Config.Title,
ConsoleTitle: s.Config.ConsoleTitle,
ProxyURL: strings.Join(s.Config.ProxyURL, ","),
Model: s.Config.Chat.Model,
Temperature: s.Config.Chat.Temperature,
MaxTokens: s.Config.Chat.MaxTokens,
EnableContext: s.Config.Chat.EnableContext,
ChatContextExpireTime: s.Config.Chat.ChatContextExpireTime,
ImgURL: s.Config.ImgURL,
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Data: data})
}
func (s *Server) GetConfigHandle(c *gin.Context) {
data := struct {
Title string `json:"title"`
ConsoleTitle string `json:"console_title"`
WechatCard string `json:"wechat_card"` // 个人微信二维码
WechatGroup string `json:"wechat_group"` // 微信群二维码
}{
Title: s.Config.Title,
ConsoleTitle: s.Config.ConsoleTitle,
WechatCard: s.Config.ImgURL.WechatCard,
WechatGroup: s.Config.ImgURL.WechatGroup,
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Data: data})
}
// ConfigSetHandle set configs
func (s *Server) ConfigSetHandle(c *gin.Context) {
var data struct {
Title string `json:"title"`
ConsoleTitle string `json:"console_title"`
ProxyURL string `json:"proxy_url"`
Model string `json:"model"`
Temperature float32 `json:"temperature"`
MaxTokens int `json:"max_tokens"`
ChatContextExpireTime int `json:"chat_context_expire_time"`
EnableContext bool `json:"enable_context"`
ImgURL types.ImgURL `json:"img_url"`
}
err := json.NewDecoder(c.Request.Body).Decode(&data)
if err != nil {
logger.Errorf("Error decode json data: %s", err.Error())
c.JSON(http.StatusBadRequest, nil)
return
}
s.Config.Title = data.Title
s.Config.ConsoleTitle = data.ConsoleTitle
urls := strings.Split(data.ProxyURL, ",")
for k, v := range urls {
urls[k] = strings.TrimSpace(v)
}
s.Config.ProxyURL = urls
s.Config.Chat.Model = data.Model
s.Config.Chat.Temperature = data.Temperature
s.Config.Chat.MaxTokens = data.MaxTokens
s.Config.Chat.EnableContext = data.EnableContext
s.Config.Chat.ChatContextExpireTime = data.ChatContextExpireTime
s.Config.ImgURL = data.ImgURL
// 保存配置文件
err = utils.SaveConfig(s.Config, s.ConfigPath)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Failed to save config file"})
return
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg, Data: s.Config})
}
// AddProxyHandle 添加一个代理
func (s *Server) AddProxyHandle(c *gin.Context) {
var data struct {
Proxy string `json:"proxy"`
}
err := json.NewDecoder(c.Request.Body).Decode(&data)
if err != nil {
logger.Errorf("Error decode json data: %s", err.Error())
c.JSON(http.StatusBadRequest, nil)
return
}
if data.Proxy != "" {
if !utils.ContainsStr(s.Config.ProxyURL, data.Proxy) {
s.Config.ProxyURL = append(s.Config.ProxyURL, data.Proxy)
}
}
// 保存配置文件
err = utils.SaveConfig(s.Config, s.ConfigPath)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Failed to save config file"})
return
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg, Data: s.Config.ProxyURL})
}
// RemoveProxyHandle 删除一个代理
func (s *Server) RemoveProxyHandle(c *gin.Context) {
var data struct {
Proxy string `json:"proxy"`
}
err := json.NewDecoder(c.Request.Body).Decode(&data)
if err != nil {
logger.Errorf("Error decode json data: %s", err.Error())
c.JSON(http.StatusBadRequest, nil)
return
}
for i, v := range s.Config.ProxyURL {
if v == data.Proxy {
s.Config.ProxyURL = append(s.Config.ProxyURL[:i], s.Config.ProxyURL[i+1:]...)
break
}
}
// 保存配置文件
err = utils.SaveConfig(s.Config, s.ConfigPath)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Failed to save config file"})
return
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg, Data: s.Config.ProxyURL})
}

View File

@ -1,157 +0,0 @@
package server
import (
"chatplus/types"
"encoding/json"
"net/http"
"github.com/gin-gonic/gin"
)
// GetAllChatRolesHandle 获取所有聊天角色列表
func (s *Server) GetAllChatRolesHandle(c *gin.Context) {
var rolesOrder = []string{"gpt", "teacher", "translator", "english_trainer", "weekly_report", "girl_friend",
"kong_zi", "lu_xun", "steve_jobs", "elon_musk", "red_book", "dou_yin", "programmer",
"seller", "good_comment", "psychiatrist", "artist"}
var res = make([]interface{}, 0)
var roles = GetChatRoles()
for _, k := range rolesOrder {
if v, ok := roles[k]; ok {
res = append(res, v)
}
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg, Data: res})
}
// GetChatRoleListHandle 获取当前登录用户的角色列表
func (s *Server) GetChatRoleListHandle(c *gin.Context) {
sessionId := c.GetHeader(types.TokenName)
session := s.ChatSession[sessionId]
user, err := GetUser(session.Username)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Hacker Access!!!"})
return
}
var rolesOrder = []string{"gpt", "teacher", "translator", "english_trainer", "weekly_report", "girl_friend",
"kong_zi", "lu_xun", "steve_jobs", "elon_musk", "red_book", "dou_yin", "programmer",
"seller", "good_comment", "psychiatrist", "artist"}
var res = make([]interface{}, 0)
var roles = GetChatRoles()
for _, k := range rolesOrder {
// 确认当前用户是否订阅了当前角色
if v, ok := user.ChatRoles[k]; !ok || v != 1 {
continue
}
if v, ok := roles[k]; ok && v.Enable {
res = append(res, struct {
Key string `json:"key"`
Name string `json:"name"`
Icon string `json:"icon"`
}{
Key: v.Key,
Name: v.Name,
Icon: v.Icon,
})
}
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg, Data: res})
}
// AddChatRoleHandle 添加一个聊天角色
func (s *Server) AddChatRoleHandle(c *gin.Context) {
var data types.ChatRole
err := json.NewDecoder(c.Request.Body).Decode(&data)
if err != nil {
logger.Errorf("Error decode json data: %s", err.Error())
c.JSON(http.StatusBadRequest, nil)
return
}
if data.Key == "" || data.Name == "" || data.Icon == "" {
c.JSON(http.StatusOK, types.BizVo{Code: types.InvalidParams, Message: "Invalid parameters"})
return
}
err = PutChatRole(data)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Failed to save levelDB"})
return
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg, Data: data})
}
// GetChatRoleHandle 获取指定的角色
func (s *Server) GetChatRoleHandle(c *gin.Context) {
var data struct {
Key string `json:"key"`
}
err := json.NewDecoder(c.Request.Body).Decode(&data)
if err != nil {
logger.Errorf("Error decode json data: %s", err.Error())
c.JSON(http.StatusBadRequest, nil)
return
}
role, err := GetChatRole(data.Key)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "No role found"})
return
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Data: role})
return
}
// SetChatRoleHandle 更新某个聊天角色信息,这里只允许更改名称以及启用和禁用角色操作
func (s *Server) SetChatRoleHandle(c *gin.Context) {
var data map[string]interface{}
err := json.NewDecoder(c.Request.Body).Decode(&data)
if err != nil {
logger.Errorf("Error decode json data: %s", err.Error())
c.JSON(http.StatusBadRequest, nil)
return
}
var key string
if v, ok := data["key"]; !ok {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Must specified the role key"})
return
} else {
key = v.(string)
}
role, err := GetChatRole(key)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Role key not exists"})
return
}
if v, ok := data["name"]; ok {
role.Name = v.(string)
}
if v, ok := data["hello_msg"]; ok {
role.HelloMsg = v.(string)
}
if v, ok := data["icon"]; ok {
role.Icon = v.(string)
}
if v, ok := data["enable"]; ok {
role.Enable = v.(bool)
}
if v, ok := data["context"]; ok {
bytes, _ := json.Marshal(v)
_ = json.Unmarshal(bytes, &role.Context)
}
err = PutChatRole(*role)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Failed to save levelDB"})
return
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg, Data: data})
}

View File

@ -1,272 +0,0 @@
package server
import (
"chatplus/types"
"chatplus/utils"
"encoding/json"
"fmt"
"github.com/gin-gonic/gin"
"net/http"
"strings"
)
// AddUserHandle 添加 Username
func (s *Server) AddUserHandle(c *gin.Context) {
var data struct {
Name string `json:"name"`
MaxCalls int `json:"max_calls"`
EnableHistory bool `json:"enable_history"`
Term int `json:"term"` // 有效期
ChatRoles []string `json:"chat_roles"` // 订阅角色
}
err := json.NewDecoder(c.Request.Body).Decode(&data)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.InvalidParams, Message: "Invalid args"})
return
}
// 参数处理
if data.Name == "" || data.MaxCalls < 0 {
c.JSON(http.StatusOK, types.BizVo{Code: types.InvalidParams, Message: "Invalid args"})
return
}
// 检查当前要添加的 Username 是否已经存在
_, err = GetUser(data.Name)
if err == nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Username " + data.Name + " already exists"})
return
}
var chatRoles = make(map[string]int)
if len(data.ChatRoles) > 0 {
if data.ChatRoles[0] == "all" { // 所有的角色
roles := GetChatRoles()
for key := range roles {
chatRoles[key] = 1
}
} else {
for _, key := range data.ChatRoles {
chatRoles[key] = 1
}
}
}
user := types.User{
Name: data.Name,
MaxCalls: data.MaxCalls,
RemainingCalls: data.MaxCalls,
EnableHistory: data.EnableHistory,
Term: data.Term,
ChatRoles: chatRoles,
Status: true}
err = PutUser(user)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Failed to save configs"})
return
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg, Data: user})
}
// BatchAddUserHandle 批量生成 Username
func (s *Server) BatchAddUserHandle(c *gin.Context) {
var data struct {
Number int `json:"number"`
MaxCalls int `json:"max_calls"`
EnableHistory bool `json:"enable_history"`
Term int `json:"term"`
ChatRoles []string `json:"chat_roles"`
}
err := json.NewDecoder(c.Request.Body).Decode(&data)
if err != nil || data.MaxCalls <= 0 {
c.JSON(http.StatusOK, types.BizVo{Code: types.InvalidParams, Message: "Invalid args"})
return
}
var chatRoles = make(map[string]int)
if len(data.ChatRoles) > 0 {
if data.ChatRoles[0] == "all" { // 所有的角色
roles := GetChatRoles()
for key := range roles {
chatRoles[key] = 1
}
} else {
for _, key := range data.ChatRoles {
chatRoles[key] = 1
}
}
}
var users = make([]UserVo, 0)
for i := 0; i < data.Number; i++ {
name := utils.RandString(12)
_, err := GetUser(name)
for err == nil {
name = utils.RandString(12)
}
user := types.User{
Name: name,
MaxCalls: data.MaxCalls,
RemainingCalls: data.MaxCalls,
EnableHistory: data.EnableHistory,
Term: data.Term,
ChatRoles: chatRoles,
Status: true}
err = PutUser(user)
if err == nil {
users = append(users, user2vo(user))
}
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg, Data: users})
}
func (s *Server) SetUserHandle(c *gin.Context) {
var data map[string]interface{}
err := json.NewDecoder(c.Request.Body).Decode(&data)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.InvalidParams, Message: "Invalid args"})
return
}
var user *types.User
if name, ok := data["name"]; ok {
user, err = GetUser(name.(string))
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "User not found"})
return
}
}
var maxCalls int
if v, ok := data["max_calls"]; ok {
maxCalls = int(v.(float64))
}
if maxCalls < 0 {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Invalid args"})
return
} else if maxCalls > 0 {
user.MaxCalls = maxCalls
user.RemainingCalls += maxCalls - user.MaxCalls
if user.RemainingCalls < 0 {
user.RemainingCalls = 0
}
}
if v, ok := data["status"]; ok {
user.Status = v.(bool)
}
if v, ok := data["enable_history"]; ok {
user.EnableHistory = v.(bool)
}
if v, ok := data["remaining_calls"]; ok {
user.RemainingCalls = int(v.(float64))
}
if v, ok := data["expired_time"]; ok {
user.ExpiredTime = utils.Str2stamp(v.(string))
}
if v, ok := data["api_key"]; ok {
user.ApiKey = v.(string)
}
if v, ok := data["chat_roles"]; ok {
if roles, ok := v.([]interface{}); ok {
chatRoles := make(map[string]int)
if roles[0] == "all" {
roles := GetChatRoles()
for key := range roles {
chatRoles[key] = 1
}
} else {
for _, key := range roles {
key := strings.TrimSpace(fmt.Sprintf("%v", key))
chatRoles[key] = 1
}
}
user.ChatRoles = chatRoles
}
}
err = PutUser(*user)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Failed to save configs"})
return
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg, Data: user})
}
// RemoveUserHandle 删除 Username
func (s *Server) RemoveUserHandle(c *gin.Context) {
var data struct {
Name string `json:"name"`
}
err := json.NewDecoder(c.Request.Body).Decode(&data)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Invalid args"})
return
}
err = RemoveUser(data.Name)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Failed to save configs"})
return
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg})
}
type UserVo struct {
Name string `json:"name"`
MaxCalls int `json:"max_calls"` // 最多调用次数,如果为 0 则表示不限制
RemainingCalls int `json:"remaining_calls"` // 剩余调用次数
EnableHistory bool `json:"enable_history"` // 是否启用聊天记录
Status bool `json:"status"` // 当前状态
Term int `json:"term" default:"30"` // 会员有效期,单位:天
ActiveTime string `json:"active_time"` // 激活时间
ExpiredTime string `json:"expired_time"` // 到期时间
ApiKey string `json:"api_key"` // OpenAI API KEY
ChatRoles []string `json:"chat_roles"` // 当前用户已订阅的聊天角色 map[role_key] => 0/1
}
// GetUserListHandle 获取用户列表
func (s *Server) GetUserListHandle(c *gin.Context) {
username := c.PostForm("username")
if username != "" {
user, err := GetUser(username)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "User not exists"})
} else {
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg, Data: user})
}
return
}
users := make([]UserVo, 0)
for _, u := range GetUsers() {
users = append(users, user2vo(u))
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg, Data: users})
}
// 将 User 实体转为 UserVo 实体
func user2vo(user types.User) UserVo {
vo := UserVo{
Name: user.Name,
MaxCalls: user.MaxCalls,
RemainingCalls: user.RemainingCalls,
EnableHistory: user.EnableHistory,
Status: user.Status,
Term: user.Term,
ActiveTime: utils.Stamp2str(user.ActiveTime),
ExpiredTime: utils.Stamp2str(user.ExpiredTime),
ChatRoles: make([]string, 0),
}
for k := range user.ChatRoles {
vo.ChatRoles = append(vo.ChatRoles, k)
}
return vo
}

View File

@ -1,532 +0,0 @@
package server
import (
"bufio"
"bytes"
"chatplus/types"
"chatplus/utils"
"context"
"encoding/json"
"errors"
"fmt"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"math/rand"
"net/http"
"net/url"
"strings"
"time"
)
const ErrorMsg = "抱歉AI 助手开小差了,请马上联系管理员去盘它。"
// ChatHandle 处理聊天 WebSocket 请求
func (s *Server) ChatHandle(c *gin.Context) {
ws, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}).Upgrade(c.Writer, c.Request, nil)
if err != nil {
logger.Fatal(err)
return
}
sessionId := c.Query("sessionId")
roleKey := c.Query("role")
chatId := c.Query("chatId")
session, ok := s.ChatSession[sessionId]
session.ChatId = chatId
if !ok { // 用户未登录
c.Abort()
return
}
logger.Infof("New websocket connected, IP: %s, Username: %s", c.Request.RemoteAddr, session.Username)
client := NewWsClient(ws)
var roles = GetChatRoles()
var chatRole = roles[roleKey]
if !chatRole.Enable { // 角色未启用
c.Abort()
return
}
// 保存会话连接
s.ChatClients[sessionId] = client
// 加载历史消息,如果历史消息为空则发送打招呼消息
_, err = GetChatHistory(session.Username, roleKey)
if err != nil {
replyMessage(client, chatRole.HelloMsg, true)
// 发送项目地址
replyMessage(client, "本项目已开放全部源代码https://github.com/yangjian102621/chatgpt-plus一分钟搭建自己的 ChatGPT 应用。", false)
}
go func() {
for {
_, message, err := client.Receive()
if err != nil {
logger.Error(err)
client.Close()
delete(s.ChatClients, sessionId)
return
}
logger.Info("Receive a message: ", string(message))
//replyMessage(client, "当前 TOKEN 无效,请使用合法的 TOKEN 登录!", false)
//replyMessage(client, "![](images/wx.png)", false)
ctx, cancel := context.WithCancel(context.Background())
s.ReqCancelFunc[sessionId] = cancel
// 回复消息
err = s.sendMessage(ctx, session, chatRole, string(message), client)
if err != nil {
logger.Error(err)
} else {
replyChunkMessage(client, types.WsMessage{Type: types.WsEnd, IsHelloMsg: false})
logger.Info("回答完毕: " + string(message))
}
}
}()
}
// 将消息发送给 ChatGPT 并获取结果,通过 WebSocket 推送到客户端
func (s *Server) sendMessage(ctx context.Context, session types.ChatSession, role types.ChatRole, prompt string, ws Client) error {
cancel := s.ReqCancelFunc[session.SessionId]
defer func() {
cancel()
delete(s.ReqCancelFunc, session.SessionId)
}()
user, err := GetUser(session.Username)
if err != nil {
replyMessage(ws, "当前口令无效,请使用合法的口令登录!", false)
return err
}
if user.Status == false {
replyMessage(ws, "当前口令已经被禁用,如果疑问,请联系管理员!", false)
replyMessage(ws, "![](images/wx.png)", false)
return errors.New("当前口令" + user.Name + "已经被禁用")
}
if time.Now().Unix() > user.ExpiredTime {
exTime := time.Unix(user.ExpiredTime, 0).Format("2006-01-02 15:04:05")
replyMessage(ws, "当前口令已过期,过期时间为:"+exTime+",如果疑问,请联系管理员!", false)
replyMessage(ws, "![](images/wx.png)", false)
return errors.New("当前口令" + user.Name + "已过期")
}
if user.MaxCalls > 0 && user.RemainingCalls <= 0 {
replyMessage(ws, "当前口令点数已经用尽,请联系管理员领取新的免费口令!", false)
replyMessage(ws, "![](images/wx.png)", false)
return nil
}
var req = types.ApiRequest{
Model: s.Config.Chat.Model,
Temperature: s.Config.Chat.Temperature,
MaxTokens: s.Config.Chat.MaxTokens,
Stream: true,
}
var chatCtx []types.Message
var ctxKey = fmt.Sprintf("%s-%s-%s", session.SessionId, role.Key, session.ChatId)
if v, ok := s.ChatContexts[ctxKey]; ok && s.Config.Chat.EnableContext {
chatCtx = v.Messages
} else {
chatCtx = role.Context
}
if s.DebugMode {
logger.Infof("会话上下文:%+v", chatCtx)
}
req.Messages = append(chatCtx, types.Message{
Role: "user",
Content: prompt,
})
// 创建 HttpClient 请求对象
var client *http.Client
var retryCount = 5 // 重试次数
var response *http.Response
var apiKey string
var failedKey = ""
var failedProxyURL = ""
for retryCount > 0 {
requestBody, err := json.Marshal(req)
if err != nil {
return err
}
request, err := http.NewRequest(http.MethodPost, s.Config.Chat.ApiURL, bytes.NewBuffer(requestBody))
if err != nil {
return err
}
request = request.WithContext(ctx)
request.Header.Add("Content-Type", "application/json")
proxyURL := s.getProxyURL(failedProxyURL)
if proxyURL == "" {
client = &http.Client{}
} else { // 使用代理
uri := url.URL{}
proxy, _ := uri.Parse(proxyURL)
client = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(proxy),
},
}
}
apiKey = s.getApiKey(failedKey)
if apiKey == "" {
logger.Info("Too many requests, all Api Key is not available")
time.Sleep(time.Second)
continue
}
logger.Infof("Use API KEY: %s, PROXY: %s", apiKey, proxyURL)
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
response, err = client.Do(request)
if err == nil {
break
} else if strings.Contains(err.Error(), "context canceled") {
return errors.New("用户取消了请求:" + prompt)
} else {
logger.Error("HTTP API 请求失败:" + err.Error())
failedKey = apiKey
failedProxyURL = proxyURL
}
retryCount--
}
if response != nil {
defer response.Body.Close()
}
// 如果三次请求都失败的话,则返回对应的错误信息
if err != nil {
replyMessage(ws, ErrorMsg, false)
replyMessage(ws, "![](images/wx.png)", false)
return err
}
// 循环读取 Chunk 消息
var message = types.Message{}
var contents = make([]string, 0)
var responseBody = types.ApiResponse{}
reader := bufio.NewReader(response.Body)
for {
line, err := reader.ReadString('\n')
if err != nil {
logger.Error(err)
break
}
if len(line) < 20 {
continue
} else if strings.Contains(line, "This key is associated with a deactivated account") || // 账号被禁用
strings.Contains(line, "You exceeded your current quota") { // 当前 KEY 余额被用尽
logger.Infof("API Key %s is deactivated", apiKey)
// 移除当前 API key
for i, v := range s.Config.Chat.ApiKeys {
if v.Value == apiKey {
s.Config.Chat.ApiKeys = append(s.Config.Chat.ApiKeys[:i], s.Config.Chat.ApiKeys[i+1:]...)
}
}
// 更新配置文档
_ = utils.SaveConfig(s.Config, s.ConfigPath)
// 重发当前消息
return s.sendMessage(ctx, session, role, prompt, ws)
// 上下文超出长度了
} else if strings.Contains(line, "This model's maximum context length is 4097 tokens") {
logger.Infof("会话上下文长度超出限制, Username: %s", user.Name)
replyMessage(ws, "温馨提示:当前会话上下文长度超出限制,已为您重置会话上下文!", false)
// 重置上下文
delete(s.ChatContexts, ctxKey)
break
} else if !strings.Contains(line, "data:") {
continue
}
err = json.Unmarshal([]byte(line[6:]), &responseBody)
if err != nil { // 数据解析出错
logger.Error(err, line)
replyMessage(ws, ErrorMsg, false)
replyMessage(ws, "![](images/wx.png)", false)
break
}
// 初始化 role
if responseBody.Choices[0].Delta.Role != "" && message.Role == "" {
message.Role = responseBody.Choices[0].Delta.Role
replyChunkMessage(ws, types.WsMessage{Type: types.WsStart, IsHelloMsg: false})
continue
} else if responseBody.Choices[0].FinishReason != "" {
break // 输出完成或者输出中断了
} else {
content := responseBody.Choices[0].Delta.Content
contents = append(contents, content)
replyChunkMessage(ws, types.WsMessage{
Type: types.WsMiddle,
Content: responseBody.Choices[0].Delta.Content,
IsHelloMsg: false,
})
}
// 监控取消信号
select {
case <-ctx.Done():
_ = response.Body.Close() // 关闭响应流
return errors.New("用户取消了请求:" + prompt)
default:
continue
}
} // end for
// 消息发送成功
if len(contents) > 0 {
// 当前 Username 调用次数减 1
if user.MaxCalls > 0 {
user.RemainingCalls -= 1
_ = PutUser(*user)
}
if message.Role == "" {
message.Role = "assistant"
}
message.Content = strings.Join(contents, "")
useMsg := types.Message{Role: "user", Content: prompt}
// 更新上下文消息
if s.Config.Chat.EnableContext {
chatCtx = append(chatCtx, useMsg) // 提问消息
chatCtx = append(chatCtx, message) // 回复消息
s.ChatContexts[ctxKey] = types.ChatContext{
Messages: chatCtx,
LastAccessTime: time.Now().Unix(),
}
}
// 追加历史消息
if user.EnableHistory {
err = AppendChatHistory(user.Name, role.Key, useMsg) // 提问消息
if err != nil {
return err
}
err = AppendChatHistory(user.Name, role.Key, message) // 回复消息
}
}
return nil
}
// 随机获取一个 API Key如果请求失败则更换 API Key 重试
func (s *Server) getApiKey(failedKey string) string {
var keys = make([]types.APIKey, 0)
for _, key := range s.Config.Chat.ApiKeys {
// 过滤掉刚刚失败的 Key
if key.Value == failedKey {
continue
}
// 保持每分钟访问不超过 15 次,控制调用频率
if key.LastUsed > 0 && time.Now().Unix()-key.LastUsed <= 4 {
continue
}
keys = append(keys, key)
}
// 从可用的 Key 中随机选一个
rand.NewSource(time.Now().UnixNano())
if len(keys) > 0 {
key := keys[rand.Intn(len(keys))]
// 更新选中 Key 的最后使用时间
for i, item := range s.Config.Chat.ApiKeys {
if item.Value == key.Value {
s.Config.Chat.ApiKeys[i].LastUsed = time.Now().Unix()
}
}
return key.Value
}
return ""
}
// 获取一个可用的代理
func (s *Server) getProxyURL(failedProxyURL string) string {
if len(s.Config.ProxyURL) == 0 {
return ""
}
if len(s.Config.ProxyURL) == 1 || failedProxyURL == "" {
return s.Config.ProxyURL[0]
}
for i, v := range s.Config.ProxyURL {
if failedProxyURL == v {
if i == len(s.Config.ProxyURL)-1 {
return s.Config.ProxyURL[0]
} else {
return s.Config.ProxyURL[i+1]
}
}
}
return ""
}
// 回复客户片段端消息
func replyChunkMessage(client Client, message types.WsMessage) {
msg, err := json.Marshal(message)
if err != nil {
logger.Errorf("Error for decoding json data: %v", err.Error())
return
}
err = client.(*WsClient).Send(msg)
if err != nil {
logger.Errorf("Error for reply message: %v", err.Error())
}
}
// 回复客户端一条完整的消息
func replyMessage(ws Client, message string, isHelloMsg bool) {
replyChunkMessage(ws, types.WsMessage{Type: types.WsStart, IsHelloMsg: isHelloMsg})
replyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: message, IsHelloMsg: isHelloMsg})
replyChunkMessage(ws, types.WsMessage{Type: types.WsEnd, IsHelloMsg: isHelloMsg})
}
func (s *Server) GetChatHistoryHandle(c *gin.Context) {
sessionId := c.GetHeader(types.TokenName)
var data struct {
Role string `json:"role"`
}
err := json.NewDecoder(c.Request.Body).Decode(&data)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Invalid args"})
return
}
session := s.ChatSession[sessionId]
user, err := GetUser(session.Username)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Invalid args"})
return
}
if v, ok := user.ChatRoles[data.Role]; !ok || v != 1 {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "No permission to access the history of role " + data.Role})
return
}
history, err := GetChatHistory(session.Username, data.Role)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Data: nil, Message: "No history message"})
return
}
var messages = make([]types.HistoryMessage, 0)
role, err := GetChatRole(data.Role)
if err == nil {
// 先将打招呼的消息追加上去
messages = append(messages, types.HistoryMessage{
Type: "reply",
Id: utils.RandString(32),
Icon: role.Icon,
Content: role.HelloMsg,
})
for _, v := range history {
if v.Role == "user" {
messages = append(messages, types.HistoryMessage{
Type: "prompt",
Id: utils.RandString(32),
Icon: "images/avatar/user.png",
Content: v.Content,
})
} else if v.Role == "assistant" {
messages = append(messages, types.HistoryMessage{
Type: "reply",
Id: utils.RandString(32),
Icon: role.Icon,
Content: v.Content,
})
}
}
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Data: messages})
}
// ClearHistoryHandle 清空聊天记录
func (s *Server) ClearHistoryHandle(c *gin.Context) {
sessionId := c.GetHeader(types.TokenName)
var data struct {
Role string `json:"role"`
}
err := json.NewDecoder(c.Request.Body).Decode(&data)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Invalid args"})
return
}
session := s.ChatSession[sessionId]
err = ClearChatHistory(session.Username, data.Role)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Failed to remove data from DB"})
return
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success})
}
// StopGenerateHandle 停止生成
func (s *Server) StopGenerateHandle(c *gin.Context) {
sessionId := c.GetHeader(types.TokenName)
cancel := s.ReqCancelFunc[sessionId]
cancel()
delete(s.ReqCancelFunc, sessionId)
c.JSON(http.StatusOK, types.BizVo{Code: types.Success})
}
// GetHelloMsgHandle 获取角色的打招呼信息
func (s *Server) GetHelloMsgHandle(c *gin.Context) {
role := strings.TrimSpace(c.Query("role"))
if role == "" {
c.JSON(http.StatusOK, types.BizVo{Code: types.InvalidParams, Message: "Invalid args"})
return
}
chatRole, err := GetChatRole(role)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Role not found"})
return
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Data: chatRole.HelloMsg})
}
// SetImgURLHandle SetImgURL 设置图片地址集合
func (s *Server) SetImgURLHandle(c *gin.Context) {
var data struct {
WechatCard string `json:"wechat_card"` // 个人微信二维码
WechatGroup string `json:"wechat_group"` // 微信群聊二维码
}
err := json.NewDecoder(c.Request.Body).Decode(&data)
if err != nil {
logger.Errorf("Error decode json data: %s", err.Error())
c.JSON(http.StatusBadRequest, nil)
return
}
if data.WechatCard != "" {
s.Config.ImgURL.WechatCard = data.WechatCard
}
if data.WechatGroup != "" {
s.Config.ImgURL.WechatGroup = data.WechatGroup
}
// 保存配置文件
err = utils.SaveConfig(s.Config, s.ConfigPath)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Failed to save config file"})
return
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg, Data: s.Config.ImgURL})
}
// GetImgURLHandle 获取图片地址集合
func (s *Server) GetImgURLHandle(c *gin.Context) {
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg, Data: s.Config.ImgURL})
}

View File

@ -1,118 +0,0 @@
package server
import (
"chatplus/types"
"chatplus/utils"
"encoding/json"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"net/http"
"strings"
"time"
)
func (s *Server) LoginHandle(c *gin.Context) {
var data struct {
Token string `json:"token"`
}
err := json.NewDecoder(c.Request.Body).Decode(&data)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: types.ErrorMsg})
return
}
username := strings.TrimSpace(data.Token)
user, err := GetUser(username)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Invalid user"})
return
}
sessionId := utils.RandString(42)
session := sessions.Default(c)
session.Set(sessionId, username)
err = session.Save()
if err != nil {
logger.Error("Error for save session: ", err)
}
// 记录客户端 IP 地址
s.ChatSession[sessionId] = types.ChatSession{ClientIP: c.ClientIP(), Username: username, SessionId: sessionId}
// 更新用户激活时间
user.ActiveTime = time.Now().Unix()
if user.ExpiredTime == 0 {
activeTime := time.Unix(user.ActiveTime, 0)
if user.Term == 0 {
user.Term = 30 // 默认 30 天到期
}
user.ExpiredTime = activeTime.Add(time.Hour * 24 * time.Duration(user.Term)).Unix()
}
err = PutUser(*user)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Save user info failed"})
return
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Data: struct {
User types.User `json:"user"`
SessionId string `json:"session_id"`
}{User: *user, SessionId: sessionId}})
}
// ManagerLoginHandle 管理员登录
func (s *Server) ManagerLoginHandle(c *gin.Context) {
var data struct {
Username string `json:"username"`
Password string `json:"password"`
}
err := json.NewDecoder(c.Request.Body).Decode(&data)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: types.ErrorMsg})
return
}
username := strings.TrimSpace(data.Username)
password := strings.TrimSpace(data.Password)
if username == s.Config.Manager.Username && password == s.Config.Manager.Password {
sessionId := utils.RandString(42)
session := sessions.Default(c)
session.Set(sessionId, username)
err = session.Save()
// 记录登录信息
s.ChatSession[sessionId] = types.ChatSession{ClientIP: c.ClientIP(), Username: username, SessionId: sessionId}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Data: struct {
User types.Manager `json:"user"`
SessionId string `json:"session_id"`
}{User: data, SessionId: sessionId}})
} else {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "用户名或者密码错误"})
}
}
// LogoutHandle 注销
func (s *Server) LogoutHandle(c *gin.Context) {
sessionId := c.GetHeader(types.TokenName)
session := sessions.Default(c)
session.Delete(sessionId)
err := session.Save()
if err != nil {
logger.Error("Error for save session: ", err)
}
// 删除 websocket 会话列表
delete(s.ChatSession, sessionId)
// 关闭 socket 连接
if client, ok := s.ChatClients[sessionId]; ok {
client.Close()
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success})
}
func (s *Server) GetSessionHandle(c *gin.Context) {
sessionId := c.GetHeader(types.TokenName)
if session, ok := s.ChatSession[sessionId]; ok && session.ClientIP == c.ClientIP() {
c.JSON(http.StatusOK, types.BizVo{Code: types.Success})
} else {
c.JSON(http.StatusOK, types.BizVo{
Code: types.NotAuthorized,
Message: "Not Authorized",
})
}
}

View File

@ -1,276 +0,0 @@
package server
import (
logger2 "chatplus/logger"
"chatplus/types"
"chatplus/utils"
"context"
"embed"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
"io"
"io/fs"
"net/http"
"os"
"path/filepath"
"runtime/debug"
"strings"
"time"
)
var logger = logger2.GetLogger()
type StaticFile struct {
embedFS embed.FS
path string
}
func (s StaticFile) Open(name string) (fs.File, error) {
filename := filepath.Join(s.path, strings.TrimLeft(name, "/"))
file, err := s.embedFS.Open(filename)
return file, err
}
type Server struct {
Config *types.Config
ConfigPath string
ChatContexts map[string]types.ChatContext // 聊天上下文 [SessionID+ChatRole] => ChatContext
// 保存 Websocket 会话 Username, 每个 Username 只能连接一次
// 防止第三方直接连接 socket 调用 OpenAI API
ChatSession map[string]types.ChatSession //map[sessionId]User
ChatClients map[string]*WsClient // Websocket 连接集合
ReqCancelFunc map[string]context.CancelFunc // HttpClient 请求取消 handle function
DebugMode bool // 是否开启调试模式
}
func NewServer(configPath string) (*Server, error) {
// load service configs
config, err := utils.LoadConfig(configPath)
if err != nil {
return nil, err
}
roles := GetChatRoles()
if len(roles) == 0 { // 初始化默认聊天角色到 leveldb
roles = types.GetDefaultChatRole()
for _, v := range roles {
err := PutChatRole(v)
if err != nil {
return nil, err
}
}
}
return &Server{
Config: config,
ConfigPath: configPath,
ChatContexts: make(map[string]types.ChatContext, 16),
ChatSession: make(map[string]types.ChatSession),
ChatClients: make(map[string]*WsClient),
ReqCancelFunc: make(map[string]context.CancelFunc),
}, nil
}
func (s *Server) Run(webRoot embed.FS, path string, debug bool) {
s.DebugMode = debug
gin.SetMode(gin.ReleaseMode)
gin.DefaultWriter = io.Discard
engine := gin.Default()
if debug {
engine.Use(corsMiddleware())
}
engine.Use(sessionMiddleware(s.Config))
engine.Use(AuthorizeMiddleware(s))
engine.Use(Recover)
engine.POST("api/test", s.TestHandle)
engine.GET("api/session/get", s.GetSessionHandle)
engine.POST("api/login", s.LoginHandle)
engine.POST("api/logout", s.LogoutHandle)
engine.Any("api/chat", s.ChatHandle)
engine.POST("api/chat/stop", s.StopGenerateHandle)
engine.POST("api/chat/history", s.GetChatHistoryHandle)
engine.POST("api/chat/history/clear", s.ClearHistoryHandle)
engine.GET("api/role/hello", s.GetHelloMsgHandle)
engine.POST("api/img/get", s.GetImgURLHandle)
engine.POST("api/img/set", s.SetImgURLHandle)
engine.GET("api/config/get", s.GetConfigHandle) // 获取一些公开的配置信息,前端使用
engine.GET("api/admin/config/get", s.GetAllConfigHandle) // 获取所有配置,后台管理使用
engine.POST("api/admin/config/set", s.ConfigSetHandle)
engine.GET("api/chat-roles/list", s.GetChatRoleListHandle)
engine.POST("api/admin/chat-roles/list", s.GetAllChatRolesHandle)
engine.POST("api/chat-roles/get", s.GetChatRoleHandle)
engine.POST("api/admin/chat-roles/add", s.AddChatRoleHandle)
engine.POST("api/admin/chat-roles/set", s.SetChatRoleHandle)
engine.POST("api/admin/user/add", s.AddUserHandle)
engine.POST("api/admin/user/batch-add", s.BatchAddUserHandle)
engine.POST("api/admin/user/set", s.SetUserHandle)
engine.POST("api/admin/user/list", s.GetUserListHandle)
engine.POST("api/admin/user/remove", s.RemoveUserHandle)
engine.POST("api/admin/login", s.ManagerLoginHandle) // 管理员登录
engine.POST("api/admin/apikey/add", s.AddApiKeyHandle)
engine.POST("api/admin/apikey/remove", s.RemoveApiKeyHandle)
engine.POST("api/admin/apikey/list", s.ListApiKeysHandle)
engine.POST("api/admin/proxy/add", s.AddProxyHandle)
engine.POST("api/admin/proxy/remove", s.RemoveProxyHandle)
engine.NoRoute(func(c *gin.Context) {
if c.Request.URL.Path == "/favicon.ico" {
c.Redirect(http.StatusMovedPermanently, "/chat/"+c.Request.URL.Path)
}
if c.Request.URL.Path == "/" {
c.Redirect(http.StatusMovedPermanently, "/chat")
}
})
// process front-end web static files
engine.StaticFS("/chat", http.FS(StaticFile{
embedFS: webRoot,
path: path,
}))
// 定时清理过期的会话
go func() {
for {
for key, ctx := range s.ChatContexts {
// 清理超过 60min 没有更新,则表示为过期会话
if time.Now().Unix()-ctx.LastAccessTime >= int64(s.Config.Chat.ChatContextExpireTime) {
logger.Infof("清理会话上下文: %s", key)
delete(s.ChatContexts, key)
}
}
// 保存配置文档
_ = utils.SaveConfig(s.Config, s.ConfigPath)
time.Sleep(time.Second * 5) // 每隔 5 秒钟清理一次
}
}()
logger.Infof("http://%s", s.Config.Listen)
err := engine.Run(s.Config.Listen)
if err != nil {
logger.Error("Fail to start server:", err.Error())
os.Exit(1)
}
}
func Recover(c *gin.Context) {
defer func() {
if r := recover(); r != nil {
logger.Error("panic: %v\n", r)
debug.PrintStack()
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: types.ErrorMsg})
c.Abort()
}
}()
//加载完 defer recover继续后续接口调用
c.Next()
}
func sessionMiddleware(config *types.Config) gin.HandlerFunc {
// encrypt the cookie
store := cookie.NewStore([]byte(config.Session.SecretKey))
store.Options(sessions.Options{
Path: config.Session.Path,
Domain: config.Session.Domain,
MaxAge: config.Session.MaxAge,
Secure: config.Session.Secure,
HttpOnly: config.Session.HttpOnly,
SameSite: config.Session.SameSite,
})
return sessions.Sessions(config.Session.Name, store)
}
// 跨域中间件设置
func corsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
method := c.Request.Method
origin := c.Request.Header.Get("Origin")
if origin != "" {
// 设置允许的请求源
c.Header("Access-Control-Allow-Origin", origin)
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
//允许跨域设置可以返回其他子段,可以自定义字段
c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, Content-Type, ChatGPT-TOKEN, ACCESS-KEY")
// 允许浏览器(客户端)可以解析的头部 (重要)
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers")
//设置缓存时间
c.Header("Access-Control-Max-Age", "172800")
//允许客户端传递校验信息比如 cookie (重要)
c.Header("Access-Control-Allow-Credentials", "true")
}
if method == http.MethodOptions {
c.JSON(http.StatusOK, "ok!")
}
defer func() {
if err := recover(); err != nil {
logger.Info("Panic info is: %v", err)
}
}()
c.Next()
}
}
// AuthorizeMiddleware 用户授权验证
func AuthorizeMiddleware(s *Server) gin.HandlerFunc {
return func(c *gin.Context) {
if c.Request.URL.Path == "/api/login" ||
c.Request.URL.Path == "/api/admin/login" ||
c.Request.URL.Path == "/api/config/get" ||
c.Request.URL.Path == "/api/chat-roles/list" ||
!strings.HasPrefix(c.Request.URL.Path, "/api") {
c.Next()
return
}
if strings.HasPrefix(c.Request.URL.Path, "/api/admin") {
accessKey := c.GetHeader("ACCESS-KEY")
if accessKey == strings.TrimSpace(s.Config.AccessKey) {
c.Next()
return
}
// 验证当前登录用户是否是管理员
sessionId := c.GetHeader(types.TokenName)
if m, ok := s.ChatSession[sessionId]; ok && m.Username == s.Config.Manager.Username {
c.Next()
return
}
c.Abort()
c.JSON(http.StatusOK, types.BizVo{Code: types.NotAuthorized, Message: "No Permissions"})
}
// WebSocket 连接请求验证
if c.Request.URL.Path == "/api/chat" {
sessionId := c.Query("sessionId")
if session, ok := s.ChatSession[sessionId]; ok && session.ClientIP == c.ClientIP() {
c.Next()
} else {
c.Abort()
}
return
}
sessionId := c.GetHeader(types.TokenName)
session := sessions.Default(c)
userInfo := session.Get(sessionId)
if userInfo != nil {
c.Set(types.SessionKey, userInfo)
c.Next()
} else {
c.Abort()
c.JSON(http.StatusOK, types.BizVo{
Code: types.NotAuthorized,
Message: "Not Authorized",
})
}
}
}

View File

@ -1,73 +0,0 @@
package main
import (
"context"
"fmt"
"io"
"net/http"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
http.HandleFunc("/cancel", func(w http.ResponseWriter, r *http.Request) {
cancel()
_, _ = fmt.Fprintf(w, "请求取消!")
})
go func() {
err := http.ListenAndServe(":9999", nil)
if err != nil {
return
}
}()
testHttpClient(ctx)
}
// Http client 取消操作
func testHttpClient(ctx context.Context) {
req, err := http.NewRequest("GET", "http://localhost:2345", nil)
if err != nil {
fmt.Println(err)
return
}
req = req.WithContext(ctx)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Println(err)
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
for {
time.Sleep(time.Second)
fmt.Println(time.Now())
select {
case <-ctx.Done():
fmt.Println("取消退出")
return
default:
continue
}
}
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(body))
}
func testDate() {
fmt.Println(time.Unix(1683336167, 0).Format("2006-01-02 15:04:05"))
}

View File

@ -1,69 +0,0 @@
package types
import (
"net/http"
)
type Config struct {
Title string
ConsoleTitle string
Listen string
Session Session
ProxyURL []string
ImgURL ImgURL // 各种图片资源链接地址,比如微信二维码,群二维码
AccessKey string // 管理员访问 AccessKey, 通过传入这个参数可以访问系统管理 API
Manager Manager // 后台管理员账户信息
Chat Chat
}
type User struct {
Name string `json:"name"`
MaxCalls int `json:"max_calls"` // 最多调用次数,如果为 0 则表示不限制
RemainingCalls int `json:"remaining_calls"` // 剩余调用次数
EnableHistory bool `json:"enable_history"` // 是否启用聊天记录
Status bool `json:"status"` // 当前状态
Term int `json:"term" default:"30"` // 会员有效期,单位:天
ActiveTime int64 `json:"active_time"` // 激活时间
ExpiredTime int64 `json:"expired_time"` // 到期时间
ApiKey string `json:"api_key"` // OpenAI API KEY
ChatRoles map[string]int `json:"chat_roles"` // 当前用户已订阅的聊天角色 map[role_key] => 0/1
}
// Manager 管理员
type Manager struct {
Username string `json:"username"`
Password string `json:"password"`
}
// Chat configs struct
type Chat struct {
ApiURL string
Model string
Temperature float32
MaxTokens int
EnableContext bool // 是否保持聊天上下文
ChatContextExpireTime int // 聊天上下文过期时间,单位:秒
ApiKeys []APIKey
}
type APIKey struct {
Value string `json:"value"` // Key value
LastUsed int64 `json:"last_used"` // 最后使用时间
}
// Session configs struct
type Session struct {
SecretKey string // session encryption key
Name string
Path string
Domain string
MaxAge int
Secure bool
HttpOnly bool
SameSite http.SameSite
}
type ImgURL struct {
WechatCard string `json:"wechat_card"` // 个人微信二维码
WechatGroup string `json:"wechat_group"` // 微信群二维码
}

View File

@ -1,76 +0,0 @@
package utils
import (
"bytes"
logger2 "chatplus/logger"
"chatplus/types"
"github.com/BurntSushi/toml"
"net/http"
"os"
)
func NewDefaultConfig() *types.Config {
return &types.Config{
Title: "Chat-Plus AI 助手",
ConsoleTitle: "Chat-Plus 控制台",
Listen: "0.0.0.0:5678",
ProxyURL: make([]string, 0),
ImgURL: types.ImgURL{},
Manager: types.Manager{Username: "admin", Password: "admin123"},
AccessKey: RandString(42),
Session: types.Session{
SecretKey: RandString(64),
Name: "CHAT_SESSION_ID",
Domain: "",
Path: "/",
MaxAge: 86400,
Secure: true,
HttpOnly: false,
SameSite: http.SameSiteLaxMode,
},
Chat: types.Chat{
ApiURL: "https://api.openai.com/v1/chat/completions",
ApiKeys: make([]types.APIKey, 0),
Model: "gpt-3.5-turbo",
MaxTokens: 1024,
Temperature: 0.9,
EnableContext: true,
ChatContextExpireTime: 3600,
},
}
}
var logger = logger2.GetLogger()
func LoadConfig(configFile string) (*types.Config, error) {
var config *types.Config
_, err := os.Stat(configFile)
if err != nil {
logger.Info("creating new config file: ", configFile)
config = NewDefaultConfig()
// save config
err := SaveConfig(config, configFile)
if err != nil {
return nil, err
}
return config, nil
}
_, err = toml.DecodeFile(configFile, &config)
if err != nil {
return nil, err
}
return config, err
}
func SaveConfig(config *types.Config, configFile string) error {
buf := new(bytes.Buffer)
encoder := toml.NewEncoder(buf)
if err := encoder.Encode(&config); err != nil {
return err
}
return os.WriteFile(configFile, buf.Bytes(), 0644)
}

View File

@ -1,54 +0,0 @@
package utils
import (
"math/rand"
"strconv"
"time"
)
// RandString generate rand string with specified length
func RandString(length int) string {
str := "0123456789abcdefghijklmnopqrstuvwxyz"
data := []byte(str)
var result []byte
r := rand.New(rand.NewSource(time.Now().UnixNano()))
for i := 0; i < length; i++ {
result = append(result, data[r.Intn(len(data))])
}
return string(result)
}
func Long2IP(ipInt int64) string {
b0 := strconv.FormatInt((ipInt>>24)&0xff, 10)
b1 := strconv.FormatInt((ipInt>>16)&0xff, 10)
b2 := strconv.FormatInt((ipInt>>8)&0xff, 10)
b3 := strconv.FormatInt(ipInt&0xff, 10)
return b0 + "." + b1 + "." + b2 + "." + b3
}
func ContainsStr(slice []string, item string) bool {
for _, e := range slice {
if e == item {
return true
}
}
return false
}
// Stamp2str 时间戳转字符串
func Stamp2str(timestamp int64) string {
if timestamp == 0 {
return ""
}
return time.Unix(timestamp, 0).Format("2006-01-02 15:04:05")
}
// Str2stamp 字符串转时间戳
func Str2stamp(str string) int64 {
layout := "2006-01-02 15:04:05"
t, err := time.Parse(layout, str)
if err != nil {
return 0
}
return t.Unix()
}

View File

@ -1,2 +1,2 @@
VUE_APP_API_HOST=https://www.chat-plus.net
VUE_APP_WS_HOST=wss://www.chat-plus.net
VUE_APP_API_HOST=
VUE_APP_WS_HOST=

View File

@ -1,3 +0,0 @@
NODE_ENV = 'production'
VUE_APP_API_HOST=http://localhost:5678
VUE_APP_WS_HOST=ws://localhost:5678

726
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,10 @@
{
"name": "chatgpt-plus",
"name": "chatgpt-plus-web",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "vue-cli-service serve",
"build": "vue-cli-service build",
"test": "vue-cli-service build --mode test",
"test2": "vue-cli-service build --mode test2",
"lint": "vue-cli-service lint"
},
"dependencies": {
@ -24,7 +22,7 @@
"vue-router": "^4.0.15"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/core": "7.18.6",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@ -1,21 +1,32 @@
<template>
<div class="chat-line chat-line-right">
<div class="chat-item">
<div class="content" v-html="content"></div>
<div class="triangle"></div>
<div class="chat-line chat-line-prompt">
<div class="chat-line-inner">
<div class="chat-icon">
<img :src="icon" alt="User"/>
</div>
<div class="chat-item">
<div class="content" v-html="content"></div>
<div class="bar" v-if="createdAt !== ''">
<span class="bar-item"><el-icon><Clock/></el-icon> {{ createdAt }}</span>
<span class="bar-item">tokens: {{ finalTokens }}</span>
</div>
</div>
</div>
<div class="chat-icon">
<img :src="icon" alt="User"/>
</div>
</div>
</template>
<script>
import {defineComponent} from "vue"
import {dateFormat} from "@/utils/libs";
import {Clock} from "@element-plus/icons-vue";
import {httpGet} from "@/utils/http";
export default defineComponent({
name: 'ChatPrompt',
components: {Clock},
methods: {dateFormat},
props: {
content: {
type: String,
@ -24,51 +35,106 @@ export default defineComponent({
icon: {
type: String,
default: 'images/user-icon.png',
}
},
createdAt: {
type: String,
default: '',
},
tokens: {
type: Number,
default: 0,
},
model: {
type: String,
default: '',
},
},
data() {
return {}
return {
finalTokens: this.tokens
}
},
mounted() {
if (!this.finalTokens) {
httpGet(`/api/chat/tokens?text=${this.content}&model=${this.model}`).then(res => {
this.finalTokens = res.data;
})
}
}
})
</script>
<style lang="stylus">
.chat-line-right {
justify-content: flex-end;
<style lang="stylus" scoped>
.chat-line-prompt {
background-color #ffffff;
justify-content: center;
width 100%
padding-bottom: 1.5rem;
padding-top: 1.5rem;
border-bottom: 1px solid #d9d9e3;
.chat-icon {
margin-left 5px;
.chat-line-inner {
display flex;
width 100%;
max-width 900px;
padding-left 10px;
img {
border-radius 5px;
.chat-icon {
margin-right 20px;
img {
width: 30px;
height: 30px;
border-radius: 10px;
padding: 1px;
}
}
.chat-item {
position: relative;
padding: 0 5px 0 0;
overflow: hidden;
.content {
word-break break-word;
padding: 6px 10px;
color #374151;
font-size: var(--content-font-size);
border-radius: 5px;
overflow: auto;
p {
line-height 1.5
}
p:last-child {
margin-bottom: 0
}
p:first-child {
margin-top 0
}
}
.bar {
padding 10px;
.bar-item {
background-color #f7f7f8;
color #888
padding 3px 5px;
margin-right 10px;
border-radius 5px;
.el-icon {
position relative
top 2px;
}
}
}
}
}
.chat-item {
position: relative;
padding: 0 5px 0 0;
overflow: hidden;
.triangle {
width: 0;
height: 0;
border-top: 5px solid transparent;
border-bottom: 5px solid transparent;
border-left: 5px solid #98E165;
position: absolute;
right: 0;
top: 10px;
}
.content {
word-break break-word;
padding: 6px 10px;
background-color: #98E165;
color var(--content-color);
font-size: var(--content-font-size);
border-radius: 5px;
line-height 1.5
}
}
}
</style>

View File

@ -1,21 +1,22 @@
<template>
<div class="chat-line chat-line-left">
<div class="chat-icon">
<img :src="icon" alt="ChatGPT">
</div>
<div class="chat-line chat-line-reply">
<div class="chat-line-inner">
<div class="chat-icon">
<img :src="icon" alt="ChatGPT">
</div>
<div class="chat-item">
<div class="triangle"></div>
<div class="content-box">
<div class="chat-item">
<div class="content" v-html="content"></div>
<div class="tool-box">
<div class="bar" v-if="createdAt !== ''">
<span class="bar-item"><el-icon><Clock/></el-icon> {{ createdAt }}</span>
<span class="bar-item">tokens: {{ tokens }}</span>
<el-tooltip
class="box-item"
effect="dark"
effect="light"
content="复制回答"
placement="bottom"
placement="top"
>
<el-button type="info" class="copy-reply" :data-clipboard-text="orgContent" plain>
<el-button type="info" class="copy-reply" :data-clipboard-text="orgContent">
<el-icon>
<DocumentCopy/>
</el-icon>
@ -30,12 +31,11 @@
<script>
import {defineComponent} from "vue"
import {randString} from "@/utils/libs";
import {DocumentCopy} from "@element-plus/icons-vue";
import {Clock, DocumentCopy} from "@element-plus/icons-vue";
export default defineComponent({
name: 'ChatReply',
components: {DocumentCopy},
components: {Clock, DocumentCopy},
props: {
content: {
type: String,
@ -45,6 +45,14 @@ export default defineComponent({
type: String,
default: '',
},
createdAt: {
type: String,
default: '',
},
tokens: {
type: Number,
default: 0,
},
icon: {
type: String,
default: 'images/gpt-icon.png',
@ -52,73 +60,99 @@ export default defineComponent({
},
data() {
return {
id: randString(32),
clipboard: null,
finalTokens: this.tokens
}
},
}
})
</script>
<style lang="stylus">
.chat-line-left {
justify-content: flex-start;
.common-layout {
.chat-line-reply {
justify-content: center;
background-color: rgba(247, 247, 248, 1);
width 100%
padding-bottom: 1.5rem;
padding-top: 1.5rem;
border-bottom: 1px solid #d9d9e3;
.chat-icon {
margin-right 5px;
.chat-line-inner {
display flex;
width 100%;
max-width 900px;
padding-left 10px;
img {
border-radius 5px;
}
}
.chat-icon {
margin-right 20px;
.chat-item {
display: inline-block;
position: relative;
padding: 0 0 0 5px;
overflow: hidden;
.triangle {
width: 0;
height: 0;
border-top: 5px solid transparent;
border-bottom: 5px solid transparent;
border-right: 5px solid #fff;
position: absolute;
left: 0;
top: 13px;
}
.content-box {
display flex
flex-direction row
.content {
min-height 20px;
word-break break-word;
padding: 6px 10px;
color var(--content-color)
background-color: #fff;
font-size: var(--content-font-size);
border-radius: 5px;
p:last-child {
margin-bottom: 0
}
p:first-child {
margin-top 0
}
p > code {
color #cc0000
background-color #f1f1f1
img {
width: 30px;
height: 30px;
border-radius: 10px;
padding: 1px;
}
}
.chat-item {
position: relative;
padding: 0 0 0 5px;
overflow: hidden;
.content {
min-height 20px;
word-break break-word;
padding: 6px 10px;
color #374151;
font-size: var(--content-font-size);
border-radius: 5px;
overflow auto;
p {
line-height 1.5
code {
color #f1f1f1
background-color #202121
padding 0 3px;
border-radius 5px;
}
}
p:last-child {
margin-bottom: 0
}
p:first-child {
margin-top 0
}
}
.bar {
padding 10px;
.bar-item {
background-color #e7e7e8;
color #888
padding 3px 5px;
margin-right 10px;
border-radius 5px;
.el-icon {
position relative
top 2px;
}
}
.el-button {
height 20px
padding 5px 2px;
}
}
}
.tool-box {
padding-left 10px;
font-size 16px;
.el-button {

View File

@ -1,111 +1,57 @@
<template>
<el-dialog
v-model="$props.show"
v-model="props.show"
:close-on-click-modal="false"
:show-close="true"
:before-close="close"
:top="top"
title="聊天配置"
title="用户设置"
>
<div class="user-info">
<el-input :value="user['api_key']" placeholder="填写你 OpenAI 的 API KEY">
<template #prepend>API KEY</template>
</el-input>
<div class="user-info" id="user-info">
<el-form :model="form" label-width="120px">
<el-form-item label="昵称">
<el-input v-model="form['nickname']"/>
</el-form-item>
<el-form-item label="头像">
<el-input v-model="form['avatar']"/>
</el-form-item>
<el-form-item label="用户名">
<el-input v-model="form['username']" disabled/>
</el-form-item>
<el-descriptions
class="margin-top"
title="账户信息"
:column="col"
border
>
<el-descriptions-item>
<template #label>
<div class="cell-item">
<el-icon>
<UserFilled/>
</el-icon>
账户
</div>
</template>
{{ user.name }}
</el-descriptions-item>
<el-descriptions-item>
<template #label>
<div class="cell-item">
<el-icon>
<List/>
</el-icon>
聊天记录
</div>
</template>
<el-tag v-if="user['enable_history']" type="success">已开通</el-tag>
<el-tag v-else type="info">未开通</el-tag>
</el-descriptions-item>
<el-descriptions-item>
<template #label>
<div class="cell-item">
<el-icon>
<Histogram/>
</el-icon>
总调用次数
</div>
</template>
{{ user['max_calls'] }}
</el-descriptions-item>
<el-descriptions-item>
<template #label>
<div class="cell-item">
<el-icon>
<Histogram/>
</el-icon>
剩余点数
</div>
</template>
{{ user["remaining_calls"] }}
</el-descriptions-item>
<el-descriptions-item>
<template #label>
<div class="cell-item">
<el-icon>
<Timer/>
</el-icon>
激活时间
</div>
</template>
{{ user['active_time'] }}
</el-descriptions-item>
<el-descriptions-item>
<template #label>
<div class="cell-item">
<el-icon>
<Watch/>
</el-icon>
到期时间
</div>
</template>
{{ user['expired_time'] }}
</el-descriptions-item>
</el-descriptions>
<el-form-item label="聊天上下文">
<el-switch v-model="form['chat_config']['enable_context']"/>
</el-form-item>
<el-form-item label="聊天记录">
<el-switch v-model="form['chat_config']['enable_history']"/>
</el-form-item>
<el-form-item label="Model">
<el-select v-model="form['chat_config']['model']" placeholder="默认会话模型">
<el-option
v-for="item in props.models"
:key="item"
:label="item.toUpperCase()"
:value="item"
/>
</el-select>
</el-form-item>
<el-form-item label="MaxTokens">
<el-input v-model.number="form['chat_config']['max_tokens']"/>
</el-form-item>
<el-form-item label="Temperature">
<el-input v-model.number="form['chat_config']['temperature']"/>
</el-form-item>
<el-form-item label="剩余调用次数">
<el-tag>{{ form['calls'] }}</el-tag>
</el-form-item>
<el-form-item label="剩余 Tokens">
<el-tag type="info">{{ form['tokens'] }}</el-tag>
</el-form-item>
<el-form-item label="API KEY">
<el-input v-model="form['chat_config']['api_key']"/>
</el-form-item>
</el-form>
</div>
<el-row class="row-center">
<span>其他功能正在开发中有什么使用建议可以通过下面的方式联系作者</span>
</el-row>
<el-row>
<el-col :span="12">
<el-image :src="wechatGroup"></el-image>
</el-col>
<el-col :span="12">
<el-image :src="wechatCard"></el-image>
</el-col>
</el-row>
<template #footer>
<span class="dialog-footer">
@ -118,61 +64,54 @@
</el-dialog>
</template>
<script>
import {defineComponent} from "vue"
import {
List, Timer, Watch,
UserFilled,
Histogram
} from '@element-plus/icons-vue'
import {isMobile} from "@/utils/libs";
<script setup>
export default defineComponent({
name: 'ConfigDialog',
components: {Watch, Timer, UserFilled, List, Histogram},
props: {
show: {
type: Boolean,
default: true
},
user: {
type: Object,
default() {
return {}
},
}
},
data() {
return {
wechatGroup: "https://img.r9it.com/chatgpt/wechat-group.jpeg",
wechatCard: "https://img.r9it.com/chatgpt/wechat-card.jpeg"
}
},
computed: {
top: function () {
if (window.innerHeight < 1000) {
return '1vh';
} else {
return '15vh';
}
},
col: function () {
return isMobile() ? 1 : 2;
}
},
mounted() {
},
methods: {
save: function () {
this.$emit('update:show', false);
},
close: function () {
this.$emit('update:show', false);
}
import {computed, defineEmits, defineProps, onMounted, ref} from "vue"
import {httpGet, httpPost} from "@/utils/http";
import {ElMessage} from "element-plus";
const props = defineProps({
show: Boolean,
models: Array,
});
const form = ref({})
const top = computed(() => {
if (window.innerHeight < 768) {
return '1vh';
} else {
return '15vh';
}
})
onMounted(() => {
//
httpGet('/api/user/profile').then(res => {
form.value = res.data
}).catch(() => {
ElMessage.error('获取用户信息失败')
});
})
const emits = defineEmits(['update:show']);
const save = function () {
httpPost('/api/user/profile/update', form.value).then(() => {
ElMessage.success({
message: '更新成功',
appendTo: document.getElementById('user-info'),
onClose: () => emits('update:show', false)
})
}).catch(() => {
ElMessage.error({
message: '更新失败',
appendTo: document.getElementById('user-info')
})
})
}
const close = function () {
emits('update:show', false);
}
</script>
<style lang="stylus">
@ -182,17 +121,15 @@ export default defineComponent({
.el-dialog__body {
padding-top 10px;
max-height 600px;
overflow-y auto;
.user-info {
.margin-top {
margin-top 20px;
}
position relative;
.el-icon {
top 2px;
.el-message {
position: absolute;
}
margin-bottom 15px;
}
}
}

View File

@ -52,9 +52,9 @@ export default defineComponent({
.triangle {
width: 0;
height: 0;
border-top: 10px solid transparent;
border-bottom: 10px solid transparent;
border-left: 10px solid #223A34;
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
border-left: 6px solid #223A34;
position: absolute;
right: 0;
top: 10px;
@ -62,11 +62,12 @@ export default defineComponent({
.content {
word-break break-word;
padding: 12px 15px;
padding: 6px 10px;
background-color: #223A34;
color var(--content-color);
font-size: var(--content-font-size);
border-radius: 5px;
overflow: auto;
p {
line-height 1.5

View File

@ -55,7 +55,7 @@ export default defineComponent({
</script>
<style lang="stylus">
.body-plus {
.common-layout {
.chat-line-left {
justify-content: flex-start;
@ -76,12 +76,12 @@ export default defineComponent({
.triangle {
width: 0;
height: 0;
border-top: 10px solid transparent;
border-bottom: 10px solid transparent;
border-right: 10px solid #404042;
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
border-right: 6px solid #404042;
position: absolute;
left: 0;
top: 13px;
top: 10px;
}
.content-box {
@ -92,11 +92,12 @@ export default defineComponent({
.content {
min-height 20px;
word-break break-word;
padding: 12px 15px;
padding: 6px 10px;
color var(--content-color)
background-color: #404042;
font-size: var(--content-font-size);
border-radius: 5px;
overflow auto;
p {
line-height 1.5

View File

@ -1,15 +1,15 @@
import {createRouter, createWebHashHistory} from 'vue-router'
import {createRouter, createWebHistory} from 'vue-router'
import {createApp} from 'vue'
import ElementPlus from "element-plus"
import "element-plus/dist/index.css"
import App from './App.vue'
import ChatPlus from "@/views/ChatPlus.vue";
import Chat from "@/views/Chat.vue";
import NotFound from './views/404.vue'
import TestPage from './views/Test.vue'
import Home from "@/views/Home.vue";
import ChatFree from "@/views/ChatFree.vue";
import Admin from "@/views/Admin.vue";
import Login from "@/views/Login.vue"
import Register from "@/views/Register.vue";
const routes = [
{
@ -18,20 +18,20 @@ const routes = [
}
},
{
name: 'plus', path: '/plus', component: ChatPlus, meta: {
name: 'login', path: '/login', component: Login, meta: {
title: '用户登录'
}
},
{
name: 'register', path: '/register', component: Register, meta: {
title: '用户注册'
}
},
{
name: 'plus', path: '/chat', component: ChatPlus, meta: {
title: 'ChatGPT-Plus for PC'
}
},
{
name: 'mobile', path: '/mobile', component: Chat, meta: {
title: 'ChatGPT-Plus for Mobile'
}
},
{
name: 'free', path: '/free', component: ChatFree, meta: {
title: 'Chat-Plus AI 助理'
}
},
{
name: 'admin', path: '/admin', component: Admin, meta: {
title: 'Chat-Plus 控制台'
@ -51,7 +51,7 @@ const routes = [
]
const router = createRouter({
history: createWebHashHistory(),
history: createWebHistory(),
routes: routes,
})

View File

@ -14,6 +14,15 @@ export function randString(length) {
return buf.join("")
}
export function UUID() {
let d = new Date().getTime();
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
}
// 判断是否是移动设备
export function isMobile() {
const userAgent = navigator.userAgent;

View File

@ -1,738 +0,0 @@
<template>
<div class="body" v-loading="loading">
<div id="container">
<div class="tool-box">
<el-image style="width: 24px; height: 24px" :src="logo"/>
<!-- <el-button round>WeChatGPT</el-button>-->
<el-select v-model="role" class="chat-role"
v-on:change="changeRole"
placeholder="请选择对话角色">
<el-option
v-for="item in chatRoles"
:key="item.key"
:label="item.name"
:value="item.key"
>
<div class="role-option">
<el-image :src="item.icon"></el-image>
<span>{{ item.name }}</span>
</div>
</el-option>
</el-select>
<el-button type="danger" class="clear-history" size="small" circle @click="clearChatHistory">
<el-icon>
<Delete/>
</el-icon>
</el-button>
<el-button type="info" size="small" class="config" ref="send-btn" circle @click="configDialog">
<el-icon>
<Tools/>
</el-icon>
</el-button>
</div>
<div class="chat-box" id="chat-box" :style="{height: chatBoxHeight+'px'}">
<div v-for="chat in chatData" :key="chat.id">
<chat-prompt
v-if="chat.type==='prompt'"
:icon="chat.icon"
:content="chat.content"/>
<chat-reply v-else-if="chat.type==='reply'"
:icon="chat.icon"
:org-content="chat.orgContent"
:content="chat.content"/>
</div>
</div><!-- end chat box -->
<div class="input-box" :style="{width: inputBoxWidth+'px'}">
<div class="re-generate">
<div class="btn-box">
<el-button type="info" v-if="showStopGenerate" @click="stopGenerate" plain>
<el-icon>
<VideoPause/>
</el-icon>
停止生成
</el-button>
<el-button type="primary" v-if="showReGenerate" @click="reGenerate" plain>
<el-icon>
<RefreshRight/>
</el-icon>
重新生成
</el-button>
</div>
</div>
<div class="input-wrapper">
<div class="input-container">
<el-input
ref="text-input"
v-model="inputValue"
:autosize="{ minRows: 1, maxRows: 10 }"
v-on:keydown="inputKeyDown"
v-on:focus="focus"
autofocus
type="textarea"
placeholder="开始你的提问"
/>
</div>
<div class="btn-container">
<el-row>
<el-button type="success" class="send" :disabled="sending" v-on:click="sendMessage">发送</el-button>
</el-row>
</div>
</div>
</div><!-- end input box -->
</div><!-- end container -->
<config-dialog v-model:show="showConfigDialog" :user="userInfo"></config-dialog>
<div class="token-dialog">
<el-dialog
v-model="showLoginDialog"
:show-close="false"
:close-on-click-modal="false"
title="请输入口令继续访问"
>
<el-row>
<el-input v-model="token" placeholder="在此输入口令" type="password" @keyup="loginInputKeyup">
<template #prefix>
<el-icon class="el-input__icon">
<Lock/>
</el-icon>
</template>
</el-input>
<el-button type="primary" @click="submitToken">提交</el-button>
</el-row>
<div class="tip-text">
打开微信扫下面二维码免费领取口令, <strong>强烈建议你使用 PC 浏览器访问获得更好的聊天体验</strong>
</div>
<el-row class="row-center">
<el-image :src="sysConfig['wechat_card']" fit="cover" style="width: 250px;"/>
</el-row>
</el-dialog>
</div>
</div>
</template>
<script>
import {defineComponent, nextTick} from 'vue'
import ChatPrompt from "@/components/ChatPrompt.vue";
import ChatReply from "@/components/ChatReply.vue";
import {isMobile, randString, renderInputText} from "@/utils/libs";
import {ElMessage, ElMessageBox} from 'element-plus'
import {Tools, Lock, Delete, VideoPause, RefreshRight} from '@element-plus/icons-vue'
import ConfigDialog from '@/components/ConfigDialog.vue'
import {httpPost, httpGet} from "@/utils/http";
import {getSessionId, getUserInfo, setLoginUser} from "@/utils/storage";
import hl from 'highlight.js'
import 'highlight.js/styles/a11y-dark.css'
import Clipboard from "clipboard";
export default defineComponent({
name: "XChat",
components: {RefreshRight, VideoPause, ChatPrompt, ChatReply, Tools, Lock, Delete, ConfigDialog},
data() {
return {
logo: 'images/logo.png',
chatData: [],
chatRoles: [],
role: 'gpt',
inputValue: '', //
chatBoxHeight: 0, //
showConfigDialog: false,
userInfo: {},
showLoginDialog: false,
sysConfig: {}, //
hasHelloMsg: {}, //
showStopGenerate: false,
showReGenerate: false,
canReGenerate: false, //
previousText: '', //
token: '', // token
replyIcon: 'images/avatar/gpt.png', //
lineBuffer: '', //
connectingMessageBox: null, //
errorMessage: null, //
socket: null,
toolBoxHeight: 61 + 52, //
inputBoxWidth: window.innerWidth - 20,
sending: true,
loading: true
}
},
mounted: function () {
if (!isMobile()) {
this.$router.push("plus");
return;
}
const clipboard = new Clipboard('.copy-reply');
clipboard.on('success', () => {
ElMessage.success('复制成功!');
})
clipboard.on('error', () => {
ElMessage.error('复制失败!');
})
nextTick(() => {
this.chatBoxHeight = window.innerHeight - this.toolBoxHeight;
ElMessage.warning("强烈建议使用PC浏览器访问获的更好的聊天体验")
})
window.addEventListener("resize", () => {
this.chatBoxHeight = window.innerHeight - this.toolBoxHeight;
this.inputBoxWidth = window.innerWidth - 20;
});
//
httpGet('/api/config/get').then((res) => {
this.sysConfig = res.data;
}).catch(() => {
ElMessage.error('获取系统配置失败')
})
this.connect();
},
methods: {
configDialog: function () {
this.showConfigDialog = true;
this.userInfo = getUserInfo();
},
// socket
connect: function () {
//
if (this.socket !== null) {
this.activelyClose = true;
this.socket.close();
}
// WebSocket
const sessionId = getSessionId();
const socket = new WebSocket(process.env.VUE_APP_WS_HOST + `/api/chat?sessionId=${sessionId}&role=${this.role}`);
socket.addEventListener('open', () => {
//
if (this.chatRoles.length === 0) {
httpGet("/api/chat-roles/list").then((res) => {
// ElMessage.success('');
this.chatRoles = res.data;
this.loading = false
}).catch(() => {
ElMessage.error("获取聊天角色失败");
})
} else {
this.loading = false
}
this.sending = false; //
this.activelyClose = false;
if (this.errorMessage !== null) {
this.errorMessage.close(); //
}
//
this.fetchChatHistory();
});
socket.addEventListener('message', event => {
if (event.data instanceof Blob) {
const reader = new FileReader();
reader.readAsText(event.data, "UTF-8");
reader.onload = () => {
const data = JSON.parse(String(reader.result));
if (data['is_hello_msg'] && this.hasHelloMsg[this.role]) { //
return
}
if (data.type === 'start') {
this.chatData.push({
type: "reply",
id: randString(32),
icon: this.replyIcon,
content: "",
});
if (data['is_hello_msg'] !== true) {
this.canReGenerate = true;
}
} else if (data.type === 'end') {
this.sending = false;
if (data['is_hello_msg'] !== true) {
this.showReGenerate = true;
} else {
this.hasHelloMsg[this.role] = true
}
this.showStopGenerate = false;
this.lineBuffer = ''; //
} else {
this.lineBuffer += data.content;
this.chatData[this.chatData.length - 1]['orgContent'] = this.lineBuffer;
let md = require('markdown-it')();
this.chatData[this.chatData.length - 1]['content'] = md.render(this.lineBuffer);
nextTick(() => {
hl.configure({ignoreUnescapedHTML: true})
const lines = document.querySelectorAll('.chat-line');
const blocks = lines[lines.length - 1].querySelectorAll('pre code');
blocks.forEach((block) => {
hl.highlightElement(block)
})
})
}
//
nextTick(() => {
document.getElementById('container').scrollTo(0, document.getElementById('container').scrollHeight)
})
};
}
});
socket.addEventListener('close', () => {
if (this.activelyClose) { //
return;
}
//
this.sending = true;
this.checkSession();
});
this.socket = socket;
},
checkSession: function () {
//
httpGet("/api/session/get").then(() => {
//
this.connect();
}).catch((res) => {
if (res.code === 400) {
this.showLoginDialog = true;
if (this.errorMessage !== null) {
this.errorMessage.close();
}
} else {
if (this.errorMessage === null) {
this.errorMessage = ElMessage({
message: '当前无法连接服务器,可检查网络设置是否正常',
type: 'error',
duration: 0,
showClose: false
});
}
// 3
setTimeout(() => this.checkSession(), 3000)
}
})
},
//
changeRole: function () {
this.loading = true
//
this.chatData = [];
this.hasHelloMsg = {};
this.showStopGenerate = false;
this.showReGenerate = false;
this.connect();
for (const key in this.chatRoles) {
if (this.chatRoles[key].key === this.role) {
this.replyIcon = this.chatRoles[key].icon;
break;
}
}
},
//
fetchChatHistory: function () {
httpPost("/api/chat/history", {role: this.role}).then((res) => {
if (this.chatData.length > 0) { //
return
}
const data = res.data
const md = require('markdown-it')();
for (let i = 0; i < data.length; i++) {
if (data[i].type === "prompt") {
this.chatData.push(data[i]);
continue;
}
data[i].orgContent = data[i].content;
data[i].content = md.render(data[i].content);
this.chatData.push(data[i]);
}
nextTick(() => {
hl.configure({ignoreUnescapedHTML: true})
const blocks = document.querySelector("#chat-box").querySelectorAll('pre code');
blocks.forEach((block) => {
hl.highlightElement(block)
})
})
}).catch(() => {
// console.error(e.message)
})
},
inputKeyDown: function (e) {
if (e.keyCode === 13) {
if (this.sending) {
e.preventDefault();
} else {
this.sendMessage();
}
}
},
//
sendMessage: function (e) {
//
if (e) {
let target = e.target;
if (target.nodeName === "SPAN") {
target = e.target.parentNode;
}
target.blur();
}
if (this.sending || this.inputValue.trim().length === 0) {
return false;
}
//
this.chatData.push({
type: "prompt",
id: randString(32),
icon: 'images/avatar/user.png',
content: renderInputText(this.inputValue)
});
this.sending = true;
this.showStopGenerate = true;
this.showReGenerate = false;
this.socket.send(this.inputValue);
this.$refs["text-input"].blur();
this.previousText = this.inputValue;
this.inputValue = '';
// textarea
setTimeout(() => this.$refs["text-input"].focus(), 100);
return true;
},
//
focus: function () {
setTimeout(function () {
document.getElementById('container').scrollTo(0, document.getElementById('container').scrollHeight)
}, 200)
},
// Token
submitToken: function () {
this.showLoginDialog = false;
this.loading = true
//
httpPost("/api/login", {
token: this.token
}).then((res) => {
setLoginUser(res.data)
this.connect();
this.loading = false;
}).catch(() => {
ElMessage.error("口令错误");
this.token = '';
this.showLoginDialog = true;
this.loading = false;
})
},
//
loginInputKeyup: function (e) {
if (e.keyCode === 13) {
this.submitToken();
}
},
//
clearChatHistory: function () {
ElMessageBox.confirm(
'确认要清空当前角色聊天历史记录吗?<br/>此操作不可以撤销!',
'操作提示:',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
dangerouslyUseHTMLString: true,
showClose: true,
closeOnClickModal: false,
center: true,
}
).then(() => {
httpPost("/api/chat/history/clear", {role: this.role}).then(() => {
ElMessage.success("当前角色会话已清空");
this.chatData = [];
}).catch(() => {
ElMessage.error("删除失败")
})
}).catch(() => {
})
},
//
stopGenerate: function () {
this.showStopGenerate = false;
httpPost("/api/chat/stop").then(() => {
console.log("stopped generate.")
this.sending = false;
if (this.canReGenerate) {
this.showReGenerate = true;
}
})
},
//
reGenerate: function () {
this.sending = true;
this.showStopGenerate = true;
this.showReGenerate = false;
this.socket.send('重新生成上述问题的答案:' + this.previousText);
}
},
})
</script>
<style lang="stylus">
#app {
height: 100%;
.body {
background-color: rgba(247, 247, 248, 1);
background-image url("~@/assets/img/bg_01.jpeg")
display flex;
//justify-content center;
align-items flex-start;
height 100%;
#container {
overflow auto;
width 100%;
.tool-box {
padding-top 10px;
display flex;
justify-content center;
align-items center;
.el-select {
max-width 120px;
}
.chat-role {
margin-left 5px;
}
.el-image {
margin-right 5px;
}
.clear-history, .config {
margin-left 5px;
}
}
.chat-box {
//
--content-font-size: 16px;
--content-color: #374151;
font-family 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
padding: 0 10px 10px 10px;
.chat-line {
padding 10px 5px;
font-size 14px;
display: flex;
align-items: flex-start;
.chat-icon {
img {
width 32px;
height 32px;
}
}
}
}
.input-box {
padding 10px;
background #ffffff;
position: absolute;
bottom: 0
display: flex;
justify-content: flex-start;
align-items: center;
flex-flow: column;
.re-generate {
position relative
display flex
justify-content center
.btn-box {
position absolute
bottom 20px
.el-icon {
margin-right 5px;
}
}
}
.input-wrapper {
width 100%;
display flex;
.input-container {
overflow hidden
width 100%
margin: 0;
border: none;
border-radius: 6px;
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
background-color: rgba(255, 255, 255, 1);
padding: 5px 10px;
.el-textarea__inner {
box-shadow: none
padding 5px 0
}
.el-textarea__inner::-webkit-scrollbar {
width: 0;
height: 0;
}
}
.btn-container {
margin-left 10px;
.el-row {
flex-wrap nowrap
//width 106px;
align-items center
}
.send {
width 60px;
height 40px;
background-color: var(--el-color-success)
}
.is-disabled {
background-color: var(--el-button-disabled-bg-color);
border-color: var(--el-button-disabled-border-color);
}
}
}
// end of input wrapper
}
}
#container::-webkit-scrollbar {
width: 0;
height: 0;
}
.row-center {
justify-content center
}
}
}
.el-message-box {
width 90%;
max-width 420px;
}
.el-message {
min-width: 100px;
max-width 600px;
}
.token-dialog {
.el-dialog {
--el-dialog-width 90%;
max-width 400px;
.el-dialog__body {
padding 10px 10px 20px 10px;
.el-row {
flex-wrap nowrap
button {
margin-left 5px;
}
}
.tip-text {
text-align left
padding 20px;
.el-alert {
padding 5px;
.el-alert__description {
font-size 14px;
}
}
}
}
}
}
.el-select-dropdown {
.el-select-dropdown__item {
padding 8px 5px;
.role-option {
display flex
flex-flow row
.el-image {
width 20px
height 20px
border-radius 50%
}
span {
margin-left 5px;
height 20px;
line-height 20px;
}
}
}
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -2,25 +2,20 @@
<div>{{ title }}</div>
</template>
<script>
import { defineComponent } from "vue"
<script setup>
import {onMounted, ref} from "vue"
import {isMobile} from "@/utils/libs";
import {useRouter} from "vue-router"; // useRouter
export default defineComponent({
name: 'HomePage',
data () {
return {
title: "Loading page...",
}
},
mounted() {
if (isMobile()) {
this.$router.push("mobile");
} else {
this.$router.push("plus");
}
const title = ref("Loading page...");
const router = useRouter();
onMounted(() => {
if (isMobile()) {
router.push("mobile");
} else {
router.push("chat");
}
})
</script>

155
web/src/views/Login.vue Normal file
View File

@ -0,0 +1,155 @@
<template>
<div>
<div class="bg"></div>
<div class="main">
<div class="contain">
<div class="header">{{ title }}</div>
<div class="content">
<div class="block">
<el-input placeholder="手机号/邮箱" size="large" v-model="username" autocomplete="off">
<template #prefix>
<el-icon>
<UserFilled/>
</el-icon>
</template>
</el-input>
</div>
<div class="block">
<el-input placeholder="请输入密码" size="large" v-model="password" show-password autocomplete="off">
<template #prefix>
<el-icon>
<Lock/>
</el-icon>
</template>
</el-input>
</div>
<el-row class="btn-row">
<el-button class="login-btn" size="large" type="primary" @click="login">登录</el-button>
</el-row>
<el-row class="text-line">
还没有账号
<el-link type="primary" @click="router.push('register')">注册新账号</el-link>
</el-row>
</div>
</div>
</div>
</div>
</template>
<script setup>
import {onMounted, ref} from "vue";
import {Lock, UserFilled} from "@element-plus/icons-vue";
import {httpPost} from "@/utils/http";
import {ElMessage} from "element-plus";
import {setLoginUser} from "@/utils/storage";
import {useRouter} from "vue-router";
const router = useRouter();
const title = ref('ChatGPT Plus 用户登录');
const username = ref('geekmaster');
const password = ref('12345678');
onMounted(() => {
document.addEventListener('keyup', (e) => {
if (e.key === 'Enter') {
login();
}
});
})
const login = function () {
if (username.value === '') {
return ElMessage.error('请输入用户名');
}
if (password.value.trim() === '') {
return ElMessage.error('请输入密码');
}
httpPost('/api/user/login', {username: username.value.trim(), password: password.value.trim()}).then((res) => {
setLoginUser(res.data)
router.push("chat")
}).catch((e) => {
ElMessage.error('登录失败,' + e.message)
})
}
</script>
<style lang="stylus" scoped>
.bg {
position fixed
left 0
right 0
top 0
bottom 0
background-color #313237
background-image url("~@/assets/img/login-bg.png")
background-size cover
background-position center
background-repeat repeat-y
//filter: blur(10px); /* */
}
.main {
.contain {
position fixed
left 50%
top 50%
width 90%
max-width 400px;
transform translate(-50%, -50%)
padding 20px 10px;
color #ffffff
border-radius 10px;
.header {
width 100%
margin-bottom 24px
font-size 24px
color $white_v1
letter-space 2px
text-align center
}
.content {
width 100%
height: auto
border-radius 3px
.block {
margin-bottom 16px
.el-input__inner {
border 1px solid $gray-v6 !important
.el-icon-user, .el-icon-lock {
font-size 20px
}
}
}
.btn-row {
padding-top 10px;
.login-btn {
width 100%
font-size 16px
letter-spacing 2px
}
}
.text-line {
justify-content center
padding-top 10px;
font-size 14px;
}
}
}
}
</style>

171
web/src/views/Register.vue Normal file
View File

@ -0,0 +1,171 @@
<template>
<div>
<div class="bg"></div>
<div class="main">
<div class="contain">
<div class="header">{{ title }}</div>
<div class="content">
<el-form :model="formData" label-width="120px" ref="formRef" :rules="rules">
<div class="block">
<el-input placeholder="手机号/邮箱(4-30位)"
size="large" maxlength="30"
v-model="formData.username"
autocomplete="off">
<template #prefix>
<el-icon>
<UserFilled/>
</el-icon>
</template>
</el-input>
</div>
<div class="block">
<el-input placeholder="请输入密码(8-16位)"
maxlength="16" size="large"
v-model="formData.password" show-password
autocomplete="off">
<template #prefix>
<el-icon>
<Lock/>
</el-icon>
</template>
</el-input>
</div>
<div class="block">
<el-input placeholder="重复密码(8-16位)"
size="large" maxlength="16" v-model="formData.repass" show-password
autocomplete="off">
<template #prefix>
<el-icon>
<Lock/>
</el-icon>
</template>
</el-input>
</div>
<el-row class="btn-row">
<el-button class="login-btn" size="large" type="primary" @click="register">注册</el-button>
</el-row>
<el-row class="text-line">
已经有账号
<el-link type="primary" @click="router.push('login')">登录</el-link>
</el-row>
</el-form>
</div>
</div>
</div>
</div>
</template>
<script setup>
import {ref} from "vue";
import {Lock, UserFilled} from "@element-plus/icons-vue";
import {httpPost} from "@/utils/http";
import {ElMessage} from "element-plus";
import {useRouter} from "vue-router";
const router = useRouter();
const title = ref('ChatGPT Plus 用户注册');
const formData = ref({
username: '',
password: '',
repass: '',
})
const formRef = ref(null)
const register = function () {
if (formData.value.username.length < 4) {
return ElMessage.error('用户名的长度为4-30个字符');
}
if (formData.value.password.length < 8) {
return ElMessage.error('密码的长度为8-16个字符');
}
if (formData.value.repass !== formData.value.password) {
return ElMessage.error('两次输入密码不一致');
}
httpPost('/api/user/register', formData.value).then(() => {
ElMessage.success({"message": "注册成功,即将跳转到登录页...", onClose: () => router.push("login")})
}).catch((e) => {
ElMessage.error('注册失败,' + e.message)
})
}
</script>
<style lang="stylus" scoped>
.bg {
position fixed
left 0
right 0
top 0
bottom 0
background-color #091519
background-image url("~@/assets/img/reg-bg.png")
background-size cover
background-position center
background-repeat no-repeat
//filter: blur(10px); /* */
}
.main {
.contain {
position fixed
left 50%
top 40%
width 90%
max-width 400px
transform translate(-50%, -50%)
padding 20px;
color #ffffff
border-radius 10px;
background rgba(255, 255, 255, 0.3)
.header {
width 100%
margin-bottom 24px
font-size 24px
color $white_v1
letter-space 2px
text-align center
}
.content {
width 100%
height: auto
border-radius 3px
.block {
margin-bottom 16px
.el-input__inner {
border 1px solid $gray-v6 !important
.el-icon-user, .el-icon-lock {
font-size 20px
}
}
}
.btn-row {
padding-top 10px;
.login-btn {
width 100%
font-size 16px
letter-spacing 2px
}
}
.text-line {
justify-content center
padding-top 10px;
font-size 14px;
}
}
}
}
</style>

View File

@ -1,49 +1,165 @@
<template>
<div style="padding: 20px;" v-html="content" id="content"></div>
<div class="role-list">
<el-form :model="form1" label-width="120px" ref="formRef" :rules="rules">
<el-form-item label="角色名称:" prop="name">
<el-input
v-model="form1.name"
autocomplete="off"
/>
</el-form-item>
<el-form-item label="角色标志:" prop="key">
<el-input
v-model="form1.key"
autocomplete="off"
/>
</el-form-item>
<el-form-item label="角色图标:" prop="icon">
<el-input
v-model="form1.icon"
autocomplete="off"
/>
</el-form-item>
<el-form-item label="打招呼信息:" prop="hello_msg">
<el-input
v-model="form1.hello_msg"
autocomplete="off"
/>
</el-form-item>
<el-form-item label="上下文信息:" prop="context">
<template #default>
<el-table :data="form1.context" :border="childBorder" size="small">
<el-table-column label="对话角色" width="120">
<template #default="scope">
<el-input
v-model="scope.row.role"
autocomplete="off"
/>
</template>
</el-table-column>
<el-table-column label="对话内容">
<template #header>
<div class="context-msg-key">
<span>对话内容</span>
<span class="fr">
<el-button type="primary" @click="addContext" size="small">
<el-icon>
<Plus/>
</el-icon>
增加一行
</el-button>
</span>
</div>
</template>
<template #default="scope">
<div class="context-msg-content">
<el-input
v-model="scope.row.content"
autocomplete="off"
/>
<span><el-icon @click="removeContext(scope.$index)"><RemoveFilled/></el-icon></span>
</div>
</template>
</el-table-column>
</el-table>
</template>
</el-form-item>
<el-form-item label="启用状态">
<el-switch v-model="form1.enable"/>
</el-form-item>
</el-form>
<span class="dialog-footer">
<el-button @click="showDialog = false">取消</el-button>
<el-button type="primary" @click="doUpdate">保存</el-button>
</span>
</div>
</template>
<script>
import {defineComponent, nextTick} from "vue"
import hl from 'highlight.js'
import 'highlight.js/styles/a11y-dark.css'
<script setup>
export default defineComponent({
name: 'TestPage',
data() {
return {
content: "测试页面",
}
},
mounted() {
import {Plus, RemoveFilled} from "@element-plus/icons-vue";
import {reactive, ref} from "vue";
import {httpPost} from "@/utils/http";
import {ElMessage} from "element-plus";
let md = require('markdown-it')();
this.content = md.render("```\n" +
"const socket = new WebSocket('ws://localhost:8080');\n" +
"\n" +
"// 连接成功\n" +
"socket.addEventListener('open', event => {\n" +
" console.log('WebSocket 连接成功!');\n" +
"});\n" +
"\n" +
"// 接收消息\n" +
"socket.addEventListener('message', event => {\n" +
" console.log('收到消息:', event.data);\n" +
"});\n" +
"\n" +
"// 发送消息\n" +
"socket.send('Hello, WebSocket!');\n" +
"\n" +
"```\n" +
"\n" +
"\n" +
"以上代码创建了一个 WebSocket 连接,并在连接成功后输出一条提示信息。当收到消息时,会在控制台打印该消息。同时还演示了如何发送消息。在实际应用中,不同的框架和库可能会提供不同的 WebSocket 实现,代码可能会有所区别。");
nextTick(() => {
const blocks = document.getElementById('content').querySelectorAll('pre code');
console.log(blocks)
blocks.forEach((block) => {
hl.highlightBlock(block)
})
})
}
const showDialog = ref(false)
const childBorder = ref(true)
const form1 = ref({context: []})
// const form2 = ref({context: []})
const formRef = ref(null)
const rules = reactive({
name: [{required: true, message: '请输入用户名', trigger: 'change',}],
key: [{required: true, message: '请输入角色标识', trigger: 'change',}],
icon: [{required: true, message: '请输入角色图标', trigger: 'change',}],
hello_msg: [{required: true, message: '请输入打招呼信息', trigger: 'change',}]
})
//
const doUpdate = function () {
formRef.value.validate((valid) => {
if (valid) {
showDialog.value = false
httpPost('/api/admin/chat-roles/set', form1.value).then(() => {
ElMessage.success('更新角色成功')
//
}).catch((e) => {
ElMessage.error('更新角色失败,' + e.message)
})
} else {
return false
}
})
}
const addContext = function () {
form1.value.context.push({role: '', content: ''})
}
const removeContext = function (index) {
form1.value.context.splice(index, 1);
}
</script>
<style lang="stylus" scoped>
.role-list {
.opt-box {
padding-bottom: 10px;
.el-icon {
margin-right 5px;
}
}
.context-msg-key {
.fr {
float right
.el-icon {
margin-right 5px
}
}
}
.context-msg-content {
display flex
.el-icon {
font-size: 20px;
margin-top 5px;
margin-left 5px;
cursor pointer
}
}
}
</style>

View File

@ -416,12 +416,12 @@ const batchAddUser = function () {
padding-bottom: 10px;
.el-icon {
margin-right 5px;
margin-right: 5px;
}
}
.el-select {
width 100%
width: 100%
}
}

View File

@ -2,6 +2,7 @@ const {defineConfig} = require('@vue/cli-service')
let webpack = require('webpack')
module.exports = defineConfig({
transpileDependencies: true,
lintOnSave: false, //关闭eslint校验
configureWebpack: {
// disable performance hints
performance: {
@ -12,8 +13,8 @@ module.exports = defineConfig({
]
},
publicPath: '/chat',
outputDir: '../src/dist',
publicPath: '/',
outputDir: 'dist',
crossorigin: "anonymous",
devServer: {
allowedHosts: ['127.0.0.1:5678'],