using System.Net; using System.Net.Http.Json; using System.Text.Json; using MAX.Core.DependencyInjection; using MAX.Plugins; using Store.Modules.CloudProduct.Contracts; namespace Store.Modules.CloudProduct.Services; /// 通过千问兼容接口识别上传图片中的文字及品牌。 public sealed class QwenInspectionIntegration : IInspectionIntegration, ISingletonDependency, IDisposable { private readonly HttpClient client; private readonly string model; /// 按插件配置创建并复用模型客户端,定期更新池内连接以跟随 DNS 变化。 public QwenInspectionIntegration(PluginContext context) { model = context.RequireConfiguration("ProductInspection:Model"); var endpoint = new Uri(context.RequireConfiguration("ProductInspection:Endpoint")); var key = context.Configuration["ProductInspection:ApiKey"]; if (string.IsNullOrWhiteSpace(key)) key = Environment.GetEnvironmentVariable("DASHSCOPE_API_KEY"); client = new HttpClient(new SocketsHttpHandler { PooledConnectionLifetime = TimeSpan.FromMinutes(5), //Proxy = new WebProxy("http://127.0.0.1:8080") }) { BaseAddress = endpoint, Timeout = TimeSpan.FromMinutes(10) }; if (!string.IsNullOrWhiteSpace(key)) client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", key); } public void Dispose() => client.Dispose(); private const string Prompt = """ 你是图像品牌与文字识别引擎。请识别图片中的文字和品牌 Logo,并严格按下方规则输出。 【识别范围】 - 所有可见文字(标题、正文、按钮文字、标签、说明等),原样识别输出。 - 品牌 Logo:识别 Logo 对应的品牌,输出品牌中文名(不输出 Logo 上的原始英文/字母)。 - 图片、图标、二维码、条形码、分隔线等非文字非品牌元素:忽略,不输出。 【Logo 处理规则】 - 遇到品牌 Logo 时,不要输出 Logo 上的原始英文/字母。 - 必须输出该品牌对应的中文品牌名。 - 示例: * 看到 "ERKE" 或鸿鹄图形 → 输出 "鸿星尔克" * 看到 "Nike" 或钩子图形 → 输出 "耐克" * 看到 "Adidas" 或三叶草/三条纹 → 输出 "阿迪达斯" * 看到 "Li-Ning" 或李宁图形 → 输出 "李宁" * 看到 "ANTA" 或安踏图形 → 输出 "安踏" * 看到 "Xtep" 或特步图形 → 输出 "特步" * 看到 "361°" → 输出 "361度" - 如遇无法识别为已知品牌的 Logo,输出 "未知品牌"。 - 如 Logo 上同时有中英文,优先输出中文品牌名。 【品牌名归一化规则(重要)】 - 输出的品牌必须是一个独立、纯净的品牌名,不能包含子品牌、系列名、品类词、修饰词,也不能用连接符拼接。 - 子品牌或系列要归并到主品牌,只输出主品牌名。 - 正例: * "安踏儿童" → 输出 "安踏"(不要输出 "安踏儿童") * "安踏冠军系列" → 输出 "安踏" * "Nike Air" / "耐克 Air" → 输出 "耐克" * "Adidas Originals" / "阿迪达斯三叶草" → 输出 "阿迪达斯" * "李宁中国李宁" → 输出 "李宁" * "鸿星尔克ERKE" → 输出 "鸿星尔克" - 反例(禁止输出): * "安踏儿童"、"安踏-儿童"、"安踏 儿童"、"安踏(儿童)" * "耐克/Nike"、"阿迪达斯 Adidas" - 禁止使用 "/"、"-"、"·"、空格、括号等连接符拼接多个名称。 【输出要求】 1. 只输出一个 JSON 字符串数组,不要输出对象、markdown 代码块、解释性文字或多余内容。 2. 格式固定为:["文字1", "文字2", "文字3", ...] 3. 按从上到下、从左到右的阅读顺序排列。 4. 每个元素只包含文字本身,不包含类型、坐标等额外字段。 5. 文字中的双引号需转义(\"),确保输出是合法 JSON。 6. 若图中没有任何可识别文字或品牌,输出 []。 """; public async Task AnalyzeAsync(Stream image, string mediaType, CancellationToken cancellationToken) { if (client.DefaultRequestHeaders.Authorization is null) throw new InvalidOperationException("请配置 ProductInspection:ApiKey 或 DASHSCOPE_API_KEY。"); using var buffer = new MemoryStream(); await image.CopyToAsync(buffer, cancellationToken); var url = $"data:{mediaType};base64,{Convert.ToBase64String(buffer.GetBuffer(), 0, (int)buffer.Length)}"; using var response = await client.PostAsJsonAsync("", new { model, messages = new[] { new { role = "user", content = new object[] { new { type = "image_url", image_url = new { url } }, new { type = "text", text = Prompt } } } }, stream = false, enable_thinking = false }, cancellationToken); response.EnsureSuccessStatusCode(); using var body = await response.Content.ReadFromJsonAsync(cancellationToken); var content = body!.RootElement.GetProperty("choices")[0].GetProperty("message").GetProperty("content").GetString(); var words = JsonSerializer.Deserialize(content ?? throw new JsonException("模型未返回识别内容。")); if (words == null || words.Any(word => word == null)) throw new JsonException("模型必须返回 JSON 字符串数组。"); return words; } }