mirror of
https://github.com/yangjian102621/geekai.git
synced 2025-11-07 17:53:42 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c1cc3afc5 |
15
.gitignore
vendored
15
.gitignore
vendored
@@ -1,7 +1,15 @@
|
|||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
src/dist
|
||||||
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
@@ -14,3 +22,10 @@ logs
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
src/tmp
|
||||||
|
src/bin
|
||||||
|
src/data
|
||||||
|
web/.env.development
|
||||||
|
web/.env.test
|
||||||
|
web/.env.test2
|
||||||
|
config.toml
|
||||||
|
|||||||
118
CHANGELOG.md
118
CHANGELOG.md
@@ -1,118 +0,0 @@
|
|||||||
# 更新日志
|
|
||||||
## v3.1.4
|
|
||||||
1. 功能新增:新增阿里云 OSS 图片上传实现,目前已支持本地存储,七牛云,Minio和阿里云 OSS 四种存储介质。
|
|
||||||
2. 功能新增:**增加 Stable Diffusion 绘画功能页面**。
|
|
||||||
3. 功能重构:将 [chatgpt-plus-exts](https://github.com/yangjian102621/chatgpt-plus-exts) 合并到本项目,部署更加简单,无需部署两个项目了。
|
|
||||||
4. Bug修复:修复[用户注册报错BUG #37](https://github.com/yangjian102621/chatgpt-plus/issues/37)。
|
|
||||||
5. Bug修复:修复 MidJourney API 接口升级导致图片文保存失败的 Bug。
|
|
||||||
6. 功能优化:增加阿里云短信服务配置项 `Sign` 和 `CodeTempId` 用来配置自己的短信签名和短信验证码模版 ID。
|
|
||||||
7. 功能优化:添加系统配置用来设置自定义的众筹微信收款二维码。
|
|
||||||
8. 功能优化:优化绘画页面的弹窗样式和页面布局。
|
|
||||||
|
|
||||||
## v3.1.3
|
|
||||||
1. 页面重构:重后 Home 页面,拆分成聊天,MJ绘画,SD 绘画,应用广场等多个功能菜单。
|
|
||||||
2. 功能新增:新增 MidJourney 专业绘画页面,开放更高级的 MJ 绘画姿势。
|
|
||||||
3. 功能优化:采用队列的方式控制绘画任务并发,简化任务回调通知逻辑,给任务回调加锁。
|
|
||||||
4. 功能优化:精简用户表字段,删除用户名和昵称,只保留手机号。
|
|
||||||
5. 功能优化:优化文件上传服务工厂实现,只创建激活的 Uploader 服务,节省资源。
|
|
||||||
6. Bug修复:修复 JWT token 有效期计算错误的 Bug。
|
|
||||||
|
|
||||||
## v3.1.2
|
|
||||||
1. 功能新增:新增七牛云 OSS 实现,目前已支持三种文件上传服务:Local, Minio, QiNiu OSS。
|
|
||||||
2. 功能新增:新增桌面版,使用 electron 套壳网页版。
|
|
||||||
3. Bug修复:自动去除众筹核销时候转账单号中的空格,防止复制的时候多复制了空格。
|
|
||||||
4. 功能优化:ChatPlus.vue 页面支持通过 chat_id path variable 来定位到指定的聊天。
|
|
||||||
5. 功能优化:取消导出聊天页面的授权验证
|
|
||||||
6. 功能优化:所有路由跳转都使用绝对路径
|
|
||||||
|
|
||||||
## v3.1.1
|
|
||||||
紧急修复版本,采用弹窗的方式显示验证码,解决验证码在低分辨率下被掩盖的Bug
|
|
||||||
|
|
||||||
## v3.1.0(大版本更新)
|
|
||||||
1. 功能重构:将聊天模型独立拆分,以便支持多平台模型,目前已经内置支持 OPenAI,Azure 以及 ChatGLM,用户可以在这两个平台的模型中随意切换,体验不同的模型聊天。
|
|
||||||
2. 功能重构:重写系统 API 授权机制,使用 JWT 替换传统的 session 会话授权,使得 API 授权变得更加灵活。
|
|
||||||
3. 功能重构:重构文件夹上传服务,支持多种文件上传存储handler,目前已经实现本地存储和 minio oss 存储。
|
|
||||||
4. 功能优化:更新头像自动删除旧的图片资源。
|
|
||||||
5. 功能优化:将应用日志在终端输出的同时存盘,方便 docker 部署查看日志。
|
|
||||||
6. 功能新增:允许用户配置自己的 OPenAI,Azure 以及 ChatGLM API KEY。
|
|
||||||
7. 功能优化:优化移动版的行为验证码样式,修复低分辨率显示器验证码被遮挡的 Bug
|
|
||||||
8. 升级 gin, element-plus,redis 组件到最新版本。
|
|
||||||
9. Bug修复:修复若干已知的的 Bug
|
|
||||||
|
|
||||||
## v3.0.7
|
|
||||||
|
|
||||||
1. 聊天主界面:新增聊天引导页面,介绍产品功能
|
|
||||||
2. 功能重构:拆分项目,将函数插件以及微信机器人,MidJourney 机器人等功能拆分新项目独立部署。
|
|
||||||
3. 功能新增:新增 MidJourney AI 绘画支持,当识别到用户的绘画需求时,自动调用 MidJourney 绘画函数进行绘画。
|
|
||||||
4. 功能新增:支持导出聊天记录为 PDF 文件。
|
|
||||||
5. 功能优化:在后台 dashboard 页面新增统计今日众筹收入。
|
|
||||||
6. 功能优化:支持用户设置默认的 GPT 模型
|
|
||||||
7. Bug修复:修复若干已知的的 Bug
|
|
||||||
|
|
||||||
## v3.0.6
|
|
||||||
|
|
||||||
1. 管理后台:新增用户名和手机号码搜索功能
|
|
||||||
2. 管理后台:新增重置用户密码功能
|
|
||||||
3. 管理后台:支持关闭注册功能,新增添加用户功能,适用于内部使用场景
|
|
||||||
4. 管理后台:新增仪表盘页面,统计当天的新增用户,新增会话数据,以及 Token 消耗
|
|
||||||
5. Bug修复:修复注册页面验证码不显示 Bug
|
|
||||||
6. Bug修复:优化上下文 Token 计算算法,修复聊天上下文超出限制时循环发送消息的 Bug
|
|
||||||
7. 功能修正:允许用户使用手机号码登录
|
|
||||||
8. 功能优化:更新系统配置后同步更新服务端内存变量数据
|
|
||||||
9. 功能优化:优化打包脚本,减少容器镜像大小
|
|
||||||
|
|
||||||
## v3.0.5
|
|
||||||
|
|
||||||
重磅功能更新!!! 新增函数插件支持,可以轻松地接入你的第三方插件服务,ChatGPT 自动帮您调用对应的函数完成任务。
|
|
||||||
|
|
||||||
1. 新增函数功能支持,全球早报,今日头条和微博热搜等插件服务,您也可以接入自己的第三方服务。
|
|
||||||
2. 集成微信机器人模块,可以通过微信个人收款码来完成充值,无需接入微信支付功能也可以完成收款功能。
|
|
||||||
3. 用户注册添加短信验证码功能,引入交互安全认证服务,有效防刷短信。
|
|
||||||
4. 支持配置聊天上下文深度,精确统计每轮对话所消耗的总 TOKEN 数量。
|
|
||||||
5. 修复已知的 Bug。
|
|
||||||
|
|
||||||
## v3.0.4
|
|
||||||
|
|
||||||
1. 调整项目目录结构,移除其他语言 API 目录
|
|
||||||
2. 修复 nodejs apple M1 跨平台打包,运行报错 exec format error
|
|
||||||
3. 增加用户 token 消耗统计功能
|
|
||||||
|
|
||||||
## v3.0.3
|
|
||||||
|
|
||||||
1. 优化启动参数接收处理,支持环境变量传参
|
|
||||||
2. 修复 PC 端聊天界面出现滚动条的 Bug
|
|
||||||
3. 修正前端 user_init_call 字段错误和用户注册初始化头像路径问题
|
|
||||||
4. 更改 docker 构建镜像的基础镜像,改用作者的阿里云镜像,这样打包更快一些。
|
|
||||||
|
|
||||||
## v3.0.2
|
|
||||||
|
|
||||||
1. Feat:新增移动端的聊天和用户设置功能
|
|
||||||
2. Fix: 修复 markdown 换行符解析的 Bug
|
|
||||||
3. Feat: 新增头像上传功能
|
|
||||||
4. Docs: 增加容器部署支持,支持 docker-compose 一键部署
|
|
||||||
5. Fix: 增加全局错误处理 handler,修复业务处理异常导致服务退出的 Bug
|
|
||||||
|
|
||||||
## v3.0.1
|
|
||||||
|
|
||||||
1. 紧急修复前端 Home 组件路由被后台管理 Home 组件路由覆盖的 Bug。
|
|
||||||
2. 增加 docker-compose 部署脚本
|
|
||||||
|
|
||||||
## v3.0.0
|
|
||||||
|
|
||||||
全新的重构版本!!!
|
|
||||||
新版的系统前后端都进行大改动的重构,后端还是用的 Gin Web 框架,但是作者整合了 fx 自动注入框架,整个后端应用结构非常简洁,特别适合二次开发。
|
|
||||||
另外,数据存储用 MySQL 替换了 leveldb, 因为要对 C 端,后期会涉及到很多业务数据查询统计,leveldb 已经完全不够用了。
|
|
||||||
前后台技术架构还是基于 `Vue3 + Element-Plus` ,但是页面风格已经全部变了,几乎所有页面样式代码都重写了,希望会你是希望的风格!
|
|
||||||
|
|
||||||
此次重构改版主要是为了后面功能的扩展准备了。
|
|
||||||
|
|
||||||
新版本已经实现的功能如下:
|
|
||||||
|
|
||||||
1. 引入用户体系,新增用户注册和登录功能。
|
|
||||||
2. 聊天页面改版,实现了跟 ChatGPT 官方版本一致的聊天体验。
|
|
||||||
3. 创建会话的时候可以选择聊天角色和模型。
|
|
||||||
4. 新增聊天设置功能,用户可以导入自己的 API KEY
|
|
||||||
5. 保存聊天记录,支持聊天上下文。
|
|
||||||
6. 重构后台管理模块,更友好,扩展性更好的后台管理系统。
|
|
||||||
7. 引入 ip2region 组件,记录用户的登录IP和地址。
|
|
||||||
8. 支持会话搜索过滤。
|
|
||||||
426
README.md
426
README.md
@@ -1,313 +1,157 @@
|
|||||||
# ChatGPT-Plus
|
# ChatGPT-Plus
|
||||||
|
|
||||||
**ChatGPT-PLUS** 基于 AI 大语言模型 API 实现的 AI 助手全套开源解决方案,自带运营管理后台,开箱即用。集成了 OpenAI, Azure,
|
基于 OpenAI API 实现的 ChatGPT Web 应用,一共分为两个版本:
|
||||||
ChatGLM,讯飞星火,文心一言等多个平台的大语言模型。主要有如下特性:
|
|
||||||
|
|
||||||
* 完整的开源系统,前端应用和后台管理系统皆可开箱即用。
|
* 通用版:交互体验跟 ChatGPT 官方一致,聊天记录保存在客户端(浏览器)
|
||||||
* 聊天体验跟 ChatGPT 官方版本完全一致。
|
* 角色版:内置了各种预训练好的角色,比如小红书写手,英语翻译大师,苏格拉底,孔子,乔布斯,周报助手等。轻松满足你的各种聊天和应用需求。
|
||||||
* 内置了各种预训练好的角色,比如小红书写手,英语翻译大师,苏格拉底,孔子,乔布斯,周报助手等。轻松满足你的各种聊天和应用需求。
|
聊天记录保存在云端(可以配置是否保存聊天记录)
|
||||||
* 支持 MidJourney AI 绘画集成,开箱即用。
|
每个版本都有 PC 版和移动版,PC 版本的体验要略优于移动版。
|
||||||
* 支持使用个人微信二维码作为充值收费的支付渠道,无需企业支付通道。(可定制开发其他支付通道支持)
|
|
||||||
* 集成插件 API 功能,可结合 GPT 开发各种强大的插件,已内置实现了微博热搜,今日头条,今日早报和 AI 绘画函数插件。
|
**本项目基于 MIT 协议,免费开放全部源代码,可以作为个人学习使用或者商用。如需商用建议联系作者登记,仅做统计使用,优秀项目我们将在项目首页为您展示。**
|
||||||
|
|
||||||
## 功能截图
|
## 功能截图
|
||||||
|
|
||||||
### PC 端聊天界面
|
### 1. 角色版PC端
|
||||||
|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### 新版聊天界面
|
### 2. 角色版移动端
|
||||||
|

|
||||||
|
|
||||||

|
### 3. 通用版
|
||||||
|

|
||||||
|
|
||||||
### MidJourney 专业绘画界面(v3.1.3)
|
### 4. 管理后台
|
||||||
|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### 自动调用函数插件
|

|
||||||
|
|
||||||

|
### 5. 体验地址
|
||||||
|
> 体验地址:[https://www.chat-plus.net/chat](https://www.chat-plus.net/chat) <br/>
|
||||||

|
> 涉及到数据隐私问题,没有提供共享账号,大家自己快速注册一个账号就可以免费体验
|
||||||
|
|
||||||
### 用户设置
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### 登录页面
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### 管理后台
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### 移动端 Web 页面
|
|
||||||
|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
### 7. 体验地址
|
|
||||||
|
|
||||||
> 免费体验地址:[https://ai.r9it.com/chat](https://ai.r9it.com/chat) <br/>
|
|
||||||
> **注意:请合法使用,禁止输出任何敏感、不友好或违规的内容!!!**
|
|
||||||
|
|
||||||
## 使用须知
|
|
||||||
|
|
||||||
1. 本项目基于 MIT 协议,免费开放全部源代码,可以作为个人学习使用或者商用。
|
|
||||||
2. 如需商用必须保留版权信息,请自觉遵守。确保合法合规使用,在运营过程中产生的一切任何后果自负,与作者无关。
|
|
||||||
|
|
||||||
## 项目介绍
|
## 项目介绍
|
||||||
|
这一套完整的系统,包括两套前端聊天应用和一个后台管理系统。系统有用户鉴权,你可以自己使用,也可以部署直接给 C 端用户提供 ChatGPT 的服务。
|
||||||
|
|
||||||
这一套完整的系统,包括前端聊天应用和一个后台管理系统。系统有用户鉴权,你可以自己使用,也可以部署直接给 C 端用户提供
|
项目的技术架构是
|
||||||
ChatGPT 的服务。
|
|
||||||
|
|
||||||
### 项目的技术架构
|
> Go + Vue3 + element-plus
|
||||||
|
|
||||||
新版的系统前后端都进行大改动的重构,后端还是用的 Gin Web 框架,但是作者整合了 fx 自动注入框架,整个后端应用结构非常简洁,特别适合二次开发。
|
后端采用的是 Go 语言开发的 Gin Web 框架。前端用的是 Vue3 + element-plus UI 框架
|
||||||
另外,数据存储用 MySQL 替换了 leveldb, 因为要对 C 端,后期会涉及到很多业务数据查询统计,leveldb 已经完全不够用了。
|
|
||||||
|
|
||||||
> Gin + fx + MySQL
|
目前已经实现了以下功能:
|
||||||
|
1. 通用版的 ChatGPT 聊天界面和功能,聊天记录保存在客户端。
|
||||||
3.0 版本之后会陆续添加其他语言的 API 实现,比如 PHP,Java 等。考虑到作者精力有限,api 目录已经添加了,有兴趣的同学自主去认领各自擅长的语言去实现。
|
2. 口令机制:输入口令才可以访问,支持设置口令的对话次数,有效期。
|
||||||
|
3. 角色版的聊天界面和功能,角色设定,预设一些角色,比如程序员,客服,作家,老师,艺术家...
|
||||||
前端的框架还是:
|
4. 保存聊天记录,支持聊天上下文。
|
||||||
|
5. OpenAI API 负载均衡,限制每个 API Key 每分钟之内调用次数不超过 15次,防止被封。
|
||||||
> Vue3 + Element-Plus
|
6. 支持配置多个代理,保证高可用。
|
||||||
|
7. 实现 markdown 语法解析和代码高亮,支持复制回复内容功能。
|
||||||
前后台的页面风格已经全部变了,几乎所有页面样式代码都重写了。逻辑代码还是沿用之前的,毕竟功能没有太大的变化。
|
8. 后台管理功能,实现系统的动态配置,口令和角色的动态管理。
|
||||||
|
|
||||||
此次重构改版主要是为了后面功能的扩展准备了。
|
|
||||||
|
|
||||||
新版本已经实现的功能如下:
|
|
||||||
|
|
||||||
1. 引入用户体系,新增用户注册和登录功能。
|
|
||||||
2. 聊天页面改版,实现了跟 ChatGPT 官方版本一致的聊天体验。
|
|
||||||
3. 创建会话的时候可以选择聊天角色和模型。
|
|
||||||
4. 新增聊天设置功能,用户可以导入自己的 API KEY
|
|
||||||
5. 保存聊天记录,支持聊天上下文。
|
|
||||||
6. 重构后台管理模块,更友好,扩展性更好的后台管理系统。
|
|
||||||
7. 引入 ip2region 组件,记录用户的登录IP和地址。
|
|
||||||
8. 支持会话搜索过滤。
|
|
||||||
9. 支持微信支付充值
|
|
||||||
|
|
||||||
## 项目地址
|
## 项目地址
|
||||||
|
|
||||||
* Github 地址:https://github.com/yangjian102621/chatgpt-plus
|
* Github 地址:https://github.com/yangjian102621/chatgpt-plus
|
||||||
* 码云地址:https://gitee.com/blackfox/chatgpt-plus
|
* 码云地址:https://gitee.com/blackfox/chatgpt-plus
|
||||||
|
|
||||||
|
|
||||||
## 客户端下载
|
|
||||||
|
|
||||||
目前已经支持 Win/Linux/Mac/Android 客户端,下载地址为:https://github.com/yangjian102621/chatgpt-plus/releases/tag/v3.1.2
|
|
||||||
|
|
||||||
|
|
||||||
## TODOLIST
|
## TODOLIST
|
||||||
|
* [ ] 让用户配置自己的 API KEY,调用自己的 API Key,将不消耗口令的对话次数
|
||||||
|
* [ ] 嵌入 AI 绘画功能,支持根据描述词生成图片
|
||||||
|
* [ ] 接入自己训练的开源大语言模型
|
||||||
|
* [ ] 接入 Google 语音 API,支持语音聊天
|
||||||
|
|
||||||
* [x] 整合 Midjourney AI 绘画 API
|
## 快速本地部署
|
||||||
* [x] 开发移动端聊天页面
|
如果你想在本地快速尝鲜一下,你可以直接下载我打包好的一键运行包,然后直接运行就好了,就这么简单。
|
||||||
* [x] 接入微信支付功能
|
支持 windows, linux 和 Mac 系统,[GitHub 下载地址](https://github.com/yangjian102621/chatgpt-plus/releases)。
|
||||||
* [x] 支持 ChatGPT 函数功能,通过函数实现插件
|
|
||||||
* [ ] 支持基于知识库的 AI 问答
|
|
||||||
* [ ] 开发桌面版应用
|
|
||||||
* [ ] 开发手机 App 客户端
|
|
||||||
|
|
||||||
## Docker 快速部署
|
我这里以 linux 系统为例,演示一下部署过程:
|
||||||
|
|
||||||
>
|
|
||||||
鉴于最新不少网友反馈在部署的时候遇到一些问题,大部分问题都是相同的,所以我这边做了一个视频教程 [五分钟部署自己的 ChatGPT 服务](https://www.bilibili.com/video/BV1H14y1B7Qw/)。
|
|
||||||
> 习惯看视频教程的朋友可以去看视频教程,视频的语速比较慢,建议 2 倍速观看。
|
|
||||||
|
|
||||||
V3.0.0 版本以后已经支持使用容器部署了,跳过所有的繁琐的环境准备,一条命令就可以轻松部署上线。
|
|
||||||
|
|
||||||
### 1. 导入数据库
|
|
||||||
|
|
||||||
首先我们需要创建一个 MySQL 容器,并导入初始数据库。
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
cd docker/mysql
|
# 1. 下载程序
|
||||||
# 创建 mysql 容器
|
wget https://github.com/yangjian102621/chatgpt-plus/releases/download/v2.0.0/wechatGPT-amd64-linux
|
||||||
docker-compose up -d
|
# 2. 添加执行权限
|
||||||
# 导入数据库
|
chmod +x wechatGPT-amd64-linux
|
||||||
docker exec -i chatgpt-plus-mysql sh -c 'exec mysql -uroot -p12345678' < ../../database/chatgpt_plus-v3.1.4.sql
|
# 3. 运行程序
|
||||||
|
./wechatGPT-amd64-linux
|
||||||
```
|
```
|
||||||
|
服务启动成功之后直接访问后台管理页面(http://localhost:5678/chat/#/admin),初始用户名和密码分别为 `admin` 和 `admin123`。
|
||||||
|
|
||||||
如果你本地已经安装了 MySQL 服务,那么你只需手动导入数据库即可。
|
修改系统配置。这里主要配置 OpenAI 的 [API KEY](https://platform.openai.com/account/api-keys) 和魔法,因为调用 OpenAI 的 API 需要魔法。
|
||||||
|
|
||||||
```shell
|

|
||||||
# 连接数据库
|
|
||||||
mysql -u username -p password
|
|
||||||
# 导入数据库
|
|
||||||
source database/chatgpt_plus.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 修改配置文档
|
最后,你还需要一个口令来访问 ChatGPT 前端应用。直接在【后台管理系统->口令管理】新增一个口令就好了。
|
||||||
|
|
||||||
修改配置文档 `docker/conf/config.toml` 配置文档,修改代理地址和管理员密码:
|

|
||||||
|
|
||||||
```toml
|
**切记要先退出后台登录,再登录前端应用!!!前后端账号权限不一致。**
|
||||||
Listen = "0.0.0.0:5678"
|
|
||||||
ProxyURL = "" # 如 http://127.0.0.1:7777
|
|
||||||
MysqlDns = "root:12345678@tcp(172.22.11.200:3307)/chatgpt_plus?charset=utf8&parseTime=True&loc=Local"
|
|
||||||
StaticDir = "./static" # 静态资源的目录
|
|
||||||
StaticUrl = "/static" # 静态资源访问 URL
|
|
||||||
AesEncryptKey = ""
|
|
||||||
WeChatBot = false # 是否启动微信机器人
|
|
||||||
|
|
||||||
[Session]
|
然后直接使用口令名称登录前端应用:
|
||||||
SecretKey = "azyehq3ivunjhbntz78isj00i4hz2mt9xtddysfucxakadq4qbfrt0b7q3lnvg80" # 注意:这个是 JWT Token 授权密钥,生产环境请务必更换
|
|
||||||
MaxAge = 86400
|
|
||||||
|
|
||||||
[Manager]
|
* 角色版:http://localhost:5678/chat/#/plus
|
||||||
Username = "admin"
|
* 普通版:http://localhost:5678/chat/#/free
|
||||||
Password = "admin123" # 如果是生产环境的话,这里管理员的密码记得修改
|
|
||||||
|
|
||||||
[Redis] # redis 配置信息
|
祝你使用愉快!
|
||||||
Host = "localhost"
|
|
||||||
Port = 6379
|
|
||||||
Password = ""
|
|
||||||
DB = 0
|
|
||||||
|
|
||||||
[ApiConfig] # 微博热搜,今日头条等函数服务 API 配置,此为第三方插件服务,如需使用请联系作者开通
|
|
||||||
ApiURL = ""
|
|
||||||
AppId = ""
|
|
||||||
Token = ""
|
|
||||||
|
|
||||||
[SmsConfig] # 阿里云短信服务配置
|
|
||||||
AccessKey = ""
|
|
||||||
AccessSecret = ""
|
|
||||||
Product = "Dysmsapi"
|
|
||||||
Domain = "dysmsapi.aliyuncs.com"
|
|
||||||
|
|
||||||
[ExtConfig] # MidJourney和微信机器人服务 API 配置,开通此功能需要配合 chatpgt-plus-exts 项目部署
|
|
||||||
ApiURL = "" # 插件扩展 API 地址
|
|
||||||
Token = "" # 这个 token 随便填,只要确保跟 chatgpt-plus-exts 项目的 token 一样就行
|
|
||||||
|
|
||||||
[OSS] # OSS 配置,用于存储 MJ 绘画图片
|
|
||||||
Active = "local" # 默认使用本地文件存储引擎
|
|
||||||
[OSS.Local]
|
|
||||||
BasePath = "./static/upload" # 本地文件上传根路径
|
|
||||||
BaseURL = "http://localhost:5678/static/upload" # 本地上传文件根 URL 如果是线上,则直接设置为 /static/upload 即可
|
|
||||||
[OSS.Minio]
|
|
||||||
Endpoint = "" # 如 172.22.11.200:9000
|
|
||||||
AccessKey = "" # 自己去 Minio 控制台去创建一个 Access Key
|
|
||||||
AccessSecret = ""
|
|
||||||
Bucket = "chatgpt-plus" # 替换为你自己创建的 Bucket,注意要给 Bucket 设置公开的读权限,否则会出现图片无法显示。
|
|
||||||
UseSSL = false
|
|
||||||
Domain = "" # 地址必须是能够通过公网访问的,否则会出现图片无法显示。
|
|
||||||
[OSS.QiNiu] # 七牛云 OSS 配置
|
|
||||||
Zone = "z2" # 区域,z0:华东,z1: 华北,na0:北美,as0:新加坡
|
|
||||||
AccessKey = ""
|
|
||||||
AccessSecret = ""
|
|
||||||
Bucket = ""
|
|
||||||
Domain = "" # OSS Bucket 所绑定的域名,如 https://img.r9it.com
|
|
||||||
|
|
||||||
[MjConfig] # MidJourney AI 绘画配置
|
|
||||||
Enabled = false # 是否启动 MidJourney 机器人服务
|
|
||||||
UserToken = "" # 用户授权 Token
|
|
||||||
BotToken = "" # Discord 机器人 Token
|
|
||||||
GuildId = "" # 服务器 ID
|
|
||||||
ChanelId = "" # 频道 ID
|
|
||||||
|
|
||||||
[SdConfig]
|
|
||||||
Enabled = false # 是否启动 Stable Diffusion 机器人服务
|
|
||||||
ApiURL = "http://172.22.11.200:7860" # stable-diffusion-webui API 地址
|
|
||||||
ApiKey = "" # 如果开启了授权,这里需要配置授权的 ApiKey
|
|
||||||
Txt2ImgJsonPath = "res/text2img.json" # 文生图的 API 请求报文 json 模板,允许自定义请求json报文,因为不同版本的 API 绘图的参数以及 fn_index 会不同。
|
|
||||||
```
|
|
||||||
|
|
||||||
> 1. 如果你不知道如何获取 Discord 用户 Token 和 Bot Token
|
|
||||||
请查参考 [Midjourney|如何集成到自己的平台](https://zhuanlan.zhihu.com/p/631079476)。
|
|
||||||
> 2. `Txt2ImgJsonPath` 的默认用的是使用最广泛的 [stable-diffusion-webui](https://github.com/AUTOMATIC1111/stable-diffusion-webui) 项目的 API,如果你用的是其他版本,比如秋叶的懒人包部署的,那么请将对应的 text2img 的参数报文复制放在 `res/text2img.json` 文件中即可。
|
|
||||||
|
|
||||||
修改 nginx 配置文档 `docker/conf/nginx/conf.d/chatgpt-plus.conf`,把后端转发的地址改成当前主机的内网 IP 地址。
|
|
||||||
|
|
||||||
```shell
|
|
||||||
# 这里配置后端 API 的转发
|
|
||||||
location /api/ {
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_connect_timeout 300s;
|
|
||||||
proxy_read_timeout 300s;
|
|
||||||
proxy_send_timeout 12s;
|
|
||||||
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://172.28.173.76:6789; # 这里改成后端服务的内网 IP 地址
|
|
||||||
|
|
||||||
# 静态资源转发
|
|
||||||
location /static/ {
|
|
||||||
proxy_pass http://172.22.11.47:5678; # 这里改成后端服务的内网 IP 地址
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 启动应用
|
|
||||||
|
|
||||||
```shell
|
|
||||||
cd docker
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
* 前端访问地址:http://localhost:8080/chat
|
|
||||||
* 后台管理地址:http://localhost:8080/admin
|
|
||||||
* 移动端地址:http://localhost:8080/mobile
|
|
||||||
|
|
||||||
> 注意:你得访问后台管理系统 http://localhost:8080/admin
|
|
||||||
> 输入你前面配置文档中设置的管理员用户名和密码登录。
|
|
||||||
> 然后进入 `API KEY 管理` 菜单,添加一个 OpenAI 的 API KEY 才可以正常开启 AI 对话。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
最后登录前端聊天页面 [http://localhost:8080/chat](http://localhost:8080/chat)
|
|
||||||
你可以注册新用户,也可以使用系统默认有个账号:`geekmaster/12345678` 登录聊天。
|
|
||||||
|
|
||||||
祝你使用愉快!!!
|
|
||||||
|
|
||||||
## 本地开发调试
|
## 本地开发调试
|
||||||
|
本项目的技术架构是
|
||||||
|
|
||||||
本地开发同样要分别运行前端和后端程序。
|
> Go + Vue3 + element-plus
|
||||||
|
|
||||||
### 运行后端程序
|
所以你需要提前安装好 Go(1.19以上)语言环境和 Nodejs 环境。
|
||||||
|
|
||||||
1. 同样你首先要 [导入数据库](#1-导入数据库)
|
### 1. 运行后端程序
|
||||||
2. 然后 [修改配置文档](#2-修改配置文档)
|
|
||||||
3. 运行后端程序:
|
首先,我们需要拷贝默认的配置文档:
|
||||||
|
```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
|
```shell
|
||||||
cd api
|
|
||||||
# 1. 先下载依赖
|
# 1. 先下载依赖
|
||||||
go mod tidy
|
go mod tidy
|
||||||
# 2. 运行程序
|
# 2. 运行程序
|
||||||
go run main.go
|
go run main.go
|
||||||
# 如果你安装了 fresh 可以使用 fresh 实现热启动
|
|
||||||
fresh -c fresh.conf
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 运行前端程序
|
### 2. 运行前端程序
|
||||||
|
|
||||||
同样先拷贝配置文档:
|
同样先拷贝配置文档:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
cd web
|
cd web
|
||||||
cp .env.production .env.development
|
cp .env.production .env.development
|
||||||
```
|
```
|
||||||
|
|
||||||
编辑 `.env.development` 文件,修改后端 API 的访问路径:
|
编辑 `.env.development` 文件,修改后端 API 的访问路径:
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
VUE_APP_API_HOST=http://localhost:5678
|
VUE_APP_API_HOST=http://172.22.11.200:5678 # 这里也是需要把 IP 替换成你本地的内网 IP 地址
|
||||||
VUE_APP_WS_HOST=ws://localhost:5678
|
VUE_APP_WS_HOST=ws://172.22.11.200:5678
|
||||||
```
|
```
|
||||||
|
|
||||||
配置好了之后就可以运行前端应用了:
|
配置好了之后就可以运行前端应用了:
|
||||||
|
|
||||||
```
|
```
|
||||||
# 安装依赖
|
# 安装依赖
|
||||||
npm install
|
npm install
|
||||||
@@ -315,46 +159,102 @@ npm install
|
|||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
* 前端页面:http://localhost:8888/chat
|
启动之后通过**内网地址**访问后台管理页面:http://172.22.11.200:8888/chat/#/admin ,添加一个口令就可以开始测试了。
|
||||||
* 后台管理页面:http://localhost:8888/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
|
```shell
|
||||||
cd web
|
|
||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
### 打包后端
|
### 打包后端
|
||||||
|
|
||||||
你可以根据个人需求将项目打包成 windows/linux/darwin 平台项目。
|
你可以根据个人需求将项目打包成 windows/linux/darwin 平台项目。
|
||||||
|
```shell
|
||||||
|
# for all platforms
|
||||||
|
make all
|
||||||
|
# for linux only
|
||||||
|
make linux
|
||||||
|
```
|
||||||
|
打包后的可执行文件在 `src/bin` 目录下。
|
||||||
|
|
||||||
|
### 线上部署
|
||||||
|
部署方式跟 [快速本地部署](#快速本地部署) 一样,将打包好的可执行文件在线上服务器运行即可。
|
||||||
|
|
||||||
|
> **特别注意:** 线上发布请记得修改配置文档中的 AccessKey, 以免给你的应用造成风险!!!
|
||||||
|
|
||||||
|
**另外,如果你部署在国外的服务器,那么就不需要配置代理,直接留空就行了。**
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
cd api
|
ProxyURL = []
|
||||||
# for all platforms
|
|
||||||
make clean all
|
|
||||||
# for linux only
|
|
||||||
make clean linux
|
|
||||||
```
|
```
|
||||||
|
### 使用 Nginx 代理
|
||||||
|
|
||||||
打包后的可执行文件在 `bin` 目录下。
|
```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 等。
|
个人的力量始终有限,任何形式的贡献都是欢迎的,包括但不限于贡献代码,优化文档,提交 issue 和 PR 等。
|
||||||
|
|
||||||
如果有兴趣的话,也可以加微信进入微信讨论群(**添加好友时请注明来自Github!!!**)。
|
如果有兴趣的话,也可以加微信进入微信讨论群。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### 特此声明:不接受在微信或者微信群给开发者提 Bug,有问题或者优化建议请提交 Issue 和 PR。非常感谢您的配合!
|
#### 特此声明:不接受在微信或者微信群给开发者提 Bug,有问题或者优化建议请提交 Issue 和 PR。非常感谢您的配合!
|
||||||
|
|
||||||
### Commit 类型
|
### Commit 类型
|
||||||
|
|
||||||
* feat: 新特性或功能
|
* feat: 新特性或功能
|
||||||
* fix: 缺陷修复
|
* fix: 缺陷修复
|
||||||
* docs: 文档更新
|
* docs: 文档更新
|
||||||
@@ -363,8 +263,8 @@ make clean linux
|
|||||||
* opt: 性能优化
|
* opt: 性能优化
|
||||||
* chore: 一些不涉及到功能变动的小提交,比如修改文字表述,修改注释等
|
* chore: 一些不涉及到功能变动的小提交,比如修改文字表述,修改注释等
|
||||||
|
|
||||||
## 打赏
|
|
||||||
|
|
||||||
|
## 打赏
|
||||||
如果你觉得这个项目对你有帮助,并且情况允许的话,可以请作者喝杯咖啡,非常感谢你的支持~
|
如果你觉得这个项目对你有帮助,并且情况允许的话,可以请作者喝杯咖啡,非常感谢你的支持~
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
20
api/.gitignore
vendored
20
api/.gitignore
vendored
@@ -1,20 +0,0 @@
|
|||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/extensions.json
|
|
||||||
.idea
|
|
||||||
.DS_Store
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
||||||
tmp
|
|
||||||
bin
|
|
||||||
data
|
|
||||||
config.toml
|
|
||||||
static/upload
|
|
||||||
storage.json
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# chatgpt-plus-go
|
|
||||||
|
|
||||||
chatgpt-plus 后端 API Go 语言实现。技术选型采用 Gin + Mysql 架构,依赖注入使用的是 fx 框架,ORM 采用的是 GORM 框架。
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
Listen = "0.0.0.0:5678"
|
|
||||||
ProxyURL = "" # 如 http://127.0.0.1:7777
|
|
||||||
MysqlDns = "root:12345678@tcp(172.22.11.200:3307)/chatgpt_plus?charset=utf8&parseTime=True&loc=Local"
|
|
||||||
StaticDir = "./static" # 静态资源的目录
|
|
||||||
StaticUrl = "/static" # 静态资源访问 URL
|
|
||||||
AesEncryptKey = ""
|
|
||||||
WeChatBot = false
|
|
||||||
|
|
||||||
[Session]
|
|
||||||
SecretKey = "azyehq3ivunjhbntz78isj00i4hz2mt9xtddysfucxakadq4qbfrt0b7q3lnvg80" # 注意:这个是 JWT Token 授权密钥,生产环境请务必更换
|
|
||||||
MaxAge = 86400
|
|
||||||
|
|
||||||
[Manager]
|
|
||||||
Username = "admin"
|
|
||||||
Password = "admin123" # 如果是生产环境的话,这里管理员的密码记得修改
|
|
||||||
|
|
||||||
[Redis] # redis 配置信息
|
|
||||||
Host = "localhost"
|
|
||||||
Port = 6379
|
|
||||||
Password = ""
|
|
||||||
DB = 0
|
|
||||||
|
|
||||||
[ApiConfig] # 微博热搜,今日头条等函数服务 API 配置,此为第三方插件服务,如需使用请联系作者开通
|
|
||||||
ApiURL = ""
|
|
||||||
AppId = ""
|
|
||||||
Token = ""
|
|
||||||
|
|
||||||
[SmsConfig] # 阿里云短信服务配置
|
|
||||||
AccessKey = ""
|
|
||||||
AccessSecret = ""
|
|
||||||
Product = "Dysmsapi"
|
|
||||||
Domain = "dysmsapi.aliyuncs.com"
|
|
||||||
Sign = ""
|
|
||||||
CodeTempId = ""
|
|
||||||
|
|
||||||
[ExtConfig] # MidJourney和微信机器人服务 API 配置,开通此功能需要配合 chatpgt-plus-exts 项目部署
|
|
||||||
ApiURL = "" # 插件扩展 API 地址
|
|
||||||
Token = "" # 这个 token 随便填,只要确保跟 chatgpt-plus-exts 项目的 token 一样就行
|
|
||||||
|
|
||||||
[OSS] # OSS 配置,用于存储 MJ 绘画图片
|
|
||||||
Active = "local" # 默认使用本地文件存储引擎
|
|
||||||
[OSS.Local]
|
|
||||||
BasePath = "./static/upload" # 本地文件上传根路径
|
|
||||||
BaseURL = "http://localhost:5678/static/upload" # 本地上传文件根 URL 如果是线上,则直接设置为 /static/upload 即可
|
|
||||||
[OSS.Minio]
|
|
||||||
Endpoint = "" # 如 172.22.11.200:9000
|
|
||||||
AccessKey = "" # 自己去 Minio 控制台去创建一个 Access Key
|
|
||||||
AccessSecret = ""
|
|
||||||
Bucket = "chatgpt-plus" # 替换为你自己创建的 Bucket,注意要给 Bucket 设置公开的读权限,否则会出现图片无法显示。
|
|
||||||
UseSSL = false
|
|
||||||
Domain = "" # 地址必须是能够通过公网访问的,否则会出现图片无法显示。
|
|
||||||
[OSS.QiNiu] # 七牛云 OSS 配置
|
|
||||||
Zone = "z2" # 区域,z0:华东,z1: 华北,na0:北美,as0:新加坡
|
|
||||||
AccessKey = ""
|
|
||||||
AccessSecret = ""
|
|
||||||
Bucket = ""
|
|
||||||
Domain = "" # OSS Bucket 所绑定的域名,如 https://img.r9it.com
|
|
||||||
|
|
||||||
[MjConfig]
|
|
||||||
Enabled = false
|
|
||||||
UserToken = ""
|
|
||||||
BotToken = ""
|
|
||||||
GuildId = ""
|
|
||||||
ChanelId = ""
|
|
||||||
|
|
||||||
[SdConfig]
|
|
||||||
Enabled = false
|
|
||||||
ApiURL = "http://172.22.11.200:7860"
|
|
||||||
ApiKey = ""
|
|
||||||
Txt2ImgJsonPath = "res/text2img.json"
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core/types"
|
|
||||||
"chatplus/service/fun"
|
|
||||||
"chatplus/store/model"
|
|
||||||
"chatplus/utils"
|
|
||||||
"chatplus/utils/resp"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/go-redis/redis/v8"
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"runtime/debug"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AppServer struct {
|
|
||||||
Debug bool
|
|
||||||
Config *types.AppConfig
|
|
||||||
Engine *gin.Engine
|
|
||||||
ChatContexts *types.LMap[string, []interface{}] // 聊天上下文 Map [chatId] => []Message
|
|
||||||
|
|
||||||
ChatConfig *types.ChatConfig // chat config cache
|
|
||||||
SysConfig *types.SystemConfig // system config cache
|
|
||||||
|
|
||||||
// 保存 Websocket 会话 UserId, 每个 UserId 只能连接一次
|
|
||||||
// 防止第三方直接连接 socket 调用 OpenAI API
|
|
||||||
ChatSession *types.LMap[string, *types.ChatSession] //map[sessionId]UserId
|
|
||||||
ChatClients *types.LMap[string, *types.WsClient] // map[sessionId]Websocket 连接集合
|
|
||||||
ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function
|
|
||||||
Functions map[string]fun.Function
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewServer(appConfig *types.AppConfig, functions map[string]fun.Function) *AppServer {
|
|
||||||
gin.SetMode(gin.ReleaseMode)
|
|
||||||
gin.DefaultWriter = io.Discard
|
|
||||||
return &AppServer{
|
|
||||||
Debug: false,
|
|
||||||
Config: appConfig,
|
|
||||||
Engine: gin.Default(),
|
|
||||||
ChatContexts: types.NewLMap[string, []interface{}](),
|
|
||||||
ChatSession: types.NewLMap[string, *types.ChatSession](),
|
|
||||||
ChatClients: types.NewLMap[string, *types.WsClient](),
|
|
||||||
ReqCancelFunc: types.NewLMap[string, context.CancelFunc](),
|
|
||||||
Functions: functions,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AppServer) Init(debug bool, client *redis.Client) {
|
|
||||||
if debug { // 调试模式允许跨域请求 API
|
|
||||||
s.Debug = debug
|
|
||||||
logger.Info("Enabled debug mode")
|
|
||||||
}
|
|
||||||
s.Engine.Use(corsMiddleware())
|
|
||||||
s.Engine.Use(authorizeMiddleware(s, client))
|
|
||||||
s.Engine.Use(errorHandler)
|
|
||||||
// 添加静态资源访问
|
|
||||||
s.Engine.Static("/static", s.Config.StaticDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AppServer) Run(db *gorm.DB) error {
|
|
||||||
// load chat config from database
|
|
||||||
var chatConfig model.Config
|
|
||||||
res := db.Where("marker", "chat").First(&chatConfig)
|
|
||||||
if res.Error != nil {
|
|
||||||
return res.Error
|
|
||||||
}
|
|
||||||
err := utils.JsonDecode(chatConfig.Config, &s.ChatConfig)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// load system configs
|
|
||||||
var sysConfig model.Config
|
|
||||||
res = db.Where("marker", "system").First(&sysConfig)
|
|
||||||
if res.Error != nil {
|
|
||||||
return res.Error
|
|
||||||
}
|
|
||||||
err = utils.JsonDecode(sysConfig.Config, &s.SysConfig)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
logger.Infof("http://%s", s.Config.Listen)
|
|
||||||
return s.Engine.Run(s.Config.Listen)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 全局异常处理
|
|
||||||
func errorHandler(c *gin.Context) {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
logger.Errorf("Handler Panic: %v", r)
|
|
||||||
debug.PrintStack()
|
|
||||||
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: types.ErrorMsg})
|
|
||||||
c.Abort()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
//加载完 defer recover,继续后续接口调用
|
|
||||||
c.Next()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 跨域中间件设置
|
|
||||||
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, Chat-Token, Admin-Authorization")
|
|
||||||
// 允许浏览器(客户端)可以解析的头部 (重要)
|
|
||||||
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, client *redis.Client) gin.HandlerFunc {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
if c.Request.URL.Path == "/api/user/login" ||
|
|
||||||
c.Request.URL.Path == "/api/admin/login" ||
|
|
||||||
c.Request.URL.Path == "/api/user/register" ||
|
|
||||||
c.Request.URL.Path == "/api/reward/notify" ||
|
|
||||||
c.Request.URL.Path == "/api/mj/notify" ||
|
|
||||||
c.Request.URL.Path == "/api/chat/history" ||
|
|
||||||
c.Request.URL.Path == "/api/chat/detail" ||
|
|
||||||
c.Request.URL.Path == "/api/mj/proxy" ||
|
|
||||||
strings.HasPrefix(c.Request.URL.Path, "/api/sms/") ||
|
|
||||||
strings.HasPrefix(c.Request.URL.Path, "/api/captcha/") ||
|
|
||||||
strings.HasPrefix(c.Request.URL.Path, "/static/") ||
|
|
||||||
c.Request.URL.Path == "/api/admin/config/get" {
|
|
||||||
c.Next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var tokenString string
|
|
||||||
if strings.Contains(c.Request.URL.Path, "/api/admin/") { // 后台管理 API
|
|
||||||
tokenString = c.GetHeader(types.AdminAuthHeader)
|
|
||||||
} else if c.Request.URL.Path == "/api/chat/new" ||
|
|
||||||
c.Request.URL.Path == "/api/mj/client" ||
|
|
||||||
c.Request.URL.Path == "/api/sd/client" {
|
|
||||||
tokenString = c.Query("token")
|
|
||||||
} else {
|
|
||||||
tokenString = c.GetHeader(types.UserAuthHeader)
|
|
||||||
}
|
|
||||||
if tokenString == "" {
|
|
||||||
resp.ERROR(c, "You should put Authorization in request headers")
|
|
||||||
c.Abort()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
|
||||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
|
||||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
|
||||||
}
|
|
||||||
|
|
||||||
return []byte(s.Config.Session.SecretKey), nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
resp.NotAuth(c, fmt.Sprintf("Error with parse auth token: %v", err))
|
|
||||||
c.Abort()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
claims, ok := token.Claims.(jwt.MapClaims)
|
|
||||||
if !ok || !token.Valid {
|
|
||||||
resp.NotAuth(c, "Token is invalid")
|
|
||||||
c.Abort()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
expr := utils.IntValue(utils.InterfaceToString(claims["expired"]), 0)
|
|
||||||
if expr > 0 && int64(expr) < time.Now().Unix() {
|
|
||||||
resp.NotAuth(c, "Token is expired")
|
|
||||||
c.Abort()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
key := fmt.Sprintf("users/%v", claims["user_id"])
|
|
||||||
if _, err := client.Get(context.Background(), key).Result(); err != nil {
|
|
||||||
resp.NotAuth(c, "Token is not found in redis")
|
|
||||||
c.Abort()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.Set(types.LoginUserID, claims["user_id"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"chatplus/core/types"
|
|
||||||
logger2 "chatplus/logger"
|
|
||||||
"chatplus/utils"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/BurntSushi/toml"
|
|
||||||
)
|
|
||||||
|
|
||||||
var logger = logger2.GetLogger()
|
|
||||||
|
|
||||||
func NewDefaultConfig() *types.AppConfig {
|
|
||||||
return &types.AppConfig{
|
|
||||||
Listen: "0.0.0.0:5678",
|
|
||||||
ProxyURL: "",
|
|
||||||
Manager: types.Manager{Username: "admin", Password: "admin123"},
|
|
||||||
StaticDir: "./static",
|
|
||||||
StaticUrl: "http://localhost/5678/static",
|
|
||||||
Redis: types.RedisConfig{Host: "localhost", Port: 6379, Password: ""},
|
|
||||||
AesEncryptKey: utils.RandString(24),
|
|
||||||
Session: types.Session{
|
|
||||||
SecretKey: utils.RandString(64),
|
|
||||||
MaxAge: 86400,
|
|
||||||
},
|
|
||||||
ApiConfig: types.ChatPlusApiConfig{},
|
|
||||||
OSS: types.OSSConfig{
|
|
||||||
Active: "local",
|
|
||||||
Local: types.LocalStorageConfig{
|
|
||||||
BaseURL: "http://localhost/5678/static/upload",
|
|
||||||
BasePath: "./static/upload",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
MjConfig: types.MidJourneyConfig{Enabled: false},
|
|
||||||
SdConfig: types.StableDiffusionConfig{Enabled: false, Txt2ImgJsonPath: "res/text2img.json"},
|
|
||||||
WeChatBot: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
package types
|
|
||||||
|
|
||||||
// ApiRequest API 请求实体
|
|
||||||
type ApiRequest struct {
|
|
||||||
Model string `json:"model,omitempty"` // 兼容百度文心一言
|
|
||||||
Temperature float32 `json:"temperature"`
|
|
||||||
MaxTokens int `json:"max_tokens,omitempty"` // 兼容百度文心一言
|
|
||||||
Stream bool `json:"stream"`
|
|
||||||
Messages []interface{} `json:"messages,omitempty"`
|
|
||||||
Prompt []interface{} `json:"prompt,omitempty"` // 兼容 ChatGLM
|
|
||||||
Functions []Function `json:"functions,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Message struct {
|
|
||||||
Role string `json:"role"`
|
|
||||||
Content string `json:"content"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ApiResponse struct {
|
|
||||||
Choices []ChoiceItem `json:"choices"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChoiceItem API 响应实体
|
|
||||||
type ChoiceItem struct {
|
|
||||||
Delta Delta `json:"delta"`
|
|
||||||
FinishReason string `json:"finish_reason"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Delta struct {
|
|
||||||
Role string `json:"role"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Content interface{} `json:"content"`
|
|
||||||
FunctionCall FunctionCall `json:"function_call,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 ChatModel `json:"model"` // GPT 模型
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChatModel struct {
|
|
||||||
Id uint `json:"id"`
|
|
||||||
Platform Platform `json:"platform"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ApiError struct {
|
|
||||||
Error struct {
|
|
||||||
Message string
|
|
||||||
Type string
|
|
||||||
Param interface{}
|
|
||||||
Code string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const PromptMsg = "prompt" // prompt message
|
|
||||||
const ReplyMsg = "reply" // reply message
|
|
||||||
const MjMsg = "mj"
|
|
||||||
|
|
||||||
var ModelToTokens = map[string]int{
|
|
||||||
"gpt-3.5-turbo": 4096,
|
|
||||||
"gpt-3.5-turbo-16k": 16384,
|
|
||||||
"gpt-4": 8192,
|
|
||||||
"gpt-4-32k": 32768,
|
|
||||||
"chatglm_pro": 32768, // 清华智普
|
|
||||||
"chatglm_std": 16384,
|
|
||||||
"chatglm_lite": 4096,
|
|
||||||
"ernie_bot_turbo": 8192, // 文心一言
|
|
||||||
"general": 8192, // 科大讯飞
|
|
||||||
"general2": 8192,
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
package types
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AppConfig struct {
|
|
||||||
Path string `toml:"-"`
|
|
||||||
Listen string
|
|
||||||
Session Session
|
|
||||||
ProxyURL string
|
|
||||||
MysqlDns string // mysql 连接地址
|
|
||||||
Manager Manager // 后台管理员账户信息
|
|
||||||
StaticDir string // 静态资源目录
|
|
||||||
StaticUrl string // 静态资源 URL
|
|
||||||
Redis RedisConfig // redis 连接信息
|
|
||||||
ApiConfig ChatPlusApiConfig // ChatPlus API authorization configs
|
|
||||||
AesEncryptKey string
|
|
||||||
SmsConfig AliYunSmsConfig // AliYun send message service config
|
|
||||||
OSS OSSConfig // OSS config
|
|
||||||
MjConfig MidJourneyConfig // mj 绘画配置
|
|
||||||
WeChatBot bool // 是否启用微信机器人
|
|
||||||
SdConfig StableDiffusionConfig // sd 绘画配置
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChatPlusApiConfig struct {
|
|
||||||
ApiURL string
|
|
||||||
AppId string
|
|
||||||
Token string
|
|
||||||
}
|
|
||||||
|
|
||||||
type MidJourneyConfig struct {
|
|
||||||
Enabled bool
|
|
||||||
UserToken string
|
|
||||||
BotToken string
|
|
||||||
GuildId string // Server ID
|
|
||||||
ChanelId string // Chanel ID
|
|
||||||
}
|
|
||||||
|
|
||||||
type WeChatConfig struct {
|
|
||||||
Enabled bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type StableDiffusionConfig struct {
|
|
||||||
Enabled bool
|
|
||||||
ApiURL string
|
|
||||||
ApiKey string
|
|
||||||
Txt2ImgJsonPath string
|
|
||||||
}
|
|
||||||
|
|
||||||
type AliYunSmsConfig struct {
|
|
||||||
AccessKey string
|
|
||||||
AccessSecret string
|
|
||||||
Product string
|
|
||||||
Domain string
|
|
||||||
Sign string // 短信签名
|
|
||||||
CodeTempId string // 验证码短信模板 ID
|
|
||||||
}
|
|
||||||
|
|
||||||
type RedisConfig struct {
|
|
||||||
Host string
|
|
||||||
Port int
|
|
||||||
Password string
|
|
||||||
DB int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c RedisConfig) Url() string {
|
|
||||||
return fmt.Sprintf("%s:%d", c.Host, c.Port)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Manager 管理员
|
|
||||||
type Manager struct {
|
|
||||||
Username string `json:"username"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChatConfig 系统默认的聊天配置
|
|
||||||
type ChatConfig struct {
|
|
||||||
OpenAI ModelAPIConfig `json:"open_ai"`
|
|
||||||
Azure ModelAPIConfig `json:"azure"`
|
|
||||||
ChatGML ModelAPIConfig `json:"chat_gml"`
|
|
||||||
Baidu ModelAPIConfig `json:"baidu"`
|
|
||||||
XunFei ModelAPIConfig `json:"xun_fei"`
|
|
||||||
|
|
||||||
EnableContext bool `json:"enable_context"` // 是否开启聊天上下文
|
|
||||||
EnableHistory bool `json:"enable_history"` // 是否允许保存聊天记录
|
|
||||||
ContextDeep int `json:"context_deep"` // 上下文深度
|
|
||||||
}
|
|
||||||
|
|
||||||
type Platform string
|
|
||||||
|
|
||||||
const OpenAI = Platform("OpenAI")
|
|
||||||
const Azure = Platform("Azure")
|
|
||||||
const ChatGLM = Platform("ChatGLM")
|
|
||||||
const Baidu = Platform("Baidu")
|
|
||||||
const XunFei = Platform("XunFei")
|
|
||||||
|
|
||||||
// UserChatConfig 用户的聊天配置
|
|
||||||
type UserChatConfig struct {
|
|
||||||
ApiKeys map[Platform]string `json:"api_keys"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ModelAPIConfig struct {
|
|
||||||
ApiURL string `json:"api_url,omitempty"`
|
|
||||||
Temperature float32 `json:"temperature"`
|
|
||||||
MaxTokens int `json:"max_tokens"`
|
|
||||||
ApiKey string `json:"api_key"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SystemConfig struct {
|
|
||||||
Title string `json:"title"`
|
|
||||||
AdminTitle string `json:"admin_title"`
|
|
||||||
Models []string `json:"models"`
|
|
||||||
UserInitCalls int `json:"user_init_calls"` // 新用户注册默认总送多少次调用
|
|
||||||
InitImgCalls int `json:"init_img_calls"`
|
|
||||||
VipMonthCalls int `json:"vip_month_calls"` // 会员每个赠送的调用次数
|
|
||||||
EnabledRegister bool `json:"enabled_register"`
|
|
||||||
EnabledMsg bool `json:"enabled_msg"` // 启用短信验证码服务
|
|
||||||
EnabledDraw bool `json:"enabled_draw"` // 启动 AI 绘画功能
|
|
||||||
RewardImg string `json:"reward_img"` // 众筹收款二维码地址
|
|
||||||
EnabledFunction bool `json:"enabled_function"` // 启用 API 函数功能
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
package types
|
|
||||||
|
|
||||||
type FunctionCall struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Arguments string `json:"arguments"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Function struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Parameters Parameters `json:"parameters"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Parameters struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Required []string `json:"required"`
|
|
||||||
Properties map[string]Property `json:"properties"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Property struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
FuncZaoBao = "zao_bao" // 每日早报
|
|
||||||
FuncHeadLine = "headline" // 今日头条
|
|
||||||
FuncWeibo = "weibo_hot" // 微博热搜
|
|
||||||
FuncMidJourney = "mid_journey" // MJ 绘画
|
|
||||||
)
|
|
||||||
|
|
||||||
var InnerFunctions = []Function{
|
|
||||||
{
|
|
||||||
Name: FuncZaoBao,
|
|
||||||
Description: "每日早报,获取当天全球的热门新闻事件列表",
|
|
||||||
Parameters: Parameters{
|
|
||||||
|
|
||||||
Type: "object",
|
|
||||||
Properties: map[string]Property{
|
|
||||||
"text": {
|
|
||||||
Type: "string",
|
|
||||||
Description: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Required: []string{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: FuncWeibo,
|
|
||||||
Description: "新浪微博热搜榜,微博当日热搜榜单",
|
|
||||||
Parameters: Parameters{
|
|
||||||
Type: "object",
|
|
||||||
Properties: map[string]Property{
|
|
||||||
"text": {
|
|
||||||
Type: "string",
|
|
||||||
Description: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Required: []string{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
Name: FuncHeadLine,
|
|
||||||
Description: "今日头条,给用户推荐当天的头条新闻,周榜热文",
|
|
||||||
Parameters: Parameters{
|
|
||||||
Type: "object",
|
|
||||||
Properties: map[string]Property{
|
|
||||||
"text": {
|
|
||||||
Type: "string",
|
|
||||||
Description: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Required: []string{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
Name: FuncMidJourney,
|
|
||||||
Description: "AI 绘画工具,使用 MJ MidJourney API 进行 AI 绘画",
|
|
||||||
Parameters: Parameters{
|
|
||||||
Type: "object",
|
|
||||||
Properties: map[string]Property{
|
|
||||||
"prompt": {
|
|
||||||
Type: "string",
|
|
||||||
Description: "提示词,如果该参数中有中文的话,则需要翻译成英文。提示词中的参数作为提示的一部分,不要删除",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Required: []string{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
package types
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MKey interface {
|
|
||||||
string | int
|
|
||||||
}
|
|
||||||
type MValue interface {
|
|
||||||
*WsClient | *ChatSession | context.CancelFunc | []interface{}
|
|
||||||
}
|
|
||||||
type LMap[K MKey, T MValue] struct {
|
|
||||||
lock sync.RWMutex
|
|
||||||
data map[K]T
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLMap[K MKey, T MValue]() *LMap[K, T] {
|
|
||||||
return &LMap[K, T]{
|
|
||||||
lock: sync.RWMutex{},
|
|
||||||
data: make(map[K]T),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *LMap[K, T]) Put(key K, value T) {
|
|
||||||
m.lock.Lock()
|
|
||||||
defer m.lock.Unlock()
|
|
||||||
|
|
||||||
m.data[key] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *LMap[K, T]) Get(key K) T {
|
|
||||||
m.lock.RLock()
|
|
||||||
defer m.lock.RUnlock()
|
|
||||||
|
|
||||||
return m.data[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *LMap[K, T]) Has(key K) bool {
|
|
||||||
m.lock.RLock()
|
|
||||||
defer m.lock.RUnlock()
|
|
||||||
_, ok := m.data[key]
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *LMap[K, T]) Delete(key K) {
|
|
||||||
m.lock.Lock()
|
|
||||||
defer m.lock.Unlock()
|
|
||||||
|
|
||||||
delete(m.data, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *LMap[K, T]) ToList() []T {
|
|
||||||
m.lock.Lock()
|
|
||||||
defer m.lock.Unlock()
|
|
||||||
|
|
||||||
var s = make([]T, 0)
|
|
||||||
for _, v := range m.data {
|
|
||||||
s = append(s, v)
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package types
|
|
||||||
|
|
||||||
type OSSConfig struct {
|
|
||||||
Active string
|
|
||||||
Local LocalStorageConfig
|
|
||||||
Minio MiniOssConfig
|
|
||||||
QiNiu QiNiuOssConfig
|
|
||||||
AliYun AliYunOssConfig
|
|
||||||
}
|
|
||||||
type MiniOssConfig struct {
|
|
||||||
Endpoint string
|
|
||||||
AccessKey string
|
|
||||||
AccessSecret string
|
|
||||||
Bucket string
|
|
||||||
UseSSL bool
|
|
||||||
Domain string
|
|
||||||
}
|
|
||||||
|
|
||||||
type QiNiuOssConfig struct {
|
|
||||||
Zone string
|
|
||||||
AccessKey string
|
|
||||||
AccessSecret string
|
|
||||||
Bucket string
|
|
||||||
Domain string
|
|
||||||
}
|
|
||||||
|
|
||||||
type AliYunOssConfig struct {
|
|
||||||
Endpoint string
|
|
||||||
AccessKey string
|
|
||||||
AccessSecret string
|
|
||||||
Bucket string
|
|
||||||
Domain string
|
|
||||||
}
|
|
||||||
|
|
||||||
type LocalStorageConfig struct {
|
|
||||||
BasePath string
|
|
||||||
BaseURL string
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package types
|
|
||||||
|
|
||||||
const LoginUserID = "LOGIN_USER_ID"
|
|
||||||
const LoginUserCache = "LOGIN_USER_CACHE"
|
|
||||||
|
|
||||||
const UserAuthHeader = "Authorization"
|
|
||||||
const AdminAuthHeader = "Admin-Authorization"
|
|
||||||
const ChatTokenHeader = "Chat-Token"
|
|
||||||
|
|
||||||
// Session configs struct
|
|
||||||
type Session struct {
|
|
||||||
SecretKey string
|
|
||||||
MaxAge int
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
package types
|
|
||||||
|
|
||||||
// TaskType 任务类别
|
|
||||||
type TaskType string
|
|
||||||
|
|
||||||
func (t TaskType) String() string {
|
|
||||||
return string(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
TaskImage = TaskType("image")
|
|
||||||
TaskUpscale = TaskType("upscale")
|
|
||||||
TaskVariation = TaskType("variation")
|
|
||||||
TaskTxt2Img = TaskType("text2img")
|
|
||||||
)
|
|
||||||
|
|
||||||
// TaskSrc 任务来源
|
|
||||||
type TaskSrc string
|
|
||||||
|
|
||||||
const (
|
|
||||||
TaskSrcChat = TaskSrc("chat") // 来自聊天页面
|
|
||||||
TaskSrcImg = TaskSrc("img") // 专业绘画页面
|
|
||||||
)
|
|
||||||
|
|
||||||
// MjTask MidJourney 任务
|
|
||||||
type MjTask struct {
|
|
||||||
Id int `json:"id"`
|
|
||||||
SessionId string `json:"session_id"`
|
|
||||||
Src TaskSrc `json:"src"`
|
|
||||||
Type TaskType `json:"type"`
|
|
||||||
UserId int `json:"user_id"`
|
|
||||||
Prompt string `json:"prompt,omitempty"`
|
|
||||||
ChatId string `json:"chat_id,omitempty"`
|
|
||||||
RoleId int `json:"role_id,omitempty"`
|
|
||||||
Icon string `json:"icon,omitempty"`
|
|
||||||
Index int `json:"index,omitempty"`
|
|
||||||
MessageId string `json:"message_id,omitempty"`
|
|
||||||
MessageHash string `json:"message_hash,omitempty"`
|
|
||||||
RetryCount int `json:"retry_count"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SdTask struct {
|
|
||||||
Id int `json:"id"` // job 数据库ID
|
|
||||||
SessionId string `json:"session_id"`
|
|
||||||
Src TaskSrc `json:"src"`
|
|
||||||
Type TaskType `json:"type"`
|
|
||||||
UserId int `json:"user_id"`
|
|
||||||
Prompt string `json:"prompt,omitempty"`
|
|
||||||
Params SdTaskParams `json:"params"`
|
|
||||||
RetryCount int `json:"retry_count"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SdTaskParams struct {
|
|
||||||
TaskId string `json:"task_id"`
|
|
||||||
Prompt string `json:"prompt"` // 提示词
|
|
||||||
NegativePrompt string `json:"negative_prompt"` // 反向提示词
|
|
||||||
Steps int `json:"steps"` // 迭代步数,默认20
|
|
||||||
Sampler string `json:"sampler"` // 采样器
|
|
||||||
FaceFix bool `json:"face_fix"` // 面部修复
|
|
||||||
CfgScale float32 `json:"cfg_scale"` //引导系数,默认 7
|
|
||||||
Seed int64 `json:"seed"` // 随机数种子
|
|
||||||
Height int `json:"height"`
|
|
||||||
Width int `json:"width"`
|
|
||||||
HdFix bool `json:"hd_fix"` // 启用高清修复
|
|
||||||
HdRedrawRate float32 `json:"hd_redraw_rate"` // 高清修复重绘幅度
|
|
||||||
HdScale int `json:"hd_scale"` // 放大倍数
|
|
||||||
HdScaleAlg string `json:"hd_scale_alg"` // 放大算法
|
|
||||||
HdSteps int `json:"hd_steps"` // 高清修复迭代步数
|
|
||||||
}
|
|
||||||
95
api/go.mod
95
api/go.mod
@@ -1,95 +0,0 @@
|
|||||||
module chatplus
|
|
||||||
|
|
||||||
go 1.19
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/BurntSushi/toml v1.1.0
|
|
||||||
github.com/aliyun/alibaba-cloud-sdk-go v1.62.405
|
|
||||||
github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible
|
|
||||||
github.com/bwmarrin/discordgo v0.27.1
|
|
||||||
github.com/eatmoreapple/openwechat v1.2.1
|
|
||||||
github.com/gin-gonic/gin v1.9.1
|
|
||||||
github.com/go-redis/redis/v8 v8.11.5
|
|
||||||
github.com/golang-jwt/jwt/v5 v5.0.0
|
|
||||||
github.com/gorilla/websocket v1.5.0
|
|
||||||
github.com/imroc/req/v3 v3.37.2
|
|
||||||
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0
|
|
||||||
github.com/minio/minio-go/v7 v7.0.62
|
|
||||||
github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480
|
|
||||||
github.com/qiniu/go-sdk/v7 v7.17.1
|
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
|
||||||
github.com/syndtr/goleveldb v1.0.0
|
|
||||||
go.uber.org/zap v1.23.0
|
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
|
||||||
gorm.io/driver/mysql v1.4.7
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
|
||||||
github.com/bytedance/sonic v1.9.1 // indirect
|
|
||||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
|
||||||
github.com/dlclark/regexp2 v1.8.1 // indirect
|
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
|
||||||
github.com/gaukas/godicttls v0.0.3 // indirect
|
|
||||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
|
||||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
|
||||||
github.com/golang/mock v1.6.0 // indirect
|
|
||||||
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 // indirect
|
|
||||||
github.com/google/uuid v1.3.0 // indirect
|
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
|
||||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af // indirect
|
|
||||||
github.com/klauspost/compress v1.16.7 // indirect
|
|
||||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
|
||||||
github.com/minio/md5-simd v1.1.2 // indirect
|
|
||||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
|
||||||
github.com/onsi/ginkgo/v2 v2.10.0 // indirect
|
|
||||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
|
||||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
|
||||||
github.com/quic-go/qpack v0.4.0 // indirect
|
|
||||||
github.com/quic-go/qtls-go1-19 v0.3.2 // indirect
|
|
||||||
github.com/quic-go/qtls-go1-20 v0.2.2 // indirect
|
|
||||||
github.com/quic-go/quic-go v0.35.1 // indirect
|
|
||||||
github.com/refraction-networking/utls v1.3.2 // indirect
|
|
||||||
github.com/rs/xid v1.5.0 // indirect
|
|
||||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
|
||||||
go.uber.org/dig v1.16.1 // indirect
|
|
||||||
golang.org/x/arch v0.3.0 // indirect
|
|
||||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
|
|
||||||
golang.org/x/mod v0.11.0 // indirect
|
|
||||||
golang.org/x/net v0.14.0 // indirect
|
|
||||||
golang.org/x/sync v0.3.0 // indirect
|
|
||||||
golang.org/x/text v0.12.0 // indirect
|
|
||||||
golang.org/x/time v0.3.0 // indirect
|
|
||||||
golang.org/x/tools v0.10.0 // indirect
|
|
||||||
google.golang.org/protobuf v1.30.0 // indirect
|
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
|
||||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
|
||||||
github.com/golang/snappy v0.0.1 // indirect
|
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
|
||||||
github.com/leodido/go-urn v1.2.4 // indirect
|
|
||||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
|
||||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
|
||||||
go.uber.org/atomic v1.9.0 // indirect
|
|
||||||
go.uber.org/fx v1.19.3
|
|
||||||
go.uber.org/multierr v1.6.0 // indirect
|
|
||||||
golang.org/x/crypto v0.12.0
|
|
||||||
golang.org/x/sys v0.11.0 // indirect
|
|
||||||
gorm.io/gorm v1.25.1
|
|
||||||
)
|
|
||||||
311
api/go.sum
311
api/go.sum
@@ -1,311 +0,0 @@
|
|||||||
github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
|
|
||||||
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
|
||||||
github.com/aliyun/alibaba-cloud-sdk-go v1.62.405 h1:cKNFQmeCQFN0WNfjScKoVrGi7vXxTVbkCvCqSrOf+P4=
|
|
||||||
github.com/aliyun/alibaba-cloud-sdk-go v1.62.405/go.mod h1:Api2AkmMgGaSUAhmk76oaFObkoeCPc/bKAqcyplPODs=
|
|
||||||
github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible h1:Sg/2xHwDrioHpxTN6WMiwbXTpUEinBpHsN7mG21Rc2k=
|
|
||||||
github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
|
|
||||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
|
||||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
|
||||||
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
|
|
||||||
github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY=
|
|
||||||
github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
|
||||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
|
||||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
|
||||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
|
||||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
|
||||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
|
||||||
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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
|
||||||
github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0=
|
|
||||||
github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
|
||||||
github.com/eatmoreapple/openwechat v1.2.1 h1:ez4oqF/Y2NSEX/DbPV8lvj7JlfkYqvieeo4awx5lzfU=
|
|
||||||
github.com/eatmoreapple/openwechat v1.2.1/go.mod h1:61HOzTyvLobGdgWhL68jfGNwTJEv0mhQ1miCXQrvWU8=
|
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
|
||||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
|
||||||
github.com/gaukas/godicttls v0.0.3 h1:YNDIf0d9adcxOijiLrEzpfZGAkNwLRzPaG6OjU7EITk=
|
|
||||||
github.com/gaukas/godicttls v0.0.3/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
|
|
||||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
|
||||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
|
||||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
|
||||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
|
||||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
|
||||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
|
||||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
|
||||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
|
||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
|
||||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
|
||||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
|
||||||
github.com/go-playground/validator/v10 v10.8.0/go.mod h1:9JhgTzTaE31GZDpH/HSvHiRJrJ3iKAgqqH0Bl/Ocjdk=
|
|
||||||
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
|
||||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
|
||||||
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
|
||||||
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
|
||||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
|
||||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
|
||||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
|
||||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
|
||||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
|
||||||
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
|
|
||||||
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
|
|
||||||
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
|
||||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
|
||||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
|
||||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
|
||||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
|
||||||
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
|
||||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
|
||||||
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 h1:hR7/MlvK23p6+lIw9SN1TigNLn9ZnF3W4SYRKq2gAHs=
|
|
||||||
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA=
|
|
||||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
|
||||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
|
||||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
|
||||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
|
||||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
|
||||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
|
||||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
|
||||||
github.com/imroc/req/v3 v3.37.2 h1:vEemuA0cq9zJ6lhe+mSRhsZm951bT0CdiSH47+KTn6I=
|
|
||||||
github.com/imroc/req/v3 v3.37.2/go.mod h1:DECzjVIrj6jcUr5n6e+z0ygmCO93rx4Jy0RjOEe1YCI=
|
|
||||||
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/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
|
|
||||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
|
||||||
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
|
||||||
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
|
|
||||||
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
|
||||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
|
||||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
|
||||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
|
||||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
|
||||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
|
||||||
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0 h1:LgmjED/yQILqmUED4GaXjrINWe7YJh4HM6z2EvEINPs=
|
|
||||||
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0/go.mod h1:C5LA5UO2ZXJrLaPLYtE1wUJMiyd/nwWaCO5cw/2pSHs=
|
|
||||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
|
||||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
|
||||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
|
||||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
|
||||||
github.com/minio/minio-go/v7 v7.0.62 h1:qNYsFZHEzl+NfH8UxW4jpmlKav1qUAgfY30YNRneVhc=
|
|
||||||
github.com/minio/minio-go/v7 v7.0.62/go.mod h1:Q6X7Qjb7WMhvG65qKf4gUgA5XaiSox74kR1uAEjxRS4=
|
|
||||||
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
|
||||||
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
|
||||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
|
||||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
|
||||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
|
||||||
github.com/onsi/ginkgo/v2 v2.10.0 h1:sfUl4qgLdvkChZrWCYndY2EAu9BRIw1YphNAzy1VNWs=
|
|
||||||
github.com/onsi/ginkgo/v2 v2.10.0/go.mod h1:UDQOh5wbQUlMnkLfVaIUMtQ1Vus92oM+P2JX1aulgcE=
|
|
||||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
|
||||||
github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU=
|
|
||||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A=
|
|
||||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
|
|
||||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
|
||||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
|
||||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
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/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdkk=
|
|
||||||
github.com/qiniu/go-sdk/v7 v7.17.1 h1:UoQv7fBKtzAiD1qZPIvTy62Se48YLKxcCYP9nAwWMa0=
|
|
||||||
github.com/qiniu/go-sdk/v7 v7.17.1/go.mod h1:nqoYCNo53ZlGA521RvRethvxUDvXKt4gtYXOwye868w=
|
|
||||||
github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs=
|
|
||||||
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
|
||||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
|
||||||
github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U=
|
|
||||||
github.com/quic-go/qtls-go1-19 v0.3.2/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI=
|
|
||||||
github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8GEa3E=
|
|
||||||
github.com/quic-go/qtls-go1-20 v0.2.2/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM=
|
|
||||||
github.com/quic-go/quic-go v0.35.1 h1:b0kzj6b/cQAf05cT0CkQubHM31wiA+xH3IBkxP62poo=
|
|
||||||
github.com/quic-go/quic-go v0.35.1/go.mod h1:+4CVgVppm0FNjpG3UcX8Joi/frKOH7/ciD5yGcwOO1g=
|
|
||||||
github.com/refraction-networking/utls v1.3.2 h1:o+AkWB57mkcoW36ET7uJ002CpBWHu0KPxi6vzxvPnv8=
|
|
||||||
github.com/refraction-networking/utls v1.3.2/go.mod h1:fmoaOww2bxzzEpIKOebIsnBvjQpqP7L2vcm/9KUfm/E=
|
|
||||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
|
||||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
|
||||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
|
||||||
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
|
|
||||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
|
||||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
|
||||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
|
||||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
|
||||||
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
|
||||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
|
||||||
github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=
|
|
||||||
github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
|
|
||||||
github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg=
|
|
||||||
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
|
|
||||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
|
||||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
|
||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
|
||||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
|
||||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
|
||||||
go.uber.org/atomic v1.9.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/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
|
|
||||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
|
||||||
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/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
|
||||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
|
||||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
|
||||||
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-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
|
||||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
|
||||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
|
||||||
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
|
|
||||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
|
||||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
|
|
||||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
|
||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
|
||||||
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
|
|
||||||
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
|
||||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
|
||||||
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
|
|
||||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
|
||||||
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/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
|
||||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
|
||||||
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-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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
|
||||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
|
||||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
|
||||||
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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|
||||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
|
||||||
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
|
|
||||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|
||||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
|
||||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
|
||||||
golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=
|
|
||||||
golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
|
|
||||||
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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
|
||||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
|
||||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
|
||||||
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
|
||||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
|
||||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
|
||||||
gopkg.in/yaml.v2 v2.2.1/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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
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=
|
|
||||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
package admin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core"
|
|
||||||
"chatplus/core/types"
|
|
||||||
"chatplus/handler"
|
|
||||||
logger2 "chatplus/logger"
|
|
||||||
"chatplus/store/model"
|
|
||||||
"chatplus/utils"
|
|
||||||
"chatplus/utils/resp"
|
|
||||||
"context"
|
|
||||||
"github.com/go-redis/redis/v8"
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
var logger = logger2.GetLogger()
|
|
||||||
|
|
||||||
type ManagerHandler struct {
|
|
||||||
handler.BaseHandler
|
|
||||||
db *gorm.DB
|
|
||||||
redis *redis.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAdminHandler(app *core.AppServer, db *gorm.DB, client *redis.Client) *ManagerHandler {
|
|
||||||
h := ManagerHandler{db: db, redis: client}
|
|
||||||
h.App = app
|
|
||||||
return &h
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login 登录
|
|
||||||
func (h *ManagerHandler) Login(c *gin.Context) {
|
|
||||||
var data types.Manager
|
|
||||||
if err := c.ShouldBindJSON(&data); err != nil {
|
|
||||||
resp.ERROR(c, types.InvalidArgs)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
manager := h.App.Config.Manager
|
|
||||||
if data.Username == manager.Username && data.Password == manager.Password {
|
|
||||||
// 创建 token
|
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
|
||||||
"user_id": manager.Username,
|
|
||||||
"expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)).Unix(),
|
|
||||||
})
|
|
||||||
tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
|
|
||||||
if err != nil {
|
|
||||||
resp.ERROR(c, "Failed to generate token, "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 保存到 redis
|
|
||||||
key := "users/" + manager.Username
|
|
||||||
if _, err := h.redis.Set(context.Background(), key, tokenString, 0).Result(); err != nil {
|
|
||||||
resp.ERROR(c, "error with save token: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp.SUCCESS(c, tokenString)
|
|
||||||
} else {
|
|
||||||
resp.ERROR(c, "用户名或者密码错误")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logout 注销
|
|
||||||
func (h *ManagerHandler) Logout(c *gin.Context) {
|
|
||||||
key := h.GetUserKey(c)
|
|
||||||
if _, err := h.redis.Del(c, key).Result(); err != nil {
|
|
||||||
logger.Error("error with delete session: ", err)
|
|
||||||
} else {
|
|
||||||
resp.SUCCESS(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Session 会话检测
|
|
||||||
func (h *ManagerHandler) Session(c *gin.Context) {
|
|
||||||
token := c.GetHeader(types.AdminAuthHeader)
|
|
||||||
if token == "" {
|
|
||||||
resp.NotAuth(c)
|
|
||||||
} else {
|
|
||||||
resp.SUCCESS(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migrate 数据修正
|
|
||||||
func (h *ManagerHandler) Migrate(c *gin.Context) {
|
|
||||||
opt := c.Query("opt")
|
|
||||||
switch opt {
|
|
||||||
case "user":
|
|
||||||
// 将用户订阅角色的数据结构从 map 改成数组
|
|
||||||
var users []model.User
|
|
||||||
h.db.Find(&users)
|
|
||||||
for _, u := range users {
|
|
||||||
var m map[string]int
|
|
||||||
var roleKeys = make([]string, 0)
|
|
||||||
err := utils.JsonDecode(u.ChatRoles, &m)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for k := range m {
|
|
||||||
roleKeys = append(roleKeys, k)
|
|
||||||
}
|
|
||||||
u.ChatRoles = utils.JsonEncode(roleKeys)
|
|
||||||
h.db.Updates(&u)
|
|
||||||
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case "role":
|
|
||||||
// 修改角色图片,改成绝对路径
|
|
||||||
var roles []model.ChatRole
|
|
||||||
h.db.Find(&roles)
|
|
||||||
for _, r := range roles {
|
|
||||||
if !strings.HasPrefix(r.Icon, "/") {
|
|
||||||
r.Icon = "/" + r.Icon
|
|
||||||
h.db.Updates(&r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case "history":
|
|
||||||
// 修改角色图片,改成绝对路径
|
|
||||||
var message []model.HistoryMessage
|
|
||||||
h.db.Find(&message)
|
|
||||||
for _, r := range message {
|
|
||||||
if !strings.HasPrefix(r.Icon, "/") {
|
|
||||||
r.Icon = "/" + r.Icon
|
|
||||||
h.db.Updates(&r)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
break
|
|
||||||
|
|
||||||
case "avatar":
|
|
||||||
// 更新用户的头像地址
|
|
||||||
var users []model.User
|
|
||||||
h.db.Find(&users)
|
|
||||||
for _, u := range users {
|
|
||||||
if !strings.HasPrefix(u.Avatar, "/") {
|
|
||||||
u.Avatar = "/" + u.Avatar
|
|
||||||
h.db.Updates(&u)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.SUCCESS(c, "SUCCESS")
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
package admin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core"
|
|
||||||
"chatplus/core/types"
|
|
||||||
"chatplus/handler"
|
|
||||||
"chatplus/store/model"
|
|
||||||
"chatplus/store/vo"
|
|
||||||
"chatplus/utils"
|
|
||||||
"chatplus/utils/resp"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ApiKeyHandler struct {
|
|
||||||
handler.BaseHandler
|
|
||||||
db *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewApiKeyHandler(app *core.AppServer, db *gorm.DB) *ApiKeyHandler {
|
|
||||||
h := ApiKeyHandler{db: db}
|
|
||||||
h.App = app
|
|
||||||
return &h
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ApiKeyHandler) Save(c *gin.Context) {
|
|
||||||
var data struct {
|
|
||||||
Id uint `json:"id"`
|
|
||||||
Platform string `json:"platform"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
}
|
|
||||||
if err := c.ShouldBindJSON(&data); err != nil {
|
|
||||||
resp.ERROR(c, types.InvalidArgs)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
apiKey := model.ApiKey{}
|
|
||||||
if data.Id > 0 {
|
|
||||||
h.db.Find(&apiKey, data.Id)
|
|
||||||
}
|
|
||||||
apiKey.Platform = data.Platform
|
|
||||||
apiKey.Value = data.Value
|
|
||||||
res := h.db.Debug().Save(&apiKey)
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c, "更新数据库失败!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var keyVo vo.ApiKey
|
|
||||||
err := utils.CopyObject(apiKey, &keyVo)
|
|
||||||
if err != nil {
|
|
||||||
resp.ERROR(c, "数据拷贝失败!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
keyVo.Id = apiKey.Id
|
|
||||||
keyVo.CreatedAt = apiKey.CreatedAt.Unix()
|
|
||||||
resp.SUCCESS(c, keyVo)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ApiKeyHandler) List(c *gin.Context) {
|
|
||||||
var items []model.ApiKey
|
|
||||||
var keys = make([]vo.ApiKey, 0)
|
|
||||||
res := h.db.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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resp.SUCCESS(c, keys)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ApiKeyHandler) Remove(c *gin.Context) {
|
|
||||||
id := h.GetInt(c, "id", 0)
|
|
||||||
|
|
||||||
if id > 0 {
|
|
||||||
res := h.db.Where("id = ?", id).Delete(&model.ApiKey{})
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c, "更新数据库失败!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resp.SUCCESS(c)
|
|
||||||
}
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
package admin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core"
|
|
||||||
"chatplus/core/types"
|
|
||||||
"chatplus/handler"
|
|
||||||
"chatplus/store/model"
|
|
||||||
"chatplus/store/vo"
|
|
||||||
"chatplus/utils"
|
|
||||||
"chatplus/utils/resp"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ChatModelHandler struct {
|
|
||||||
handler.BaseHandler
|
|
||||||
db *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewChatModelHandler(app *core.AppServer, db *gorm.DB) *ChatModelHandler {
|
|
||||||
h := ChatModelHandler{db: db}
|
|
||||||
h.App = app
|
|
||||||
return &h
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ChatModelHandler) Save(c *gin.Context) {
|
|
||||||
var data struct {
|
|
||||||
Id uint `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
SortNum int `json:"sort_num"`
|
|
||||||
Platform string `json:"platform"`
|
|
||||||
CreatedAt int64 `json:"created_at"`
|
|
||||||
}
|
|
||||||
if err := c.ShouldBindJSON(&data); err != nil {
|
|
||||||
resp.ERROR(c, types.InvalidArgs)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
item := model.ChatModel{Platform: data.Platform, Name: data.Name, Value: data.Value, Enabled: data.Enabled}
|
|
||||||
item.Id = data.Id
|
|
||||||
if item.Id > 0 {
|
|
||||||
item.CreatedAt = time.Unix(data.CreatedAt, 0)
|
|
||||||
}
|
|
||||||
res := h.db.Save(&item)
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c, "更新数据库失败!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var itemVo vo.ChatModel
|
|
||||||
err := utils.CopyObject(item, &itemVo)
|
|
||||||
if err != nil {
|
|
||||||
resp.ERROR(c, "数据拷贝失败!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
itemVo.Id = item.Id
|
|
||||||
itemVo.CreatedAt = item.CreatedAt.Unix()
|
|
||||||
resp.SUCCESS(c, itemVo)
|
|
||||||
}
|
|
||||||
|
|
||||||
// List 模型列表
|
|
||||||
func (h *ChatModelHandler) List(c *gin.Context) {
|
|
||||||
session := h.db.Session(&gorm.Session{})
|
|
||||||
enable := h.GetBool(c, "enable")
|
|
||||||
if enable {
|
|
||||||
session = session.Where("enabled", enable)
|
|
||||||
}
|
|
||||||
var items []model.ChatModel
|
|
||||||
var cms = make([]vo.ChatModel, 0)
|
|
||||||
res := session.Order("sort_num ASC").Find(&items)
|
|
||||||
if res.Error == nil {
|
|
||||||
for _, item := range items {
|
|
||||||
var cm vo.ChatModel
|
|
||||||
err := utils.CopyObject(item, &cm)
|
|
||||||
if err == nil {
|
|
||||||
cm.Id = item.Id
|
|
||||||
cm.CreatedAt = item.CreatedAt.Unix()
|
|
||||||
cm.UpdatedAt = item.UpdatedAt.Unix()
|
|
||||||
cms = append(cms, cm)
|
|
||||||
} else {
|
|
||||||
logger.Error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resp.SUCCESS(c, cms)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ChatModelHandler) Enable(c *gin.Context) {
|
|
||||||
var data struct {
|
|
||||||
Id uint `json:"id"`
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&data); err != nil {
|
|
||||||
resp.ERROR(c, types.InvalidArgs)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
res := h.db.Model(&model.ChatModel{}).Where("id = ?", data.Id).Update("enabled", data.Enabled)
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c, "更新数据库失败!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp.SUCCESS(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ChatModelHandler) Sort(c *gin.Context) {
|
|
||||||
var data struct {
|
|
||||||
Ids []uint `json:"ids"`
|
|
||||||
Sorts []int `json:"sorts"`
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&data); err != nil {
|
|
||||||
resp.ERROR(c, types.InvalidArgs)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for index, id := range data.Ids {
|
|
||||||
res := h.db.Model(&model.ChatModel{}).Where("id = ?", id).Update("sort_num", data.Sorts[index])
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c, "更新数据库失败!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.SUCCESS(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ChatModelHandler) Remove(c *gin.Context) {
|
|
||||||
id := h.GetInt(c, "id", 0)
|
|
||||||
|
|
||||||
if id > 0 {
|
|
||||||
res := h.db.Where("id = ?", id).Delete(&model.ChatModel{})
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c, "更新数据库失败!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resp.SUCCESS(c)
|
|
||||||
}
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
package admin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core"
|
|
||||||
"chatplus/core/types"
|
|
||||||
"chatplus/handler"
|
|
||||||
"chatplus/store/model"
|
|
||||||
"chatplus/store/vo"
|
|
||||||
"chatplus/utils"
|
|
||||||
"chatplus/utils/resp"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ChatRoleHandler struct {
|
|
||||||
handler.BaseHandler
|
|
||||||
db *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewChatRoleHandler(app *core.AppServer, db *gorm.DB) *ChatRoleHandler {
|
|
||||||
h := ChatRoleHandler{db: db}
|
|
||||||
h.App = app
|
|
||||||
return &h
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save 创建或者更新某个角色
|
|
||||||
func (h *ChatRoleHandler) Save(c *gin.Context) {
|
|
||||||
var data vo.ChatRole
|
|
||||||
if err := c.ShouldBindJSON(&data); err != nil {
|
|
||||||
resp.ERROR(c, types.InvalidArgs)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var role model.ChatRole
|
|
||||||
err := utils.CopyObject(data, &role)
|
|
||||||
if err != nil {
|
|
||||||
resp.ERROR(c, types.InvalidArgs)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
role.Id = data.Id
|
|
||||||
if data.CreatedAt > 0 {
|
|
||||||
role.CreatedAt = time.Unix(data.CreatedAt, 0)
|
|
||||||
}
|
|
||||||
res := h.db.Save(&role)
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c, "更新数据库失败!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 填充 ID 数据
|
|
||||||
data.Id = role.Id
|
|
||||||
data.CreatedAt = role.CreatedAt.Unix()
|
|
||||||
resp.SUCCESS(c, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ChatRoleHandler) List(c *gin.Context) {
|
|
||||||
var items []model.ChatRole
|
|
||||||
var roles = make([]vo.ChatRole, 0)
|
|
||||||
res := h.db.Order("sort_num ASC").Find(&items)
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c, "No data found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, v := range items {
|
|
||||||
var role vo.ChatRole
|
|
||||||
err := utils.CopyObject(v, &role)
|
|
||||||
if err == nil {
|
|
||||||
role.Id = v.Id
|
|
||||||
role.CreatedAt = v.CreatedAt.Unix()
|
|
||||||
role.UpdatedAt = v.UpdatedAt.Unix()
|
|
||||||
roles = append(roles, role)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.SUCCESS(c, roles)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort 更新角色排序
|
|
||||||
func (h *ChatRoleHandler) Sort(c *gin.Context) {
|
|
||||||
var data struct {
|
|
||||||
Ids []uint `json:"ids"`
|
|
||||||
Sorts []int `json:"sorts"`
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&data); err != nil {
|
|
||||||
resp.ERROR(c, types.InvalidArgs)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for index, id := range data.Ids {
|
|
||||||
res := h.db.Model(&model.ChatRole{}).Where("id = ?", id).Update("sort_num", data.Sorts[index])
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c, "更新数据库失败!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.SUCCESS(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ChatRoleHandler) Remove(c *gin.Context) {
|
|
||||||
id := h.GetInt(c, "id", 0)
|
|
||||||
if id <= 0 {
|
|
||||||
resp.ERROR(c, types.InvalidArgs)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
res := h.db.Where("id = ?", id).Delete(&model.ChatRole{})
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c, "删除失败!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp.SUCCESS(c)
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
package admin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core"
|
|
||||||
"chatplus/core/types"
|
|
||||||
"chatplus/handler"
|
|
||||||
"chatplus/store/model"
|
|
||||||
"chatplus/utils"
|
|
||||||
"chatplus/utils/resp"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ConfigHandler struct {
|
|
||||||
handler.BaseHandler
|
|
||||||
db *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewConfigHandler(app *core.AppServer, db *gorm.DB) *ConfigHandler {
|
|
||||||
h := ConfigHandler{db: db}
|
|
||||||
h.App = app
|
|
||||||
return &h
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// update config cache for AppServer
|
|
||||||
var cfg model.Config
|
|
||||||
h.db.Where("marker", data.Key).First(&cfg)
|
|
||||||
var err error
|
|
||||||
if data.Key == "system" {
|
|
||||||
err = utils.JsonDecode(cfg.Config, &h.App.SysConfig)
|
|
||||||
} else if data.Key == "chat" {
|
|
||||||
err = utils.JsonDecode(cfg.Config, &h.App.ChatConfig)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
resp.ERROR(c, "Failed to update config cache: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
logger.Infof("Update AppServer's config successfully: %v", config.Config)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.SUCCESS(c, config)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get 获取指定的系统配置
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
package admin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core"
|
|
||||||
"chatplus/handler"
|
|
||||||
"chatplus/store/model"
|
|
||||||
"chatplus/utils/resp"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DashboardHandler struct {
|
|
||||||
handler.BaseHandler
|
|
||||||
db *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDashboardHandler(app *core.AppServer, db *gorm.DB) *DashboardHandler {
|
|
||||||
h := DashboardHandler{db: db}
|
|
||||||
h.App = app
|
|
||||||
return &h
|
|
||||||
}
|
|
||||||
|
|
||||||
type statsVo struct {
|
|
||||||
Users int64 `json:"users"`
|
|
||||||
Chats int64 `json:"chats"`
|
|
||||||
Tokens int64 `json:"tokens"`
|
|
||||||
Rewards float64 `json:"rewards"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *DashboardHandler) Stats(c *gin.Context) {
|
|
||||||
stats := statsVo{}
|
|
||||||
// new users statistic
|
|
||||||
var userCount int64
|
|
||||||
now := time.Now()
|
|
||||||
zeroTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
|
||||||
res := h.db.Model(&model.User{}).Where("created_at > ?", zeroTime).Count(&userCount)
|
|
||||||
if res.Error == nil {
|
|
||||||
stats.Users = userCount
|
|
||||||
}
|
|
||||||
|
|
||||||
// new chats statistic
|
|
||||||
var chatCount int64
|
|
||||||
res = h.db.Model(&model.ChatItem{}).Where("created_at > ?", zeroTime).Count(&chatCount)
|
|
||||||
if res.Error == nil {
|
|
||||||
stats.Chats = chatCount
|
|
||||||
}
|
|
||||||
|
|
||||||
// tokens took stats
|
|
||||||
var tokenCount int64
|
|
||||||
res = h.db.Model(&model.HistoryMessage{}).Select("sum(tokens) as total").Where("created_at > ?", zeroTime).Scan(&tokenCount)
|
|
||||||
if res.Error == nil {
|
|
||||||
stats.Tokens = tokenCount
|
|
||||||
}
|
|
||||||
|
|
||||||
// reward revenue
|
|
||||||
var amount float64
|
|
||||||
res = h.db.Model(&model.Reward{}).Select("sum(amount) as total").Where("created_at > ?", zeroTime).Scan(&amount)
|
|
||||||
if res.Error == nil {
|
|
||||||
stats.Rewards = amount
|
|
||||||
}
|
|
||||||
resp.SUCCESS(c, stats)
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
package admin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core"
|
|
||||||
"chatplus/handler"
|
|
||||||
"chatplus/store/model"
|
|
||||||
"chatplus/store/vo"
|
|
||||||
"chatplus/utils"
|
|
||||||
"chatplus/utils/resp"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RewardHandler struct {
|
|
||||||
handler.BaseHandler
|
|
||||||
db *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRewardHandler(app *core.AppServer, db *gorm.DB) *RewardHandler {
|
|
||||||
h := RewardHandler{db: db}
|
|
||||||
h.App = app
|
|
||||||
return &h
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *RewardHandler) List(c *gin.Context) {
|
|
||||||
var items []model.Reward
|
|
||||||
res := h.db.Order("id DESC").Find(&items)
|
|
||||||
var rewards = make([]vo.Reward, 0)
|
|
||||||
if res.Error == nil {
|
|
||||||
userIds := make([]uint, 0)
|
|
||||||
for _, v := range items {
|
|
||||||
userIds = append(userIds, v.UserId)
|
|
||||||
}
|
|
||||||
var users []model.User
|
|
||||||
h.db.Where("id IN ?", userIds).Find(&users)
|
|
||||||
var userMap = make(map[uint]model.User)
|
|
||||||
for _, u := range users {
|
|
||||||
userMap[u.Id] = u
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, v := range items {
|
|
||||||
var r vo.Reward
|
|
||||||
err := utils.CopyObject(v, &r)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
r.Id = v.Id
|
|
||||||
r.Username = userMap[v.UserId].Mobile
|
|
||||||
r.CreatedAt = v.CreatedAt.Unix()
|
|
||||||
r.UpdatedAt = v.UpdatedAt.Unix()
|
|
||||||
rewards = append(rewards, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.SUCCESS(c, rewards)
|
|
||||||
}
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
package admin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core"
|
|
||||||
"chatplus/core/types"
|
|
||||||
"chatplus/handler"
|
|
||||||
"chatplus/store/model"
|
|
||||||
"chatplus/store/vo"
|
|
||||||
"chatplus/utils"
|
|
||||||
"chatplus/utils/resp"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UserHandler struct {
|
|
||||||
handler.BaseHandler
|
|
||||||
db *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewUserHandler(app *core.AppServer, db *gorm.DB) *UserHandler {
|
|
||||||
h := UserHandler{db: db}
|
|
||||||
h.App = app
|
|
||||||
return &h
|
|
||||||
}
|
|
||||||
|
|
||||||
// List 用户列表
|
|
||||||
func (h *UserHandler) List(c *gin.Context) {
|
|
||||||
page := h.GetInt(c, "page", 1)
|
|
||||||
pageSize := h.GetInt(c, "page_size", 20)
|
|
||||||
mobile := h.GetTrim(c, "mobile")
|
|
||||||
|
|
||||||
offset := (page - 1) * pageSize
|
|
||||||
var items []model.User
|
|
||||||
var users = make([]vo.User, 0)
|
|
||||||
var total int64
|
|
||||||
|
|
||||||
session := h.db.Session(&gorm.Session{})
|
|
||||||
if mobile != "" {
|
|
||||||
session = session.Where("mobile LIKE ?", "%"+mobile+"%")
|
|
||||||
}
|
|
||||||
|
|
||||||
session.Model(&model.User{}).Count(&total)
|
|
||||||
res := session.Offset(offset).Limit(pageSize).Find(&items)
|
|
||||||
if res.Error == nil {
|
|
||||||
for _, item := range items {
|
|
||||||
var user vo.User
|
|
||||||
err := utils.CopyObject(item, &user)
|
|
||||||
if err == nil {
|
|
||||||
user.Id = item.Id
|
|
||||||
user.CreatedAt = item.CreatedAt.Unix()
|
|
||||||
user.UpdatedAt = item.UpdatedAt.Unix()
|
|
||||||
users = append(users, user)
|
|
||||||
} else {
|
|
||||||
logger.Error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pageVo := vo.NewPage(total, page, pageSize, users)
|
|
||||||
resp.SUCCESS(c, pageVo)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *UserHandler) Save(c *gin.Context) {
|
|
||||||
var data struct {
|
|
||||||
Id uint `json:"id"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
Mobile string `json:"mobile"`
|
|
||||||
Calls int `json:"calls"`
|
|
||||||
ImgCalls int `json:"img_calls"`
|
|
||||||
ChatRoles []string `json:"chat_roles"`
|
|
||||||
ExpiredTime string `json:"expired_time"`
|
|
||||||
Status bool `json:"status"`
|
|
||||||
}
|
|
||||||
if err := c.ShouldBindJSON(&data); err != nil {
|
|
||||||
resp.ERROR(c, types.InvalidArgs)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var user = model.User{}
|
|
||||||
var res *gorm.DB
|
|
||||||
var userVo vo.User
|
|
||||||
if data.Id > 0 { // 更新
|
|
||||||
user.Id = data.Id
|
|
||||||
// 此处需要用 map 更新,用结构体无法更新 0 值
|
|
||||||
res = h.db.Model(&user).Updates(map[string]interface{}{
|
|
||||||
"mobile": data.Mobile,
|
|
||||||
"calls": data.Calls,
|
|
||||||
"img_calls": data.ImgCalls,
|
|
||||||
"status": data.Status,
|
|
||||||
"chat_roles_json": utils.JsonEncode(data.ChatRoles),
|
|
||||||
"expired_time": utils.Str2stamp(data.ExpiredTime),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
salt := utils.RandString(8)
|
|
||||||
u := model.User{
|
|
||||||
Mobile: data.Mobile,
|
|
||||||
Password: utils.GenPassword(data.Password, salt),
|
|
||||||
Avatar: "/images/avatar/user.png",
|
|
||||||
Salt: salt,
|
|
||||||
Status: true,
|
|
||||||
ChatRoles: utils.JsonEncode(data.ChatRoles),
|
|
||||||
ExpiredTime: utils.Str2stamp(data.ExpiredTime),
|
|
||||||
ChatConfig: utils.JsonEncode(types.UserChatConfig{
|
|
||||||
ApiKeys: map[types.Platform]string{
|
|
||||||
types.OpenAI: "",
|
|
||||||
types.Azure: "",
|
|
||||||
types.ChatGLM: "",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
Calls: data.Calls,
|
|
||||||
ImgCalls: data.ImgCalls,
|
|
||||||
}
|
|
||||||
res = h.db.Create(&u)
|
|
||||||
_ = utils.CopyObject(u, &userVo)
|
|
||||||
userVo.Id = u.Id
|
|
||||||
userVo.CreatedAt = u.CreatedAt.Unix()
|
|
||||||
userVo.UpdatedAt = u.UpdatedAt.Unix()
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c, "更新数据库失败")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.SUCCESS(c, userVo)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResetPass 重置密码
|
|
||||||
func (h *UserHandler) ResetPass(c *gin.Context) {
|
|
||||||
var data struct {
|
|
||||||
Id uint
|
|
||||||
Password string
|
|
||||||
}
|
|
||||||
if err := c.ShouldBindJSON(&data); err != nil {
|
|
||||||
resp.ERROR(c, types.InvalidArgs)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var user model.User
|
|
||||||
res := h.db.First(&user, data.Id)
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c, "No user found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
password := utils.GenPassword(data.Password, user.Salt)
|
|
||||||
user.Password = password
|
|
||||||
res = h.db.Updates(&user)
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c)
|
|
||||||
} else {
|
|
||||||
resp.SUCCESS(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *UserHandler) Remove(c *gin.Context) {
|
|
||||||
id := h.GetInt(c, "id", 0)
|
|
||||||
if id > 0 {
|
|
||||||
tx := h.db.Begin()
|
|
||||||
res := h.db.Where("id = ?", id).Delete(&model.User{})
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c, "删除失败")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 删除聊天记录
|
|
||||||
res = h.db.Where("user_id = ?", id).Delete(&model.ChatItem{})
|
|
||||||
if res.Error != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
resp.ERROR(c, "删除失败")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 删除聊天历史记录
|
|
||||||
res = h.db.Where("user_id = ?", id).Delete(&model.HistoryMessage{})
|
|
||||||
if res.Error != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
resp.ERROR(c, "删除失败")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 删除登录日志
|
|
||||||
res = h.db.Where("user_id = ?", id).Delete(&model.UserLoginLog{})
|
|
||||||
if res.Error != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
resp.ERROR(c, "删除失败")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tx.Commit()
|
|
||||||
}
|
|
||||||
resp.SUCCESS(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *UserHandler) LoginLog(c *gin.Context) {
|
|
||||||
page := h.GetInt(c, "page", 1)
|
|
||||||
pageSize := h.GetInt(c, "page_size", 20)
|
|
||||||
var total int64
|
|
||||||
h.db.Model(&model.UserLoginLog{}).Count(&total)
|
|
||||||
offset := (page - 1) * pageSize
|
|
||||||
var items []model.UserLoginLog
|
|
||||||
res := h.db.Offset(offset).Limit(pageSize).Order("id DESC").Find(&items)
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c, "获取数据失败")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var logs []vo.UserLoginLog
|
|
||||||
for _, v := range items {
|
|
||||||
var log vo.UserLoginLog
|
|
||||||
err := utils.CopyObject(v, &log)
|
|
||||||
if err == nil {
|
|
||||||
log.Id = v.Id
|
|
||||||
log.CreatedAt = v.CreatedAt.Unix()
|
|
||||||
logs = append(logs, log)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.SUCCESS(c, vo.NewPage(total, page, pageSize, logs))
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core"
|
|
||||||
"chatplus/core/types"
|
|
||||||
logger2 "chatplus/logger"
|
|
||||||
"chatplus/utils"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
var logger = logger2.GetLogger()
|
|
||||||
|
|
||||||
type BaseHandler struct {
|
|
||||||
App *core.AppServer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *BaseHandler) GetTrim(c *gin.Context, key string) string {
|
|
||||||
return strings.TrimSpace(c.Query(key))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *BaseHandler) PostInt(c *gin.Context, key string, defaultValue int) int {
|
|
||||||
return utils.IntValue(c.PostForm(key), defaultValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *BaseHandler) GetInt(c *gin.Context, key string, defaultValue int) int {
|
|
||||||
return utils.IntValue(c.Query(key), defaultValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *BaseHandler) GetFloat(c *gin.Context, key string) float64 {
|
|
||||||
return utils.FloatValue(c.Query(key))
|
|
||||||
}
|
|
||||||
func (h *BaseHandler) PostFloat(c *gin.Context, key string) float64 {
|
|
||||||
return utils.FloatValue(c.PostForm(key))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *BaseHandler) GetBool(c *gin.Context, key string) bool {
|
|
||||||
return utils.BoolValue(c.Query(key))
|
|
||||||
}
|
|
||||||
func (h *BaseHandler) PostBool(c *gin.Context, key string) bool {
|
|
||||||
return utils.BoolValue(c.PostForm(key))
|
|
||||||
}
|
|
||||||
func (h *BaseHandler) GetUserKey(c *gin.Context) string {
|
|
||||||
userId, ok := c.Get(types.LoginUserID)
|
|
||||||
if !ok {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("users/%v", userId)
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core/types"
|
|
||||||
"chatplus/service"
|
|
||||||
"chatplus/utils/resp"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 今日头条函数实现
|
|
||||||
|
|
||||||
type CaptchaHandler struct {
|
|
||||||
service *service.CaptchaService
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCaptchaHandler(s *service.CaptchaService) *CaptchaHandler {
|
|
||||||
return &CaptchaHandler{service: s}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *CaptchaHandler) Get(c *gin.Context) {
|
|
||||||
data, err := h.service.Get()
|
|
||||||
if err != nil {
|
|
||||||
resp.ERROR(c, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.SUCCESS(c, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check verify the captcha data
|
|
||||||
func (h *CaptchaHandler) Check(c *gin.Context) {
|
|
||||||
var data struct {
|
|
||||||
Key string `json:"key"`
|
|
||||||
Dots string `json:"dots"`
|
|
||||||
}
|
|
||||||
if err := c.ShouldBindJSON(&data); err != nil {
|
|
||||||
resp.ERROR(c, types.InvalidArgs)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if h.service.Check(data) {
|
|
||||||
resp.SUCCESS(c)
|
|
||||||
} else {
|
|
||||||
resp.ERROR(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core"
|
|
||||||
"chatplus/store/model"
|
|
||||||
"chatplus/store/vo"
|
|
||||||
"chatplus/utils"
|
|
||||||
"chatplus/utils/resp"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ChatModelHandler struct {
|
|
||||||
BaseHandler
|
|
||||||
db *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewChatModelHandler(app *core.AppServer, db *gorm.DB) *ChatModelHandler {
|
|
||||||
h := ChatModelHandler{db: db}
|
|
||||||
h.App = app
|
|
||||||
return &h
|
|
||||||
}
|
|
||||||
|
|
||||||
// List 模型列表
|
|
||||||
func (h *ChatModelHandler) List(c *gin.Context) {
|
|
||||||
var items []model.ChatModel
|
|
||||||
var cms = make([]vo.ChatModel, 0)
|
|
||||||
res := h.db.Where("enabled = ?", true).Order("sort_num ASC").Find(&items)
|
|
||||||
if res.Error == nil {
|
|
||||||
for _, item := range items {
|
|
||||||
var cm vo.ChatModel
|
|
||||||
err := utils.CopyObject(item, &cm)
|
|
||||||
if err == nil {
|
|
||||||
cm.Id = item.Id
|
|
||||||
cm.CreatedAt = item.CreatedAt.Unix()
|
|
||||||
cm.UpdatedAt = item.UpdatedAt.Unix()
|
|
||||||
cms = append(cms, cm)
|
|
||||||
} else {
|
|
||||||
logger.Error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resp.SUCCESS(c, cms)
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core"
|
|
||||||
"chatplus/store/model"
|
|
||||||
"chatplus/store/vo"
|
|
||||||
"chatplus/utils"
|
|
||||||
"chatplus/utils/resp"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ChatRoleHandler struct {
|
|
||||||
BaseHandler
|
|
||||||
db *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewChatRoleHandler(app *core.AppServer, db *gorm.DB) *ChatRoleHandler {
|
|
||||||
handler := &ChatRoleHandler{db: db}
|
|
||||||
handler.App = app
|
|
||||||
return handler
|
|
||||||
}
|
|
||||||
|
|
||||||
// List get user list
|
|
||||||
func (h *ChatRoleHandler) List(c *gin.Context) {
|
|
||||||
var roles []model.ChatRole
|
|
||||||
res := h.db.Where("enable", true).Order("sort_num ASC").Find(&roles)
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c, "No roles found,"+res.Error.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := utils.GetLoginUser(c, h.db)
|
|
||||||
if err != nil {
|
|
||||||
resp.NotAuth(c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var roleKeys []string
|
|
||||||
err = utils.JsonDecode(user.ChatRoles, &roleKeys)
|
|
||||||
if err != nil {
|
|
||||||
resp.ERROR(c, "角色解析失败!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 转成 vo
|
|
||||||
var roleVos = make([]vo.ChatRole, 0)
|
|
||||||
for _, r := range roles {
|
|
||||||
if !utils.ContainsStr(roleKeys, r.Key) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var v vo.ChatRole
|
|
||||||
err := utils.CopyObject(r, &v)
|
|
||||||
if err == nil {
|
|
||||||
v.Id = r.Id
|
|
||||||
roleVos = append(roleVos, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resp.SUCCESS(c, roleVos)
|
|
||||||
}
|
|
||||||
@@ -1,289 +0,0 @@
|
|||||||
package chatimpl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"chatplus/core/types"
|
|
||||||
"chatplus/store/model"
|
|
||||||
"chatplus/store/vo"
|
|
||||||
"chatplus/utils"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
"unicode/utf8"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 微软 Azure 模型消息发送实现
|
|
||||||
|
|
||||||
func (h *ChatHandler) sendAzureMessage(
|
|
||||||
chatCtx []interface{},
|
|
||||||
req types.ApiRequest,
|
|
||||||
userVo vo.User,
|
|
||||||
ctx context.Context,
|
|
||||||
session *types.ChatSession,
|
|
||||||
role model.ChatRole,
|
|
||||||
prompt string,
|
|
||||||
ws *types.WsClient) error {
|
|
||||||
promptCreatedAt := time.Now() // 记录提问时间
|
|
||||||
start := time.Now()
|
|
||||||
var apiKey = userVo.ChatConfig.ApiKeys[session.Model.Platform]
|
|
||||||
response, err := h.doRequest(ctx, req, session.Model.Platform, &apiKey)
|
|
||||||
logger.Info("HTTP请求完成,耗时:", time.Now().Sub(start))
|
|
||||||
if err != nil {
|
|
||||||
if strings.Contains(err.Error(), "context canceled") {
|
|
||||||
logger.Info("用户取消了请求:", prompt)
|
|
||||||
return nil
|
|
||||||
} else if strings.Contains(err.Error(), "no available key") {
|
|
||||||
utils.ReplyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
|
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
logger.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.ReplyMessage(ws, ErrorMsg)
|
|
||||||
utils.ReplyMessage(ws, "")
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
defer response.Body.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
contentType := response.Header.Get("Content-Type")
|
|
||||||
if strings.Contains(contentType, "text/event-stream") {
|
|
||||||
replyCreatedAt := time.Now() // 记录回复时间
|
|
||||||
// 循环读取 Chunk 消息
|
|
||||||
var message = types.Message{}
|
|
||||||
var contents = make([]string, 0)
|
|
||||||
var functionCall = false
|
|
||||||
var functionName string
|
|
||||||
var arguments = make([]string, 0)
|
|
||||||
scanner := bufio.NewScanner(response.Body)
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
if !strings.Contains(line, "data:") || len(line) < 30 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var responseBody = types.ApiResponse{}
|
|
||||||
err = json.Unmarshal([]byte(line[6:]), &responseBody)
|
|
||||||
if err != nil || len(responseBody.Choices) == 0 { // 数据解析出错
|
|
||||||
logger.Error(err, line)
|
|
||||||
utils.ReplyMessage(ws, ErrorMsg)
|
|
||||||
utils.ReplyMessage(ws, "")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
fun := responseBody.Choices[0].Delta.FunctionCall
|
|
||||||
if functionCall && fun.Name == "" {
|
|
||||||
arguments = append(arguments, fun.Arguments)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !utils.IsEmptyValue(fun) {
|
|
||||||
functionName = fun.Name
|
|
||||||
f := h.App.Functions[functionName]
|
|
||||||
if f != nil {
|
|
||||||
functionCall = true
|
|
||||||
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
|
|
||||||
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: fmt.Sprintf("正在调用函数 `%s` 作答 ...\n\n", f.Name())})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if responseBody.Choices[0].FinishReason == "function_call" { // 函数调用完毕
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化 role
|
|
||||||
if responseBody.Choices[0].Delta.Role != "" && message.Role == "" {
|
|
||||||
message.Role = responseBody.Choices[0].Delta.Role
|
|
||||||
utils.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, utils.InterfaceToString(content))
|
|
||||||
utils.ReplyChunkMessage(ws, types.WsMessage{
|
|
||||||
Type: types.WsMiddle,
|
|
||||||
Content: utils.InterfaceToString(responseBody.Choices[0].Delta.Content),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} // end for
|
|
||||||
|
|
||||||
if err := scanner.Err(); err != nil {
|
|
||||||
if strings.Contains(err.Error(), "context canceled") {
|
|
||||||
logger.Info("用户取消了请求:", prompt)
|
|
||||||
} else {
|
|
||||||
logger.Error("信息读取出错:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if functionCall { // 调用函数完成任务
|
|
||||||
var params map[string]interface{}
|
|
||||||
_ = utils.JsonDecode(strings.Join(arguments, ""), ¶ms)
|
|
||||||
logger.Debugf("函数名称: %s, 函数参数:%s", functionName, params)
|
|
||||||
|
|
||||||
// for creating image, check if the user's img_calls > 0
|
|
||||||
if functionName == types.FuncMidJourney && userVo.ImgCalls <= 0 {
|
|
||||||
utils.ReplyMessage(ws, "**当前用户剩余绘图次数已用尽,请扫描下面二维码联系管理员!**")
|
|
||||||
utils.ReplyMessage(ws, "")
|
|
||||||
} else {
|
|
||||||
f := h.App.Functions[functionName]
|
|
||||||
if functionName == types.FuncMidJourney {
|
|
||||||
params["user_id"] = userVo.Id
|
|
||||||
params["role_id"] = role.Id
|
|
||||||
params["chat_id"] = session.ChatId
|
|
||||||
params["icon"] = "/images/avatar/mid_journey.png"
|
|
||||||
params["session_id"] = session.SessionId
|
|
||||||
}
|
|
||||||
data, err := f.Invoke(params)
|
|
||||||
if err != nil {
|
|
||||||
msg := "调用函数出错:" + err.Error()
|
|
||||||
utils.ReplyChunkMessage(ws, types.WsMessage{
|
|
||||||
Type: types.WsMiddle,
|
|
||||||
Content: msg,
|
|
||||||
})
|
|
||||||
contents = append(contents, msg)
|
|
||||||
} else {
|
|
||||||
content := data
|
|
||||||
if functionName == types.FuncMidJourney {
|
|
||||||
content = fmt.Sprintf("绘画提示词:%s 已推送任务到 MidJourney 机器人,请耐心等待任务执行...", data)
|
|
||||||
h.mjService.ChatClients.Put(session.SessionId, ws)
|
|
||||||
// update user's img_calls
|
|
||||||
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.ReplyChunkMessage(ws, types.WsMessage{
|
|
||||||
Type: types.WsMiddle,
|
|
||||||
Content: content,
|
|
||||||
})
|
|
||||||
contents = append(contents, content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 消息发送成功
|
|
||||||
if len(contents) > 0 {
|
|
||||||
// 更新用户的对话次数
|
|
||||||
if userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" {
|
|
||||||
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("calls", gorm.Expr("calls - ?", 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
if message.Role == "" {
|
|
||||||
message.Role = "assistant"
|
|
||||||
}
|
|
||||||
message.Content = strings.Join(contents, "")
|
|
||||||
useMsg := types.Message{Role: "user", Content: prompt}
|
|
||||||
|
|
||||||
// 更新上下文消息,如果是调用函数则不需要更新上下文
|
|
||||||
if h.App.ChatConfig.EnableContext && functionCall == false {
|
|
||||||
chatCtx = append(chatCtx, useMsg) // 提问消息
|
|
||||||
chatCtx = append(chatCtx, message) // 回复消息
|
|
||||||
h.App.ChatContexts.Put(session.ChatId, chatCtx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 追加聊天记录
|
|
||||||
if h.App.ChatConfig.EnableHistory {
|
|
||||||
useContext := true
|
|
||||||
if functionCall {
|
|
||||||
useContext = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// for prompt
|
|
||||||
promptToken, err := utils.CalcTokens(prompt, req.Model)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(err)
|
|
||||||
}
|
|
||||||
historyUserMsg := model.HistoryMessage{
|
|
||||||
UserId: userVo.Id,
|
|
||||||
ChatId: session.ChatId,
|
|
||||||
RoleId: role.Id,
|
|
||||||
Type: types.PromptMsg,
|
|
||||||
Icon: userVo.Avatar,
|
|
||||||
Content: prompt,
|
|
||||||
Tokens: promptToken,
|
|
||||||
UseContext: useContext,
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算本次对话消耗的总 token 数量
|
|
||||||
var totalTokens = 0
|
|
||||||
if functionCall { // prompt + 函数名 + 参数 token
|
|
||||||
tokens, _ := utils.CalcTokens(functionName, req.Model)
|
|
||||||
totalTokens += tokens
|
|
||||||
tokens, _ = utils.CalcTokens(utils.InterfaceToString(arguments), req.Model)
|
|
||||||
totalTokens += tokens
|
|
||||||
} else {
|
|
||||||
totalTokens, _ = utils.CalcTokens(message.Content, req.Model)
|
|
||||||
}
|
|
||||||
totalTokens += getTotalTokens(req)
|
|
||||||
|
|
||||||
historyReplyMsg := model.HistoryMessage{
|
|
||||||
UserId: userVo.Id,
|
|
||||||
ChatId: session.ChatId,
|
|
||||||
RoleId: role.Id,
|
|
||||||
Type: types.ReplyMsg,
|
|
||||||
Icon: role.Icon,
|
|
||||||
Content: message.Content,
|
|
||||||
Tokens: totalTokens,
|
|
||||||
UseContext: useContext,
|
|
||||||
}
|
|
||||||
historyReplyMsg.CreatedAt = replyCreatedAt
|
|
||||||
historyReplyMsg.UpdatedAt = replyCreatedAt
|
|
||||||
res = h.db.Create(&historyReplyMsg)
|
|
||||||
if res.Error != nil {
|
|
||||||
logger.Error("failed to save reply history message: ", res.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新用户信息
|
|
||||||
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).
|
|
||||||
UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", totalTokens))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存当前会话
|
|
||||||
var chatItem model.ChatItem
|
|
||||||
res := h.db.Where("chat_id = ?", session.ChatId).First(&chatItem)
|
|
||||||
if res.Error != nil {
|
|
||||||
chatItem.ChatId = session.ChatId
|
|
||||||
chatItem.UserId = session.UserId
|
|
||||||
chatItem.RoleId = role.Id
|
|
||||||
chatItem.ModelId = session.Model.Id
|
|
||||||
if utf8.RuneCountInString(prompt) > 30 {
|
|
||||||
chatItem.Title = string([]rune(prompt)[:30]) + "..."
|
|
||||||
} else {
|
|
||||||
chatItem.Title = prompt
|
|
||||||
}
|
|
||||||
h.db.Create(&chatItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
body, err := io.ReadAll(response.Body)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error with reading response: %v", err)
|
|
||||||
}
|
|
||||||
var res types.ApiError
|
|
||||||
err = json.Unmarshal(body, &res)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error with decode response: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.Contains(res.Error.Message, "maximum context length") {
|
|
||||||
logger.Error(res.Error.Message)
|
|
||||||
utils.ReplyMessage(ws, "当前会话上下文长度超出限制,已为您清空会话上下文!")
|
|
||||||
h.App.ChatContexts.Delete(session.ChatId)
|
|
||||||
return h.sendMessage(ctx, session, role, prompt, ws)
|
|
||||||
} else {
|
|
||||||
utils.ReplyMessage(ws, "请求 Azure API 失败:"+res.Error.Message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,273 +0,0 @@
|
|||||||
package chatimpl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"chatplus/core/types"
|
|
||||||
"chatplus/store/model"
|
|
||||||
"chatplus/store/vo"
|
|
||||||
"chatplus/utils"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
"unicode/utf8"
|
|
||||||
)
|
|
||||||
|
|
||||||
type baiduResp struct {
|
|
||||||
Id string `json:"id"`
|
|
||||||
Object string `json:"object"`
|
|
||||||
Created int `json:"created"`
|
|
||||||
SentenceId int `json:"sentence_id"`
|
|
||||||
IsEnd bool `json:"is_end"`
|
|
||||||
IsTruncated bool `json:"is_truncated"`
|
|
||||||
Result string `json:"result"`
|
|
||||||
NeedClearHistory bool `json:"need_clear_history"`
|
|
||||||
Usage struct {
|
|
||||||
PromptTokens int `json:"prompt_tokens"`
|
|
||||||
CompletionTokens int `json:"completion_tokens"`
|
|
||||||
TotalTokens int `json:"total_tokens"`
|
|
||||||
} `json:"usage"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 百度文心一言消息发送实现
|
|
||||||
|
|
||||||
func (h *ChatHandler) sendBaiduMessage(
|
|
||||||
chatCtx []interface{},
|
|
||||||
req types.ApiRequest,
|
|
||||||
userVo vo.User,
|
|
||||||
ctx context.Context,
|
|
||||||
session *types.ChatSession,
|
|
||||||
role model.ChatRole,
|
|
||||||
prompt string,
|
|
||||||
ws *types.WsClient) error {
|
|
||||||
promptCreatedAt := time.Now() // 记录提问时间
|
|
||||||
start := time.Now()
|
|
||||||
var apiKey = userVo.ChatConfig.ApiKeys[session.Model.Platform]
|
|
||||||
response, err := h.doRequest(ctx, req, session.Model.Platform, &apiKey)
|
|
||||||
logger.Info("HTTP请求完成,耗时:", time.Now().Sub(start))
|
|
||||||
if err != nil {
|
|
||||||
if strings.Contains(err.Error(), "context canceled") {
|
|
||||||
logger.Info("用户取消了请求:", prompt)
|
|
||||||
return nil
|
|
||||||
} else if strings.Contains(err.Error(), "no available key") {
|
|
||||||
utils.ReplyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
|
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
logger.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.ReplyMessage(ws, ErrorMsg)
|
|
||||||
utils.ReplyMessage(ws, "")
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
defer response.Body.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
contentType := response.Header.Get("Content-Type")
|
|
||||||
if strings.Contains(contentType, "text/event-stream") {
|
|
||||||
replyCreatedAt := time.Now() // 记录回复时间
|
|
||||||
// 循环读取 Chunk 消息
|
|
||||||
var message = types.Message{}
|
|
||||||
var contents = make([]string, 0)
|
|
||||||
var content string
|
|
||||||
scanner := bufio.NewScanner(response.Body)
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
if len(line) < 5 || strings.HasPrefix(line, "id:") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(line, "data:") {
|
|
||||||
content = line[5:]
|
|
||||||
}
|
|
||||||
|
|
||||||
var resp baiduResp
|
|
||||||
err := utils.JsonDecode(content, &resp)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("error with parse data line: ", err)
|
|
||||||
utils.ReplyMessage(ws, fmt.Sprintf("**解析数据行失败:%s**", err))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(contents) == 0 {
|
|
||||||
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
|
|
||||||
}
|
|
||||||
utils.ReplyChunkMessage(ws, types.WsMessage{
|
|
||||||
Type: types.WsMiddle,
|
|
||||||
Content: utils.InterfaceToString(resp.Result),
|
|
||||||
})
|
|
||||||
contents = append(contents, resp.Result)
|
|
||||||
|
|
||||||
if resp.IsTruncated {
|
|
||||||
utils.ReplyMessage(ws, "AI 输出异常中断")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.IsEnd {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
} // end for
|
|
||||||
|
|
||||||
if err := scanner.Err(); err != nil {
|
|
||||||
if strings.Contains(err.Error(), "context canceled") {
|
|
||||||
logger.Info("用户取消了请求:", prompt)
|
|
||||||
} else {
|
|
||||||
logger.Error("信息读取出错:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 消息发送成功
|
|
||||||
if len(contents) > 0 {
|
|
||||||
// 更新用户的对话次数
|
|
||||||
if userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" {
|
|
||||||
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("calls", gorm.Expr("calls - ?", 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
if message.Role == "" {
|
|
||||||
message.Role = "assistant"
|
|
||||||
}
|
|
||||||
message.Content = strings.Join(contents, "")
|
|
||||||
useMsg := types.Message{Role: "user", Content: prompt}
|
|
||||||
|
|
||||||
// 更新上下文消息,如果是调用函数则不需要更新上下文
|
|
||||||
if h.App.ChatConfig.EnableContext {
|
|
||||||
chatCtx = append(chatCtx, useMsg) // 提问消息
|
|
||||||
chatCtx = append(chatCtx, message) // 回复消息
|
|
||||||
h.App.ChatContexts.Put(session.ChatId, chatCtx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 追加聊天记录
|
|
||||||
if h.App.ChatConfig.EnableHistory {
|
|
||||||
// for prompt
|
|
||||||
promptToken, err := utils.CalcTokens(prompt, req.Model)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(err)
|
|
||||||
}
|
|
||||||
historyUserMsg := model.HistoryMessage{
|
|
||||||
UserId: userVo.Id,
|
|
||||||
ChatId: session.ChatId,
|
|
||||||
RoleId: role.Id,
|
|
||||||
Type: types.PromptMsg,
|
|
||||||
Icon: userVo.Avatar,
|
|
||||||
Content: prompt,
|
|
||||||
Tokens: promptToken,
|
|
||||||
UseContext: true,
|
|
||||||
}
|
|
||||||
historyUserMsg.CreatedAt = promptCreatedAt
|
|
||||||
historyUserMsg.UpdatedAt = promptCreatedAt
|
|
||||||
res := h.db.Save(&historyUserMsg)
|
|
||||||
if res.Error != nil {
|
|
||||||
logger.Error("failed to save prompt history message: ", res.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// for reply
|
|
||||||
// 计算本次对话消耗的总 token 数量
|
|
||||||
replyToken, _ := utils.CalcTokens(message.Content, req.Model)
|
|
||||||
totalTokens := replyToken + getTotalTokens(req)
|
|
||||||
historyReplyMsg := model.HistoryMessage{
|
|
||||||
UserId: userVo.Id,
|
|
||||||
ChatId: session.ChatId,
|
|
||||||
RoleId: role.Id,
|
|
||||||
Type: types.ReplyMsg,
|
|
||||||
Icon: role.Icon,
|
|
||||||
Content: message.Content,
|
|
||||||
Tokens: totalTokens,
|
|
||||||
UseContext: true,
|
|
||||||
}
|
|
||||||
historyReplyMsg.CreatedAt = replyCreatedAt
|
|
||||||
historyReplyMsg.UpdatedAt = replyCreatedAt
|
|
||||||
res = h.db.Create(&historyReplyMsg)
|
|
||||||
if res.Error != nil {
|
|
||||||
logger.Error("failed to save reply history message: ", res.Error)
|
|
||||||
}
|
|
||||||
// 更新用户信息
|
|
||||||
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).
|
|
||||||
UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", totalTokens))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存当前会话
|
|
||||||
var chatItem model.ChatItem
|
|
||||||
res := h.db.Where("chat_id = ?", session.ChatId).First(&chatItem)
|
|
||||||
if res.Error != nil {
|
|
||||||
chatItem.ChatId = session.ChatId
|
|
||||||
chatItem.UserId = session.UserId
|
|
||||||
chatItem.RoleId = role.Id
|
|
||||||
chatItem.ModelId = session.Model.Id
|
|
||||||
if utf8.RuneCountInString(prompt) > 30 {
|
|
||||||
chatItem.Title = string([]rune(prompt)[:30]) + "..."
|
|
||||||
} else {
|
|
||||||
chatItem.Title = prompt
|
|
||||||
}
|
|
||||||
h.db.Create(&chatItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
body, err := io.ReadAll(response.Body)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error with reading response: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var res struct {
|
|
||||||
Code int `json:"error_code"`
|
|
||||||
Msg string `json:"error_msg"`
|
|
||||||
}
|
|
||||||
err = json.Unmarshal(body, &res)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error with decode response: %v", err)
|
|
||||||
}
|
|
||||||
utils.ReplyMessage(ws, "请求百度文心大模型 API 失败:"+res.Msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ChatHandler) getBaiduToken(apiKey string) (string, error) {
|
|
||||||
ctx := context.Background()
|
|
||||||
tokenString, err := h.redis.Get(ctx, apiKey).Result()
|
|
||||||
if err == nil {
|
|
||||||
return tokenString, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
expr := time.Hour * 24 * 20 // access_token 有效期
|
|
||||||
key := strings.Split(apiKey, "|")
|
|
||||||
if len(key) != 2 {
|
|
||||||
return "", fmt.Errorf("invalid api key: %s", apiKey)
|
|
||||||
}
|
|
||||||
url := fmt.Sprintf("https://aip.baidubce.com/oauth/2.0/token?client_id=%s&client_secret=%s&grant_type=client_credentials", key[0], key[1])
|
|
||||||
client := &http.Client{}
|
|
||||||
req, err := http.NewRequest("POST", url, nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
req.Header.Add("Content-Type", "application/json")
|
|
||||||
req.Header.Add("Accept", "application/json")
|
|
||||||
|
|
||||||
res, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error with send request: %w", err)
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
|
|
||||||
body, err := io.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error with read response: %w", err)
|
|
||||||
}
|
|
||||||
var r map[string]interface{}
|
|
||||||
err = json.Unmarshal(body, &r)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error with parse response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if r["error"] != nil {
|
|
||||||
return "", fmt.Errorf("error with api response: %s", r["error_description"])
|
|
||||||
}
|
|
||||||
|
|
||||||
tokenString = fmt.Sprintf("%s", r["access_token"])
|
|
||||||
h.redis.Set(ctx, apiKey, tokenString, expr)
|
|
||||||
return tokenString, nil
|
|
||||||
}
|
|
||||||
@@ -1,465 +0,0 @@
|
|||||||
package chatimpl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"chatplus/core"
|
|
||||||
"chatplus/core/types"
|
|
||||||
"chatplus/handler"
|
|
||||||
logger2 "chatplus/logger"
|
|
||||||
"chatplus/service/mj"
|
|
||||||
"chatplus/store"
|
|
||||||
"chatplus/store/model"
|
|
||||||
"chatplus/store/vo"
|
|
||||||
"chatplus/utils"
|
|
||||||
"chatplus/utils/resp"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/go-redis/redis/v8"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const ErrorMsg = "抱歉,AI 助手开小差了,请稍后再试。"
|
|
||||||
|
|
||||||
var logger = logger2.GetLogger()
|
|
||||||
|
|
||||||
type ChatHandler struct {
|
|
||||||
handler.BaseHandler
|
|
||||||
db *gorm.DB
|
|
||||||
leveldb *store.LevelDB
|
|
||||||
redis *redis.Client
|
|
||||||
mjService *mj.Service
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewChatHandler(app *core.AppServer, db *gorm.DB, levelDB *store.LevelDB, redis *redis.Client, service *mj.Service) *ChatHandler {
|
|
||||||
h := ChatHandler{
|
|
||||||
db: db,
|
|
||||||
leveldb: levelDB,
|
|
||||||
redis: redis,
|
|
||||||
mjService: service,
|
|
||||||
}
|
|
||||||
h.App = app
|
|
||||||
return &h
|
|
||||||
}
|
|
||||||
|
|
||||||
var chatConfig types.ChatConfig
|
|
||||||
|
|
||||||
// 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("session_id")
|
|
||||||
roleId := h.GetInt(c, "role_id", 0)
|
|
||||||
chatId := c.Query("chat_id")
|
|
||||||
modelId := h.GetInt(c, "model_id", 0)
|
|
||||||
|
|
||||||
client := types.NewWsClient(ws)
|
|
||||||
// get model info
|
|
||||||
var chatModel model.ChatModel
|
|
||||||
res := h.db.First(&chatModel, modelId)
|
|
||||||
if res.Error != nil || chatModel.Enabled == false {
|
|
||||||
utils.ReplyMessage(client, "当前AI模型暂未启用,连接已关闭!!!")
|
|
||||||
c.Abort()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
session := h.App.ChatSession.Get(sessionId)
|
|
||||||
if session == nil {
|
|
||||||
user, err := utils.GetLoginUser(c, h.db)
|
|
||||||
if err != nil {
|
|
||||||
logger.Info("用户未登录")
|
|
||||||
c.Abort()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
session = &types.ChatSession{
|
|
||||||
SessionId: sessionId,
|
|
||||||
ClientIP: c.ClientIP(),
|
|
||||||
Username: user.Mobile,
|
|
||||||
UserId: user.Id,
|
|
||||||
}
|
|
||||||
h.App.ChatSession.Put(sessionId, session)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.Id = chat.ModelId
|
|
||||||
roleId = int(chat.RoleId)
|
|
||||||
}
|
|
||||||
|
|
||||||
session.ChatId = chatId
|
|
||||||
session.Model = types.ChatModel{
|
|
||||||
Id: chatModel.Id,
|
|
||||||
Value: chatModel.Value,
|
|
||||||
Platform: types.Platform(chatModel.Platform)}
|
|
||||||
logger.Infof("New websocket connected, IP: %s, Username: %s", c.ClientIP(), session.Username)
|
|
||||||
var chatRole model.ChatRole
|
|
||||||
res = h.db.First(&chatRole, roleId)
|
|
||||||
if res.Error != nil || !chatRole.Enable {
|
|
||||||
utils.ReplyMessage(client, "当前聊天角色不存在或者未启用,连接已关闭!!!")
|
|
||||||
c.Abort()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化聊天配置
|
|
||||||
var config model.Config
|
|
||||||
h.db.Where("marker", "chat").First(&config)
|
|
||||||
err = utils.JsonDecode(config.Config, &chatConfig)
|
|
||||||
if err != nil {
|
|
||||||
utils.ReplyMessage(client, "加载系统配置失败,连接已关闭!!!")
|
|
||||||
c.Abort()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存会话连接
|
|
||||||
h.App.ChatClients.Put(sessionId, client)
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
_, msg, err := client.Receive()
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(err)
|
|
||||||
client.Close()
|
|
||||||
h.App.ChatClients.Delete(sessionId)
|
|
||||||
cancelFunc := h.App.ReqCancelFunc.Get(sessionId)
|
|
||||||
if cancelFunc != nil {
|
|
||||||
cancelFunc()
|
|
||||||
h.App.ReqCancelFunc.Delete(sessionId)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
message := string(msg)
|
|
||||||
logger.Info("Receive a message: ", message)
|
|
||||||
//utils.ReplyMessage(client, "这是一条测试消息!")
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
h.App.ReqCancelFunc.Put(sessionId, cancel)
|
|
||||||
// 回复消息
|
|
||||||
err = h.sendMessage(ctx, session, chatRole, message, client)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(err)
|
|
||||||
utils.ReplyChunkMessage(client, types.WsMessage{Type: types.WsEnd})
|
|
||||||
} else {
|
|
||||||
utils.ReplyChunkMessage(client, types.WsMessage{Type: types.WsEnd})
|
|
||||||
logger.Info("回答完毕: " + string(message))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSession, role model.ChatRole, prompt string, ws *types.WsClient) error {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
logger.Error("Recover message from error: ", r)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
var user model.User
|
|
||||||
res := h.db.Model(&model.User{}).First(&user, session.UserId)
|
|
||||||
if res.Error != nil {
|
|
||||||
utils.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 {
|
|
||||||
utils.ReplyMessage(ws, "您的账号已经被禁用,如果疑问,请联系管理员!")
|
|
||||||
utils.ReplyMessage(ws, "")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if userVo.Calls <= 0 && userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" {
|
|
||||||
utils.ReplyMessage(ws, "您的对话次数已经用尽,请联系管理员或者点击左下角菜单加入众筹获得100次对话!")
|
|
||||||
utils.ReplyMessage(ws, "")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if userVo.ExpiredTime > 0 && userVo.ExpiredTime <= time.Now().Unix() {
|
|
||||||
utils.ReplyMessage(ws, "您的账号已经过期,请联系管理员!")
|
|
||||||
utils.ReplyMessage(ws, "")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var req = types.ApiRequest{
|
|
||||||
Model: session.Model.Value,
|
|
||||||
Stream: true,
|
|
||||||
}
|
|
||||||
switch session.Model.Platform {
|
|
||||||
case types.Azure:
|
|
||||||
req.Temperature = h.App.ChatConfig.Azure.Temperature
|
|
||||||
req.MaxTokens = h.App.ChatConfig.Azure.MaxTokens
|
|
||||||
break
|
|
||||||
case types.ChatGLM:
|
|
||||||
req.Temperature = h.App.ChatConfig.ChatGML.Temperature
|
|
||||||
req.MaxTokens = h.App.ChatConfig.ChatGML.MaxTokens
|
|
||||||
break
|
|
||||||
case types.Baidu:
|
|
||||||
req.Temperature = h.App.ChatConfig.OpenAI.Temperature
|
|
||||||
// TODO: 目前只支持 ERNIE-Bot-turbo 模型,如果是 ERNIE-Bot 模型则需要增加函数支持
|
|
||||||
case types.OpenAI:
|
|
||||||
req.Temperature = h.App.ChatConfig.OpenAI.Temperature
|
|
||||||
req.MaxTokens = h.App.ChatConfig.OpenAI.MaxTokens
|
|
||||||
// OpenAI 支持函数功能
|
|
||||||
if h.App.SysConfig.EnabledFunction {
|
|
||||||
var functions = make([]types.Function, 0)
|
|
||||||
for _, f := range types.InnerFunctions {
|
|
||||||
if !h.App.SysConfig.EnabledDraw && f.Name == types.FuncMidJourney {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
functions = append(functions, f)
|
|
||||||
}
|
|
||||||
req.Functions = functions
|
|
||||||
}
|
|
||||||
case types.XunFei:
|
|
||||||
req.Temperature = h.App.ChatConfig.XunFei.Temperature
|
|
||||||
req.MaxTokens = h.App.ChatConfig.XunFei.MaxTokens
|
|
||||||
default:
|
|
||||||
utils.ReplyMessage(ws, "不支持的平台:"+session.Model.Platform+",请联系管理员!")
|
|
||||||
utils.ReplyMessage(ws, "")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载聊天上下文
|
|
||||||
var chatCtx []interface{}
|
|
||||||
if h.App.ChatConfig.EnableContext {
|
|
||||||
if h.App.ChatContexts.Has(session.ChatId) {
|
|
||||||
chatCtx = h.App.ChatContexts.Get(session.ChatId)
|
|
||||||
} else {
|
|
||||||
// calculate the tokens of current request, to prevent to exceeding the max tokens num
|
|
||||||
tokens := req.MaxTokens
|
|
||||||
for _, f := range types.InnerFunctions {
|
|
||||||
tks, _ := utils.CalcTokens(utils.JsonEncode(f), req.Model)
|
|
||||||
tokens += tks
|
|
||||||
}
|
|
||||||
|
|
||||||
// loading the role context
|
|
||||||
var messages []types.Message
|
|
||||||
err := utils.JsonDecode(role.Context, &messages)
|
|
||||||
if err == nil {
|
|
||||||
for _, v := range messages {
|
|
||||||
tks, _ := utils.CalcTokens(v.Content, req.Model)
|
|
||||||
if tokens+tks >= types.ModelToTokens[req.Model] {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
tokens += tks
|
|
||||||
chatCtx = append(chatCtx, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// loading recent chat history as chat context
|
|
||||||
if chatConfig.ContextDeep > 0 {
|
|
||||||
var historyMessages []model.HistoryMessage
|
|
||||||
res := h.db.Debug().Where("chat_id = ? and use_context = 1", session.ChatId).Limit(chatConfig.ContextDeep).Order("id desc").Find(&historyMessages)
|
|
||||||
if res.Error == nil {
|
|
||||||
for i := len(historyMessages) - 1; i >= 0; i-- {
|
|
||||||
msg := historyMessages[i]
|
|
||||||
if tokens+msg.Tokens >= types.ModelToTokens[session.Model.Value] {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
tokens += msg.Tokens
|
|
||||||
ms := types.Message{Role: "user", Content: msg.Content}
|
|
||||||
if msg.Type == types.ReplyMsg {
|
|
||||||
ms.Role = "assistant"
|
|
||||||
}
|
|
||||||
chatCtx = append(chatCtx, ms)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logger.Debugf("聊天上下文:%+v", chatCtx)
|
|
||||||
}
|
|
||||||
reqMgs := make([]interface{}, 0)
|
|
||||||
for _, m := range chatCtx {
|
|
||||||
reqMgs = append(reqMgs, m)
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Messages = append(reqMgs, map[string]interface{}{
|
|
||||||
"role": "user",
|
|
||||||
"content": prompt,
|
|
||||||
})
|
|
||||||
|
|
||||||
switch session.Model.Platform {
|
|
||||||
case types.Azure:
|
|
||||||
return h.sendAzureMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
|
|
||||||
case types.OpenAI:
|
|
||||||
return h.sendOpenAiMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
|
|
||||||
case types.ChatGLM:
|
|
||||||
return h.sendChatGLMMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
|
|
||||||
case types.Baidu:
|
|
||||||
return h.sendBaiduMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
|
|
||||||
case types.XunFei:
|
|
||||||
return h.sendXunFeiMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
|
|
||||||
|
|
||||||
}
|
|
||||||
utils.ReplyChunkMessage(ws, types.WsMessage{
|
|
||||||
Type: types.WsMiddle,
|
|
||||||
Content: fmt.Sprintf("Not supported platform: %s", session.Model.Platform),
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tokens 统计 token 数量
|
|
||||||
func (h *ChatHandler) Tokens(c *gin.Context) {
|
|
||||||
var data struct {
|
|
||||||
Text string `json:"text"`
|
|
||||||
Model string `json:"model"`
|
|
||||||
}
|
|
||||||
if err := c.ShouldBindJSON(&data); err != nil {
|
|
||||||
resp.ERROR(c, types.InvalidArgs)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有传入 text 字段,则说明是获取当前 reply 总的 token 消耗(带上下文)
|
|
||||||
if data.Text == "" {
|
|
||||||
var item model.HistoryMessage
|
|
||||||
userId, _ := c.Get(types.LoginUserID)
|
|
||||||
res := h.db.Where("user_id = ?", userId).Last(&item)
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c, res.Error.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp.SUCCESS(c, item.Tokens)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tokens, err := utils.CalcTokens(data.Text, data.Model)
|
|
||||||
if err != nil {
|
|
||||||
resp.ERROR(c, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.SUCCESS(c, tokens)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getTotalTokens(req types.ApiRequest) int {
|
|
||||||
encode := utils.JsonEncode(req.Messages)
|
|
||||||
var items []map[string]interface{}
|
|
||||||
err := utils.JsonDecode(encode, &items)
|
|
||||||
if err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
tokens := 0
|
|
||||||
for _, item := range items {
|
|
||||||
content, ok := item["content"]
|
|
||||||
if ok && !utils.IsEmptyValue(content) {
|
|
||||||
t, err := utils.CalcTokens(utils.InterfaceToString(content), req.Model)
|
|
||||||
if err == nil {
|
|
||||||
tokens += t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tokens
|
|
||||||
}
|
|
||||||
|
|
||||||
// StopGenerate 停止生成
|
|
||||||
func (h *ChatHandler) StopGenerate(c *gin.Context) {
|
|
||||||
sessionId := c.Query("session_id")
|
|
||||||
if h.App.ReqCancelFunc.Has(sessionId) {
|
|
||||||
h.App.ReqCancelFunc.Get(sessionId)()
|
|
||||||
h.App.ReqCancelFunc.Delete(sessionId)
|
|
||||||
}
|
|
||||||
resp.SUCCESS(c, types.OkMsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送请求到 OpenAI 服务器
|
|
||||||
// useOwnApiKey: 是否使用了用户自己的 API KEY
|
|
||||||
func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platform types.Platform, apiKey *string) (*http.Response, error) {
|
|
||||||
|
|
||||||
var apiURL string
|
|
||||||
switch platform {
|
|
||||||
case types.Azure:
|
|
||||||
md := strings.Replace(req.Model, ".", "", 1)
|
|
||||||
apiURL = strings.Replace(h.App.ChatConfig.Azure.ApiURL, "{model}", md, 1)
|
|
||||||
break
|
|
||||||
case types.ChatGLM:
|
|
||||||
apiURL = strings.Replace(h.App.ChatConfig.ChatGML.ApiURL, "{model}", req.Model, 1)
|
|
||||||
req.Prompt = req.Messages // 使用 prompt 字段替代 message 字段
|
|
||||||
req.Messages = nil
|
|
||||||
break
|
|
||||||
case types.Baidu:
|
|
||||||
apiURL = h.App.ChatConfig.Baidu.ApiURL
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
apiURL = h.App.ChatConfig.OpenAI.ApiURL
|
|
||||||
}
|
|
||||||
if *apiKey == "" {
|
|
||||||
var key model.ApiKey
|
|
||||||
res := h.db.Where("platform = ?", platform).Order("last_used_at ASC").First(&key)
|
|
||||||
if res.Error != nil {
|
|
||||||
return nil, errors.New("no available key, please import key")
|
|
||||||
}
|
|
||||||
// 更新 API KEY 的最后使用时间
|
|
||||||
h.db.Model(&key).UpdateColumn("last_used_at", time.Now().Unix())
|
|
||||||
*apiKey = key.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
// 百度文心,需要串接 access_token
|
|
||||||
if platform == types.Baidu {
|
|
||||||
token, err := h.getBaiduToken(*apiKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
logger.Info("百度文心 Access_Token:", token)
|
|
||||||
apiURL = fmt.Sprintf("%s?access_token=%s", apiURL, token)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建 HttpClient 请求对象
|
|
||||||
var client *http.Client
|
|
||||||
requestBody, err := json.Marshal(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
request, err := http.NewRequest(http.MethodPost, apiURL, bytes.NewBuffer(requestBody))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
request = request.WithContext(ctx)
|
|
||||||
request.Header.Set("Content-Type", "application/json")
|
|
||||||
proxyURL := h.App.Config.ProxyURL
|
|
||||||
if proxyURL != "" && platform == types.OpenAI { // 使用代理
|
|
||||||
proxy, _ := url.Parse(proxyURL)
|
|
||||||
client = &http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
Proxy: http.ProxyURL(proxy),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
client = http.DefaultClient
|
|
||||||
}
|
|
||||||
logger.Infof("Sending %s request, KEY: %s, PROXY: %s, Model: %s", platform, *apiKey, proxyURL, req.Model)
|
|
||||||
switch platform {
|
|
||||||
case types.Azure:
|
|
||||||
request.Header.Set("api-key", *apiKey)
|
|
||||||
break
|
|
||||||
case types.ChatGLM:
|
|
||||||
token, err := h.getChatGLMToken(*apiKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
logger.Info(token)
|
|
||||||
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
|
||||||
break
|
|
||||||
case types.Baidu:
|
|
||||||
request.RequestURI = ""
|
|
||||||
case types.OpenAI:
|
|
||||||
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiKey))
|
|
||||||
}
|
|
||||||
return client.Do(request)
|
|
||||||
}
|
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
package chatimpl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core/types"
|
|
||||||
"chatplus/store/model"
|
|
||||||
"chatplus/store/vo"
|
|
||||||
"chatplus/utils"
|
|
||||||
"chatplus/utils/resp"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// List 获取会话列表
|
|
||||||
func (h *ChatHandler) List(c *gin.Context) {
|
|
||||||
userId := h.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 {
|
|
||||||
ChatId string `json:"chat_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
}
|
|
||||||
if err := c.ShouldBindJSON(&data); err != nil {
|
|
||||||
resp.ERROR(c, types.InvalidArgs)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
res := h.db.Model(&model.ChatItem{}).Where("chat_id = ?", data.ChatId).UpdateColumn("title", data.Title)
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c, "Failed to update database")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.SUCCESS(c, types.OkMsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear 清空所有聊天记录
|
|
||||||
func (h *ChatHandler) Clear(c *gin.Context) {
|
|
||||||
// 获取当前登录用户所有的聊天会话
|
|
||||||
user, err := utils.GetLoginUser(c, h.db)
|
|
||||||
if err != nil {
|
|
||||||
resp.NotAuth(c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var chats []model.ChatItem
|
|
||||||
res := h.db.Where("user_id = ?", user.Id).Find(&chats)
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c, "No chats found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var chatIds = make([]string, 0)
|
|
||||||
for _, chat := range chats {
|
|
||||||
chatIds = append(chatIds, chat.ChatId)
|
|
||||||
// 清空会话上下文
|
|
||||||
h.App.ChatContexts.Delete(chat.ChatId)
|
|
||||||
}
|
|
||||||
err = h.db.Transaction(func(tx *gorm.DB) error {
|
|
||||||
res := h.db.Where("user_id =?", user.Id).Delete(&model.ChatItem{})
|
|
||||||
if res.Error != nil {
|
|
||||||
return res.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
res = h.db.Where("user_id = ? AND chat_id IN ?", user.Id, chatIds).Delete(&model.HistoryMessage{})
|
|
||||||
if res.Error != nil {
|
|
||||||
return res.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: 是否要删除 MidJourney 绘画记录和图片文件?
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorf("Error with delete chats: %+v", err)
|
|
||||||
resp.ERROR(c, "Failed to remove chat from database.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.SUCCESS(c, types.OkMsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// History 获取聊天历史记录
|
|
||||||
func (h *ChatHandler) History(c *gin.Context) {
|
|
||||||
chatId := c.Query("chat_id") // 会话 ID
|
|
||||||
var items []model.HistoryMessage
|
|
||||||
var messages = make([]vo.HistoryMessage, 0)
|
|
||||||
res := h.db.Where("chat_id = ?", chatId).Find(&items)
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c, "No history message")
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
for _, item := range items {
|
|
||||||
var v vo.HistoryMessage
|
|
||||||
err := utils.CopyObject(item, &v)
|
|
||||||
v.CreatedAt = item.CreatedAt.Unix()
|
|
||||||
v.UpdatedAt = item.UpdatedAt.Unix()
|
|
||||||
if err == nil {
|
|
||||||
messages = append(messages, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.SUCCESS(c, messages)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove 删除会话
|
|
||||||
func (h *ChatHandler) Remove(c *gin.Context) {
|
|
||||||
chatId := h.GetTrim(c, "chat_id")
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除当前会话的聊天记录
|
|
||||||
res = h.db.Where("user_id = ? AND chat_id =?", user.Id, chatId).Delete(&model.ChatItem{})
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c, "Failed to remove chat from database.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: 是否要删除 MidJourney 绘画记录和图片文件?
|
|
||||||
|
|
||||||
// 清空会话上下文
|
|
||||||
h.App.ChatContexts.Delete(chatId)
|
|
||||||
resp.SUCCESS(c, types.OkMsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detail 对话详情,用户导出对话
|
|
||||||
func (h *ChatHandler) Detail(c *gin.Context) {
|
|
||||||
chatId := h.GetTrim(c, "chat_id")
|
|
||||||
if utils.IsEmptyValue(chatId) {
|
|
||||||
resp.ERROR(c, "Invalid chatId")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var chatItem model.ChatItem
|
|
||||||
res := h.db.Where("chat_id = ?", chatId).First(&chatItem)
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c, "No chat found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var chatItemVo vo.ChatItem
|
|
||||||
err := utils.CopyObject(chatItem, &chatItemVo)
|
|
||||||
if err != nil {
|
|
||||||
resp.ERROR(c, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.SUCCESS(c, chatItemVo)
|
|
||||||
}
|
|
||||||
@@ -1,236 +0,0 @@
|
|||||||
package chatimpl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"chatplus/core/types"
|
|
||||||
"chatplus/store/model"
|
|
||||||
"chatplus/store/vo"
|
|
||||||
"chatplus/utils"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
"unicode/utf8"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 清华大学 ChatGML 消息发送实现
|
|
||||||
|
|
||||||
func (h *ChatHandler) sendChatGLMMessage(
|
|
||||||
chatCtx []interface{},
|
|
||||||
req types.ApiRequest,
|
|
||||||
userVo vo.User,
|
|
||||||
ctx context.Context,
|
|
||||||
session *types.ChatSession,
|
|
||||||
role model.ChatRole,
|
|
||||||
prompt string,
|
|
||||||
ws *types.WsClient) error {
|
|
||||||
promptCreatedAt := time.Now() // 记录提问时间
|
|
||||||
start := time.Now()
|
|
||||||
var apiKey = userVo.ChatConfig.ApiKeys[session.Model.Platform]
|
|
||||||
response, err := h.doRequest(ctx, req, session.Model.Platform, &apiKey)
|
|
||||||
logger.Info("HTTP请求完成,耗时:", time.Now().Sub(start))
|
|
||||||
if err != nil {
|
|
||||||
if strings.Contains(err.Error(), "context canceled") {
|
|
||||||
logger.Info("用户取消了请求:", prompt)
|
|
||||||
return nil
|
|
||||||
} else if strings.Contains(err.Error(), "no available key") {
|
|
||||||
utils.ReplyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
|
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
logger.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.ReplyMessage(ws, ErrorMsg)
|
|
||||||
utils.ReplyMessage(ws, "")
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
defer response.Body.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
contentType := response.Header.Get("Content-Type")
|
|
||||||
if strings.Contains(contentType, "text/event-stream") {
|
|
||||||
replyCreatedAt := time.Now() // 记录回复时间
|
|
||||||
// 循环读取 Chunk 消息
|
|
||||||
var message = types.Message{}
|
|
||||||
var contents = make([]string, 0)
|
|
||||||
var event, content string
|
|
||||||
scanner := bufio.NewScanner(response.Body)
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
if len(line) < 5 || strings.HasPrefix(line, "id:") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(line, "event:") {
|
|
||||||
event = line[6:]
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(line, "data:") {
|
|
||||||
content = line[5:]
|
|
||||||
}
|
|
||||||
switch event {
|
|
||||||
case "add":
|
|
||||||
if len(contents) == 0 {
|
|
||||||
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
|
|
||||||
}
|
|
||||||
utils.ReplyChunkMessage(ws, types.WsMessage{
|
|
||||||
Type: types.WsMiddle,
|
|
||||||
Content: utils.InterfaceToString(content),
|
|
||||||
})
|
|
||||||
contents = append(contents, content)
|
|
||||||
case "finish":
|
|
||||||
break
|
|
||||||
case "error":
|
|
||||||
utils.ReplyMessage(ws, fmt.Sprintf("**调用 ChatGLM API 出错:%s**", content))
|
|
||||||
break
|
|
||||||
case "interrupted":
|
|
||||||
utils.ReplyMessage(ws, "**调用 ChatGLM API 出错,当前输出被中断!**")
|
|
||||||
}
|
|
||||||
|
|
||||||
} // end for
|
|
||||||
|
|
||||||
if err := scanner.Err(); err != nil {
|
|
||||||
if strings.Contains(err.Error(), "context canceled") {
|
|
||||||
logger.Info("用户取消了请求:", prompt)
|
|
||||||
} else {
|
|
||||||
logger.Error("信息读取出错:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 消息发送成功
|
|
||||||
if len(contents) > 0 {
|
|
||||||
// 更新用户的对话次数
|
|
||||||
if userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" {
|
|
||||||
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("calls", gorm.Expr("calls - ?", 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
if message.Role == "" {
|
|
||||||
message.Role = "assistant"
|
|
||||||
}
|
|
||||||
message.Content = strings.Join(contents, "")
|
|
||||||
useMsg := types.Message{Role: "user", Content: prompt}
|
|
||||||
|
|
||||||
// 更新上下文消息,如果是调用函数则不需要更新上下文
|
|
||||||
if h.App.ChatConfig.EnableContext {
|
|
||||||
chatCtx = append(chatCtx, useMsg) // 提问消息
|
|
||||||
chatCtx = append(chatCtx, message) // 回复消息
|
|
||||||
h.App.ChatContexts.Put(session.ChatId, chatCtx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 追加聊天记录
|
|
||||||
if h.App.ChatConfig.EnableHistory {
|
|
||||||
// for prompt
|
|
||||||
promptToken, err := utils.CalcTokens(prompt, req.Model)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(err)
|
|
||||||
}
|
|
||||||
historyUserMsg := model.HistoryMessage{
|
|
||||||
UserId: userVo.Id,
|
|
||||||
ChatId: session.ChatId,
|
|
||||||
RoleId: role.Id,
|
|
||||||
Type: types.PromptMsg,
|
|
||||||
Icon: userVo.Avatar,
|
|
||||||
Content: prompt,
|
|
||||||
Tokens: promptToken,
|
|
||||||
UseContext: true,
|
|
||||||
}
|
|
||||||
historyUserMsg.CreatedAt = promptCreatedAt
|
|
||||||
historyUserMsg.UpdatedAt = promptCreatedAt
|
|
||||||
res := h.db.Save(&historyUserMsg)
|
|
||||||
if res.Error != nil {
|
|
||||||
logger.Error("failed to save prompt history message: ", res.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// for reply
|
|
||||||
// 计算本次对话消耗的总 token 数量
|
|
||||||
replyToken, _ := utils.CalcTokens(message.Content, req.Model)
|
|
||||||
totalTokens := replyToken + getTotalTokens(req)
|
|
||||||
historyReplyMsg := model.HistoryMessage{
|
|
||||||
UserId: userVo.Id,
|
|
||||||
ChatId: session.ChatId,
|
|
||||||
RoleId: role.Id,
|
|
||||||
Type: types.ReplyMsg,
|
|
||||||
Icon: role.Icon,
|
|
||||||
Content: message.Content,
|
|
||||||
Tokens: totalTokens,
|
|
||||||
UseContext: true,
|
|
||||||
}
|
|
||||||
historyReplyMsg.CreatedAt = replyCreatedAt
|
|
||||||
historyReplyMsg.UpdatedAt = replyCreatedAt
|
|
||||||
res = h.db.Create(&historyReplyMsg)
|
|
||||||
if res.Error != nil {
|
|
||||||
logger.Error("failed to save reply history message: ", res.Error)
|
|
||||||
}
|
|
||||||
// 更新用户信息
|
|
||||||
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).
|
|
||||||
UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", totalTokens))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存当前会话
|
|
||||||
var chatItem model.ChatItem
|
|
||||||
res := h.db.Where("chat_id = ?", session.ChatId).First(&chatItem)
|
|
||||||
if res.Error != nil {
|
|
||||||
chatItem.ChatId = session.ChatId
|
|
||||||
chatItem.UserId = session.UserId
|
|
||||||
chatItem.RoleId = role.Id
|
|
||||||
chatItem.ModelId = session.Model.Id
|
|
||||||
if utf8.RuneCountInString(prompt) > 30 {
|
|
||||||
chatItem.Title = string([]rune(prompt)[:30]) + "..."
|
|
||||||
} else {
|
|
||||||
chatItem.Title = prompt
|
|
||||||
}
|
|
||||||
h.db.Create(&chatItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
body, err := io.ReadAll(response.Body)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error with reading response: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var res struct {
|
|
||||||
Code int `json:"code"`
|
|
||||||
Success bool `json:"success"`
|
|
||||||
Msg string `json:"msg"`
|
|
||||||
}
|
|
||||||
err = json.Unmarshal(body, &res)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error with decode response: %v", err)
|
|
||||||
}
|
|
||||||
if !res.Success {
|
|
||||||
utils.ReplyMessage(ws, "请求 ChatGLM 失败:"+res.Msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ChatHandler) getChatGLMToken(apiKey string) (string, error) {
|
|
||||||
ctx := context.Background()
|
|
||||||
tokenString, err := h.redis.Get(ctx, apiKey).Result()
|
|
||||||
if err == nil {
|
|
||||||
return tokenString, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
expr := time.Hour * 2
|
|
||||||
key := strings.Split(apiKey, ".")
|
|
||||||
if len(key) != 2 {
|
|
||||||
return "", fmt.Errorf("invalid api key: %s", apiKey)
|
|
||||||
}
|
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
|
||||||
"api_key": key[0],
|
|
||||||
"timestamp": time.Now().Unix(),
|
|
||||||
"exp": time.Now().Add(expr).Add(time.Second * 10).Unix(),
|
|
||||||
})
|
|
||||||
token.Header["alg"] = "HS256"
|
|
||||||
token.Header["sign_type"] = "SIGN"
|
|
||||||
delete(token.Header, "typ")
|
|
||||||
// Sign and get the complete encoded token as a string using the secret
|
|
||||||
tokenString, err = token.SignedString([]byte(key[1]))
|
|
||||||
h.redis.Set(ctx, apiKey, tokenString, expr)
|
|
||||||
return tokenString, err
|
|
||||||
}
|
|
||||||
@@ -1,295 +0,0 @@
|
|||||||
package chatimpl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"chatplus/core/types"
|
|
||||||
"chatplus/store/model"
|
|
||||||
"chatplus/store/vo"
|
|
||||||
"chatplus/utils"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
"unicode/utf8"
|
|
||||||
)
|
|
||||||
|
|
||||||
// OPenAI 消息发送实现
|
|
||||||
func (h *ChatHandler) sendOpenAiMessage(
|
|
||||||
chatCtx []interface{},
|
|
||||||
req types.ApiRequest,
|
|
||||||
userVo vo.User,
|
|
||||||
ctx context.Context,
|
|
||||||
session *types.ChatSession,
|
|
||||||
role model.ChatRole,
|
|
||||||
prompt string,
|
|
||||||
ws *types.WsClient) error {
|
|
||||||
promptCreatedAt := time.Now() // 记录提问时间
|
|
||||||
start := time.Now()
|
|
||||||
var apiKey = userVo.ChatConfig.ApiKeys[session.Model.Platform]
|
|
||||||
response, err := h.doRequest(ctx, req, session.Model.Platform, &apiKey)
|
|
||||||
logger.Info("HTTP请求完成,耗时:", time.Now().Sub(start))
|
|
||||||
if err != nil {
|
|
||||||
if strings.Contains(err.Error(), "context canceled") {
|
|
||||||
logger.Info("用户取消了请求:", prompt)
|
|
||||||
return nil
|
|
||||||
} else if strings.Contains(err.Error(), "no available key") {
|
|
||||||
utils.ReplyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
|
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
logger.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.ReplyMessage(ws, ErrorMsg)
|
|
||||||
utils.ReplyMessage(ws, "")
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
defer response.Body.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
contentType := response.Header.Get("Content-Type")
|
|
||||||
if strings.Contains(contentType, "text/event-stream") {
|
|
||||||
replyCreatedAt := time.Now() // 记录回复时间
|
|
||||||
// 循环读取 Chunk 消息
|
|
||||||
var message = types.Message{}
|
|
||||||
var contents = make([]string, 0)
|
|
||||||
var functionCall = false
|
|
||||||
var functionName string
|
|
||||||
var arguments = make([]string, 0)
|
|
||||||
scanner := bufio.NewScanner(response.Body)
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
if !strings.Contains(line, "data:") || len(line) < 30 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var responseBody = types.ApiResponse{}
|
|
||||||
err = json.Unmarshal([]byte(line[6:]), &responseBody)
|
|
||||||
if err != nil || len(responseBody.Choices) == 0 { // 数据解析出错
|
|
||||||
logger.Error(err, line)
|
|
||||||
utils.ReplyMessage(ws, ErrorMsg)
|
|
||||||
utils.ReplyMessage(ws, "")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
fun := responseBody.Choices[0].Delta.FunctionCall
|
|
||||||
if functionCall && fun.Name == "" {
|
|
||||||
arguments = append(arguments, fun.Arguments)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !utils.IsEmptyValue(fun) {
|
|
||||||
functionName = fun.Name
|
|
||||||
f := h.App.Functions[functionName]
|
|
||||||
if f != nil {
|
|
||||||
functionCall = true
|
|
||||||
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
|
|
||||||
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: fmt.Sprintf("正在调用函数 `%s` 作答 ...\n\n", f.Name())})
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if responseBody.Choices[0].FinishReason == "function_call" { // 函数调用完毕
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化 role
|
|
||||||
if responseBody.Choices[0].Delta.Role != "" && message.Role == "" {
|
|
||||||
message.Role = responseBody.Choices[0].Delta.Role
|
|
||||||
utils.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, utils.InterfaceToString(content))
|
|
||||||
utils.ReplyChunkMessage(ws, types.WsMessage{
|
|
||||||
Type: types.WsMiddle,
|
|
||||||
Content: utils.InterfaceToString(responseBody.Choices[0].Delta.Content),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} // end for
|
|
||||||
|
|
||||||
if err := scanner.Err(); err != nil {
|
|
||||||
if strings.Contains(err.Error(), "context canceled") {
|
|
||||||
logger.Info("用户取消了请求:", prompt)
|
|
||||||
} else {
|
|
||||||
logger.Error("信息读取出错:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if functionCall { // 调用函数完成任务
|
|
||||||
var params map[string]interface{}
|
|
||||||
_ = utils.JsonDecode(strings.Join(arguments, ""), ¶ms)
|
|
||||||
logger.Debugf("函数名称: %s, 函数参数:%s", functionName, params)
|
|
||||||
|
|
||||||
// for creating image, check if the user's img_calls > 0
|
|
||||||
if functionName == types.FuncMidJourney && userVo.ImgCalls <= 0 {
|
|
||||||
utils.ReplyMessage(ws, "**当前用户剩余绘图次数已用尽,请扫描下面二维码联系管理员!**")
|
|
||||||
utils.ReplyMessage(ws, "")
|
|
||||||
} else {
|
|
||||||
f := h.App.Functions[functionName]
|
|
||||||
if functionName == types.FuncMidJourney {
|
|
||||||
params["user_id"] = userVo.Id
|
|
||||||
params["role_id"] = role.Id
|
|
||||||
params["chat_id"] = session.ChatId
|
|
||||||
params["icon"] = "/images/avatar/mid_journey.png"
|
|
||||||
params["session_id"] = session.SessionId
|
|
||||||
}
|
|
||||||
data, err := f.Invoke(params)
|
|
||||||
if err != nil {
|
|
||||||
msg := "调用函数出错:" + err.Error()
|
|
||||||
utils.ReplyChunkMessage(ws, types.WsMessage{
|
|
||||||
Type: types.WsMiddle,
|
|
||||||
Content: msg,
|
|
||||||
})
|
|
||||||
contents = append(contents, msg)
|
|
||||||
} else {
|
|
||||||
content := data
|
|
||||||
if functionName == types.FuncMidJourney {
|
|
||||||
content = fmt.Sprintf("绘画提示词:%s 已推送任务到 MidJourney 机器人,请耐心等待任务执行...", data)
|
|
||||||
h.mjService.ChatClients.Put(session.SessionId, ws)
|
|
||||||
// update user's img_calls
|
|
||||||
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.ReplyChunkMessage(ws, types.WsMessage{
|
|
||||||
Type: types.WsMiddle,
|
|
||||||
Content: content,
|
|
||||||
})
|
|
||||||
contents = append(contents, content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 消息发送成功
|
|
||||||
if len(contents) > 0 {
|
|
||||||
// 更新用户的对话次数
|
|
||||||
if userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" {
|
|
||||||
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("calls", gorm.Expr("calls - ?", 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
if message.Role == "" {
|
|
||||||
message.Role = "assistant"
|
|
||||||
}
|
|
||||||
message.Content = strings.Join(contents, "")
|
|
||||||
useMsg := types.Message{Role: "user", Content: prompt}
|
|
||||||
|
|
||||||
// 更新上下文消息,如果是调用函数则不需要更新上下文
|
|
||||||
if h.App.ChatConfig.EnableContext && functionCall == false {
|
|
||||||
chatCtx = append(chatCtx, useMsg) // 提问消息
|
|
||||||
chatCtx = append(chatCtx, message) // 回复消息
|
|
||||||
h.App.ChatContexts.Put(session.ChatId, chatCtx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 追加聊天记录
|
|
||||||
if h.App.ChatConfig.EnableHistory {
|
|
||||||
useContext := true
|
|
||||||
if functionCall {
|
|
||||||
useContext = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// for prompt
|
|
||||||
promptToken, err := utils.CalcTokens(prompt, req.Model)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(err)
|
|
||||||
}
|
|
||||||
historyUserMsg := model.HistoryMessage{
|
|
||||||
UserId: userVo.Id,
|
|
||||||
ChatId: session.ChatId,
|
|
||||||
RoleId: role.Id,
|
|
||||||
Type: types.PromptMsg,
|
|
||||||
Icon: userVo.Avatar,
|
|
||||||
Content: prompt,
|
|
||||||
Tokens: promptToken,
|
|
||||||
UseContext: useContext,
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算本次对话消耗的总 token 数量
|
|
||||||
var totalTokens = 0
|
|
||||||
if functionCall { // prompt + 函数名 + 参数 token
|
|
||||||
tokens, _ := utils.CalcTokens(functionName, req.Model)
|
|
||||||
totalTokens += tokens
|
|
||||||
tokens, _ = utils.CalcTokens(utils.InterfaceToString(arguments), req.Model)
|
|
||||||
totalTokens += tokens
|
|
||||||
} else {
|
|
||||||
totalTokens, _ = utils.CalcTokens(message.Content, req.Model)
|
|
||||||
}
|
|
||||||
totalTokens += getTotalTokens(req)
|
|
||||||
|
|
||||||
historyReplyMsg := model.HistoryMessage{
|
|
||||||
UserId: userVo.Id,
|
|
||||||
ChatId: session.ChatId,
|
|
||||||
RoleId: role.Id,
|
|
||||||
Type: types.ReplyMsg,
|
|
||||||
Icon: role.Icon,
|
|
||||||
Content: message.Content,
|
|
||||||
Tokens: totalTokens,
|
|
||||||
UseContext: useContext,
|
|
||||||
}
|
|
||||||
historyReplyMsg.CreatedAt = replyCreatedAt
|
|
||||||
historyReplyMsg.UpdatedAt = replyCreatedAt
|
|
||||||
res = h.db.Create(&historyReplyMsg)
|
|
||||||
if res.Error != nil {
|
|
||||||
logger.Error("failed to save reply history message: ", res.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新用户信息
|
|
||||||
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).
|
|
||||||
UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", totalTokens))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存当前会话
|
|
||||||
var chatItem model.ChatItem
|
|
||||||
res := h.db.Where("chat_id = ?", session.ChatId).First(&chatItem)
|
|
||||||
if res.Error != nil {
|
|
||||||
chatItem.ChatId = session.ChatId
|
|
||||||
chatItem.UserId = session.UserId
|
|
||||||
chatItem.RoleId = role.Id
|
|
||||||
chatItem.ModelId = session.Model.Id
|
|
||||||
if utf8.RuneCountInString(prompt) > 30 {
|
|
||||||
chatItem.Title = string([]rune(prompt)[:30]) + "..."
|
|
||||||
} else {
|
|
||||||
chatItem.Title = prompt
|
|
||||||
}
|
|
||||||
h.db.Create(&chatItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
body, err := io.ReadAll(response.Body)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error with reading response: %v", err)
|
|
||||||
}
|
|
||||||
var res types.ApiError
|
|
||||||
err = json.Unmarshal(body, &res)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error with decode response: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenAI API 调用异常处理
|
|
||||||
if strings.Contains(res.Error.Message, "This key is associated with a deactivated account") {
|
|
||||||
utils.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") {
|
|
||||||
utils.ReplyMessage(ws, "请求 OpenAI API 失败:API KEY 触发并发限制,请稍后再试。")
|
|
||||||
} else if strings.Contains(res.Error.Message, "This model's maximum context length") {
|
|
||||||
logger.Error(res.Error.Message)
|
|
||||||
utils.ReplyMessage(ws, "当前会话上下文长度超出限制,已为您清空会话上下文!")
|
|
||||||
h.App.ChatContexts.Delete(session.ChatId)
|
|
||||||
return h.sendMessage(ctx, session, role, prompt, ws)
|
|
||||||
} else {
|
|
||||||
utils.ReplyMessage(ws, "请求 OpenAI API 失败:"+res.Error.Message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,322 +0,0 @@
|
|||||||
package chatimpl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core/types"
|
|
||||||
"chatplus/store/model"
|
|
||||||
"chatplus/store/vo"
|
|
||||||
"chatplus/utils"
|
|
||||||
"context"
|
|
||||||
"crypto/hmac"
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
"unicode/utf8"
|
|
||||||
)
|
|
||||||
|
|
||||||
type xunFeiResp struct {
|
|
||||||
Header struct {
|
|
||||||
Code int `json:"code"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Sid string `json:"sid"`
|
|
||||||
Status int `json:"status"`
|
|
||||||
} `json:"header"`
|
|
||||||
Payload struct {
|
|
||||||
Choices struct {
|
|
||||||
Status int `json:"status"`
|
|
||||||
Seq int `json:"seq"`
|
|
||||||
Text []struct {
|
|
||||||
Content string `json:"content"`
|
|
||||||
Role string `json:"role"`
|
|
||||||
Index int `json:"index"`
|
|
||||||
} `json:"text"`
|
|
||||||
} `json:"choices"`
|
|
||||||
Usage struct {
|
|
||||||
Text struct {
|
|
||||||
QuestionTokens int `json:"question_tokens"`
|
|
||||||
PromptTokens int `json:"prompt_tokens"`
|
|
||||||
CompletionTokens int `json:"completion_tokens"`
|
|
||||||
TotalTokens int `json:"total_tokens"`
|
|
||||||
} `json:"text"`
|
|
||||||
} `json:"usage"`
|
|
||||||
} `json:"payload"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 科大讯飞消息发送实现
|
|
||||||
|
|
||||||
func (h *ChatHandler) sendXunFeiMessage(
|
|
||||||
chatCtx []interface{},
|
|
||||||
req types.ApiRequest,
|
|
||||||
userVo vo.User,
|
|
||||||
ctx context.Context,
|
|
||||||
session *types.ChatSession,
|
|
||||||
role model.ChatRole,
|
|
||||||
prompt string,
|
|
||||||
ws *types.WsClient) error {
|
|
||||||
promptCreatedAt := time.Now() // 记录提问时间
|
|
||||||
var apiKey = userVo.ChatConfig.ApiKeys[session.Model.Platform]
|
|
||||||
if apiKey == "" {
|
|
||||||
var key model.ApiKey
|
|
||||||
res := h.db.Where("platform = ?", session.Model.Platform).Order("last_used_at ASC").First(&key)
|
|
||||||
if res.Error != nil {
|
|
||||||
utils.ReplyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// 更新 API KEY 的最后使用时间
|
|
||||||
h.db.Model(&key).UpdateColumn("last_used_at", time.Now().Unix())
|
|
||||||
apiKey = key.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
d := websocket.Dialer{
|
|
||||||
HandshakeTimeout: 5 * time.Second,
|
|
||||||
}
|
|
||||||
key := strings.Split(apiKey, "|")
|
|
||||||
if len(key) != 3 {
|
|
||||||
utils.ReplyMessage(ws, "非法的 API KEY!")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var apiURL string
|
|
||||||
if req.Model == "generalv2" {
|
|
||||||
apiURL = strings.Replace(h.App.ChatConfig.XunFei.ApiURL, "{version}", "v2.1", 1)
|
|
||||||
} else {
|
|
||||||
apiURL = strings.Replace(h.App.ChatConfig.XunFei.ApiURL, "{version}", "v1.1", 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
wsURL, err := assembleAuthUrl(apiURL, key[1], key[2])
|
|
||||||
//握手并建立websocket 连接
|
|
||||||
conn, resp, err := d.Dial(wsURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(readResp(resp) + err.Error())
|
|
||||||
utils.ReplyMessage(ws, "请求讯飞星火模型 API 失败:"+readResp(resp)+err.Error())
|
|
||||||
return nil
|
|
||||||
} else if resp.StatusCode != 101 {
|
|
||||||
utils.ReplyMessage(ws, "请求讯飞星火模型 API 失败:"+readResp(resp)+err.Error())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
data := buildRequest(key[0], req)
|
|
||||||
fmt.Printf("%+v", data)
|
|
||||||
fmt.Println(apiURL)
|
|
||||||
err = conn.WriteJSON(data)
|
|
||||||
if err != nil {
|
|
||||||
utils.ReplyMessage(ws, "发送消息失败:"+err.Error())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
replyCreatedAt := time.Now() // 记录回复时间
|
|
||||||
// 循环读取 Chunk 消息
|
|
||||||
var message = types.Message{}
|
|
||||||
var contents = make([]string, 0)
|
|
||||||
var content string
|
|
||||||
for {
|
|
||||||
_, msg, err := conn.ReadMessage()
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("error with read message:", err)
|
|
||||||
utils.ReplyMessage(ws, fmt.Sprintf("**数据读取失败:%s**", err))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析数据
|
|
||||||
var result xunFeiResp
|
|
||||||
err = json.Unmarshal(msg, &result)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("error with parsing JSON:", err)
|
|
||||||
utils.ReplyMessage(ws, fmt.Sprintf("**解析数据行失败:%s**", err))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if result.Header.Code != 0 {
|
|
||||||
utils.ReplyMessage(ws, fmt.Sprintf("**请求 API 返回错误:%s**", result.Header.Message))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
content = result.Payload.Choices.Text[0].Content
|
|
||||||
contents = append(contents, content)
|
|
||||||
// 第一个结果
|
|
||||||
if result.Payload.Choices.Status == 0 {
|
|
||||||
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
|
|
||||||
}
|
|
||||||
utils.ReplyChunkMessage(ws, types.WsMessage{
|
|
||||||
Type: types.WsMiddle,
|
|
||||||
Content: utils.InterfaceToString(content),
|
|
||||||
})
|
|
||||||
|
|
||||||
if result.Payload.Choices.Status == 2 { // 最终结果
|
|
||||||
_ = conn.Close() // 关闭连接
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
utils.ReplyMessage(ws, "**用户取消了生成指令!**")
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// 消息发送成功
|
|
||||||
if len(contents) > 0 {
|
|
||||||
// 更新用户的对话次数
|
|
||||||
if userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" {
|
|
||||||
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("calls", gorm.Expr("calls - ?", 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
if message.Role == "" {
|
|
||||||
message.Role = "assistant"
|
|
||||||
}
|
|
||||||
message.Content = strings.Join(contents, "")
|
|
||||||
useMsg := types.Message{Role: "user", Content: prompt}
|
|
||||||
|
|
||||||
// 更新上下文消息,如果是调用函数则不需要更新上下文
|
|
||||||
if h.App.ChatConfig.EnableContext {
|
|
||||||
chatCtx = append(chatCtx, useMsg) // 提问消息
|
|
||||||
chatCtx = append(chatCtx, message) // 回复消息
|
|
||||||
h.App.ChatContexts.Put(session.ChatId, chatCtx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 追加聊天记录
|
|
||||||
if h.App.ChatConfig.EnableHistory {
|
|
||||||
// for prompt
|
|
||||||
promptToken, err := utils.CalcTokens(prompt, req.Model)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(err)
|
|
||||||
}
|
|
||||||
historyUserMsg := model.HistoryMessage{
|
|
||||||
UserId: userVo.Id,
|
|
||||||
ChatId: session.ChatId,
|
|
||||||
RoleId: role.Id,
|
|
||||||
Type: types.PromptMsg,
|
|
||||||
Icon: userVo.Avatar,
|
|
||||||
Content: prompt,
|
|
||||||
Tokens: promptToken,
|
|
||||||
UseContext: true,
|
|
||||||
}
|
|
||||||
historyUserMsg.CreatedAt = promptCreatedAt
|
|
||||||
historyUserMsg.UpdatedAt = promptCreatedAt
|
|
||||||
res := h.db.Save(&historyUserMsg)
|
|
||||||
if res.Error != nil {
|
|
||||||
logger.Error("failed to save prompt history message: ", res.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// for reply
|
|
||||||
// 计算本次对话消耗的总 token 数量
|
|
||||||
replyToken, _ := utils.CalcTokens(message.Content, req.Model)
|
|
||||||
totalTokens := replyToken + getTotalTokens(req)
|
|
||||||
historyReplyMsg := model.HistoryMessage{
|
|
||||||
UserId: userVo.Id,
|
|
||||||
ChatId: session.ChatId,
|
|
||||||
RoleId: role.Id,
|
|
||||||
Type: types.ReplyMsg,
|
|
||||||
Icon: role.Icon,
|
|
||||||
Content: message.Content,
|
|
||||||
Tokens: totalTokens,
|
|
||||||
UseContext: true,
|
|
||||||
}
|
|
||||||
historyReplyMsg.CreatedAt = replyCreatedAt
|
|
||||||
historyReplyMsg.UpdatedAt = replyCreatedAt
|
|
||||||
res = h.db.Create(&historyReplyMsg)
|
|
||||||
if res.Error != nil {
|
|
||||||
logger.Error("failed to save reply history message: ", res.Error)
|
|
||||||
}
|
|
||||||
// 更新用户信息
|
|
||||||
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).
|
|
||||||
UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", totalTokens))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存当前会话
|
|
||||||
var chatItem model.ChatItem
|
|
||||||
res := h.db.Where("chat_id = ?", session.ChatId).First(&chatItem)
|
|
||||||
if res.Error != nil {
|
|
||||||
chatItem.ChatId = session.ChatId
|
|
||||||
chatItem.UserId = session.UserId
|
|
||||||
chatItem.RoleId = role.Id
|
|
||||||
chatItem.ModelId = session.Model.Id
|
|
||||||
if utf8.RuneCountInString(prompt) > 30 {
|
|
||||||
chatItem.Title = string([]rune(prompt)[:30]) + "..."
|
|
||||||
} else {
|
|
||||||
chatItem.Title = prompt
|
|
||||||
}
|
|
||||||
h.db.Create(&chatItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建 websocket 请求实体
|
|
||||||
func buildRequest(appid string, req types.ApiRequest) map[string]interface{} {
|
|
||||||
return map[string]interface{}{
|
|
||||||
"header": map[string]interface{}{
|
|
||||||
"app_id": appid,
|
|
||||||
},
|
|
||||||
"parameter": map[string]interface{}{
|
|
||||||
"chat": map[string]interface{}{
|
|
||||||
"domain": req.Model,
|
|
||||||
"temperature": float64(req.Temperature),
|
|
||||||
"top_k": int64(6),
|
|
||||||
"max_tokens": int64(req.MaxTokens),
|
|
||||||
"auditing": "default",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"payload": map[string]interface{}{
|
|
||||||
"message": map[string]interface{}{
|
|
||||||
"text": req.Messages,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建鉴权 URL
|
|
||||||
func assembleAuthUrl(hostURL string, apiKey, apiSecret string) (string, error) {
|
|
||||||
ul, err := url.Parse(hostURL)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
date := time.Now().UTC().Format(time.RFC1123)
|
|
||||||
signString := []string{"host: " + ul.Host, "date: " + date, "GET " + ul.Path + " HTTP/1.1"}
|
|
||||||
//拼接签名字符串
|
|
||||||
signStr := strings.Join(signString, "\n")
|
|
||||||
sha := hmacWithSha256(signStr, apiSecret)
|
|
||||||
|
|
||||||
authUrl := fmt.Sprintf("hmac username=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"", apiKey,
|
|
||||||
"hmac-sha256", "host date request-line", sha)
|
|
||||||
//将请求参数使用base64编码
|
|
||||||
authorization := base64.StdEncoding.EncodeToString([]byte(authUrl))
|
|
||||||
v := url.Values{}
|
|
||||||
v.Add("host", ul.Host)
|
|
||||||
v.Add("date", date)
|
|
||||||
v.Add("authorization", authorization)
|
|
||||||
//将编码后的字符串url encode后添加到url后面
|
|
||||||
return hostURL + "?" + v.Encode(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用 sha256 签名
|
|
||||||
func hmacWithSha256(data, key string) string {
|
|
||||||
mac := hmac.New(sha256.New, []byte(key))
|
|
||||||
mac.Write([]byte(data))
|
|
||||||
encodeData := mac.Sum(nil)
|
|
||||||
return base64.StdEncoding.EncodeToString(encodeData)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取响应
|
|
||||||
func readResp(resp *http.Response) string {
|
|
||||||
if resp == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
b, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("code=%d,body=%s", resp.StatusCode, string(b))
|
|
||||||
}
|
|
||||||
@@ -1,354 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core"
|
|
||||||
"chatplus/core/types"
|
|
||||||
"chatplus/service/mj"
|
|
||||||
"chatplus/store/model"
|
|
||||||
"chatplus/store/vo"
|
|
||||||
"chatplus/utils"
|
|
||||||
"chatplus/utils/resp"
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/go-redis/redis/v8"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MidJourneyHandler struct {
|
|
||||||
BaseHandler
|
|
||||||
redis *redis.Client
|
|
||||||
db *gorm.DB
|
|
||||||
mjService *mj.Service
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMidJourneyHandler(
|
|
||||||
app *core.AppServer,
|
|
||||||
client *redis.Client,
|
|
||||||
db *gorm.DB,
|
|
||||||
mjService *mj.Service) *MidJourneyHandler {
|
|
||||||
h := MidJourneyHandler{
|
|
||||||
redis: client,
|
|
||||||
db: db,
|
|
||||||
mjService: mjService,
|
|
||||||
}
|
|
||||||
h.App = app
|
|
||||||
return &h
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client WebSocket 客户端,用于通知任务状态变更
|
|
||||||
func (h *MidJourneyHandler) Client(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("session_id")
|
|
||||||
client := types.NewWsClient(ws)
|
|
||||||
h.mjService.Clients.Put(sessionId, client)
|
|
||||||
logger.Infof("New websocket connected, IP: %s", c.ClientIP())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *MidJourneyHandler) checkLimits(c *gin.Context) bool {
|
|
||||||
user, err := utils.GetLoginUser(c, h.db)
|
|
||||||
if err != nil {
|
|
||||||
resp.NotAuth(c)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if user.ImgCalls <= 0 {
|
|
||||||
resp.ERROR(c, "您的绘图次数不足,请联系管理员充值!")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Image 创建一个绘画任务
|
|
||||||
func (h *MidJourneyHandler) Image(c *gin.Context) {
|
|
||||||
if !h.App.Config.MjConfig.Enabled {
|
|
||||||
resp.ERROR(c, "MidJourney service is disabled")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var data struct {
|
|
||||||
SessionId string `json:"session_id"`
|
|
||||||
Prompt string `json:"prompt"`
|
|
||||||
Rate string `json:"rate"`
|
|
||||||
Model string `json:"model"`
|
|
||||||
Chaos int `json:"chaos"`
|
|
||||||
Raw bool `json:"raw"`
|
|
||||||
Seed int64 `json:"seed"`
|
|
||||||
Stylize int `json:"stylize"`
|
|
||||||
Img string `json:"img"`
|
|
||||||
Weight float32 `json:"weight"`
|
|
||||||
}
|
|
||||||
if err := c.ShouldBindJSON(&data); err != nil {
|
|
||||||
resp.ERROR(c, types.InvalidArgs)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !h.checkLimits(c) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var prompt = data.Prompt
|
|
||||||
if data.Rate != "" && !strings.Contains(prompt, "--ar") {
|
|
||||||
prompt += " --ar " + data.Rate
|
|
||||||
}
|
|
||||||
if data.Seed > 0 && !strings.Contains(prompt, "--seed") {
|
|
||||||
prompt += fmt.Sprintf(" --seed %d", data.Seed)
|
|
||||||
}
|
|
||||||
if data.Stylize > 0 && !strings.Contains(prompt, "--s") && !strings.Contains(prompt, "--stylize") {
|
|
||||||
prompt += fmt.Sprintf(" --s %d", data.Stylize)
|
|
||||||
}
|
|
||||||
if data.Chaos > 0 && !strings.Contains(prompt, "--c") && !strings.Contains(prompt, "--chaos") {
|
|
||||||
prompt += fmt.Sprintf(" --c %d", data.Chaos)
|
|
||||||
}
|
|
||||||
if data.Img != "" {
|
|
||||||
prompt = fmt.Sprintf("%s %s", data.Img, prompt)
|
|
||||||
if data.Weight > 0 {
|
|
||||||
prompt += fmt.Sprintf(" --iw %f", data.Weight)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if data.Raw {
|
|
||||||
prompt += " --style raw"
|
|
||||||
}
|
|
||||||
if data.Model != "" && !strings.Contains(prompt, "--v") && !strings.Contains(prompt, "--niji") {
|
|
||||||
prompt += data.Model
|
|
||||||
}
|
|
||||||
|
|
||||||
idValue, _ := c.Get(types.LoginUserID)
|
|
||||||
userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
|
|
||||||
job := model.MidJourneyJob{
|
|
||||||
Type: types.TaskImage.String(),
|
|
||||||
UserId: userId,
|
|
||||||
Progress: 0,
|
|
||||||
Prompt: prompt,
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
}
|
|
||||||
if res := h.db.Create(&job); res.Error != nil {
|
|
||||||
resp.ERROR(c, "添加任务失败:"+res.Error.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.mjService.PushTask(types.MjTask{
|
|
||||||
Id: int(job.Id),
|
|
||||||
SessionId: data.SessionId,
|
|
||||||
Src: types.TaskSrcImg,
|
|
||||||
Type: types.TaskImage,
|
|
||||||
Prompt: prompt,
|
|
||||||
UserId: userId,
|
|
||||||
})
|
|
||||||
|
|
||||||
var jobVo vo.MidJourneyJob
|
|
||||||
err := utils.CopyObject(job, &jobVo)
|
|
||||||
if err == nil {
|
|
||||||
// 推送任务到前端
|
|
||||||
client := h.mjService.Clients.Get(data.SessionId)
|
|
||||||
if client != nil {
|
|
||||||
utils.ReplyChunkMessage(client, jobVo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resp.SUCCESS(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
type reqVo struct {
|
|
||||||
Src string `json:"src"`
|
|
||||||
Index int `json:"index"`
|
|
||||||
MessageId string `json:"message_id"`
|
|
||||||
MessageHash string `json:"message_hash"`
|
|
||||||
SessionId string `json:"session_id"`
|
|
||||||
Prompt string `json:"prompt"`
|
|
||||||
ChatId string `json:"chat_id"`
|
|
||||||
RoleId int `json:"role_id"`
|
|
||||||
Icon string `json:"icon"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upscale send upscale command to MidJourney Bot
|
|
||||||
func (h *MidJourneyHandler) Upscale(c *gin.Context) {
|
|
||||||
var data reqVo
|
|
||||||
if err := c.ShouldBindJSON(&data); err != nil || data.SessionId == "" {
|
|
||||||
resp.ERROR(c, types.InvalidArgs)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !h.checkLimits(c) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
idValue, _ := c.Get(types.LoginUserID)
|
|
||||||
jobId := 0
|
|
||||||
userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
|
|
||||||
src := types.TaskSrc(data.Src)
|
|
||||||
if src == types.TaskSrcImg {
|
|
||||||
job := model.MidJourneyJob{
|
|
||||||
Type: types.TaskUpscale.String(),
|
|
||||||
UserId: userId,
|
|
||||||
Hash: data.MessageHash,
|
|
||||||
Progress: 0,
|
|
||||||
Prompt: data.Prompt,
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
}
|
|
||||||
if res := h.db.Create(&job); res.Error == nil {
|
|
||||||
jobId = int(job.Id)
|
|
||||||
} else {
|
|
||||||
resp.ERROR(c, "添加任务失败:"+res.Error.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var jobVo vo.MidJourneyJob
|
|
||||||
err := utils.CopyObject(job, &jobVo)
|
|
||||||
if err == nil {
|
|
||||||
// 推送任务到前端
|
|
||||||
client := h.mjService.Clients.Get(data.SessionId)
|
|
||||||
if client != nil {
|
|
||||||
utils.ReplyChunkMessage(client, jobVo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
h.mjService.PushTask(types.MjTask{
|
|
||||||
Id: jobId,
|
|
||||||
SessionId: data.SessionId,
|
|
||||||
Src: src,
|
|
||||||
Type: types.TaskUpscale,
|
|
||||||
Prompt: data.Prompt,
|
|
||||||
UserId: userId,
|
|
||||||
RoleId: data.RoleId,
|
|
||||||
Icon: data.Icon,
|
|
||||||
ChatId: data.ChatId,
|
|
||||||
Index: data.Index,
|
|
||||||
MessageId: data.MessageId,
|
|
||||||
MessageHash: data.MessageHash,
|
|
||||||
})
|
|
||||||
|
|
||||||
if src == types.TaskSrcChat {
|
|
||||||
wsClient := h.App.ChatClients.Get(data.SessionId)
|
|
||||||
if wsClient != nil {
|
|
||||||
content := fmt.Sprintf("**%s** 已推送 upscale 任务到 MidJourney 机器人,请耐心等待任务执行...", data.Prompt)
|
|
||||||
utils.ReplyMessage(wsClient, content)
|
|
||||||
if h.mjService.ChatClients.Get(data.SessionId) == nil {
|
|
||||||
h.mjService.ChatClients.Put(data.SessionId, wsClient)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resp.SUCCESS(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Variation send variation command to MidJourney Bot
|
|
||||||
func (h *MidJourneyHandler) Variation(c *gin.Context) {
|
|
||||||
var data reqVo
|
|
||||||
if err := c.ShouldBindJSON(&data); err != nil || data.SessionId == "" {
|
|
||||||
resp.ERROR(c, types.InvalidArgs)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !h.checkLimits(c) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
idValue, _ := c.Get(types.LoginUserID)
|
|
||||||
jobId := 0
|
|
||||||
userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
|
|
||||||
src := types.TaskSrc(data.Src)
|
|
||||||
if src == types.TaskSrcImg {
|
|
||||||
job := model.MidJourneyJob{
|
|
||||||
Type: types.TaskVariation.String(),
|
|
||||||
UserId: userId,
|
|
||||||
ImgURL: "",
|
|
||||||
Hash: data.MessageHash,
|
|
||||||
Progress: 0,
|
|
||||||
Prompt: data.Prompt,
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
}
|
|
||||||
if res := h.db.Create(&job); res.Error == nil {
|
|
||||||
jobId = int(job.Id)
|
|
||||||
} else {
|
|
||||||
resp.ERROR(c, "添加任务失败:"+res.Error.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var jobVo vo.MidJourneyJob
|
|
||||||
err := utils.CopyObject(job, &jobVo)
|
|
||||||
if err == nil {
|
|
||||||
// 推送任务到前端
|
|
||||||
client := h.mjService.Clients.Get(data.SessionId)
|
|
||||||
if client != nil {
|
|
||||||
utils.ReplyChunkMessage(client, jobVo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
h.mjService.PushTask(types.MjTask{
|
|
||||||
Id: jobId,
|
|
||||||
SessionId: data.SessionId,
|
|
||||||
Src: src,
|
|
||||||
Type: types.TaskVariation,
|
|
||||||
Prompt: data.Prompt,
|
|
||||||
UserId: userId,
|
|
||||||
RoleId: data.RoleId,
|
|
||||||
Icon: data.Icon,
|
|
||||||
ChatId: data.ChatId,
|
|
||||||
Index: data.Index,
|
|
||||||
MessageId: data.MessageId,
|
|
||||||
MessageHash: data.MessageHash,
|
|
||||||
})
|
|
||||||
|
|
||||||
if src == types.TaskSrcChat {
|
|
||||||
// 从聊天窗口发送的请求,记录客户端信息
|
|
||||||
wsClient := h.mjService.ChatClients.Get(data.SessionId)
|
|
||||||
if wsClient != nil {
|
|
||||||
content := fmt.Sprintf("**%s** 已推送 variation 任务到 MidJourney 机器人,请耐心等待任务执行...", data.Prompt)
|
|
||||||
utils.ReplyMessage(wsClient, content)
|
|
||||||
if h.mjService.Clients.Get(data.SessionId) == nil {
|
|
||||||
h.mjService.Clients.Put(data.SessionId, wsClient)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resp.SUCCESS(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// JobList 获取 MJ 任务列表
|
|
||||||
func (h *MidJourneyHandler) JobList(c *gin.Context) {
|
|
||||||
status := h.GetInt(c, "status", 0)
|
|
||||||
var items []model.MidJourneyJob
|
|
||||||
var res *gorm.DB
|
|
||||||
userId, _ := c.Get(types.LoginUserID)
|
|
||||||
if status == 1 {
|
|
||||||
res = h.db.Where("user_id = ? AND progress = 100", userId).Order("id DESC").Find(&items)
|
|
||||||
} else {
|
|
||||||
res = h.db.Where("user_id = ? AND progress < 100", userId).Order("id ASC").Find(&items)
|
|
||||||
}
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c, types.NoData)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var jobs = make([]vo.MidJourneyJob, 0)
|
|
||||||
for _, item := range items {
|
|
||||||
var job vo.MidJourneyJob
|
|
||||||
err := utils.CopyObject(item, &job)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if item.Progress < 100 {
|
|
||||||
// 30 分钟还没完成的任务直接删除
|
|
||||||
if time.Now().Sub(item.CreatedAt) > time.Minute*30 {
|
|
||||||
h.db.Delete(&item)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if item.ImgURL != "" { // 正在运行中任务使用代理访问图片
|
|
||||||
image, err := utils.DownloadImage(item.ImgURL, h.App.Config.ProxyURL)
|
|
||||||
if err == nil {
|
|
||||||
job.ImgURL = "data:image/png;base64," + base64.StdEncoding.EncodeToString(image)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
jobs = append(jobs, job)
|
|
||||||
}
|
|
||||||
resp.SUCCESS(c, jobs)
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core"
|
|
||||||
"chatplus/core/types"
|
|
||||||
"chatplus/store/model"
|
|
||||||
"chatplus/utils"
|
|
||||||
"chatplus/utils/resp"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RewardHandler struct {
|
|
||||||
BaseHandler
|
|
||||||
db *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRewardHandler(server *core.AppServer, db *gorm.DB) *RewardHandler {
|
|
||||||
h := RewardHandler{db: db}
|
|
||||||
h.App = server
|
|
||||||
return &h
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify 打赏码核销
|
|
||||||
func (h *RewardHandler) Verify(c *gin.Context) {
|
|
||||||
var data struct {
|
|
||||||
TxId string `json:"tx_id"`
|
|
||||||
}
|
|
||||||
if err := c.ShouldBindJSON(&data); err != nil {
|
|
||||||
resp.ERROR(c, types.InvalidArgs)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 移除转账单号中间的空格,防止有人复制的时候多复制了空格
|
|
||||||
data.TxId = strings.ReplaceAll(data.TxId, " ", "")
|
|
||||||
|
|
||||||
var item model.Reward
|
|
||||||
res := h.db.Where("tx_id = ?", data.TxId).First(&item)
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c, "无效的众筹交易流水号!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if item.Status {
|
|
||||||
resp.ERROR(c, "当前众筹交易流水号已经被核销,请不要重复核销!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := utils.GetLoginUser(c, h.db)
|
|
||||||
if err != nil {
|
|
||||||
resp.HACKER(c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tx := h.db.Begin()
|
|
||||||
calls := (item.Amount + 0.1) * 10
|
|
||||||
res = h.db.Model(&user).UpdateColumn("calls", gorm.Expr("calls + ?", calls))
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c, "更新数据库失败!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新核销状态
|
|
||||||
item.Status = true
|
|
||||||
item.UserId = user.Id
|
|
||||||
res = h.db.Updates(&item)
|
|
||||||
if res.Error != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
resp.ERROR(c, "更新数据库失败!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tx.Commit()
|
|
||||||
resp.SUCCESS(c)
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core"
|
|
||||||
"chatplus/core/types"
|
|
||||||
"chatplus/service/sd"
|
|
||||||
"chatplus/store/model"
|
|
||||||
"chatplus/store/vo"
|
|
||||||
"chatplus/utils"
|
|
||||||
"chatplus/utils/resp"
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/go-redis/redis/v8"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SdJobHandler struct {
|
|
||||||
BaseHandler
|
|
||||||
redis *redis.Client
|
|
||||||
db *gorm.DB
|
|
||||||
service *sd.Service
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSdJobHandler(app *core.AppServer, redisCli *redis.Client, db *gorm.DB, service *sd.Service) *SdJobHandler {
|
|
||||||
h := SdJobHandler{
|
|
||||||
redis: redisCli,
|
|
||||||
db: db,
|
|
||||||
service: service,
|
|
||||||
}
|
|
||||||
h.App = app
|
|
||||||
return &h
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client WebSocket 客户端,用于通知任务状态变更
|
|
||||||
func (h *SdJobHandler) Client(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("session_id")
|
|
||||||
client := types.NewWsClient(ws)
|
|
||||||
// 删除旧的连接
|
|
||||||
h.service.Clients.Put(sessionId, client)
|
|
||||||
logger.Infof("New websocket connected, IP: %s", c.ClientIP())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *SdJobHandler) checkLimits(c *gin.Context) bool {
|
|
||||||
user, err := utils.GetLoginUser(c, h.db)
|
|
||||||
if err != nil {
|
|
||||||
resp.NotAuth(c)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if user.ImgCalls <= 0 {
|
|
||||||
resp.ERROR(c, "您的绘图次数不足,请联系管理员充值!")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Image 创建一个绘画任务
|
|
||||||
func (h *SdJobHandler) Image(c *gin.Context) {
|
|
||||||
if !h.App.Config.SdConfig.Enabled {
|
|
||||||
resp.ERROR(c, "Stable Diffusion service is disabled")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !h.checkLimits(c) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var data struct {
|
|
||||||
SessionId string `json:"session_id"`
|
|
||||||
types.SdTaskParams
|
|
||||||
}
|
|
||||||
if err := c.ShouldBindJSON(&data); err != nil || data.Prompt == "" {
|
|
||||||
resp.ERROR(c, types.InvalidArgs)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if data.Width <= 0 {
|
|
||||||
data.Width = 512
|
|
||||||
}
|
|
||||||
if data.Height <= 0 {
|
|
||||||
data.Height = 512
|
|
||||||
}
|
|
||||||
if data.CfgScale <= 0 {
|
|
||||||
data.CfgScale = 7
|
|
||||||
}
|
|
||||||
if data.Seed == 0 {
|
|
||||||
data.Seed = -1
|
|
||||||
}
|
|
||||||
if data.Steps <= 0 {
|
|
||||||
data.Steps = 20
|
|
||||||
}
|
|
||||||
if data.Sampler == "" {
|
|
||||||
data.Sampler = "Euler a"
|
|
||||||
}
|
|
||||||
idValue, _ := c.Get(types.LoginUserID)
|
|
||||||
userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
|
|
||||||
params := types.SdTaskParams{
|
|
||||||
TaskId: fmt.Sprintf("task(%s)", utils.RandString(15)),
|
|
||||||
Prompt: data.Prompt,
|
|
||||||
NegativePrompt: data.NegativePrompt,
|
|
||||||
Steps: data.Steps,
|
|
||||||
Sampler: data.Sampler,
|
|
||||||
FaceFix: data.FaceFix,
|
|
||||||
CfgScale: data.CfgScale,
|
|
||||||
Seed: data.Seed,
|
|
||||||
Height: data.Height,
|
|
||||||
Width: data.Width,
|
|
||||||
HdFix: data.HdFix,
|
|
||||||
HdRedrawRate: data.HdRedrawRate,
|
|
||||||
HdScale: data.HdScale,
|
|
||||||
HdScaleAlg: data.HdScaleAlg,
|
|
||||||
HdSteps: data.HdSteps,
|
|
||||||
}
|
|
||||||
job := model.SdJob{
|
|
||||||
UserId: userId,
|
|
||||||
Type: types.TaskImage.String(),
|
|
||||||
TaskId: params.TaskId,
|
|
||||||
Params: utils.JsonEncode(params),
|
|
||||||
Prompt: data.Prompt,
|
|
||||||
Progress: 0,
|
|
||||||
Started: false,
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
}
|
|
||||||
res := h.db.Create(&job)
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c, "error with save job: "+res.Error.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.service.PushTask(types.SdTask{
|
|
||||||
Id: int(job.Id),
|
|
||||||
SessionId: data.SessionId,
|
|
||||||
Src: types.TaskSrcImg,
|
|
||||||
Type: types.TaskImage,
|
|
||||||
Prompt: data.Prompt,
|
|
||||||
Params: params,
|
|
||||||
UserId: userId,
|
|
||||||
})
|
|
||||||
var jobVo vo.SdJob
|
|
||||||
err := utils.CopyObject(job, &jobVo)
|
|
||||||
if err == nil {
|
|
||||||
// 推送任务到前端
|
|
||||||
client := h.service.Clients.Get(data.SessionId)
|
|
||||||
if client != nil {
|
|
||||||
utils.ReplyChunkMessage(client, jobVo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resp.SUCCESS(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// JobList 获取 MJ 任务列表
|
|
||||||
func (h *SdJobHandler) JobList(c *gin.Context) {
|
|
||||||
status := h.GetInt(c, "status", 0)
|
|
||||||
var items []model.SdJob
|
|
||||||
var res *gorm.DB
|
|
||||||
userId, _ := c.Get(types.LoginUserID)
|
|
||||||
if status == 1 {
|
|
||||||
res = h.db.Where("user_id = ? AND progress = 100", userId).Order("id DESC").Find(&items)
|
|
||||||
} else {
|
|
||||||
res = h.db.Where("user_id = ? AND progress < 100", userId).Order("id ASC").Find(&items)
|
|
||||||
}
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c, types.NoData)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var jobs = make([]vo.SdJob, 0)
|
|
||||||
for _, item := range items {
|
|
||||||
var job vo.SdJob
|
|
||||||
err := utils.CopyObject(item, &job)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if item.Progress < 100 {
|
|
||||||
// 30 分钟还没完成的任务直接删除
|
|
||||||
if time.Now().Sub(item.CreatedAt) > time.Minute*30 {
|
|
||||||
h.db.Delete(&item)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if item.ImgURL != "" { // 正在运行中任务使用代理访问图片
|
|
||||||
image, err := utils.DownloadImage(item.ImgURL, h.App.Config.ProxyURL)
|
|
||||||
if err == nil {
|
|
||||||
job.ImgURL = "data:image/png;base64," + base64.StdEncoding.EncodeToString(image)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
jobs = append(jobs, job)
|
|
||||||
}
|
|
||||||
resp.SUCCESS(c, jobs)
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core"
|
|
||||||
"chatplus/core/types"
|
|
||||||
"chatplus/service"
|
|
||||||
"chatplus/store"
|
|
||||||
"chatplus/utils"
|
|
||||||
"chatplus/utils/resp"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
const CodeStorePrefix = "/verify/codes/"
|
|
||||||
|
|
||||||
type SmsHandler struct {
|
|
||||||
BaseHandler
|
|
||||||
leveldb *store.LevelDB
|
|
||||||
sms *service.AliYunSmsService
|
|
||||||
captcha *service.CaptchaService
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSmsHandler(app *core.AppServer, db *store.LevelDB, sms *service.AliYunSmsService, captcha *service.CaptchaService) *SmsHandler {
|
|
||||||
handler := &SmsHandler{leveldb: db, sms: sms, captcha: captcha}
|
|
||||||
handler.App = app
|
|
||||||
return handler
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendCode 发送验证码短信
|
|
||||||
func (h *SmsHandler) SendCode(c *gin.Context) {
|
|
||||||
var data struct {
|
|
||||||
Mobile string `json:"mobile"`
|
|
||||||
Key string `json:"key"`
|
|
||||||
Dots string `json:"dots"`
|
|
||||||
}
|
|
||||||
if err := c.ShouldBindJSON(&data); err != nil {
|
|
||||||
resp.ERROR(c, types.InvalidArgs)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !h.captcha.Check(data) {
|
|
||||||
resp.ERROR(c, "验证码错误,请先完人机验证")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
code := utils.RandomNumber(6)
|
|
||||||
err := h.sms.SendVerifyCode(data.Mobile, code)
|
|
||||||
if err != nil {
|
|
||||||
resp.ERROR(c, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 存储验证码,等待后面注册验证
|
|
||||||
err = h.leveldb.Put(CodeStorePrefix+data.Mobile, code)
|
|
||||||
if err != nil {
|
|
||||||
resp.ERROR(c, "验证码保存失败")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.SUCCESS(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
type statusVo struct {
|
|
||||||
EnabledMsgService bool `json:"enabled_msg_service"`
|
|
||||||
EnabledRegister bool `json:"enabled_register"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status check if the message service is enabled
|
|
||||||
func (h *SmsHandler) Status(c *gin.Context) {
|
|
||||||
resp.SUCCESS(c, statusVo{EnabledMsgService: h.App.SysConfig.EnabledMsg, EnabledRegister: h.App.SysConfig.EnabledRegister})
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core"
|
|
||||||
"chatplus/service/oss"
|
|
||||||
"chatplus/utils/resp"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UploadHandler struct {
|
|
||||||
BaseHandler
|
|
||||||
db *gorm.DB
|
|
||||||
uploaderManager *oss.UploaderManager
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewUploadHandler(app *core.AppServer, db *gorm.DB, manager *oss.UploaderManager) *UploadHandler {
|
|
||||||
handler := &UploadHandler{db: db, uploaderManager: manager}
|
|
||||||
handler.App = app
|
|
||||||
return handler
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *UploadHandler) Upload(c *gin.Context) {
|
|
||||||
fileURL, err := h.uploaderManager.GetUploadHandler().PutFile(c, "file")
|
|
||||||
if err != nil {
|
|
||||||
resp.ERROR(c, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.SUCCESS(c, fileURL)
|
|
||||||
}
|
|
||||||
@@ -1,351 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core"
|
|
||||||
"chatplus/core/types"
|
|
||||||
"chatplus/store"
|
|
||||||
"chatplus/store/model"
|
|
||||||
"chatplus/store/vo"
|
|
||||||
"chatplus/utils"
|
|
||||||
"chatplus/utils/resp"
|
|
||||||
"fmt"
|
|
||||||
"github.com/go-redis/redis/v8"
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UserHandler struct {
|
|
||||||
BaseHandler
|
|
||||||
db *gorm.DB
|
|
||||||
searcher *xdb.Searcher
|
|
||||||
leveldb *store.LevelDB
|
|
||||||
redis *redis.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewUserHandler(
|
|
||||||
app *core.AppServer,
|
|
||||||
db *gorm.DB,
|
|
||||||
searcher *xdb.Searcher,
|
|
||||||
levelDB *store.LevelDB,
|
|
||||||
client *redis.Client) *UserHandler {
|
|
||||||
handler := &UserHandler{db: db, searcher: searcher, leveldb: levelDB, redis: client}
|
|
||||||
handler.App = app
|
|
||||||
return handler
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register user register
|
|
||||||
func (h *UserHandler) Register(c *gin.Context) {
|
|
||||||
// parameters process
|
|
||||||
var data struct {
|
|
||||||
Mobile string `json:"mobile"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
Code int `json:"code"`
|
|
||||||
}
|
|
||||||
if err := c.ShouldBindJSON(&data); err != nil {
|
|
||||||
resp.ERROR(c, types.InvalidArgs)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data.Password = strings.TrimSpace(data.Password)
|
|
||||||
|
|
||||||
if len(data.Mobile) < 10 {
|
|
||||||
resp.ERROR(c, "请输入合法的手机号")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(data.Password) < 8 {
|
|
||||||
resp.ERROR(c, "密码长度不能少于8个字符")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查验证码
|
|
||||||
key := CodeStorePrefix + data.Mobile
|
|
||||||
if h.App.SysConfig.EnabledMsg {
|
|
||||||
var code int
|
|
||||||
err := h.leveldb.Get(key, &code)
|
|
||||||
if err != nil || code != data.Code {
|
|
||||||
resp.ERROR(c, "短信验证码错误")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if the username is exists
|
|
||||||
var item model.User
|
|
||||||
res := h.db.Where("mobile = ?", data.Mobile).First(&item)
|
|
||||||
if res.RowsAffected > 0 {
|
|
||||||
resp.ERROR(c, "该手机号码已经被注册,请更换其他手机号")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认订阅所有角色
|
|
||||||
var chatRoles []model.ChatRole
|
|
||||||
h.db.Find(&chatRoles)
|
|
||||||
var roleKeys = make([]string, 0)
|
|
||||||
for _, r := range chatRoles {
|
|
||||||
roleKeys = append(roleKeys, r.Key)
|
|
||||||
}
|
|
||||||
|
|
||||||
salt := utils.RandString(8)
|
|
||||||
user := model.User{
|
|
||||||
Password: utils.GenPassword(data.Password, salt),
|
|
||||||
Avatar: "/images/avatar/user.png",
|
|
||||||
Salt: salt,
|
|
||||||
Status: true,
|
|
||||||
Mobile: data.Mobile,
|
|
||||||
ChatRoles: utils.JsonEncode(roleKeys),
|
|
||||||
ChatConfig: utils.JsonEncode(types.UserChatConfig{
|
|
||||||
ApiKeys: map[types.Platform]string{
|
|
||||||
types.OpenAI: "",
|
|
||||||
types.Azure: "",
|
|
||||||
types.ChatGLM: "",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
Calls: h.App.SysConfig.UserInitCalls,
|
|
||||||
ImgCalls: h.App.SysConfig.InitImgCalls,
|
|
||||||
}
|
|
||||||
res = h.db.Create(&user)
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c, "保存数据失败")
|
|
||||||
logger.Error(res.Error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if h.App.SysConfig.EnabledMsg {
|
|
||||||
_ = h.leveldb.Delete(key) // 注册成功,删除短信验证码
|
|
||||||
}
|
|
||||||
resp.SUCCESS(c, user)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login 用户登录
|
|
||||||
func (h *UserHandler) Login(c *gin.Context) {
|
|
||||||
var data struct {
|
|
||||||
Mobile string `json:"username"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
}
|
|
||||||
if err := c.ShouldBindJSON(&data); err != nil {
|
|
||||||
resp.ERROR(c, types.InvalidArgs)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var user model.User
|
|
||||||
res := h.db.Where("mobile = ?", data.Mobile).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
|
|
||||||
}
|
|
||||||
|
|
||||||
if user.Status == false {
|
|
||||||
resp.ERROR(c, "该用户已被禁止登录,请联系管理员")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新最后登录时间和IP
|
|
||||||
user.LastLoginIp = c.ClientIP()
|
|
||||||
user.LastLoginAt = time.Now().Unix()
|
|
||||||
h.db.Model(&user).Updates(user)
|
|
||||||
|
|
||||||
h.db.Create(&model.UserLoginLog{
|
|
||||||
UserId: user.Id,
|
|
||||||
Username: user.Mobile,
|
|
||||||
LoginIp: c.ClientIP(),
|
|
||||||
LoginAddress: utils.Ip2Region(h.searcher, c.ClientIP()),
|
|
||||||
})
|
|
||||||
|
|
||||||
// 创建 token
|
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
|
||||||
"user_id": user.Id,
|
|
||||||
"expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)).Unix(),
|
|
||||||
})
|
|
||||||
tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
|
|
||||||
if err != nil {
|
|
||||||
resp.ERROR(c, "Failed to generate token, "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 保存到 redis
|
|
||||||
key := fmt.Sprintf("users/%d", user.Id)
|
|
||||||
if _, err := h.redis.Set(c, key, tokenString, 0).Result(); err != nil {
|
|
||||||
resp.ERROR(c, "error with save token: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp.SUCCESS(c, tokenString)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logout 注 销
|
|
||||||
func (h *UserHandler) Logout(c *gin.Context) {
|
|
||||||
sessionId := c.GetHeader(types.ChatTokenHeader)
|
|
||||||
key := h.GetUserKey(c)
|
|
||||||
if _, err := h.redis.Del(c, key).Result(); err != nil {
|
|
||||||
logger.Error("error with delete session: ", err)
|
|
||||||
}
|
|
||||||
// 删除 websocket 会话列表
|
|
||||||
h.App.ChatSession.Delete(sessionId)
|
|
||||||
// 关闭 socket 连接
|
|
||||||
client := h.App.ChatClients.Get(sessionId)
|
|
||||||
if client != nil {
|
|
||||||
client.Close()
|
|
||||||
}
|
|
||||||
resp.SUCCESS(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Session 获取/验证会话
|
|
||||||
func (h *UserHandler) Session(c *gin.Context) {
|
|
||||||
user, err := utils.GetLoginUser(c, h.db)
|
|
||||||
if err == nil {
|
|
||||||
var userVo vo.User
|
|
||||||
err := utils.CopyObject(user, &userVo)
|
|
||||||
if err != nil {
|
|
||||||
resp.ERROR(c)
|
|
||||||
}
|
|
||||||
userVo.Id = user.Id
|
|
||||||
resp.SUCCESS(c, userVo)
|
|
||||||
} else {
|
|
||||||
resp.NotAuth(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
type userProfile struct {
|
|
||||||
Id uint `json:"id"`
|
|
||||||
Mobile string `json:"mobile"`
|
|
||||||
Avatar string `json:"avatar"`
|
|
||||||
ChatConfig types.UserChatConfig `json:"chat_config"`
|
|
||||||
Calls int `json:"calls"`
|
|
||||||
ImgCalls int `json:"img_calls"`
|
|
||||||
TotalTokens int64 `json:"total_tokens"`
|
|
||||||
}
|
|
||||||
|
|
||||||
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 profile userProfile
|
|
||||||
err = utils.CopyObject(user, &profile)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("对象拷贝失败:", err.Error())
|
|
||||||
resp.ERROR(c, "获取用户信息失败")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
profile.Id = user.Id
|
|
||||||
resp.SUCCESS(c, profile)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *UserHandler) ProfileUpdate(c *gin.Context) {
|
|
||||||
var data userProfile
|
|
||||||
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.Avatar = data.Avatar
|
|
||||||
user.ChatConfig = utils.JsonEncode(data.ChatConfig)
|
|
||||||
res := h.db.Updates(&user)
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c, "更新用户信息失败")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.SUCCESS(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Password 更新密码
|
|
||||||
func (h *UserHandler) Password(c *gin.Context) {
|
|
||||||
var data struct {
|
|
||||||
OldPass string `json:"old_pass"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
}
|
|
||||||
if err := c.ShouldBindJSON(&data); err != nil {
|
|
||||||
resp.ERROR(c, types.InvalidArgs)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(data.Password) < 8 {
|
|
||||||
resp.ERROR(c, "密码长度不能少于8个字符")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := utils.GetLoginUser(c, h.db)
|
|
||||||
if err != nil {
|
|
||||||
resp.NotAuth(c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
password := utils.GenPassword(data.OldPass, user.Salt)
|
|
||||||
logger.Info(user.Salt, ",", user.Password, ",", password, ",", data.OldPass)
|
|
||||||
if password != user.Password {
|
|
||||||
resp.ERROR(c, "原密码错误")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
newPass := utils.GenPassword(data.Password, user.Salt)
|
|
||||||
res := h.db.Model(&user).UpdateColumn("password", newPass)
|
|
||||||
if res.Error != nil {
|
|
||||||
logger.Error("更新数据库失败: ", res.Error)
|
|
||||||
resp.ERROR(c, "更新数据库失败")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.SUCCESS(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BindMobile 绑定手机号
|
|
||||||
func (h *UserHandler) BindMobile(c *gin.Context) {
|
|
||||||
var data struct {
|
|
||||||
Mobile string `json:"mobile"`
|
|
||||||
Code int `json:"code"`
|
|
||||||
}
|
|
||||||
if err := c.ShouldBindJSON(&data); err != nil {
|
|
||||||
resp.ERROR(c, types.InvalidArgs)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查手机号是否被其他账号绑定
|
|
||||||
var item model.User
|
|
||||||
res := h.db.Where("mobile = ?", data.Mobile).First(&item)
|
|
||||||
if res.Error == nil {
|
|
||||||
resp.ERROR(c, "该手机号已经被其他账号绑定")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查验证码
|
|
||||||
key := CodeStorePrefix + data.Mobile
|
|
||||||
var code int
|
|
||||||
err := h.leveldb.Get(key, &code)
|
|
||||||
if err != nil || code != data.Code {
|
|
||||||
resp.ERROR(c, "短信验证码错误")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := utils.GetLoginUser(c, h.db)
|
|
||||||
if err != nil {
|
|
||||||
resp.NotAuth(c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
res = h.db.Model(&user).UpdateColumn("mobile", data.Mobile)
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c, "更新数据库失败")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = h.leveldb.Delete(key) // 删除短信验证码
|
|
||||||
resp.SUCCESS(c)
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
package logger
|
|
||||||
|
|
||||||
import (
|
|
||||||
"go.uber.org/zap"
|
|
||||||
"go.uber.org/zap/zapcore"
|
|
||||||
"gopkg.in/natefinch/lumberjack.v2"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var logger *zap.Logger
|
|
||||||
var sugarLogger *zap.SugaredLogger
|
|
||||||
|
|
||||||
func GetLogger() *zap.SugaredLogger {
|
|
||||||
if sugarLogger != nil {
|
|
||||||
return sugarLogger
|
|
||||||
}
|
|
||||||
|
|
||||||
logLevel := zap.NewAtomicLevelAt(getLogLevel(os.Getenv("LOG_LEVEL")))
|
|
||||||
encoder := getEncoder()
|
|
||||||
writerSyncer := getLogWriter()
|
|
||||||
fileCore := zapcore.NewCore(encoder, writerSyncer, logLevel)
|
|
||||||
consoleOutput := zapcore.Lock(os.Stdout)
|
|
||||||
consoleCore := zapcore.NewCore(
|
|
||||||
encoder,
|
|
||||||
consoleOutput,
|
|
||||||
logLevel,
|
|
||||||
)
|
|
||||||
core := zapcore.NewTee(fileCore, consoleCore)
|
|
||||||
logger = zap.New(core, zap.AddCaller())
|
|
||||||
sugarLogger = logger.Sugar()
|
|
||||||
return sugarLogger
|
|
||||||
}
|
|
||||||
|
|
||||||
// core 三个参数之 编码
|
|
||||||
func getEncoder() zapcore.Encoder {
|
|
||||||
encoderConfig := zapcore.EncoderConfig{
|
|
||||||
TimeKey: "time",
|
|
||||||
LevelKey: "level",
|
|
||||||
NameKey: "logger",
|
|
||||||
CallerKey: "caller",
|
|
||||||
MessageKey: "msg",
|
|
||||||
StacktraceKey: "stacktrace",
|
|
||||||
EncodeTime: zapcore.ISO8601TimeEncoder,
|
|
||||||
EncodeDuration: zapcore.SecondsDurationEncoder,
|
|
||||||
EncodeCaller: zapcore.ShortCallerEncoder,
|
|
||||||
EncodeLevel: zapcore.CapitalLevelEncoder,
|
|
||||||
}
|
|
||||||
return zapcore.NewConsoleEncoder(encoderConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getLogWriter() zapcore.WriteSyncer {
|
|
||||||
lumberJackLogger := &lumberjack.Logger{
|
|
||||||
Filename: "logs/app.log",
|
|
||||||
MaxSize: 10,
|
|
||||||
MaxBackups: 5,
|
|
||||||
MaxAge: 30,
|
|
||||||
Compress: false,
|
|
||||||
}
|
|
||||||
return zapcore.AddSync(lumberJackLogger)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getLogLevel(level string) zapcore.Level {
|
|
||||||
switch strings.ToUpper(level) {
|
|
||||||
case "DEBUG":
|
|
||||||
return zapcore.DebugLevel
|
|
||||||
case "WARN":
|
|
||||||
return zapcore.WarnLevel
|
|
||||||
case "ERROR":
|
|
||||||
return zapcore.ErrorLevel
|
|
||||||
default:
|
|
||||||
return zapcore.InfoLevel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
337
api/main.go
337
api/main.go
@@ -1,337 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core"
|
|
||||||
"chatplus/core/types"
|
|
||||||
"chatplus/handler"
|
|
||||||
"chatplus/handler/admin"
|
|
||||||
"chatplus/handler/chatimpl"
|
|
||||||
logger2 "chatplus/logger"
|
|
||||||
"chatplus/service"
|
|
||||||
"chatplus/service/fun"
|
|
||||||
"chatplus/service/mj"
|
|
||||||
"chatplus/service/oss"
|
|
||||||
"chatplus/service/sd"
|
|
||||||
"chatplus/service/wx"
|
|
||||||
"chatplus/store"
|
|
||||||
"context"
|
|
||||||
"embed"
|
|
||||||
"github.com/go-redis/redis/v8"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"strconv"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
|
|
||||||
"go.uber.org/fx"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
var logger = logger2.GetLogger()
|
|
||||||
|
|
||||||
//go:embed res/ip2region.xdb
|
|
||||||
var xdbFS embed.FS
|
|
||||||
|
|
||||||
// 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() {
|
|
||||||
configFile := os.Getenv("CONFIG_FILE")
|
|
||||||
if configFile == "" {
|
|
||||||
configFile = "config.toml"
|
|
||||||
}
|
|
||||||
var debug bool
|
|
||||||
debugEnv := os.Getenv("DEBUG")
|
|
||||||
if debugEnv == "" {
|
|
||||||
debug = true
|
|
||||||
} else {
|
|
||||||
debug, _ = strconv.ParseBool(os.Getenv("DEBUG"))
|
|
||||||
}
|
|
||||||
logger.Info("Loading config file: ", configFile)
|
|
||||||
defer func() {
|
|
||||||
if err := recover(); err != nil {
|
|
||||||
logger.Error("Panic Error:", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
app := fx.New(
|
|
||||||
// 初始化配置应用配置
|
|
||||||
fx.Provide(func() *types.AppConfig {
|
|
||||||
config, err := core.LoadConfig(configFile)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
config.Path = configFile
|
|
||||||
if debug {
|
|
||||||
_ = core.SaveConfig(config)
|
|
||||||
}
|
|
||||||
return config
|
|
||||||
}),
|
|
||||||
// 创建应用服务
|
|
||||||
fx.Provide(core.NewServer),
|
|
||||||
// 初始化
|
|
||||||
fx.Invoke(func(s *core.AppServer, client *redis.Client) {
|
|
||||||
s.Init(debug, client)
|
|
||||||
}),
|
|
||||||
|
|
||||||
// 初始化数据库
|
|
||||||
fx.Provide(store.NewGormConfig),
|
|
||||||
fx.Provide(store.NewMysql),
|
|
||||||
fx.Provide(store.NewLevelDB),
|
|
||||||
fx.Provide(store.NewRedisClient),
|
|
||||||
|
|
||||||
// 创建 Ip2Region 查询对象
|
|
||||||
fx.Provide(func() (*xdb.Searcher, error) {
|
|
||||||
file, err := xdbFS.Open("res/ip2region.xdb")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
cBuff, err := io.ReadAll(file)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return xdb.NewWithBuffer(cBuff)
|
|
||||||
}),
|
|
||||||
|
|
||||||
// 创建函数
|
|
||||||
fx.Provide(fun.NewFunctions),
|
|
||||||
|
|
||||||
// 创建控制器
|
|
||||||
fx.Provide(handler.NewChatRoleHandler),
|
|
||||||
fx.Provide(handler.NewUserHandler),
|
|
||||||
fx.Provide(chatimpl.NewChatHandler),
|
|
||||||
fx.Provide(handler.NewUploadHandler),
|
|
||||||
fx.Provide(handler.NewSmsHandler),
|
|
||||||
fx.Provide(handler.NewRewardHandler),
|
|
||||||
fx.Provide(handler.NewCaptchaHandler),
|
|
||||||
fx.Provide(handler.NewMidJourneyHandler),
|
|
||||||
fx.Provide(handler.NewChatModelHandler),
|
|
||||||
fx.Provide(handler.NewSdJobHandler),
|
|
||||||
|
|
||||||
fx.Provide(admin.NewConfigHandler),
|
|
||||||
fx.Provide(admin.NewAdminHandler),
|
|
||||||
fx.Provide(admin.NewApiKeyHandler),
|
|
||||||
fx.Provide(admin.NewUserHandler),
|
|
||||||
fx.Provide(admin.NewChatRoleHandler),
|
|
||||||
fx.Provide(admin.NewRewardHandler),
|
|
||||||
fx.Provide(admin.NewDashboardHandler),
|
|
||||||
fx.Provide(admin.NewChatModelHandler),
|
|
||||||
|
|
||||||
// 创建服务
|
|
||||||
fx.Provide(service.NewAliYunSmsService),
|
|
||||||
fx.Provide(func(config *types.AppConfig) *service.CaptchaService {
|
|
||||||
return service.NewCaptchaService(config.ApiConfig)
|
|
||||||
}),
|
|
||||||
fx.Provide(oss.NewUploaderManager),
|
|
||||||
fx.Provide(mj.NewService),
|
|
||||||
|
|
||||||
// 微信机器人服务
|
|
||||||
fx.Provide(wx.NewWeChatBot),
|
|
||||||
fx.Invoke(func(config *types.AppConfig, bot *wx.Bot) {
|
|
||||||
if config.WeChatBot {
|
|
||||||
err := bot.Run()
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("微信登录失败:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
// MidJourney 机器人
|
|
||||||
fx.Provide(mj.NewBot),
|
|
||||||
fx.Provide(mj.NewClient),
|
|
||||||
fx.Invoke(func(config *types.AppConfig, bot *mj.Bot) {
|
|
||||||
if config.MjConfig.Enabled {
|
|
||||||
err := bot.Run()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("MidJourney 服务启动失败:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
fx.Invoke(func(config *types.AppConfig, mjService *mj.Service) {
|
|
||||||
if config.MjConfig.Enabled {
|
|
||||||
go func() {
|
|
||||||
mjService.Run()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Stable Diffusion 机器人
|
|
||||||
fx.Provide(sd.NewService),
|
|
||||||
fx.Invoke(func(config *types.AppConfig, service *sd.Service) {
|
|
||||||
if config.SdConfig.Enabled {
|
|
||||||
go func() {
|
|
||||||
service.Run()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
// 注册路由
|
|
||||||
fx.Invoke(func(s *core.AppServer, h *handler.ChatRoleHandler) {
|
|
||||||
group := s.Engine.Group("/api/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.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)
|
|
||||||
group.POST("password", h.Password)
|
|
||||||
group.POST("bind/mobile", h.BindMobile)
|
|
||||||
}),
|
|
||||||
fx.Invoke(func(s *core.AppServer, h *chatimpl.ChatHandler) {
|
|
||||||
group := s.Engine.Group("/api/chat/")
|
|
||||||
group.Any("new", h.ChatHandle)
|
|
||||||
group.GET("list", h.List)
|
|
||||||
group.GET("detail", h.Detail)
|
|
||||||
group.POST("update", h.Update)
|
|
||||||
group.GET("remove", h.Remove)
|
|
||||||
group.GET("history", h.History)
|
|
||||||
group.GET("clear", h.Clear)
|
|
||||||
group.POST("tokens", h.Tokens)
|
|
||||||
group.GET("stop", h.StopGenerate)
|
|
||||||
}),
|
|
||||||
fx.Invoke(func(s *core.AppServer, h *handler.UploadHandler) {
|
|
||||||
s.Engine.POST("/api/upload", h.Upload)
|
|
||||||
}),
|
|
||||||
fx.Invoke(func(s *core.AppServer, h *handler.SmsHandler) {
|
|
||||||
group := s.Engine.Group("/api/sms/")
|
|
||||||
group.GET("status", h.Status)
|
|
||||||
group.POST("code", h.SendCode)
|
|
||||||
}),
|
|
||||||
fx.Invoke(func(s *core.AppServer, h *handler.CaptchaHandler) {
|
|
||||||
group := s.Engine.Group("/api/captcha/")
|
|
||||||
group.GET("get", h.Get)
|
|
||||||
group.POST("check", h.Check)
|
|
||||||
}),
|
|
||||||
fx.Invoke(func(s *core.AppServer, h *handler.RewardHandler) {
|
|
||||||
group := s.Engine.Group("/api/reward/")
|
|
||||||
group.POST("verify", h.Verify)
|
|
||||||
}),
|
|
||||||
fx.Invoke(func(s *core.AppServer, h *handler.MidJourneyHandler) {
|
|
||||||
group := s.Engine.Group("/api/mj/")
|
|
||||||
group.POST("image", h.Image)
|
|
||||||
group.POST("upscale", h.Upscale)
|
|
||||||
group.POST("variation", h.Variation)
|
|
||||||
group.GET("jobs", h.JobList)
|
|
||||||
group.Any("client", h.Client)
|
|
||||||
}),
|
|
||||||
fx.Invoke(func(s *core.AppServer, h *handler.SdJobHandler) {
|
|
||||||
group := s.Engine.Group("/api/sd")
|
|
||||||
group.POST("image", h.Image)
|
|
||||||
group.GET("jobs", h.JobList)
|
|
||||||
group.Any("client", h.Client)
|
|
||||||
}),
|
|
||||||
|
|
||||||
// 管理后台控制器
|
|
||||||
fx.Invoke(func(s *core.AppServer, h *admin.ConfigHandler) {
|
|
||||||
group := s.Engine.Group("/api/admin/config/")
|
|
||||||
group.POST("update", h.Update)
|
|
||||||
group.GET("get", h.Get)
|
|
||||||
}),
|
|
||||||
fx.Invoke(func(s *core.AppServer, h *admin.ManagerHandler) {
|
|
||||||
group := s.Engine.Group("/api/admin/")
|
|
||||||
group.POST("login", h.Login)
|
|
||||||
group.GET("logout", h.Logout)
|
|
||||||
group.GET("session", h.Session)
|
|
||||||
group.GET("migrate", h.Migrate)
|
|
||||||
}),
|
|
||||||
fx.Invoke(func(s *core.AppServer, h *admin.ApiKeyHandler) {
|
|
||||||
group := s.Engine.Group("/api/admin/apikey/")
|
|
||||||
group.POST("save", h.Save)
|
|
||||||
group.GET("list", h.List)
|
|
||||||
group.GET("remove", h.Remove)
|
|
||||||
}),
|
|
||||||
fx.Invoke(func(s *core.AppServer, h *admin.UserHandler) {
|
|
||||||
group := s.Engine.Group("/api/admin/user/")
|
|
||||||
group.GET("list", h.List)
|
|
||||||
group.POST("save", h.Save)
|
|
||||||
group.GET("remove", h.Remove)
|
|
||||||
group.GET("loginLog", h.LoginLog)
|
|
||||||
group.POST("resetPass", h.ResetPass)
|
|
||||||
}),
|
|
||||||
fx.Invoke(func(s *core.AppServer, h *admin.ChatRoleHandler) {
|
|
||||||
group := s.Engine.Group("/api/admin/role/")
|
|
||||||
group.GET("list", h.List)
|
|
||||||
group.POST("save", h.Save)
|
|
||||||
group.POST("sort", h.Sort)
|
|
||||||
group.GET("remove", h.Remove)
|
|
||||||
}),
|
|
||||||
fx.Invoke(func(s *core.AppServer, h *admin.RewardHandler) {
|
|
||||||
group := s.Engine.Group("/api/admin/reward/")
|
|
||||||
group.GET("list", h.List)
|
|
||||||
}),
|
|
||||||
fx.Invoke(func(s *core.AppServer, h *admin.DashboardHandler) {
|
|
||||||
group := s.Engine.Group("/api/admin/dashboard/")
|
|
||||||
group.GET("stats", h.Stats)
|
|
||||||
}),
|
|
||||||
fx.Invoke(func(s *core.AppServer, h *handler.ChatModelHandler) {
|
|
||||||
group := s.Engine.Group("/api/model/")
|
|
||||||
group.GET("list", h.List)
|
|
||||||
}),
|
|
||||||
fx.Invoke(func(s *core.AppServer, h *admin.ChatModelHandler) {
|
|
||||||
group := s.Engine.Group("/api/admin/model/")
|
|
||||||
group.POST("save", h.Save)
|
|
||||||
group.GET("list", h.List)
|
|
||||||
group.POST("enable", h.Enable)
|
|
||||||
group.POST("sort", h.Sort)
|
|
||||||
group.GET("remove", h.Remove)
|
|
||||||
}),
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
Binary file not shown.
@@ -1,98 +0,0 @@
|
|||||||
{
|
|
||||||
"data": [
|
|
||||||
"task(38194gitxp745ha)",
|
|
||||||
"A beautiful Chinese girl riding on a tiger",
|
|
||||||
"",
|
|
||||||
[],
|
|
||||||
20,
|
|
||||||
"Euler a",
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
7,
|
|
||||||
-1,
|
|
||||||
-1,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
false,
|
|
||||||
512,
|
|
||||||
512,
|
|
||||||
true,
|
|
||||||
0.7,
|
|
||||||
2,
|
|
||||||
"ESRGAN_4x",
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
"Use same sampler",
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
[],
|
|
||||||
"None",
|
|
||||||
null,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
"positive",
|
|
||||||
"comma",
|
|
||||||
0,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
"",
|
|
||||||
"Seed",
|
|
||||||
"",
|
|
||||||
[],
|
|
||||||
"Nothing",
|
|
||||||
"",
|
|
||||||
[],
|
|
||||||
"Nothing",
|
|
||||||
"",
|
|
||||||
[],
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
0,
|
|
||||||
"Not set",
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
1.3,
|
|
||||||
"Not set",
|
|
||||||
"Not set",
|
|
||||||
1.3,
|
|
||||||
"Not set",
|
|
||||||
1.3,
|
|
||||||
"Not set",
|
|
||||||
1.3,
|
|
||||||
1.3,
|
|
||||||
"Not set",
|
|
||||||
1.3,
|
|
||||||
"Not set",
|
|
||||||
1.3,
|
|
||||||
"Not set",
|
|
||||||
1.3,
|
|
||||||
"Not set",
|
|
||||||
1.3,
|
|
||||||
"Not set",
|
|
||||||
1.3,
|
|
||||||
"Not set",
|
|
||||||
false,
|
|
||||||
"None",
|
|
||||||
null,
|
|
||||||
false,
|
|
||||||
50,
|
|
||||||
[],
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"event_data": null,
|
|
||||||
"fn_index": 232,
|
|
||||||
"session_hash": "3xedmn4nuzq"
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core/types"
|
|
||||||
"chatplus/store"
|
|
||||||
"fmt"
|
|
||||||
"github.com/aliyun/alibaba-cloud-sdk-go/services/dysmsapi"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AliYunSmsService struct {
|
|
||||||
config *types.AliYunSmsConfig
|
|
||||||
db *store.LevelDB
|
|
||||||
client *dysmsapi.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAliYunSmsService(config *types.AppConfig, db *store.LevelDB) (*AliYunSmsService, error) {
|
|
||||||
// 创建阿里云短信客户端
|
|
||||||
client, err := dysmsapi.NewClientWithAccessKey(
|
|
||||||
"cn-hangzhou",
|
|
||||||
config.SmsConfig.AccessKey,
|
|
||||||
config.SmsConfig.AccessSecret)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create client: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &AliYunSmsService{
|
|
||||||
config: &config.SmsConfig,
|
|
||||||
db: db,
|
|
||||||
client: client,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AliYunSmsService) SendVerifyCode(mobile string, code int) error {
|
|
||||||
// 创建短信请求并设置参数
|
|
||||||
request := dysmsapi.CreateSendSmsRequest()
|
|
||||||
request.Scheme = "https"
|
|
||||||
request.Domain = s.config.Domain
|
|
||||||
request.PhoneNumbers = mobile
|
|
||||||
request.SignName = s.config.Sign
|
|
||||||
request.TemplateCode = s.config.CodeTempId
|
|
||||||
request.TemplateParam = fmt.Sprintf("{\"code\":\"%d\"}", code) // 短信模板中的参数
|
|
||||||
|
|
||||||
// 发送短信
|
|
||||||
response, err := s.client.SendSms(request)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to send SMS:%v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if response.Code != "OK" {
|
|
||||||
return fmt.Errorf("failed to send SMS:%v", response.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core/types"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/imroc/req/v3"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CaptchaService struct {
|
|
||||||
config types.ChatPlusApiConfig
|
|
||||||
client *req.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCaptchaService(config types.ChatPlusApiConfig) *CaptchaService {
|
|
||||||
return &CaptchaService{
|
|
||||||
config: config,
|
|
||||||
client: req.C().SetTimeout(10 * time.Second),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *CaptchaService) Get() (interface{}, error) {
|
|
||||||
if s.config.Token == "" {
|
|
||||||
return nil, errors.New("无效的 API Token")
|
|
||||||
}
|
|
||||||
|
|
||||||
url := fmt.Sprintf("%s/api/captcha/get", s.config.ApiURL)
|
|
||||||
var res types.BizVo
|
|
||||||
r, err := s.client.R().
|
|
||||||
SetHeader("AppId", s.config.AppId).
|
|
||||||
SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.Token)).
|
|
||||||
SetSuccessResult(&res).Get(url)
|
|
||||||
if err != nil || r.IsErrorState() {
|
|
||||||
return nil, fmt.Errorf("请求 API 失败:%v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.Code != types.Success {
|
|
||||||
return nil, fmt.Errorf("请求 API 失败:%s", res.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.Data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *CaptchaService) Check(data interface{}) bool {
|
|
||||||
url := fmt.Sprintf("%s/api/captcha/check", s.config.ApiURL)
|
|
||||||
var res types.BizVo
|
|
||||||
r, err := s.client.R().
|
|
||||||
SetHeader("AppId", s.config.AppId).
|
|
||||||
SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.Token)).
|
|
||||||
SetBodyJsonMarshal(data).
|
|
||||||
SetSuccessResult(&res).Post(url)
|
|
||||||
if err != nil || r.IsErrorState() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.Code != types.Success {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package fun
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core/types"
|
|
||||||
"chatplus/service/mj"
|
|
||||||
"chatplus/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AI 绘画函数
|
|
||||||
|
|
||||||
type FuncMidJourney struct {
|
|
||||||
name string
|
|
||||||
service *mj.Service
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMidJourneyFunc(mjService *mj.Service) FuncMidJourney {
|
|
||||||
return FuncMidJourney{
|
|
||||||
name: "MidJourney AI 绘画",
|
|
||||||
service: mjService}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f FuncMidJourney) Invoke(params map[string]interface{}) (string, error) {
|
|
||||||
logger.Infof("MJ 绘画参数:%+v", params)
|
|
||||||
prompt := utils.InterfaceToString(params["prompt"])
|
|
||||||
f.service.PushTask(types.MjTask{
|
|
||||||
SessionId: utils.InterfaceToString(params["session_id"]),
|
|
||||||
Src: types.TaskSrcChat,
|
|
||||||
Type: types.TaskImage,
|
|
||||||
Prompt: prompt,
|
|
||||||
UserId: utils.IntValue(utils.InterfaceToString(params["user_id"]), 0),
|
|
||||||
RoleId: utils.IntValue(utils.InterfaceToString(params["role_id"]), 0),
|
|
||||||
Icon: utils.InterfaceToString(params["icon"]),
|
|
||||||
ChatId: utils.InterfaceToString(params["chat_id"]),
|
|
||||||
})
|
|
||||||
return prompt, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f FuncMidJourney) Name() string {
|
|
||||||
return f.name
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Function = &FuncMidJourney{}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
package fun
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core/types"
|
|
||||||
logger2 "chatplus/logger"
|
|
||||||
"chatplus/service/mj"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Function interface {
|
|
||||||
Invoke(map[string]interface{}) (string, error)
|
|
||||||
Name() string
|
|
||||||
}
|
|
||||||
|
|
||||||
var logger = logger2.GetLogger()
|
|
||||||
|
|
||||||
type resVo struct {
|
|
||||||
Code types.BizCode `json:"code"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Data struct {
|
|
||||||
Title string `json:"title"`
|
|
||||||
UpdatedAt string `json:"updated_at"`
|
|
||||||
Items []dataItem `json:"items"`
|
|
||||||
} `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type dataItem struct {
|
|
||||||
Title string `json:"title"`
|
|
||||||
Url string `json:"url"`
|
|
||||||
Remark string `json:"remark"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewFunctions(config *types.AppConfig, mjService *mj.Service) map[string]Function {
|
|
||||||
return map[string]Function{
|
|
||||||
types.FuncZaoBao: NewZaoBao(config.ApiConfig),
|
|
||||||
types.FuncWeibo: NewWeiboHot(config.ApiConfig),
|
|
||||||
types.FuncHeadLine: NewHeadLines(config.ApiConfig),
|
|
||||||
types.FuncMidJourney: NewMidJourneyFunc(mjService),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
package fun
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core/types"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/imroc/req/v3"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 今日头条函数实现
|
|
||||||
|
|
||||||
type FuncHeadlines struct {
|
|
||||||
name string
|
|
||||||
config types.ChatPlusApiConfig
|
|
||||||
client *req.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHeadLines(config types.ChatPlusApiConfig) FuncHeadlines {
|
|
||||||
return FuncHeadlines{
|
|
||||||
name: "今日头条",
|
|
||||||
config: config,
|
|
||||||
client: req.C().SetTimeout(10 * time.Second)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f FuncHeadlines) Invoke(map[string]interface{}) (string, error) {
|
|
||||||
if f.config.Token == "" {
|
|
||||||
return "", errors.New("无效的 API Token")
|
|
||||||
}
|
|
||||||
|
|
||||||
url := fmt.Sprintf("%s/api/headline/fetch", f.config.ApiURL)
|
|
||||||
var res resVo
|
|
||||||
r, err := f.client.R().
|
|
||||||
SetHeader("AppId", f.config.AppId).
|
|
||||||
SetHeader("Authorization", fmt.Sprintf("Bearer %s", f.config.Token)).
|
|
||||||
SetSuccessResult(&res).Get(url)
|
|
||||||
if err != nil || r.IsErrorState() {
|
|
||||||
return "", fmt.Errorf("%v%v", err, r.Err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.Code != types.Success {
|
|
||||||
return "", errors.New(res.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
builder := make([]string, 0)
|
|
||||||
builder = append(builder, fmt.Sprintf("**%s**,最新更新:%s", res.Data.Title, res.Data.UpdatedAt))
|
|
||||||
for i, v := range res.Data.Items {
|
|
||||||
builder = append(builder, fmt.Sprintf("%d、 [%s](%s) [%s]", i+1, v.Title, v.Url, v.Remark))
|
|
||||||
}
|
|
||||||
return strings.Join(builder, "\n\n"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f FuncHeadlines) Name() string {
|
|
||||||
return f.name
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Function = &FuncHeadlines{}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
package fun
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core/types"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/imroc/req/v3"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 微博热搜函数实现
|
|
||||||
|
|
||||||
type FuncWeiboHot struct {
|
|
||||||
name string
|
|
||||||
config types.ChatPlusApiConfig
|
|
||||||
client *req.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewWeiboHot(config types.ChatPlusApiConfig) FuncWeiboHot {
|
|
||||||
return FuncWeiboHot{
|
|
||||||
name: "微博热搜",
|
|
||||||
config: config,
|
|
||||||
client: req.C().SetTimeout(10 * time.Second)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f FuncWeiboHot) Invoke(map[string]interface{}) (string, error) {
|
|
||||||
if f.config.Token == "" {
|
|
||||||
return "", errors.New("无效的 API Token")
|
|
||||||
}
|
|
||||||
|
|
||||||
url := fmt.Sprintf("%s/api/weibo/fetch", f.config.ApiURL)
|
|
||||||
var res resVo
|
|
||||||
r, err := f.client.R().
|
|
||||||
SetHeader("AppId", f.config.AppId).
|
|
||||||
SetHeader("Authorization", fmt.Sprintf("Bearer %s", f.config.Token)).
|
|
||||||
SetSuccessResult(&res).Get(url)
|
|
||||||
if err != nil || r.IsErrorState() {
|
|
||||||
return "", fmt.Errorf("%v%v", err, r.Err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.Code != types.Success {
|
|
||||||
return "", errors.New(res.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
builder := make([]string, 0)
|
|
||||||
builder = append(builder, fmt.Sprintf("**%s**,最新更新:%s", res.Data.Title, res.Data.UpdatedAt))
|
|
||||||
for i, v := range res.Data.Items {
|
|
||||||
builder = append(builder, fmt.Sprintf("%d、 [%s](%s) [热度:%s]", i+1, v.Title, v.Url, v.Remark))
|
|
||||||
}
|
|
||||||
return strings.Join(builder, "\n\n"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f FuncWeiboHot) Name() string {
|
|
||||||
return f.name
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Function = &FuncWeiboHot{}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
package fun
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core/types"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/imroc/req/v3"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 每日早报函数实现
|
|
||||||
|
|
||||||
type FuncZaoBao struct {
|
|
||||||
name string
|
|
||||||
config types.ChatPlusApiConfig
|
|
||||||
client *req.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewZaoBao(config types.ChatPlusApiConfig) FuncZaoBao {
|
|
||||||
return FuncZaoBao{
|
|
||||||
name: "每日早报",
|
|
||||||
config: config,
|
|
||||||
client: req.C().SetTimeout(10 * time.Second)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f FuncZaoBao) Invoke(map[string]interface{}) (string, error) {
|
|
||||||
if f.config.Token == "" {
|
|
||||||
return "", errors.New("无效的 API Token")
|
|
||||||
}
|
|
||||||
|
|
||||||
url := fmt.Sprintf("%s/api/zaobao/fetch", f.config.ApiURL)
|
|
||||||
var res resVo
|
|
||||||
r, err := f.client.R().
|
|
||||||
SetHeader("AppId", f.config.AppId).
|
|
||||||
SetHeader("Authorization", fmt.Sprintf("Bearer %s", f.config.Token)).
|
|
||||||
SetSuccessResult(&res).Get(url)
|
|
||||||
if err != nil || r.IsErrorState() {
|
|
||||||
return "", fmt.Errorf("%v%v", err, r.Err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.Code != types.Success {
|
|
||||||
return "", errors.New(res.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
builder := make([]string, 0)
|
|
||||||
builder = append(builder, fmt.Sprintf("**%s 早报:**", res.Data.UpdatedAt))
|
|
||||||
for _, v := range res.Data.Items {
|
|
||||||
builder = append(builder, v.Title)
|
|
||||||
}
|
|
||||||
builder = append(builder, fmt.Sprintf("%s", res.Data.Title))
|
|
||||||
return strings.Join(builder, "\n\n"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f FuncZaoBao) Name() string {
|
|
||||||
return f.name
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Function = &FuncZaoBao{}
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
package mj
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core/types"
|
|
||||||
logger2 "chatplus/logger"
|
|
||||||
"chatplus/utils"
|
|
||||||
"github.com/bwmarrin/discordgo"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MidJourney 机器人
|
|
||||||
|
|
||||||
var logger = logger2.GetLogger()
|
|
||||||
|
|
||||||
type Bot struct {
|
|
||||||
config *types.MidJourneyConfig
|
|
||||||
bot *discordgo.Session
|
|
||||||
service *Service
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewBot(config *types.AppConfig, service *Service) (*Bot, error) {
|
|
||||||
discord, err := discordgo.New("Bot " + config.MjConfig.BotToken)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.ProxyURL != "" {
|
|
||||||
proxy, _ := url.Parse(config.ProxyURL)
|
|
||||||
discord.Client = &http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
Proxy: http.ProxyURL(proxy),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
discord.Dialer = &websocket.Dialer{
|
|
||||||
Proxy: http.ProxyURL(proxy),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Bot{
|
|
||||||
config: &config.MjConfig,
|
|
||||||
bot: discord,
|
|
||||||
service: service,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bot) Run() error {
|
|
||||||
b.bot.Identify.Intents = discordgo.IntentsAllWithoutPrivileged | discordgo.IntentsGuildMessages | discordgo.IntentMessageContent
|
|
||||||
b.bot.AddHandler(b.messageCreate)
|
|
||||||
b.bot.AddHandler(b.messageUpdate)
|
|
||||||
|
|
||||||
logger.Info("Starting MidJourney Bot...")
|
|
||||||
err := b.bot.Open()
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("Error opening Discord connection:", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
logger.Info("Starting MidJourney Bot successfully!")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type TaskStatus string
|
|
||||||
|
|
||||||
const (
|
|
||||||
Start = TaskStatus("Started")
|
|
||||||
Running = TaskStatus("Running")
|
|
||||||
Stopped = TaskStatus("Stopped")
|
|
||||||
Finished = TaskStatus("Finished")
|
|
||||||
)
|
|
||||||
|
|
||||||
type Image struct {
|
|
||||||
URL string `json:"url"`
|
|
||||||
ProxyURL string `json:"proxy_url"`
|
|
||||||
Filename string `json:"filename"`
|
|
||||||
Width int `json:"width"`
|
|
||||||
Height int `json:"height"`
|
|
||||||
Size int `json:"size"`
|
|
||||||
Hash string `json:"hash"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bot) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
|
|
||||||
// ignore messages for other channels
|
|
||||||
if m.GuildID != b.config.GuildId || m.ChannelID != b.config.ChanelId {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// ignore messages for self
|
|
||||||
if m.Author.ID == s.State.User.ID {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Debugf("CREATE: %s", utils.JsonEncode(m))
|
|
||||||
var referenceId = ""
|
|
||||||
if m.ReferencedMessage != nil {
|
|
||||||
referenceId = m.ReferencedMessage.ID
|
|
||||||
}
|
|
||||||
if strings.Contains(m.Content, "(Waiting to start)") && !strings.Contains(m.Content, "Rerolling **") {
|
|
||||||
// parse content
|
|
||||||
req := CBReq{
|
|
||||||
MessageId: m.ID,
|
|
||||||
ReferenceId: referenceId,
|
|
||||||
Prompt: extractPrompt(m.Content),
|
|
||||||
Content: m.Content,
|
|
||||||
Progress: 0,
|
|
||||||
Status: Start}
|
|
||||||
b.service.Notify(req)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
b.addAttachment(m.ID, referenceId, m.Content, m.Attachments)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bot) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdate) {
|
|
||||||
// ignore messages for other channels
|
|
||||||
if m.GuildID != b.config.GuildId || m.ChannelID != b.config.ChanelId {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// ignore messages for self
|
|
||||||
if m.Author.ID == s.State.User.ID {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Debugf("UPDATE: %s", utils.JsonEncode(m))
|
|
||||||
|
|
||||||
var referenceId = ""
|
|
||||||
if m.ReferencedMessage != nil {
|
|
||||||
referenceId = m.ReferencedMessage.ID
|
|
||||||
}
|
|
||||||
if strings.Contains(m.Content, "(Stopped)") {
|
|
||||||
req := CBReq{
|
|
||||||
MessageId: m.ID,
|
|
||||||
ReferenceId: referenceId,
|
|
||||||
Prompt: extractPrompt(m.Content),
|
|
||||||
Content: m.Content,
|
|
||||||
Progress: extractProgress(m.Content),
|
|
||||||
Status: Stopped}
|
|
||||||
b.service.Notify(req)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
b.addAttachment(m.ID, referenceId, m.Content, m.Attachments)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bot) addAttachment(messageId string, referenceId string, content string, attachments []*discordgo.MessageAttachment) {
|
|
||||||
progress := extractProgress(content)
|
|
||||||
var status TaskStatus
|
|
||||||
if progress == 100 {
|
|
||||||
status = Finished
|
|
||||||
} else {
|
|
||||||
status = Running
|
|
||||||
}
|
|
||||||
for _, attachment := range attachments {
|
|
||||||
if attachment.Width == 0 || attachment.Height == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
image := Image{
|
|
||||||
URL: attachment.URL,
|
|
||||||
Height: attachment.Height,
|
|
||||||
ProxyURL: attachment.ProxyURL,
|
|
||||||
Width: attachment.Width,
|
|
||||||
Size: attachment.Size,
|
|
||||||
Filename: attachment.Filename,
|
|
||||||
Hash: extractHashFromFilename(attachment.Filename),
|
|
||||||
}
|
|
||||||
req := CBReq{
|
|
||||||
MessageId: messageId,
|
|
||||||
ReferenceId: referenceId,
|
|
||||||
Image: image,
|
|
||||||
Prompt: extractPrompt(content),
|
|
||||||
Content: content,
|
|
||||||
Progress: progress,
|
|
||||||
Status: status,
|
|
||||||
}
|
|
||||||
b.service.Notify(req)
|
|
||||||
break // only get one image
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// extract prompt from string
|
|
||||||
func extractPrompt(input string) string {
|
|
||||||
pattern := `\*\*(.*?)\*\*`
|
|
||||||
re := regexp.MustCompile(pattern)
|
|
||||||
matches := re.FindStringSubmatch(input)
|
|
||||||
if len(matches) > 1 {
|
|
||||||
return strings.TrimSpace(matches[1])
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractProgress(input string) int {
|
|
||||||
pattern := `\((\d+)\%\)`
|
|
||||||
re := regexp.MustCompile(pattern)
|
|
||||||
matches := re.FindStringSubmatch(input)
|
|
||||||
if len(matches) > 1 {
|
|
||||||
return utils.IntValue(matches[1], 0)
|
|
||||||
}
|
|
||||||
return 100
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractHashFromFilename(filename string) string {
|
|
||||||
if !strings.HasSuffix(filename, ".png") {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
index := strings.LastIndex(filename, "_")
|
|
||||||
if index != -1 {
|
|
||||||
return filename[index+1 : len(filename)-4]
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
package mj
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core/types"
|
|
||||||
"fmt"
|
|
||||||
"github.com/imroc/req/v3"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MidJourney client
|
|
||||||
|
|
||||||
type Client struct {
|
|
||||||
client *req.Client
|
|
||||||
config *types.MidJourneyConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewClient(config *types.AppConfig) *Client {
|
|
||||||
client := req.C().SetTimeout(10 * time.Second)
|
|
||||||
// set proxy URL
|
|
||||||
if config.ProxyURL != "" {
|
|
||||||
client.SetProxyURL(config.ProxyURL)
|
|
||||||
}
|
|
||||||
return &Client{client: client, config: &config.MjConfig}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Imagine(prompt string) error {
|
|
||||||
interactionsReq := &InteractionsRequest{
|
|
||||||
Type: 2,
|
|
||||||
ApplicationID: ApplicationID,
|
|
||||||
GuildID: c.config.GuildId,
|
|
||||||
ChannelID: c.config.ChanelId,
|
|
||||||
SessionID: SessionID,
|
|
||||||
Data: map[string]any{
|
|
||||||
"version": "1118961510123847772",
|
|
||||||
"id": "938956540159881230",
|
|
||||||
"name": "imagine",
|
|
||||||
"type": "1",
|
|
||||||
"options": []map[string]any{
|
|
||||||
{
|
|
||||||
"type": 3,
|
|
||||||
"name": "prompt",
|
|
||||||
"value": prompt,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"application_command": map[string]any{
|
|
||||||
"id": "938956540159881230",
|
|
||||||
"application_id": ApplicationID,
|
|
||||||
"version": "1118961510123847772",
|
|
||||||
"default_permission": true,
|
|
||||||
"default_member_permissions": nil,
|
|
||||||
"type": 1,
|
|
||||||
"nsfw": false,
|
|
||||||
"name": "imagine",
|
|
||||||
"description": "Create images with Midjourney",
|
|
||||||
"dm_permission": true,
|
|
||||||
"options": []map[string]any{
|
|
||||||
{
|
|
||||||
"type": 3,
|
|
||||||
"name": "prompt",
|
|
||||||
"description": "The prompt to imagine",
|
|
||||||
"required": true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"attachments": []any{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
url := "https://discord.com/api/v9/interactions"
|
|
||||||
r, err := c.client.R().SetHeader("Authorization", c.config.UserToken).
|
|
||||||
SetHeader("Content-Type", "application/json").
|
|
||||||
SetBody(interactionsReq).
|
|
||||||
Post(url)
|
|
||||||
|
|
||||||
if err != nil || r.IsErrorState() {
|
|
||||||
return fmt.Errorf("error with http request: %w%v", err, r.Err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upscale 放大指定的图片
|
|
||||||
func (c *Client) Upscale(index int, messageId string, hash string) error {
|
|
||||||
flags := 0
|
|
||||||
interactionsReq := &InteractionsRequest{
|
|
||||||
Type: 3,
|
|
||||||
ApplicationID: ApplicationID,
|
|
||||||
GuildID: c.config.GuildId,
|
|
||||||
ChannelID: c.config.ChanelId,
|
|
||||||
MessageFlags: &flags,
|
|
||||||
MessageID: &messageId,
|
|
||||||
SessionID: SessionID,
|
|
||||||
Data: map[string]any{
|
|
||||||
"component_type": 2,
|
|
||||||
"custom_id": fmt.Sprintf("MJ::JOB::upsample::%d::%s", index, hash),
|
|
||||||
},
|
|
||||||
Nonce: fmt.Sprintf("%d", time.Now().UnixNano()),
|
|
||||||
}
|
|
||||||
|
|
||||||
url := "https://discord.com/api/v9/interactions"
|
|
||||||
var res InteractionsResult
|
|
||||||
r, err := c.client.R().SetHeader("Authorization", c.config.UserToken).
|
|
||||||
SetHeader("Content-Type", "application/json").
|
|
||||||
SetBody(interactionsReq).
|
|
||||||
SetErrorResult(&res).
|
|
||||||
Post(url)
|
|
||||||
if err != nil || r.IsErrorState() {
|
|
||||||
return fmt.Errorf("error with http request: %v%v%v", err, r.Err, res.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Variation 以指定的图片的视角进行变换再创作,注意需要在对应的频道中关闭 Remix 变换,否则 Variation 指令将不会生效
|
|
||||||
func (c *Client) Variation(index int, messageId string, hash string) error {
|
|
||||||
flags := 0
|
|
||||||
interactionsReq := &InteractionsRequest{
|
|
||||||
Type: 3,
|
|
||||||
ApplicationID: ApplicationID,
|
|
||||||
GuildID: c.config.GuildId,
|
|
||||||
ChannelID: c.config.ChanelId,
|
|
||||||
MessageFlags: &flags,
|
|
||||||
MessageID: &messageId,
|
|
||||||
SessionID: SessionID,
|
|
||||||
Data: map[string]any{
|
|
||||||
"component_type": 2,
|
|
||||||
"custom_id": fmt.Sprintf("MJ::JOB::variation::%d::%s", index, hash),
|
|
||||||
},
|
|
||||||
Nonce: fmt.Sprintf("%d", time.Now().UnixNano()),
|
|
||||||
}
|
|
||||||
|
|
||||||
url := "https://discord.com/api/v9/interactions"
|
|
||||||
var res InteractionsResult
|
|
||||||
r, err := c.client.R().SetHeader("Authorization", c.config.UserToken).
|
|
||||||
SetHeader("Content-Type", "application/json").
|
|
||||||
SetBody(interactionsReq).
|
|
||||||
SetErrorResult(&res).
|
|
||||||
Post(url)
|
|
||||||
if err != nil || r.IsErrorState() {
|
|
||||||
return fmt.Errorf("error with http request: %v%v%v", err, r.Err, res.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,249 +0,0 @@
|
|||||||
package mj
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core/types"
|
|
||||||
"chatplus/service/oss"
|
|
||||||
"chatplus/store"
|
|
||||||
"chatplus/store/model"
|
|
||||||
"chatplus/store/vo"
|
|
||||||
"chatplus/utils"
|
|
||||||
"context"
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
"github.com/go-redis/redis/v8"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MJ 绘画服务
|
|
||||||
|
|
||||||
const RunningJobKey = "MidJourney_Running_Job"
|
|
||||||
|
|
||||||
type Service struct {
|
|
||||||
client *Client // MJ 客户端
|
|
||||||
taskQueue *store.RedisQueue
|
|
||||||
redis *redis.Client
|
|
||||||
db *gorm.DB
|
|
||||||
uploadManager *oss.UploaderManager
|
|
||||||
Clients *types.LMap[string, *types.WsClient] // MJ 绘画页面 websocket 连接池,用户推送绘画消息
|
|
||||||
ChatClients *types.LMap[string, *types.WsClient] // 聊天页面 websocket 连接池,用于推送绘画消息
|
|
||||||
proxyURL string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewService(redisCli *redis.Client, db *gorm.DB, client *Client, manager *oss.UploaderManager, config *types.AppConfig) *Service {
|
|
||||||
return &Service{
|
|
||||||
redis: redisCli,
|
|
||||||
db: db,
|
|
||||||
taskQueue: store.NewRedisQueue("MidJourney_Task_Queue", redisCli),
|
|
||||||
client: client,
|
|
||||||
uploadManager: manager,
|
|
||||||
Clients: types.NewLMap[string, *types.WsClient](),
|
|
||||||
ChatClients: types.NewLMap[string, *types.WsClient](),
|
|
||||||
proxyURL: config.ProxyURL,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) Run() {
|
|
||||||
logger.Info("Starting MidJourney job consumer.")
|
|
||||||
ctx := context.Background()
|
|
||||||
for {
|
|
||||||
_, err := s.redis.Get(ctx, RunningJobKey).Result()
|
|
||||||
if err == nil { // 队列串行执行
|
|
||||||
time.Sleep(time.Second * 3)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var task types.MjTask
|
|
||||||
err = s.taskQueue.LPop(&task)
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorf("taking task with error: %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
logger.Infof("Consuming Task: %+v", task)
|
|
||||||
switch task.Type {
|
|
||||||
case types.TaskImage:
|
|
||||||
err = s.client.Imagine(task.Prompt)
|
|
||||||
break
|
|
||||||
case types.TaskUpscale:
|
|
||||||
err = s.client.Upscale(task.Index, task.MessageId, task.MessageHash)
|
|
||||||
|
|
||||||
break
|
|
||||||
case types.TaskVariation:
|
|
||||||
err = s.client.Variation(task.Index, task.MessageId, task.MessageHash)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("绘画任务执行失败:", err)
|
|
||||||
if task.RetryCount <= 5 {
|
|
||||||
s.taskQueue.RPush(task)
|
|
||||||
}
|
|
||||||
task.RetryCount += 1
|
|
||||||
time.Sleep(time.Second * 3)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新任务的执行状态
|
|
||||||
s.db.Model(&model.MidJourneyJob{}).Where("id = ?", task.Id).UpdateColumn("started", true)
|
|
||||||
// 锁定任务执行通道,直到任务超时(5分钟)
|
|
||||||
s.redis.Set(ctx, RunningJobKey, utils.JsonEncode(task), time.Minute*5)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) PushTask(task types.MjTask) {
|
|
||||||
logger.Infof("add a new MidJourney Task: %+v", task)
|
|
||||||
s.taskQueue.RPush(task)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) Notify(data CBReq) {
|
|
||||||
taskString, err := s.redis.Get(context.Background(), RunningJobKey).Result()
|
|
||||||
if err != nil { // 过期任务,丢弃
|
|
||||||
logger.Warn("任务已过期:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var task types.MjTask
|
|
||||||
err = utils.JsonDecode(taskString, &task)
|
|
||||||
if err != nil { // 非标准任务,丢弃
|
|
||||||
logger.Warn("任务解析失败:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var job model.MidJourneyJob
|
|
||||||
res := s.db.Where("message_id = ?", data.MessageId).First(&job)
|
|
||||||
if res.Error == nil && data.Status == Finished {
|
|
||||||
logger.Warn("重复消息:", data.MessageId)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if task.Src == types.TaskSrcImg { // 绘画任务
|
|
||||||
var job model.MidJourneyJob
|
|
||||||
res := s.db.Where("id = ?", task.Id).First(&job)
|
|
||||||
if res.Error != nil {
|
|
||||||
logger.Warn("非法任务:", res.Error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
job.MessageId = data.MessageId
|
|
||||||
job.ReferenceId = data.ReferenceId
|
|
||||||
job.Progress = data.Progress
|
|
||||||
job.Prompt = data.Prompt
|
|
||||||
job.Hash = data.Image.Hash
|
|
||||||
|
|
||||||
// 任务完成,将最终的图片下载下来
|
|
||||||
if data.Progress == 100 {
|
|
||||||
imgURL, err := s.uploadManager.GetUploadHandler().PutImg(data.Image.URL, true)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("error with download img: ", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
job.ImgURL = imgURL
|
|
||||||
} else {
|
|
||||||
// 临时图片直接保存,访问的时候使用代理进行转发
|
|
||||||
job.ImgURL = data.Image.URL
|
|
||||||
}
|
|
||||||
res = s.db.Updates(&job)
|
|
||||||
if res.Error != nil {
|
|
||||||
logger.Error("error with update job: ", res.Error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var jobVo vo.MidJourneyJob
|
|
||||||
err := utils.CopyObject(job, &jobVo)
|
|
||||||
if err == nil {
|
|
||||||
if data.Progress < 100 {
|
|
||||||
image, err := utils.DownloadImage(jobVo.ImgURL, s.proxyURL)
|
|
||||||
if err == nil {
|
|
||||||
jobVo.ImgURL = "data:image/png;base64," + base64.StdEncoding.EncodeToString(image)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 推送任务到前端
|
|
||||||
client := s.Clients.Get(task.SessionId)
|
|
||||||
if client != nil {
|
|
||||||
utils.ReplyChunkMessage(client, jobVo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if task.Src == types.TaskSrcChat { // 聊天任务
|
|
||||||
wsClient := s.ChatClients.Get(task.SessionId)
|
|
||||||
if data.Status == Finished {
|
|
||||||
if wsClient != nil && data.ReferenceId != "" {
|
|
||||||
content := fmt.Sprintf("**%s** 任务执行成功,正在从 MidJourney 服务器下载图片,请稍后...", data.Prompt)
|
|
||||||
utils.ReplyMessage(wsClient, content)
|
|
||||||
}
|
|
||||||
// download image
|
|
||||||
imgURL, err := s.uploadManager.GetUploadHandler().PutImg(data.Image.URL, true)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("error with download image: ", err)
|
|
||||||
if wsClient != nil && data.ReferenceId != "" {
|
|
||||||
content := fmt.Sprintf("**%s** 图片下载失败:%s", data.Prompt, err.Error())
|
|
||||||
utils.ReplyMessage(wsClient, content)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tx := s.db.Begin()
|
|
||||||
data.Image.URL = imgURL
|
|
||||||
message := model.HistoryMessage{
|
|
||||||
UserId: uint(task.UserId),
|
|
||||||
ChatId: task.ChatId,
|
|
||||||
RoleId: uint(task.RoleId),
|
|
||||||
Type: types.MjMsg,
|
|
||||||
Icon: task.Icon,
|
|
||||||
Content: utils.JsonEncode(data),
|
|
||||||
Tokens: 0,
|
|
||||||
UseContext: false,
|
|
||||||
}
|
|
||||||
res = tx.Create(&message)
|
|
||||||
if res.Error != nil {
|
|
||||||
logger.Error("error with update database: ", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// save the job
|
|
||||||
job.UserId = task.UserId
|
|
||||||
job.Type = task.Type.String()
|
|
||||||
job.MessageId = data.MessageId
|
|
||||||
job.ReferenceId = data.ReferenceId
|
|
||||||
job.Prompt = data.Prompt
|
|
||||||
job.ImgURL = imgURL
|
|
||||||
job.Progress = data.Progress
|
|
||||||
job.Hash = data.Image.Hash
|
|
||||||
job.CreatedAt = time.Now()
|
|
||||||
res = tx.Create(&job)
|
|
||||||
if res.Error != nil {
|
|
||||||
logger.Error("error with update database: ", err)
|
|
||||||
tx.Rollback()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tx.Commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
if wsClient == nil { // 客户端断线,则丢弃
|
|
||||||
logger.Errorf("Client is offline: %+v", data)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if data.Status == Finished {
|
|
||||||
utils.ReplyChunkMessage(wsClient, types.WsMessage{Type: types.WsMjImg, Content: data})
|
|
||||||
utils.ReplyChunkMessage(wsClient, types.WsMessage{Type: types.WsEnd})
|
|
||||||
// 本次绘画完毕,移除客户端
|
|
||||||
s.ChatClients.Delete(task.SessionId)
|
|
||||||
} else {
|
|
||||||
// 使用代理临时转发图片
|
|
||||||
if data.Image.URL != "" {
|
|
||||||
image, err := utils.DownloadImage(data.Image.URL, s.proxyURL)
|
|
||||||
if err == nil {
|
|
||||||
data.Image.URL = "data:image/png;base64," + base64.StdEncoding.EncodeToString(image)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
utils.ReplyChunkMessage(wsClient, types.WsMessage{Type: types.WsMjImg, Content: data})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新用户剩余绘图次数
|
|
||||||
// TODO: 放大图片是否需要消耗绘图次数?
|
|
||||||
if data.Status == Finished {
|
|
||||||
s.db.Model(&model.User{}).Where("id = ?", task.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
|
|
||||||
// 解除任务锁定
|
|
||||||
s.redis.Del(context.Background(), RunningJobKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
package mj
|
|
||||||
|
|
||||||
const (
|
|
||||||
ApplicationID string = "936929561302675456"
|
|
||||||
SessionID string = "ea8816d857ba9ae2f74c59ae1a953afe"
|
|
||||||
)
|
|
||||||
|
|
||||||
type InteractionsRequest struct {
|
|
||||||
Type int `json:"type"`
|
|
||||||
ApplicationID string `json:"application_id"`
|
|
||||||
MessageFlags *int `json:"message_flags,omitempty"`
|
|
||||||
MessageID *string `json:"message_id,omitempty"`
|
|
||||||
GuildID string `json:"guild_id"`
|
|
||||||
ChannelID string `json:"channel_id"`
|
|
||||||
SessionID string `json:"session_id"`
|
|
||||||
Data map[string]any `json:"data"`
|
|
||||||
Nonce string `json:"nonce,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type InteractionsResult struct {
|
|
||||||
Code int `json:"code"`
|
|
||||||
Message string
|
|
||||||
Error map[string]any
|
|
||||||
}
|
|
||||||
|
|
||||||
type CBReq struct {
|
|
||||||
MessageId string `json:"message_id"`
|
|
||||||
ReferenceId string `json:"reference_id"`
|
|
||||||
Image Image `json:"image"`
|
|
||||||
Content string `json:"content"`
|
|
||||||
Prompt string `json:"prompt"`
|
|
||||||
Status TaskStatus `json:"status"`
|
|
||||||
Progress int `json:"progress"`
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
package oss
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"chatplus/core/types"
|
|
||||||
"chatplus/utils"
|
|
||||||
"fmt"
|
|
||||||
"github.com/aliyun/aliyun-oss-go-sdk/oss"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"net/url"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AliYunOss struct {
|
|
||||||
config *types.AliYunOssConfig
|
|
||||||
bucket *oss.Bucket
|
|
||||||
proxyURL string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAliYunOss(appConfig *types.AppConfig) (*AliYunOss, error) {
|
|
||||||
config := &appConfig.OSS.AliYun
|
|
||||||
// 创建 OSS 客户端
|
|
||||||
client, err := oss.New(config.Endpoint, config.AccessKey, config.AccessSecret)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取存储空间
|
|
||||||
bucket, err := client.Bucket(config.Bucket)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &AliYunOss{
|
|
||||||
config: config,
|
|
||||||
bucket: bucket,
|
|
||||||
proxyURL: appConfig.ProxyURL,
|
|
||||||
}, nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s AliYunOss) PutFile(ctx *gin.Context, name string) (string, error) {
|
|
||||||
// 解析表单
|
|
||||||
file, err := ctx.FormFile(name)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
// 打开上传文件
|
|
||||||
src, err := file.Open()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer src.Close()
|
|
||||||
|
|
||||||
fileExt := filepath.Ext(file.Filename)
|
|
||||||
objectKey := fmt.Sprintf("%d%s", time.Now().UnixMicro(), fileExt)
|
|
||||||
// 上传文件
|
|
||||||
err = s.bucket.PutObject(objectKey, src)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("https://%s.%s/%s", s.config.Bucket, s.config.Endpoint, objectKey), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s AliYunOss) PutImg(imageURL string, useProxy bool) (string, error) {
|
|
||||||
var imageData []byte
|
|
||||||
var err error
|
|
||||||
if useProxy {
|
|
||||||
imageData, err = utils.DownloadImage(imageURL, s.proxyURL)
|
|
||||||
} else {
|
|
||||||
imageData, err = utils.DownloadImage(imageURL, "")
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error with download image: %v", err)
|
|
||||||
}
|
|
||||||
parse, err := url.Parse(imageURL)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error with parse image URL: %v", err)
|
|
||||||
}
|
|
||||||
fileExt := filepath.Ext(parse.Path)
|
|
||||||
objectKey := fmt.Sprintf("%d%s", time.Now().UnixMicro(), fileExt)
|
|
||||||
// 上传文件字节数据
|
|
||||||
err = s.bucket.PutObject(objectKey, bytes.NewReader(imageData))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("https://%s.%s/%s", s.config.Bucket, s.config.Endpoint, objectKey), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s AliYunOss) Delete(fileURL string) error {
|
|
||||||
objectName := filepath.Base(fileURL)
|
|
||||||
return s.bucket.DeleteObject(objectName)
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Uploader = AliYunOss{}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
package oss
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core/types"
|
|
||||||
"chatplus/utils"
|
|
||||||
"fmt"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LocalStorage struct {
|
|
||||||
config *types.LocalStorageConfig
|
|
||||||
proxyURL string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLocalStorage(config *types.AppConfig) LocalStorage {
|
|
||||||
return LocalStorage{
|
|
||||||
config: &config.OSS.Local,
|
|
||||||
proxyURL: config.ProxyURL,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s LocalStorage) PutFile(ctx *gin.Context, name string) (string, error) {
|
|
||||||
file, err := ctx.FormFile(name)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error with get form: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
filePath, err := utils.GenUploadPath(s.config.BasePath, file.Filename)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error with generate filename: %s", err.Error())
|
|
||||||
}
|
|
||||||
// 将文件保存到指定路径
|
|
||||||
err = ctx.SaveUploadedFile(file, filePath)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error with save upload file: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return utils.GenUploadUrl(s.config.BasePath, s.config.BaseURL, filePath), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s LocalStorage) PutImg(imageURL string, useProxy bool) (string, error) {
|
|
||||||
parse, err := url.Parse(imageURL)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error with parse image URL: %v", err)
|
|
||||||
}
|
|
||||||
filename := filepath.Base(parse.Path)
|
|
||||||
filePath, err := utils.GenUploadPath(s.config.BasePath, filename)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error with generate image dir: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if useProxy {
|
|
||||||
err = utils.DownloadFile(imageURL, filePath, s.proxyURL)
|
|
||||||
} else {
|
|
||||||
err = utils.DownloadFile(imageURL, filePath, "")
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error with download image: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return utils.GenUploadUrl(s.config.BasePath, s.config.BaseURL, filePath), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s LocalStorage) Delete(fileURL string) error {
|
|
||||||
filePath := strings.Replace(fileURL, s.config.BaseURL, s.config.BasePath, 1)
|
|
||||||
return os.Remove(filePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Uploader = LocalStorage{}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
package oss
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core/types"
|
|
||||||
"chatplus/utils"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/minio/minio-go/v7"
|
|
||||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
|
||||||
"net/url"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MiniOss struct {
|
|
||||||
config *types.MiniOssConfig
|
|
||||||
client *minio.Client
|
|
||||||
proxyURL string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMiniOss(appConfig *types.AppConfig) (MiniOss, error) {
|
|
||||||
config := &appConfig.OSS.Minio
|
|
||||||
minioClient, err := minio.New(config.Endpoint, &minio.Options{
|
|
||||||
Creds: credentials.NewStaticV4(config.AccessKey, config.AccessSecret, ""),
|
|
||||||
Secure: config.UseSSL,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return MiniOss{}, err
|
|
||||||
}
|
|
||||||
return MiniOss{config: config, client: minioClient, proxyURL: appConfig.ProxyURL}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s MiniOss) PutImg(imageURL string, useProxy bool) (string, error) {
|
|
||||||
var imageData []byte
|
|
||||||
var err error
|
|
||||||
if useProxy {
|
|
||||||
imageData, err = utils.DownloadImage(imageURL, s.proxyURL)
|
|
||||||
} else {
|
|
||||||
imageData, err = utils.DownloadImage(imageURL, "")
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error with download image: %v", err)
|
|
||||||
}
|
|
||||||
parse, err := url.Parse(imageURL)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error with parse image URL: %v", err)
|
|
||||||
}
|
|
||||||
fileExt := filepath.Ext(parse.Path)
|
|
||||||
filename := fmt.Sprintf("%d%s", time.Now().UnixMicro(), fileExt)
|
|
||||||
info, err := s.client.PutObject(
|
|
||||||
context.Background(),
|
|
||||||
s.config.Bucket,
|
|
||||||
filename,
|
|
||||||
strings.NewReader(string(imageData)),
|
|
||||||
int64(len(imageData)),
|
|
||||||
minio.PutObjectOptions{ContentType: "image/png"})
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%s/%s/%s", s.config.Domain, s.config.Bucket, info.Key), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s MiniOss) PutFile(ctx *gin.Context, name string) (string, error) {
|
|
||||||
file, err := ctx.FormFile(name)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error with get form: %v", err)
|
|
||||||
}
|
|
||||||
// Open the uploaded file
|
|
||||||
fileReader, err := file.Open()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error opening file: %v", err)
|
|
||||||
}
|
|
||||||
defer fileReader.Close()
|
|
||||||
|
|
||||||
fileExt := filepath.Ext(file.Filename)
|
|
||||||
filename := fmt.Sprintf("%d%s", time.Now().UnixMicro(), fileExt)
|
|
||||||
info, err := s.client.PutObject(ctx, s.config.Bucket, filename, fileReader, file.Size, minio.PutObjectOptions{
|
|
||||||
ContentType: file.Header.Get("Content-Type"),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error uploading to MinIO: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s/%s/%s", s.config.Domain, s.config.Bucket, info.Key), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s MiniOss) Delete(fileURL string) error {
|
|
||||||
objectName := filepath.Base(fileURL)
|
|
||||||
return s.client.RemoveObject(context.Background(), s.config.Bucket, objectName, minio.RemoveObjectOptions{})
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Uploader = MiniOss{}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
package oss
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"chatplus/core/types"
|
|
||||||
"chatplus/utils"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/qiniu/go-sdk/v7/auth/qbox"
|
|
||||||
"github.com/qiniu/go-sdk/v7/storage"
|
|
||||||
"net/url"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type QinNiuOss struct {
|
|
||||||
config *types.QiNiuOssConfig
|
|
||||||
token string
|
|
||||||
uploader *storage.FormUploader
|
|
||||||
manager *storage.BucketManager
|
|
||||||
proxyURL string
|
|
||||||
dir string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewQiNiuOss(appConfig *types.AppConfig) QinNiuOss {
|
|
||||||
config := &appConfig.OSS.QiNiu
|
|
||||||
// build storage uploader
|
|
||||||
zone, ok := storage.GetRegionByID(storage.RegionID(config.Zone))
|
|
||||||
if !ok {
|
|
||||||
zone = storage.ZoneHuanan
|
|
||||||
}
|
|
||||||
storeConfig := storage.Config{Zone: &zone}
|
|
||||||
formUploader := storage.NewFormUploader(&storeConfig)
|
|
||||||
// generate token
|
|
||||||
mac := qbox.NewMac(config.AccessKey, config.AccessSecret)
|
|
||||||
putPolicy := storage.PutPolicy{
|
|
||||||
Scope: config.Bucket,
|
|
||||||
}
|
|
||||||
return QinNiuOss{
|
|
||||||
config: config,
|
|
||||||
token: putPolicy.UploadToken(mac),
|
|
||||||
uploader: formUploader,
|
|
||||||
manager: storage.NewBucketManager(mac, &storeConfig),
|
|
||||||
proxyURL: appConfig.ProxyURL,
|
|
||||||
dir: "chatgpt-plus",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s QinNiuOss) PutFile(ctx *gin.Context, name string) (string, error) {
|
|
||||||
// 解析表单
|
|
||||||
file, err := ctx.FormFile(name)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
// 打开上传文件
|
|
||||||
src, err := file.Open()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer src.Close()
|
|
||||||
|
|
||||||
fileExt := filepath.Ext(file.Filename)
|
|
||||||
key := fmt.Sprintf("%s/%d%s", s.dir, time.Now().UnixMicro(), fileExt)
|
|
||||||
// 上传文件
|
|
||||||
ret := storage.PutRet{}
|
|
||||||
extra := storage.PutExtra{}
|
|
||||||
err = s.uploader.Put(ctx, &ret, s.token, key, src, file.Size, &extra)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s/%s", s.config.Domain, ret.Key), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s QinNiuOss) PutImg(imageURL string, useProxy bool) (string, error) {
|
|
||||||
var imageData []byte
|
|
||||||
var err error
|
|
||||||
if useProxy {
|
|
||||||
imageData, err = utils.DownloadImage(imageURL, s.proxyURL)
|
|
||||||
} else {
|
|
||||||
imageData, err = utils.DownloadImage(imageURL, "")
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error with download image: %v", err)
|
|
||||||
}
|
|
||||||
parse, err := url.Parse(imageURL)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error with parse image URL: %v", err)
|
|
||||||
}
|
|
||||||
fileExt := filepath.Ext(parse.Path)
|
|
||||||
key := fmt.Sprintf("%s/%d%s", s.dir, time.Now().UnixMicro(), fileExt)
|
|
||||||
ret := storage.PutRet{}
|
|
||||||
extra := storage.PutExtra{}
|
|
||||||
// 上传文件字节数据
|
|
||||||
err = s.uploader.Put(context.Background(), &ret, s.token, key, bytes.NewReader(imageData), int64(len(imageData)), &extra)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%s/%s", s.config.Domain, ret.Key), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s QinNiuOss) Delete(fileURL string) error {
|
|
||||||
objectName := filepath.Base(fileURL)
|
|
||||||
key := fmt.Sprintf("%s/%s", s.dir, objectName)
|
|
||||||
return s.manager.Delete(s.config.Bucket, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Uploader = QinNiuOss{}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package oss
|
|
||||||
|
|
||||||
import "github.com/gin-gonic/gin"
|
|
||||||
|
|
||||||
type Uploader interface {
|
|
||||||
PutFile(ctx *gin.Context, name string) (string, error)
|
|
||||||
PutImg(imageURL string, useProxy bool) (string, error)
|
|
||||||
Delete(fileURL string) error
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
package oss
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core/types"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UploaderManager struct {
|
|
||||||
handler Uploader
|
|
||||||
}
|
|
||||||
|
|
||||||
const Local = "LOCAL"
|
|
||||||
const Minio = "MINIO"
|
|
||||||
const QiNiu = "QINIU"
|
|
||||||
const AliYun = "ALIYUN"
|
|
||||||
|
|
||||||
func NewUploaderManager(config *types.AppConfig) (*UploaderManager, error) {
|
|
||||||
active := Local
|
|
||||||
if config.OSS.Active != "" {
|
|
||||||
active = strings.ToUpper(config.OSS.Active)
|
|
||||||
}
|
|
||||||
var handler Uploader
|
|
||||||
switch active {
|
|
||||||
case Local:
|
|
||||||
handler = NewLocalStorage(config)
|
|
||||||
break
|
|
||||||
case Minio:
|
|
||||||
client, err := NewMiniOss(config)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
handler = client
|
|
||||||
break
|
|
||||||
case QiNiu:
|
|
||||||
handler = NewQiNiuOss(config)
|
|
||||||
break
|
|
||||||
case AliYun:
|
|
||||||
client, err := NewAliYunOss(config)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
handler = client
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
return &UploaderManager{handler: handler}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *UploaderManager) GetUploadHandler() Uploader {
|
|
||||||
return m.handler
|
|
||||||
}
|
|
||||||
@@ -1,305 +0,0 @@
|
|||||||
package sd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core/types"
|
|
||||||
"chatplus/service/oss"
|
|
||||||
"chatplus/store"
|
|
||||||
"chatplus/store/model"
|
|
||||||
"chatplus/store/vo"
|
|
||||||
"chatplus/utils"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"github.com/go-redis/redis/v8"
|
|
||||||
"github.com/imroc/req/v3"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SD 绘画服务
|
|
||||||
|
|
||||||
const RunningJobKey = "StableDiffusion_Running_Job"
|
|
||||||
|
|
||||||
type Service struct {
|
|
||||||
httpClient *req.Client
|
|
||||||
config *types.StableDiffusionConfig
|
|
||||||
taskQueue *store.RedisQueue
|
|
||||||
redis *redis.Client
|
|
||||||
db *gorm.DB
|
|
||||||
uploadManager *oss.UploaderManager
|
|
||||||
Clients *types.LMap[string, *types.WsClient] // SD 绘画页面 websocket 连接池
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewService(config *types.AppConfig, redisCli *redis.Client, db *gorm.DB, manager *oss.UploaderManager) *Service {
|
|
||||||
return &Service{
|
|
||||||
config: &config.SdConfig,
|
|
||||||
httpClient: req.C(),
|
|
||||||
redis: redisCli,
|
|
||||||
db: db,
|
|
||||||
uploadManager: manager,
|
|
||||||
Clients: types.NewLMap[string, *types.WsClient](),
|
|
||||||
taskQueue: store.NewRedisQueue("stable_diffusion_task_queue", redisCli),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) Run() {
|
|
||||||
logger.Info("Starting StableDiffusion job consumer.")
|
|
||||||
ctx := context.Background()
|
|
||||||
for {
|
|
||||||
_, err := s.redis.Get(ctx, RunningJobKey).Result()
|
|
||||||
if err == nil { // 队列串行执行
|
|
||||||
time.Sleep(time.Second * 3)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var task types.SdTask
|
|
||||||
err = s.taskQueue.LPop(&task)
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorf("taking task with error: %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
logger.Infof("Consuming Task: %+v", task)
|
|
||||||
err = s.Txt2Img(task)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("绘画任务执行失败:", err)
|
|
||||||
if task.RetryCount <= 5 {
|
|
||||||
s.taskQueue.RPush(task)
|
|
||||||
}
|
|
||||||
task.RetryCount += 1
|
|
||||||
time.Sleep(time.Second * 3)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新任务的执行状态
|
|
||||||
s.db.Model(&model.SdJob{}).Where("id = ?", task.Id).UpdateColumn("started", true)
|
|
||||||
// 锁定任务执行通道,直到任务超时(5分钟)
|
|
||||||
s.redis.Set(ctx, RunningJobKey, utils.JsonEncode(task), time.Minute*5)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PushTask 推送任务到队列
|
|
||||||
func (s *Service) PushTask(task types.SdTask) {
|
|
||||||
logger.Infof("add a new MidJourney Task: %+v", task)
|
|
||||||
s.taskQueue.RPush(task)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Txt2Img 文生图 API
|
|
||||||
func (s *Service) Txt2Img(task types.SdTask) error {
|
|
||||||
var taskInfo TaskInfo
|
|
||||||
bytes, err := os.ReadFile(s.config.Txt2ImgJsonPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error with load text2img json template file: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(bytes, &taskInfo)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error with decode json params: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
data := taskInfo.Data
|
|
||||||
params := task.Params
|
|
||||||
data[ParamKeys["task_id"]] = params.TaskId
|
|
||||||
data[ParamKeys["prompt"]] = params.Prompt
|
|
||||||
data[ParamKeys["negative_prompt"]] = params.NegativePrompt
|
|
||||||
data[ParamKeys["steps"]] = params.Steps
|
|
||||||
data[ParamKeys["sampler"]] = params.Sampler
|
|
||||||
data[ParamKeys["face_fix"]] = params.FaceFix
|
|
||||||
data[ParamKeys["cfg_scale"]] = params.CfgScale
|
|
||||||
data[ParamKeys["seed"]] = params.Seed
|
|
||||||
data[ParamKeys["height"]] = params.Height
|
|
||||||
data[ParamKeys["width"]] = params.Width
|
|
||||||
data[ParamKeys["hd_fix"]] = params.HdFix
|
|
||||||
data[ParamKeys["hd_redraw_rate"]] = params.HdRedrawRate
|
|
||||||
data[ParamKeys["hd_scale"]] = params.HdScale
|
|
||||||
data[ParamKeys["hd_scale_alg"]] = params.HdScaleAlg
|
|
||||||
data[ParamKeys["hd_sample_num"]] = params.HdSteps
|
|
||||||
|
|
||||||
taskInfo.SessionId = task.SessionId
|
|
||||||
taskInfo.TaskId = params.TaskId
|
|
||||||
taskInfo.Data = data
|
|
||||||
taskInfo.JobId = task.Id
|
|
||||||
go func() {
|
|
||||||
s.runTask(taskInfo, s.httpClient)
|
|
||||||
}()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 执行任务
|
|
||||||
func (s *Service) runTask(taskInfo TaskInfo, client *req.Client) {
|
|
||||||
body := map[string]any{
|
|
||||||
"data": taskInfo.Data,
|
|
||||||
"event_data": taskInfo.EventData,
|
|
||||||
"fn_index": taskInfo.FnIndex,
|
|
||||||
"session_hash": taskInfo.SessionHash,
|
|
||||||
}
|
|
||||||
logger.Debug(utils.JsonEncode(body))
|
|
||||||
var result = make(chan CBReq)
|
|
||||||
go func() {
|
|
||||||
var res struct {
|
|
||||||
Data []interface{} `json:"data"`
|
|
||||||
IsGenerating bool `json:"is_generating"`
|
|
||||||
Duration float64 `json:"duration"`
|
|
||||||
AverageDuration float64 `json:"average_duration"`
|
|
||||||
}
|
|
||||||
var cbReq = CBReq{TaskId: taskInfo.TaskId, JobId: taskInfo.JobId, SessionId: taskInfo.SessionId}
|
|
||||||
response, err := client.R().SetBody(body).SetSuccessResult(&res).Post(s.config.ApiURL + "/run/predict")
|
|
||||||
if err != nil {
|
|
||||||
cbReq.Message = "error with send request: " + err.Error()
|
|
||||||
cbReq.Success = false
|
|
||||||
result <- cbReq
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if response.IsErrorState() {
|
|
||||||
bytes, _ := io.ReadAll(response.Body)
|
|
||||||
cbReq.Message = "error http status code: " + string(bytes)
|
|
||||||
cbReq.Success = false
|
|
||||||
result <- cbReq
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var images []struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Data interface{} `json:"data"`
|
|
||||||
IsFile bool `json:"is_file"`
|
|
||||||
}
|
|
||||||
err = utils.ForceCovert(res.Data[0], &images)
|
|
||||||
if err != nil {
|
|
||||||
cbReq.Message = "error with decode image:" + err.Error()
|
|
||||||
cbReq.Success = false
|
|
||||||
result <- cbReq
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var info map[string]any
|
|
||||||
err = utils.JsonDecode(utils.InterfaceToString(res.Data[1]), &info)
|
|
||||||
if err != nil {
|
|
||||||
cbReq.Message = err.Error()
|
|
||||||
cbReq.Success = false
|
|
||||||
result <- cbReq
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取真实的 seed 值
|
|
||||||
cbReq.ImageName = images[0].Name
|
|
||||||
seed, _ := strconv.ParseInt(utils.InterfaceToString(info["seed"]), 10, 64)
|
|
||||||
cbReq.Seed = seed
|
|
||||||
cbReq.Success = true
|
|
||||||
cbReq.Progress = 100
|
|
||||||
result <- cbReq
|
|
||||||
close(result)
|
|
||||||
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case value := <-result:
|
|
||||||
s.callback(value)
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
var progressReq = map[string]any{
|
|
||||||
"id_task": taskInfo.TaskId,
|
|
||||||
"id_live_preview": 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
var progressRes struct {
|
|
||||||
Active bool `json:"active"`
|
|
||||||
Queued bool `json:"queued"`
|
|
||||||
Completed bool `json:"completed"`
|
|
||||||
Progress float64 `json:"progress"`
|
|
||||||
Eta float64 `json:"eta"`
|
|
||||||
LivePreview string `json:"live_preview"`
|
|
||||||
IDLivePreview int `json:"id_live_preview"`
|
|
||||||
TextInfo interface{} `json:"textinfo"`
|
|
||||||
}
|
|
||||||
response, err := client.R().SetBody(progressReq).SetSuccessResult(&progressRes).Post(s.config.ApiURL + "/internal/progress")
|
|
||||||
var cbReq = CBReq{TaskId: taskInfo.TaskId, Success: true, JobId: taskInfo.JobId, SessionId: taskInfo.SessionId}
|
|
||||||
if err != nil { // TODO: 这里可以考虑设置失败重试次数
|
|
||||||
logger.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if response.IsErrorState() {
|
|
||||||
bytes, _ := io.ReadAll(response.Body)
|
|
||||||
logger.Error(string(bytes))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cbReq.ImageData = progressRes.LivePreview
|
|
||||||
cbReq.Progress = int(progressRes.Progress * 100)
|
|
||||||
s.callback(cbReq)
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) callback(data CBReq) {
|
|
||||||
// 释放任务锁
|
|
||||||
s.redis.Del(context.Background(), RunningJobKey)
|
|
||||||
client := s.Clients.Get(data.SessionId)
|
|
||||||
if data.Success { // 任务成功
|
|
||||||
var job model.SdJob
|
|
||||||
res := s.db.Where("id = ?", data.JobId).First(&job)
|
|
||||||
if res.Error != nil {
|
|
||||||
logger.Warn("非法任务:", res.Error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 更新任务进度
|
|
||||||
job.Progress = data.Progress
|
|
||||||
// 更新任务 seed
|
|
||||||
var params types.SdTaskParams
|
|
||||||
err := utils.JsonDecode(job.Params, ¶ms)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("任务解析失败:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
params.Seed = data.Seed
|
|
||||||
if data.ImageName != "" { // 下载图片
|
|
||||||
imageURL := fmt.Sprintf("%s/file=%s", s.config.ApiURL, data.ImageName)
|
|
||||||
imageURL, err := s.uploadManager.GetUploadHandler().PutImg(imageURL, false)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("error with download img: ", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
job.ImgURL = imageURL
|
|
||||||
}
|
|
||||||
|
|
||||||
job.Params = utils.JsonEncode(params)
|
|
||||||
res = s.db.Updates(&job)
|
|
||||||
if res.Error != nil {
|
|
||||||
logger.Error("error with update job: ", res.Error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var jobVo vo.SdJob
|
|
||||||
err = utils.CopyObject(job, &jobVo)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("error with copy object: ", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if data.Progress < 100 && data.ImageData != "" {
|
|
||||||
jobVo.ImgURL = data.ImageData
|
|
||||||
}
|
|
||||||
|
|
||||||
// 推送任务到前端
|
|
||||||
if client != nil {
|
|
||||||
utils.ReplyChunkMessage(client, jobVo)
|
|
||||||
}
|
|
||||||
} else { // 任务失败
|
|
||||||
logger.Error("任务执行失败:", data.Message)
|
|
||||||
// 删除任务
|
|
||||||
s.db.Delete(&model.SdJob{Id: uint(data.JobId)})
|
|
||||||
// 推送消息到前端
|
|
||||||
if client != nil {
|
|
||||||
utils.ReplyChunkMessage(client, vo.SdJob{
|
|
||||||
Id: uint(data.JobId),
|
|
||||||
Progress: -1,
|
|
||||||
TaskId: data.TaskId,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package sd
|
|
||||||
|
|
||||||
import logger2 "chatplus/logger"
|
|
||||||
|
|
||||||
var logger = logger2.GetLogger()
|
|
||||||
|
|
||||||
type TaskInfo struct {
|
|
||||||
SessionId string `json:"session_id"`
|
|
||||||
JobId int `json:"job_id"`
|
|
||||||
TaskId string `json:"task_id"`
|
|
||||||
Data []interface{} `json:"data"`
|
|
||||||
EventData interface{} `json:"event_data"`
|
|
||||||
FnIndex int `json:"fn_index"`
|
|
||||||
SessionHash string `json:"session_hash"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CBReq struct {
|
|
||||||
SessionId string
|
|
||||||
JobId int
|
|
||||||
TaskId string
|
|
||||||
ImageName string
|
|
||||||
ImageData string
|
|
||||||
Progress int
|
|
||||||
Seed int64
|
|
||||||
Success bool
|
|
||||||
Message string
|
|
||||||
}
|
|
||||||
|
|
||||||
var ParamKeys = map[string]int{
|
|
||||||
"task_id": 0,
|
|
||||||
"prompt": 1,
|
|
||||||
"negative_prompt": 2,
|
|
||||||
"steps": 4,
|
|
||||||
"sampler": 5,
|
|
||||||
"face_fix": 6,
|
|
||||||
"cfg_scale": 10,
|
|
||||||
"seed": 11,
|
|
||||||
"height": 17,
|
|
||||||
"width": 18,
|
|
||||||
"hd_fix": 19,
|
|
||||||
"hd_redraw_rate": 20, //高清修复重绘幅度
|
|
||||||
"hd_scale": 21, // 高清修复放大倍数
|
|
||||||
"hd_scale_alg": 22, // 高清修复放大算法
|
|
||||||
"hd_sample_num": 23, // 高清修复采样次数
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
type SmsService interface {
|
|
||||||
SendVerifyCode(mobile string, code int) error
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
package wx
|
|
||||||
|
|
||||||
import (
|
|
||||||
logger2 "chatplus/logger"
|
|
||||||
"chatplus/store/model"
|
|
||||||
"github.com/eatmoreapple/openwechat"
|
|
||||||
"github.com/skip2/go-qrcode"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 微信收款机器人
|
|
||||||
var logger = logger2.GetLogger()
|
|
||||||
|
|
||||||
type Bot struct {
|
|
||||||
bot *openwechat.Bot
|
|
||||||
token string
|
|
||||||
db *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewWeChatBot(db *gorm.DB) *Bot {
|
|
||||||
bot := openwechat.DefaultBot(openwechat.Desktop)
|
|
||||||
return &Bot{
|
|
||||||
bot: bot,
|
|
||||||
db: db,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bot) Run() error {
|
|
||||||
logger.Info("Starting WeChat Bot...")
|
|
||||||
|
|
||||||
// set message handler
|
|
||||||
b.bot.MessageHandler = func(msg *openwechat.Message) {
|
|
||||||
b.messageHandler(msg)
|
|
||||||
}
|
|
||||||
// scan code login callback
|
|
||||||
b.bot.UUIDCallback = b.qrCodeCallBack
|
|
||||||
|
|
||||||
err := b.bot.Login()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info("微信登录成功!")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// message handler
|
|
||||||
func (b *Bot) messageHandler(msg *openwechat.Message) {
|
|
||||||
sender, err := msg.Sender()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 只处理微信支付的推送消息
|
|
||||||
if sender.NickName == "微信支付" ||
|
|
||||||
msg.MsgType == openwechat.MsgTypeApp ||
|
|
||||||
msg.AppMsgType == openwechat.AppMsgTypeUrl {
|
|
||||||
// 解析支付金额
|
|
||||||
message, err := parseTransactionMessage(msg.Content)
|
|
||||||
if err == nil {
|
|
||||||
transaction := extractTransaction(message)
|
|
||||||
logger.Infof("解析到收款信息:%+v", transaction)
|
|
||||||
var item model.Reward
|
|
||||||
res := b.db.Where("tx_id = ?", transaction.TransId).First(&item)
|
|
||||||
if res.Error == nil {
|
|
||||||
logger.Error("当前交易 ID 己经存在!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
res = b.db.Create(&model.Reward{
|
|
||||||
TxId: transaction.TransId,
|
|
||||||
Amount: transaction.Amount,
|
|
||||||
Remark: transaction.Remark,
|
|
||||||
Status: false,
|
|
||||||
})
|
|
||||||
if res.Error != nil {
|
|
||||||
logger.Errorf("交易保存失败: %v", res.Error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bot) qrCodeCallBack(uuid string) {
|
|
||||||
logger.Info("请使用微信扫描下面二维码登录")
|
|
||||||
q, _ := qrcode.New("https://login.weixin.qq.com/l/"+uuid, qrcode.Medium)
|
|
||||||
logger.Info(q.ToString(true))
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
package wx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Message 转账消息
|
|
||||||
type Message struct {
|
|
||||||
XMLName xml.Name `xml:"msg"`
|
|
||||||
AppMsg struct {
|
|
||||||
Des string `xml:"des"`
|
|
||||||
Url string `xml:"url"`
|
|
||||||
} `xml:"appmsg"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transaction 解析后的交易信息
|
|
||||||
type Transaction struct {
|
|
||||||
TransId string `json:"trans_id"` // 微信转账交易 ID
|
|
||||||
Amount float64 `json:"amount"` // 微信转账交易金额
|
|
||||||
Remark string `json:"remark"` // 转账备注
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析微信转账消息
|
|
||||||
func parseTransactionMessage(xmlData string) (*Message, error) {
|
|
||||||
var msg Message
|
|
||||||
if err := xml.Unmarshal([]byte(xmlData), &msg); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &msg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导出交易信息
|
|
||||||
func extractTransaction(message *Message) Transaction {
|
|
||||||
var tx = Transaction{}
|
|
||||||
// 导出交易金额和备注
|
|
||||||
lines := strings.Split(message.AppMsg.Des, "\n")
|
|
||||||
for _, line := range lines {
|
|
||||||
line = strings.TrimSpace(line)
|
|
||||||
if len(line) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// 解析收款金额
|
|
||||||
prefix := "收款金额¥"
|
|
||||||
if strings.HasPrefix(line, prefix) {
|
|
||||||
if value, err := strconv.ParseFloat(line[len(prefix):], 64); err == nil {
|
|
||||||
tx.Amount = value
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 解析收款备注
|
|
||||||
prefix = "付款方备注"
|
|
||||||
if strings.HasPrefix(line, prefix) {
|
|
||||||
tx.Remark = line[len(prefix):]
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析交易 ID
|
|
||||||
index := strings.Index(message.AppMsg.Url, "trans_id=")
|
|
||||||
if index != -1 {
|
|
||||||
end := strings.LastIndex(message.AppMsg.Url, "&")
|
|
||||||
tx.TransId = strings.TrimSpace(message.AppMsg.Url[index+9 : end])
|
|
||||||
}
|
|
||||||
return tx
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
hello, world!
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
// ApiKey OpenAI API 模型
|
|
||||||
type ApiKey struct {
|
|
||||||
BaseModel
|
|
||||||
Platform string
|
|
||||||
Value string // API Key 的值
|
|
||||||
LastUsedAt int64 // 最后使用时间
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
type BaseModel struct {
|
|
||||||
Id uint `gorm:"primarykey;column:id"`
|
|
||||||
CreatedAt time.Time
|
|
||||||
UpdatedAt time.Time
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
import "gorm.io/gorm"
|
|
||||||
|
|
||||||
type HistoryMessage struct {
|
|
||||||
BaseModel
|
|
||||||
ChatId string // 会话 ID
|
|
||||||
UserId uint // 用户 ID
|
|
||||||
RoleId uint // 角色 ID
|
|
||||||
Type string
|
|
||||||
Icon string
|
|
||||||
Tokens int
|
|
||||||
Content string
|
|
||||||
UseContext bool // 是否可以作为聊天上下文
|
|
||||||
DeletedAt gorm.DeletedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
func (HistoryMessage) TableName() string {
|
|
||||||
return "chatgpt_chat_history"
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
import "gorm.io/gorm"
|
|
||||||
|
|
||||||
type ChatItem struct {
|
|
||||||
BaseModel
|
|
||||||
ChatId string `gorm:"column:chat_id;unique"` // 会话 ID
|
|
||||||
UserId uint // 用户 ID
|
|
||||||
RoleId uint // 角色 ID
|
|
||||||
ModelId uint // 会话模型
|
|
||||||
Title string // 会话标题
|
|
||||||
DeletedAt gorm.DeletedAt
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
type ChatModel struct {
|
|
||||||
BaseModel
|
|
||||||
Platform string
|
|
||||||
Name string
|
|
||||||
Value string // API Key 的值
|
|
||||||
SortNum int
|
|
||||||
Enabled bool
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
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 // 是否启用被启用
|
|
||||||
SortNum int //排序数字
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
Id uint `gorm:"primarykey;column:id"`
|
|
||||||
Key string `gorm:"column:marker;unique"`
|
|
||||||
Config string `gorm:"column:config_json"`
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
type MidJourneyJob struct {
|
|
||||||
Id uint `gorm:"primarykey;column:id"`
|
|
||||||
Type string
|
|
||||||
UserId int
|
|
||||||
MessageId string
|
|
||||||
ReferenceId string
|
|
||||||
ImgURL string
|
|
||||||
Hash string // message hash
|
|
||||||
Progress int
|
|
||||||
Prompt string
|
|
||||||
Started bool
|
|
||||||
CreatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func (MidJourneyJob) TableName() string {
|
|
||||||
return "chatgpt_mj_jobs"
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
// 用户打赏
|
|
||||||
|
|
||||||
type Reward struct {
|
|
||||||
BaseModel
|
|
||||||
UserId uint // 用户 ID
|
|
||||||
TxId string // 交易ID
|
|
||||||
Amount float64 // 打赏金额
|
|
||||||
Remark string // 打赏备注
|
|
||||||
Status bool // 核销状态
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
type SdJob struct {
|
|
||||||
Id uint `gorm:"primarykey;column:id"`
|
|
||||||
Type string
|
|
||||||
UserId int
|
|
||||||
TaskId string
|
|
||||||
ImgURL string
|
|
||||||
Progress int
|
|
||||||
Prompt string
|
|
||||||
Params string
|
|
||||||
Started bool
|
|
||||||
CreatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func (SdJob) TableName() string {
|
|
||||||
return "chatgpt_sd_jobs"
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
type User struct {
|
|
||||||
BaseModel
|
|
||||||
Mobile string
|
|
||||||
Password string
|
|
||||||
Avatar string
|
|
||||||
Salt string // 密码盐
|
|
||||||
TotalTokens int64 // 总消耗 tokens
|
|
||||||
Calls int // 剩余对话次数
|
|
||||||
ImgCalls int // 剩余绘图次数
|
|
||||||
ChatConfig string `gorm:"column:chat_config_json"` // 聊天配置 json
|
|
||||||
ChatRoles string `gorm:"column:chat_roles_json"` // 聊天角色
|
|
||||||
ExpiredTime int64 // 账户到期时间
|
|
||||||
Status bool `gorm:"default:true"` // 当前状态
|
|
||||||
LastLoginAt int64 // 最后登录时间
|
|
||||||
LastLoginIp string // 最后登录 IP
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
type UserLoginLog struct {
|
|
||||||
BaseModel
|
|
||||||
UserId uint
|
|
||||||
Username string
|
|
||||||
LoginIp string
|
|
||||||
LoginAddress string
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package store
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core/types"
|
|
||||||
"context"
|
|
||||||
"github.com/go-redis/redis/v8"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewRedisClient(config *types.AppConfig) (*redis.Client, error) {
|
|
||||||
client := redis.NewClient(&redis.Options{
|
|
||||||
Addr: config.Redis.Url(),
|
|
||||||
Password: config.Redis.Password,
|
|
||||||
DB: config.Redis.DB,
|
|
||||||
})
|
|
||||||
_, err := client.Ping(context.Background()).Result()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package store
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/utils"
|
|
||||||
"context"
|
|
||||||
"github.com/go-redis/redis/v8"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RedisQueue struct {
|
|
||||||
name string
|
|
||||||
client *redis.Client
|
|
||||||
ctx context.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRedisQueue(name string, client *redis.Client) *RedisQueue {
|
|
||||||
return &RedisQueue{name: name, client: client, ctx: context.Background()}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *RedisQueue) RPush(value interface{}) {
|
|
||||||
q.client.RPush(q.ctx, q.name, utils.JsonEncode(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *RedisQueue) LPush(value interface{}) {
|
|
||||||
q.client.LPush(q.ctx, q.name, utils.JsonEncode(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *RedisQueue) LPop(value interface{}) error {
|
|
||||||
result, err := q.client.BLPop(q.ctx, 0, q.name).Result()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return utils.JsonDecode(result[1], value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *RedisQueue) RPop(value interface{}) error {
|
|
||||||
result, err := q.client.BRPop(q.ctx, 0, q.name).Result()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return utils.JsonDecode(result[1], value)
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package vo
|
|
||||||
|
|
||||||
// ApiKey OpenAI API 模型
|
|
||||||
type ApiKey struct {
|
|
||||||
BaseVo
|
|
||||||
Platform string `json:"platform"`
|
|
||||||
Value string `json:"value"` // API Key 的值
|
|
||||||
LastUsedAt int64 `json:"last_used_at"` // 最后使用时间
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package vo
|
|
||||||
|
|
||||||
type BaseVo struct {
|
|
||||||
Id uint `json:"id"`
|
|
||||||
CreatedAt int64 `json:"created_at"`
|
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
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"`
|
|
||||||
UseContext bool `json:"use_context"`
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
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"`
|
|
||||||
ModelId uint `json:"model_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package vo
|
|
||||||
|
|
||||||
type ChatModel struct {
|
|
||||||
BaseVo
|
|
||||||
Platform string `json:"platform"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
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"` // 是否启用被启用
|
|
||||||
SortNum int `json:"sort"` // 排序
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
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"`
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package vo
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
type MidJourneyJob struct {
|
|
||||||
Id uint `json:"id"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
UserId int `json:"user_id"`
|
|
||||||
MessageId string `json:"message_id"`
|
|
||||||
ReferenceId string `json:"reference_id"`
|
|
||||||
ImgURL string `json:"img_url"`
|
|
||||||
Hash string `json:"hash"`
|
|
||||||
Progress int `json:"progress"`
|
|
||||||
Prompt string `json:"prompt"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
Started bool `json:"started"`
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package vo
|
|
||||||
|
|
||||||
import "math"
|
|
||||||
|
|
||||||
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 := math.Ceil(float64(total) / float64(pageSize))
|
|
||||||
return Page{
|
|
||||||
Items: items,
|
|
||||||
Page: page,
|
|
||||||
PageSize: pageSize,
|
|
||||||
Total: total,
|
|
||||||
TotalPage: int(totalPage),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package vo
|
|
||||||
|
|
||||||
type Reward struct {
|
|
||||||
BaseVo
|
|
||||||
UserId uint `json:"user_id"` // 用户 ID
|
|
||||||
Username string `json:"username"`
|
|
||||||
TxId string `json:"tx_id"` // 交易ID
|
|
||||||
Amount float64 `json:"amount"` // 打赏金额
|
|
||||||
Remark string `json:"remark"` // 打赏备注
|
|
||||||
Status bool `json:"status"` // 核销状态
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package vo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core/types"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SdJob struct {
|
|
||||||
Id uint `json:"id"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
UserId int `json:"user_id"`
|
|
||||||
TaskId string `json:"task_id"`
|
|
||||||
ImgURL string `json:"img_url"`
|
|
||||||
Params types.SdTaskParams `json:"params"`
|
|
||||||
Progress int `json:"progress"`
|
|
||||||
Prompt string `json:"prompt"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
Started bool `json:"started"`
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package vo
|
|
||||||
|
|
||||||
import "chatplus/core/types"
|
|
||||||
|
|
||||||
type User struct {
|
|
||||||
BaseVo
|
|
||||||
Mobile string `json:"mobile"`
|
|
||||||
Avatar string `json:"avatar"`
|
|
||||||
Salt string `json:"salt"` // 密码盐
|
|
||||||
TotalTokens int64 `json:"total_tokens"` // 总消耗tokens
|
|
||||||
Calls int `json:"calls"` // 剩余对话次数
|
|
||||||
ImgCalls int `json:"img_calls"`
|
|
||||||
ChatConfig types.UserChatConfig `json:"chat_config"` // 聊天配置
|
|
||||||
ChatRoles []string `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
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package vo
|
|
||||||
|
|
||||||
type UserLoginLog struct {
|
|
||||||
BaseVo
|
|
||||||
UserId uint `json:"user_id"`
|
|
||||||
Username string `json:"username"`
|
|
||||||
LoginIp string `json:"login_ip"`
|
|
||||||
LoginAddress string `json:"login_address"`
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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 string,dst 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())
|
|
||||||
}
|
|
||||||
// map, struct, slice to string
|
|
||||||
} 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 {
|
|
||||||
val := string(ba)
|
|
||||||
if strings.Contains(val, "{") {
|
|
||||||
value.Set(reflect.ValueOf(string(ba)))
|
|
||||||
} else {
|
|
||||||
value.Set(reflect.ValueOf(""))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} 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])
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsEmptyValue(obj interface{}) bool {
|
|
||||||
if obj == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
v := reflect.ValueOf(obj)
|
|
||||||
switch v.Kind() {
|
|
||||||
case reflect.Ptr, reflect.Interface:
|
|
||||||
return v.IsNil()
|
|
||||||
case reflect.Array, reflect.Slice, reflect.Map, reflect.String:
|
|
||||||
return v.Len() == 0
|
|
||||||
case reflect.Bool:
|
|
||||||
return !v.Bool()
|
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
||||||
return v.Int() == 0
|
|
||||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
|
||||||
return v.Uint() == 0
|
|
||||||
case reflect.Float32, reflect.Float64:
|
|
||||||
return v.Float() == 0
|
|
||||||
case reflect.Complex64, reflect.Complex128:
|
|
||||||
return v.Complex() == 0
|
|
||||||
default:
|
|
||||||
return reflect.DeepEqual(obj, reflect.Zero(reflect.TypeOf(obj)).Interface())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BoolValue(str string) bool {
|
|
||||||
value, err := strconv.ParseBool(str)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
func FloatValue(str string) float64 {
|
|
||||||
value, err := strconv.ParseFloat(str, 64)
|
|
||||||
if err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
func IntValue(str string, defaultValue int) int {
|
|
||||||
value, err := strconv.Atoi(str)
|
|
||||||
if err != nil {
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
func ForceCovert(src any, dst interface{}) error {
|
|
||||||
bytes, err := json.Marshal(src)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = json.Unmarshal(bytes, dst)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/aes"
|
|
||||||
"crypto/cipher"
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AesEncrypt 加密
|
|
||||||
func AesEncrypt(keyStr string, data []byte) (string, error) {
|
|
||||||
//创建加密实例
|
|
||||||
key := []byte(keyStr)
|
|
||||||
block, err := aes.NewCipher(key)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
blockSize := block.BlockSize()
|
|
||||||
encryptBytes := pkcs7Padding(data, blockSize)
|
|
||||||
result := make([]byte, len(encryptBytes))
|
|
||||||
//使用cbc加密模式
|
|
||||||
blockMode := cipher.NewCBCEncrypter(block, key[:blockSize])
|
|
||||||
//执行加密
|
|
||||||
blockMode.CryptBlocks(result, encryptBytes)
|
|
||||||
return base64.StdEncoding.EncodeToString(result), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AesDecrypt 解密
|
|
||||||
func AesDecrypt(keyStr string, dataStr string) ([]byte, error) {
|
|
||||||
//创建实例
|
|
||||||
key := []byte(keyStr)
|
|
||||||
block, err := aes.NewCipher(key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := base64.StdEncoding.DecodeString(dataStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
blockSize := block.BlockSize()
|
|
||||||
blockMode := cipher.NewCBCDecrypter(block, key[:blockSize])
|
|
||||||
result := make([]byte, len(data))
|
|
||||||
//执行解密
|
|
||||||
blockMode.CryptBlocks(result, data)
|
|
||||||
//去除填充
|
|
||||||
result, err = pkcs7UnPadding(result)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func pkcs7Padding(data []byte, blockSize int) []byte {
|
|
||||||
padding := blockSize - len(data)%blockSize
|
|
||||||
padText := bytes.Repeat([]byte{byte(padding)}, padding)
|
|
||||||
return append(data, padText...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func pkcs7UnPadding(data []byte) ([]byte, error) {
|
|
||||||
length := len(data)
|
|
||||||
if length == 0 {
|
|
||||||
return nil, errors.New("empty encrypt data")
|
|
||||||
}
|
|
||||||
unPadding := int(data[length-1])
|
|
||||||
return data[:(length - unPadding)], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func Sha256(data string) string {
|
|
||||||
hash := sha256.New()
|
|
||||||
_, err := io.WriteString(hash, data)
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
hashValue := hash.Sum(nil)
|
|
||||||
return fmt.Sprintf("%x", hashValue)
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user