Files
StoreWeb/Plugins/CloudProduct/Services/QwenInspectionIntegration.cs
T
2026-11-06 19:25:36 +08:00

113 lines
5.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
/// <summary>通过千问兼容接口识别上传图片中的文字及品牌。</summary>
public sealed class QwenInspectionIntegration : IInspectionIntegration, ISingletonDependency, IDisposable
{
private readonly HttpClient client;
private readonly string model;
/// <summary>按插件配置创建并复用模型客户端,定期更新池内连接以跟随 DNS 变化。</summary>
public QwenInspectionIntegration(PluginContext<CloudProductPlugin> 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<string[]> 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<JsonDocument>(cancellationToken);
var content = body!.RootElement.GetProperty("choices")[0].GetProperty("message").GetProperty("content").GetString();
var words = JsonSerializer.Deserialize<string[]>(content ?? throw new JsonException("模型未返回识别内容。"));
if (words == null || words.Any(word => word == null)) throw new JsonException("模型必须返回 JSON 字符串数组。");
return words;
}
}