diff --git a/dsAiSupport/src/main/java/com/dsideal/aiSupport/Util/JiMeng/JmCommon.java b/dsAiSupport/src/main/java/com/dsideal/aiSupport/Util/JiMeng/JmCommon.java index 80972f4e..22707035 100644 --- a/dsAiSupport/src/main/java/com/dsideal/aiSupport/Util/JiMeng/JmCommon.java +++ b/dsAiSupport/src/main/java/com/dsideal/aiSupport/Util/JiMeng/JmCommon.java @@ -10,6 +10,7 @@ import javax.crypto.spec.SecretKeySpec; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; +import java.net.URI; import java.net.URL; import java.nio.ByteBuffer; import java.nio.charset.Charset; @@ -18,53 +19,50 @@ import java.security.MessageDigest; import java.text.SimpleDateFormat; import java.util.*; +/** + * 即梦API通用工具类 + * 用于处理火山引擎API的请求签名和发送 + */ public class JmCommon { // 请求域名 public static String host = "visual.volcengineapi.com"; - public static String path = "/"; // 路径,不包含 Query// 请求接口信息 - public static String service = "cv"; - public static String region = "cn-north-1"; - public static String schema = "https"; - public static String version = "2022-08-31"; + public static String path = "/"; // 路径,不包含 Query + // 请求接口信息 + public static String service = "cv"; // 服务名称,固定为cv + public static String region = "cn-north-1"; // 区域,固定为cn-north-1 + public static String schema = "https"; // 协议,使用https + public static String version = "2022-08-31"; // API版本号 + + // URL编码相关常量 private static final BitSet URLENCODER = new BitSet(256); private static final String CONST_ENCODE = "0123456789ABCDEF"; public static final Charset UTF_8 = StandardCharsets.UTF_8; - protected static final String ak; - protected static final String sk; - public static Prop PropKit; + + // API访问凭证 + protected static final String ak; // Access Key + protected static final String sk; // Secret Key + public static Prop PropKit; // 配置文件工具 + /** + * 静态初始化块 + * 加载配置文件并初始化URL编码器 + */ static { //加载配置文件 String configFile = "application_{?}.yaml".replace("{?}", getEnvPrefix()); PropKit = new YamlProp(configFile); - System.out.println("当前环境: " + getEnvPrefix()); - // 火山官网密钥信息, 注意sk结尾有== - ak = PropKit.get("JiMeng.ak"); - sk = PropKit.get("JiMeng.sk"); - } - - /** - * 功能:获取是否为开发环境 - * - * @return - */ - public static String getEnvPrefix() { - String myEnvVar = System.getenv("WORKING_ENV"); - if (myEnvVar == null) { - myEnvVar = "dev"; - } - return myEnvVar; - } + ak = PropKit.get("JiMeng.ak"); // 从配置文件获取Access Key + sk = PropKit.get("JiMeng.sk"); // 从配置文件获取Secret Key - static { + // 初始化URL编码器,设置不需要编码的字符 int i; - for (i = 97; i <= 122; ++i) { + for (i = 97; i <= 122; ++i) { // a-z URLENCODER.set(i); } - for (i = 65; i <= 90; ++i) { + for (i = 65; i <= 90; ++i) { // A-Z URLENCODER.set(i); } - for (i = 48; i <= 57; ++i) { + for (i = 48; i <= 57; ++i) { // 0-9 URLENCODER.set(i); } URLENCODER.set('-'); @@ -73,28 +71,57 @@ public class JmCommon { URLENCODER.set('~'); } + /** + * 获取当前运行环境前缀 + * + * @return 环境前缀,默认为"dev" + */ + public static String getEnvPrefix() { + String myEnvVar = System.getenv("WORKING_ENV"); + if (myEnvVar == null) { + myEnvVar = "dev"; // 默认为开发环境 + } + return myEnvVar; + } + + /** + * 发送HTTP请求到火山引擎API + * + * @param method 请求方法,如GET、POST + * @param queryList 查询参数列表 + * @param body 请求体数据 + * @param action API操作名称 + * @return 响应体字符串 + * @throws Exception 请求过程中的异常 + */ protected static String doRequest(String method, Map queryList, byte[] body, String action) throws Exception { Date date = new Date(); if (body == null) { body = new byte[0]; } + // 计算请求体的SHA256哈希值 String xContentSha256 = hashSHA256(body); + + // 格式化日期时间,用于请求头 SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"); sdf.setTimeZone(TimeZone.getTimeZone("GMT")); String xDate = sdf.format(date); - String shortXDate = xDate.substring(0, 8); - String contentType = "application/json"; - String signHeader = "host;x-date;x-content-sha256;content-type"; + String shortXDate = xDate.substring(0, 8); // 提取日期部分,用于凭证范围 + + String contentType = "application/json"; // 内容类型 + String signHeader = "host;x-date;x-content-sha256;content-type"; // 签名头部列表 + // 构建查询参数字符串 SortedMap realQueryList = new TreeMap<>(queryList); - realQueryList.put("Action", action); - realQueryList.put("Version", version); + realQueryList.put("Action", action); // 添加Action参数 + realQueryList.put("Version", version); // 添加Version参数 StringBuilder querySB = new StringBuilder(); for (String key : realQueryList.keySet()) { querySB.append(signStringEncoder(key)).append("=").append(signStringEncoder(realQueryList.get(key))).append("&"); } - querySB.deleteCharAt(querySB.length() - 1); + querySB.deleteCharAt(querySB.length() - 1); // 删除最后一个&符号 + // 构建规范请求字符串,用于签名 String canonicalStringBuilder = method + "\n" + path + "\n" + querySB + "\n" + "host:" + host + "\n" + "x-date:" + xDate + "\n" + @@ -103,16 +130,25 @@ public class JmCommon { "\n" + signHeader + "\n" + xContentSha256; + + // 计算规范请求字符串的哈希值 String hashcanonicalString = hashSHA256(canonicalStringBuilder.getBytes()); + + // 构建凭证范围和签名字符串 String credentialScope = shortXDate + "/" + region + "/" + service + "/request"; String signString = "HMAC-SHA256" + "\n" + xDate + "\n" + credentialScope + "\n" + hashcanonicalString; + // 生成签名密钥并计算签名 byte[] signKey = genSigningSecretKeyV4(shortXDate, region, service); String signature = HexFormat.of().formatHex(hmacSHA256(signKey, signString)); - URL url = new URL(schema + "://" + host + path + "?" + querySB); - + + // 构建URL并创建HTTP连接 + URI uri = new URI(schema, "//" + host + path, "?" + querySB.toString()); + URL url = uri.toURL(); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod(method); + + // 设置请求头 conn.setRequestProperty("Host", host); conn.setRequestProperty("X-Date", xDate); conn.setRequestProperty("X-Content-Sha256", xContentSha256); @@ -121,6 +157,8 @@ public class JmCommon { " Credential=" + ak + "/" + credentialScope + ", SignedHeaders=" + signHeader + ", Signature=" + signature); + + // 如果不是GET请求,写入请求体 if (!Objects.equals(conn.getRequestMethod(), "GET")) { conn.setDoOutput(true); OutputStream os = conn.getOutputStream(); @@ -128,22 +166,29 @@ public class JmCommon { os.flush(); os.close(); } + + // 建立连接并获取响应 conn.connect(); - int responseCode = conn.getResponseCode(); - InputStream is; if (responseCode == 200) { - is = conn.getInputStream(); + is = conn.getInputStream(); // 成功响应 } else { - is = conn.getErrorStream(); + is = conn.getErrorStream(); // 错误响应 } + + // 读取响应内容并关闭流 String responseBody = new String(ByteStreams.toByteArray(is)); is.close(); - return responseBody; } + /** + * URL编码字符串,用于签名 + * + * @param source 源字符串 + * @return 编码后的字符串 + */ protected static String signStringEncoder(String source) { if (source == null) { return null; @@ -153,10 +198,13 @@ public class JmCommon { while (bb.hasRemaining()) { int b = bb.get() & 255; if (URLENCODER.get(b)) { + // 不需要编码的字符直接添加 buf.append((char) b); } else if (b == 32) { + // 空格编码为%20 buf.append("%20"); } else { + // 其他字符编码为%XX格式 buf.append("%"); char hex1 = CONST_ENCODE.charAt(b >> 4); char hex2 = CONST_ENCODE.charAt(b & 15); @@ -168,6 +216,13 @@ public class JmCommon { return buf.toString(); } + /** + * 计算内容的SHA256哈希值 + * + * @param content 要计算哈希的内容 + * @return 十六进制表示的哈希值 + * @throws Exception 计算过程中的异常 + */ protected static String hashSHA256(byte[] content) throws Exception { try { MessageDigest md = MessageDigest.getInstance("SHA-256"); @@ -177,6 +232,14 @@ public class JmCommon { } } + /** + * 使用HMAC-SHA256算法计算消息认证码 + * + * @param key 密钥 + * @param content 内容 + * @return 计算结果 + * @throws Exception 计算过程中的异常 + */ protected static byte[] hmacSHA256(byte[] key, String content) throws Exception { try { Mac mac = Mac.getInstance("HmacSHA256"); @@ -187,6 +250,16 @@ public class JmCommon { } } + /** + * 生成V4版本的签名密钥 + * 使用AWS签名V4算法的派生密钥过程 + * + * @param date 日期字符串 + * @param region 区域 + * @param service 服务名称 + * @return 派生的签名密钥 + * @throws Exception 生成过程中的异常 + */ protected static byte[] genSigningSecretKeyV4(String date, String region, String service) throws Exception { byte[] kDate = hmacSHA256((JmCommon.sk).getBytes(), date); byte[] kRegion = hmacSHA256(kDate, region); diff --git a/dsAiSupport/src/main/java/com/dsideal/aiSupport/Util/JiMeng/JmText2Image.java b/dsAiSupport/src/main/java/com/dsideal/aiSupport/Util/JiMeng/JmText2Image.java index 71134f83..e3c54f38 100644 --- a/dsAiSupport/src/main/java/com/dsideal/aiSupport/Util/JiMeng/JmText2Image.java +++ b/dsAiSupport/src/main/java/com/dsideal/aiSupport/Util/JiMeng/JmText2Image.java @@ -2,12 +2,18 @@ package com.dsideal.aiSupport.Util.JiMeng; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.FileOutputStream; import java.io.OutputStream; import java.util.*; public class JmText2Image extends JmCommon { + private static final Logger log = LoggerFactory.getLogger(JmText2Image.class); + + private static final String action = "CVProcess"; + private static final String req_key = "jimeng_high_aes_general_v21_L"; /** * 生成图片 @@ -17,9 +23,9 @@ public class JmText2Image extends JmCommon { */ public static void generateImage(String prompt, String saveImgPath) throws Exception { JSONObject req = new JSONObject(); - req.put("req_key", "jimeng_high_aes_general_v21_L"); + req.put("req_key", req_key); req.put("prompt", prompt); - String responseBody = doRequest("POST", new HashMap<>(), req.toString().getBytes(), "CVProcess"); + String responseBody = doRequest("POST", new HashMap<>(), req.toString().getBytes(), action); JSONObject jo = JSON.parseObject(responseBody); String imgBase64 = jo.getJSONObject("data").getJSONArray("binary_data_base64").getFirst().toString(); // 对 Base64 字符串进行解码 @@ -29,14 +35,15 @@ public class JmText2Image extends JmCommon { try (OutputStream os = new FileOutputStream(saveImgPath)) { // 使用-with try-resources 自动关闭资源 os.write(bytes); os.flush(); // 刷新缓冲区,确保数据写入文件 - System.out.println("文件保存成功!"); + log.info("文件保存成功!"); } catch (Exception e) { - e.printStackTrace(); + log.error("文件保存失败!" + e.getMessage()); } } public static void main(String[] args) throws Exception { - String prompt="制作一张vlog视频封面。马卡龙配色,美女旅游照片+色块的拼贴画风格,主文案是“威海旅游vlog”,副文案是“特种兵一日游 被低估的旅游城市”,海报主体是一个穿着短裙、梳双马尾的少女,人物白色描边"; - JmText2Image.generateImage(prompt,"d:/Temp/1.jpg"); + String prompt = "制作一张vlog视频封面。马卡龙配色,美女旅游照片+色块的拼贴画风格,主文案是“威海旅游vlog”,副文案是“特种兵一日游 被低估的旅游城市”,海报主体是一个穿着短裙、梳双马尾的少女,人物白色描边"; + String saveImagePath = "d:/Temp/1.jpg"; + JmText2Image.generateImage(prompt, saveImagePath); } } diff --git a/dsAiSupport/src/main/resources/application_pro.yaml b/dsAiSupport/src/main/resources/application_pro.yaml index 36bb2edf..c53ac01e 100644 --- a/dsAiSupport/src/main/resources/application_pro.yaml +++ b/dsAiSupport/src/main/resources/application_pro.yaml @@ -55,3 +55,9 @@ excel: # 导入excel 的模板配置路径 ExcelImportTemplatePathSuffix: /ExcelImportTemplate/ +# 即梦 +JiMeng: + # Access Key ID + ak: AKLTZjVlOGU1NzA1YWZkNDExMzkzYzY5YTNlOTRmMTMxODg + # Secret Access Key + sk: WkdabU9UTXdNVEJpTmpWbE5HVTJZVGxtTnpWbU5XSTBaRGN5TW1NMk5tRQ==