最近玩即梦AI,文生图,文生视频等等很多玩法都很强大。即梦本身页提供了API。官方文档里有Java, Golang, Python, PHP的SDK,官方也推荐使用SDK,调用SDK会比较省事儿。官方也提供了HTTP请求示例代码,但是也只包括Java, Golang, Python, PHP,没有C#。所以就尝试写个C#调用即梦API。调用的难点在于火山引擎API的签名生成。下面介绍一下即梦API的调用过程和使用豆包将Python HTTP示例代码转成C#代码。

1. 注册火山引擎账号,并开通即梦API图片生成服务,开通方式选择免费试用。免费调用时长限额是500秒,并发限额是1。参考新手指南--AI中台公用文档-火山引擎。这个文档中还包含了API访问密钥的生成方式,这个也是使用API必不可少的步骤。注意这个密钥自己妥善保管,不能泄露。

2. 账号开通完成,API访问密钥创建完成就可以开始代码了。我参考了Python的代码,Python代码下载下来,需要替换access_key和secret_key。本地装有Python环境,可以直接跑通。

3. 使用豆包的AI编程,把官方Python代码,贴进豆包提问,请豆包转成C#代码,稍等片刻,C#代码生成完毕。

4. 把C#代码Copy到VS,开始调试。我遇到的问题是:“The request signature we calculated does not match the signature you provided. Check your Secret Access Key and signing method. Consult the service documentation for details.”。是说,request签名,C#里计算的结果和火山引擎服务器计算的结果不一致。

5. 继续调试,通过和Python的签名结果对比,C#函数计算的签名其实是没有问题的,那么问题出在Http request。

6. 进一步调试,发现,这个API的POST request的Header里应该要包含如下内容,这里主要注意Content-Type。

POST 
Host: billing.volcengineapi.com
Content-Type: application/json
X-Date: 20250329T180937Z
Authorization: HMAC-SHA256 Credential=AKLTYWViMTVmZGYzM2E0NDI5Mzk2MDZjNjFmMjc2MjRjMzg/20250329/cn-beijing/billing/request, SignedHeaders=host;x-date, Signature=5e8480ceea12d0000a23c054151c50dd02c1a7dec835004057d19f13d53a7658

7. 往Http request加header,是通过下面的代码实现的。需要注意的是注释的部分,我调试的错误就是因为AI的代码额外加了Content的UTF8编码,导致服务器端计算的签名和C#计算的不匹配。

var headers = new Dictionary<string, string>
    {
        {"X-Date", currentDate},
        {"Authorization", authorizationHeader},
        {"X-Content-Sha256", payloadHash},
        {"Content-Type", contentType}
    };


.......


foreach (var header in headers)
{
    // 这里加header,Content-Type是加不进去的,它应该要被放在Content的header
    request.Headers.TryAddWithoutValidation(header.Key, header.Value);
}

.......

// 加到这里,注意这里一定要只包含Content-Type,不要包含任何编码格式
// AI生成的代码,可能会加上编码格式,导致,发送往服务器的Header Content-Type不完全是 “application/json”
request.Content = new StringContent(formattedBody, new System.Net.Http.Headers.MediaTypeHeaderValue(contentType));

8. 最终的调用类完整代码如下。

using Newtonsoft.Json;
using System.Security.Cryptography;
using System.Text;

internal class VolcengineApiClient
{
    private readonly string _accessKey;
    private readonly string _secretKey;
    private readonly string _host;
    private readonly string _region;
    private readonly string _service;
    private readonly string _endpoint;
    private readonly HttpClient _httpClient;

    /// <summary>
    /// 初始化火山引擎API客户端
    /// </summary>
    /// <param name="accessKey">访问密钥</param>
    /// <param name="secretKey">密钥</param>
    /// <param name="host">API主机地址</param>
    /// <param name="region">区域</param>
    /// <param name="service">服务名称</param>
    public VolcengineApiClient(string accessKey, string secretKey,
                              string host = "visual.volcengineapi.com",
                              string region = "cn-north-1",
                              string service = "cv")
    {
        _accessKey = accessKey ?? throw new ArgumentNullException(nameof(accessKey));
        _secretKey = secretKey ?? throw new ArgumentNullException(nameof(secretKey));
        _host = host ?? throw new ArgumentNullException(nameof(host));
        _region = region ?? throw new ArgumentNullException(nameof(region));
        _service = service ?? throw new ArgumentNullException(nameof(service));
        _endpoint = $"https://{_host}";

        _httpClient = new HttpClient();
    }



    /// <summary>
    /// 发送API请求
    /// </summary>
    /// <param name="method">HTTP方法</param>
    /// <param name="action">API动作</param>
    /// <param name="version">API版本</param>
    /// <param name="bodyParams">请求体参数</param>
    /// <param name="extraQueryParams">额外的查询参数</param>
    /// <returns>响应结果</returns>
    public async Task<string> SendRequestAsync(string method = "POST",
                                              string action = "CVProcess",
                                              string version = "2022-08-31",
                                              object bodyParams = null,
                                              Dictionary<string, string> extraQueryParams = null)
    {
        // 准备查询参数
        var queryParams = new Dictionary<string, string>
            {
                {"Action", action},
                {"Version", version}
            };

        // 添加额外的查询参数
        if (extraQueryParams != null)
        {
            foreach (var param in extraQueryParams)
            {
                queryParams[param.Key] = param.Value;
            }
        }

        // 格式化查询参数
        string formattedQuery = FormatQueryParameters(queryParams);

        // 准备请求体
        string formattedBody = bodyParams != null ? JsonConvert.SerializeObject(bodyParams) : "{}";

        // 生成时间戳
        DateTime utcNow = DateTime.UtcNow;
        string currentDate = utcNow.ToString("yyyyMMddTHHmmssZ");
        string dateStamp = utcNow.ToString("yyyyMMdd");

        // 计算 payload hash
        string payloadHash = ComputeSha256Hash(formattedBody);

        // 构建规范请求
        string canonicalUri = "/";
        string contentType = "application/json";

        string canonicalHeaders =
            $"content-type:{contentType}\n" +
            $"host:{_host}\n" +
            $"x-content-sha256:{payloadHash}\n" +
            $"x-date:{currentDate}\n";

        string signedHeaders = "content-type;host;x-content-sha256;x-date";

        string canonicalRequest = $"{method}\n{canonicalUri}\n{formattedQuery}\n{canonicalHeaders}\n{signedHeaders}\n{payloadHash}";

        // 计算签名
        string algorithm = "HMAC-SHA256";
        string credentialScope = $"{dateStamp}/{_region}/{_service}/request";
        string stringToSign = $"{algorithm}\n{currentDate}\n{credentialScope}\n{ComputeSha256Hash(canonicalRequest)}";
        
        byte[] signingKey = GetSignatureKey(_secretKey, dateStamp, _region, _service);
        string signature = ComputeHmacSha256(signingKey, stringToSign);

        // 构建授权头
        string authorizationHeader = $"{algorithm} Credential={_accessKey}/{credentialScope}, " +
                                    $"SignedHeaders={signedHeaders}, " +
                                    $"Signature={signature}";

        // 准备请求头
        var headers = new Dictionary<string, string>
            {
                {"X-Date", currentDate},
                {"Authorization", authorizationHeader},
                {"X-Content-Sha256", payloadHash},
                {"Content-Type", contentType}
            };

        // 构建请求URL
        string requestUrl = $"{_endpoint}?{formattedQuery}";

        Console.WriteLine("\nBEGIN REQUEST++++++++++++++++++++++++++++++++++++");
        Console.WriteLine($"Request URL = {requestUrl}");

        // 发送请求
        HttpResponseMessage response;
        try
        {
            using (var request = new HttpRequestMessage(new HttpMethod(method), requestUrl))
            {
                // 添加请求头
                foreach (var header in headers)
                {
                    request.Headers.TryAddWithoutValidation(header.Key, header.Value);
                }

                // 添加请求体
                if (method.Equals("POST", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(formattedBody))
                {
                    request.Content = new StringContent(formattedBody, new System.Net.Http.Headers.MediaTypeHeaderValue(contentType));                    
                }

                response = await _httpClient.SendAsync(request);
            }

            string responseBody = await response.Content.ReadAsStringAsync();

            Console.WriteLine("\nRESPONSE++++++++++++++++++++++++++++++++++++");
            Console.WriteLine($"Response code: {(int)response.StatusCode}");
            Console.WriteLine($"Response body: {responseBody.Replace("\\u0026", "&")}\n");

            return responseBody;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error occurred: {ex.Message}");
            throw;
        }
    }

    /// <summary>
    /// 格式化查询参数
    /// </summary>
    private string FormatQueryParameters(Dictionary<string, string> parameters)
    {
        if (parameters == null || parameters.Count == 0)
            return "";

        // 按键名排序并拼接
        var sortedParams = parameters.OrderBy(p => p.Key);
        return string.Join("&", sortedParams.Select(p => $"{p.Key}={p.Value}"));
    }

    /// <summary>
    /// 计算SHA256哈希
    /// </summary>
    private string ComputeSha256Hash(string input)
    {
        // 创建SHA256实例
        using (SHA256 sha256Hash = SHA256.Create())
        {
            // 将输入字符串转换为UTF8字节数组
            byte[] inputBytes = Encoding.UTF8.GetBytes(input);

            // 计算哈希值
            byte[] hashBytes = sha256Hash.ComputeHash(inputBytes);

            // 将字节数组转换为十六进制字符串
            StringBuilder builder = new StringBuilder();
            for (int i = 0; i < hashBytes.Length; i++)
            {
                builder.Append(hashBytes[i].ToString("x2")); // "x2"确保每个字节用两位十六进制表示
            }
            return builder.ToString();

        }
    }

    /// <summary>
    /// 计算HMAC-SHA256
    /// </summary>
    private string ComputeHmacSha256(byte[] key, string input)
    {
        using (HMACSHA256 hmac = new HMACSHA256(key))
        {
            byte[] hashBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(input));
            return BitConverter.ToString(hashBytes).Replace("-", "").ToLower();
        }
    }

    /// <summary>
    /// 获取签名密钥
    /// </summary>
    private byte[] GetSignatureKey(string key, string dateStamp, string regionName, string serviceName)
    {
        byte[] kDate = ComputeHmacSha256Bytes(Encoding.UTF8.GetBytes(key), dateStamp);
        byte[] kRegion = ComputeHmacSha256Bytes(kDate, regionName);
        byte[] kService = ComputeHmacSha256Bytes(kRegion, serviceName);
        return ComputeHmacSha256Bytes(kService, "request");
    }

    /// <summary>
    /// 计算HMAC-SHA256字节数组
    /// </summary>
    private byte[] ComputeHmacSha256Bytes(byte[] key, string input)
    {
        using (HMACSHA256 hmac = new HMACSHA256(key))
        {
            return hmac.ComputeHash(Encoding.UTF8.GetBytes(input));
        }
    }
}

9. 调用代码

private static async Task Main(string[] args)
{
    try
    {
        // 火山官网密钥信息, 注意sk结尾有==
        string accessKey = "AKL...jI";
        string secretKey = "WVR..==";

        // 创建客户端实例
        var client = new VolcengineApiClient(accessKey, secretKey);

        // 准备请求参数
        var bodyParams = new
        {
            req_key = "jimeng_high_aes_general_v21_L",
            prompt = "stand in forest",
            return_url = true
        };

        // 发送请求
        string response = await client.SendRequestAsync(
            action: "CVProcess",
            version: "2022-08-31",
            bodyParams: bodyParams
        );
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Error: {ex.Message}");
        if (ex.InnerException != null)
        {
            Console.WriteLine($"Inner Exception: {ex.InnerException.Message}");
        }
    }
}

10. 调用输出

image

 至此,即梦API调用成功。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐