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;
}
}