mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-04-26 21:14:27 +08:00
完成即梦配置功能页面
This commit is contained in:
@@ -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[:])
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user