完成即梦配置功能页面

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

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
}