diff --git a/JIMENG_CONFIG_README.md b/JIMENG_CONFIG_README.md new file mode 100644 index 00000000..9ac0975c --- /dev/null +++ b/JIMENG_CONFIG_README.md @@ -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 格式 diff --git a/api/core/app_server.go b/api/core/app_server.go index 24cf805f..626b09d6 100644 --- a/api/core/app_server.go +++ b/api/core/app_server.go @@ -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) { diff --git a/api/core/types/config.go b/api/core/types/config.go index bd57b31d..9fbcc73d 100644 --- a/api/core/types/config.go +++ b/api/core/types/config.go @@ -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 // 是否沙盒环境 diff --git a/api/core/types/jimeng.go b/api/core/types/jimeng.go new file mode 100644 index 00000000..bd9bde14 --- /dev/null +++ b/api/core/types/jimeng.go @@ -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"` +} diff --git a/api/go.mod b/api/go.mod index 21adc772..b5b16127 100644 --- a/api/go.mod +++ b/api/go.mod @@ -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 diff --git a/api/go.sum b/api/go.sum index d04c1fd5..808cd458 100644 --- a/api/go.sum +++ b/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,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= diff --git a/api/handler/admin/jimeng_handler.go b/api/handler/admin/jimeng_handler.go index 7580a50f..6d191789 100644 --- a/api/handler/admin/jimeng_handler.go +++ b/api/handler/admin/jimeng_handler.go @@ -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) -} \ No newline at end of file +} + +// 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": "配置更新成功"}) +} diff --git a/api/handler/admin/media_handler.go b/api/handler/admin/media_handler.go index d1347038..da18ddfa 100644 --- a/api/handler/admin/media_handler.go +++ b/api/handler/admin/media_handler.go @@ -154,7 +154,6 @@ func (h *MediaHandler) Remove(c *gin.Context) { 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 @@ -174,7 +173,6 @@ func (h *MediaHandler) Remove(c *gin.Context) { if fileURL == "" { fileURL = job.WaterURL } - break default: resp.ERROR(c, types.InvalidArgs) return diff --git a/api/handler/jimeng_handler.go b/api/handler/jimeng_handler.go index b104e1bb..533e961c 100644 --- a/api/handler/jimeng_handler.go +++ b/api/handler/jimeng_handler.go @@ -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 + } +} diff --git a/api/main.go b/api/main.go index 670636a1..18dc32c7 100644 --- a/api/main.go +++ b/api/main.go @@ -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) { diff --git a/api/service/jimeng/client.go b/api/service/jimeng/client.go index 291a0aae..f0e6d54e 100644 --- a/api/service/jimeng/client.go +++ b/api/service/jimeng/client.go @@ -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[:]) -} +} \ No newline at end of file diff --git a/api/service/jimeng/consumer.go b/api/service/jimeng/consumer.go index 72b2ae4c..19fdf8e8 100644 --- a/api/service/jimeng/consumer.go +++ b/api/service/jimeng/consumer.go @@ -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), diff --git a/api/service/jimeng/service.go b/api/service/jimeng/service.go index 017e1d88..d58cf175 100644 --- a/api/service/jimeng/service.go +++ b/api/service/jimeng/service.go @@ -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 +} diff --git a/web/src/views/admin/jimeng/JimengSetting.vue b/web/src/views/admin/jimeng/JimengSetting.vue index 367989c4..e4b17650 100644 --- a/web/src/views/admin/jimeng/JimengSetting.vue +++ b/web/src/views/admin/jimeng/JimengSetting.vue @@ -2,10 +2,10 @@