using Mango.Nop.Core.Loggers;
using Nop.Services.Configuration;
using System.Text;
using System.Text.Json;
#nullable enable
namespace Nop.Plugin.Misc.FruitBankPlugin.Services
{
///
/// Z.ai GLM-OCR service — szállítólevelek, rendelési dokumentumok strukturált szövegkinyerésére.
/// Endpoint: POST https://api.z.ai/api/paas/v4/layout_parsing
/// Konfiguráció: FruitBankSettings.ZaiApiKey (+ opcionális ZaiModel, default: "glm-ocr")
///
/// Output formátum: Markdown + HTML vegyes szöveg (md_results mező).
/// A táblázatokat <table>/<thead>/<td> tagekben adja vissza — LLM-nek közvetlenül átadható.
///
public class ZaiService
{
private const string LayoutParsingEndpoint = "https://api.z.ai/api/paas/v4/layout_parsing";
private const string DefaultModel = "glm-ocr";
private readonly ISettingService _settingService;
private readonly FruitBankSettings _settings;
private readonly HttpClient _httpClient;
private readonly ILogger _logger;
public ZaiService(
ISettingService settingService,
HttpClient httpClient,
ILogger logger)
{
_settingService = settingService;
_settings = _settingService.LoadSetting();
_httpClient = httpClient;
_logger = logger;
}
private string ApiKey => _settings.ZaiApiKey
?? throw new InvalidOperationException("ZAI API kulcs nincs konfigurálva (FruitBankSettings.ZaiApiKey).");
private string Model => string.IsNullOrWhiteSpace(_settings.ZaiModel)
? DefaultModel
: _settings.ZaiModel;
// ── Publikus API ─────────────────────────────────────────────────────────────
///
/// OCR elemzés nyilvánosan elérhető URL alapján (kép vagy PDF).
///
/// Nyilvánosan elérhető HTTP(S) URL. Kép: max 10 MB, PDF: max 50 MB / 100 oldal.
public async Task AnalyzeUrlAsync(string fileUrl)
{
if (string.IsNullOrWhiteSpace(fileUrl))
return ZaiOcrResult.Failure("A fileUrl paraméter üres.");
var body = JsonSerializer.Serialize(new { model = Model, file = fileUrl });
return await CallApiAsync(body);
}
///
/// OCR elemzés memóriából (Stream).
/// A stream tartalma base64-re konvertálódik, majd data URI-ként kerül az API-hoz.
///
/// Kép vagy PDF stream.
/// MIME típus, pl. "image/jpeg", "application/pdf".
public async Task AnalyzeStreamAsync(Stream stream, string mimeType)
{
if (stream == null || stream.Length == 0)
return ZaiOcrResult.Failure("Az átadott stream üres.");
byte[] bytes;
using (var ms = new MemoryStream())
{
await stream.CopyToAsync(ms);
bytes = ms.ToArray();
}
return await AnalyzeBase64Async(Convert.ToBase64String(bytes), mimeType);
}
///
/// OCR elemzés base64 kódolt adat alapján.
/// Ha az adat még nem tartalmazza a "data:" prefixet, automatikusan data URI-vá alakítja.
///
/// Nyers base64 vagy teljes data URI.
/// MIME típus (csak nyers base64 esetén szükséges).
public async Task AnalyzeBase64Async(string base64Data, string mimeType = "image/jpeg")
{
if (string.IsNullOrWhiteSpace(base64Data))
return ZaiOcrResult.Failure("A base64Data paraméter üres.");
var dataUri = base64Data.StartsWith("data:", StringComparison.OrdinalIgnoreCase)
? base64Data
: $"data:{mimeType};base64,{base64Data}";
var body = JsonSerializer.Serialize(new { model = Model, file = dataUri });
return await CallApiAsync(body);
}
// ── Segédmetódus: MIME típus meghatározása fájlnév alapján ──────────────────
///
/// Fájlkiterjesztés alapján visszaadja a megfelelő MIME típust.
///
public static string GetMimeType(string fileName)
{
var ext = Path.GetExtension(fileName).ToLowerInvariant();
return ext switch
{
".pdf" => "application/pdf",
".jpg" or ".jpeg" => "image/jpeg",
".png" => "image/png",
".gif" => "image/gif",
".webp" => "image/webp",
".tif" or ".tiff" => "image/tiff",
_ => "application/octet-stream"
};
}
// ── Belső API hívás ──────────────────────────────────────────────────────────
private async Task CallApiAsync(string jsonBody)
{
using var request = new HttpRequestMessage(HttpMethod.Post, LayoutParsingEndpoint);
request.Headers.Add("Authorization", $"Bearer {ApiKey}");
request.Content = new StringContent(jsonBody, Encoding.UTF8, "application/json");
try
{
var response = await _httpClient.SendAsync(request);
var responseBody = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
_logger.Error("ZAI API hiba {StatusCode}: {Body}", new Exception(responseBody));
return ZaiOcrResult.Failure($"API hiba {(int)response.StatusCode}: {responseBody}");
}
using var doc = JsonDocument.Parse(responseBody);
var root = doc.RootElement;
var markdown = TryGetMarkdown(root);
// Token statisztika logolása (debug szinten, hogy ne spammelje a logot)
if (root.TryGetProperty("usage", out var usage))
{
var prompt = usage.TryGetProperty("prompt_tokens", out var pt) ? pt.GetInt32() : 0;
var completion = usage.TryGetProperty("completion_tokens", out var ct) ? ct.GetInt32() : 0;
_logger.Debug("ZAI GLM-OCR token felhasználás: {Prompt} + {Completion} = {Total}" +$"{prompt}, {completion}, {prompt} + {completion}");
}
if (string.IsNullOrEmpty(markdown))
{
_logger.Warning("ZAI GLM-OCR: md_results mező üres. Raw válasz: {Body}", responseBody);
return ZaiOcrResult.Failure("Az OCR eredmény üres (md_results mező hiányzik a válaszból).");
}
_logger.Debug($"ZAI GLM-OCR sikeres, karakter { markdown.Length}");
return ZaiOcrResult.Success(markdown, responseBody);
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
_logger.Error("ZAI GLM-OCR időtúllépés", ex);
return ZaiOcrResult.Failure("Időtúllépés: a GLM-OCR API nem válaszolt időben. Nagy PDF-eknél növeld a HttpClient timeout-ját.");
}
catch (Exception ex)
{
_logger.Error("ZAI GLM-OCR hívás kivétellel végződött", ex);
return ZaiOcrResult.Failure($"Hálózati hiba: {ex.Message}");
}
}
///
/// Az md_results mezőt keresi elsőként (layout_parsing API), majd fallback-eket próbál
/// a chat completion API formátumhoz — így a service toleráns az esetleges API verziókkal szemben.
///
private static string TryGetMarkdown(JsonElement root)
{
// Elsődleges: layout_parsing endpoint saját mezője
if (root.TryGetProperty("md_results", out var mdResults))
{
var val = mdResults.GetString();
if (!string.IsNullOrEmpty(val)) return val;
}
// Fallback 1: chat completion stílusú choices tömb
if (root.TryGetProperty("choices", out var choices) &&
choices.GetArrayLength() > 0 &&
choices[0].TryGetProperty("message", out var msg) &&
msg.TryGetProperty("content", out var content))
{
var val = content.GetString();
if (!string.IsNullOrEmpty(val)) return val;
}
// Fallback 2: egyszerű result mező
if (root.TryGetProperty("result", out var result))
{
var val = result.GetString();
if (!string.IsNullOrEmpty(val)) return val;
}
return string.Empty;
}
}
// ── Result record ────────────────────────────────────────────────────────────────
///
/// A ZaiService által visszaadott OCR eredmény.
///
public sealed class ZaiOcrResult
{
public bool IsSuccess { get; private init; }
///
/// A teljes dokumentum Markdown+HTML vegyes formátumban.
/// Táblázatokat <table>/<th>/<td> tagek tartalmazzák — LLM promptba közvetlenül illeszthető.
///
public string Markdown { get; private init; } = string.Empty;
///
/// A nyers JSON válasz (diagnosztikához / layout_details feldolgozáshoz).
///
public string? RawResponse { get; private init; }
public string? ErrorMessage { get; private init; }
public static ZaiOcrResult Success(string markdown, string raw) =>
new() { IsSuccess = true, Markdown = markdown, RawResponse = raw };
public static ZaiOcrResult Failure(string error) =>
new() { IsSuccess = false, ErrorMessage = error };
}
}