113 lines
5.5 KiB
C#
113 lines
5.5 KiB
C#
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;
|
||
}
|
||
}
|