最近玩即梦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 hashstring 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}};// 构建请求URLstring 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. 调用输出
至此,即梦API调用成功。