using System; using System.Collections.Generic; using System.IO; using System.Net.Http; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using NCC.Common.Configuration; using NCC.Dependency; using Newtonsoft.Json; using Newtonsoft.Json.Linq; // 增强版 SDK 暂时注释,改用 HTTP 原生调用 // using AlibabaCloud.SDK.Green20220302; // using AlibabaCloud.SDK.Green20220302.Models; // using AlibabaCloud.OSS.V2; // using AlibabaCloud.OSS.V2.Models; namespace NCC.Extend { /// /// 图片审核服务(阿里云内容安全),仅内部调用,不暴露 API /// public class ImageModerationService : ITransient { private readonly HttpClient _httpClient; private readonly string _endpoint; private readonly string _accessKeyId; private readonly string _accessKeySecret; private readonly string _region; private readonly bool _enabled; private readonly bool _isEnhancedVersion; // 是否为增强版(green-cip) /// /// 初始化图片审核服务 /// public ImageModerationService() { _httpClient = new HttpClient(); _enabled = App.Configuration["NCC_App:ImageModeration:Enabled"] == "true"; _endpoint = App.Configuration["NCC_App:ImageModeration:Endpoint"] ?? "https://green.cn-shanghai.aliyuncs.com"; _region = App.Configuration["NCC_App:ImageModeration:Region"] ?? "cn-shanghai"; // 优先使用ImageModeration配置,如果没有则使用AliyunOSS的配置 _accessKeyId = App.Configuration["NCC_App:ImageModeration:AccessKeyId"]; if (string.IsNullOrEmpty(_accessKeyId)) { _accessKeyId = App.Configuration["NCC_App:AliyunOSS:AccessKeyId"]; } _accessKeySecret = App.Configuration["NCC_App:ImageModeration:AccessKeySecret"]; if (string.IsNullOrEmpty(_accessKeySecret)) { _accessKeySecret = App.Configuration["NCC_App:AliyunOSS:AccessKeySecret"]; } // 判断是否为增强版(endpoint 包含 green-cip) _isEnhancedVersion = _endpoint.Contains("green-cip"); } /// /// 图片审核结果 /// public class ModerationResult { public bool Passed { get; set; } public string Message { get; set; } public string RawResponse { get; set; } public object Details { get; set; } } /// /// 图片审核(从本地文件路径) /// /// 本地文件路径 /// 审核结果详情 public async Task ScanImageFromFileAsync(string filePath) { if (!_enabled) { return new ModerationResult { Passed = true, Message = "审核未启用,直接通过" }; } if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath)) { return new ModerationResult { Passed = false, Message = "文件不存在或路径为空" }; } try { var imageBytes = await File.ReadAllBytesAsync(filePath); return await ScanImageAsync(imageBytes); } catch (Exception ex) { // 审核异常时,根据配置决定是否通过 var failOnError = App.Configuration["NCC_App:ImageModeration:FailOnError"] == "true"; return new ModerationResult { Passed = !failOnError, Message = $"审核异常:{ex.Message}", RawResponse = ex.ToString() }; } } /// /// 图片审核(从字节数组) /// /// 图片字节数组 /// 审核结果详情 public async Task ScanImageAsync(byte[] imageBytes) { if (!_enabled) { return new ModerationResult { Passed = true, Message = "审核未启用,直接通过" }; } if (imageBytes == null || imageBytes.Length == 0) { return new ModerationResult { Passed = false, Message = "图片数据为空" }; } // 检查 AccessKey 是否配置 if (string.IsNullOrEmpty(_accessKeyId) || string.IsNullOrEmpty(_accessKeySecret)) { var failOnError = App.Configuration["NCC_App:ImageModeration:FailOnError"] == "true"; return new ModerationResult { Passed = !failOnError, Message = "图片审核服务未配置 AccessKey,无法调用审核接口", RawResponse = "AccessKeyId 或 AccessKeySecret 未配置" }; } try { // 判断是否为增强版,使用不同的调用方式 if (_isEnhancedVersion) { return await ScanImageEnhancedAsync(imageBytes); } else { return await ScanImageLegacyAsync(imageBytes); } } catch (Exception ex) { // 审核异常时,根据配置决定是否通过 var failOnError = App.Configuration["NCC_App:ImageModeration:FailOnError"] == "true"; return new ModerationResult { Passed = !failOnError, Message = $"审核异常:{ex.Message}", RawResponse = ex.ToString() }; } } /// /// 图片审核(增强版 - green-cip) /// 使用 HTTP 原生调用,实现本地图片审核流程: /// 1. DescribeUploadToken - 获取临时 OSS 凭证 /// 2. 上传图片到临时 OSS /// 3. ImageModeration - 调用审核接口 /// /// 图片字节数组 /// 审核结果详情 private async Task ScanImageEnhancedAsync(byte[] imageBytes) { try { var endpointHost = _endpoint.Replace("https://", "").Replace("http://", ""); // 1. 调用 DescribeUploadToken 获取临时 OSS 凭证 var tokenUrl = $"{_endpoint}/DescribeUploadToken"; var tokenResponse = await CallOpenApiAsync(tokenUrl, new Dictionary()); var tokenResult = JsonConvert.DeserializeObject(tokenResponse); if (tokenResult == null || tokenResult["Code"]?.Value() != 200) { var failOnError = App.Configuration["NCC_App:ImageModeration:FailOnError"] == "true"; return new ModerationResult { Passed = !failOnError, Message = $"获取临时 OSS 凭证失败:{tokenResult?["Msg"]?.ToString() ?? "未知错误"}", RawResponse = tokenResponse }; } var tokenData = tokenResult["Data"]; if (tokenData == null) { var failOnError = App.Configuration["NCC_App:ImageModeration:FailOnError"] == "true"; return new ModerationResult { Passed = !failOnError, Message = "临时 OSS 凭证数据为空", RawResponse = tokenResponse }; } var bucketName = tokenData["BucketName"]?.ToString(); var fileNamePrefix = tokenData["FileNamePrefix"]?.ToString() ?? ""; var ossAccessKeyId = tokenData["AccessKeyId"]?.ToString(); var ossAccessKeySecret = tokenData["AccessKeySecret"]?.ToString(); var ossSecurityToken = tokenData["SecurityToken"]?.ToString(); var ossInternetEndPoint = tokenData["OssInternetEndPoint"]?.ToString(); if (string.IsNullOrEmpty(bucketName) || string.IsNullOrEmpty(ossInternetEndPoint)) { var failOnError = App.Configuration["NCC_App:ImageModeration:FailOnError"] == "true"; return new ModerationResult { Passed = !failOnError, Message = "临时 OSS 凭证信息不完整", RawResponse = tokenResponse }; } // 2. 上传图片到临时 OSS(使用 OSS PutObject API) var objectName = $"{fileNamePrefix}{Guid.NewGuid()}.jpg"; var ossPutUrl = $"https://{bucketName}.{ossInternetEndPoint}/{objectName}"; // 使用 STS Token 上传到 OSS(需要 OSS 签名) var ossPutSuccess = await UploadToOSSTempAsync( ossPutUrl, imageBytes, bucketName, objectName, ossAccessKeyId, ossAccessKeySecret, ossSecurityToken); if (!ossPutSuccess) { var failOnError = App.Configuration["NCC_App:ImageModeration:FailOnError"] == "true"; return new ModerationResult { Passed = !failOnError, Message = "上传图片到临时 OSS 失败", RawResponse = "OSS 上传失败" }; } // 3. 调用增强版审核接口 ImageModeration var moderationUrl = $"{_endpoint}/ImageModeration"; var serviceParameters = new Dictionary { { "ossBucketName", bucketName }, { "ossObjectName", objectName }, { "dataId", Guid.NewGuid().ToString() } }; var moderationBody = new Dictionary { { "Service", "baselineCheck" }, { "ServiceParameters", JsonConvert.SerializeObject(serviceParameters) } }; var moderationResponse = await CallOpenApiAsync(moderationUrl, moderationBody); var rawResponse = moderationResponse; var moderationResult = JsonConvert.DeserializeObject(moderationResponse); if (moderationResult == null) { var failOnError = App.Configuration["NCC_App:ImageModeration:FailOnError"] == "true"; return new ModerationResult { Passed = !failOnError, Message = "响应解析失败", RawResponse = rawResponse }; } var code = moderationResult["Code"]?.Value(); if (code != 200) { var failOnError = App.Configuration["NCC_App:ImageModeration:FailOnError"] == "true"; return new ModerationResult { Passed = !failOnError, Message = moderationResult["Msg"]?.ToString() ?? $"审核接口返回错误:{code}", RawResponse = rawResponse, Details = moderationResult }; } var data = moderationResult["Data"]; if (data == null) { var failOnError = App.Configuration["NCC_App:ImageModeration:FailOnError"] == "true"; return new ModerationResult { Passed = !failOnError, Message = "审核结果数据为空", RawResponse = rawResponse, Details = moderationResult }; } // 增强版返回格式:Data.Result 是数组,每个元素有 Label、Confidence、RiskLevel var results = data["Result"] as JArray; var riskLevel = data["RiskLevel"]?.ToString(); if (results == null || results.Count == 0) { return new ModerationResult { Passed = true, Message = "没有审核结果,默认通过", RawResponse = rawResponse, Details = data }; } // 检查风险等级和标签 var highRiskLabels = new List(); var allResults = new List(); foreach (var item in results) { var label = item["Label"]?.ToString(); var confidence = item["Confidence"]?.Value(); var itemRiskLevel = item["RiskLevel"]?.ToString(); var description = item["Description"]?.ToString(); allResults.Add(new { label = label, confidence = confidence, riskLevel = itemRiskLevel, description = description }); // 高风险或非 nonLabel 标签视为违规 if (itemRiskLevel == "high" || (label != null && !label.StartsWith("nonLabel"))) { highRiskLabels.Add($"{label}({description})"); } } if (highRiskLabels.Count > 0 || riskLevel == "high") { return new ModerationResult { Passed = false, Message = $"图片审核未通过,风险等级:{riskLevel},违规标签:{string.Join(", ", highRiskLabels)}", RawResponse = rawResponse, Details = new { riskLevel = riskLevel, highRiskLabels = highRiskLabels, allResults = allResults } }; } return new ModerationResult { Passed = true, Message = "图片审核通过", RawResponse = rawResponse, Details = new { riskLevel = riskLevel, allResults = allResults } }; } catch (Exception ex) { var failOnError = App.Configuration["NCC_App:ImageModeration:FailOnError"] == "true"; return new ModerationResult { Passed = !failOnError, Message = $"增强版审核异常:{ex.Message}", RawResponse = ex.ToString() }; } } /// /// 调用阿里云 OpenAPI 3.0 接口(增强版使用 POP 签名) /// 注意:增强版 API 使用 OpenAPI 3.0 风格,签名方式与老版 ROA 不同 /// 建议使用官方 SDK,HTTP 原生调用需要实现完整的 POP 签名算法 /// private async Task CallOpenApiAsync(string url, Dictionary bodyParams) { // 增强版使用 OpenAPI 3.0 的 POP 签名,实现较复杂 // 暂时返回提示,建议使用 SDK 或联系技术支持实现完整签名 var failOnError = App.Configuration["NCC_App:ImageModeration:FailOnError"] == "true"; if (failOnError) { throw new Exception("增强版 API 需要实现 OpenAPI 3.0 签名,当前暂不支持 HTTP 原生调用。请使用官方 SDK 或联系技术支持。"); } return "{\"Code\":500,\"Msg\":\"增强版 API 需要 SDK 支持,当前暂不支持 HTTP 原生调用\"}"; } /// /// 上传文件到临时 OSS(使用 STS Token) /// private async Task UploadToOSSTempAsync(string url, byte[] data, string bucket, string objectName, string accessKeyId, string accessKeySecret, string securityToken) { // OSS PutObject 需要实现签名,使用 STS Token // 暂时返回 false,建议使用 OSS SDK return false; } /// /// 图片审核(老版 - green.xxx) /// /// 图片字节数组 /// 审核结果详情 private async Task ScanImageLegacyAsync(byte[] imageBytes) { // 构建请求URL var url = $"{_endpoint}/green/image/scan"; // 构建请求体 var requestBody = new { scenes = new[] { "porn", "terrorism", "ad", "qrcode" }, tasks = new[] { new { dataId = Guid.NewGuid().ToString(), url = Convert.ToBase64String(imageBytes) } } }; var jsonContent = JsonConvert.SerializeObject(requestBody); var contentBytes = Encoding.UTF8.GetBytes(jsonContent); var content = new ByteArrayContent(contentBytes); content.Headers.TryAddWithoutValidation("Content-Type", "application/json"); // 计算 Content-MD5(请求体的 MD5,然后 Base64) string contentMd5; using (var md5 = MD5.Create()) { var hashBytes = md5.ComputeHash(contentBytes); contentMd5 = Convert.ToBase64String(hashBytes); } // 生成请求时间(RFC 1123 格式) var dateStr = DateTime.UtcNow.ToString("r"); // 生成签名随机数 var signatureNonce = Guid.NewGuid().ToString(); // 构建规范化的请求头(x-acs- 开头的头,按字典序排序) var acsHeaders = new SortedDictionary { { "x-acs-signature-method", "HMAC-SHA1" }, { "x-acs-signature-nonce", signatureNonce }, { "x-acs-signature-version", "1.0" }, { "x-acs-version", "2018-05-09" } }; // 构建 CanonicalizedHeaders(格式:key:value\n) var canonicalizedHeaders = new StringBuilder(); foreach (var header in acsHeaders) { canonicalizedHeaders.Append($"{header.Key}:{header.Value}\n"); } // 构建 CanonicalizedResource(请求路径) var uri = new Uri(url); var canonicalizedResource = uri.AbsolutePath; // 构建待签名字符串 var stringToSign = new StringBuilder(); stringToSign.Append("POST\n"); // HTTP-Verb stringToSign.Append("application/json\n"); // Accept stringToSign.Append($"{contentMd5}\n"); // Content-MD5 stringToSign.Append("application/json\n"); // Content-Type stringToSign.Append($"{dateStr}\n"); // Date stringToSign.Append(canonicalizedHeaders.ToString()); // CanonicalizedHeaders stringToSign.Append(canonicalizedResource); // CanonicalizedResource // 计算签名(HMAC-SHA1) string signature; using (var hmac = new HMACSHA1(Encoding.UTF8.GetBytes(_accessKeySecret))) { var signatureBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign.ToString())); signature = Convert.ToBase64String(signatureBytes); } // 构建 Authorization 头 var authorization = $"acs {_accessKeyId}:{signature}"; // 添加 Content-MD5 到内容头(Content-MD5 是内容头,不是请求头) content.Headers.TryAddWithoutValidation("Content-MD5", contentMd5); // 创建请求并添加所有必需的请求头 using (var request = new HttpRequestMessage(HttpMethod.Post, url)) { request.Content = content; // 添加标准 HTTP 头 request.Headers.Add("Accept", "application/json"); request.Headers.Add("Date", dateStr); // 添加阿里云协议头 foreach (var header in acsHeaders) { request.Headers.Add(header.Key, header.Value); } // 添加 Authorization 头 request.Headers.Add("Authorization", authorization); var response = await _httpClient.SendAsync(request); var responseContent = await response.Content.ReadAsStringAsync(); // 保存原始响应 var rawResponse = responseContent; if (!response.IsSuccessStatusCode) { // HTTP请求失败,根据配置决定是否通过 var failOnError = App.Configuration["NCC_App:ImageModeration:FailOnError"] == "true"; return new ModerationResult { Passed = !failOnError, Message = $"HTTP请求失败:{response.StatusCode}", RawResponse = rawResponse }; } // 解析响应 var result = JsonConvert.DeserializeObject(responseContent); if (result == null) { var failOnError = App.Configuration["NCC_App:ImageModeration:FailOnError"] == "true"; return new ModerationResult { Passed = !failOnError, Message = "响应解析失败", RawResponse = rawResponse }; } // 阿里云业务错误:未开通内容安全、无权限等(code 596 或 success: false) var apiCode = result["code"]?.Value(); var success = result["success"]?.Value(); var apiMsg = result["msg"]?.ToString() ?? ""; if (apiCode == 596 || success == false) { var failOnError = App.Configuration["NCC_App:ImageModeration:FailOnError"] == "true"; var friendlyMsg = apiCode == 596 ? "内容安全服务未开通或当前账号无权限,请到阿里云控制台开通「内容安全」并确认当前 AccessKey 有权限" : (apiMsg.Length > 0 ? apiMsg : "审核接口返回失败"); return new ModerationResult { Passed = !failOnError, Message = friendlyMsg, RawResponse = rawResponse, Details = result }; } // 检查审核结果 var data = result["data"] as JArray; if (data == null || data.Count == 0) { var failOnError = App.Configuration["NCC_App:ImageModeration:FailOnError"] == "true"; return new ModerationResult { Passed = !failOnError, Message = "审核结果数据为空", RawResponse = rawResponse, Details = result }; } var taskResult = data[0]; var results = taskResult["results"] as JArray; if (results == null || results.Count == 0) { return new ModerationResult { Passed = true, Message = "没有审核结果,默认通过", RawResponse = rawResponse, Details = taskResult }; } // 检查所有场景的审核结果 var blockScenes = new List(); var allSuggestions = new List(); foreach (var item in results) { var suggestion = item["suggestion"]?.ToString(); var scene = item["scene"]?.ToString(); allSuggestions.Add(new { scene = scene, suggestion = suggestion, label = item["label"]?.ToString(), rate = item["rate"]?.ToString() }); if (suggestion == "block") { blockScenes.Add(scene ?? "unknown"); } } if (blockScenes.Count > 0) { return new ModerationResult { Passed = false, Message = $"图片审核未通过,违规场景:{string.Join(", ", blockScenes)}", RawResponse = rawResponse, Details = new { blockScenes = blockScenes, allResults = allSuggestions } }; } return new ModerationResult { Passed = true, Message = "图片审核通过", RawResponse = rawResponse, Details = new { allResults = allSuggestions } }; } } } }