完成即梦配置功能页面

This commit is contained in:
RockYang
2025-07-21 07:08:06 +08:00
parent 73d003d6c3
commit 41eb0e634a
14 changed files with 993 additions and 406 deletions

195
JIMENG_CONFIG_README.md Normal file
View 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 格式

View File

@@ -34,6 +34,55 @@ 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/chat/history": false,
"/api/chat/detail": false,
"/api/chat/list": 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/suno/detail": false,
"/api/suno/play": 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 {
Config *types.AppConfig
Engine *gin.Engine
@@ -212,6 +261,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/")
@@ -230,18 +284,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 {
@@ -252,21 +301,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
@@ -276,57 +325,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) {

View File

@@ -49,12 +49,6 @@ type ApiConfig struct {
JimengConfig JimengConfig // 即梦AI配置
}
// JimengConfig 即梦AI配置
type JimengConfig struct {
AccessKey string // 火山引擎AccessKey
SecretKey string // 火山引擎SecretKey
}
type AlipayConfig struct {
Enabled bool // 是否启用该支付通道
SandBox bool // 是否沙盒环境

18
api/core/types/jimeng.go Normal file
View 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"`
}

View File

@@ -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

View File

@@ -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,12 +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/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=
@@ -131,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=
@@ -183,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=
@@ -212,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=
@@ -234,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=
@@ -279,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=
@@ -298,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=
@@ -342,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=
@@ -364,6 +425,7 @@ 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=
@@ -376,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=

View File

@@ -4,12 +4,15 @@ import (
"strconv"
"geekai/core"
"geekai/core/types"
"geekai/handler"
"geekai/service/jimeng"
"geekai/store/model"
"geekai/utils"
"geekai/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// AdminJimengHandler 管理后台即梦AI处理器
@@ -19,13 +22,25 @@ type AdminJimengHandler struct {
}
// NewAdminJimengHandler 创建管理后台即梦AI处理器
func NewAdminJimengHandler(app *core.AppServer, jimengService *jimeng.Service) *AdminJimengHandler {
func NewAdminJimengHandler(app *core.AppServer, db *gorm.DB, jimengService *jimeng.Service) *AdminJimengHandler {
return &AdminJimengHandler{
BaseHandler: handler.BaseHandler{App: app},
BaseHandler: handler.BaseHandler{App: app, DB: db},
jimengService: jimengService,
}
}
// 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.DELETE("/jobs/:id", h.Remove)
rg.POST("/jobs/batch-remove", h.BatchRemove)
rg.GET("/stats", h.Stats)
rg.GET("/config", h.GetConfig)
rg.POST("/config", h.UpdateConfig)
}
// Jobs 获取任务列表
func (h *AdminJimengHandler) Jobs(c *gin.Context) {
page := h.GetInt(c, "page", 1)
@@ -174,4 +189,115 @@ func (h *AdminJimengHandler) Stats(c *gin.Context) {
}
resp.SUCCESS(c, result)
}
}
// GetConfig 获取即梦AI配置
func (h *AdminJimengHandler) GetConfig(c *gin.Context) {
var config model.Config
err := h.DB.Debug().Where("name", "jimeng").First(&config).Error
if err != nil {
// 如果配置不存在,返回默认配置
defaultConfig := types.JimengConfig{
AccessKey: "",
SecretKey: "",
Power: types.JimengPower{
TextToImage: 10,
ImageToImage: 15,
ImageEdit: 20,
ImageEffects: 25,
TextToVideo: 30,
ImageToVideo: 35,
},
}
resp.SUCCESS(c, defaultConfig)
return
}
var jimengConfig types.JimengConfig
err = utils.JsonDecode(config.Value, &jimengConfig)
if err != nil {
resp.ERROR(c, "解析配置失败: "+err.Error())
return
}
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
}
testErr := h.jimengService.TestConnection(req.AccessKey, req.SecretKey)
if testErr != nil {
resp.ERROR(c, "连接测试失败: "+testErr.Error())
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
}
// 保存配置
value := utils.JsonEncode(&req)
config := model.Config{Name: "jimeng", Value: value}
err := h.DB.FirstOrCreate(&config, model.Config{Name: "jimeng"}).Error
if err != nil {
resp.ERROR(c, "保存配置失败: "+err.Error())
return
}
if config.Id > 0 {
config.Value = value
err = h.DB.Updates(&config).Error
if err != nil {
resp.ERROR(c, "更新配置失败: "+err.Error())
return
}
}
// 更新服务中的客户端配置
updateErr := h.jimengService.UpdateClientConfig(req.AccessKey, req.SecretKey)
if updateErr != nil {
// 配置已保存,但客户端更新失败,记录日志但不返回错误
logger.Errorf("更新即梦AI客户端配置失败: %v", updateErr)
}
resp.SUCCESS(c, gin.H{"message": "配置更新成功"})
}

View File

@@ -154,7 +154,6 @@ func (h *MediaHandler) Remove(c *gin.Context) {
remark = fmt.Sprintf("SUNO 任务失败退回算力。任务ID%dErr: %s", job.Id, job.ErrMsg)
progress = job.Progress
fileURL = job.AudioURL
break
case "luma":
case "keling":
var job model.VideoJob
@@ -174,7 +173,6 @@ func (h *MediaHandler) Remove(c *gin.Context) {
if fileURL == "" {
fileURL = job.WaterURL
}
break
default:
resp.ERROR(c, types.InvalidArgs)
return

View File

@@ -28,6 +28,20 @@ func NewJimengHandler(app *core.AppServer, jimengService *jimeng.Service) *Jimen
}
}
func (h *JimengHandler) RegisterRoutes() {
rg := h.App.Engine.Group("/api/jimeng")
rg.POST("text-to-image", h.TextToImage)
rg.POST("image-to-image-portrait", h.ImageToImagePortrait)
rg.POST("image-edit", h.ImageEdit)
rg.POST("image-effects", h.ImageEffects)
rg.POST("text-to-video", h.TextToVideo)
rg.POST("image-to-video", h.ImageToVideo)
rg.GET("jobs", h.Jobs)
rg.GET("pending-count", h.PendingCount)
rg.GET("remove", h.Remove)
rg.GET("retry", h.Retry)
}
// TextToImage 文生图
func (h *JimengHandler) TextToImage(c *gin.Context) {
var req struct {
@@ -51,9 +65,12 @@ func (h *JimengHandler) TextToImage(c *gin.Context) {
return
}
// 获取配置中的算力消耗
powerCost := h.getPowerFromConfig(model.JMTaskTypeTextToImage)
// 检查用户算力
if user.Power < 20 { // 文生图消耗20算力
resp.ERROR(c, "算力不足")
if user.Power < powerCost {
resp.ERROR(c, fmt.Sprintf("算力不足,需要%d算力", powerCost))
return
}
@@ -86,7 +103,7 @@ func (h *JimengHandler) TextToImage(c *gin.Context) {
Prompt: req.Prompt,
Params: params,
ReqKey: jimeng.ReqKeyTextToImage,
Power: 20,
Power: powerCost,
}
job, err := h.jimengService.CreateTask(user.Id, taskReq)
@@ -97,7 +114,7 @@ func (h *JimengHandler) TextToImage(c *gin.Context) {
}
// 扣除用户算力
h.subUserPower(user.Id, 20, model.PowerLog{
h.subUserPower(user.Id, powerCost, model.PowerLog{
Type: types.PowerConsume,
Model: "即梦文生图",
Remark: fmt.Sprintf("任务ID%d", job.Id),
@@ -132,9 +149,12 @@ func (h *JimengHandler) ImageToImagePortrait(c *gin.Context) {
return
}
// 获取配置中的算力消耗
powerCost := h.getPowerFromConfig(model.JMTaskTypeImageToImage)
// 检查用户算力
if user.Power < 30 { // 图生图消耗30算力
resp.ERROR(c, "算力不足")
if user.Power < powerCost {
resp.ERROR(c, fmt.Sprintf("算力不足,需要%d算力", powerCost))
return
}
@@ -183,7 +203,7 @@ func (h *JimengHandler) ImageToImagePortrait(c *gin.Context) {
Prompt: req.Prompt,
Params: params,
ReqKey: jimeng.ReqKeyImageToImagePortrait,
Power: 30,
Power: powerCost,
}
job, err := h.jimengService.CreateTask(user.Id, taskReq)
@@ -194,7 +214,7 @@ func (h *JimengHandler) ImageToImagePortrait(c *gin.Context) {
}
// 扣除用户算力
h.subUserPower(user.Id, 30, model.PowerLog{
h.subUserPower(user.Id, powerCost, model.PowerLog{
Type: types.PowerConsume,
Model: "即梦图生图",
Remark: fmt.Sprintf("任务ID%d", job.Id),
@@ -230,9 +250,12 @@ func (h *JimengHandler) ImageEdit(c *gin.Context) {
return
}
// 获取配置中的算力消耗
powerCost := h.getPowerFromConfig(model.JMTaskTypeImageEdit)
// 检查用户算力
if user.Power < 25 { // 图像编辑消耗25算力
resp.ERROR(c, "算力不足")
if user.Power < powerCost {
resp.ERROR(c, fmt.Sprintf("算力不足,需要%d算力", powerCost))
return
}
@@ -262,7 +285,7 @@ func (h *JimengHandler) ImageEdit(c *gin.Context) {
Prompt: req.Prompt,
Params: params,
ReqKey: jimeng.ReqKeyImageEdit,
Power: 25,
Power: powerCost,
}
job, err := h.jimengService.CreateTask(user.Id, taskReq)
@@ -273,7 +296,7 @@ func (h *JimengHandler) ImageEdit(c *gin.Context) {
}
// 扣除用户算力
h.subUserPower(user.Id, 25, model.PowerLog{
h.subUserPower(user.Id, powerCost, model.PowerLog{
Type: types.PowerConsume,
Model: "即梦图像编辑",
Remark: fmt.Sprintf("任务ID%d", job.Id),
@@ -303,9 +326,12 @@ func (h *JimengHandler) ImageEffects(c *gin.Context) {
return
}
// 获取配置中的算力消耗
powerCost := h.getPowerFromConfig(model.JMTaskTypeImageEffects)
// 检查用户算力
if user.Power < 15 { // 图像特效消耗15算力
resp.ERROR(c, "算力不足")
if user.Power < powerCost {
resp.ERROR(c, fmt.Sprintf("算力不足,需要%d算力", powerCost))
return
}
@@ -331,7 +357,7 @@ func (h *JimengHandler) ImageEffects(c *gin.Context) {
Prompt: "",
Params: params,
ReqKey: jimeng.ReqKeyImageEffects,
Power: 15,
Power: powerCost,
}
job, err := h.jimengService.CreateTask(user.Id, taskReq)
@@ -342,7 +368,7 @@ func (h *JimengHandler) ImageEffects(c *gin.Context) {
}
// 扣除用户算力
h.subUserPower(user.Id, 15, model.PowerLog{
h.subUserPower(user.Id, powerCost, model.PowerLog{
Type: types.PowerConsume,
Model: "即梦图像特效",
Remark: fmt.Sprintf("任务ID%d", job.Id),
@@ -371,9 +397,12 @@ func (h *JimengHandler) TextToVideo(c *gin.Context) {
return
}
// 获取配置中的算力消耗
powerCost := h.getPowerFromConfig(model.JMTaskTypeTextToVideo)
// 检查用户算力
if user.Power < 100 { // 文生视频消耗100算力
resp.ERROR(c, "算力不足")
if user.Power < powerCost {
resp.ERROR(c, fmt.Sprintf("算力不足,需要%d算力", powerCost))
return
}
@@ -397,7 +426,7 @@ func (h *JimengHandler) TextToVideo(c *gin.Context) {
Prompt: req.Prompt,
Params: params,
ReqKey: jimeng.ReqKeyTextToVideo,
Power: 100,
Power: powerCost,
}
job, err := h.jimengService.CreateTask(user.Id, taskReq)
@@ -408,7 +437,7 @@ func (h *JimengHandler) TextToVideo(c *gin.Context) {
}
// 扣除用户算力
h.subUserPower(user.Id, 100, model.PowerLog{
h.subUserPower(user.Id, powerCost, model.PowerLog{
Type: types.PowerConsume,
Model: "即梦文生视频",
Remark: fmt.Sprintf("任务ID%d", job.Id),
@@ -444,9 +473,12 @@ func (h *JimengHandler) ImageToVideo(c *gin.Context) {
return
}
// 获取配置中的算力消耗
powerCost := h.getPowerFromConfig(model.JMTaskTypeImageToVideo)
// 检查用户算力
if user.Power < 120 { // 图生视频消耗120算力
resp.ERROR(c, "算力不足")
if user.Power < powerCost {
resp.ERROR(c, fmt.Sprintf("算力不足,需要%d算力", powerCost))
return
}
@@ -473,7 +505,7 @@ func (h *JimengHandler) ImageToVideo(c *gin.Context) {
Prompt: req.Prompt,
Params: params,
ReqKey: jimeng.ReqKeyImageToVideo,
Power: 120,
Power: powerCost,
}
job, err := h.jimengService.CreateTask(user.Id, taskReq)
@@ -484,7 +516,7 @@ func (h *JimengHandler) ImageToVideo(c *gin.Context) {
}
// 扣除用户算力
h.subUserPower(user.Id, 120, model.PowerLog{
h.subUserPower(user.Id, powerCost, model.PowerLog{
Type: types.PowerConsume,
Model: "即梦图生视频",
Remark: fmt.Sprintf("任务ID%d", job.Id),
@@ -635,3 +667,45 @@ func (h *JimengHandler) subUserPower(userId uint, power int, powerLog model.Powe
session.Commit()
}
// getPowerFromConfig 从配置中获取指定类型的算力消耗
func (h *JimengHandler) getPowerFromConfig(taskType model.JMTaskType) int {
config, err := h.jimengService.GetConfig()
if err != nil {
logger.Errorf("获取即梦AI配置失败: %v", err)
// 返回默认值
switch taskType {
case model.JMTaskTypeTextToImage:
return 10
case model.JMTaskTypeImageToImage:
return 15
case model.JMTaskTypeImageEdit:
return 20
case model.JMTaskTypeImageEffects:
return 25
case model.JMTaskTypeTextToVideo:
return 30
case model.JMTaskTypeImageToVideo:
return 35
default:
return 10
}
}
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
}
}

View File

@@ -155,9 +155,7 @@ func main() {
fx.Provide(admin.NewOrderHandler),
fx.Provide(admin.NewChatHandler),
fx.Provide(admin.NewPowerLogHandler),
fx.Provide(func(app *core.AppServer, service *jimeng.Service) *admin.AdminJimengHandler {
return admin.NewAdminJimengHandler(app, service)
}),
fx.Provide(admin.NewAdminJimengHandler),
// 创建服务
fx.Provide(sms.NewSendServiceManager),
@@ -211,9 +209,17 @@ func main() {
// 即梦AI 服务
fx.Provide(func(config *types.AppConfig) *jimeng.Client {
return jimeng.NewClient(config.ApiConfig.JimengConfig.AccessKey, config.ApiConfig.JimengConfig.SecretKey)
// 使用默认配置初始化客户端,后续会从数据库加载
return jimeng.NewClient("", "")
}),
fx.Provide(jimeng.NewService),
fx.Invoke(func(service *jimeng.Service) {
// 从数据库加载配置
err := service.LoadConfigFromDB()
if err != nil {
logger.Errorf("加载即梦AI配置失败: %v", err)
}
}),
fx.Provide(jimeng.NewConsumer),
fx.Invoke(func(consumer *jimeng.Consumer) {
consumer.Start()
@@ -515,25 +521,10 @@ func main() {
// 即梦AI 路由
fx.Invoke(func(s *core.AppServer, h *handler.JimengHandler) {
group := s.Engine.Group("/api/jimeng")
group.POST("text-to-image", h.TextToImage)
group.POST("image-to-image-portrait", h.ImageToImagePortrait)
group.POST("image-edit", h.ImageEdit)
group.POST("image-effects", h.ImageEffects)
group.POST("text-to-video", h.TextToVideo)
group.POST("image-to-video", h.ImageToVideo)
group.GET("jobs", h.Jobs)
group.GET("pending-count", h.PendingCount)
group.GET("remove", h.Remove)
group.GET("retry", h.Retry)
h.RegisterRoutes()
}),
fx.Invoke(func(s *core.AppServer, h *admin.AdminJimengHandler) {
group := s.Engine.Group("/api/admin/jimeng")
group.GET("jobs", h.Jobs)
group.GET("job/:id", h.JobDetail)
group.DELETE("job/:id", h.Remove)
group.POST("batch-remove", h.BatchRemove)
group.GET("stats", h.Stats)
h.RegisterRoutes()
}),
fx.Provide(admin.NewChatAppTypeHandler),
fx.Invoke(func(s *core.AppServer, h *admin.ChatAppTypeHandler) {

View File

@@ -1,19 +1,13 @@
package jimeng
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"sort"
"strings"
"time"
"github.com/volcengine/volc-sdk-golang/base"
"github.com/volcengine/volc-sdk-golang/service/visual"
"geekai/logger"
)
@@ -21,69 +15,69 @@ var clientLogger = logger.GetLogger()
// Client 即梦API客户端
type Client struct {
accessKey string
secretKey string
region string
service string
baseURL string
httpClient *http.Client
visual *visual.Visual
}
// NewClient 创建即梦API客户端
func NewClient(accessKey, secretKey string) *Client {
return &Client{
accessKey: accessKey,
secretKey: secretKey,
region: "cn-north-1",
service: "cv",
baseURL: "https://visual.volcengineapi.com",
httpClient: &http.Client{
Timeout: 30 * time.Second,
// 使用官方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 提交任务
// SubmitTask 提交异步任务
func (c *Client) SubmitTask(req *SubmitTaskRequest) (*SubmitTaskResponse, error) {
// 构建请求URL
queryParams := map[string]string{
"Action": "CVSync2AsyncSubmitTask",
"Version": "2022-08-31",
}
reqURL := c.buildURL(queryParams)
// 序列化请求体
reqBody, err := json.Marshal(req)
// 直接将请求转为map[string]interface{}
reqBodyBytes, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("marshal request body failed: %w", err)
return nil, fmt.Errorf("marshal request failed: %w", err)
}
// 创建HTTP请求
httpReq, err := http.NewRequest("POST", reqURL, bytes.NewBuffer(reqBody))
// 直接使用序列化后的字节
jsonBody := reqBodyBytes
// 调用SDK的JSON方法
respBody, statusCode, err := c.visual.Client.Json("CVSync2AsyncSubmitTask", nil, string(jsonBody))
if err != nil {
return nil, fmt.Errorf("create http request failed: %w", err)
}
// 设置请求头
httpReq.Header.Set("Content-Type", "application/json")
// 签名请求
if err := c.signRequest(httpReq, reqBody); err != nil {
return nil, fmt.Errorf("sign request failed: %w", err)
}
// 发送请求
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("send http request failed: %w", err)
}
defer resp.Body.Close()
// 读取响应
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read response body failed: %w", err)
return nil, fmt.Errorf("submit task failed (status: %d): %w", statusCode, err)
}
clientLogger.Infof("Jimeng SubmitTask Response: %s", string(respBody))
@@ -97,47 +91,18 @@ func (c *Client) SubmitTask(req *SubmitTaskRequest) (*SubmitTaskResponse, error)
return &result, nil
}
// QueryTask 查询任务
// QueryTask 查询任务结果
func (c *Client) QueryTask(req *QueryTaskRequest) (*QueryTaskResponse, error) {
// 构建请求URL
queryParams := map[string]string{
"Action": "CVSync2AsyncGetResult",
"Version": "2022-08-31",
}
reqURL := c.buildURL(queryParams)
// 序列化请求体
reqBody, err := json.Marshal(req)
// 序列化请求
jsonBody, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("marshal request body failed: %w", err)
return nil, fmt.Errorf("marshal request failed: %w", err)
}
// 创建HTTP请求
httpReq, err := http.NewRequest("POST", reqURL, bytes.NewBuffer(reqBody))
// 调用SDK的JSON方法
respBody, statusCode, err := c.visual.Client.Json("CVSync2AsyncGetResult", nil, string(jsonBody))
if err != nil {
return nil, fmt.Errorf("create http request failed: %w", err)
}
// 设置请求头
httpReq.Header.Set("Content-Type", "application/json")
// 签名请求
if err := c.signRequest(httpReq, reqBody); err != nil {
return nil, fmt.Errorf("sign request failed: %w", err)
}
// 发送请求
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("send http request failed: %w", err)
}
defer resp.Body.Close()
// 读取响应
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read response body failed: %w", err)
return nil, fmt.Errorf("query task failed (status: %d): %w", statusCode, err)
}
clientLogger.Infof("Jimeng QueryTask Response: %s", string(respBody))
@@ -153,180 +118,25 @@ func (c *Client) QueryTask(req *QueryTaskRequest) (*QueryTaskResponse, error) {
// SubmitSyncTask 提交同步任务(仅用于文生图)
func (c *Client) SubmitSyncTask(req *SubmitTaskRequest) (*QueryTaskResponse, error) {
// 构建请求URL
queryParams := map[string]string{
"Action": "CVProcess",
"Version": "2022-08-31",
}
reqURL := c.buildURL(queryParams)
// 序列化请求体
reqBody, err := json.Marshal(req)
// 序列化请求
jsonBody, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("marshal request body failed: %w", err)
return nil, fmt.Errorf("marshal request failed: %w", err)
}
// 创建HTTP请求
httpReq, err := http.NewRequest("POST", reqURL, bytes.NewBuffer(reqBody))
// 调用SDK的JSON方法
respBody, statusCode, err := c.visual.Client.Json("CVProcess", nil, string(jsonBody))
if err != nil {
return nil, fmt.Errorf("create http request failed: %w", err)
}
// 设置请求头
httpReq.Header.Set("Content-Type", "application/json")
// 签名请求
if err := c.signRequest(httpReq, reqBody); err != nil {
return nil, fmt.Errorf("sign request failed: %w", err)
}
// 发送请求
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("send http request failed: %w", err)
}
defer resp.Body.Close()
// 读取响应
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read response body failed: %w", err)
return nil, fmt.Errorf("submit sync task failed (status: %d): %w", statusCode, err)
}
clientLogger.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
}
// buildURL 构建请求URL
func (c *Client) buildURL(queryParams map[string]string) string {
u, _ := url.Parse(c.baseURL)
q := u.Query()
for k, v := range queryParams {
q.Set(k, v)
}
u.RawQuery = q.Encode()
return u.String()
}
// signRequest 签名请求
func (c *Client) signRequest(req *http.Request, body []byte) error {
now := time.Now().UTC()
// 设置基本头部
req.Header.Set("X-Date", now.Format("20060102T150405Z"))
req.Header.Set("Host", req.URL.Host)
// 计算内容哈希
contentHash := sha256.Sum256(body)
req.Header.Set("X-Content-Sha256", hex.EncodeToString(contentHash[:]))
// 构建签名字符串
canonicalRequest := c.buildCanonicalRequest(req)
credentialScope := fmt.Sprintf("%s/%s/%s/request", now.Format("20060102"), c.region, c.service)
stringToSign := fmt.Sprintf("HMAC-SHA256\n%s\n%s\n%s",
now.Format("20060102T150405Z"), credentialScope, sha256Hash(canonicalRequest))
// 计算签名
signature := c.calculateSignature(stringToSign, now)
// 设置Authorization头部
authorization := fmt.Sprintf("HMAC-SHA256 Credential=%s/%s, SignedHeaders=%s, Signature=%s",
c.accessKey, credentialScope, c.getSignedHeaders(req), signature)
req.Header.Set("Authorization", authorization)
return nil
}
// buildCanonicalRequest 构建规范请求
func (c *Client) buildCanonicalRequest(req *http.Request) string {
// HTTP方法
method := req.Method
// 规范URI
uri := req.URL.Path
if uri == "" {
uri = "/"
}
// 规范查询字符串
query := req.URL.Query()
var queryParts []string
for k, v := range query {
for _, val := range v {
queryParts = append(queryParts, fmt.Sprintf("%s=%s", url.QueryEscape(k), url.QueryEscape(val)))
}
}
sort.Strings(queryParts)
canonicalQuery := strings.Join(queryParts, "&")
// 规范头部
var headerParts []string
headers := make(map[string]string)
for k, v := range req.Header {
key := strings.ToLower(k)
if len(v) > 0 {
headers[key] = strings.TrimSpace(v[0])
}
}
var headerKeys []string
for k := range headers {
headerKeys = append(headerKeys, k)
}
sort.Strings(headerKeys)
for _, k := range headerKeys {
headerParts = append(headerParts, fmt.Sprintf("%s:%s", k, headers[k]))
}
canonicalHeaders := strings.Join(headerParts, "\n") + "\n"
// 签名头部
signedHeaders := c.getSignedHeaders(req)
// 载荷哈希
payloadHash := req.Header.Get("X-Content-Sha256")
return fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s",
method, uri, canonicalQuery, canonicalHeaders, signedHeaders, payloadHash)
}
// getSignedHeaders 获取签名头部
func (c *Client) getSignedHeaders(req *http.Request) string {
var headers []string
for k := range req.Header {
headers = append(headers, strings.ToLower(k))
}
sort.Strings(headers)
return strings.Join(headers, ";")
}
// calculateSignature 计算签名
func (c *Client) calculateSignature(stringToSign string, t time.Time) string {
kDate := hmacSha256([]byte("HMAC-SHA256"+c.secretKey), []byte(t.Format("20060102")))
kRegion := hmacSha256(kDate, []byte(c.region))
kService := hmacSha256(kRegion, []byte(c.service))
kSigning := hmacSha256(kService, []byte("request"))
signature := hmacSha256(kSigning, []byte(stringToSign))
return hex.EncodeToString(signature)
}
// hmacSha256 计算HMAC-SHA256
func hmacSha256(key []byte, data []byte) []byte {
h := hmac.New(sha256.New, key)
h.Write(data)
return h.Sum(nil)
}
// sha256Hash 计算SHA256哈希
func sha256Hash(data string) string {
hash := sha256.Sum256([]byte(data))
return hex.EncodeToString(hash[:])
}
}

View File

@@ -145,7 +145,7 @@ func (c *Consumer) PushTaskToQueue(task map[string]interface{}) error {
}
// GetTaskStats 获取任务统计信息
func (c *Consumer) GetTaskStats() (map[string]interface{}, error) {
func (c *Consumer) GetTaskStats() (map[string]any, error) {
type StatResult struct {
Status string `json:"status"`
Count int64 `json:"count"`
@@ -160,7 +160,7 @@ func (c *Consumer) GetTaskStats() (map[string]interface{}, error) {
return nil, err
}
result := map[string]interface{}{
result := map[string]any{
"total": int64(0),
"completed": int64(0),
"processing": int64(0),

View File

@@ -13,6 +13,8 @@ import (
"geekai/store/model"
"geekai/utils"
"geekai/core/types"
"github.com/go-redis/redis/v8"
)
@@ -636,3 +638,89 @@ func (s *Service) DeleteJob(jobId uint, userId uint) error {
func (s *Service) PushTaskToQueue(task map[string]interface{}) error {
return s.taskQueue.RPush(task)
}
// TestConnection 测试即梦AI连接
func (s *Service) TestConnection(accessKey, secretKey string) error {
// 创建临时客户端进行测试
testClient := NewClient(accessKey, secretKey)
// 使用一个简单的查询任务来测试连接
// 这里使用一个不存在的任务ID来测试API连接是否正常
testReq := &QueryTaskRequest{
ReqKey: "test_connection",
TaskId: "test_task_id_12345",
}
_, err := testClient.QueryTask(testReq)
// 即使任务不存在,只要不是认证错误就说明连接正常
if err != nil {
// 检查是否是认证错误
if err.Error() == "unauthorized" || err.Error() == "access denied" {
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 fmt.Errorf("新配置测试失败: %w", err)
}
// 更新客户端
s.client = newClient
return nil
}
// GetConfig 获取即梦AI配置
func (s *Service) GetConfig() (*types.JimengConfig, error) {
var config model.Config
err := s.db.Where("name", "jimeng").First(&config).Error
if err != nil {
// 如果配置不存在,返回默认配置
return &types.JimengConfig{
AccessKey: "",
SecretKey: "",
Power: types.JimengPower{
TextToImage: 10,
ImageToImage: 15,
ImageEdit: 20,
ImageEffects: 25,
TextToVideo: 30,
ImageToVideo: 35,
},
}, nil
}
var jimengConfig types.JimengConfig
err = utils.JsonDecode(config.Value, &jimengConfig)
if err != nil {
return nil, fmt.Errorf("解析配置失败: %w", err)
}
return &jimengConfig, nil
}
// LoadConfigFromDB 从数据库加载配置并更新客户端
func (s *Service) LoadConfigFromDB() error {
config, err := s.GetConfig()
if err != nil {
return err
}
// 如果配置中有AccessKey和SecretKey则更新客户端
if config.AccessKey != "" && config.SecretKey != "" {
return s.UpdateClientConfig(config.AccessKey, config.SecretKey)
}
return nil
}

View File

@@ -2,10 +2,10 @@
<div class="system-config form" v-loading="loading">
<div class="container">
<el-form
:model="system"
:model="jimengConfig"
label-width="150px"
label-position="right"
ref="systemFormRef"
ref="configFormRef"
:rules="rules"
>
<el-tabs type="border-card">
@@ -14,8 +14,19 @@
<i class="iconfont icon-token mr-1"></i>
<span>秘钥配置</span>
</template>
<el-form-item label="网站标题" prop="title">
<el-input v-model="system['title']" />
<el-form-item label="AccessKey" prop="access_key">
<el-input
v-model="jimengConfig.access_key"
placeholder="请输入即梦AI的AccessKey"
show-password
/>
</el-form-item>
<el-form-item label="SecretKey" prop="secret_key">
<el-input
v-model="jimengConfig.secret_key"
placeholder="请输入即梦AI的SecretKey"
show-password
/>
</el-form-item>
</el-tab-pane>
@@ -24,17 +35,13 @@
<i class="iconfont icon-logout mr-1"></i>
<span>算力配置</span>
</template>
<el-form-item label="注册赠送算力" prop="init_power">
<el-input v-model.number="system['init_power']" placeholder="新用户注册赠送算力" />
</el-form-item>
<el-form-item>
<template #label>
<div class="label-title">
提示词算力
文生图算力
<el-tooltip
effect="dark"
content="生成AI绘图提示词歌词视频描述消耗的算力"
content="用户使用文生图功能时消耗的算力"
raw-content
placement="right"
>
@@ -44,14 +51,140 @@
</el-tooltip>
</div>
</template>
<el-input v-model.number="system['prompt_power']" placeholder="" />
<el-input-number
v-model="jimengConfig.power.text_to_image"
:min="1"
:max="100"
placeholder="请输入文生图算力消耗"
/>
</el-form-item>
<el-form-item>
<template #label>
<div class="label-title">
图生图算力
<el-tooltip
effect="dark"
content="用户使用图生图功能时消耗的算力"
raw-content
placement="right"
>
<el-icon>
<InfoFilled />
</el-icon>
</el-tooltip>
</div>
</template>
<el-input-number
v-model="jimengConfig.power.image_to_image"
:min="1"
:max="100"
placeholder="请输入图生图算力消耗"
/>
</el-form-item>
<el-form-item>
<template #label>
<div class="label-title">
图片编辑算力
<el-tooltip
effect="dark"
content="用户使用图片编辑功能时消耗的算力"
raw-content
placement="right"
>
<el-icon>
<InfoFilled />
</el-icon>
</el-tooltip>
</div>
</template>
<el-input-number
v-model="jimengConfig.power.image_edit"
:min="1"
:max="100"
placeholder="请输入图片编辑算力消耗"
/>
</el-form-item>
<el-form-item>
<template #label>
<div class="label-title">
图片特效算力
<el-tooltip
effect="dark"
content="用户使用图片特效功能时消耗的算力"
raw-content
placement="right"
>
<el-icon>
<InfoFilled />
</el-icon>
</el-tooltip>
</div>
</template>
<el-input-number
v-model="jimengConfig.power.image_effects"
:min="1"
:max="100"
placeholder="请输入图片特效算力消耗"
/>
</el-form-item>
<el-form-item>
<template #label>
<div class="label-title">
文生视频算力
<el-tooltip
effect="dark"
content="用户使用文生视频功能时消耗的算力"
raw-content
placement="right"
>
<el-icon>
<InfoFilled />
</el-icon>
</el-tooltip>
</div>
</template>
<el-input-number
v-model="jimengConfig.power.text_to_video"
:min="1"
:max="100"
placeholder="请输入文生视频算力消耗"
/>
</el-form-item>
<el-form-item>
<template #label>
<div class="label-title">
图生视频算力
<el-tooltip
effect="dark"
content="用户使用图生视频功能时消耗的算力"
raw-content
placement="right"
>
<el-icon>
<InfoFilled />
</el-icon>
</el-tooltip>
</div>
</template>
<el-input-number
v-model="jimengConfig.power.image_to_video"
:min="1"
:max="100"
placeholder="请输入图生视频算力消耗"
/>
</el-form-item>
</el-tab-pane>
</el-tabs>
<div style="padding: 10px">
<el-form-item>
<el-button type="primary" @click="save('system')">保存</el-button>
<el-button type="primary" @click="saveConfig" :loading="saving">保存配置</el-button>
<el-button @click="resetConfig">重置</el-button>
</el-form-item>
</div>
</el-form>
@@ -63,52 +196,107 @@
import { httpGet, httpPost } from '@/utils/http'
import { InfoFilled } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import 'md-editor-v3/lib/style.css'
import { onMounted, ref } from 'vue'
const system = ref({ models: [] })
const loading = ref(true)
onMounted(() => {
// 加载系统配置
httpGet('/api/admin/config/get?key=system')
.then((res) => {
system.value = res.data
})
.catch((e) => {
ElMessage.error('加载系统配置失败: ' + e.message)
})
.finally(() => {
loading.value = false
})
const jimengConfig = ref({
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,
},
})
const save = function (key) {
httpPost('/api/admin/config/update', {
key: key,
config: { content: notice.value, updated: true },
})
.then(() => {
ElMessage.success('操作成功!')
})
.catch((e) => {
ElMessage.error('操作失败:' + e.message)
const loading = ref(true)
const saving = ref(false)
const testing = ref(false)
const configFormRef = ref()
// 表单验证规则
const rules = {
access_key: [{ required: true, message: '请输入AccessKey', trigger: 'blur' }],
secret_key: [{ required: true, message: '请输入SecretKey', trigger: 'blur' }],
}
onMounted(() => {
loadConfig()
})
// 加载配置
const loadConfig = async () => {
try {
const res = await httpGet('/api/admin/jimeng/config')
jimengConfig.value = res.data
} catch (e) {
ElMessage.error('加载配置失败: ' + e.message)
} finally {
loading.value = false
}
}
// 保存配置
const saveConfig = async () => {
try {
await configFormRef.value.validate()
saving.value = true
await httpPost('/api/admin/jimeng/config', {
config: jimengConfig.value,
})
ElMessage.success('配置保存成功!')
} catch (e) {
if (e.message) {
ElMessage.error('保存失败:' + e.message)
}
} finally {
saving.value = false
}
}
// 重置配置
const resetConfig = () => {
jimengConfig.value = {
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,
},
}
ElMessage.info('配置已重置')
}
</script>
<style lang="stylus" scoped>
@import '../../../assets/css/admin/form.styl'
@import '../../../assets/css/main.styl'
.system-config {
display flex
justify-content center
.sys-tabs {
.container {
width 100%
max-width 800px
}
.label-title {
display flex
align-items center
gap 5px
}
.el-input-number {
width 100%
background-color var(--el-bg-color)
padding 10px 20px 40px 20px
//border: 1px solid var(--el-border-color);
}
}
</style>