mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-04-10 05:04:26 +08:00
Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6cfc7175e8 | ||
|
|
a3f6a641aa | ||
|
|
54fe49de5d | ||
|
|
454dfc1aa7 | ||
|
|
3156701d4e | ||
|
|
41eb0e634a | ||
|
|
73d003d6c3 | ||
|
|
5e4ba6d971 | ||
|
|
76d32c78d8 | ||
|
|
66776556d8 | ||
|
|
149f598f6d | ||
|
|
1d6f0ab714 | ||
|
|
545f257476 | ||
|
|
6cb1f16f56 | ||
|
|
598f6c48fb | ||
|
|
97e489901a | ||
|
|
eea57790de | ||
|
|
81b32523ed | ||
|
|
6b6fe1bebd | ||
|
|
a7063bf30a | ||
|
|
32fc4d86a2 | ||
|
|
e685876cc0 | ||
|
|
41e4b1c7ac | ||
|
|
76a3ada85f | ||
|
|
b1ddcef593 | ||
|
|
94a5187e75 | ||
|
|
521ca77541 | ||
|
|
5dd3c1835a | ||
|
|
9fb01ee3ee | ||
|
|
9d72edc048 | ||
|
|
a9505ff72d | ||
|
|
628ae15fd7 | ||
|
|
1ce71374ac | ||
|
|
7c81d946a7 | ||
|
|
cdaeb2a404 | ||
|
|
ca2de54438 | ||
|
|
303e9ed052 | ||
|
|
347b640614 | ||
|
|
19099aed6f | ||
|
|
615515094b | ||
|
|
c4fe6c825e | ||
|
|
26c18fcd5a | ||
|
|
a914994483 | ||
|
|
9edd6621b1 | ||
|
|
10e3e61b2c | ||
|
|
643cf6085a | ||
|
|
fa74ae18ee | ||
|
|
dffdbf697b | ||
|
|
5e59b3a708 | ||
|
|
7bc55f3ed1 | ||
|
|
73f5a44e0a | ||
|
|
2c6abbe7e4 | ||
|
|
1f0cf11636 | ||
|
|
c44f5d40fe | ||
|
|
314d81303b | ||
|
|
b9859e5591 | ||
|
|
a3d65ba939 | ||
|
|
8a2d2f66b5 | ||
|
|
d1c9fd6eba | ||
|
|
d629d842be | ||
|
|
f752ec5b06 | ||
|
|
c7b09f29ca | ||
|
|
51c270fb29 | ||
|
|
b97d4b7895 | ||
|
|
0627109b2b | ||
|
|
c2d4530395 | ||
|
|
c3be47d4ce |
33
CHANGELOG.md
33
CHANGELOG.md
@@ -1,5 +1,38 @@
|
||||
# 更新日志
|
||||
|
||||
## v4.2.5
|
||||
|
||||
- 功能优化:在代码右下角增加复制代码功能按钮,增加收起和展开代码功能
|
||||
- Bug 修复:修复 Shift + Enter 不换行的 Bug
|
||||
- Bug 修复:修复管理后台菜单添加页面的文本错误
|
||||
- Bug 修复:解决聊天页面异常退出不断重连的 bug
|
||||
- 功能优化:把 Luma 和可灵视频生成页面整合成一个视频创作中心页面,统一管理视频任务
|
||||
- 功能新增:增加即梦 AI 专题页面,支持即梦官方原生 API 的图片和视频生成 🎉🎉🎉
|
||||
|
||||
## v4.2.4
|
||||
|
||||
- 功能优化:更改前端构建技术选型,使用 Vite 构建,提升构建速度和兼容性
|
||||
- 功能优化:使用 SSE 发送消息,替换原来的 Websocket 消息方案
|
||||
- 功能新增:管理后台支持设置默认昵称
|
||||
- 功能优化:支持 Suno v4.5 模型支持
|
||||
- 功能新增:用户注册和用户登录增加用户协议和隐私政策功能,需要用户同意协议才可注册和登录。
|
||||
- 功能优化:修改重新回答功能,撤回千面的问答内容为可编辑内容,撤回的内容不会增加额外的上下文
|
||||
- 功能优化:优化聊天记录的存储结构,增加模型名称字段,支持存储更长的模型名称
|
||||
- Bug 修复:聊天应用绑定模型后无效,还是会轮询 API KEY,导致一会成功,一会请求失败。
|
||||
- 功能优化:如果管理后台没有启用会员充值菜单,移动端也不显示充值套餐功能
|
||||
|
||||
## v4.2.3
|
||||
|
||||
- 功能优化:增加模型分组与模型描述,采用卡片展示模式改进模型选择功能体验
|
||||
- 功能优化:化思维导图下载图片的清晰度以及解决拖动、缩放操作后下载图片内容不全问题
|
||||
- Bug 修复:修复 MJ 画图页面已画出的图,点复制指令无效问题
|
||||
- 功能优化:MJ 画图的分辨率支持自定义,优先使用 prompt 中--ar 参数
|
||||
- Bug 修复:修复 MJ 绘画 U1-V1,拼写错误
|
||||
- 功能优化:支持自动迁移数据表结构,无需在手动执行 SQL 了
|
||||
- 功能优化:移除首页的文字动画效果
|
||||
- 功能优化:在聊天页面增加对话列表展开和隐藏功能
|
||||
- 功能优化:聊天页面增加 AI 思考中动画效果
|
||||
|
||||
## v4.2.2
|
||||
|
||||
- 功能优化:开启图形验证码功能的时候现检查是否配置了 API 服务,防止开启之后没法登录的 Bug。
|
||||
|
||||
66
CLAUDE.md
Normal file
66
CLAUDE.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Build Commands
|
||||
|
||||
### Go Backend (api/)
|
||||
- **Development**: `cd api && go run main.go` (uses config.toml)
|
||||
- **Build**: `cd api && make` (builds both amd64 and arm64 binaries)
|
||||
- **Individual builds**: `make amd64` or `make arm64`
|
||||
- **Clean**: `make clean`
|
||||
- **Config**: Copy `config.sample.toml` to `config.toml` and configure
|
||||
|
||||
### Web Frontend (web/)
|
||||
- **Development**: `cd web && npm run dev` (runs on Vite dev server with --host)
|
||||
- **Build**: `cd web && npm run build`
|
||||
- **Lint**: `cd web && npm run lint` (ESLint with auto-fix)
|
||||
|
||||
### Testing
|
||||
- Backend tests: `cd api/test && bash run_crawler_test.sh`
|
||||
- No specific frontend test configuration found
|
||||
|
||||
## Project Architecture
|
||||
|
||||
### Backend (Go)
|
||||
- **Framework**: Gin web framework with dependency injection via uber-go/fx
|
||||
- **Database**: GORM with MySQL, Redis for caching, LevelDB for local storage
|
||||
- **Authentication**: JWT tokens with Redis session storage
|
||||
- **Middleware**: CORS, authorization, parameter handling, static resource serving
|
||||
- **Structure**:
|
||||
- `handler/`: HTTP request handlers (REST API endpoints)
|
||||
- `service/`: Business logic services (AI integrations, payments, etc.)
|
||||
- `store/`: Database models and data access layer
|
||||
- `core/`: Application server and middleware configuration
|
||||
- `utils/`: Utility functions and helpers
|
||||
|
||||
### Frontend (Vue.js)
|
||||
- **Framework**: Vue 3 with Composition API
|
||||
- **UI Components**: Element Plus + Vant (mobile components)
|
||||
- **State Management**: Pinia
|
||||
- **Routing**: Vue Router with nested routes
|
||||
- **Build Tool**: Vite
|
||||
- **CSS**: Stylus preprocessor with Tailwind CSS utilities
|
||||
- **Features**: Responsive design (desktop/mobile views), theme switching (dark/light)
|
||||
|
||||
### Key Features
|
||||
- **AI Chat**: Multiple chat models and conversation management
|
||||
- **Image Generation**: MidJourney, Stable Diffusion, DALL-E integration
|
||||
- **Audio/Video**: Suno music creation, Luma/KeLing video generation
|
||||
- **User Management**: Authentication, payments, power logs, invitations
|
||||
- **Admin Panel**: Comprehensive management interface
|
||||
|
||||
### Database Models
|
||||
Key entities: User, ChatItem, ChatMessage, ChatRole, ChatModel, Order, Product, AdminUser, and various job types for AI services.
|
||||
|
||||
### API Structure
|
||||
- User APIs: `/api/user/*` (auth, profile, settings)
|
||||
- Chat APIs: `/api/chat/*` (conversations, messages)
|
||||
- AI Service APIs: `/api/mj/*`, `/api/sd/*`, `/api/dall/*`, `/api/suno/*`, `/api/video/*`
|
||||
- Admin APIs: `/api/admin/*` (management functions)
|
||||
|
||||
### Configuration
|
||||
- Backend: TOML configuration file (`config.toml`)
|
||||
- Database: MySQL with automatic migrations
|
||||
- Services: Redis, various AI API integrations
|
||||
- File Storage: Local, Aliyun OSS, MinIO, Qiniu options
|
||||
195
JIMENG_CONFIG_README.md
Normal file
195
JIMENG_CONFIG_README.md
Normal file
@@ -0,0 +1,195 @@
|
||||
# 即梦 AI 配置功能说明
|
||||
|
||||
## 功能概述
|
||||
|
||||
即梦 AI 配置功能允许管理员通过 Web 界面配置即梦 AI 的 API 密钥和算力消耗设置,支持动态配置更新,无需重启服务。
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 1. 秘钥配置
|
||||
|
||||
- AccessKey 和 SecretKey 配置
|
||||
- 支持密码显示/隐藏
|
||||
- 连接测试功能
|
||||
|
||||
### 2. 算力配置
|
||||
|
||||
- 文生图算力消耗
|
||||
- 图生图算力消耗
|
||||
- 图片编辑算力消耗
|
||||
- 图片特效算力消耗
|
||||
- 文生视频算力消耗
|
||||
- 图生视频算力消耗
|
||||
|
||||
### 3. 动态配置
|
||||
|
||||
- 配置实时生效
|
||||
- 无需重启服务
|
||||
- 支持配置验证
|
||||
|
||||
## API 接口
|
||||
|
||||
### 获取配置
|
||||
|
||||
```
|
||||
GET /api/admin/jimeng/config
|
||||
```
|
||||
|
||||
### 更新配置
|
||||
|
||||
```
|
||||
POST /api/admin/jimeng/config
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"config": {
|
||||
"access_key": "your_access_key",
|
||||
"secret_key": "your_secret_key",
|
||||
"power": {
|
||||
"text_to_image": 10,
|
||||
"image_to_image": 15,
|
||||
"image_edit": 20,
|
||||
"image_effects": 25,
|
||||
"text_to_video": 30,
|
||||
"image_to_video": 35
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 测试连接
|
||||
|
||||
```
|
||||
POST /api/admin/jimeng/config/test
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"config": {
|
||||
"access_key": "your_access_key",
|
||||
"secret_key": "your_secret_key"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 前端页面
|
||||
|
||||
### 访问路径
|
||||
|
||||
管理后台 -> 即梦 AI -> 配置设置
|
||||
|
||||
### 页面功能
|
||||
|
||||
1. **秘钥配置标签页**
|
||||
|
||||
- AccessKey 输入框(密码模式)
|
||||
- SecretKey 输入框(密码模式)
|
||||
- 测试连接按钮
|
||||
|
||||
2. **算力配置标签页**
|
||||
|
||||
- 各种任务类型的算力消耗配置
|
||||
- 数字输入框,支持 1-100 范围
|
||||
- 提示信息说明
|
||||
|
||||
3. **操作按钮**
|
||||
- 保存配置
|
||||
- 重置配置
|
||||
|
||||
## 配置存储
|
||||
|
||||
配置存储在数据库的`config`表中:
|
||||
|
||||
- 配置键:`jimeng`
|
||||
- 配置值:JSON 格式的即梦 AI 配置
|
||||
|
||||
## 默认配置
|
||||
|
||||
如果配置不存在,系统会使用以下默认值:
|
||||
|
||||
```json
|
||||
{
|
||||
"access_key": "",
|
||||
"secret_key": "",
|
||||
"power": {
|
||||
"text_to_image": 10,
|
||||
"image_to_image": 15,
|
||||
"image_edit": 20,
|
||||
"image_effects": 25,
|
||||
"text_to_video": 30,
|
||||
"image_to_video": 35
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 使用流程
|
||||
|
||||
1. **初始配置**
|
||||
|
||||
- 访问管理后台即梦 AI 配置页面
|
||||
- 填写 AccessKey 和 SecretKey
|
||||
- 点击"测试连接"验证配置
|
||||
- 调整各功能算力消耗
|
||||
- 保存配置
|
||||
|
||||
2. **配置更新**
|
||||
|
||||
- 修改需要更新的配置项
|
||||
- 保存配置
|
||||
- 配置立即生效
|
||||
|
||||
3. **故障排查**
|
||||
- 使用"测试连接"功能验证 API 密钥
|
||||
- 检查配置是否正确保存
|
||||
- 查看服务日志
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **权限要求**
|
||||
|
||||
- 只有管理员可以访问配置页面
|
||||
- 需要有效的管理员登录会话
|
||||
|
||||
2. **配置验证**
|
||||
|
||||
- AccessKey 和 SecretKey 不能为空
|
||||
- 算力消耗必须大于 0
|
||||
- 建议先测试连接再保存配置
|
||||
|
||||
3. **服务影响**
|
||||
- 配置更新不会影响正在进行的任务
|
||||
- 新任务会使用更新后的配置
|
||||
- 客户端配置会在下次请求时更新
|
||||
|
||||
## 错误处理
|
||||
|
||||
1. **配置加载失败**
|
||||
|
||||
- 使用默认配置
|
||||
- 记录错误日志
|
||||
|
||||
2. **连接测试失败**
|
||||
|
||||
- 显示具体错误信息
|
||||
- 建议检查 API 密钥
|
||||
|
||||
3. **配置保存失败**
|
||||
- 显示错误信息
|
||||
- 保留原有配置
|
||||
|
||||
## 开发说明
|
||||
|
||||
### 后端文件
|
||||
|
||||
- `api/handler/admin/jimeng_handler.go` - 配置管理 API
|
||||
- `api/service/jimeng/service.go` - 配置服务逻辑
|
||||
- `api/core/types/jimeng.go` - 配置类型定义
|
||||
|
||||
### 前端文件
|
||||
|
||||
- `web/src/views/admin/jimeng/JimengSetting.vue` - 配置页面
|
||||
|
||||
### 数据库
|
||||
|
||||
- `config`表存储配置信息
|
||||
- 配置键:`jimeng`
|
||||
- 配置值:JSON 格式
|
||||
1
api/.gitignore
vendored
1
api/.gitignore
vendored
@@ -17,5 +17,6 @@ bin
|
||||
data
|
||||
config.toml
|
||||
static/upload
|
||||
static/audio
|
||||
storage.json
|
||||
res/certs/wechat/apiclient_key.pem
|
||||
|
||||
@@ -34,8 +34,51 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AuthConfig 定义授权配置
|
||||
type AuthConfig struct {
|
||||
ExactPaths map[string]bool // 精确匹配的路径
|
||||
PrefixPaths map[string]bool // 前缀匹配的路径
|
||||
}
|
||||
|
||||
var authConfig = &AuthConfig{
|
||||
ExactPaths: map[string]bool{
|
||||
"/api/user/login": false,
|
||||
"/api/user/logout": false,
|
||||
"/api/user/resetPass": false,
|
||||
"/api/user/register": false,
|
||||
"/api/admin/login": false,
|
||||
"/api/admin/logout": false,
|
||||
"/api/admin/login/captcha": false,
|
||||
"/api/app/list": false,
|
||||
"/api/app/type/list": false,
|
||||
"/api/app/list/user": false,
|
||||
"/api/model/list": false,
|
||||
"/api/mj/imgWall": false,
|
||||
"/api/mj/notify": false,
|
||||
"/api/invite/hits": false,
|
||||
"/api/sd/imgWall": false,
|
||||
"/api/dall/imgWall": false,
|
||||
"/api/product/list": false,
|
||||
"/api/menu/list": false,
|
||||
"/api/markMap/client": false,
|
||||
"/api/payment/doPay": false,
|
||||
"/api/payment/payWays": false,
|
||||
"/api/download": false,
|
||||
"/api/dall/models": false,
|
||||
},
|
||||
PrefixPaths: map[string]bool{
|
||||
"/api/test/": false,
|
||||
"/api/payment/notify/": false,
|
||||
"/api/user/clogin": false,
|
||||
"/api/config/": false,
|
||||
"/api/function/": false,
|
||||
"/api/sms/": false,
|
||||
"/api/captcha/": false,
|
||||
"/static/": false,
|
||||
},
|
||||
}
|
||||
|
||||
type AppServer struct {
|
||||
Debug bool
|
||||
Config *types.AppConfig
|
||||
Engine *gin.Engine
|
||||
SysConfig *types.SystemConfig // system config cache
|
||||
@@ -45,7 +88,6 @@ func NewServer(appConfig *types.AppConfig) *AppServer {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
gin.DefaultWriter = io.Discard
|
||||
return &AppServer{
|
||||
Debug: false,
|
||||
Config: appConfig,
|
||||
Engine: gin.Default(),
|
||||
}
|
||||
@@ -62,16 +104,83 @@ func (s *AppServer) Init(debug bool, client *redis.Client) {
|
||||
}
|
||||
|
||||
func (s *AppServer) Run(db *gorm.DB) error {
|
||||
|
||||
// 重命名 config 表字段
|
||||
if db.Migrator().HasColumn(&model.Config{}, "config_json") {
|
||||
db.Migrator().RenameColumn(&model.Config{}, "config_json", "value")
|
||||
}
|
||||
if db.Migrator().HasColumn(&model.Config{}, "marker") {
|
||||
db.Migrator().RenameColumn(&model.Config{}, "marker", "name")
|
||||
}
|
||||
if db.Migrator().HasIndex(&model.Config{}, "idx_chatgpt_configs_key") {
|
||||
db.Migrator().DropIndex(&model.Config{}, "idx_chatgpt_configs_key")
|
||||
}
|
||||
if db.Migrator().HasIndex(&model.Config{}, "marker") {
|
||||
db.Migrator().DropIndex(&model.Config{}, "marker")
|
||||
}
|
||||
|
||||
// load system configs
|
||||
var sysConfig model.Config
|
||||
err := db.Where("marker", "system").First(&sysConfig).Error
|
||||
err := db.Where("name", "system").First(&sysConfig).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load system config: %v", err)
|
||||
}
|
||||
err = utils.JsonDecode(sysConfig.Config, &s.SysConfig)
|
||||
err = utils.JsonDecode(sysConfig.Value, &s.SysConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode system config: %v", err)
|
||||
}
|
||||
|
||||
// 迁移数据表
|
||||
logger.Info("Migrating database tables...")
|
||||
db.AutoMigrate(
|
||||
&model.ChatItem{},
|
||||
&model.ChatMessage{},
|
||||
&model.ChatRole{},
|
||||
&model.ChatModel{},
|
||||
&model.InviteCode{},
|
||||
&model.InviteLog{},
|
||||
&model.Menu{},
|
||||
&model.Order{},
|
||||
&model.Product{},
|
||||
&model.User{},
|
||||
&model.Function{},
|
||||
&model.File{},
|
||||
&model.Redeem{},
|
||||
&model.Config{},
|
||||
&model.ApiKey{},
|
||||
&model.AdminUser{},
|
||||
&model.AppType{},
|
||||
&model.SdJob{},
|
||||
&model.SunoJob{},
|
||||
&model.PowerLog{},
|
||||
&model.VideoJob{},
|
||||
&model.MidJourneyJob{},
|
||||
&model.UserLoginLog{},
|
||||
&model.DallJob{},
|
||||
&model.JimengJob{},
|
||||
)
|
||||
// 手动删除字段
|
||||
if db.Migrator().HasColumn(&model.Order{}, "deleted_at") {
|
||||
db.Migrator().DropColumn(&model.Order{}, "deleted_at")
|
||||
}
|
||||
if db.Migrator().HasColumn(&model.ChatItem{}, "deleted_at") {
|
||||
db.Migrator().DropColumn(&model.ChatItem{}, "deleted_at")
|
||||
}
|
||||
if db.Migrator().HasColumn(&model.ChatMessage{}, "deleted_at") {
|
||||
db.Migrator().DropColumn(&model.ChatMessage{}, "deleted_at")
|
||||
}
|
||||
if db.Migrator().HasColumn(&model.User{}, "chat_config") {
|
||||
db.Migrator().DropColumn(&model.User{}, "chat_config")
|
||||
}
|
||||
if db.Migrator().HasColumn(&model.ChatModel{}, "category") {
|
||||
db.Migrator().DropColumn(&model.ChatModel{}, "category")
|
||||
}
|
||||
if db.Migrator().HasColumn(&model.ChatModel{}, "description") {
|
||||
db.Migrator().DropColumn(&model.ChatModel{}, "description")
|
||||
}
|
||||
|
||||
logger.Info("Database tables migrated successfully")
|
||||
|
||||
// 统计安装信息
|
||||
go func() {
|
||||
info, err := host.Info()
|
||||
@@ -112,14 +221,14 @@ 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)
|
||||
} else {
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
}
|
||||
|
||||
|
||||
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
|
||||
//允许跨域设置可以返回其他子段,可以自定义字段
|
||||
c.Header("Access-Control-Allow-Headers", "Authorization, Body-Length, Body-Type, Admin-Authorization,content-type")
|
||||
@@ -147,6 +256,11 @@ func corsMiddleware() gin.HandlerFunc {
|
||||
// 用户授权验证
|
||||
func authorizeMiddleware(s *AppServer, client *redis.Client) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if !needLogin(c) {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
clientProtocols := c.GetHeader("Sec-WebSocket-Protocol")
|
||||
var tokenString string
|
||||
isAdminApi := strings.Contains(c.Request.URL.Path, "/api/admin/")
|
||||
@@ -165,18 +279,13 @@ func authorizeMiddleware(s *AppServer, client *redis.Client) gin.HandlerFunc {
|
||||
}
|
||||
|
||||
if tokenString == "" {
|
||||
if needLogin(c) {
|
||||
resp.NotAuth(c, "You should put Authorization in request headers")
|
||||
c.Abort()
|
||||
return
|
||||
} else { // 直接放行
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
resp.NotAuth(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 && needLogin(c) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
if isAdminApi {
|
||||
@@ -187,21 +296,21 @@ func authorizeMiddleware(s *AppServer, client *redis.Client) gin.HandlerFunc {
|
||||
|
||||
})
|
||||
|
||||
if err != nil && needLogin(c) {
|
||||
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 && needLogin(c) {
|
||||
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() && needLogin(c) {
|
||||
if expr > 0 && int64(expr) < time.Now().Unix() {
|
||||
resp.NotAuth(c, "Token is expired")
|
||||
c.Abort()
|
||||
return
|
||||
@@ -211,57 +320,48 @@ func authorizeMiddleware(s *AppServer, client *redis.Client) gin.HandlerFunc {
|
||||
if isAdminApi {
|
||||
key = fmt.Sprintf("admin/%v", claims["user_id"])
|
||||
}
|
||||
if _, err := client.Get(context.Background(), key).Result(); err != nil && needLogin(c) {
|
||||
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"])
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func needLogin(c *gin.Context) bool {
|
||||
if c.Request.URL.Path == "/api/user/login" ||
|
||||
c.Request.URL.Path == "/api/user/logout" ||
|
||||
c.Request.URL.Path == "/api/user/resetPass" ||
|
||||
c.Request.URL.Path == "/api/admin/login" ||
|
||||
c.Request.URL.Path == "/api/admin/logout" ||
|
||||
c.Request.URL.Path == "/api/admin/login/captcha" ||
|
||||
c.Request.URL.Path == "/api/user/register" ||
|
||||
c.Request.URL.Path == "/api/chat/history" ||
|
||||
c.Request.URL.Path == "/api/chat/detail" ||
|
||||
c.Request.URL.Path == "/api/chat/list" ||
|
||||
c.Request.URL.Path == "/api/app/list" ||
|
||||
c.Request.URL.Path == "/api/app/type/list" ||
|
||||
c.Request.URL.Path == "/api/app/list/user" ||
|
||||
c.Request.URL.Path == "/api/model/list" ||
|
||||
c.Request.URL.Path == "/api/mj/imgWall" ||
|
||||
c.Request.URL.Path == "/api/mj/notify" ||
|
||||
c.Request.URL.Path == "/api/invite/hits" ||
|
||||
c.Request.URL.Path == "/api/sd/imgWall" ||
|
||||
c.Request.URL.Path == "/api/dall/imgWall" ||
|
||||
c.Request.URL.Path == "/api/product/list" ||
|
||||
c.Request.URL.Path == "/api/menu/list" ||
|
||||
c.Request.URL.Path == "/api/markMap/client" ||
|
||||
c.Request.URL.Path == "/api/payment/doPay" ||
|
||||
c.Request.URL.Path == "/api/payment/payWays" ||
|
||||
c.Request.URL.Path == "/api/suno/detail" ||
|
||||
c.Request.URL.Path == "/api/suno/play" ||
|
||||
c.Request.URL.Path == "/api/download" ||
|
||||
c.Request.URL.Path == "/api/dall/models" ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/test") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/payment/notify/") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/user/clogin") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/config/") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/function/") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/sms/") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/api/captcha/") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/static/") {
|
||||
path := c.Request.URL.Path
|
||||
|
||||
// 如果不是 API 路径,不需要登录
|
||||
if !strings.HasPrefix(path, "/api") {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查精确匹配的路径
|
||||
if skip, exists := authConfig.ExactPaths[path]; exists {
|
||||
return skip
|
||||
}
|
||||
|
||||
// 检查前缀匹配的路径
|
||||
for prefix, skip := range authConfig.PrefixPaths {
|
||||
if strings.HasPrefix(path, prefix) {
|
||||
return skip
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 跳过授权
|
||||
func (s *AppServer) SkipAuth(url string, prefix bool) {
|
||||
if prefix {
|
||||
authConfig.PrefixPaths[url] = false
|
||||
} else {
|
||||
authConfig.ExactPaths[url] = false
|
||||
}
|
||||
}
|
||||
|
||||
// 统一参数处理
|
||||
func parameterHandlerMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
|
||||
@@ -52,17 +52,6 @@ type Delta struct {
|
||||
} `json:"function_call,omitempty"`
|
||||
}
|
||||
|
||||
// ChatSession 聊天会话对象
|
||||
type ChatSession struct {
|
||||
UserId uint `json:"user_id"`
|
||||
ClientIP string `json:"client_ip"` // 客户端 IP
|
||||
ChatId string `json:"chat_id"` // 客户端聊天会话 ID, 多会话模式专用字段
|
||||
Model ChatModel `json:"model"` // GPT 模型
|
||||
Start int64 `json:"start"` // 开始请求时间戳
|
||||
Tools []int `json:"tools"` // 工具函数列表
|
||||
Stream bool `json:"stream"` // 是否采用流式输出
|
||||
}
|
||||
|
||||
type ChatModel struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
@@ -70,6 +59,8 @@ type ChatModel struct {
|
||||
Power int `json:"power"`
|
||||
MaxTokens int `json:"max_tokens"` // 最大响应长度
|
||||
MaxContext int `json:"max_context"` // 最大上下文长度
|
||||
Description string `json:"description"` //模型描述
|
||||
Category string `json:"category"` //模型类别
|
||||
Temperature float32 `json:"temperature"` // 模型温度
|
||||
KeyId int `json:"key_id"` // 绑定 API KEY
|
||||
}
|
||||
|
||||
@@ -43,9 +43,10 @@ type SmtpConfig struct {
|
||||
}
|
||||
|
||||
type ApiConfig struct {
|
||||
ApiURL string
|
||||
AppId string
|
||||
Token string
|
||||
ApiURL string
|
||||
AppId string
|
||||
Token string
|
||||
JimengConfig JimengConfig // 即梦AI配置
|
||||
}
|
||||
|
||||
type AlipayConfig struct {
|
||||
@@ -162,14 +163,15 @@ type SystemConfig struct {
|
||||
SdNegPrompt string `json:"sd_neg_prompt"` // SD 默认反向提示词
|
||||
MjMode string `json:"mj_mode"` // midjourney 默认的API模式,relax, fast, turbo
|
||||
|
||||
IndexNavs []int `json:"index_navs"` // 首页显示的导航菜单
|
||||
Copyright string `json:"copyright"` // 版权信息
|
||||
ICP string `json:"icp"` // ICP 备案号
|
||||
MarkMapText string `json:"mark_map_text"` // 思维导入的默认文本
|
||||
IndexNavs []int `json:"index_navs"` // 首页显示的导航菜单
|
||||
Copyright string `json:"copyright"` // 版权信息
|
||||
DefaultNickname string `json:"default_nickname"` // 默认昵称
|
||||
ICP string `json:"icp"` // ICP 备案号
|
||||
MarkMapText string `json:"mark_map_text"` // 思维导入的默认文本
|
||||
|
||||
EnabledVerify bool `json:"enabled_verify"` // 是否启用验证码
|
||||
EmailWhiteList []string `json:"email_white_list"` // 邮箱白名单列表
|
||||
TranslateModelId int `json:"translate_model_id"` // 用来做提示词翻译的大模型 id
|
||||
AssistantModelId int `json:"assistant_model_id"` // 用来做提示词,翻译的AI模型 id
|
||||
MaxFileSize int `json:"max_file_size"` // 最大文件大小,单位:MB
|
||||
|
||||
}
|
||||
|
||||
18
api/core/types/jimeng.go
Normal file
18
api/core/types/jimeng.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package types
|
||||
|
||||
// JimengConfig 即梦AI配置
|
||||
type JimengConfig struct {
|
||||
AccessKey string `json:"access_key"`
|
||||
SecretKey string `json:"secret_key"`
|
||||
Power JimengPower `json:"power"`
|
||||
}
|
||||
|
||||
// JimengPower 即梦AI算力配置
|
||||
type JimengPower struct {
|
||||
TextToImage int `json:"text_to_image"`
|
||||
ImageToImage int `json:"image_to_image"`
|
||||
ImageEdit int `json:"image_edit"`
|
||||
ImageEffects int `json:"image_effects"`
|
||||
TextToVideo int `json:"text_to_video"`
|
||||
ImageToVideo int `json:"image_to_video"`
|
||||
}
|
||||
@@ -16,7 +16,7 @@ type MKey interface {
|
||||
string | int | uint
|
||||
}
|
||||
type MValue interface {
|
||||
*WsClient | *ChatSession | context.CancelFunc | []any
|
||||
*WsClient | context.CancelFunc | []any
|
||||
}
|
||||
type LMap[K MKey, T MValue] struct {
|
||||
lock sync.RWMutex
|
||||
|
||||
@@ -18,6 +18,7 @@ require (
|
||||
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/volcengine/volc-sdk-golang v1.0.23
|
||||
go.uber.org/zap v1.23.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gorm.io/driver/mysql v1.4.7
|
||||
@@ -45,7 +46,7 @@ require (
|
||||
github.com/go-pay/util v0.0.2 // indirect
|
||||
github.com/go-pay/xlog v0.0.2 // indirect
|
||||
github.com/go-pay/xtime v0.0.2 // indirect
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.13 // indirect
|
||||
github.com/tklauser/numcpus v0.7.0 // indirect
|
||||
@@ -78,7 +79,7 @@ require (
|
||||
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/jmespath/go-jmespath v0.4.0 // 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
|
||||
@@ -120,7 +121,7 @@ require (
|
||||
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
|
||||
go.uber.org/multierr v1.7.0 // indirect
|
||||
golang.org/x/crypto v0.23.0
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
gorm.io/gorm v1.25.1
|
||||
|
||||
79
api/go.sum
79
api/go.sum
@@ -1,3 +1,5 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
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=
|
||||
@@ -6,6 +8,7 @@ github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible h1:Sg/2xHwDrioHpxTN6WMiw
|
||||
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/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
|
||||
@@ -13,11 +16,13 @@ github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZx
|
||||
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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
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=
|
||||
@@ -28,6 +33,8 @@ 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/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
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/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
@@ -84,11 +91,27 @@ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG
|
||||
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/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-tika v0.3.1 h1:l+jr10hDhZjcgxFRfcQChRLo1bPXQeLFluMyvDhXTTA=
|
||||
@@ -115,8 +138,11 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr
|
||||
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/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
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=
|
||||
@@ -127,6 +153,7 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02
|
||||
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.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
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=
|
||||
@@ -179,6 +206,7 @@ github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480 h1:IFhPCcB0/H
|
||||
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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
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=
|
||||
@@ -208,6 +236,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||
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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
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=
|
||||
@@ -230,6 +259,8 @@ github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVK
|
||||
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/volcengine/volc-sdk-golang v1.0.23 h1:anOslb2Qp6ywnsbyq9jqR0ljuO63kg9PY+4OehIk5R8=
|
||||
github.com/volcengine/volc-sdk-golang v1.0.23/go.mod h1:AfG/PZRUkHJ9inETvbjNifTDgut25Wbkm2QoYBTbvyU=
|
||||
github.com/xxl-job/xxl-job-executor-go v1.2.0 h1:MTl2DpwrK2+hNjRRks2k7vB3oy+3onqm9OaSarneeLQ=
|
||||
github.com/xxl-job/xxl-job-executor-go v1.2.0/go.mod h1:bUFhz/5Irp9zkdYk5MxhQcDDT6LlZrI8+rv5mHtQ1mo=
|
||||
github.com/ysmood/fetchup v0.3.0 h1:UhYz9xnLEVn2ukSuK3KCgcznWpHMdrmbsPpllcylyu8=
|
||||
@@ -260,8 +291,8 @@ go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
|
||||
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
||||
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||
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/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec=
|
||||
go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
|
||||
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=
|
||||
@@ -275,15 +306,23 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
|
||||
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
@@ -294,12 +333,15 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
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-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -338,16 +380,39 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
|
||||
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
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-20190902080502-41f04d3bba15/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=
|
||||
@@ -360,6 +425,10 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYs
|
||||
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.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
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=
|
||||
@@ -369,4 +438,6 @@ gorm.io/driver/mysql v1.4.7/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8o
|
||||
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=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
|
||||
@@ -78,10 +78,10 @@ func (h *ChatAppHandler) List(c *gin.Context) {
|
||||
typeIds := make([]int, 0)
|
||||
for _, v := range items {
|
||||
if v.ModelId > 0 {
|
||||
modelIds = append(modelIds, v.ModelId)
|
||||
modelIds = append(modelIds, int(v.ModelId))
|
||||
}
|
||||
if v.Tid > 0 {
|
||||
typeIds = append(typeIds, v.Tid)
|
||||
typeIds = append(typeIds, int(v.Tid))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,8 +113,8 @@ func (h *ChatAppHandler) List(c *gin.Context) {
|
||||
role.Id = v.Id
|
||||
role.CreatedAt = v.CreatedAt.Unix()
|
||||
role.UpdatedAt = v.UpdatedAt.Unix()
|
||||
role.ModelName = modelNameMap[role.ModelId]
|
||||
role.TypeName = typeNameMap[role.Tid]
|
||||
role.ModelName = modelNameMap[int(role.ModelId)]
|
||||
role.TypeName = typeNameMap[int(role.Tid)]
|
||||
roles = append(roles, role)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -189,7 +190,7 @@ func (h *ChatHandler) Messages(c *gin.Context) {
|
||||
}
|
||||
for _, item := range items {
|
||||
list = append(list, chatMessageVo{
|
||||
Id: item.Id,
|
||||
Id: uint(item.Id),
|
||||
UserId: item.UserId,
|
||||
Username: userMap[item.UserId],
|
||||
Content: item.Content,
|
||||
@@ -208,20 +209,28 @@ func (h *ChatHandler) Messages(c *gin.Context) {
|
||||
func (h *ChatHandler) History(c *gin.Context) {
|
||||
chatId := c.Query("chat_id") // 会话 ID
|
||||
var items []model.ChatMessage
|
||||
var messages = make([]vo.HistoryMessage, 0)
|
||||
var messages = make([]vo.ChatMessage, 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
|
||||
var v vo.ChatMessage
|
||||
err := utils.CopyObject(item, &v)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// 解析内容
|
||||
var content vo.MsgContent
|
||||
err = utils.JsonDecode(item.Content, &content)
|
||||
if err != nil {
|
||||
content.Text = item.Content
|
||||
}
|
||||
v.Content = content
|
||||
v.CreatedAt = item.CreatedAt.Unix()
|
||||
v.UpdatedAt = item.UpdatedAt.Unix()
|
||||
if err == nil {
|
||||
messages = append(messages, v)
|
||||
}
|
||||
messages = append(messages, v)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ func (h *ChatModelHandler) Save(c *gin.Context) {
|
||||
Power int `json:"power"`
|
||||
MaxTokens int `json:"max_tokens"` // 最大响应长度
|
||||
MaxContext int `json:"max_context"` // 最大上下文长度
|
||||
Desc string `json:"desc"` //模型描述
|
||||
Tag string `json:"tag"` //模型标签
|
||||
Temperature float32 `json:"temperature"` // 模型温度
|
||||
KeyId int `json:"key_id,omitempty"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
@@ -64,8 +66,10 @@ func (h *ChatModelHandler) Save(c *gin.Context) {
|
||||
item.Power = data.Power
|
||||
item.MaxTokens = data.MaxTokens
|
||||
item.MaxContext = data.MaxContext
|
||||
item.Desc = data.Desc
|
||||
item.Tag = data.Tag
|
||||
item.Temperature = data.Temperature
|
||||
item.KeyId = data.KeyId
|
||||
item.KeyId = uint(data.KeyId)
|
||||
item.Type = data.Type
|
||||
item.Options = utils.JsonEncode(data.Options)
|
||||
var res *gorm.DB
|
||||
@@ -96,12 +100,16 @@ func (h *ChatModelHandler) List(c *gin.Context) {
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
enable := h.GetBool(c, "enable")
|
||||
name := h.GetTrim(c, "name")
|
||||
modelType := h.GetTrim(c, "type")
|
||||
if enable {
|
||||
session = session.Where("enabled", enable)
|
||||
}
|
||||
if name != "" {
|
||||
session = session.Where("name LIKE ?", name+"%")
|
||||
}
|
||||
if modelType != "" {
|
||||
session = session.Where("type", modelType)
|
||||
}
|
||||
var items []model.ChatModel
|
||||
var cms = make([]vo.ChatModel, 0)
|
||||
res := session.Order("sort_num ASC").Find(&items)
|
||||
@@ -113,7 +121,7 @@ func (h *ChatModelHandler) List(c *gin.Context) {
|
||||
// initialize key name
|
||||
keyIds := make([]int, 0)
|
||||
for _, v := range items {
|
||||
keyIds = append(keyIds, v.KeyId)
|
||||
keyIds = append(keyIds, int(v.KeyId))
|
||||
}
|
||||
var keys []model.ApiKey
|
||||
keyMap := make(map[uint]string)
|
||||
|
||||
@@ -66,15 +66,15 @@ func (h *ConfigHandler) Update(c *gin.Context) {
|
||||
}
|
||||
|
||||
value := utils.JsonEncode(&data.Config)
|
||||
config := model.Config{Key: data.Key, Config: value}
|
||||
res := h.DB.FirstOrCreate(&config, model.Config{Key: data.Key})
|
||||
config := model.Config{Name: data.Key, Value: value}
|
||||
res := h.DB.FirstOrCreate(&config, model.Config{Name: data.Key})
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if config.Id > 0 {
|
||||
config.Config = value
|
||||
config.Value = value
|
||||
res := h.DB.Updates(&config)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
@@ -83,16 +83,16 @@ func (h *ConfigHandler) Update(c *gin.Context) {
|
||||
|
||||
// update config cache for AppServer
|
||||
var cfg model.Config
|
||||
h.DB.Where("marker", data.Key).First(&cfg)
|
||||
h.DB.Where("name", data.Key).First(&cfg)
|
||||
var err error
|
||||
if data.Key == "system" {
|
||||
err = utils.JsonDecode(cfg.Config, &h.App.SysConfig)
|
||||
err = utils.JsonDecode(cfg.Value, &h.App.SysConfig)
|
||||
}
|
||||
if err != nil {
|
||||
resp.ERROR(c, "Failed to update config cache: "+err.Error())
|
||||
return
|
||||
}
|
||||
logger.Infof("Update AppServer's config successfully: %v", config.Config)
|
||||
logger.Infof("Update AppServer's config successfully: %v", config.Value)
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, config)
|
||||
@@ -102,14 +102,14 @@ func (h *ConfigHandler) Update(c *gin.Context) {
|
||||
func (h *ConfigHandler) Get(c *gin.Context) {
|
||||
key := c.Query("key")
|
||||
var config model.Config
|
||||
res := h.DB.Where("marker", key).First(&config)
|
||||
res := h.DB.Where("name", key).First(&config)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var value map[string]interface{}
|
||||
err := utils.JsonDecode(config.Config, &value)
|
||||
err := utils.JsonDecode(config.Value, &value)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -189,11 +190,10 @@ func (h *ImageHandler) Remove(c *gin.Context) {
|
||||
tx.Delete(&job)
|
||||
md = "mid-journey"
|
||||
power = job.Power
|
||||
userId = job.UserId
|
||||
userId = int(job.UserId)
|
||||
remark = fmt.Sprintf("任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg)
|
||||
progress = job.Progress
|
||||
imgURL = job.ImgURL
|
||||
break
|
||||
case "sd":
|
||||
var job model.SdJob
|
||||
if res := h.DB.Where("id", id).First(&job); res.Error != nil {
|
||||
@@ -205,11 +205,10 @@ func (h *ImageHandler) Remove(c *gin.Context) {
|
||||
tx.Delete(&job)
|
||||
md = "stable-diffusion"
|
||||
power = job.Power
|
||||
userId = job.UserId
|
||||
userId = int(job.UserId)
|
||||
remark = fmt.Sprintf("任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg)
|
||||
progress = job.Progress
|
||||
imgURL = job.ImgURL
|
||||
break
|
||||
case "dall":
|
||||
var job model.DallJob
|
||||
if res := h.DB.Where("id", id).First(&job); res.Error != nil {
|
||||
@@ -225,14 +224,13 @@ func (h *ImageHandler) Remove(c *gin.Context) {
|
||||
remark = fmt.Sprintf("任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg)
|
||||
progress = job.Progress
|
||||
imgURL = job.ImgURL
|
||||
break
|
||||
default:
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
if progress != 100 {
|
||||
err := h.userService.IncreasePower(userId, power, model.PowerLog{
|
||||
err := h.userService.IncreasePower(uint(userId), power, model.PowerLog{
|
||||
Type: types.PowerRefund,
|
||||
Model: md,
|
||||
Remark: remark,
|
||||
|
||||
296
api/handler/admin/jimeng_handler.go
Normal file
296
api/handler/admin/jimeng_handler.go
Normal file
@@ -0,0 +1,296 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/handler"
|
||||
"geekai/service"
|
||||
"geekai/service/jimeng"
|
||||
"geekai/service/oss"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AdminJimengHandler 管理后台即梦AI处理器
|
||||
type AdminJimengHandler struct {
|
||||
handler.BaseHandler
|
||||
jimengService *jimeng.Service
|
||||
userService *service.UserService
|
||||
uploader *oss.UploaderManager
|
||||
}
|
||||
|
||||
// NewAdminJimengHandler 创建管理后台即梦AI处理器
|
||||
func NewAdminJimengHandler(app *core.AppServer, db *gorm.DB, jimengService *jimeng.Service, userService *service.UserService, uploader *oss.UploaderManager) *AdminJimengHandler {
|
||||
return &AdminJimengHandler{
|
||||
BaseHandler: handler.BaseHandler{App: app, DB: db},
|
||||
jimengService: jimengService,
|
||||
userService: userService,
|
||||
uploader: uploader,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册即梦AI管理后台路由
|
||||
func (h *AdminJimengHandler) RegisterRoutes() {
|
||||
rg := h.App.Engine.Group("/api/admin/jimeng/")
|
||||
rg.GET("/jobs", h.Jobs)
|
||||
rg.GET("/jobs/:id", h.JobDetail)
|
||||
rg.POST("/jobs/remove", h.BatchRemove)
|
||||
rg.GET("/stats", h.Stats)
|
||||
rg.GET("/config", h.GetConfig)
|
||||
rg.POST("/config/update", h.UpdateConfig)
|
||||
}
|
||||
|
||||
// Jobs 获取任务列表
|
||||
func (h *AdminJimengHandler) Jobs(c *gin.Context) {
|
||||
page := h.GetInt(c, "page", 1)
|
||||
pageSize := h.GetInt(c, "page_size", 20)
|
||||
userId := h.GetInt(c, "user_id", 0)
|
||||
taskType := h.GetTrim(c, "type")
|
||||
status := h.GetTrim(c, "status")
|
||||
|
||||
var tasks []model.JimengJob
|
||||
var total int64
|
||||
|
||||
session := h.DB.Model(&model.JimengJob{})
|
||||
|
||||
// 构建查询条件
|
||||
if userId > 0 {
|
||||
session = session.Where("user_id = ?", userId)
|
||||
}
|
||||
if taskType != "" {
|
||||
session = session.Where("type = ?", taskType)
|
||||
}
|
||||
if status != "" {
|
||||
session = session.Where("status = ?", status)
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
err := session.Count(&total).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "获取任务数量失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
offset := (page - 1) * pageSize
|
||||
err = session.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&tasks).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "获取任务列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, gin.H{
|
||||
"jobs": tasks,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
// JobDetail 获取任务详情
|
||||
func (h *AdminJimengHandler) JobDetail(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
jobId, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var job model.JimengJob
|
||||
err = h.DB.Where("id = ?", jobId).First(&job).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "任务不存在")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, job)
|
||||
}
|
||||
|
||||
// BatchRemove 批量删除任务
|
||||
func (h *AdminJimengHandler) BatchRemove(c *gin.Context) {
|
||||
var req struct {
|
||||
JobIds []uint `json:"job_ids" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.ERROR(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var deletedCount int64 = 0
|
||||
for _, jobId := range req.JobIds {
|
||||
var job model.JimengJob
|
||||
err := h.DB.Where("id = ?", jobId).First(&job).Error
|
||||
if err != nil {
|
||||
continue // 跳过不存在的
|
||||
}
|
||||
tx := h.DB.Begin()
|
||||
if job.Status != model.JMTaskStatusSuccess && job.Power > 0 {
|
||||
remark := fmt.Sprintf("任务未成功,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg)
|
||||
err = h.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{
|
||||
Type: types.PowerRefund,
|
||||
Model: "jimeng",
|
||||
Remark: remark,
|
||||
})
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
continue
|
||||
}
|
||||
}
|
||||
err = tx.Where("id = ?", jobId).Delete(&model.JimengJob{}).Error
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
continue
|
||||
}
|
||||
tx.Commit()
|
||||
deletedCount++
|
||||
if job.ImgURL != "" {
|
||||
err = h.uploader.GetUploadHandler().Delete(job.ImgURL)
|
||||
if err != nil {
|
||||
logger.Error("remove image failed: ", err)
|
||||
}
|
||||
}
|
||||
if job.VideoURL != "" {
|
||||
err = h.uploader.GetUploadHandler().Delete(job.VideoURL)
|
||||
if err != nil {
|
||||
logger.Error("remove video failed: ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.SUCCESS(c, gin.H{
|
||||
"message": "批量删除成功",
|
||||
"deleted_count": deletedCount,
|
||||
})
|
||||
}
|
||||
|
||||
// Stats 获取统计信息
|
||||
func (h *AdminJimengHandler) Stats(c *gin.Context) {
|
||||
type StatResult struct {
|
||||
Status model.JMTaskStatus `json:"status"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
var stats []StatResult
|
||||
err := h.DB.Model(&model.JimengJob{}).
|
||||
Select("status, COUNT(*) as count").
|
||||
Group("status").
|
||||
Find(&stats).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "获取统计信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 整理统计数据
|
||||
result := gin.H{
|
||||
"totalTasks": int64(0),
|
||||
"completedTasks": int64(0),
|
||||
"processingTasks": int64(0),
|
||||
"failedTasks": int64(0),
|
||||
"pendingTasks": int64(0),
|
||||
}
|
||||
|
||||
for _, stat := range stats {
|
||||
result["totalTasks"] = result["totalTasks"].(int64) + stat.Count
|
||||
switch stat.Status {
|
||||
case model.JMTaskStatusInQueue:
|
||||
result["pendingTasks"] = stat.Count
|
||||
case model.JMTaskStatusSuccess:
|
||||
result["completedTasks"] = stat.Count
|
||||
case model.JMTaskStatusGenerating:
|
||||
result["processingTasks"] = stat.Count
|
||||
case model.JMTaskStatusFailed:
|
||||
result["failedTasks"] = stat.Count
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, result)
|
||||
}
|
||||
|
||||
// GetConfig 获取即梦AI配置
|
||||
func (h *AdminJimengHandler) GetConfig(c *gin.Context) {
|
||||
jimengConfig := h.jimengService.GetConfig()
|
||||
resp.SUCCESS(c, jimengConfig)
|
||||
}
|
||||
|
||||
// UpdateConfig 更新即梦AI配置
|
||||
func (h *AdminJimengHandler) UpdateConfig(c *gin.Context) {
|
||||
var req types.JimengConfig
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.ERROR(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证必填字段
|
||||
if req.AccessKey == "" {
|
||||
resp.ERROR(c, "AccessKey不能为空")
|
||||
return
|
||||
}
|
||||
if req.SecretKey == "" {
|
||||
resp.ERROR(c, "SecretKey不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证算力配置
|
||||
if req.Power.TextToImage <= 0 {
|
||||
resp.ERROR(c, "文生图算力必须大于0")
|
||||
return
|
||||
}
|
||||
if req.Power.ImageToImage <= 0 {
|
||||
resp.ERROR(c, "图生图算力必须大于0")
|
||||
return
|
||||
}
|
||||
if req.Power.ImageEdit <= 0 {
|
||||
resp.ERROR(c, "图片编辑算力必须大于0")
|
||||
return
|
||||
}
|
||||
if req.Power.ImageEffects <= 0 {
|
||||
resp.ERROR(c, "图片特效算力必须大于0")
|
||||
return
|
||||
}
|
||||
if req.Power.TextToVideo <= 0 {
|
||||
resp.ERROR(c, "文生视频算力必须大于0")
|
||||
return
|
||||
}
|
||||
if req.Power.ImageToVideo <= 0 {
|
||||
resp.ERROR(c, "图生视频算力必须大于0")
|
||||
return
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
tx := h.DB.Begin()
|
||||
value := utils.JsonEncode(&req)
|
||||
config := model.Config{Name: "jimeng", Value: value}
|
||||
|
||||
err := tx.FirstOrCreate(&config, model.Config{Name: "jimeng"}).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "保存配置失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if config.Id > 0 {
|
||||
config.Value = value
|
||||
err = tx.Updates(&config).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "更新配置失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 更新服务中的客户端配置
|
||||
updateErr := h.jimengService.UpdateClientConfig(req.AccessKey, req.SecretKey)
|
||||
if updateErr != nil {
|
||||
resp.ERROR(c, updateErr.Error())
|
||||
tx.Rollback()
|
||||
return
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
resp.SUCCESS(c, gin.H{"message": "配置更新成功"})
|
||||
}
|
||||
@@ -150,11 +150,10 @@ func (h *MediaHandler) Remove(c *gin.Context) {
|
||||
tx.Delete(&job)
|
||||
md = "suno"
|
||||
power = job.Power
|
||||
userId = job.UserId
|
||||
userId = int(job.UserId)
|
||||
remark = fmt.Sprintf("SUNO 任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg)
|
||||
progress = job.Progress
|
||||
fileURL = job.AudioURL
|
||||
break
|
||||
case "luma":
|
||||
case "keling":
|
||||
var job model.VideoJob
|
||||
@@ -167,21 +166,20 @@ func (h *MediaHandler) Remove(c *gin.Context) {
|
||||
tx.Delete(&job)
|
||||
md = job.Type
|
||||
power = job.Power
|
||||
userId = job.UserId
|
||||
userId = int(job.UserId)
|
||||
remark = fmt.Sprintf("LUMA 任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg)
|
||||
progress = job.Progress
|
||||
fileURL = job.VideoURL
|
||||
if fileURL == "" {
|
||||
fileURL = job.WaterURL
|
||||
}
|
||||
break
|
||||
default:
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
if progress != 100 {
|
||||
err := h.userService.IncreasePower(userId, power, model.PowerLog{
|
||||
err := h.userService.IncreasePower(uint(userId), power, model.PowerLog{
|
||||
Type: types.PowerRefund,
|
||||
Model: md,
|
||||
Remark: remark,
|
||||
|
||||
@@ -106,8 +106,8 @@ func (h *RedeemHandler) Export(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 设置响应头,告诉浏览器这是一个附件,需要下载
|
||||
c.Header("Content-Disposition", "attachment; filename=output.csv")
|
||||
c.Header("Content-Type", "text/csv")
|
||||
c.Header("Prompt-Disposition", "attachment; filename=output.csv")
|
||||
c.Header("Prompt-Type", "text/csv")
|
||||
|
||||
// 创建一个 CSV writer
|
||||
writer := csv.NewWriter(c.Writer)
|
||||
|
||||
@@ -49,7 +49,7 @@ func (h *UploadHandler) Upload(c *gin.Context) {
|
||||
|
||||
userId := 0
|
||||
res := h.DB.Create(&model.File{
|
||||
UserId: userId,
|
||||
UserId: uint(userId),
|
||||
Name: file.Name,
|
||||
ObjKey: file.ObjKey,
|
||||
URL: file.URL,
|
||||
|
||||
@@ -178,6 +178,7 @@ func (h *UserHandler) Save(c *gin.Context) {
|
||||
Power: data.Power,
|
||||
Status: true,
|
||||
ChatRoles: utils.JsonEncode(data.ChatRoles),
|
||||
ChatConfig: "{}",
|
||||
ChatModels: utils.JsonEncode(data.ChatModels),
|
||||
ExpiredTime: utils.Str2stamp(data.ExpiredTime),
|
||||
}
|
||||
@@ -353,4 +354,4 @@ func (h *UserHandler) GenLoginLink(c *gin.Context) {
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, tokenString)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,11 +21,11 @@ import (
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
@@ -36,13 +36,34 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
ChatEventStart = "start"
|
||||
ChatEventEnd = "end"
|
||||
ChatEventError = "error"
|
||||
ChatEventMessageDelta = "message_delta"
|
||||
ChatEventTitle = "title"
|
||||
)
|
||||
|
||||
type ChatInput struct {
|
||||
UserId uint `json:"user_id"`
|
||||
RoleId uint `json:"role_id"`
|
||||
ModelId uint `json:"model_id"`
|
||||
ChatId string `json:"chat_id"`
|
||||
Prompt string `json:"prompt"`
|
||||
Tools []uint `json:"tools"`
|
||||
Stream bool `json:"stream"`
|
||||
Files []vo.File `json:"files"`
|
||||
ChatModel model.ChatModel `json:"chat_model,omitempty"`
|
||||
ChatRole model.ChatRole `json:"chat_role,omitempty"`
|
||||
LastMsgId uint `json:"last_msg_id,omitempty"` // 最后的消息ID,用于重新生成答案的时候过滤上下文
|
||||
}
|
||||
|
||||
type ChatHandler struct {
|
||||
BaseHandler
|
||||
redis *redis.Client
|
||||
uploadManager *oss.UploaderManager
|
||||
licenseService *service.LicenseService
|
||||
ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function
|
||||
ChatContexts *types.LMap[string, []any] // 聊天上下文 Map [chatId] => []Message
|
||||
userService *service.UserService
|
||||
}
|
||||
|
||||
@@ -53,22 +74,83 @@ func NewChatHandler(app *core.AppServer, db *gorm.DB, redis *redis.Client, manag
|
||||
uploadManager: manager,
|
||||
licenseService: licenseService,
|
||||
ReqCancelFunc: types.NewLMap[string, context.CancelFunc](),
|
||||
ChatContexts: types.NewLMap[string, []any](),
|
||||
userService: userService,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSession, role model.ChatRole, prompt string, ws *types.WsClient) error {
|
||||
if !h.App.Debug {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.Error("Recover message from error: ", r)
|
||||
}
|
||||
}()
|
||||
// Chat 处理聊天请求
|
||||
func (h *ChatHandler) Chat(c *gin.Context) {
|
||||
var input ChatInput
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置SSE响应头
|
||||
c.Header("Prompt-Type", "text/event-stream")
|
||||
c.Header("Cache-Control", "no-cache")
|
||||
c.Header("Connection", "keep-alive")
|
||||
c.Header("X-Accel-Buffering", "no")
|
||||
|
||||
ctx, cancel := context.WithCancel(c.Request.Context())
|
||||
defer cancel()
|
||||
|
||||
// 这里做个全局的异常处理,防止整个请求异常,导致 SSE 连接断开
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.Errorf("chat handler error: %v", err)
|
||||
pushMessage(c, ChatEventError, err)
|
||||
c.Abort()
|
||||
}
|
||||
}()
|
||||
|
||||
// 使用旧的聊天数据覆盖模型和角色ID
|
||||
var chat model.ChatItem
|
||||
h.DB.Where("chat_id", input.ChatId).First(&chat)
|
||||
if chat.Id > 0 {
|
||||
input.ModelId = chat.ModelId
|
||||
input.RoleId = chat.RoleId
|
||||
}
|
||||
|
||||
// 验证聊天角色
|
||||
var chatRole model.ChatRole
|
||||
err := h.DB.First(&chatRole, input.RoleId).Error
|
||||
if err != nil || !chatRole.Enable {
|
||||
pushMessage(c, ChatEventError, "当前聊天角色不存在或者未启用,请更换角色之后再发起对话!")
|
||||
return
|
||||
}
|
||||
input.ChatRole = chatRole
|
||||
|
||||
// 获取模型信息
|
||||
var chatModel model.ChatModel
|
||||
err = h.DB.Where("id", input.ModelId).First(&chatModel).Error
|
||||
if err != nil || !chatModel.Enabled {
|
||||
pushMessage(c, ChatEventError, "当前AI模型暂未启用,请更换模型后再发起对话!")
|
||||
return
|
||||
}
|
||||
input.ChatModel = chatModel
|
||||
|
||||
// 发送消息
|
||||
err = h.sendMessage(ctx, input, c)
|
||||
if err != nil {
|
||||
pushMessage(c, ChatEventError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
pushMessage(c, ChatEventEnd, "对话完成")
|
||||
}
|
||||
|
||||
func pushMessage(c *gin.Context, msgType string, content interface{}) {
|
||||
c.SSEvent("message", map[string]interface{}{
|
||||
"type": msgType,
|
||||
"body": content,
|
||||
})
|
||||
c.Writer.Flush()
|
||||
}
|
||||
|
||||
func (h *ChatHandler) sendMessage(ctx context.Context, input ChatInput, c *gin.Context) error {
|
||||
var user model.User
|
||||
res := h.DB.Model(&model.User{}).First(&user, session.UserId)
|
||||
res := h.DB.Model(&model.User{}).First(&user, input.UserId)
|
||||
if res.Error != nil {
|
||||
return errors.New("未授权用户,您正在进行非法操作!")
|
||||
}
|
||||
@@ -79,12 +161,12 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
|
||||
return errors.New("User 对象转换失败," + err.Error())
|
||||
}
|
||||
|
||||
if userVo.Status == false {
|
||||
if !userVo.Status {
|
||||
return errors.New("您的账号已经被禁用,如果疑问,请联系管理员!")
|
||||
}
|
||||
|
||||
if userVo.Power < session.Model.Power {
|
||||
return fmt.Errorf("您当前剩余算力 %d 已不足以支付当前模型的单次对话需要消耗的算力 %d,[立即购买](/member)。", userVo.Power, session.Model.Power)
|
||||
if userVo.Power < input.ChatModel.Power {
|
||||
return fmt.Errorf("您当前剩余算力 %d 已不足以支付当前模型的单次对话需要消耗的算力 %d,[立即购买](/member)。", userVo.Power, input.ChatModel.Power)
|
||||
}
|
||||
|
||||
if userVo.ExpiredTime > 0 && userVo.ExpiredTime <= time.Now().Unix() {
|
||||
@@ -92,30 +174,29 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
|
||||
}
|
||||
|
||||
// 检查 prompt 长度是否超过了当前模型允许的最大上下文长度
|
||||
promptTokens, _ := utils.CalcTokens(prompt, session.Model.Value)
|
||||
if promptTokens > session.Model.MaxContext {
|
||||
promptTokens, _ := utils.CalcTokens(input.Prompt, input.ChatModel.Value)
|
||||
if promptTokens > input.ChatModel.MaxContext {
|
||||
|
||||
return errors.New("对话内容超出了当前模型允许的最大上下文长度!")
|
||||
}
|
||||
|
||||
var req = types.ApiRequest{
|
||||
Model: session.Model.Value,
|
||||
Stream: session.Stream,
|
||||
Temperature: session.Model.Temperature,
|
||||
Model: input.ChatModel.Value,
|
||||
Stream: input.Stream,
|
||||
Temperature: input.ChatModel.Temperature,
|
||||
}
|
||||
// 兼容 OpenAI 模型
|
||||
if strings.HasPrefix(session.Model.Value, "o1-") ||
|
||||
strings.HasPrefix(session.Model.Value, "o3-") ||
|
||||
strings.HasPrefix(session.Model.Value, "gpt") {
|
||||
req.MaxCompletionTokens = session.Model.MaxTokens
|
||||
session.Start = time.Now().Unix()
|
||||
if strings.HasPrefix(input.ChatModel.Value, "o1-") ||
|
||||
strings.HasPrefix(input.ChatModel.Value, "o3-") ||
|
||||
strings.HasPrefix(input.ChatModel.Value, "gpt") {
|
||||
req.MaxCompletionTokens = input.ChatModel.MaxTokens
|
||||
} else {
|
||||
req.MaxTokens = session.Model.MaxTokens
|
||||
req.MaxTokens = input.ChatModel.MaxTokens
|
||||
}
|
||||
|
||||
if len(session.Tools) > 0 && !strings.HasPrefix(session.Model.Value, "o1-") {
|
||||
if len(input.Tools) > 0 && !strings.HasPrefix(input.ChatModel.Value, "o1-") {
|
||||
var items []model.Function
|
||||
res = h.DB.Where("enabled", true).Where("id IN ?", session.Tools).Find(&items)
|
||||
res = h.DB.Where("enabled", true).Where("id IN ?", input.Tools).Find(&items)
|
||||
if res.Error == nil {
|
||||
var tools = make([]types.Tool, 0)
|
||||
for _, v := range items {
|
||||
@@ -146,25 +227,27 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
|
||||
}
|
||||
|
||||
// 加载聊天上下文
|
||||
chatCtx := make([]interface{}, 0)
|
||||
messages := make([]interface{}, 0)
|
||||
chatCtx := make([]any, 0)
|
||||
messages := make([]any, 0)
|
||||
if h.App.SysConfig.EnableContext {
|
||||
if h.ChatContexts.Has(session.ChatId) {
|
||||
messages = h.ChatContexts.Get(session.ChatId)
|
||||
} else {
|
||||
_ = utils.JsonDecode(role.Context, &messages)
|
||||
if h.App.SysConfig.ContextDeep > 0 {
|
||||
var historyMessages []model.ChatMessage
|
||||
res := h.DB.Where("chat_id = ? and use_context = 1", session.ChatId).Limit(h.App.SysConfig.ContextDeep).Order("id DESC").Find(&historyMessages)
|
||||
if res.Error == nil {
|
||||
for i := len(historyMessages) - 1; i >= 0; i-- {
|
||||
msg := historyMessages[i]
|
||||
ms := types.Message{Role: "user", Content: msg.Content}
|
||||
if msg.Type == types.ReplyMsg {
|
||||
ms.Role = "assistant"
|
||||
}
|
||||
chatCtx = append(chatCtx, ms)
|
||||
_ = utils.JsonDecode(input.ChatRole.Context, &messages)
|
||||
if h.App.SysConfig.ContextDeep > 0 {
|
||||
var historyMessages []model.ChatMessage
|
||||
dbSession := h.DB.Session(&gorm.Session{}).Where("chat_id", input.ChatId)
|
||||
if input.LastMsgId > 0 { // 重新生成逻辑
|
||||
dbSession = dbSession.Where("id < ?", input.LastMsgId)
|
||||
// 删除对应的聊天记录
|
||||
h.DB.Debug().Where("chat_id", input.ChatId).Where("id >= ?", input.LastMsgId).Delete(&model.ChatMessage{})
|
||||
}
|
||||
err = dbSession.Limit(h.App.SysConfig.ContextDeep).Order("id DESC").Find(&historyMessages).Error
|
||||
if err == nil {
|
||||
for i := len(historyMessages) - 1; i >= 0; i-- {
|
||||
msg := historyMessages[i]
|
||||
ms := types.Message{Role: "user", Content: msg.Content}
|
||||
if msg.Type == types.ReplyMsg {
|
||||
ms.Role = "assistant"
|
||||
}
|
||||
chatCtx = append(chatCtx, ms)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -179,7 +262,7 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
|
||||
v := messages[i]
|
||||
tks, _ = utils.CalcTokens(utils.JsonEncode(v), req.Model)
|
||||
// 上下文 token 超出了模型的最大上下文长度
|
||||
if tokens+tks >= session.Model.MaxContext {
|
||||
if tokens+tks >= input.ChatModel.MaxContext {
|
||||
break
|
||||
}
|
||||
|
||||
@@ -191,78 +274,106 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
|
||||
tokens += tks
|
||||
chatCtx = append(chatCtx, v)
|
||||
}
|
||||
|
||||
logger.Debugf("聊天上下文:%+v", chatCtx)
|
||||
}
|
||||
reqMgs := make([]interface{}, 0)
|
||||
reqMgs := make([]any, 0)
|
||||
|
||||
for i := len(chatCtx) - 1; i >= 0; i-- {
|
||||
reqMgs = append(reqMgs, chatCtx[i])
|
||||
}
|
||||
|
||||
fullPrompt := prompt
|
||||
text := prompt
|
||||
// extract files in prompt
|
||||
files := utils.ExtractFileURLs(prompt)
|
||||
logger.Debugf("detected FILES: %+v", files)
|
||||
// 如果不是逆向模型,则提取文件内容
|
||||
if len(files) > 0 && !(session.Model.Value == "gpt-4-all" ||
|
||||
strings.HasPrefix(session.Model.Value, "gpt-4-gizmo") ||
|
||||
strings.HasSuffix(session.Model.Value, "claude-3")) {
|
||||
contents := make([]string, 0)
|
||||
var file model.File
|
||||
for _, v := range files {
|
||||
h.DB.Where("url = ?", v).First(&file)
|
||||
content, err := utils.ReadFileContent(v, h.App.Config.TikaHost)
|
||||
if err != nil {
|
||||
logger.Error("error with read file: ", err)
|
||||
} else {
|
||||
contents = append(contents, fmt.Sprintf("%s 文件内容:%s", file.Name, content))
|
||||
}
|
||||
text = strings.Replace(text, v, "", 1)
|
||||
}
|
||||
if len(contents) > 0 {
|
||||
fullPrompt = fmt.Sprintf("请根据提供的文件内容信息回答问题(其中Excel 已转成 HTML):\n\n %s\n\n 问题:%s", strings.Join(contents, "\n"), text)
|
||||
}
|
||||
|
||||
tokens, _ := utils.CalcTokens(fullPrompt, req.Model)
|
||||
if tokens > session.Model.MaxContext {
|
||||
return fmt.Errorf("文件的长度超出模型允许的最大上下文长度,请减少文件内容数量或文件大小。")
|
||||
}
|
||||
}
|
||||
logger.Debug("最终Prompt:", fullPrompt)
|
||||
|
||||
// extract images from prompt
|
||||
imgURLs := utils.ExtractImgURLs(prompt)
|
||||
logger.Debugf("detected IMG: %+v", imgURLs)
|
||||
var content interface{}
|
||||
if len(imgURLs) > 0 {
|
||||
data := make([]interface{}, 0)
|
||||
for _, v := range imgURLs {
|
||||
text = strings.Replace(text, v, "", 1)
|
||||
data = append(data, gin.H{
|
||||
fileContents := make([]string, 0) // 文件内容
|
||||
var finalPrompt = input.Prompt
|
||||
imgList := make([]any, 0)
|
||||
for _, file := range input.Files {
|
||||
logger.Debugf("detected file: %+v", file.URL)
|
||||
// 处理图片
|
||||
if isImageURL(file.URL) {
|
||||
imgList = append(imgList, gin.H{
|
||||
"type": "image_url",
|
||||
"image_url": gin.H{
|
||||
"url": v,
|
||||
"url": file.URL,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// 如果不是逆向模型,则提取文件内容
|
||||
modelValue := input.ChatModel.Value
|
||||
if !(strings.Contains(modelValue, "-all") || strings.HasPrefix(modelValue, "gpt-4-gizmo") || strings.HasPrefix(modelValue, "claude")) {
|
||||
content, err := utils.ReadFileContent(file.URL, h.App.Config.TikaHost)
|
||||
if err != nil {
|
||||
logger.Error("error with read file: ", err)
|
||||
continue
|
||||
} else {
|
||||
fileContents = append(fileContents, fmt.Sprintf("%s 文件内容:%s", file.Name, content))
|
||||
}
|
||||
}
|
||||
}
|
||||
data = append(data, gin.H{
|
||||
"type": "text",
|
||||
"text": strings.TrimSpace(text),
|
||||
})
|
||||
content = data
|
||||
} else {
|
||||
content = fullPrompt
|
||||
}
|
||||
req.Messages = append(reqMgs, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": content,
|
||||
})
|
||||
|
||||
logger.Debugf("%+v", req.Messages)
|
||||
if len(fileContents) > 0 {
|
||||
finalPrompt = fmt.Sprintf("请根据提供的文件内容信息回答问题(其中Excel 已转成 HTML):\n\n %s\n\n 问题:%s", strings.Join(fileContents, "\n"), input.Prompt)
|
||||
tokens, _ := utils.CalcTokens(finalPrompt, req.Model)
|
||||
if tokens > input.ChatModel.MaxContext {
|
||||
return fmt.Errorf("文件的长度超出模型允许的最大上下文长度,请减少文件内容数量或文件大小。")
|
||||
}
|
||||
} else {
|
||||
finalPrompt = input.Prompt
|
||||
}
|
||||
|
||||
return h.sendOpenAiMessage(req, userVo, ctx, session, role, prompt, ws)
|
||||
if len(imgList) > 0 {
|
||||
imgList = append(imgList, map[string]interface{}{
|
||||
"type": "text",
|
||||
"text": input.Prompt,
|
||||
})
|
||||
req.Messages = append(reqMgs, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": imgList,
|
||||
})
|
||||
} else {
|
||||
req.Messages = append(reqMgs, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": finalPrompt,
|
||||
})
|
||||
}
|
||||
|
||||
return h.sendOpenAiMessage(req, userVo, ctx, input, c)
|
||||
}
|
||||
|
||||
// 判断一个 URL 是否图片链接
|
||||
func isImageURL(url string) bool {
|
||||
// 检查是否是有效的URL
|
||||
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查文件扩展名
|
||||
ext := strings.ToLower(path.Ext(url))
|
||||
validImageExts := map[string]bool{
|
||||
".jpg": true,
|
||||
".jpeg": true,
|
||||
".png": true,
|
||||
".gif": true,
|
||||
".bmp": true,
|
||||
".webp": true,
|
||||
".svg": true,
|
||||
".ico": true,
|
||||
}
|
||||
|
||||
if !validImageExts[ext] {
|
||||
return false
|
||||
}
|
||||
|
||||
// 发送HEAD请求检查Content-Type
|
||||
client := &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
resp, err := client.Head(url)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
return strings.HasPrefix(contentType, "image/")
|
||||
}
|
||||
|
||||
// Tokens 统计 token 数量
|
||||
@@ -331,15 +442,14 @@ func (h *ChatHandler) StopGenerate(c *gin.Context) {
|
||||
|
||||
// 发送请求到 OpenAI 服务器
|
||||
// useOwnApiKey: 是否使用了用户自己的 API KEY
|
||||
func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, session *types.ChatSession, apiKey *model.ApiKey) (*http.Response, error) {
|
||||
func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, input ChatInput, apiKey *model.ApiKey) (*http.Response, error) {
|
||||
// if the chat model bind a KEY, use it directly
|
||||
if session.Model.KeyId > 0 {
|
||||
h.DB.Where("id", session.Model.KeyId).Find(apiKey)
|
||||
}
|
||||
// use the last unused key
|
||||
if apiKey.Id == 0 {
|
||||
if input.ChatModel.KeyId > 0 {
|
||||
h.DB.Where("id", input.ChatModel.KeyId).Find(apiKey)
|
||||
} else { // use the last unused key
|
||||
h.DB.Where("type", "chat").Where("enabled", true).Order("last_used_at ASC").First(apiKey)
|
||||
}
|
||||
|
||||
if apiKey.Id == 0 {
|
||||
return nil, errors.New("no available key, please import key")
|
||||
}
|
||||
@@ -389,16 +499,16 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, sessi
|
||||
}
|
||||
|
||||
// 扣减用户算力
|
||||
func (h *ChatHandler) subUserPower(userVo vo.User, session *types.ChatSession, promptTokens int, replyTokens int) {
|
||||
func (h *ChatHandler) subUserPower(userVo vo.User, input ChatInput, promptTokens int, replyTokens int) {
|
||||
power := 1
|
||||
if session.Model.Power > 0 {
|
||||
power = session.Model.Power
|
||||
if input.ChatModel.Power > 0 {
|
||||
power = input.ChatModel.Power
|
||||
}
|
||||
|
||||
err := h.userService.DecreasePower(int(userVo.Id), power, model.PowerLog{
|
||||
err := h.userService.DecreasePower(userVo.Id, power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: session.Model.Value,
|
||||
Remark: fmt.Sprintf("模型名称:%s, 提问长度:%d,回复长度:%d", session.Model.Name, promptTokens, replyTokens),
|
||||
Model: input.ChatModel.Value,
|
||||
Remark: fmt.Sprintf("模型名称:%s, 提问长度:%d,回复长度:%d", input.ChatModel.Name, promptTokens, replyTokens),
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
@@ -409,19 +519,11 @@ func (h *ChatHandler) saveChatHistory(
|
||||
req types.ApiRequest,
|
||||
usage Usage,
|
||||
message types.Message,
|
||||
session *types.ChatSession,
|
||||
role model.ChatRole,
|
||||
input ChatInput,
|
||||
userVo vo.User,
|
||||
promptCreatedAt time.Time,
|
||||
replyCreatedAt time.Time) {
|
||||
|
||||
// 更新上下文消息
|
||||
if h.App.SysConfig.EnableContext {
|
||||
chatCtx := req.Messages // 提问消息
|
||||
chatCtx = append(chatCtx, message) // 回复消息
|
||||
h.ChatContexts.Put(session.ChatId, chatCtx)
|
||||
}
|
||||
|
||||
// 追加聊天记录
|
||||
// for prompt
|
||||
var promptTokens, replyTokens, totalTokens int
|
||||
@@ -432,12 +534,15 @@ func (h *ChatHandler) saveChatHistory(
|
||||
}
|
||||
|
||||
historyUserMsg := model.ChatMessage{
|
||||
UserId: userVo.Id,
|
||||
ChatId: session.ChatId,
|
||||
RoleId: role.Id,
|
||||
Type: types.PromptMsg,
|
||||
Icon: userVo.Avatar,
|
||||
Content: template.HTMLEscapeString(usage.Prompt),
|
||||
UserId: userVo.Id,
|
||||
ChatId: input.ChatId,
|
||||
RoleId: input.RoleId,
|
||||
Type: types.PromptMsg,
|
||||
Icon: userVo.Avatar,
|
||||
Content: utils.JsonEncode(vo.MsgContent{
|
||||
Text: usage.Prompt,
|
||||
Files: input.Files,
|
||||
}),
|
||||
Tokens: promptTokens,
|
||||
TotalTokens: promptTokens,
|
||||
UseContext: true,
|
||||
@@ -460,12 +565,15 @@ func (h *ChatHandler) saveChatHistory(
|
||||
totalTokens = replyTokens + getTotalTokens(req)
|
||||
}
|
||||
historyReplyMsg := model.ChatMessage{
|
||||
UserId: userVo.Id,
|
||||
ChatId: session.ChatId,
|
||||
RoleId: role.Id,
|
||||
Type: types.ReplyMsg,
|
||||
Icon: role.Icon,
|
||||
Content: usage.Content,
|
||||
UserId: userVo.Id,
|
||||
ChatId: input.ChatId,
|
||||
RoleId: input.RoleId,
|
||||
Type: types.ReplyMsg,
|
||||
Icon: input.ChatRole.Icon,
|
||||
Content: utils.JsonEncode(vo.MsgContent{
|
||||
Text: message.Content,
|
||||
Files: input.Files,
|
||||
}),
|
||||
Tokens: replyTokens,
|
||||
TotalTokens: totalTokens,
|
||||
UseContext: true,
|
||||
@@ -479,17 +587,17 @@ func (h *ChatHandler) saveChatHistory(
|
||||
}
|
||||
|
||||
// 更新用户算力
|
||||
if session.Model.Power > 0 {
|
||||
h.subUserPower(userVo, session, promptTokens, replyTokens)
|
||||
if input.ChatModel.Power > 0 {
|
||||
h.subUserPower(userVo, input, promptTokens, replyTokens)
|
||||
}
|
||||
// 保存当前会话
|
||||
var chatItem model.ChatItem
|
||||
err = h.DB.Where("chat_id = ?", session.ChatId).First(&chatItem).Error
|
||||
err = h.DB.Where("chat_id = ?", input.ChatId).First(&chatItem).Error
|
||||
if err != nil {
|
||||
chatItem.ChatId = session.ChatId
|
||||
chatItem.ChatId = input.ChatId
|
||||
chatItem.UserId = userVo.Id
|
||||
chatItem.RoleId = role.Id
|
||||
chatItem.ModelId = session.Model.Id
|
||||
chatItem.RoleId = input.RoleId
|
||||
chatItem.ModelId = input.ModelId
|
||||
if utf8.RuneCountInString(usage.Prompt) > 30 {
|
||||
chatItem.Title = string([]rune(usage.Prompt)[:30]) + "..."
|
||||
} else {
|
||||
@@ -503,7 +611,7 @@ func (h *ChatHandler) saveChatHistory(
|
||||
}
|
||||
}
|
||||
|
||||
// 文本生成语音
|
||||
// TextToSpeech 文本生成语音
|
||||
func (h *ChatHandler) TextToSpeech(c *gin.Context) {
|
||||
var data struct {
|
||||
ModelId int `json:"model_id"`
|
||||
@@ -517,13 +625,19 @@ func (h *ChatHandler) TextToSpeech(c *gin.Context) {
|
||||
textHash := utils.Sha256(fmt.Sprintf("%d/%s", data.ModelId, data.Text))
|
||||
audioFile := fmt.Sprintf("%s/audio", h.App.Config.StaticDir)
|
||||
if _, err := os.Stat(audioFile); err != nil {
|
||||
os.MkdirAll(audioFile, 0755)
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(audioFile, 0755); err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
audioFile = fmt.Sprintf("%s/%s.mp3", audioFile, textHash)
|
||||
if _, err := os.Stat(audioFile); err == nil {
|
||||
// 设置响应头
|
||||
c.Header("Content-Type", "audio/mpeg")
|
||||
c.Header("Content-Disposition", "attachment; filename=speech.mp3")
|
||||
c.Header("Prompt-Type", "audio/mpeg")
|
||||
c.Header("Prompt-Disposition", "attachment; filename=speech.mp3")
|
||||
c.File(audioFile)
|
||||
return
|
||||
}
|
||||
@@ -587,9 +701,230 @@ func (h *ChatHandler) TextToSpeech(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
c.Header("Content-Type", "audio/mpeg")
|
||||
c.Header("Content-Disposition", "attachment; filename=speech.mp3")
|
||||
c.Header("Prompt-Type", "audio/mpeg")
|
||||
c.Header("Prompt-Disposition", "attachment; filename=speech.mp3")
|
||||
|
||||
// 直接写入完整的音频数据到响应
|
||||
c.Writer.Write(audioBytes)
|
||||
_, err = c.Writer.Write(audioBytes)
|
||||
if err != nil {
|
||||
logger.Error("写入音频数据到响应失败:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// // OPenAI 消息发送实现
|
||||
// func (h *ChatHandler) sendOpenAiMessage(
|
||||
// req types.ApiRequest,
|
||||
// userVo vo.User,
|
||||
// ctx context.Context,
|
||||
// session *types.ChatSession,
|
||||
// role model.ChatRole,
|
||||
// prompt string,
|
||||
// c *gin.Context) error {
|
||||
// promptCreatedAt := time.Now() // 记录提问时间
|
||||
// start := time.Now()
|
||||
// var apiKey = model.ApiKey{}
|
||||
// response, err := h.doRequest(ctx, req, session, &apiKey)
|
||||
// logger.Info("HTTP请求完成,耗时:", time.Since(start))
|
||||
// if err != nil {
|
||||
// if strings.Contains(err.Error(), "context canceled") {
|
||||
// return fmt.Errorf("用户取消了请求:%s", prompt)
|
||||
// } else if strings.Contains(err.Error(), "no available key") {
|
||||
// return errors.New("抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
|
||||
// }
|
||||
// return err
|
||||
// } else {
|
||||
// defer response.Body.Close()
|
||||
// }
|
||||
|
||||
// if response.StatusCode != 200 {
|
||||
// body, _ := io.ReadAll(response.Body)
|
||||
// return fmt.Errorf("请求 OpenAI API 失败:%d, %v", response.StatusCode, string(body))
|
||||
// }
|
||||
|
||||
// contentType := response.Header.Get("Prompt-Type")
|
||||
// if strings.Contains(contentType, "text/event-stream") {
|
||||
// replyCreatedAt := time.Now() // 记录回复时间
|
||||
// // 循环读取 Chunk 消息
|
||||
// var message = types.Message{Role: "assistant"}
|
||||
// var contents = make([]string, 0)
|
||||
// var function model.Function
|
||||
// var toolCall = false
|
||||
// var arguments = make([]string, 0)
|
||||
// var reasoning = false
|
||||
|
||||
// pushMessage(c, ChatEventStart, "开始响应")
|
||||
// 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 { // 数据解析出错
|
||||
// return errors.New(line)
|
||||
// }
|
||||
// if len(responseBody.Choices) == 0 { // Fixed: 兼容 Azure API 第一个输出空行
|
||||
// continue
|
||||
// }
|
||||
// if responseBody.Choices[0].Delta.Prompt == nil &&
|
||||
// responseBody.Choices[0].Delta.ToolCalls == nil &&
|
||||
// responseBody.Choices[0].Delta.ReasoningContent == "" {
|
||||
// continue
|
||||
// }
|
||||
|
||||
// if responseBody.Choices[0].FinishReason == "stop" && len(contents) == 0 {
|
||||
// pushMessage(c, ChatEventError, "抱歉😔😔😔,AI助手由于未知原因已经停止输出内容。")
|
||||
// break
|
||||
// }
|
||||
|
||||
// var tool types.ToolCall
|
||||
// if len(responseBody.Choices[0].Delta.ToolCalls) > 0 {
|
||||
// tool = responseBody.Choices[0].Delta.ToolCalls[0]
|
||||
// if toolCall && tool.Function.Name == "" {
|
||||
// arguments = append(arguments, tool.Function.Arguments)
|
||||
// continue
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 兼容 Function Call
|
||||
// fun := responseBody.Choices[0].Delta.FunctionCall
|
||||
// if fun.Name != "" {
|
||||
// tool = *new(types.ToolCall)
|
||||
// tool.Function.Name = fun.Name
|
||||
// } else if toolCall {
|
||||
// arguments = append(arguments, fun.Arguments)
|
||||
// continue
|
||||
// }
|
||||
|
||||
// if !utils.IsEmptyValue(tool) {
|
||||
// res := h.DB.Where("name = ?", tool.Function.Name).First(&function)
|
||||
// if res.Error == nil {
|
||||
// toolCall = true
|
||||
// callMsg := fmt.Sprintf("正在调用工具 `%s` 作答 ...\n\n", function.Label)
|
||||
// pushMessage(c, ChatEventMessageDelta, map[string]interface{}{
|
||||
// "type": "text",
|
||||
// "content": callMsg,
|
||||
// })
|
||||
// contents = append(contents, callMsg)
|
||||
// }
|
||||
// continue
|
||||
// }
|
||||
|
||||
// if responseBody.Choices[0].FinishReason == "tool_calls" ||
|
||||
// responseBody.Choices[0].FinishReason == "function_call" { // 函数调用完毕
|
||||
// break
|
||||
// }
|
||||
|
||||
// // output stopped
|
||||
// if responseBody.Choices[0].FinishReason != "" {
|
||||
// break // 输出完成或者输出中断了
|
||||
// } else { // 正常输出结果
|
||||
// // 兼容思考过程
|
||||
// if responseBody.Choices[0].Delta.ReasoningContent != "" {
|
||||
// reasoningContent := responseBody.Choices[0].Delta.ReasoningContent
|
||||
// if !reasoning {
|
||||
// reasoningContent = fmt.Sprintf("<think>%s", reasoningContent)
|
||||
// reasoning = true
|
||||
// }
|
||||
|
||||
// pushMessage(c, ChatEventMessageDelta, map[string]interface{}{
|
||||
// "type": "text",
|
||||
// "content": reasoningContent,
|
||||
// })
|
||||
// contents = append(contents, reasoningContent)
|
||||
// } else if responseBody.Choices[0].Delta.Prompt != "" {
|
||||
// finalContent := responseBody.Choices[0].Delta.Prompt
|
||||
// if reasoning {
|
||||
// finalContent = fmt.Sprintf("</think>%s", responseBody.Choices[0].Delta.Prompt)
|
||||
// reasoning = false
|
||||
// }
|
||||
// contents = append(contents, utils.InterfaceToString(finalContent))
|
||||
// pushMessage(c, ChatEventMessageDelta, map[string]interface{}{
|
||||
// "type": "text",
|
||||
// "content": finalContent,
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// } // end for
|
||||
|
||||
// if err := scanner.Err(); err != nil {
|
||||
// if strings.Contains(err.Error(), "context canceled") {
|
||||
// logger.Info("用户取消了请求:", prompt)
|
||||
// } else {
|
||||
// logger.Error("信息读取出错:", err)
|
||||
// }
|
||||
// }
|
||||
|
||||
// if toolCall { // 调用函数完成任务
|
||||
// params := make(map[string]any)
|
||||
// _ = utils.JsonDecode(strings.Join(arguments, ""), ¶ms)
|
||||
// logger.Debugf("函数名称: %s, 函数参数:%s", function.Name, params)
|
||||
// params["user_id"] = userVo.Id
|
||||
// var apiRes types.BizVo
|
||||
// r, err := req2.C().R().SetHeader("Body-Type", "application/json").
|
||||
// SetHeader("Authorization", function.Token).
|
||||
// SetBody(params).Post(function.Action)
|
||||
// errMsg := ""
|
||||
// if err != nil {
|
||||
// errMsg = err.Error()
|
||||
// } else {
|
||||
// all, _ := io.ReadAll(r.Body)
|
||||
// err = json.Unmarshal(all, &apiRes)
|
||||
// if err != nil {
|
||||
// errMsg = err.Error()
|
||||
// } else if apiRes.Code != types.Success {
|
||||
// errMsg = apiRes.Message
|
||||
// }
|
||||
// }
|
||||
|
||||
// if errMsg != "" {
|
||||
// errMsg = "调用函数工具出错:" + errMsg
|
||||
// contents = append(contents, errMsg)
|
||||
// } else {
|
||||
// errMsg = utils.InterfaceToString(apiRes.Data)
|
||||
// contents = append(contents, errMsg)
|
||||
// }
|
||||
// pushMessage(c, ChatEventMessageDelta, map[string]interface{}{
|
||||
// "type": "text",
|
||||
// "content": errMsg,
|
||||
// })
|
||||
// }
|
||||
|
||||
// // 消息发送成功
|
||||
// if len(contents) > 0 {
|
||||
// usage := Usage{
|
||||
// Prompt: prompt,
|
||||
// Prompt: strings.Join(contents, ""),
|
||||
// PromptTokens: 0,
|
||||
// CompletionTokens: 0,
|
||||
// TotalTokens: 0,
|
||||
// }
|
||||
// message.Prompt = usage.Prompt
|
||||
// h.saveChatHistory(req, usage, message, session, role, userVo, promptCreatedAt, replyCreatedAt)
|
||||
// }
|
||||
// } else {
|
||||
// var respVo OpenAIResVo
|
||||
// body, err := io.ReadAll(response.Body)
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("读取响应失败:%v", body)
|
||||
// }
|
||||
// err = json.Unmarshal(body, &respVo)
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("解析响应失败:%v", body)
|
||||
// }
|
||||
// content := respVo.Choices[0].Message.Prompt
|
||||
// if strings.HasPrefix(req.Model, "o1-") {
|
||||
// content = fmt.Sprintf("AI思考结束,耗时:%d 秒。\n%s", time.Now().Unix()-session.Start, respVo.Choices[0].Message.Prompt)
|
||||
// }
|
||||
// pushMessage(c, ChatEventMessageDelta, map[string]interface{}{
|
||||
// "type": "text",
|
||||
// "content": content,
|
||||
// })
|
||||
// respVo.Usage.Prompt = prompt
|
||||
// respVo.Usage.Prompt = content
|
||||
// h.saveChatHistory(req, respVo.Usage, respVo.Choices[0].Message, session, role, userVo, promptCreatedAt, time.Now())
|
||||
// }
|
||||
|
||||
// return nil
|
||||
// }
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
|
||||
// List 获取会话列表
|
||||
func (h *ChatHandler) List(c *gin.Context) {
|
||||
logger.Info(h.GetLoginUserId(c))
|
||||
if !h.IsLogin(c) {
|
||||
resp.SUCCESS(c)
|
||||
return
|
||||
@@ -28,7 +29,7 @@ func (h *ChatHandler) List(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
var items = make([]vo.ChatItem, 0)
|
||||
var chats []model.ChatItem
|
||||
h.DB.Where("user_id", userId).Order("id DESC").Find(&chats)
|
||||
h.DB.Debug().Where("user_id", userId).Order("id DESC").Find(&chats)
|
||||
if len(chats) == 0 {
|
||||
resp.SUCCESS(c, items)
|
||||
return
|
||||
@@ -104,8 +105,6 @@ func (h *ChatHandler) Clear(c *gin.Context) {
|
||||
var chatIds = make([]string, 0)
|
||||
for _, chat := range chats {
|
||||
chatIds = append(chatIds, chat.ChatId)
|
||||
// 清空会话上下文
|
||||
h.ChatContexts.Delete(chat.ChatId)
|
||||
}
|
||||
err = h.DB.Transaction(func(tx *gorm.DB) error {
|
||||
res := h.DB.Where("user_id =?", user.Id).Delete(&model.ChatItem{})
|
||||
@@ -133,20 +132,28 @@ func (h *ChatHandler) Clear(c *gin.Context) {
|
||||
func (h *ChatHandler) History(c *gin.Context) {
|
||||
chatId := c.Query("chat_id") // 会话 ID
|
||||
var items []model.ChatMessage
|
||||
var messages = make([]vo.HistoryMessage, 0)
|
||||
var messages = make([]vo.ChatMessage, 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
|
||||
var v vo.ChatMessage
|
||||
err := utils.CopyObject(item, &v)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// 解析内容
|
||||
var content vo.MsgContent
|
||||
err = utils.JsonDecode(item.Content, &content)
|
||||
if err != nil {
|
||||
content.Text = item.Content
|
||||
}
|
||||
v.Content = content
|
||||
v.CreatedAt = item.CreatedAt.Unix()
|
||||
v.UpdatedAt = item.UpdatedAt.Unix()
|
||||
if err == nil {
|
||||
messages = append(messages, v)
|
||||
}
|
||||
messages = append(messages, v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,10 +186,6 @@ func (h *ChatHandler) Remove(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 是否要删除 MidJourney 绘画记录和图片文件?
|
||||
|
||||
// 清空会话上下文
|
||||
h.ChatContexts.Delete(chatId)
|
||||
resp.SUCCESS(c, types.OkMsg)
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
req2 "github.com/imroc/req/v3"
|
||||
)
|
||||
|
||||
@@ -55,18 +56,16 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
req types.ApiRequest,
|
||||
userVo vo.User,
|
||||
ctx context.Context,
|
||||
session *types.ChatSession,
|
||||
role model.ChatRole,
|
||||
prompt string,
|
||||
ws *types.WsClient) error {
|
||||
input ChatInput,
|
||||
c *gin.Context) error {
|
||||
promptCreatedAt := time.Now() // 记录提问时间
|
||||
start := time.Now()
|
||||
var apiKey = model.ApiKey{}
|
||||
response, err := h.doRequest(ctx, req, session, &apiKey)
|
||||
response, err := h.doRequest(ctx, req, input, &apiKey)
|
||||
logger.Info("HTTP请求完成,耗时:", time.Since(start))
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "context canceled") {
|
||||
return fmt.Errorf("用户取消了请求:%s", prompt)
|
||||
return fmt.Errorf("用户取消了请求:%s", input.Prompt)
|
||||
} else if strings.Contains(err.Error(), "no available key") {
|
||||
return errors.New("抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
|
||||
}
|
||||
@@ -112,7 +111,7 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
}
|
||||
|
||||
if responseBody.Choices[0].FinishReason == "stop" && len(contents) == 0 {
|
||||
utils.SendChunkMsg(ws, "抱歉😔😔😔,AI助手由于未知原因已经停止输出内容。")
|
||||
pushMessage(c, "text", "抱歉😔😔😔,AI助手由于未知原因已经停止输出内容。")
|
||||
break
|
||||
}
|
||||
|
||||
@@ -140,7 +139,7 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
if res.Error == nil {
|
||||
toolCall = true
|
||||
callMsg := fmt.Sprintf("正在调用工具 `%s` 作答 ...\n\n", function.Label)
|
||||
utils.SendChunkMsg(ws, callMsg)
|
||||
pushMessage(c, "text", callMsg)
|
||||
contents = append(contents, callMsg)
|
||||
}
|
||||
continue
|
||||
@@ -163,7 +162,7 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
reasoning = true
|
||||
}
|
||||
|
||||
utils.SendChunkMsg(ws, reasoningContent)
|
||||
pushMessage(c, "text", reasoningContent)
|
||||
contents = append(contents, reasoningContent)
|
||||
} else if responseBody.Choices[0].Delta.Content != "" {
|
||||
finalContent := responseBody.Choices[0].Delta.Content
|
||||
@@ -172,14 +171,14 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
reasoning = false
|
||||
}
|
||||
contents = append(contents, utils.InterfaceToString(finalContent))
|
||||
utils.SendChunkMsg(ws, finalContent)
|
||||
pushMessage(c, "text", finalContent)
|
||||
}
|
||||
}
|
||||
} // end for
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
if strings.Contains(err.Error(), "context canceled") {
|
||||
logger.Info("用户取消了请求:", prompt)
|
||||
logger.Info("用户取消了请求:", input.Prompt)
|
||||
} else {
|
||||
logger.Error("信息读取出错:", err)
|
||||
}
|
||||
@@ -214,20 +213,20 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
errMsg = utils.InterfaceToString(apiRes.Data)
|
||||
contents = append(contents, errMsg)
|
||||
}
|
||||
utils.SendChunkMsg(ws, errMsg)
|
||||
pushMessage(c, "text", errMsg)
|
||||
}
|
||||
|
||||
// 消息发送成功
|
||||
if len(contents) > 0 {
|
||||
usage := Usage{
|
||||
Prompt: prompt,
|
||||
Prompt: input.Prompt,
|
||||
Content: strings.Join(contents, ""),
|
||||
PromptTokens: 0,
|
||||
CompletionTokens: 0,
|
||||
TotalTokens: 0,
|
||||
}
|
||||
message.Content = usage.Content
|
||||
h.saveChatHistory(req, usage, message, session, role, userVo, promptCreatedAt, replyCreatedAt)
|
||||
h.saveChatHistory(req, usage, message, input, userVo, promptCreatedAt, replyCreatedAt)
|
||||
}
|
||||
} else { // 非流式输出
|
||||
var respVo OpenAIResVo
|
||||
@@ -240,13 +239,10 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
return fmt.Errorf("解析响应失败:%v", body)
|
||||
}
|
||||
content := respVo.Choices[0].Message.Content
|
||||
if strings.HasPrefix(req.Model, "o1-") {
|
||||
content = fmt.Sprintf("AI思考结束,耗时:%d 秒。\n%s", time.Now().Unix()-session.Start, respVo.Choices[0].Message.Content)
|
||||
}
|
||||
utils.SendChunkMsg(ws, content)
|
||||
respVo.Usage.Prompt = prompt
|
||||
pushMessage(c, "text", content)
|
||||
respVo.Usage.Prompt = input.Prompt
|
||||
respVo.Usage.Content = content
|
||||
h.saveChatHistory(req, respVo.Usage, respVo.Choices[0].Message, session, role, userVo, promptCreatedAt, time.Now())
|
||||
h.saveChatHistory(req, respVo.Usage, respVo.Choices[0].Message, input, userVo, promptCreatedAt, time.Now())
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -64,10 +64,12 @@ func (h *ChatRoleHandler) ListByUser(c *gin.Context) {
|
||||
var user model.User
|
||||
h.DB.First(&user, userId)
|
||||
var roleKeys []string
|
||||
err := utils.JsonDecode(user.ChatRoles, &roleKeys)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "角色解析失败!")
|
||||
return
|
||||
if user.ChatRoles != "" {
|
||||
err := utils.JsonDecode(user.ChatRoles, &roleKeys)
|
||||
if err != nil {
|
||||
resp.ERROR(c, "角色解析失败!")
|
||||
return
|
||||
}
|
||||
}
|
||||
// 保证用户至少有一个角色可用
|
||||
if len(roleKeys) > 0 {
|
||||
|
||||
@@ -31,14 +31,14 @@ func NewConfigHandler(app *core.AppServer, db *gorm.DB, licenseService *service.
|
||||
func (h *ConfigHandler) Get(c *gin.Context) {
|
||||
key := c.Query("key")
|
||||
var config model.Config
|
||||
res := h.DB.Where("marker", key).First(&config)
|
||||
res := h.DB.Where("name", key).First(&config)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, res.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var value map[string]interface{}
|
||||
err := utils.JsonDecode(config.Config, &value)
|
||||
var value map[string]any
|
||||
err := utils.JsonDecode(config.Value, &value)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
|
||||
@@ -77,7 +77,7 @@ func (h *DallJobHandler) Image(c *gin.Context) {
|
||||
Quality: data.Quality,
|
||||
Size: data.Size,
|
||||
Style: data.Style,
|
||||
TranslateModelId: h.App.SysConfig.TranslateModelId,
|
||||
TranslateModelId: h.App.SysConfig.AssistantModelId,
|
||||
Power: chatModel.Power,
|
||||
}
|
||||
job := model.DallJob{
|
||||
@@ -96,7 +96,7 @@ func (h *DallJobHandler) Image(c *gin.Context) {
|
||||
h.dallService.PushTask(task)
|
||||
|
||||
// 扣减算力
|
||||
err = h.userService.DecreasePower(int(user.Id), chatModel.Power, model.PowerLog{
|
||||
err = h.userService.DecreasePower(user.Id, chatModel.Power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: chatModel.Value,
|
||||
Remark: fmt.Sprintf("绘画提示词:%s", utils.CutWords(task.Prompt, 10)),
|
||||
|
||||
@@ -213,7 +213,7 @@ func (h *FunctionHandler) Dall3(c *gin.Context) {
|
||||
Prompt: prompt,
|
||||
ModelId: 0,
|
||||
ModelName: "dall-e-3",
|
||||
TranslateModelId: h.App.SysConfig.TranslateModelId,
|
||||
TranslateModelId: h.App.SysConfig.AssistantModelId,
|
||||
N: 1,
|
||||
Quality: "standard",
|
||||
Size: "1024x1024",
|
||||
@@ -240,7 +240,7 @@ func (h *FunctionHandler) Dall3(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 扣减算力
|
||||
err = h.userService.DecreasePower(int(user.Id), job.Power, model.PowerLog{
|
||||
err = h.userService.DecreasePower(user.Id, job.Power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: task.ModelName,
|
||||
Remark: fmt.Sprintf("绘画提示词:%s", utils.CutWords(job.Prompt, 10)),
|
||||
@@ -265,27 +265,27 @@ func (h *FunctionHandler) WebSearch(c *gin.Context) {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 从参数中获取搜索关键词
|
||||
keyword, ok := params["keyword"].(string)
|
||||
if !ok || keyword == "" {
|
||||
resp.ERROR(c, "搜索关键词不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 从参数中获取最大页数,默认为1页
|
||||
maxPages := 1
|
||||
if pages, ok := params["max_pages"].(float64); ok {
|
||||
maxPages = int(pages)
|
||||
}
|
||||
|
||||
|
||||
// 获取用户ID
|
||||
userID, ok := params["user_id"].(float64)
|
||||
if !ok {
|
||||
resp.ERROR(c, "用户ID不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 查询用户信息
|
||||
var user model.User
|
||||
res := h.DB.Where("id = ?", int(userID)).First(&user)
|
||||
@@ -293,23 +293,23 @@ func (h *FunctionHandler) WebSearch(c *gin.Context) {
|
||||
resp.ERROR(c, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 检查用户算力是否足够
|
||||
searchPower := 1 // 每次搜索消耗1点算力
|
||||
if user.Power < searchPower {
|
||||
resp.ERROR(c, "算力不足,无法执行网络搜索")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 执行网络搜索
|
||||
searchResults, err := crawler.SearchWeb(keyword, maxPages)
|
||||
if err != nil {
|
||||
resp.ERROR(c, fmt.Sprintf("搜索失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 扣减用户算力
|
||||
err = h.userService.DecreasePower(int(user.Id), searchPower, model.PowerLog{
|
||||
err = h.userService.DecreasePower(user.Id, searchPower, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: "web_search",
|
||||
Remark: fmt.Sprintf("网络搜索:%s", utils.CutWords(keyword, 10)),
|
||||
@@ -318,7 +318,7 @@ func (h *FunctionHandler) WebSearch(c *gin.Context) {
|
||||
resp.ERROR(c, "扣减算力失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 返回搜索结果
|
||||
resp.SUCCESS(c, searchResults)
|
||||
}
|
||||
|
||||
442
api/handler/jimeng_handler.go
Normal file
442
api/handler/jimeng_handler.go
Normal file
@@ -0,0 +1,442 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/service/jimeng"
|
||||
"geekai/store/model"
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// JimengHandler 即梦AI处理器
|
||||
type JimengHandler struct {
|
||||
BaseHandler
|
||||
jimengService *jimeng.Service
|
||||
userService *service.UserService
|
||||
}
|
||||
|
||||
// NewJimengHandler 创建即梦AI处理器
|
||||
func NewJimengHandler(app *core.AppServer, jimengService *jimeng.Service, db *gorm.DB, userService *service.UserService) *JimengHandler {
|
||||
return &JimengHandler{
|
||||
BaseHandler: BaseHandler{App: app, DB: db},
|
||||
jimengService: jimengService,
|
||||
userService: userService,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由,新增统一任务接口
|
||||
func (h *JimengHandler) RegisterRoutes() {
|
||||
rg := h.App.Engine.Group("/api/jimeng")
|
||||
rg.POST("task", h.CreateTask) // 只保留统一任务接口
|
||||
rg.GET("power-config", h.GetPowerConfig) // 新增算力配置接口
|
||||
rg.POST("jobs", h.Jobs)
|
||||
rg.GET("remove", h.Remove)
|
||||
rg.GET("retry", h.Retry)
|
||||
}
|
||||
|
||||
// JimengTaskRequest 统一任务请求结构体
|
||||
// 支持所有生图和生成视频类型
|
||||
type JimengTaskRequest struct {
|
||||
TaskType string `json:"task_type" binding:"required"`
|
||||
Prompt string `json:"prompt"`
|
||||
ImageInput string `json:"image_input"`
|
||||
ImageUrls []string `json:"image_urls"`
|
||||
BinaryDataBase64 []string `json:"binary_data_base64"`
|
||||
Scale float64 `json:"scale"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Gpen float64 `json:"gpen"`
|
||||
Skin float64 `json:"skin"`
|
||||
SkinUnifi float64 `json:"skin_unifi"`
|
||||
GenMode string `json:"gen_mode"`
|
||||
Seed int64 `json:"seed"`
|
||||
UsePreLLM bool `json:"use_pre_llm"`
|
||||
TemplateId string `json:"template_id"`
|
||||
AspectRatio string `json:"aspect_ratio"`
|
||||
}
|
||||
|
||||
// CreateTask 统一任务创建接口
|
||||
func (h *JimengHandler) CreateTask(c *gin.Context) {
|
||||
var req JimengTaskRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
// 新增:除图像特效外,其他任务类型必须有提示词
|
||||
if req.TaskType != "image_effects" && req.Prompt == "" {
|
||||
resp.ERROR(c, "提示词不能为空")
|
||||
return
|
||||
}
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Width == 0 {
|
||||
req.Width = 1328
|
||||
}
|
||||
if req.Height == 0 {
|
||||
req.Height = 1328
|
||||
}
|
||||
if req.Seed == 0 {
|
||||
req.Seed = -1
|
||||
}
|
||||
|
||||
var powerCost int
|
||||
var taskType model.JMTaskType
|
||||
var params map[string]any
|
||||
var reqKey string
|
||||
var modelName string
|
||||
|
||||
switch req.TaskType {
|
||||
case "text_to_image":
|
||||
powerCost = h.getPowerFromConfig(model.JMTaskTypeTextToImage)
|
||||
taskType = model.JMTaskTypeTextToImage
|
||||
reqKey = jimeng.ReqKeyTextToImage
|
||||
modelName = "即梦文生图"
|
||||
if req.Scale == 0 {
|
||||
req.Scale = 2.5
|
||||
}
|
||||
params = map[string]any{
|
||||
"seed": req.Seed,
|
||||
"scale": req.Scale,
|
||||
"width": req.Width,
|
||||
"height": req.Height,
|
||||
"use_pre_llm": req.UsePreLLM,
|
||||
}
|
||||
case "image_to_image":
|
||||
powerCost = h.getPowerFromConfig(model.JMTaskTypeImageToImage)
|
||||
taskType = model.JMTaskTypeImageToImage
|
||||
reqKey = jimeng.ReqKeyImageToImagePortrait
|
||||
modelName = "即梦图生图"
|
||||
if req.Gpen == 0 {
|
||||
req.Gpen = 0.4
|
||||
}
|
||||
if req.Skin == 0 {
|
||||
req.Skin = 0.3
|
||||
}
|
||||
if req.GenMode == "" {
|
||||
if req.Prompt != "" {
|
||||
req.GenMode = jimeng.GenModeCreative
|
||||
} else {
|
||||
req.GenMode = jimeng.GenModeReference
|
||||
}
|
||||
}
|
||||
params = map[string]any{
|
||||
"image_input": req.ImageInput,
|
||||
"width": req.Width,
|
||||
"height": req.Height,
|
||||
"gpen": req.Gpen,
|
||||
"skin": req.Skin,
|
||||
"skin_unifi": req.SkinUnifi,
|
||||
"gen_mode": req.GenMode,
|
||||
"seed": req.Seed,
|
||||
}
|
||||
case "image_edit":
|
||||
powerCost = h.getPowerFromConfig(model.JMTaskTypeImageEdit)
|
||||
taskType = model.JMTaskTypeImageEdit
|
||||
reqKey = jimeng.ReqKeyImageEdit
|
||||
modelName = "即梦图像编辑"
|
||||
if req.Scale == 0 {
|
||||
req.Scale = 0.5
|
||||
}
|
||||
params = map[string]any{
|
||||
"seed": req.Seed,
|
||||
"scale": req.Scale,
|
||||
}
|
||||
if len(req.ImageUrls) > 0 {
|
||||
params["image_urls"] = req.ImageUrls
|
||||
}
|
||||
if len(req.BinaryDataBase64) > 0 {
|
||||
params["binary_data_base64"] = req.BinaryDataBase64
|
||||
}
|
||||
case "image_effects":
|
||||
powerCost = h.getPowerFromConfig(model.JMTaskTypeImageEffects)
|
||||
taskType = model.JMTaskTypeImageEffects
|
||||
reqKey = jimeng.ReqKeyImageEffects
|
||||
modelName = "即梦图像特效"
|
||||
if req.Width == 0 {
|
||||
req.Width = 1328
|
||||
}
|
||||
if req.Height == 0 {
|
||||
req.Height = 1328
|
||||
}
|
||||
params = map[string]any{
|
||||
"image_input1": req.ImageInput,
|
||||
"template_id": req.TemplateId,
|
||||
"width": req.Width,
|
||||
"height": req.Height,
|
||||
}
|
||||
case "text_to_video":
|
||||
powerCost = h.getPowerFromConfig(model.JMTaskTypeTextToVideo)
|
||||
taskType = model.JMTaskTypeTextToVideo
|
||||
reqKey = jimeng.ReqKeyTextToVideo
|
||||
modelName = "即梦文生视频"
|
||||
if req.Seed == 0 {
|
||||
req.Seed = -1
|
||||
}
|
||||
if req.AspectRatio == "" {
|
||||
req.AspectRatio = jimeng.AspectRatio16_9
|
||||
}
|
||||
params = map[string]any{
|
||||
"seed": req.Seed,
|
||||
"aspect_ratio": req.AspectRatio,
|
||||
}
|
||||
case "image_to_video":
|
||||
powerCost = h.getPowerFromConfig(model.JMTaskTypeImageToVideo)
|
||||
taskType = model.JMTaskTypeImageToVideo
|
||||
reqKey = jimeng.ReqKeyImageToVideo
|
||||
modelName = "即梦图生视频"
|
||||
if req.Seed == 0 {
|
||||
req.Seed = -1
|
||||
}
|
||||
params = map[string]any{
|
||||
"seed": req.Seed,
|
||||
"aspect_ratio": req.AspectRatio,
|
||||
}
|
||||
if len(req.ImageUrls) > 0 {
|
||||
params["image_urls"] = req.ImageUrls
|
||||
}
|
||||
if len(req.BinaryDataBase64) > 0 {
|
||||
params["binary_data_base64"] = req.BinaryDataBase64
|
||||
}
|
||||
default:
|
||||
resp.ERROR(c, "不支持的任务类型")
|
||||
return
|
||||
}
|
||||
|
||||
if user.Power < powerCost {
|
||||
resp.ERROR(c, fmt.Sprintf("算力不足,需要%d算力", powerCost))
|
||||
return
|
||||
}
|
||||
|
||||
taskReq := &jimeng.CreateTaskRequest{
|
||||
Type: taskType,
|
||||
Prompt: req.Prompt,
|
||||
Params: params,
|
||||
ReqKey: reqKey,
|
||||
Power: powerCost,
|
||||
}
|
||||
|
||||
job, err := h.jimengService.CreateTask(user.Id, taskReq)
|
||||
if err != nil {
|
||||
logger.Errorf("create jimeng task failed: %v", err)
|
||||
resp.ERROR(c, "创建任务失败")
|
||||
return
|
||||
}
|
||||
|
||||
h.userService.DecreasePower(user.Id, powerCost, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: "jimeng",
|
||||
Remark: fmt.Sprintf("%s,任务ID:%d", modelName, job.Id),
|
||||
})
|
||||
|
||||
resp.SUCCESS(c, job)
|
||||
}
|
||||
|
||||
// Jobs 获取任务列表
|
||||
func (h *JimengHandler) Jobs(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
|
||||
var req struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Filter string `json:"filter"`
|
||||
Ids []uint `json:"ids"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
var jobs []model.JimengJob
|
||||
var total int64
|
||||
query := h.DB.Model(&model.JimengJob{}).Where("user_id = ?", userId)
|
||||
|
||||
switch req.Filter {
|
||||
case "image":
|
||||
query = query.Where("type IN (?)", []model.JMTaskType{
|
||||
model.JMTaskTypeTextToImage,
|
||||
model.JMTaskTypeImageToImage,
|
||||
model.JMTaskTypeImageEdit,
|
||||
model.JMTaskTypeImageEffects,
|
||||
})
|
||||
case "video":
|
||||
query = query.Where("type IN (?)", []model.JMTaskType{
|
||||
model.JMTaskTypeTextToVideo,
|
||||
model.JMTaskTypeImageToVideo,
|
||||
})
|
||||
}
|
||||
|
||||
if len(req.Ids) > 0 {
|
||||
query = query.Where("id IN (?)", req.Ids)
|
||||
}
|
||||
|
||||
// 统计总数
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
offset := (req.Page - 1) * req.PageSize
|
||||
if err := query.Order("updated_at DESC").Offset(offset).Limit(req.PageSize).Find(&jobs).Error; err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 填充 VO
|
||||
var jobVos []vo.JimengJob
|
||||
for _, job := range jobs {
|
||||
var jobVo vo.JimengJob
|
||||
err := utils.CopyObject(job, &jobVo)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
jobVo.CreatedAt = job.CreatedAt.Unix()
|
||||
jobVos = append(jobVos, jobVo)
|
||||
}
|
||||
resp.SUCCESS(c, vo.NewPage(total, req.Page, req.PageSize, jobVos))
|
||||
}
|
||||
|
||||
// Remove 删除任务
|
||||
func (h *JimengHandler) Remove(c *gin.Context) {
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
jobId := h.GetInt(c, "id", 0)
|
||||
if jobId == 0 {
|
||||
resp.ERROR(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取任务,判断状态
|
||||
job, err := h.jimengService.GetJob(uint(jobId))
|
||||
if err != nil {
|
||||
resp.ERROR(c, "任务不存在")
|
||||
return
|
||||
}
|
||||
if job.UserId != user.Id {
|
||||
resp.ERROR(c, "无权限操作")
|
||||
return
|
||||
}
|
||||
if job.Status != model.JMTaskStatusFailed {
|
||||
resp.ERROR(c, "只有失败的任务才能删除")
|
||||
return
|
||||
}
|
||||
|
||||
tx := h.DB.Begin()
|
||||
if err := tx.Where("id = ? AND user_id = ?", jobId, user.Id).Delete(&model.JimengJob{}).Error; err != nil {
|
||||
logger.Errorf("delete jimeng job failed: %v", err)
|
||||
resp.ERROR(c, "删除任务失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 退回算力
|
||||
err = h.userService.IncreasePower(user.Id, job.Power, model.PowerLog{
|
||||
Type: types.PowerRefund,
|
||||
Model: "jimeng",
|
||||
Remark: fmt.Sprintf("删除任务,退回%d算力", job.Power),
|
||||
})
|
||||
if err != nil {
|
||||
resp.ERROR(c, "退回算力失败")
|
||||
tx.Rollback()
|
||||
return
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
resp.SUCCESS(c, gin.H{})
|
||||
}
|
||||
|
||||
// Retry 重试任务
|
||||
func (h *JimengHandler) Retry(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
|
||||
jobId := h.GetInt(c, "id", 0)
|
||||
if jobId == 0 {
|
||||
resp.ERROR(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查任务是否存在且属于当前用户
|
||||
job, err := h.jimengService.GetJob(uint(jobId))
|
||||
if err != nil {
|
||||
resp.ERROR(c, "任务不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if job.UserId != userId {
|
||||
resp.ERROR(c, "无权限操作")
|
||||
return
|
||||
}
|
||||
|
||||
// 只有失败的任务才能重试
|
||||
if job.Status != model.JMTaskStatusFailed {
|
||||
resp.ERROR(c, "只有失败的任务才能重试")
|
||||
return
|
||||
}
|
||||
|
||||
// 重置任务状态
|
||||
if err := h.jimengService.UpdateJobStatus(uint(jobId), model.JMTaskStatusInQueue, ""); err != nil {
|
||||
logger.Errorf("reset job status failed: %v", err)
|
||||
resp.ERROR(c, "重置任务状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 重新推送到队列
|
||||
if err := h.jimengService.PushTaskToQueue(uint(jobId)); err != nil {
|
||||
logger.Errorf("push retry task to queue failed: %v", err)
|
||||
resp.ERROR(c, "推送重试任务失败")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, gin.H{"message": "重试任务已提交"})
|
||||
}
|
||||
|
||||
// getPowerFromConfig 从配置中获取指定类型的算力消耗
|
||||
func (h *JimengHandler) getPowerFromConfig(taskType model.JMTaskType) int {
|
||||
config := h.jimengService.GetConfig()
|
||||
|
||||
switch taskType {
|
||||
case model.JMTaskTypeTextToImage:
|
||||
return config.Power.TextToImage
|
||||
case model.JMTaskTypeImageToImage:
|
||||
return config.Power.ImageToImage
|
||||
case model.JMTaskTypeImageEdit:
|
||||
return config.Power.ImageEdit
|
||||
case model.JMTaskTypeImageEffects:
|
||||
return config.Power.ImageEffects
|
||||
case model.JMTaskTypeTextToVideo:
|
||||
return config.Power.TextToVideo
|
||||
case model.JMTaskTypeImageToVideo:
|
||||
return config.Power.ImageToVideo
|
||||
default:
|
||||
return 10
|
||||
}
|
||||
}
|
||||
|
||||
// GetPowerConfig 获取即梦各任务类型算力消耗配置
|
||||
func (h *JimengHandler) GetPowerConfig(c *gin.Context) {
|
||||
config := h.jimengService.GetConfig()
|
||||
resp.SUCCESS(c, gin.H{
|
||||
"text_to_image": config.Power.TextToImage,
|
||||
"image_to_image": config.Power.ImageToImage,
|
||||
"image_edit": config.Power.ImageEdit,
|
||||
"image_effects": config.Power.ImageEffects,
|
||||
"text_to_video": config.Power.TextToVideo,
|
||||
"image_to_video": config.Power.ImageToVideo,
|
||||
})
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -95,7 +96,7 @@ func (h *MarkMapHandler) Generate(c *gin.Context) {
|
||||
|
||||
// 扣减算力
|
||||
if chatModel.Power > 0 {
|
||||
err = h.userService.DecreasePower(int(userId), chatModel.Power, model.PowerLog{
|
||||
err = h.userService.DecreasePower(userId, chatModel.Power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: chatModel.Value,
|
||||
Remark: fmt.Sprintf("AI绘制思维导图,模型名称:%s, ", chatModel.Value),
|
||||
|
||||
@@ -160,11 +160,11 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
|
||||
UserId: userId,
|
||||
ImgArr: data.ImgArr,
|
||||
Mode: h.App.SysConfig.MjMode,
|
||||
TranslateModelId: h.App.SysConfig.TranslateModelId,
|
||||
TranslateModelId: h.App.SysConfig.AssistantModelId,
|
||||
}
|
||||
job := model.MidJourneyJob{
|
||||
Type: data.TaskType,
|
||||
UserId: userId,
|
||||
UserId: uint(userId),
|
||||
TaskId: taskId,
|
||||
TaskInfo: utils.JsonEncode(task),
|
||||
Progress: 0,
|
||||
@@ -236,7 +236,7 @@ func (h *MidJourneyHandler) Upscale(c *gin.Context) {
|
||||
}
|
||||
job := model.MidJourneyJob{
|
||||
Type: types.TaskUpscale.String(),
|
||||
UserId: userId,
|
||||
UserId: uint(userId),
|
||||
TaskId: taskId,
|
||||
TaskInfo: utils.JsonEncode(task),
|
||||
Progress: 0,
|
||||
@@ -292,7 +292,7 @@ func (h *MidJourneyHandler) Variation(c *gin.Context) {
|
||||
job := model.MidJourneyJob{
|
||||
Type: types.TaskVariation.String(),
|
||||
ChannelId: data.ChannelId,
|
||||
UserId: userId,
|
||||
UserId: uint(userId),
|
||||
TaskId: taskId,
|
||||
TaskInfo: utils.JsonEncode(task),
|
||||
Progress: 0,
|
||||
@@ -422,7 +422,7 @@ func (h *MidJourneyHandler) Publish(c *gin.Context) {
|
||||
id := h.GetInt(c, "id", 0)
|
||||
userId := h.GetInt(c, "user_id", 0)
|
||||
action := h.GetBool(c, "action") // 发布动作,true => 发布,false => 取消分享
|
||||
err := h.DB.Model(&model.MidJourneyJob{Id: uint(id), UserId: userId}).UpdateColumn("publish", action).Error
|
||||
err := h.DB.Model(&model.MidJourneyJob{Id: uint(id), UserId: uint(userId)}).UpdateColumn("publish", action).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
|
||||
@@ -15,11 +15,12 @@ import (
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type NetHandler struct {
|
||||
@@ -46,7 +47,7 @@ func (h *NetHandler) Upload(c *gin.Context) {
|
||||
|
||||
userId := h.GetLoginUserId(c)
|
||||
res := h.DB.Create(&model.File{
|
||||
UserId: int(userId),
|
||||
UserId: uint(userId),
|
||||
Name: file.Name,
|
||||
ObjKey: file.ObjKey,
|
||||
URL: file.URL,
|
||||
@@ -143,7 +144,15 @@ func (h *NetHandler) Download(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
// 使用http.Get下载文件
|
||||
r, err := http.Get(fileUrl)
|
||||
req, err := http.NewRequest("GET", fileUrl, nil)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
// 模拟浏览器 UA
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36")
|
||||
client := &http.Client{}
|
||||
r, err := client.Do(req)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
@@ -156,6 +165,5 @@ func (h *NetHandler) Download(c *gin.Context) {
|
||||
}
|
||||
|
||||
c.Status(http.StatusOK)
|
||||
// 将下载的文件内容写入响应
|
||||
_, _ = io.Copy(c.Writer, r.Body)
|
||||
}
|
||||
|
||||
@@ -289,7 +289,7 @@ func (h *PaymentHandler) notify(orderNo string, tradeNo string) error {
|
||||
}
|
||||
|
||||
// 增加用户算力
|
||||
err = h.userService.IncreasePower(int(order.UserId), remark.Power, model.PowerLog{
|
||||
err = h.userService.IncreasePower(order.UserId, remark.Power, model.PowerLog{
|
||||
Type: types.PowerRecharge,
|
||||
Model: order.PayWay,
|
||||
Remark: fmt.Sprintf("充值算力,金额:%f,订单号:%s", order.Amount, order.OrderNo),
|
||||
|
||||
@@ -48,7 +48,7 @@ func (h *PromptHandler) Lyric(c *gin.Context) {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.LyricPromptTemplate, data.Prompt), h.App.SysConfig.TranslateModelId)
|
||||
content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.LyricPromptTemplate, data.Prompt), h.App.SysConfig.AssistantModelId)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
@@ -56,7 +56,7 @@ func (h *PromptHandler) Lyric(c *gin.Context) {
|
||||
|
||||
if h.App.SysConfig.PromptPower > 0 {
|
||||
userId := h.GetLoginUserId(c)
|
||||
err = h.userService.DecreasePower(int(userId), h.App.SysConfig.PromptPower, model.PowerLog{
|
||||
err = h.userService.DecreasePower(userId, h.App.SysConfig.PromptPower, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: h.getPromptModel(),
|
||||
Remark: "生成歌词",
|
||||
@@ -79,14 +79,14 @@ func (h *PromptHandler) Image(c *gin.Context) {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.ImagePromptOptimizeTemplate, data.Prompt), h.App.SysConfig.TranslateModelId)
|
||||
content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.ImagePromptOptimizeTemplate, data.Prompt), h.App.SysConfig.AssistantModelId)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
if h.App.SysConfig.PromptPower > 0 {
|
||||
userId := h.GetLoginUserId(c)
|
||||
err = h.userService.DecreasePower(int(userId), h.App.SysConfig.PromptPower, model.PowerLog{
|
||||
err = h.userService.DecreasePower(userId, h.App.SysConfig.PromptPower, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: h.getPromptModel(),
|
||||
Remark: "生成绘画提示词",
|
||||
@@ -108,7 +108,7 @@ func (h *PromptHandler) Video(c *gin.Context) {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.VideoPromptTemplate, data.Prompt), h.App.SysConfig.TranslateModelId)
|
||||
content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.VideoPromptTemplate, data.Prompt), h.App.SysConfig.AssistantModelId)
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
@@ -116,7 +116,7 @@ func (h *PromptHandler) Video(c *gin.Context) {
|
||||
|
||||
if h.App.SysConfig.PromptPower > 0 {
|
||||
userId := h.GetLoginUserId(c)
|
||||
err = h.userService.DecreasePower(int(userId), h.App.SysConfig.PromptPower, model.PowerLog{
|
||||
err = h.userService.DecreasePower(userId, h.App.SysConfig.PromptPower, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: h.getPromptModel(),
|
||||
Remark: "生成视频脚本",
|
||||
@@ -158,9 +158,9 @@ func (h *PromptHandler) MetaPrompt(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *PromptHandler) getPromptModel() string {
|
||||
if h.App.SysConfig.TranslateModelId > 0 {
|
||||
if h.App.SysConfig.AssistantModelId > 0 {
|
||||
var chatModel model.ChatModel
|
||||
h.DB.Where("id", h.App.SysConfig.TranslateModelId).First(&chatModel)
|
||||
h.DB.Where("id", h.App.SysConfig.AssistantModelId).First(&chatModel)
|
||||
return chatModel.Value
|
||||
}
|
||||
return "gpt-4o"
|
||||
|
||||
@@ -198,7 +198,7 @@ func (h *RealtimeHandler) VoiceChat(c *gin.Context) {
|
||||
h.DB.Model(&apiKey).UpdateColumn("last_used_at", time.Now().Unix())
|
||||
|
||||
// 扣减算力
|
||||
err = h.userService.DecreasePower(int(userId), h.App.SysConfig.AdvanceVoicePower, model.PowerLog{
|
||||
err = h.userService.DecreasePower(userId, h.App.SysConfig.AdvanceVoicePower, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: "advanced-voice",
|
||||
Remark: "实时语音通话",
|
||||
|
||||
@@ -61,7 +61,7 @@ func (h *RedeemHandler) Verify(c *gin.Context) {
|
||||
}
|
||||
|
||||
tx := h.DB.Begin()
|
||||
err := h.userService.IncreasePower(int(userId), item.Power, model.PowerLog{
|
||||
err := h.userService.IncreasePower(userId, item.Power, model.PowerLog{
|
||||
Type: types.PowerRedeem,
|
||||
Model: "兑换码",
|
||||
Remark: fmt.Sprintf("兑换码核销,算力:%d,兑换码:%s...", item.Power, item.Code[:10]),
|
||||
|
||||
@@ -131,11 +131,11 @@ func (h *SdJobHandler) Image(c *gin.Context) {
|
||||
HdSteps: data.HdSteps,
|
||||
},
|
||||
UserId: userId,
|
||||
TranslateModelId: h.App.SysConfig.TranslateModelId,
|
||||
TranslateModelId: h.App.SysConfig.AssistantModelId,
|
||||
}
|
||||
|
||||
job := model.SdJob{
|
||||
UserId: userId,
|
||||
UserId: uint(userId),
|
||||
Type: types.TaskImage.String(),
|
||||
TaskId: taskId,
|
||||
Params: utils.JsonEncode(task.Params),
|
||||
@@ -273,7 +273,7 @@ func (h *SdJobHandler) Publish(c *gin.Context) {
|
||||
userId := h.GetLoginUserId(c)
|
||||
action := h.GetBool(c, "action") // 发布动作,true => 发布,false => 取消分享
|
||||
|
||||
err := h.DB.Model(&model.SdJob{Id: uint(id), UserId: int(userId)}).UpdateColumn("publish", action).Error
|
||||
err := h.DB.Model(&model.SdJob{Id: uint(id), UserId: uint(userId)}).UpdateColumn("publish", action).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
|
||||
@@ -107,7 +107,7 @@ func (h *SunoHandler) Create(c *gin.Context) {
|
||||
|
||||
// 插入数据库
|
||||
job := model.SunoJob{
|
||||
UserId: task.UserId,
|
||||
UserId: uint(task.UserId),
|
||||
Prompt: data.Prompt,
|
||||
Instrumental: data.Instrumental,
|
||||
ModelName: data.Model,
|
||||
|
||||
@@ -137,13 +137,15 @@ func (h *UserHandler) Register(c *gin.Context) {
|
||||
|
||||
salt := utils.RandString(8)
|
||||
user := model.User{
|
||||
Username: data.Username,
|
||||
Password: utils.GenPassword(data.Password, salt),
|
||||
Avatar: "/images/avatar/user.png",
|
||||
Salt: salt,
|
||||
Status: true,
|
||||
ChatRoles: utils.JsonEncode([]string{"gpt"}), // 默认只订阅通用助手角色
|
||||
Power: h.App.SysConfig.InitPower,
|
||||
Username: data.Username,
|
||||
Password: utils.GenPassword(data.Password, salt),
|
||||
Avatar: "/images/avatar/user.png",
|
||||
Salt: salt,
|
||||
Status: true,
|
||||
ChatRoles: utils.JsonEncode([]string{"gpt"}), // 默认只订阅通用助手角色
|
||||
ChatConfig: "{}",
|
||||
ChatModels: "{}",
|
||||
Power: h.App.SysConfig.InitPower,
|
||||
}
|
||||
|
||||
// check if the username is existing
|
||||
@@ -170,10 +172,15 @@ func (h *UserHandler) Register(c *gin.Context) {
|
||||
if data.InviteCode != "" {
|
||||
user.Power += h.App.SysConfig.InvitePower
|
||||
}
|
||||
|
||||
if h.licenseService.GetLicense().Configs.DeCopy {
|
||||
user.Nickname = fmt.Sprintf("用户@%d", utils.RandomNumber(6))
|
||||
} else {
|
||||
user.Nickname = fmt.Sprintf("极客学长@%d", utils.RandomNumber(6))
|
||||
defaultNickname := h.App.SysConfig.DefaultNickname
|
||||
if defaultNickname == "" {
|
||||
defaultNickname = "极客学长"
|
||||
}
|
||||
user.Nickname = fmt.Sprintf("%s@%d", defaultNickname, utils.RandomNumber(6))
|
||||
}
|
||||
|
||||
tx := h.DB.Begin()
|
||||
@@ -187,7 +194,7 @@ func (h *UserHandler) Register(c *gin.Context) {
|
||||
// 增加邀请数量
|
||||
h.DB.Model(&model.InviteCode{}).Where("code = ?", data.InviteCode).UpdateColumn("reg_num", gorm.Expr("reg_num + ?", 1))
|
||||
if h.App.SysConfig.InvitePower > 0 {
|
||||
err := h.userService.IncreasePower(int(inviteCode.UserId), h.App.SysConfig.InvitePower, model.PowerLog{
|
||||
err := h.userService.IncreasePower(inviteCode.UserId, h.App.SysConfig.InvitePower, model.PowerLog{
|
||||
Type: types.PowerInvite,
|
||||
Model: "Invite",
|
||||
Remark: fmt.Sprintf("邀请用户注册奖励,金额:%d,邀请码:%s,新用户:%s", h.App.SysConfig.InvitePower, inviteCode.Code, user.Username),
|
||||
@@ -736,7 +743,7 @@ func (h *UserHandler) SignIn(c *gin.Context) {
|
||||
// 签到
|
||||
h.levelDB.Put(key, true)
|
||||
if h.App.SysConfig.DailyPower > 0 {
|
||||
h.userService.IncreasePower(int(userId), h.App.SysConfig.DailyPower, model.PowerLog{
|
||||
h.userService.IncreasePower(userId, h.App.SysConfig.DailyPower, model.PowerLog{
|
||||
Type: types.PowerSignIn,
|
||||
Model: "SignIn",
|
||||
Remark: fmt.Sprintf("每日签到奖励,金额:%d", h.App.SysConfig.DailyPower),
|
||||
|
||||
@@ -85,11 +85,11 @@ func (h *VideoHandler) LumaCreate(c *gin.Context) {
|
||||
Type: types.VideoLuma,
|
||||
Prompt: data.Prompt,
|
||||
Params: params,
|
||||
TranslateModelId: h.App.SysConfig.TranslateModelId,
|
||||
TranslateModelId: h.App.SysConfig.AssistantModelId,
|
||||
}
|
||||
// 插入数据库
|
||||
job := model.VideoJob{
|
||||
UserId: userId,
|
||||
UserId: uint(userId),
|
||||
Type: types.VideoLuma,
|
||||
Prompt: data.Prompt,
|
||||
Power: h.App.SysConfig.LumaPower,
|
||||
@@ -181,12 +181,12 @@ func (h *VideoHandler) KeLingCreate(c *gin.Context) {
|
||||
Type: types.VideoKeLing,
|
||||
Prompt: data.Prompt,
|
||||
Params: params,
|
||||
TranslateModelId: h.App.SysConfig.TranslateModelId,
|
||||
TranslateModelId: h.App.SysConfig.AssistantModelId,
|
||||
Channel: data.Channel,
|
||||
}
|
||||
// 插入数据库
|
||||
job := model.VideoJob{
|
||||
UserId: userId,
|
||||
UserId: uint(userId),
|
||||
Type: types.VideoKeLing,
|
||||
Prompt: data.Prompt,
|
||||
Power: power,
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
package handler
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"context"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"gorm.io/gorm"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Websocket 连接处理 handler
|
||||
|
||||
type WebsocketHandler struct {
|
||||
BaseHandler
|
||||
wsService *service.WebsocketService
|
||||
chatHandler *ChatHandler
|
||||
}
|
||||
|
||||
func NewWebsocketHandler(app *core.AppServer, s *service.WebsocketService, db *gorm.DB, chatHandler *ChatHandler) *WebsocketHandler {
|
||||
return &WebsocketHandler{
|
||||
BaseHandler: BaseHandler{App: app, DB: db},
|
||||
chatHandler: chatHandler,
|
||||
wsService: s,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *WebsocketHandler) Client(c *gin.Context) {
|
||||
clientProtocols := c.GetHeader("Sec-WebSocket-Protocol")
|
||||
ws, err := (&websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
Subprotocols: strings.Split(clientProtocols, ","),
|
||||
}).Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
clientId := c.Query("client_id")
|
||||
client := types.NewWsClient(ws, clientId)
|
||||
userId := h.GetLoginUserId(c)
|
||||
if userId == 0 {
|
||||
_ = client.Send([]byte("Invalid user_id"))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
var user model.User
|
||||
if err := h.DB.Where("id", userId).First(&user).Error; err != nil {
|
||||
_ = client.Send([]byte("Invalid user_id"))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
h.wsService.Clients.Put(clientId, client)
|
||||
logger.Infof("New websocket connected, IP: %s", c.RemoteIP())
|
||||
go func() {
|
||||
for {
|
||||
_, msg, err := client.Receive()
|
||||
if err != nil {
|
||||
logger.Debugf("close connection: %s", client.Conn.RemoteAddr())
|
||||
client.Close()
|
||||
h.wsService.Clients.Delete(clientId)
|
||||
break
|
||||
}
|
||||
|
||||
var message types.InputMessage
|
||||
err = utils.JsonDecode(string(msg), &message)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Debugf("Receive a message:%+v", message)
|
||||
if message.Type == types.MsgTypePing {
|
||||
utils.SendChannelMsg(client, types.ChPing, "pong")
|
||||
continue
|
||||
}
|
||||
|
||||
// 当前只处理聊天消息,其他消息全部丢弃
|
||||
var chatMessage types.ChatMessage
|
||||
err = utils.JsonDecode(utils.JsonEncode(message.Body), &chatMessage)
|
||||
if err != nil || message.Channel != types.ChChat {
|
||||
logger.Warnf("invalid message body:%+v", message.Body)
|
||||
continue
|
||||
}
|
||||
var chatRole model.ChatRole
|
||||
err = h.DB.First(&chatRole, chatMessage.RoleId).Error
|
||||
if err != nil || !chatRole.Enable {
|
||||
utils.SendAndFlush(client, "当前聊天角色不存在或者未启用,请更换角色之后再发起对话!!!")
|
||||
continue
|
||||
}
|
||||
// if the role bind a model_id, use role's bind model_id
|
||||
if chatRole.ModelId > 0 {
|
||||
chatMessage.RoleId = chatRole.ModelId
|
||||
}
|
||||
// get model info
|
||||
var chatModel model.ChatModel
|
||||
err = h.DB.Where("id", chatMessage.ModelId).First(&chatModel).Error
|
||||
if err != nil || chatModel.Enabled == false {
|
||||
utils.SendAndFlush(client, "当前AI模型暂未启用,请更换模型后再发起对话!!!")
|
||||
continue
|
||||
}
|
||||
|
||||
session := &types.ChatSession{
|
||||
ClientIP: c.ClientIP(),
|
||||
UserId: userId,
|
||||
}
|
||||
|
||||
// use old chat data override the chat model and role ID
|
||||
var chat model.ChatItem
|
||||
h.DB.Where("chat_id", chatMessage.ChatId).First(&chat)
|
||||
if chat.Id > 0 {
|
||||
chatModel.Id = chat.ModelId
|
||||
chatMessage.RoleId = int(chat.RoleId)
|
||||
}
|
||||
|
||||
session.ChatId = chatMessage.ChatId
|
||||
session.Tools = chatMessage.Tools
|
||||
session.Stream = chatMessage.Stream
|
||||
// 复制模型数据
|
||||
err = utils.CopyObject(chatModel, &session.Model)
|
||||
if err != nil {
|
||||
logger.Error(err, chatModel)
|
||||
}
|
||||
session.Model.Id = chatModel.Id
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
h.chatHandler.ReqCancelFunc.Put(clientId, cancel)
|
||||
err = h.chatHandler.sendMessage(ctx, session, chatRole, chatMessage.Content, client)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
utils.SendAndFlush(client, err.Error())
|
||||
} else {
|
||||
utils.SendMsg(client, types.ReplyMessage{Channel: types.ChChat, Type: types.MsgTypeEnd})
|
||||
logger.Infof("回答完毕: %v", message.Body)
|
||||
}
|
||||
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -8,11 +8,12 @@ package logger
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var logger *zap.Logger
|
||||
@@ -23,7 +24,7 @@ func GetLogger() *zap.SugaredLogger {
|
||||
return sugarLogger
|
||||
}
|
||||
|
||||
logLevel := zap.NewAtomicLevelAt(getLogLevel(os.Getenv("LOG_LEVEL")))
|
||||
logLevel := zap.NewAtomicLevelAt(getLogLevel(os.Getenv("GEEKAI_LOG_LEVEL")))
|
||||
encoder := getEncoder()
|
||||
writerSyncer := getLogWriter()
|
||||
fileCore := zapcore.NewCore(encoder, writerSyncer, logLevel)
|
||||
|
||||
23
api/main.go
23
api/main.go
@@ -17,6 +17,7 @@ import (
|
||||
logger2 "geekai/logger"
|
||||
"geekai/service"
|
||||
"geekai/service/dalle"
|
||||
"geekai/service/jimeng"
|
||||
"geekai/service/mj"
|
||||
"geekai/service/oss"
|
||||
"geekai/service/payment"
|
||||
@@ -140,6 +141,7 @@ func main() {
|
||||
fx.Provide(handler.NewProductHandler),
|
||||
fx.Provide(handler.NewConfigHandler),
|
||||
fx.Provide(handler.NewPowerLogHandler),
|
||||
fx.Provide(handler.NewJimengHandler),
|
||||
|
||||
fx.Provide(admin.NewConfigHandler),
|
||||
fx.Provide(admin.NewAdminHandler),
|
||||
@@ -153,6 +155,7 @@ func main() {
|
||||
fx.Provide(admin.NewOrderHandler),
|
||||
fx.Provide(admin.NewChatHandler),
|
||||
fx.Provide(admin.NewPowerLogHandler),
|
||||
fx.Provide(admin.NewAdminJimengHandler),
|
||||
|
||||
// 创建服务
|
||||
fx.Provide(sms.NewSendServiceManager),
|
||||
@@ -203,6 +206,12 @@ func main() {
|
||||
s.SyncTaskProgress()
|
||||
s.DownloadFiles()
|
||||
}),
|
||||
|
||||
// 即梦AI 服务
|
||||
fx.Provide(jimeng.NewService),
|
||||
fx.Invoke(func(service *jimeng.Service) {
|
||||
service.Start()
|
||||
}),
|
||||
fx.Provide(service.NewUserService),
|
||||
fx.Provide(payment.NewAlipayService),
|
||||
fx.Provide(payment.NewHuPiPay),
|
||||
@@ -243,6 +252,7 @@ func main() {
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.ChatHandler) {
|
||||
group := s.Engine.Group("/api/chat/")
|
||||
group.Any("message", h.Chat)
|
||||
group.GET("list", h.List)
|
||||
group.GET("detail", h.Detail)
|
||||
group.POST("update", h.Update)
|
||||
@@ -495,6 +505,14 @@ func main() {
|
||||
group.GET("remove", h.Remove)
|
||||
group.GET("publish", h.Publish)
|
||||
}),
|
||||
|
||||
// 即梦AI 路由
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.JimengHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.AdminJimengHandler) {
|
||||
h.RegisterRoutes()
|
||||
}),
|
||||
fx.Provide(admin.NewChatAppTypeHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.ChatAppTypeHandler) {
|
||||
group := s.Engine.Group("/api/admin/app/type")
|
||||
@@ -514,11 +532,6 @@ func main() {
|
||||
group := s.Engine.Group("/api/test")
|
||||
group.Any("sse", h.PostTest, h.SseTest)
|
||||
}),
|
||||
fx.Provide(service.NewWebsocketService),
|
||||
fx.Provide(handler.NewWebsocketHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.WebsocketHandler) {
|
||||
s.Engine.Any("/api/ws", h.Client)
|
||||
}),
|
||||
fx.Provide(handler.NewPromptHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.PromptHandler) {
|
||||
group := s.Engine.Group("/api/prompt")
|
||||
|
||||
@@ -24,15 +24,15 @@ func NewService() (*Service, error) {
|
||||
// 启动浏览器
|
||||
path, _ := launcher.LookPath()
|
||||
u := launcher.New().Bin(path).
|
||||
Headless(true). // 无头模式
|
||||
Set("disable-web-security", ""). // 禁用网络安全限制
|
||||
Set("disable-gpu", ""). // 禁用 GPU 加速
|
||||
Set("no-sandbox", ""). // 禁用沙箱模式
|
||||
Set("disable-setuid-sandbox", "").// 禁用 setuid 沙箱
|
||||
Headless(true). // 无头模式
|
||||
Set("disable-web-security", ""). // 禁用网络安全限制
|
||||
Set("disable-gpu", ""). // 禁用 GPU 加速
|
||||
Set("no-sandbox", ""). // 禁用沙箱模式
|
||||
Set("disable-setuid-sandbox", ""). // 禁用 setuid 沙箱
|
||||
MustLaunch()
|
||||
|
||||
browser := rod.New().ControlURL(u).MustConnect()
|
||||
|
||||
|
||||
return &Service{
|
||||
browser: browser,
|
||||
}, nil
|
||||
@@ -50,7 +50,7 @@ func (s *Service) WebSearch(keyword string, maxPages int) ([]SearchResult, error
|
||||
if keyword == "" {
|
||||
return nil, errors.New("搜索关键词不能为空")
|
||||
}
|
||||
|
||||
|
||||
if maxPages <= 0 {
|
||||
maxPages = 1
|
||||
}
|
||||
@@ -59,18 +59,18 @@ func (s *Service) WebSearch(keyword string, maxPages int) ([]SearchResult, error
|
||||
}
|
||||
|
||||
results := make([]SearchResult, 0)
|
||||
|
||||
|
||||
// 使用百度搜索
|
||||
searchURL := fmt.Sprintf("https://www.baidu.com/s?wd=%s", url.QueryEscape(keyword))
|
||||
|
||||
|
||||
// 设置页面超时
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
|
||||
// 创建页面
|
||||
page := s.browser.MustPage()
|
||||
defer page.MustClose()
|
||||
|
||||
|
||||
// 设置视口大小
|
||||
err := page.SetViewport(&proto.EmulationSetDeviceMetricsOverride{
|
||||
Width: 1280,
|
||||
@@ -79,19 +79,19 @@ func (s *Service) WebSearch(keyword string, maxPages int) ([]SearchResult, error
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("设置视口失败: %v", err)
|
||||
}
|
||||
|
||||
|
||||
// 导航到搜索页面
|
||||
err = page.Context(ctx).Navigate(searchURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("导航到搜索页面失败: %v", err)
|
||||
}
|
||||
|
||||
|
||||
// 等待搜索结果加载完成
|
||||
err = page.WaitLoad()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("等待页面加载完成失败: %v", err)
|
||||
}
|
||||
|
||||
|
||||
// 分析当前页面的搜索结果
|
||||
for i := 0; i < maxPages; i++ {
|
||||
if i > 0 {
|
||||
@@ -100,52 +100,52 @@ func (s *Service) WebSearch(keyword string, maxPages int) ([]SearchResult, error
|
||||
if err != nil || nextPage == nil {
|
||||
break // 没有下一页
|
||||
}
|
||||
|
||||
|
||||
err = nextPage.Click(proto.InputMouseButtonLeft, 1)
|
||||
if err != nil {
|
||||
break // 点击下一页失败
|
||||
}
|
||||
|
||||
|
||||
// 等待新页面加载
|
||||
err = page.WaitLoad()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 提取搜索结果
|
||||
resultElements, err := page.Elements(".result, .c-container")
|
||||
if err != nil || resultElements == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
for _, result := range resultElements {
|
||||
// 获取标题
|
||||
titleElement, err := result.Element("h3, .t")
|
||||
if err != nil || titleElement == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
title, err := titleElement.Text()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// 获取 URL
|
||||
linkElement, err := titleElement.Element("a")
|
||||
if err != nil || linkElement == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
href, err := linkElement.Attribute("href")
|
||||
if err != nil || href == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// 获取内容摘要 - 尝试多个可能的选择器
|
||||
var contentElement *rod.Element
|
||||
var content string
|
||||
|
||||
|
||||
// 尝试多个可能的选择器来适应不同版本的百度搜索结果
|
||||
selectors := []string{".content-right_8Zs40", ".c-abstract", ".content_LJ0WN", ".content"}
|
||||
for _, selector := range selectors {
|
||||
@@ -157,7 +157,7 @@ func (s *Service) WebSearch(keyword string, maxPages int) ([]SearchResult, error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 如果所有选择器都失败,尝试直接从结果块中提取文本
|
||||
if content == "" {
|
||||
// 获取结果元素的所有文本
|
||||
@@ -173,21 +173,21 @@ func (s *Service) WebSearch(keyword string, maxPages int) ([]SearchResult, error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 添加到结果集
|
||||
results = append(results, SearchResult{
|
||||
Title: title,
|
||||
URL: *href,
|
||||
Content: content,
|
||||
})
|
||||
|
||||
|
||||
// 限制结果数量,每页最多 10 条
|
||||
if len(results) >= 10*maxPages {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 获取真实 URL(百度搜索结果中的 URL 是短链接,需要跳转获取真实 URL)
|
||||
for i, result := range results {
|
||||
realURL, err := s.getRedirectURL(result.URL)
|
||||
@@ -195,7 +195,7 @@ func (s *Service) WebSearch(keyword string, maxPages int) ([]SearchResult, error
|
||||
results[i].URL = realURL
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
@@ -209,22 +209,22 @@ func (s *Service) getRedirectURL(shortURL string) (string, error) {
|
||||
defer func() {
|
||||
_ = page.Close()
|
||||
}()
|
||||
|
||||
|
||||
// 导航到短链接
|
||||
err = page.Navigate(shortURL)
|
||||
if err != nil {
|
||||
return shortURL, err // 返回原始URL
|
||||
}
|
||||
|
||||
|
||||
// 等待重定向完成
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
|
||||
// 获取当前 URL
|
||||
info, err := page.Info()
|
||||
if err != nil {
|
||||
return shortURL, err // 返回原始URL
|
||||
}
|
||||
|
||||
|
||||
return info.URL, nil
|
||||
}
|
||||
|
||||
@@ -247,21 +247,21 @@ func SearchWeb(keyword string, maxPages int) (string, error) {
|
||||
log.Errorf("爬虫服务崩溃: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
service, err := NewService()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建爬虫服务失败: %v", err)
|
||||
}
|
||||
defer service.Close()
|
||||
|
||||
|
||||
// 设置超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
|
||||
// 使用goroutine和通道来处理超时
|
||||
resultChan := make(chan []SearchResult, 1)
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
|
||||
go func() {
|
||||
results, err := service.WebSearch(keyword, maxPages)
|
||||
if err != nil {
|
||||
@@ -270,7 +270,7 @@ func SearchWeb(keyword string, maxPages int) (string, error) {
|
||||
}
|
||||
resultChan <- results
|
||||
}()
|
||||
|
||||
|
||||
// 等待结果或超时
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -281,32 +281,32 @@ func SearchWeb(keyword string, maxPages int) (string, error) {
|
||||
if len(results) == 0 {
|
||||
return "未找到关于 \"" + keyword + "\" 的相关搜索结果", nil
|
||||
}
|
||||
|
||||
|
||||
// 格式化结果
|
||||
var builder strings.Builder
|
||||
builder.WriteString(fmt.Sprintf("为您找到关于 \"%s\" 的 %d 条搜索结果:\n\n", keyword, len(results)))
|
||||
|
||||
|
||||
for i, result := range results {
|
||||
// // 尝试打开链接获取实际内容
|
||||
// page := service.browser.MustPage()
|
||||
// defer page.MustClose()
|
||||
|
||||
|
||||
// // 设置页面超时
|
||||
// pageCtx, pageCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
// defer pageCancel()
|
||||
|
||||
|
||||
// // 导航到目标页面
|
||||
// err := page.Context(pageCtx).Navigate(result.URL)
|
||||
// if err == nil {
|
||||
// // 等待页面加载
|
||||
// _ = page.WaitLoad()
|
||||
|
||||
|
||||
// // 获取页面标题
|
||||
// title, err := page.Eval("() => document.title")
|
||||
// if err == nil && title.Value.String() != "" {
|
||||
// result.Title = title.Value.String()
|
||||
// }
|
||||
|
||||
|
||||
// // 获取页面主要内容
|
||||
// if content, err := page.Element("body"); err == nil {
|
||||
// if text, err := content.Text(); err == nil {
|
||||
@@ -315,11 +315,11 @@ func SearchWeb(keyword string, maxPages int) (string, error) {
|
||||
// if len(text) > 200 {
|
||||
// text = text[:200] + "..."
|
||||
// }
|
||||
// result.Content = text
|
||||
// result.Prompt = text
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
builder.WriteString(fmt.Sprintf("%d. **%s**\n", i+1, result.Title))
|
||||
builder.WriteString(fmt.Sprintf(" 链接: %s\n", result.URL))
|
||||
if result.Content != "" {
|
||||
@@ -327,7 +327,7 @@ func SearchWeb(keyword string, maxPages int) (string, error) {
|
||||
}
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"geekai/store"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
@@ -50,7 +49,9 @@ func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Clien
|
||||
// PushTask push a new mj task in to task queue
|
||||
func (s *Service) PushTask(task types.DallTask) {
|
||||
logger.Infof("add a new DALL-E task to the task list: %+v", task)
|
||||
s.taskQueue.RPush(task)
|
||||
if err := s.taskQueue.RPush(task); err != nil {
|
||||
logger.Errorf("push dall-e task to queue failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Run() {
|
||||
@@ -132,7 +133,11 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
|
||||
}
|
||||
|
||||
var chatModel model.ChatModel
|
||||
s.db.Where("id = ?", task.ModelId).First(&chatModel)
|
||||
if task.ModelId > 0 {
|
||||
s.db.Where("id", task.ModelId).First(&chatModel)
|
||||
} else {
|
||||
s.db.Where("value", task.ModelName).First(&chatModel)
|
||||
}
|
||||
|
||||
// get image generation API KEY
|
||||
var apiKey model.ApiKey
|
||||
@@ -178,9 +183,6 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
|
||||
return "", fmt.Errorf("error with send request, status: %s, %+v", r.Status, errRes.Error)
|
||||
}
|
||||
|
||||
all, _ := io.ReadAll(r.Body)
|
||||
logger.Debugf("response: %+v", string(all))
|
||||
|
||||
// update the api key last use time
|
||||
s.db.Model(&apiKey).UpdateColumn("last_used_at", time.Now().Unix())
|
||||
var imgURL string
|
||||
@@ -242,7 +244,7 @@ func (s *Service) CheckTaskStatus() {
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
err = s.userService.IncreasePower(int(job.UserId), job.Power, model.PowerLog{
|
||||
err = s.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{
|
||||
Type: types.PowerRefund,
|
||||
Model: task.ModelName,
|
||||
Remark: fmt.Sprintf("任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg),
|
||||
@@ -291,7 +293,7 @@ func (s *Service) DownloadImages() {
|
||||
|
||||
func (s *Service) downloadImage(jobId uint, orgURL string) (string, error) {
|
||||
// sava image
|
||||
imgURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(orgURL, false)
|
||||
imgURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(orgURL, ".png", false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
139
api/service/jimeng/client.go
Normal file
139
api/service/jimeng/client.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package jimeng
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/volcengine/volc-sdk-golang/base"
|
||||
"github.com/volcengine/volc-sdk-golang/service/visual"
|
||||
)
|
||||
|
||||
// Client 即梦API客户端
|
||||
type Client struct {
|
||||
visual *visual.Visual
|
||||
}
|
||||
|
||||
// NewClient 创建即梦API客户端
|
||||
func NewClient(accessKey, secretKey string) *Client {
|
||||
// 使用官方SDK的visual实例
|
||||
visualInstance := visual.NewInstance()
|
||||
visualInstance.Client.SetAccessKey(accessKey)
|
||||
visualInstance.Client.SetSecretKey(secretKey)
|
||||
|
||||
// 添加即梦AI专有的API配置
|
||||
jimengApis := map[string]*base.ApiInfo{
|
||||
"CVSync2AsyncSubmitTask": {
|
||||
Method: http.MethodPost,
|
||||
Path: "/",
|
||||
Query: url.Values{
|
||||
"Action": []string{"CVSync2AsyncSubmitTask"},
|
||||
"Version": []string{"2022-08-31"},
|
||||
},
|
||||
},
|
||||
"CVSync2AsyncGetResult": {
|
||||
Method: http.MethodPost,
|
||||
Path: "/",
|
||||
Query: url.Values{
|
||||
"Action": []string{"CVSync2AsyncGetResult"},
|
||||
"Version": []string{"2022-08-31"},
|
||||
},
|
||||
},
|
||||
"CVProcess": {
|
||||
Method: http.MethodPost,
|
||||
Path: "/",
|
||||
Query: url.Values{
|
||||
"Action": []string{"CVProcess"},
|
||||
"Version": []string{"2022-08-31"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// 将即梦API添加到现有的ApiInfoList中
|
||||
for name, info := range jimengApis {
|
||||
visualInstance.Client.ApiInfoList[name] = info
|
||||
}
|
||||
|
||||
return &Client{
|
||||
visual: visualInstance,
|
||||
}
|
||||
}
|
||||
|
||||
// SubmitTask 提交异步任务
|
||||
func (c *Client) SubmitTask(req *SubmitTaskRequest) (*SubmitTaskResponse, error) {
|
||||
// 直接将请求转为map[string]interface{}
|
||||
reqBodyBytes, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal request failed: %w", err)
|
||||
}
|
||||
|
||||
// 直接使用序列化后的字节
|
||||
jsonBody := reqBodyBytes
|
||||
|
||||
// 调用SDK的JSON方法
|
||||
respBody, statusCode, err := c.visual.Client.Json("CVSync2AsyncSubmitTask", nil, string(jsonBody))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("submit task failed (status: %d): %w", statusCode, err)
|
||||
}
|
||||
|
||||
logger.Infof("Jimeng SubmitTask Response: %s", string(respBody))
|
||||
|
||||
// 解析响应
|
||||
var result SubmitTaskResponse
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal response failed: %w", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// QueryTask 查询任务结果
|
||||
func (c *Client) QueryTask(req *QueryTaskRequest) (*QueryTaskResponse, error) {
|
||||
// 序列化请求
|
||||
jsonBody, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal request failed: %w", err)
|
||||
}
|
||||
|
||||
// 调用SDK的JSON方法
|
||||
respBody, statusCode, err := c.visual.Client.Json("CVSync2AsyncGetResult", nil, string(jsonBody))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query task failed (status: %d): %w", statusCode, err)
|
||||
}
|
||||
|
||||
logger.Infof("Jimeng QueryTask Response: %s", string(respBody))
|
||||
|
||||
// 解析响应
|
||||
var result QueryTaskResponse
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal response failed: %w", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// SubmitSyncTask 提交同步任务(仅用于文生图)
|
||||
func (c *Client) SubmitSyncTask(req *SubmitTaskRequest) (*QueryTaskResponse, error) {
|
||||
// 序列化请求
|
||||
jsonBody, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal request failed: %w", err)
|
||||
}
|
||||
|
||||
// 调用SDK的JSON方法
|
||||
respBody, statusCode, err := c.visual.Client.Json("CVProcess", nil, string(jsonBody))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("submit sync task failed (status: %d): %w", statusCode, err)
|
||||
}
|
||||
|
||||
logger.Infof("Jimeng SubmitSyncTask Response: %s", string(respBody))
|
||||
|
||||
// 解析响应,同步任务直接返回结果
|
||||
var result QueryTaskResponse
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal response failed: %w", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
600
api/service/jimeng/service.go
Normal file
600
api/service/jimeng/service.go
Normal file
@@ -0,0 +1,600 @@
|
||||
package jimeng
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
logger2 "geekai/logger"
|
||||
"geekai/service/oss"
|
||||
"geekai/store"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
|
||||
"geekai/core/types"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
)
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
// Service 即梦服务(合并了消费者功能)
|
||||
type Service struct {
|
||||
db *gorm.DB
|
||||
redis *redis.Client
|
||||
taskQueue *store.RedisQueue
|
||||
client *Client
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
running bool
|
||||
uploader *oss.UploaderManager
|
||||
}
|
||||
|
||||
// NewService 创建即梦服务
|
||||
func NewService(db *gorm.DB, redisCli *redis.Client, uploader *oss.UploaderManager) *Service {
|
||||
taskQueue := store.NewRedisQueue("JimengTaskQueue", redisCli)
|
||||
// 从数据库加载配置
|
||||
var config model.Config
|
||||
db.Where("name = ?", "Jimeng").First(&config)
|
||||
var jimengConfig types.JimengConfig
|
||||
if config.Id > 0 {
|
||||
_ = utils.JsonDecode(config.Value, &jimengConfig)
|
||||
}
|
||||
client := NewClient(jimengConfig.AccessKey, jimengConfig.SecretKey)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &Service{
|
||||
db: db,
|
||||
redis: redisCli,
|
||||
taskQueue: taskQueue,
|
||||
client: client,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
running: false,
|
||||
uploader: uploader,
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动服务(包含消费者)
|
||||
func (s *Service) Start() {
|
||||
if s.running {
|
||||
return
|
||||
}
|
||||
logger.Info("Starting Jimeng service and task consumer...")
|
||||
s.running = true
|
||||
go s.consumeTasks()
|
||||
go s.pollTaskStatus()
|
||||
}
|
||||
|
||||
// Stop 停止服务
|
||||
func (s *Service) Stop() {
|
||||
if !s.running {
|
||||
return
|
||||
}
|
||||
logger.Info("Stopping Jimeng service and task consumer...")
|
||||
s.running = false
|
||||
s.cancel()
|
||||
}
|
||||
|
||||
// consumeTasks 消费任务
|
||||
func (s *Service) consumeTasks() {
|
||||
for {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
logger.Info("Jimeng task consumer stopped")
|
||||
return
|
||||
default:
|
||||
s.processNextTask()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processNextTask 处理下一个任务
|
||||
func (s *Service) processNextTask() {
|
||||
var jobId uint
|
||||
if err := s.taskQueue.LPop(&jobId); err != nil {
|
||||
// 队列为空,等待1秒后重试
|
||||
time.Sleep(time.Second)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof("Processing Jimeng task: job_id=%d", jobId)
|
||||
|
||||
if err := s.ProcessTask(jobId); err != nil {
|
||||
logger.Errorf("process jimeng task failed: job_id=%d, error=%v", jobId, err)
|
||||
s.UpdateJobStatus(jobId, model.JMTaskStatusFailed, err.Error())
|
||||
} else {
|
||||
logger.Infof("Jimeng task processed successfully: job_id=%d", jobId)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateTask 创建任务
|
||||
func (s *Service) CreateTask(userId uint, req *CreateTaskRequest) (*model.JimengJob, error) {
|
||||
// 生成任务ID
|
||||
taskId := utils.RandString(20)
|
||||
|
||||
// 序列化任务参数
|
||||
paramsJson, err := json.Marshal(req.Params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal task params failed: %w", err)
|
||||
}
|
||||
|
||||
// 创建任务记录
|
||||
job := &model.JimengJob{
|
||||
UserId: userId,
|
||||
TaskId: taskId,
|
||||
Type: req.Type,
|
||||
ReqKey: req.ReqKey,
|
||||
Prompt: req.Prompt,
|
||||
TaskParams: string(paramsJson),
|
||||
Status: model.JMTaskStatusInQueue,
|
||||
Power: req.Power,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// 保存到数据库
|
||||
if err := s.db.Create(job).Error; err != nil {
|
||||
return nil, fmt.Errorf("create jimeng job failed: %w", err)
|
||||
}
|
||||
|
||||
// 推送到任务队列
|
||||
if err := s.taskQueue.RPush(job.Id); err != nil {
|
||||
return nil, fmt.Errorf("push jimeng task to queue failed: %w", err)
|
||||
}
|
||||
|
||||
return job, nil
|
||||
}
|
||||
|
||||
// ProcessTask 处理任务
|
||||
func (s *Service) ProcessTask(jobId uint) error {
|
||||
// 获取任务记录
|
||||
var job model.JimengJob
|
||||
if err := s.db.First(&job, jobId).Error; err != nil {
|
||||
return fmt.Errorf("get jimeng job failed: %w", err)
|
||||
}
|
||||
|
||||
// 更新任务状态为处理中
|
||||
if err := s.UpdateJobStatus(job.Id, model.JMTaskStatusGenerating, ""); err != nil {
|
||||
return fmt.Errorf("update job status failed: %w", err)
|
||||
}
|
||||
|
||||
// 构建请求并提交任务
|
||||
req, err := s.buildTaskRequest(&job)
|
||||
if err != nil {
|
||||
return s.handleTaskError(job.Id, fmt.Sprintf("build task request failed: %v", err))
|
||||
}
|
||||
|
||||
logger.Infof("提交即梦任务: %+v", req)
|
||||
|
||||
// 提交异步任务
|
||||
resp, err := s.client.SubmitTask(req)
|
||||
if err != nil {
|
||||
return s.handleTaskError(job.Id, fmt.Sprintf("submit task failed: %v", err))
|
||||
}
|
||||
|
||||
if resp.Code != 10000 {
|
||||
return s.handleTaskError(job.Id, fmt.Sprintf("submit task failed: %s", resp.Message))
|
||||
}
|
||||
|
||||
// 更新任务ID和原始数据
|
||||
rawData, _ := json.Marshal(resp)
|
||||
if err := s.db.Model(&model.JimengJob{}).Where("id = ?", job.Id).Updates(map[string]any{
|
||||
"task_id": resp.Data.TaskId,
|
||||
"raw_data": string(rawData),
|
||||
"updated_at": time.Now(),
|
||||
}).Error; err != nil {
|
||||
logger.Errorf("update jimeng job task_id failed: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildTaskRequest 构建任务请求(统一的参数解析)
|
||||
func (s *Service) buildTaskRequest(job *model.JimengJob) (*SubmitTaskRequest, error) {
|
||||
// 解析任务参数
|
||||
var params map[string]any
|
||||
if err := json.Unmarshal([]byte(job.TaskParams), ¶ms); err != nil {
|
||||
return nil, fmt.Errorf("parse task params failed: %w", err)
|
||||
}
|
||||
|
||||
// 构建基础请求
|
||||
req := &SubmitTaskRequest{
|
||||
ReqKey: job.ReqKey,
|
||||
Prompt: job.Prompt,
|
||||
}
|
||||
|
||||
// 根据任务类型设置特定参数
|
||||
switch job.Type {
|
||||
case model.JMTaskTypeTextToImage:
|
||||
s.setTextToImageParams(req, params)
|
||||
case model.JMTaskTypeImageToImage:
|
||||
s.setImageToImageParams(req, params)
|
||||
case model.JMTaskTypeImageEdit:
|
||||
s.setImageEditParams(req, params)
|
||||
case model.JMTaskTypeImageEffects:
|
||||
s.setImageEffectsParams(req, params)
|
||||
case model.JMTaskTypeTextToVideo:
|
||||
s.setTextToVideoParams(req, params)
|
||||
case model.JMTaskTypeImageToVideo:
|
||||
s.setImageToVideoParams(req, params)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported task type: %s", job.Type)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// setTextToImageParams 设置文生图参数
|
||||
func (s *Service) setTextToImageParams(req *SubmitTaskRequest, params map[string]any) {
|
||||
if seed, ok := params["seed"]; ok {
|
||||
if seedVal, err := strconv.ParseInt(fmt.Sprintf("%.0f", seed), 10, 64); err == nil {
|
||||
req.Seed = seedVal
|
||||
}
|
||||
}
|
||||
if scale, ok := params["scale"]; ok {
|
||||
if scaleVal, ok := scale.(float64); ok {
|
||||
req.Scale = scaleVal
|
||||
}
|
||||
}
|
||||
if width, ok := params["width"]; ok {
|
||||
if widthVal, ok := width.(float64); ok {
|
||||
req.Width = int(widthVal)
|
||||
}
|
||||
}
|
||||
if height, ok := params["height"]; ok {
|
||||
if heightVal, ok := height.(float64); ok {
|
||||
req.Height = int(heightVal)
|
||||
}
|
||||
}
|
||||
if usePreLlm, ok := params["use_pre_llm"]; ok {
|
||||
if usePreLlmVal, ok := usePreLlm.(bool); ok {
|
||||
req.UsePreLLM = usePreLlmVal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// setImageToImageParams 设置图生图参数
|
||||
func (s *Service) setImageToImageParams(req *SubmitTaskRequest, params map[string]any) {
|
||||
if imageInput, ok := params["image_input"].(string); ok {
|
||||
req.ImageInput = imageInput
|
||||
}
|
||||
if gpen, ok := params["gpen"]; ok {
|
||||
if gpenVal, ok := gpen.(float64); ok {
|
||||
req.Gpen = gpenVal
|
||||
}
|
||||
}
|
||||
if skin, ok := params["skin"]; ok {
|
||||
if skinVal, ok := skin.(float64); ok {
|
||||
req.Skin = skinVal
|
||||
}
|
||||
}
|
||||
if skinUnifi, ok := params["skin_unifi"]; ok {
|
||||
if skinUnifiVal, ok := skinUnifi.(float64); ok {
|
||||
req.SkinUnifi = skinUnifiVal
|
||||
}
|
||||
}
|
||||
if genMode, ok := params["gen_mode"].(string); ok {
|
||||
req.GenMode = genMode
|
||||
}
|
||||
s.setCommonParams(req, params) // 复用通用参数
|
||||
}
|
||||
|
||||
// setImageEditParams 设置图像编辑参数
|
||||
func (s *Service) setImageEditParams(req *SubmitTaskRequest, params map[string]any) {
|
||||
if imageUrls, ok := params["image_urls"].([]any); ok {
|
||||
for _, url := range imageUrls {
|
||||
if urlStr, ok := url.(string); ok {
|
||||
req.ImageUrls = append(req.ImageUrls, urlStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
if binaryData, ok := params["binary_data_base64"].([]any); ok {
|
||||
for _, data := range binaryData {
|
||||
if dataStr, ok := data.(string); ok {
|
||||
req.BinaryDataBase64 = append(req.BinaryDataBase64, dataStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
if scale, ok := params["scale"]; ok {
|
||||
if scaleVal, ok := scale.(float64); ok {
|
||||
req.Scale = scaleVal
|
||||
}
|
||||
}
|
||||
s.setCommonParams(req, params)
|
||||
}
|
||||
|
||||
// setImageEffectsParams 设置图像特效参数
|
||||
func (s *Service) setImageEffectsParams(req *SubmitTaskRequest, params map[string]any) {
|
||||
if imageInput1, ok := params["image_input1"].(string); ok {
|
||||
req.ImageInput1 = imageInput1
|
||||
}
|
||||
if templateId, ok := params["template_id"].(string); ok {
|
||||
req.TemplateId = templateId
|
||||
}
|
||||
if width, ok := params["width"]; ok {
|
||||
if widthVal, ok := width.(float64); ok {
|
||||
req.Width = int(widthVal)
|
||||
}
|
||||
}
|
||||
if height, ok := params["height"]; ok {
|
||||
if heightVal, ok := height.(float64); ok {
|
||||
req.Height = int(heightVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// setTextToVideoParams 设置文生视频参数
|
||||
func (s *Service) setTextToVideoParams(req *SubmitTaskRequest, params map[string]any) {
|
||||
if aspectRatio, ok := params["aspect_ratio"].(string); ok {
|
||||
req.AspectRatio = aspectRatio
|
||||
}
|
||||
s.setCommonParams(req, params)
|
||||
}
|
||||
|
||||
// setImageToVideoParams 设置图生视频参数
|
||||
func (s *Service) setImageToVideoParams(req *SubmitTaskRequest, params map[string]any) {
|
||||
s.setImageEditParams(req, params) // 复用图像编辑的参数设置
|
||||
if aspectRatio, ok := params["aspect_ratio"].(string); ok {
|
||||
req.AspectRatio = aspectRatio
|
||||
}
|
||||
}
|
||||
|
||||
// setCommonParams 设置通用参数(seed, width, height等)
|
||||
func (s *Service) setCommonParams(req *SubmitTaskRequest, params map[string]any) {
|
||||
if seed, ok := params["seed"]; ok {
|
||||
if seedVal, err := strconv.ParseInt(fmt.Sprintf("%.0f", seed), 10, 64); err == nil {
|
||||
req.Seed = seedVal
|
||||
}
|
||||
}
|
||||
if width, ok := params["width"]; ok {
|
||||
if widthVal, ok := width.(float64); ok {
|
||||
req.Width = int(widthVal)
|
||||
}
|
||||
}
|
||||
if height, ok := params["height"]; ok {
|
||||
if heightVal, ok := height.(float64); ok {
|
||||
req.Height = int(heightVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// pollTaskStatus 轮询任务状态
|
||||
func (s *Service) pollTaskStatus() {
|
||||
|
||||
for {
|
||||
var jobs []model.JimengJob
|
||||
s.db.Where("status IN (?)", []model.JMTaskStatus{model.JMTaskStatusGenerating, model.JMTaskStatusInQueue}).Find(&jobs)
|
||||
if len(jobs) == 0 {
|
||||
logger.Debugf("no jimeng task to poll, sleep 10s")
|
||||
time.Sleep(10 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, job := range jobs {
|
||||
// 任务超时处理
|
||||
if job.UpdatedAt.Before(time.Now().Add(-5 * time.Minute)) {
|
||||
s.handleTaskError(job.Id, "task timeout")
|
||||
continue
|
||||
}
|
||||
|
||||
// 查询任务状态
|
||||
resp, err := s.client.QueryTask(&QueryTaskRequest{
|
||||
ReqKey: job.ReqKey,
|
||||
TaskId: job.TaskId,
|
||||
ReqJson: `{"return_url":true}`,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("query jimeng task status failed: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 更新原始数据
|
||||
rawData, _ := json.Marshal(resp)
|
||||
s.db.Model(&model.JimengJob{}).Where("id = ?", job.Id).Update("raw_data", string(rawData))
|
||||
|
||||
if resp.Code != 10000 {
|
||||
s.handleTaskError(job.Id, fmt.Sprintf("query task failed: %s", resp.Message))
|
||||
continue
|
||||
}
|
||||
|
||||
switch resp.Data.Status {
|
||||
case model.JMTaskStatusDone:
|
||||
// 判断任务是否成功
|
||||
if resp.Message != "Success" {
|
||||
s.handleTaskError(job.Id, fmt.Sprintf("task failed: %s", resp.Data.AlgorithmBaseResp.StatusMessage))
|
||||
continue
|
||||
}
|
||||
|
||||
// 任务完成,更新结果
|
||||
updates := map[string]any{
|
||||
"status": model.JMTaskStatusSuccess,
|
||||
"updated_at": time.Now(),
|
||||
}
|
||||
|
||||
// 设置结果URL
|
||||
if len(resp.Data.ImageUrls) > 0 {
|
||||
imgUrl, err := s.uploader.GetUploadHandler().PutUrlFile(resp.Data.ImageUrls[0], ".png", false)
|
||||
if err != nil {
|
||||
logger.Errorf("upload image failed: %v", err)
|
||||
imgUrl = resp.Data.ImageUrls[0]
|
||||
}
|
||||
updates["img_url"] = imgUrl
|
||||
}
|
||||
if resp.Data.VideoUrl != "" {
|
||||
videoUrl, err := s.uploader.GetUploadHandler().PutUrlFile(resp.Data.VideoUrl, ".mp4", false)
|
||||
if err != nil {
|
||||
logger.Errorf("upload video failed: %v", err)
|
||||
videoUrl = resp.Data.VideoUrl
|
||||
}
|
||||
updates["video_url"] = videoUrl
|
||||
}
|
||||
|
||||
s.db.Model(&model.JimengJob{}).Where("id = ?", job.Id).Updates(updates)
|
||||
case model.JMTaskStatusInQueue, model.JMTaskStatusGenerating:
|
||||
// 任务处理中
|
||||
s.UpdateJobStatus(job.Id, model.JMTaskStatusGenerating, "")
|
||||
|
||||
case model.JMTaskStatusNotFound:
|
||||
// 任务未找到
|
||||
s.handleTaskError(job.Id, "task not found")
|
||||
|
||||
case model.JMTaskStatusExpired:
|
||||
// 任务过期
|
||||
s.handleTaskError(job.Id, "task expired")
|
||||
|
||||
default:
|
||||
logger.Warnf("unknown task status: %s", resp.Data.Status)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// UpdateJobStatus 更新任务状态
|
||||
func (s *Service) UpdateJobStatus(jobId uint, status model.JMTaskStatus, errMsg string) error {
|
||||
updates := map[string]any{
|
||||
"status": status,
|
||||
"updated_at": time.Now(),
|
||||
}
|
||||
if errMsg != "" {
|
||||
updates["err_msg"] = errMsg
|
||||
}
|
||||
return s.db.Model(&model.JimengJob{}).Where("id = ?", jobId).Updates(updates).Error
|
||||
}
|
||||
|
||||
// handleTaskError 处理任务错误
|
||||
func (s *Service) handleTaskError(jobId uint, errMsg string) error {
|
||||
logger.Errorf("Jimeng task error (job_id: %d): %s", jobId, errMsg)
|
||||
return s.UpdateJobStatus(jobId, model.JMTaskStatusFailed, errMsg)
|
||||
}
|
||||
|
||||
// PushTaskToQueue 推送任务到队列(用于手动重试)
|
||||
func (s *Service) PushTaskToQueue(jobId uint) error {
|
||||
return s.taskQueue.RPush(jobId)
|
||||
}
|
||||
|
||||
// GetTaskStats 获取任务统计信息
|
||||
func (s *Service) GetTaskStats() (map[string]any, error) {
|
||||
type StatResult struct {
|
||||
Status string `json:"status"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
var stats []StatResult
|
||||
err := s.db.Model(&model.JimengJob{}).
|
||||
Select("status, COUNT(*) as count").
|
||||
Group("status").
|
||||
Find(&stats).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := map[string]any{
|
||||
"total": int64(0),
|
||||
"completed": int64(0),
|
||||
"processing": int64(0),
|
||||
"failed": int64(0),
|
||||
"pending": int64(0),
|
||||
}
|
||||
|
||||
for _, stat := range stats {
|
||||
result["total"] = result["total"].(int64) + stat.Count
|
||||
result[stat.Status] = stat.Count
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetJob 获取任务
|
||||
func (s *Service) GetJob(jobId uint) (*model.JimengJob, error) {
|
||||
var job model.JimengJob
|
||||
if err := s.db.First(&job, jobId).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &job, nil
|
||||
}
|
||||
|
||||
// testConnection 测试即梦AI连接
|
||||
func (s *Service) testConnection(accessKey, secretKey string) error {
|
||||
testClient := NewClient(accessKey, secretKey)
|
||||
|
||||
// 使用一个简单的查询任务来测试连接
|
||||
testReq := &QueryTaskRequest{
|
||||
ReqKey: "test_connection",
|
||||
TaskId: "test_task_id_12345",
|
||||
}
|
||||
|
||||
_, err := testClient.QueryTask(testReq)
|
||||
// 即使任务不存在,只要不是认证错误就说明连接正常
|
||||
if err != nil {
|
||||
// 检查是否是认证错误
|
||||
if strings.Contains(err.Error(), "InvalidAccessKey") {
|
||||
return fmt.Errorf("认证失败,请检查AccessKey和SecretKey是否正确")
|
||||
}
|
||||
// 其他错误(如任务不存在)说明连接正常
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateClientConfig 更新客户端配置
|
||||
func (s *Service) UpdateClientConfig(accessKey, secretKey string) error {
|
||||
// 创建新的客户端
|
||||
newClient := NewClient(accessKey, secretKey)
|
||||
|
||||
// 测试新客户端是否可用
|
||||
err := s.testConnection(accessKey, secretKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新客户端
|
||||
s.client = newClient
|
||||
return nil
|
||||
}
|
||||
|
||||
var defaultPower = types.JimengPower{
|
||||
TextToImage: 20,
|
||||
ImageToImage: 20,
|
||||
ImageEdit: 20,
|
||||
ImageEffects: 20,
|
||||
TextToVideo: 300,
|
||||
ImageToVideo: 300,
|
||||
}
|
||||
|
||||
// GetConfig 获取即梦AI配置
|
||||
func (s *Service) GetConfig() *types.JimengConfig {
|
||||
var config model.Config
|
||||
err := s.db.Where("name", "jimeng").First(&config).Error
|
||||
if err != nil {
|
||||
// 如果配置不存在,返回默认配置
|
||||
return &types.JimengConfig{
|
||||
AccessKey: "",
|
||||
SecretKey: "",
|
||||
Power: defaultPower,
|
||||
}
|
||||
}
|
||||
|
||||
var jimengConfig types.JimengConfig
|
||||
err = utils.JsonDecode(config.Value, &jimengConfig)
|
||||
if err != nil {
|
||||
return &types.JimengConfig{
|
||||
AccessKey: "",
|
||||
SecretKey: "",
|
||||
Power: defaultPower,
|
||||
}
|
||||
}
|
||||
|
||||
return &jimengConfig
|
||||
}
|
||||
145
api/service/jimeng/types.go
Normal file
145
api/service/jimeng/types.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package jimeng
|
||||
|
||||
import "geekai/store/model"
|
||||
|
||||
// ReqKey 常量定义
|
||||
const (
|
||||
ReqKeyTextToImage = "high_aes_general_v30l_zt2i" // 文生图
|
||||
ReqKeyImageToImagePortrait = "i2i_portrait_photo" // 图生图人像写真
|
||||
ReqKeyImageEdit = "seededit_v3.0" // 图像编辑
|
||||
ReqKeyImageEffects = "i2i_multi_style_zx2x" // 图像特效
|
||||
ReqKeyTextToVideo = "jimeng_vgfm_t2v_l20" // 文生视频
|
||||
ReqKeyImageToVideo = "jimeng_vgfm_i2v_l20" // 图生视频
|
||||
)
|
||||
|
||||
// SubmitTaskRequest 提交任务请求
|
||||
type SubmitTaskRequest struct {
|
||||
ReqKey string `json:"req_key"`
|
||||
// 文生图参数
|
||||
Prompt string `json:"prompt,omitempty"`
|
||||
Seed int64 `json:"seed,omitempty"`
|
||||
Scale float64 `json:"scale,omitempty"`
|
||||
Width int `json:"width,omitempty"`
|
||||
Height int `json:"height,omitempty"`
|
||||
UsePreLLM bool `json:"use_pre_llm,omitempty"`
|
||||
// 图生图参数
|
||||
ImageInput string `json:"image_input,omitempty"`
|
||||
ImageUrls []string `json:"image_urls,omitempty"`
|
||||
BinaryDataBase64 []string `json:"binary_data_base64,omitempty"`
|
||||
Gpen float64 `json:"gpen,omitempty"`
|
||||
Skin float64 `json:"skin,omitempty"`
|
||||
SkinUnifi float64 `json:"skin_unifi,omitempty"`
|
||||
GenMode string `json:"gen_mode,omitempty"`
|
||||
// 图像编辑参数
|
||||
// 图像特效参数
|
||||
ImageInput1 string `json:"image_input1,omitempty"`
|
||||
TemplateId string `json:"template_id,omitempty"`
|
||||
// 视频生成参数
|
||||
AspectRatio string `json:"aspect_ratio,omitempty"`
|
||||
}
|
||||
|
||||
// SubmitTaskResponse 提交任务响应
|
||||
type SubmitTaskResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
RequestId string `json:"request_id"`
|
||||
Status int `json:"status"`
|
||||
TimeElapsed string `json:"time_elapsed"`
|
||||
Data struct {
|
||||
TaskId string `json:"task_id"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// QueryTaskRequest 查询任务请求
|
||||
type QueryTaskRequest struct {
|
||||
ReqKey string `json:"req_key"`
|
||||
TaskId string `json:"task_id"`
|
||||
ReqJson string `json:"req_json,omitempty"`
|
||||
}
|
||||
|
||||
// QueryTaskResponse 查询任务响应
|
||||
type QueryTaskResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
RequestId string `json:"request_id"`
|
||||
Status int `json:"status"`
|
||||
TimeElapsed string `json:"time_elapsed"`
|
||||
Data struct {
|
||||
AlgorithmBaseResp struct {
|
||||
StatusCode int `json:"status_code"`
|
||||
StatusMessage string `json:"status_message"`
|
||||
} `json:"algorithm_base_resp"`
|
||||
BinaryDataBase64 []string `json:"binary_data_base64"`
|
||||
ImageUrls []string `json:"image_urls"`
|
||||
VideoUrl string `json:"video_url"`
|
||||
RespData string `json:"resp_data"`
|
||||
Status model.JMTaskStatus `json:"status"`
|
||||
LlmResult string `json:"llm_result"`
|
||||
PeResult string `json:"pe_result"`
|
||||
PredictTagsResult string `json:"predict_tags_result"`
|
||||
RephraserResult string `json:"rephraser_result"`
|
||||
VlmResult string `json:"vlm_result"`
|
||||
InferCtx any `json:"infer_ctx"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// CreateTaskRequest 创建任务请求
|
||||
type CreateTaskRequest struct {
|
||||
Type model.JMTaskType `json:"type"`
|
||||
Prompt string `json:"prompt"`
|
||||
Params map[string]any `json:"params"`
|
||||
ReqKey string `json:"req_key"`
|
||||
ImageUrls []string `json:"image_urls,omitempty"`
|
||||
Power int `json:"power,omitempty"`
|
||||
}
|
||||
|
||||
// LogoInfo 水印信息
|
||||
type LogoInfo struct {
|
||||
AddLogo bool `json:"add_logo"`
|
||||
Position int `json:"position"`
|
||||
Language int `json:"language"`
|
||||
Opacity float64 `json:"opacity"`
|
||||
LogoTextContent string `json:"logo_text_content"`
|
||||
}
|
||||
|
||||
// ReqJsonConfig 查询配置
|
||||
type ReqJsonConfig struct {
|
||||
ReturnUrl bool `json:"return_url"`
|
||||
LogoInfo *LogoInfo `json:"logo_info,omitempty"`
|
||||
}
|
||||
|
||||
// ImageEffectTemplate 图像特效模板
|
||||
const (
|
||||
TemplateIdFelt3DPolaroid = "felt_3d_polaroid" // 毛毡3d拍立得风格
|
||||
TemplateIdMyWorld = "my_world" // 像素世界风
|
||||
TemplateIdMyWorldUniversal = "my_world_universal" // 像素世界-万物通用版
|
||||
TemplateIdPlasticBubbleFigure = "plastic_bubble_figure" // 盲盒玩偶风
|
||||
TemplateIdPlasticBubbleFigureCartoon = "plastic_bubble_figure_cartoon_text" // 塑料泡罩人偶-文字卡头版
|
||||
TemplateIdFurryDreamDoll = "furry_dream_doll" // 毛绒玩偶风
|
||||
TemplateIdMicroLandscapeMiniWorld = "micro_landscape_mini_world" // 迷你世界玩偶风
|
||||
TemplateIdMicroLandscapeProfessional = "micro_landscape_mini_world_professional" // 微型景观小世界-职业版
|
||||
TemplateIdAcrylicOrnaments = "acrylic_ornaments" // 亚克力挂饰
|
||||
TemplateIdFeltKeychain = "felt_keychain" // 毛毡钥匙扣
|
||||
TemplateIdLofiPixelCharacter = "lofi_pixel_character_mini_card" // Lofi像素人物小卡
|
||||
TemplateIdAngelFigurine = "angel_figurine" // 天使形象手办
|
||||
TemplateIdLyingInFluffyBelly = "lying_in_fluffy_belly" // 躺在毛茸茸肚皮里
|
||||
TemplateIdGlassBall = "glass_ball" // 玻璃球
|
||||
)
|
||||
|
||||
// AspectRatio 视频宽高比
|
||||
const (
|
||||
AspectRatio16_9 = "16:9" // 1280×720
|
||||
AspectRatio9_16 = "9:16" // 720×1280
|
||||
AspectRatio1_1 = "1:1" // 960×960
|
||||
AspectRatio4_3 = "4:3" // 960×720
|
||||
AspectRatio3_4 = "3:4" // 720×960
|
||||
AspectRatio21_9 = "21:9" // 1680×720
|
||||
AspectRatio9_21 = "9:21" // 720×1680
|
||||
)
|
||||
|
||||
// GenMode 生成模式
|
||||
const (
|
||||
GenModeCreative = "creative" // 提示词模式
|
||||
GenModeReference = "reference" // 全参考模式
|
||||
GenModeReferenceChar = "reference_char" // 人物参考模式
|
||||
)
|
||||
@@ -102,7 +102,7 @@ func (s *LicenseService) SyncLicense() {
|
||||
if err != nil {
|
||||
retryCounter++
|
||||
if retryCounter < 5 {
|
||||
logger.Warn(err)
|
||||
logger.Debug(err)
|
||||
}
|
||||
s.license.IsActive = false
|
||||
} else {
|
||||
|
||||
@@ -191,7 +191,7 @@ func (s *Service) DownloadImages() {
|
||||
if strings.HasPrefix(v.OrgURL, "https://cdn.discordapp.com") {
|
||||
proxy = true
|
||||
}
|
||||
imgURL, err := s.uploaderManager.GetUploadHandler().PutUrlFile(v.OrgURL, proxy)
|
||||
imgURL, err := s.uploaderManager.GetUploadHandler().PutUrlFile(v.OrgURL, ".png", proxy)
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("error with download image %s, %v", v.OrgURL, err)
|
||||
@@ -212,7 +212,9 @@ func (s *Service) DownloadImages() {
|
||||
// PushTask push a new mj task in to task queue
|
||||
func (s *Service) PushTask(task types.MjTask) {
|
||||
logger.Debugf("add a new MidJourney task to the task list: %+v", task)
|
||||
s.taskQueue.RPush(task)
|
||||
if err := s.taskQueue.RPush(task); err != nil {
|
||||
logger.Errorf("push mj task to queue failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// SyncTaskProgress 异步拉取任务
|
||||
|
||||
@@ -84,7 +84,7 @@ func (s AliYunOss) PutFile(ctx *gin.Context, name string) (File, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s AliYunOss) PutUrlFile(fileURL string, useProxy bool) (string, error) {
|
||||
func (s AliYunOss) PutUrlFile(fileURL string, ext string, useProxy bool) (string, error) {
|
||||
var fileData []byte
|
||||
var err error
|
||||
if useProxy {
|
||||
@@ -99,8 +99,10 @@ func (s AliYunOss) PutUrlFile(fileURL string, useProxy bool) (string, error) {
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with parse image URL: %v", err)
|
||||
}
|
||||
fileExt := utils.GetImgExt(parse.Path)
|
||||
objectKey := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), fileExt)
|
||||
if ext == "" {
|
||||
ext = filepath.Ext(parse.Path)
|
||||
}
|
||||
objectKey := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), ext)
|
||||
// 上传文件字节数据
|
||||
err = s.bucket.PutObject(objectKey, bytes.NewReader(fileData))
|
||||
if err != nil {
|
||||
|
||||
@@ -12,11 +12,12 @@ import (
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"geekai/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type LocalStorage struct {
|
||||
@@ -37,7 +38,7 @@ func (s LocalStorage) PutFile(ctx *gin.Context, name string) (File, error) {
|
||||
return File{}, fmt.Errorf("error with get form: %v", err)
|
||||
}
|
||||
|
||||
path, err := utils.GenUploadPath(s.config.BasePath, file.Filename, false)
|
||||
path, err := utils.GenUploadPath(s.config.BasePath, file.Filename, "")
|
||||
if err != nil {
|
||||
return File{}, fmt.Errorf("error with generate filename: %s", err.Error())
|
||||
}
|
||||
@@ -57,13 +58,13 @@ func (s LocalStorage) PutFile(ctx *gin.Context, name string) (File, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s LocalStorage) PutUrlFile(fileURL string, useProxy bool) (string, error) {
|
||||
func (s LocalStorage) PutUrlFile(fileURL string, ext string, useProxy bool) (string, error) {
|
||||
parse, err := url.Parse(fileURL)
|
||||
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, true)
|
||||
filePath, err := utils.GenUploadPath(s.config.BasePath, filename, ext)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with generate image dir: %v", err)
|
||||
}
|
||||
@@ -85,7 +86,7 @@ func (s LocalStorage) PutBase64(base64Img string) (string, error) {
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error decoding base64:%v", err)
|
||||
}
|
||||
filePath, err := utils.GenUploadPath(s.config.BasePath, "", true)
|
||||
filePath, _ := utils.GenUploadPath(s.config.BasePath, "", ".png")
|
||||
err = os.WriteFile(filePath, imageData, 0644)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error writing to file:%v", err)
|
||||
|
||||
@@ -44,7 +44,7 @@ func NewMiniOss(appConfig *types.AppConfig) (MiniOss, error) {
|
||||
return MiniOss{config: config, client: minioClient, proxyURL: appConfig.ProxyURL}, nil
|
||||
}
|
||||
|
||||
func (s MiniOss) PutUrlFile(fileURL string, useProxy bool) (string, error) {
|
||||
func (s MiniOss) PutUrlFile(fileURL string, ext string, useProxy bool) (string, error) {
|
||||
var fileData []byte
|
||||
var err error
|
||||
if useProxy {
|
||||
@@ -59,8 +59,10 @@ func (s MiniOss) PutUrlFile(fileURL string, useProxy bool) (string, error) {
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with parse image URL: %v", err)
|
||||
}
|
||||
fileExt := filepath.Ext(parse.Path)
|
||||
filename := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), fileExt)
|
||||
if ext == "" {
|
||||
ext = filepath.Ext(parse.Path)
|
||||
}
|
||||
filename := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), ext)
|
||||
info, err := s.client.PutObject(
|
||||
context.Background(),
|
||||
s.config.Bucket,
|
||||
@@ -86,7 +88,7 @@ func (s MiniOss) PutFile(ctx *gin.Context, name string) (File, error) {
|
||||
}
|
||||
defer fileReader.Close()
|
||||
|
||||
fileExt := utils.GetImgExt(file.Filename)
|
||||
fileExt := filepath.Ext(file.Filename)
|
||||
filename := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), fileExt)
|
||||
info, err := s.client.PutObject(ctx, s.config.Bucket, filename, fileReader, file.Size, minio.PutObjectOptions{
|
||||
ContentType: file.Header.Get("Body-Type"),
|
||||
|
||||
@@ -93,7 +93,7 @@ func (s QinNiuOss) PutFile(ctx *gin.Context, name string) (File, error) {
|
||||
|
||||
}
|
||||
|
||||
func (s QinNiuOss) PutUrlFile(fileURL string, useProxy bool) (string, error) {
|
||||
func (s QinNiuOss) PutUrlFile(fileURL string, ext string, useProxy bool) (string, error) {
|
||||
var fileData []byte
|
||||
var err error
|
||||
if useProxy {
|
||||
@@ -108,8 +108,10 @@ func (s QinNiuOss) PutUrlFile(fileURL string, useProxy bool) (string, error) {
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error with parse image URL: %v", err)
|
||||
}
|
||||
fileExt := utils.GetImgExt(parse.Path)
|
||||
key := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), fileExt)
|
||||
if ext == "" {
|
||||
ext = filepath.Ext(parse.Path)
|
||||
}
|
||||
key := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), ext)
|
||||
ret := storage.PutRet{}
|
||||
extra := storage.PutExtra{}
|
||||
// 上传文件字节数据
|
||||
|
||||
@@ -23,7 +23,7 @@ type File struct {
|
||||
}
|
||||
type Uploader interface {
|
||||
PutFile(ctx *gin.Context, name string) (File, error)
|
||||
PutUrlFile(url string, useProxy bool) (string, error)
|
||||
PutUrlFile(url string, ext string, useProxy bool) (string, error)
|
||||
PutBase64(imageData string) (string, error)
|
||||
Delete(fileURL string) error
|
||||
}
|
||||
|
||||
@@ -253,7 +253,9 @@ func (s *Service) checkTaskProgress(apiKey model.ApiKey) (*TaskProgressResp, err
|
||||
|
||||
func (s *Service) PushTask(task types.SdTask) {
|
||||
logger.Debugf("add a new MidJourney task to the task list: %+v", task)
|
||||
s.taskQueue.RPush(task)
|
||||
if err := s.taskQueue.RPush(task); err != nil {
|
||||
logger.Errorf("push sd task to queue failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// CheckTaskStatus 检查任务状态,自动删除过期或者失败的任务
|
||||
|
||||
@@ -51,7 +51,9 @@ func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Clien
|
||||
|
||||
func (s *Service) PushTask(task types.SunoTask) {
|
||||
logger.Infof("add a new Suno task to the task list: %+v", task)
|
||||
s.taskQueue.RPush(task)
|
||||
if err := s.taskQueue.RPush(task); err != nil {
|
||||
logger.Errorf("push suno task to queue failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Run() {
|
||||
@@ -94,6 +96,8 @@ func (s *Service) Run() {
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Infof("任务提交成功: %+v", r)
|
||||
|
||||
// 更新任务信息
|
||||
s.db.Model(&model.SunoJob{Id: task.Id}).UpdateColumns(map[string]interface{}{
|
||||
"task_id": r.Data,
|
||||
@@ -127,6 +131,7 @@ func (s *Service) Create(task types.SunoTask) (RespVo, error) {
|
||||
"continue_clip_id": task.RefSongId,
|
||||
"continue_at": task.ExtendSecs,
|
||||
"make_instrumental": task.Instrumental,
|
||||
"mv": task.Model,
|
||||
}
|
||||
// 灵感模式
|
||||
if task.Type == 1 {
|
||||
@@ -134,7 +139,6 @@ func (s *Service) Create(task types.SunoTask) (RespVo, error) {
|
||||
} else { // 自定义模式
|
||||
reqBody["prompt"] = task.Lyrics
|
||||
reqBody["tags"] = task.Tags
|
||||
reqBody["mv"] = task.Model
|
||||
reqBody["title"] = task.Title
|
||||
}
|
||||
|
||||
@@ -268,14 +272,14 @@ func (s *Service) DownloadFiles() {
|
||||
for _, v := range items {
|
||||
// 下载图片和音频
|
||||
logger.Infof("try download cover image: %s", v.CoverURL)
|
||||
coverURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(v.CoverURL, true)
|
||||
coverURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(v.CoverURL, ".png", true)
|
||||
if err != nil {
|
||||
logger.Errorf("download image with error: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Infof("try download audio: %s", v.AudioURL)
|
||||
audioURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(v.AudioURL, true)
|
||||
audioURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(v.AudioURL, ".mp3", true)
|
||||
if err != nil {
|
||||
logger.Errorf("download audio with error: %v", err)
|
||||
continue
|
||||
|
||||
@@ -113,7 +113,7 @@ Please remember, the final output must be the same language with user’s input.
|
||||
- What kinds of examples may need to be included, how many, and whether they are complex enough to benefit from placeholders.
|
||||
- Clarity and Conciseness: Use clear, specific language. Avoid unnecessary instructions or bland statements.
|
||||
- Formatting: Use markdown features for readability. DO NOT USE CODE BLOCKS UNLESS SPECIFICALLY REQUESTED.
|
||||
- Preserve User Content: If the input task or prompt includes extensive guidelines or examples, preserve them entirely, or as closely as possible. If they are vague, consider breaking down into sub-steps. Keep any details, guidelines, examples, variables, or placeholders provided by the user.
|
||||
- Preserve User Prompt: If the input task or prompt includes extensive guidelines or examples, preserve them entirely, or as closely as possible. If they are vague, consider breaking down into sub-steps. Keep any details, guidelines, examples, variables, or placeholders provided by the user.
|
||||
- Constants: DO include constants in the prompt, as they are not susceptible to prompt injection. Such as guides, rubrics, and examples.
|
||||
- Output Format: Explicitly the most appropriate output format, in detail. This should include length and syntax (e.g. short sentence, paragraph, JSON, etc.)
|
||||
- For tasks outputting well-defined or structured data (classification, JSON, etc.) bias toward outputting a JSON.
|
||||
|
||||
@@ -4,9 +4,10 @@ import (
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
"geekai/store/model"
|
||||
"gorm.io/gorm"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UserService struct {
|
||||
@@ -19,7 +20,7 @@ func NewUserService(db *gorm.DB) *UserService {
|
||||
}
|
||||
|
||||
// IncreasePower 增加用户算力
|
||||
func (s *UserService) IncreasePower(userId int, power int, log model.PowerLog) error {
|
||||
func (s *UserService) IncreasePower(userId uint, power int, log model.PowerLog) error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
@@ -51,7 +52,7 @@ func (s *UserService) IncreasePower(userId int, power int, log model.PowerLog) e
|
||||
}
|
||||
|
||||
// DecreasePower 减少用户算力
|
||||
func (s *UserService) DecreasePower(userId int, power int, log model.PowerLog) error {
|
||||
func (s *UserService) DecreasePower(userId uint, power int, log model.PowerLog) error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
|
||||
@@ -51,7 +51,9 @@ func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Clien
|
||||
|
||||
func (s *Service) PushTask(task types.VideoTask) {
|
||||
logger.Infof("add a new Video task to the task list: %+v", task)
|
||||
s.taskQueue.RPush(task)
|
||||
if err := s.taskQueue.RPush(task); err != nil {
|
||||
logger.Errorf("push video task to queue failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Run() {
|
||||
@@ -162,7 +164,7 @@ func (s *Service) DownloadFiles() {
|
||||
}
|
||||
|
||||
logger.Infof("try download video: %s", v.WaterURL)
|
||||
videoURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(v.WaterURL, true)
|
||||
videoURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(v.WaterURL, ".mp4", true)
|
||||
if err != nil {
|
||||
logger.Errorf("download video with error: %v", err)
|
||||
continue
|
||||
@@ -172,7 +174,7 @@ func (s *Service) DownloadFiles() {
|
||||
|
||||
if v.VideoURL != "" {
|
||||
logger.Infof("try download no water video: %s", v.VideoURL)
|
||||
videoURL, err = s.uploadManager.GetUploadHandler().PutUrlFile(v.VideoURL, true)
|
||||
videoURL, err = s.uploadManager.GetUploadHandler().PutUrlFile(v.VideoURL, ".mp4", true)
|
||||
if err != nil {
|
||||
logger.Errorf("download video with error: %v", err)
|
||||
continue
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package service
|
||||
|
||||
import "geekai/core/types"
|
||||
|
||||
type WebsocketService struct {
|
||||
Clients *types.LMap[string, *types.WsClient] // clientId => Client
|
||||
}
|
||||
|
||||
func NewWebsocketService() *WebsocketService {
|
||||
return &WebsocketService{
|
||||
Clients: types.NewLMap[string, *types.WsClient](),
|
||||
}
|
||||
}
|
||||
@@ -9,14 +9,11 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"geekai/core/types"
|
||||
logger2 "geekai/logger"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
|
||||
"github.com/xxl-job/xxl-job-executor-go"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
@@ -46,97 +43,13 @@ func NewXXLJobExecutor(config *types.AppConfig, db *gorm.DB) *XXLJobExecutor {
|
||||
|
||||
func (e *XXLJobExecutor) Run() error {
|
||||
e.executor.RegTask("ClearOrders", e.ClearOrders)
|
||||
e.executor.RegTask("ResetVipPower", e.ResetVipPower)
|
||||
e.executor.RegTask("ResetUserPower", e.ResetUserPower)
|
||||
return e.executor.Run()
|
||||
}
|
||||
|
||||
// ClearOrders 清理未支付的订单,如果没有抛出异常则表示执行成功
|
||||
func (e *XXLJobExecutor) ClearOrders(cxt context.Context, param *xxl.RunReq) (msg string) {
|
||||
logger.Info("执行清理未支付订单...")
|
||||
var sysConfig model.Config
|
||||
res := e.db.Where("marker", "system").First(&sysConfig)
|
||||
if res.Error != nil {
|
||||
return "error with get system config: " + res.Error.Error()
|
||||
}
|
||||
|
||||
var config types.SystemConfig
|
||||
err := utils.JsonDecode(sysConfig.Config, &config)
|
||||
if err != nil {
|
||||
return "error with decode system config: " + err.Error()
|
||||
}
|
||||
|
||||
if config.OrderPayTimeout == 0 { // 默认未支付订单的生命周期为 30 分钟
|
||||
config.OrderPayTimeout = 1800
|
||||
}
|
||||
timeout := time.Now().Unix() - int64(config.OrderPayTimeout)
|
||||
start := utils.Stamp2str(timeout)
|
||||
// 这里不是用软删除,而是永久删除订单
|
||||
res = e.db.Unscoped().Where("status IN ? AND created_at < ?", []types.OrderStatus{types.OrderNotPaid, types.OrderScanned}, start).Delete(&model.Order{})
|
||||
logger.Infof("Clear order successfully, affect rows: %d", res.RowsAffected)
|
||||
return "success"
|
||||
}
|
||||
|
||||
// ResetVipPower 重置VIP会员算力
|
||||
// 自动将 VIP 会员的算力补充到每月赠送的最大值
|
||||
func (e *XXLJobExecutor) ResetVipPower(cxt context.Context, param *xxl.RunReq) (msg string) {
|
||||
logger.Info("开始进行月底账号盘点...")
|
||||
return "success"
|
||||
}
|
||||
|
||||
func (e *XXLJobExecutor) ResetUserPower(cxt context.Context, param *xxl.RunReq) (msg string) {
|
||||
logger.Info("今日算力派发开始:", time.Now())
|
||||
var users []model.User
|
||||
res := e.db.Where("status", 1).Find(&users)
|
||||
if res.Error != nil {
|
||||
return "No matching users"
|
||||
}
|
||||
|
||||
var sysConfig model.Config
|
||||
res = e.db.Where("marker", "system").First(&sysConfig)
|
||||
if res.Error != nil {
|
||||
return "error with get system config: " + res.Error.Error()
|
||||
}
|
||||
|
||||
var config types.SystemConfig
|
||||
err := utils.JsonDecode(sysConfig.Config, &config)
|
||||
if err != nil {
|
||||
return "error with decode system config: " + err.Error()
|
||||
}
|
||||
|
||||
if config.DailyPower <= 0 {
|
||||
return "success"
|
||||
}
|
||||
|
||||
var counter = 0
|
||||
var totalPower = 0
|
||||
for _, u := range users {
|
||||
if u.Power >= config.DailyPower {
|
||||
continue
|
||||
}
|
||||
var power = config.DailyPower - u.Power
|
||||
// update user
|
||||
tx := e.db.Model(&model.User{}).Where("id", u.Id).UpdateColumn("power", gorm.Expr("power + ?", power))
|
||||
// 记录算力充值日志
|
||||
if tx.Error == nil {
|
||||
var user model.User
|
||||
e.db.Where("id", u.Id).First(&user)
|
||||
e.db.Create(&model.PowerLog{
|
||||
UserId: u.Id,
|
||||
Username: u.Username,
|
||||
Type: types.PowerGift,
|
||||
Amount: power,
|
||||
Mark: types.PowerAdd,
|
||||
Balance: user.Power,
|
||||
Model: "系统赠送",
|
||||
Remark: fmt.Sprintf("系统每日算力派发,今日额度:%d", config.DailyPower),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
counter++
|
||||
totalPower += power
|
||||
}
|
||||
logger.Infof("今日派发算力结束!累计派发 %d 人,累计派发算力:%d", counter, totalPower)
|
||||
|
||||
return "success"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type AdminUser struct {
|
||||
BaseModel
|
||||
Username string
|
||||
Password string
|
||||
Salt string // 密码盐
|
||||
Status bool `gorm:"default:true"` // 当前状态
|
||||
LastLoginAt int64 // 最后登录时间
|
||||
LastLoginIp string // 最后登录 IP
|
||||
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
Username string `gorm:"column:username;type:varchar(30);uniqueIndex;not null;comment:用户名" json:"username"`
|
||||
Password string `gorm:"column:password;type:char(64);not null;comment:密码" json:"password"`
|
||||
Salt string `gorm:"column:salt;type:char(12);not null;comment:密码盐" json:"salt"`
|
||||
Status bool `gorm:"column:status;type:tinyint(1);not null;comment:当前状态" json:"status"`
|
||||
LastLoginAt int64 `gorm:"column:last_login_at;type:int;not null;comment:最后登录时间" json:"last_login_at"`
|
||||
LastLoginIp string `gorm:"column:last_login_ip;type:char(16);not null;comment:最后登录 IP" json:"last_login_ip"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null;comment:创建时间" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:datetime;not null;comment:更新时间" json:"updated_at"`
|
||||
}
|
||||
|
||||
// TableName 表名
|
||||
func (m *AdminUser) TableName() string {
|
||||
return "chatgpt_admin_users"
|
||||
}
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// ApiKey OpenAI API 模型
|
||||
type ApiKey struct {
|
||||
BaseModel
|
||||
Name string
|
||||
Type string // 用途 chat => 聊天,img => 绘图
|
||||
Value string // API Key 的值
|
||||
ApiURL string // 当前 KEY 的 API 地址
|
||||
Enabled bool // 是否启用
|
||||
ProxyURL string // 代理地址
|
||||
LastUsedAt int64 // 最后使用时间
|
||||
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
Name string `gorm:"column:name;type:varchar(30);comment:名称" json:"name"`
|
||||
Value string `gorm:"column:value;type:varchar(255);not null;comment:API KEY value" json:"value"`
|
||||
Type string `gorm:"column:type;type:varchar(10);default:chat;not null;comment:用途(chat=>聊天,img=>图片)" json:"type"`
|
||||
LastUsedAt int64 `gorm:"column:last_used_at;type:int;not null;comment:最后使用时间" json:"last_used_at"`
|
||||
ApiURL string `gorm:"column:api_url;type:varchar(255);comment:API 地址" json:"api_url"`
|
||||
Enabled bool `gorm:"column:enabled;type:tinyint(1);comment:是否启用" json:"enabled"`
|
||||
ProxyURL string `gorm:"column:proxy_url;type:varchar(100);comment:代理地址" json:"proxy_url"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:datetime;not null" json:"updated_at"`
|
||||
}
|
||||
|
||||
// TableName 表名
|
||||
func (m *ApiKey) TableName() string {
|
||||
return "chatgpt_api_keys"
|
||||
}
|
||||
|
||||
@@ -3,10 +3,15 @@ package model
|
||||
import "time"
|
||||
|
||||
type AppType struct {
|
||||
Id uint `gorm:"primarykey"`
|
||||
Name string
|
||||
Icon string
|
||||
Enabled bool
|
||||
SortNum int
|
||||
CreatedAt time.Time
|
||||
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
Name string `gorm:"column:name;type:varchar(50);not null;comment:名称" json:"name"`
|
||||
Icon string `gorm:"column:icon;type:varchar(255);not null;comment:图标URL" json:"icon"`
|
||||
SortNum int `gorm:"column:sort_num;type:tinyint;not null;comment:排序" json:"sort_num"`
|
||||
Enabled bool `gorm:"column:enabled;type:tinyint(1);not null;comment:是否启用" json:"enabled"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"`
|
||||
}
|
||||
|
||||
// TableName 表名
|
||||
func (m *AppType) TableName() string {
|
||||
return "chatgpt_app_types"
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type BaseModel struct {
|
||||
Id uint `gorm:"primarykey;column:id"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package model
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type ChatMessage struct {
|
||||
BaseModel
|
||||
ChatId string // 会话 ID
|
||||
UserId uint // 用户 ID
|
||||
RoleId uint // 角色 ID
|
||||
Model string // AI模型
|
||||
Type string
|
||||
Icon string
|
||||
Tokens int
|
||||
TotalTokens int // 总 token 消耗
|
||||
Content string
|
||||
UseContext bool // 是否可以作为聊天上下文
|
||||
DeletedAt gorm.DeletedAt
|
||||
}
|
||||
|
||||
func (ChatMessage) TableName() string {
|
||||
return "chatgpt_chat_history"
|
||||
}
|
||||
@@ -1,14 +1,21 @@
|
||||
package model
|
||||
|
||||
import "gorm.io/gorm"
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type ChatItem struct {
|
||||
BaseModel
|
||||
ChatId string `gorm:"column:chat_id;unique"` // 会话 ID
|
||||
UserId uint // 用户 ID
|
||||
RoleId uint // 角色 ID
|
||||
ModelId uint // 模型 ID
|
||||
Model string // 模型
|
||||
Title string // 会话标题
|
||||
DeletedAt gorm.DeletedAt
|
||||
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
ChatId string `gorm:"column:chat_id;type:char(40);uniqueIndex;not null;comment:会话 ID" json:"chat_id"`
|
||||
UserId uint `gorm:"column:user_id;type:int;not null;comment:用户 ID" json:"user_id"`
|
||||
RoleId uint `gorm:"column:role_id;type:int;not null;comment:角色 ID" json:"role_id"`
|
||||
Title string `gorm:"column:title;type:varchar(100);not null;comment:会话标题" json:"title"`
|
||||
ModelId uint `gorm:"column:model_id;type:int;not null;default:0;comment:模型 ID" json:"model_id"`
|
||||
Model string `gorm:"column:model;type:varchar(30);comment:模型名称" json:"model"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null;comment:创建时间" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:datetime;not null;comment:更新时间" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (m *ChatItem) TableName() string {
|
||||
return "chatgpt_chat_items"
|
||||
}
|
||||
|
||||
25
api/store/model/chat_message.go
Normal file
25
api/store/model/chat_message.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type ChatMessage struct {
|
||||
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
UserId uint `gorm:"column:user_id;type:int;not null;comment:用户 ID" json:"user_id"`
|
||||
ChatId string `gorm:"column:chat_id;type:char(40);not null;index;comment:会话 ID" json:"chat_id"`
|
||||
Type string `gorm:"column:type;type:varchar(10);not null;comment:类型:prompt|reply" json:"type"`
|
||||
Icon string `gorm:"column:icon;type:varchar(255);not null;comment:角色图标" json:"icon"`
|
||||
RoleId uint `gorm:"column:role_id;type:int;not null;comment:角色 ID" json:"role_id"`
|
||||
Model string `gorm:"column:model;type:varchar(255);comment:模型名称" json:"model"`
|
||||
Content string `gorm:"column:content;type:text;not null;comment:聊天内容" json:"content"`
|
||||
Tokens int `gorm:"column:tokens;type:smallint;not null;comment:耗费 token 数量" json:"tokens"`
|
||||
TotalTokens int `gorm:"column:total_tokens;type:int;not null;comment:消耗总Token长度" json:"total_tokens"`
|
||||
UseContext bool `gorm:"column:use_context;type:tinyint(1);not null;comment:是否允许作为上下文语料" json:"use_context"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:datetime;not null" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (m *ChatMessage) TableName() string {
|
||||
return "chatgpt_chat_history"
|
||||
}
|
||||
@@ -1,17 +1,29 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type ChatModel struct {
|
||||
BaseModel
|
||||
Name string
|
||||
Value string // API Key 的值
|
||||
SortNum int
|
||||
Enabled bool
|
||||
Power int // 每次对话消耗算力
|
||||
Open bool // 是否开放模型给所有人使用
|
||||
MaxTokens int // 最大响应长度
|
||||
MaxContext int // 最大上下文长度
|
||||
Temperature float32 // 模型温度
|
||||
KeyId int // 绑定 API KEY ID
|
||||
Type string // 模型类型
|
||||
Options string // 模型选项
|
||||
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
Desc string `gorm:"column:desc;type:varchar(1024);not null;default:'';comment:模型类型描述" json:"desc"`
|
||||
Tag string `gorm:"column:tag;type:varchar(1024);not null;default:'';comment:模型标签" json:"tag"`
|
||||
Type string `gorm:"column:type;type:varchar(10);not null;default:chat;comment:模型类型(chat,img)" json:"type"`
|
||||
Name string `gorm:"column:name;type:varchar(255);not null;comment:模型名称" json:"name"`
|
||||
Value string `gorm:"column:value;type:varchar(255);not null;comment:模型值" json:"value"`
|
||||
SortNum int `gorm:"column:sort_num;type:tinyint(1);not null;comment:排序数字" json:"sort_num"`
|
||||
Enabled bool `gorm:"column:enabled;type:tinyint(1);not null;default:0;comment:是否启用模型" json:"enabled"`
|
||||
Power int `gorm:"column:power;type:smallint;not null;comment:消耗算力点数" json:"power"`
|
||||
Temperature float32 `gorm:"column:temperature;type:float(3,1);not null;default:1.0;comment:模型创意度" json:"temperature"`
|
||||
MaxTokens int `gorm:"column:max_tokens;type:int;not null;default:1024;comment:最大响应长度" json:"max_tokens"`
|
||||
MaxContext int `gorm:"column:max_context;type:int;not null;default:4096;comment:最大上下文长度" json:"max_context"`
|
||||
Open bool `gorm:"column:open;type:tinyint(1);not null;comment:是否开放模型" json:"open"`
|
||||
KeyId uint `gorm:"column:key_id;type:int;not null;comment:绑定API KEY ID" json:"key_id"`
|
||||
Options string `gorm:"column:options;type:text;not null;comment:模型自定义选项" json:"options"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:datetime" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (m *ChatModel) TableName() string {
|
||||
return "chatgpt_chat_models"
|
||||
}
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type ChatRole struct {
|
||||
BaseModel
|
||||
Tid int
|
||||
Key string `gorm:"column:marker;unique"` // 角色唯一标识
|
||||
Name string // 角色名称
|
||||
Context string `gorm:"column:context_json"` // 角色语料信息 json
|
||||
HelloMsg string // 打招呼的消息
|
||||
Icon string // 角色聊天图标
|
||||
Enable bool // 是否启用被启用
|
||||
SortNum int //排序数字
|
||||
ModelId int // 绑定模型ID,绑定模型ID的角色只能用指定的模型来问答
|
||||
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
Name string `gorm:"column:name;type:varchar(30);not null;comment:角色名称" json:"name"`
|
||||
Tid uint `gorm:"column:tid;type:int;not null;comment:分类ID" json:"tid"`
|
||||
Key string `gorm:"column:marker;type:varchar(30);uniqueIndex;not null;comment:角色标识" json:"marker"`
|
||||
Context string `gorm:"column:context_json;type:text;not null;comment:角色语料 json" json:"context_json"`
|
||||
HelloMsg string `gorm:"column:hello_msg;type:varchar(255);not null;comment:打招呼信息" json:"hello_msg"`
|
||||
Icon string `gorm:"column:icon;type:varchar(255);not null;comment:角色图标" json:"icon"`
|
||||
Enable bool `gorm:"column:enable;type:tinyint(1);not null;comment:是否被启用" json:"enable"`
|
||||
SortNum int `gorm:"column:sort_num;type:smallint;not null;default:0;comment:角色排序" json:"sort_num"`
|
||||
ModelId uint `gorm:"column:model_id;type:int;not null;default:0;comment:绑定模型ID" json:"model_id"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:datetime;not null" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (m *ChatRole) TableName() string {
|
||||
return "chatgpt_chat_roles"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package model
|
||||
|
||||
type Config struct {
|
||||
Id uint `gorm:"primarykey;column:id"`
|
||||
Key string `gorm:"column:marker;unique"`
|
||||
Config string `gorm:"column:config_json"`
|
||||
Id uint `gorm:"column:id;primaryKey;autoIncrement"`
|
||||
Name string `gorm:"column:name;type:varchar(20);uniqueIndex;not null;comment:配置名称"`
|
||||
Value string `gorm:"column:value;type:text;not null"`
|
||||
}
|
||||
|
||||
func (m *Config) TableName() string {
|
||||
return "chatgpt_configs"
|
||||
}
|
||||
|
||||
@@ -3,15 +3,19 @@ package model
|
||||
import "time"
|
||||
|
||||
type DallJob struct {
|
||||
Id uint `gorm:"primarykey;column:id"`
|
||||
UserId uint
|
||||
Prompt string
|
||||
TaskInfo string // 原始任务信息
|
||||
ImgURL string
|
||||
OrgURL string
|
||||
Publish bool
|
||||
Power int
|
||||
Progress int
|
||||
ErrMsg string
|
||||
CreatedAt time.Time
|
||||
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
UserId uint `gorm:"column:user_id;type:int;not null;comment:用户ID" json:"user_id"`
|
||||
Prompt string `gorm:"column:prompt;type:text;not null;comment:提示词" json:"prompt"`
|
||||
TaskInfo string `gorm:"column:task_info;type:text;not null;comment:任务详情" json:"task_info"`
|
||||
ImgURL string `gorm:"column:img_url;type:varchar(255);not null;comment:图片地址" json:"img_url"`
|
||||
OrgURL string `gorm:"column:org_url;type:varchar(1024);comment:原图地址" json:"org_url"`
|
||||
Publish int `gorm:"column:publish;type:tinyint(1);not null;comment:是否发布" json:"publish"`
|
||||
Power int `gorm:"column:power;type:smallint;not null;comment:消耗算力" json:"power"`
|
||||
Progress int `gorm:"column:progress;type:smallint;not null;comment:任务进度" json:"progress"`
|
||||
ErrMsg string `gorm:"column:err_msg;type:varchar(1024);not null;comment:错误信息" json:"err_msg"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"`
|
||||
}
|
||||
|
||||
func (m *DallJob) TableName() string {
|
||||
return "chatgpt_dall_jobs"
|
||||
}
|
||||
|
||||
@@ -3,12 +3,16 @@ package model
|
||||
import "time"
|
||||
|
||||
type File struct {
|
||||
Id uint `gorm:"primarykey;column:id"`
|
||||
UserId int
|
||||
Name string
|
||||
ObjKey string
|
||||
URL string
|
||||
Ext string
|
||||
Size int64
|
||||
CreatedAt time.Time
|
||||
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
UserId uint `gorm:"column:user_id;type:int;not null;comment:用户 ID" json:"user_id"`
|
||||
Name string `gorm:"column:name;type:varchar(255);not null;comment:文件名" json:"name"`
|
||||
ObjKey string `gorm:"column:obj_key;type:varchar(100);comment:文件标识" json:"obj_key"`
|
||||
URL string `gorm:"column:url;type:varchar(255);not null;comment:文件地址" json:"url"`
|
||||
Ext string `gorm:"column:ext;type:varchar(10);not null;comment:文件后缀" json:"ext"`
|
||||
Size int64 `gorm:"column:size;type:bigint;not null;default:0;comment:文件大小" json:"size"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null;comment:创建时间" json:"created_at"`
|
||||
}
|
||||
|
||||
func (m *File) TableName() string {
|
||||
return "chatgpt_files"
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
package model
|
||||
|
||||
type Function struct {
|
||||
Id uint `gorm:"primarykey;column:id"`
|
||||
Name string
|
||||
Label string
|
||||
Description string
|
||||
Parameters string
|
||||
Action string
|
||||
Token string
|
||||
Enabled bool
|
||||
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
Name string `gorm:"column:name;type:varchar(30);uniqueIndex;not null;comment:函数名称" json:"name"`
|
||||
Label string `gorm:"column:label;type:varchar(30);comment:函数标签" json:"label"`
|
||||
Description string `gorm:"column:description;type:varchar(255);comment:函数描述" json:"description"`
|
||||
Parameters string `gorm:"column:parameters;type:text;comment:函数参数(JSON)" json:"parameters"`
|
||||
Token string `gorm:"column:token;type:varchar(255);comment:API授权token" json:"token"`
|
||||
Action string `gorm:"column:action;type:varchar(255);comment:函数处理 API" json:"action"`
|
||||
Enabled bool `gorm:"column:enabled;type:tinyint(1);not null;default:0;comment:是否启用" json:"enabled"`
|
||||
}
|
||||
|
||||
func (m *Function) TableName() string {
|
||||
return "chatgpt_functions"
|
||||
}
|
||||
|
||||
@@ -3,10 +3,14 @@ package model
|
||||
import "time"
|
||||
|
||||
type InviteCode struct {
|
||||
Id uint `gorm:"primarykey;column:id"`
|
||||
UserId uint
|
||||
Code string
|
||||
Hits int // 点击次数
|
||||
RegNum int // 注册人数
|
||||
CreatedAt time.Time
|
||||
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
UserId uint `gorm:"column:user_id;type:int;not null;comment:用户ID" json:"user_id"`
|
||||
Code string `gorm:"column:code;type:char(8);uniqueIndex;not null;comment:邀请码" json:"code"`
|
||||
Hits int `gorm:"column:hits;type:int;not null;comment:点击次数" json:"hits"`
|
||||
RegNum int `gorm:"column:reg_num;type:smallint;not null;comment:注册数量" json:"reg_num"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"`
|
||||
}
|
||||
|
||||
func (m *InviteCode) TableName() string {
|
||||
return "chatgpt_invite_codes"
|
||||
}
|
||||
|
||||
@@ -5,11 +5,15 @@ import (
|
||||
)
|
||||
|
||||
type InviteLog struct {
|
||||
Id uint `gorm:"primarykey;column:id"`
|
||||
InviterId uint
|
||||
UserId uint
|
||||
Username string
|
||||
InviteCode string
|
||||
Remark string
|
||||
CreatedAt time.Time
|
||||
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
InviterId uint `gorm:"column:inviter_id;type:int;not null;comment:邀请人ID" json:"inviter_id"`
|
||||
UserId uint `gorm:"column:user_id;type:int;not null;comment:注册用户ID" json:"user_id"`
|
||||
Username string `gorm:"column:username;type:varchar(30);not null;comment:用户名" json:"username"`
|
||||
InviteCode string `gorm:"column:invite_code;type:char(8);not null;comment:邀请码" json:"invite_code"`
|
||||
Remark string `gorm:"column:remark;type:varchar(255);not null;comment:备注" json:"remark"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"`
|
||||
}
|
||||
|
||||
func (m *InviteLog) TableName() string {
|
||||
return "chatgpt_invite_logs"
|
||||
}
|
||||
|
||||
55
api/store/model/jimeng_job.go
Normal file
55
api/store/model/jimeng_job.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// JimengJob 即梦AI任务模型
|
||||
type JimengJob struct {
|
||||
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
UserId uint `gorm:"column:user_id;type:int;not null;index;comment:用户ID" json:"user_id"`
|
||||
TaskId string `gorm:"column:task_id;type:varchar(100);not null;index;comment:任务ID" json:"task_id"`
|
||||
Type JMTaskType `gorm:"column:type;type:varchar(50);not null;comment:任务类型" json:"type"`
|
||||
ReqKey string `gorm:"column:req_key;type:varchar(100);comment:请求Key" json:"req_key"`
|
||||
Prompt string `gorm:"column:prompt;type:text;comment:提示词" json:"prompt"`
|
||||
TaskParams string `gorm:"column:task_params;type:text;comment:任务参数JSON" json:"task_params"`
|
||||
ImgURL string `gorm:"column:img_url;type:varchar(1024);comment:图片或封面URL" json:"img_url"`
|
||||
VideoURL string `gorm:"column:video_url;type:varchar(1024);comment:视频URL" json:"video_url"`
|
||||
RawData string `gorm:"column:raw_data;type:text;comment:原始API响应" json:"raw_data"`
|
||||
Progress int `gorm:"column:progress;type:int;default:0;comment:进度百分比" json:"progress"`
|
||||
Status JMTaskStatus `gorm:"column:status;type:varchar(20);default:'pending';comment:任务状态" json:"status"`
|
||||
ErrMsg string `gorm:"column:err_msg;type:varchar(1024);comment:错误信息" json:"err_msg"`
|
||||
Power int `gorm:"column:power;type:int(11);default:0;comment:消耗算力" json:"power"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null;comment:创建时间" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:datetime;not null;comment:更新时间" json:"updated_at"`
|
||||
}
|
||||
|
||||
// JMTaskStatus 任务状态
|
||||
type JMTaskStatus string
|
||||
|
||||
const (
|
||||
JMTaskStatusInQueue = JMTaskStatus("in_queue") // 任务已提交
|
||||
JMTaskStatusGenerating = JMTaskStatus("generating") // 任务处理中
|
||||
JMTaskStatusDone = JMTaskStatus("done") // 处理完成
|
||||
JMTaskStatusNotFound = JMTaskStatus("not_found") // 任务未找到
|
||||
JMTaskStatusSuccess = JMTaskStatus("success") // 任务成功
|
||||
JMTaskStatusFailed = JMTaskStatus("failed") // 任务失败
|
||||
JMTaskStatusExpired = JMTaskStatus("expired") // 任务过期
|
||||
)
|
||||
|
||||
// JMTaskType 任务类型
|
||||
type JMTaskType string
|
||||
|
||||
const (
|
||||
JMTaskTypeTextToImage = JMTaskType("text_to_image") // 文生图
|
||||
JMTaskTypeImageToImage = JMTaskType("image_to_image") // 图生图
|
||||
JMTaskTypeImageEdit = JMTaskType("image_edit") // 图像编辑
|
||||
JMTaskTypeImageEffects = JMTaskType("image_effects") // 图像特效
|
||||
JMTaskTypeTextToVideo = JMTaskType("text_to_video") // 文生视频
|
||||
JMTaskTypeImageToVideo = JMTaskType("image_to_video") // 图生视频
|
||||
)
|
||||
|
||||
// TableName 返回数据表名称
|
||||
func (JimengJob) TableName() string {
|
||||
return "chatgpt_jimeng_jobs"
|
||||
}
|
||||
@@ -2,10 +2,14 @@ package model
|
||||
|
||||
// Menu 系统菜单
|
||||
type Menu struct {
|
||||
Id uint `gorm:"primarykey;column:id"`
|
||||
Name string // 菜单名称
|
||||
Icon string // 菜单图标
|
||||
URL string // 菜单跳转地址
|
||||
SortNum int // 排序
|
||||
Enabled bool // 启用状态
|
||||
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
Name string `gorm:"column:name;type:varchar(30);not null;comment:菜单名称" json:"name"`
|
||||
Icon string `gorm:"column:icon;type:varchar(150);not null;comment:菜单图标" json:"icon"`
|
||||
URL string `gorm:"column:url;type:varchar(100);not null;comment:地址" json:"url"`
|
||||
SortNum int `gorm:"column:sort_num;type:smallint;not null;comment:排序" json:"sort_num"`
|
||||
Enabled bool `gorm:"column:enabled;type:tinyint(1);not null;comment:是否启用" json:"enabled"`
|
||||
}
|
||||
|
||||
func (m *Menu) TableName() string {
|
||||
return "chatgpt_menus"
|
||||
}
|
||||
|
||||
@@ -3,26 +3,26 @@ package model
|
||||
import "time"
|
||||
|
||||
type MidJourneyJob struct {
|
||||
Id uint `gorm:"primarykey;column:id"`
|
||||
Type string
|
||||
UserId int
|
||||
TaskId string
|
||||
TaskInfo string // 原始任务信息
|
||||
ChannelId string
|
||||
MessageId string
|
||||
ReferenceId string
|
||||
ImgURL string
|
||||
OrgURL string // 原图地址
|
||||
Hash string // message hash
|
||||
Progress int
|
||||
Prompt string
|
||||
UseProxy bool // 是否使用反代加载图片
|
||||
Publish bool //是否发布图片到画廊
|
||||
ErrMsg string // 报错信息
|
||||
Power int // 消耗算力
|
||||
CreatedAt time.Time
|
||||
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
UserId uint `gorm:"column:user_id;type:int;not null;comment:用户 ID" json:"user_id"`
|
||||
TaskId string `gorm:"column:task_id;type:varchar(20);uniqueIndex;comment:任务 ID" json:"task_id"`
|
||||
TaskInfo string `gorm:"column:task_info;type:text;not null;comment:任务详情" json:"task_info"`
|
||||
Type string `gorm:"column:type;type:varchar(20);default:image;comment:任务类别" json:"type"`
|
||||
MessageId string `gorm:"column:message_id;type:char(40);not null;index;comment:消息 ID" json:"message_id"`
|
||||
ChannelId string `gorm:"column:channel_id;type:varchar(100);comment:频道ID" json:"channel_id"`
|
||||
RefId string `gorm:"column:reference_id;type:char(40);comment:引用消息 ID" json:"reference_id"`
|
||||
Prompt string `gorm:"column:prompt;type:text;not null;comment:会话提示词" json:"prompt"`
|
||||
ImgURL string `gorm:"column:img_url;type:varchar(400);comment:图片URL" json:"img_url"`
|
||||
OrgURL string `gorm:"column:org_url;type:varchar(400);comment:原始图片地址" json:"org_url"`
|
||||
Hash string `gorm:"column:hash;type:varchar(100);comment:message hash" json:"hash"`
|
||||
Progress int `gorm:"column:progress;type:smallint;default:0;comment:任务进度" json:"progress"`
|
||||
UseProxy int `gorm:"column:use_proxy;type:tinyint(1);not null;default:0;comment:是否使用反代" json:"use_proxy"`
|
||||
Publish int `gorm:"column:publish;type:tinyint(1);not null;comment:是否发布" json:"publish"`
|
||||
ErrMsg string `gorm:"column:err_msg;type:varchar(1024);comment:错误信息" json:"err_msg"`
|
||||
Power int `gorm:"column:power;type:smallint;not null;default:0;comment:消耗算力" json:"power"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"`
|
||||
}
|
||||
|
||||
func (MidJourneyJob) TableName() string {
|
||||
func (m *MidJourneyJob) TableName() string {
|
||||
return "chatgpt_mj_jobs"
|
||||
}
|
||||
|
||||
@@ -2,21 +2,28 @@ package model
|
||||
|
||||
import (
|
||||
"geekai/core/types"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Order 充值订单
|
||||
type Order struct {
|
||||
BaseModel
|
||||
UserId uint
|
||||
ProductId uint
|
||||
Username string
|
||||
OrderNo string
|
||||
TradeNo string
|
||||
Subject string
|
||||
Amount float64
|
||||
Status types.OrderStatus
|
||||
Remark string
|
||||
PayTime int64
|
||||
PayWay string // 支付渠道
|
||||
PayType string // 支付类型
|
||||
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
UserId uint `gorm:"column:user_id;type:int;not null;comment:用户ID" json:"user_id"`
|
||||
ProductId uint `gorm:"column:product_id;type:int;not null;comment:产品ID" json:"product_id"`
|
||||
Username string `gorm:"column:username;type:varchar(30);not null;comment:用户名" json:"username"`
|
||||
OrderNo string `gorm:"column:order_no;type:varchar(30);uniqueIndex;not null;comment:订单ID" json:"order_no"`
|
||||
TradeNo string `gorm:"column:trade_no;type:varchar(60);comment:支付平台交易流水号" json:"trade_no"`
|
||||
Subject string `gorm:"column:subject;type:varchar(100);not null;comment:订单产品" json:"subject"`
|
||||
Amount float64 `gorm:"column:amount;type:decimal(10,2);not null;default:0.00;comment:订单金额" json:"amount"`
|
||||
Status types.OrderStatus `gorm:"column:status;type:tinyint(1);not null;default:0;comment:订单状态(0:待支付,1:已扫码,2:支付成功)" json:"status"`
|
||||
Remark string `gorm:"column:remark;type:varchar(255);not null;comment:备注" json:"remark"`
|
||||
PayTime int64 `gorm:"column:pay_time;type:int;comment:支付时间" json:"pay_time"`
|
||||
PayWay string `gorm:"column:pay_way;type:varchar(20);not null;comment:支付方式" json:"pay_way"`
|
||||
PayType string `gorm:"column:pay_type;type:varchar(30);not null;comment:支付类型" json:"pay_type"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:datetime;not null" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (m *Order) TableName() string {
|
||||
return "chatgpt_orders"
|
||||
}
|
||||
|
||||
@@ -7,14 +7,18 @@ import (
|
||||
|
||||
// PowerLog 算力消费日志
|
||||
type PowerLog struct {
|
||||
Id uint `gorm:"primarykey;column:id"`
|
||||
UserId uint
|
||||
Username string
|
||||
Type types.PowerType
|
||||
Amount int
|
||||
Balance int
|
||||
Model string // 模型
|
||||
Remark string // 备注
|
||||
Mark types.PowerMark // 资金类型
|
||||
CreatedAt time.Time
|
||||
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
UserId uint `gorm:"column:user_id;type:int;not null;comment:用户ID" json:"user_id"`
|
||||
Username string `gorm:"column:username;type:varchar(30);not null;comment:用户名" json:"username"`
|
||||
Type types.PowerType `gorm:"column:type;type:tinyint(1);not null;comment:类型(1:充值,2:消费,3:退费)" json:"type"`
|
||||
Amount int `gorm:"column:amount;type:smallint;not null;comment:算力数值" json:"amount"`
|
||||
Balance int `gorm:"column:balance;type:int;not null;comment:余额" json:"balance"`
|
||||
Model string `gorm:"column:model;type:varchar(30);not null;comment:模型" json:"model"`
|
||||
Remark string `gorm:"column:remark;type:varchar(512);not null;comment:备注" json:"remark"`
|
||||
Mark types.PowerMark `gorm:"column:mark;type:tinyint(1);not null;comment:资金类型(0:支出,1:收入)" json:"mark"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null;comment:创建时间" json:"created_at"`
|
||||
}
|
||||
|
||||
func (m *PowerLog) TableName() string {
|
||||
return "chatgpt_power_logs"
|
||||
}
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Product 充值产品
|
||||
type Product struct {
|
||||
BaseModel
|
||||
Name string
|
||||
Price float64
|
||||
Discount float64
|
||||
Days int
|
||||
Power int
|
||||
Enabled bool
|
||||
Sales int
|
||||
SortNum int
|
||||
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
Name string `gorm:"column:name;type:varchar(30);not null;comment:名称" json:"name"`
|
||||
Price float64 `gorm:"column:price;type:decimal(10,2);not null;default:0.00;comment:价格" json:"price"`
|
||||
Discount float64 `gorm:"column:discount;type:decimal(10,2);not null;default:0.00;comment:优惠金额" json:"discount"`
|
||||
Days int `gorm:"column:days;type:smallint;not null;default:0;comment:延长天数" json:"days"`
|
||||
Power int `gorm:"column:power;type:int;not null;default:0;comment:增加算力值" json:"power"`
|
||||
Enabled bool `gorm:"column:enabled;type:tinyint(1);not null;default:0;comment:是否启动" json:"enabled"`
|
||||
Sales int `gorm:"column:sales;type:int;not null;default:0;comment:销量" json:"sales"`
|
||||
SortNum int `gorm:"column:sort_num;type:tinyint;not null;default:0;comment:排序" json:"sort_num"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:datetime;not null" json:"updated_at"`
|
||||
AppUrl string `gorm:"column:app_url;type:varchar(255);comment:App跳转地址" json:"app_url"`
|
||||
Url string `gorm:"column:url;type:varchar(255);comment:跳转地址" json:"url"`
|
||||
}
|
||||
|
||||
func (m *Product) TableName() string {
|
||||
return "chatgpt_products"
|
||||
}
|
||||
|
||||
@@ -5,12 +5,16 @@ import "time"
|
||||
// 兑换码
|
||||
|
||||
type Redeem struct {
|
||||
Id uint `gorm:"primarykey;column:id"`
|
||||
UserId uint // 用户 ID
|
||||
Name string // 名称
|
||||
Power int // 算力
|
||||
Code string // 兑换码
|
||||
Enabled bool // 启用状态
|
||||
RedeemedAt int64 // 兑换时间
|
||||
CreatedAt time.Time
|
||||
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
UserId uint `gorm:"column:user_id;type:int;not null;comment:用户 ID" json:"user_id"`
|
||||
Name string `gorm:"column:name;type:varchar(30);not null;comment:兑换码名称" json:"name"`
|
||||
Power int `gorm:"column:power;type:int;not null;comment:算力" json:"power"`
|
||||
Code string `gorm:"column:code;type:varchar(100);uniqueIndex;not null;comment:兑换码" json:"code"`
|
||||
Enabled bool `gorm:"column:enabled;type:tinyint(1);not null;comment:是否启用" json:"enabled"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"`
|
||||
RedeemedAt int64 `gorm:"column:redeemed_at;type:int;not null;comment:兑换时间" json:"redeemed_at"`
|
||||
}
|
||||
|
||||
func (m *Redeem) TableName() string {
|
||||
return "chatgpt_redeems"
|
||||
}
|
||||
|
||||
@@ -3,21 +3,21 @@ package model
|
||||
import "time"
|
||||
|
||||
type SdJob struct {
|
||||
Id uint `gorm:"primarykey;column:id"`
|
||||
Type string
|
||||
UserId int
|
||||
TaskId string
|
||||
TaskInfo string // 原始任务信息
|
||||
ImgURL string
|
||||
Progress int
|
||||
Prompt string
|
||||
Params string
|
||||
Publish bool //是否发布图片到画廊
|
||||
ErrMsg string // 报错信息
|
||||
Power int // 消耗算力
|
||||
CreatedAt time.Time
|
||||
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
UserId uint `gorm:"column:user_id;type:int;not null;comment:用户 ID" json:"user_id"`
|
||||
Type string `gorm:"column:type;type:varchar(20);default:txt2img;comment:任务类别" json:"type"`
|
||||
TaskId string `gorm:"column:task_id;type:char(30);uniqueIndex;not null;comment:任务 ID" json:"task_id"`
|
||||
TaskInfo string `gorm:"column:task_info;type:text;not null;comment:任务详情" json:"task_info"`
|
||||
Prompt string `gorm:"column:prompt;type:text;not null;comment:会话提示词" json:"prompt"`
|
||||
ImgURL string `gorm:"column:img_url;type:varchar(255);comment:图片URL" json:"img_url"`
|
||||
Params string `gorm:"column:params;type:text;comment:绘画参数json" json:"params"`
|
||||
Progress int `gorm:"column:progress;type:smallint;default:0;comment:任务进度" json:"progress"`
|
||||
Publish int `gorm:"column:publish;type:tinyint(1);not null;comment:是否发布" json:"publish"`
|
||||
ErrMsg string `gorm:"column:err_msg;type:varchar(1024);comment:错误信息" json:"err_msg"`
|
||||
Power int `gorm:"column:power;type:smallint;not null;default:0;comment:消耗算力" json:"power"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"`
|
||||
}
|
||||
|
||||
func (SdJob) TableName() string {
|
||||
func (m *SdJob) TableName() string {
|
||||
return "chatgpt_sd_jobs"
|
||||
}
|
||||
|
||||
@@ -3,33 +3,33 @@ package model
|
||||
import "time"
|
||||
|
||||
type SunoJob struct {
|
||||
Id uint `gorm:"primarykey;column:id"`
|
||||
UserId int
|
||||
Channel string // 频道
|
||||
Title string
|
||||
Type int
|
||||
TaskId string
|
||||
TaskInfo string // 原始任务信息
|
||||
RefTaskId string // 续写的任务id
|
||||
Tags string // 歌曲风格和标签
|
||||
Instrumental bool // 是否生成纯音乐
|
||||
ExtendSecs int // 续写秒数
|
||||
SongId string // 续写的歌曲id
|
||||
RefSongId string
|
||||
Prompt string // 提示词
|
||||
CoverURL string // 封面图 URL
|
||||
AudioURL string // 音频 URL
|
||||
ModelName string // 模型名称
|
||||
Progress int // 任务进度
|
||||
Duration int // 银屏时长,秒
|
||||
Publish bool // 是否发布
|
||||
ErrMsg string // 错误信息
|
||||
RawData string // 原始数据 json
|
||||
Power int // 消耗算力
|
||||
PlayTimes int // 播放次数
|
||||
CreatedAt time.Time
|
||||
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
UserId uint `gorm:"column:user_id;type:int;not null;comment:用户 ID" json:"user_id"`
|
||||
Channel string `gorm:"column:channel;type:varchar(100);not null;comment:渠道" json:"channel"`
|
||||
Title string `gorm:"column:title;type:varchar(100);comment:歌曲标题" json:"title"`
|
||||
Type int `gorm:"column:type;type:tinyint(1);default:0;comment:任务类型,1:灵感创作,2:自定义创作" json:"type"`
|
||||
TaskId string `gorm:"column:task_id;type:varchar(50);comment:任务 ID" json:"task_id"`
|
||||
TaskInfo string `gorm:"column:task_info;type:text;not null;comment:任务详情" json:"task_info"`
|
||||
RefTaskId string `gorm:"column:ref_task_id;type:char(50);comment:引用任务 ID" json:"ref_task_id"`
|
||||
Tags string `gorm:"column:tags;type:varchar(100);comment:歌曲风格" json:"tags"`
|
||||
Instrumental bool `gorm:"column:instrumental;type:tinyint(1);default:0;comment:是否为纯音乐" json:"instrumental"`
|
||||
ExtendSecs int `gorm:"column:extend_secs;type:smallint;default:0;comment:延长秒数" json:"extend_secs"`
|
||||
SongId string `gorm:"column:song_id;type:varchar(50);comment:要续写的歌曲 ID" json:"song_id"`
|
||||
RefSongId string `gorm:"column:ref_song_id;type:varchar(50);not null;comment:引用的歌曲ID" json:"ref_song_id"`
|
||||
Prompt string `gorm:"column:prompt;type:varchar(2000);not null;comment:提示词" json:"prompt"`
|
||||
CoverURL string `gorm:"column:cover_url;type:varchar(512);comment:封面图地址" json:"cover_url"`
|
||||
AudioURL string `gorm:"column:audio_url;type:varchar(512);comment:音频地址" json:"audio_url"`
|
||||
ModelName string `gorm:"column:model_name;type:varchar(30);comment:模型地址" json:"model_name"`
|
||||
Progress int `gorm:"column:progress;type:smallint;default:0;comment:任务进度" json:"progress"`
|
||||
Duration int `gorm:"column:duration;type:smallint;not null;default:0;comment:歌曲时长" json:"duration"`
|
||||
Publish int `gorm:"column:publish;type:tinyint(1);not null;comment:是否发布" json:"publish"`
|
||||
ErrMsg string `gorm:"column:err_msg;type:varchar(1024);comment:错误信息" json:"err_msg"`
|
||||
RawData string `gorm:"column:raw_data;type:text;comment:原始数据" json:"raw_data"`
|
||||
Power int `gorm:"column:power;type:smallint;not null;default:0;comment:消耗算力" json:"power"`
|
||||
PlayTimes int `gorm:"column:play_times;type:int;comment:播放次数" json:"play_times"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"`
|
||||
}
|
||||
|
||||
func (SunoJob) TableName() string {
|
||||
func (m *SunoJob) TableName() string {
|
||||
return "chatgpt_suno_jobs"
|
||||
}
|
||||
|
||||
@@ -1,23 +1,33 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
BaseModel
|
||||
Username string
|
||||
Nickname string
|
||||
Email string
|
||||
Mobile string
|
||||
Password string
|
||||
Avatar string
|
||||
Salt string // 密码盐
|
||||
Power int // 剩余算力
|
||||
ChatConfig string `gorm:"column:chat_config_json"` // 聊天配置 json
|
||||
ChatRoles string `gorm:"column:chat_roles_json"` // 聊天角色
|
||||
ChatModels string `gorm:"column:chat_models_json"` // AI 模型,不同的用户拥有不同的聊天模型
|
||||
ExpiredTime int64 // 账户到期时间
|
||||
Status bool `gorm:"default:true"` // 当前状态
|
||||
LastLoginAt int64 // 最后登录时间
|
||||
LastLoginIp string // 最后登录 IP
|
||||
OpenId string `gorm:"column:openid"`
|
||||
Platform string `json:"platform"`
|
||||
Vip bool // 是否 VIP 会员
|
||||
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
Username string `gorm:"column:username;type:varchar(30);uniqueIndex;not null;comment:用户名" json:"username"`
|
||||
Mobile string `gorm:"column:mobile;type:char(11);comment:手机号" json:"mobile"`
|
||||
Email string `gorm:"column:email;type:varchar(50);comment:邮箱地址" json:"email"`
|
||||
Nickname string `gorm:"column:nickname;type:varchar(30);not null;comment:昵称" json:"nickname"`
|
||||
Password string `gorm:"column:password;type:char(64);not null;comment:密码" json:"password"`
|
||||
Avatar string `gorm:"column:avatar;type:varchar(255);not null;comment:头像" json:"avatar"`
|
||||
Salt string `gorm:"column:salt;type:char(12);not null;comment:密码盐" json:"salt"`
|
||||
Power int `gorm:"column:power;type:int;default:0;comment:剩余算力" json:"power"`
|
||||
ExpiredTime int64 `gorm:"column:expired_time;type:int;not null;comment:用户过期时间" json:"expired_time"`
|
||||
Status bool `gorm:"column:status;type:tinyint(1);not null;comment:当前状态" json:"status"`
|
||||
ChatConfig string `gorm:"column:chat_config_json;type:text;default:null;comment:聊天配置json" json:"chat_config"`
|
||||
ChatRoles string `gorm:"column:chat_roles_json;type:text;default:null;comment:聊天角色 json" json:"chat_roles"`
|
||||
ChatModels string `gorm:"column:chat_models_json;type:text;default:null;comment:AI模型 json" json:"chat_models"`
|
||||
LastLoginAt int64 `gorm:"column:last_login_at;type:int;not null;comment:最后登录时间" json:"last_login_at"`
|
||||
Vip bool `gorm:"column:vip;type:tinyint(1);not null;default:0;comment:是否会员" json:"vip"`
|
||||
LastLoginIp string `gorm:"column:last_login_ip;type:char(16);not null;comment:最后登录 IP" json:"last_login_ip"`
|
||||
OpenId string `gorm:"column:openid;type:varchar(100);comment:第三方登录账号ID" json:"openid"`
|
||||
Platform string `gorm:"column:platform;type:varchar(30);comment:登录平台" json:"platform"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:datetime;not null" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (m *User) TableName() string {
|
||||
return "chatgpt_users"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type UserLoginLog struct {
|
||||
BaseModel
|
||||
UserId uint
|
||||
Username string
|
||||
LoginIp string
|
||||
LoginAddress string
|
||||
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
UserId uint `gorm:"column:user_id;type:int;not null;comment:用户ID" json:"user_id"`
|
||||
Username string `gorm:"column:username;type:varchar(30);not null;comment:用户名" json:"username"`
|
||||
LoginIp string `gorm:"column:login_ip;type:char(16);not null;comment:登录IP" json:"login_ip"`
|
||||
LoginAddress string `gorm:"column:login_address;type:varchar(30);not null;comment:登录地址" json:"login_address"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:datetime;not null" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (m *UserLoginLog) TableName() string {
|
||||
return "chatgpt_user_login_logs"
|
||||
}
|
||||
|
||||
@@ -3,25 +3,25 @@ package model
|
||||
import "time"
|
||||
|
||||
type VideoJob struct {
|
||||
Id uint `gorm:"primarykey;column:id"`
|
||||
UserId int
|
||||
Channel string // 频道
|
||||
Type string // luma,runway,cog
|
||||
TaskId string
|
||||
TaskInfo string // 原始任务信息
|
||||
Prompt string // 提示词
|
||||
PromptExt string // 优化后提示词
|
||||
CoverURL string // 封面图 URL
|
||||
VideoURL string // 无水印视频 URL
|
||||
WaterURL string // 有水印视频 URL
|
||||
Progress int // 任务进度
|
||||
Publish bool // 是否发布
|
||||
ErrMsg string // 错误信息
|
||||
RawData string // 原始数据 json
|
||||
Power int // 消耗算力
|
||||
CreatedAt time.Time
|
||||
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
UserId uint `gorm:"column:user_id;type:int;not null;comment:用户 ID" json:"user_id"`
|
||||
Channel string `gorm:"column:channel;type:varchar(100);not null;comment:渠道" json:"channel"`
|
||||
TaskId string `gorm:"column:task_id;type:varchar(100);not null;comment:任务 ID" json:"task_id"`
|
||||
TaskInfo string `gorm:"column:task_info;type:text;comment:原始任务信息" json:"task_info"`
|
||||
Type string `gorm:"column:type;type:varchar(20);comment:任务类型,luma,runway,cogvideo" json:"type"`
|
||||
Prompt string `gorm:"column:prompt;type:text;not null;comment:提示词" json:"prompt"`
|
||||
PromptExt string `gorm:"column:prompt_ext;type:text;comment:优化后提示词" json:"prompt_ext"`
|
||||
CoverURL string `gorm:"column:cover_url;type:varchar(512);comment:封面图地址" json:"cover_url"`
|
||||
VideoURL string `gorm:"column:video_url;type:varchar(512);comment:视频地址" json:"video_url"`
|
||||
WaterURL string `gorm:"column:water_url;type:varchar(512);comment:带水印的视频地址" json:"water_url"`
|
||||
Progress int `gorm:"column:progress;type:smallint;default:0;comment:任务进度" json:"progress"`
|
||||
Publish int `gorm:"column:publish;type:tinyint(1);not null;comment:是否发布" json:"publish"`
|
||||
ErrMsg string `gorm:"column:err_msg;type:varchar(1024);comment:错误信息" json:"err_msg"`
|
||||
RawData string `gorm:"column:raw_data;type:text;comment:原始数据" json:"raw_data"`
|
||||
Power int `gorm:"column:power;type:smallint;not null;default:0;comment:消耗算力" json:"power"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"`
|
||||
}
|
||||
|
||||
func (VideoJob) TableName() string {
|
||||
func (m *VideoJob) TableName() string {
|
||||
return "chatgpt_video_jobs"
|
||||
}
|
||||
|
||||
@@ -9,16 +9,17 @@ package store
|
||||
|
||||
import (
|
||||
"geekai/core/types"
|
||||
"time"
|
||||
|
||||
"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.Silent),
|
||||
Logger: logger.Default.LogMode(logger.Warn),
|
||||
NamingStrategy: schema.NamingStrategy{
|
||||
TablePrefix: "chatgpt_", // 设置表前缀
|
||||
SingularTable: false, // 使用单数表名形式
|
||||
@@ -36,9 +37,9 @@ func NewMysql(config *gorm.Config, appConfig *types.AppConfig) (*gorm.DB, error)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sqlDB.SetMaxIdleConns(32)
|
||||
sqlDB.SetMaxOpenConns(512)
|
||||
sqlDB.SetConnMaxLifetime(time.Hour)
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ package store
|
||||
import (
|
||||
"context"
|
||||
"geekai/utils"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
)
|
||||
|
||||
@@ -23,15 +24,15 @@ 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) RPush(value any) error {
|
||||
return q.client.RPush(q.ctx, q.name, utils.JsonEncode(value)).Err()
|
||||
}
|
||||
|
||||
func (q *RedisQueue) LPush(value interface{}) {
|
||||
q.client.LPush(q.ctx, q.name, utils.JsonEncode(value))
|
||||
func (q *RedisQueue) LPush(value any) error {
|
||||
return q.client.LPush(q.ctx, q.name, utils.JsonEncode(value)).Err()
|
||||
}
|
||||
|
||||
func (q *RedisQueue) LPop(value interface{}) error {
|
||||
func (q *RedisQueue) LPop(value any) error {
|
||||
result, err := q.client.BLPop(q.ctx, 0, q.name).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -39,10 +40,18 @@ func (q *RedisQueue) LPop(value interface{}) error {
|
||||
return utils.JsonDecode(result[1], value)
|
||||
}
|
||||
|
||||
func (q *RedisQueue) RPop(value interface{}) error {
|
||||
func (q *RedisQueue) RPop(value any) error {
|
||||
result, err := q.client.BRPop(q.ctx, 0, q.name).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return utils.JsonDecode(result[1], value)
|
||||
}
|
||||
|
||||
func (q *RedisQueue) Size() (int64, error) {
|
||||
return q.client.LLen(q.ctx, q.name).Result()
|
||||
}
|
||||
|
||||
func (q *RedisQueue) Clear() error {
|
||||
return q.client.Del(q.ctx, q.name).Err()
|
||||
}
|
||||
|
||||
@@ -2,9 +2,8 @@ package vo
|
||||
|
||||
type AdminUser struct {
|
||||
BaseVo
|
||||
Username string `json:"username"`
|
||||
Status bool `json:"status"` // 当前状态
|
||||
LastLoginAt int64 `json:"last_login_at"` // 最后登录时间
|
||||
LastLoginIp string `json:"last_login_ip"` // 最后登录 IP
|
||||
RoleIds interface{} `json:"role_ids"` //角色ids
|
||||
Username string `json:"username"`
|
||||
Status bool `json:"status"` // 当前状态
|
||||
LastLoginAt int64 `json:"last_login_at"` // 最后登录时间
|
||||
LastLoginIp string `json:"last_login_ip"` // 最后登录 IP
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
package vo
|
||||
|
||||
type HistoryMessage struct {
|
||||
BaseVo
|
||||
ChatId string `json:"chat_id"`
|
||||
UserId uint `json:"user_id"`
|
||||
RoleId uint `json:"role_id"`
|
||||
Model string `json:"model"`
|
||||
Type string `json:"type"`
|
||||
Icon string `json:"icon"`
|
||||
Tokens int `json:"tokens"`
|
||||
Content string `json:"content"`
|
||||
UseContext bool `json:"use_context"`
|
||||
}
|
||||
21
api/store/vo/chat_message.go
Normal file
21
api/store/vo/chat_message.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package vo
|
||||
|
||||
type MsgContent struct {
|
||||
Text string `json:"text"`
|
||||
Files []File `json:"files"`
|
||||
}
|
||||
|
||||
type ChatMessage struct {
|
||||
Id uint `json:"id"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
ChatId string `json:"chat_id"`
|
||||
UserId uint `json:"user_id"`
|
||||
RoleId uint `json:"role_id"`
|
||||
Model string `json:"model"`
|
||||
Type string `json:"type"`
|
||||
Icon string `json:"icon"`
|
||||
Tokens int `json:"tokens"`
|
||||
Content MsgContent `json:"content"`
|
||||
UseContext bool `json:"use_context"`
|
||||
}
|
||||
@@ -10,8 +10,10 @@ type ChatModel struct {
|
||||
Open bool `json:"open"`
|
||||
MaxTokens int `json:"max_tokens"` // 最大响应长度
|
||||
MaxContext int `json:"max_context"` // 最大上下文长度
|
||||
Desc string `json:"desc"` // 模型描述
|
||||
Tag string `json:"tag"` //模型标签
|
||||
Temperature float32 `json:"temperature"` // 模型温度
|
||||
KeyId int `json:"key_id,omitempty"`
|
||||
KeyId uint `json:"key_id,omitempty"`
|
||||
KeyName string `json:"key_name"`
|
||||
Options map[string]string `json:"options"`
|
||||
Type string `json:"type"`
|
||||
|
||||
@@ -5,14 +5,14 @@ import "geekai/core/types"
|
||||
type ChatRole struct {
|
||||
BaseVo
|
||||
Key string `json:"key"` // 角色唯一标识
|
||||
Tid int `json:"tid"`
|
||||
Tid uint `json:"tid"`
|
||||
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"` // 排序
|
||||
ModelId int `json:"model_id"` // 绑定模型 ID
|
||||
ModelId uint `json:"model_id"` // 绑定模型 ID
|
||||
ModelName string `json:"model_name"` // 模型名称
|
||||
TypeName string `json:"type_name"` // 分类名称
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package vo
|
||||
|
||||
import "geekai/core/types"
|
||||
|
||||
type Config struct {
|
||||
Id uint `json:"id"`
|
||||
Key string `json:"key"`
|
||||
SystemConfig types.SystemConfig `json:"system_config"`
|
||||
}
|
||||
23
api/store/vo/jimeng_job.go
Normal file
23
api/store/vo/jimeng_job.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package vo
|
||||
|
||||
import "geekai/store/model"
|
||||
|
||||
// JimengJob 即梦AI任务VO
|
||||
type JimengJob struct {
|
||||
Id uint `json:"id"`
|
||||
UserId uint `json:"user_id"`
|
||||
TaskId string `json:"task_id"`
|
||||
Type model.JMTaskType `json:"type"`
|
||||
ReqKey string `json:"req_key"`
|
||||
Prompt string `json:"prompt"`
|
||||
TaskParams string `json:"task_params"`
|
||||
ImgURL string `json:"img_url"`
|
||||
VideoURL string `json:"video_url"`
|
||||
RawData string `json:"raw_data"`
|
||||
Progress int `json:"progress"`
|
||||
Status model.JMTaskStatus `json:"status"`
|
||||
ErrMsg string `json:"err_msg"`
|
||||
Power int `json:"power"`
|
||||
CreatedAt int64 `json:"created_at"` // 时间戳
|
||||
UpdatedAt int64 `json:"updated_at"` // 时间戳
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package vo
|
||||
type MidJourneyJob struct {
|
||||
Id uint `json:"id"`
|
||||
Type string `json:"type"`
|
||||
UserId int `json:"user_id"`
|
||||
UserId uint `json:"user_id"`
|
||||
ChannelId string `json:"channel_id"`
|
||||
TaskId string `json:"task_id"`
|
||||
MessageId string `json:"message_id"`
|
||||
|
||||
@@ -3,14 +3,14 @@ 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"`
|
||||
Items any `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 {
|
||||
func NewPage(total int64, page int, pageSize int, items any) Page {
|
||||
totalPage := math.Ceil(float64(total) / float64(pageSize))
|
||||
return Page{
|
||||
Items: items,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user