using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; namespace AnotherReplayReader.Utils { internal enum AIValidationSeverity { Info, WeakEvidence, Warning, Contradiction, Fatal } internal enum AIValidationIssueKind { InvalidMachineReadableClaims, MissingMachineReadableClaims, MissingAlternative, WeakEvidence, MissingEvidence, InvalidEvidenceLevel, UnitCapabilityContradiction, UnitTimelineContradiction, UnsupportedGameKnowledge } internal enum AIEvidenceLevel { Confirmed, HighlyLikely, Possible, Uncertain, RuledOut } internal sealed record AIUnitClaim( string UnitId, string Player, string Claim, AIEvidenceLevel EvidenceLevel, ImmutableArray Evidence, ImmutableArray Alternatives, ImmutableArray NeedsConfirmation); internal sealed record AIEventClaim( string Claim, AIEvidenceLevel EvidenceLevel, ImmutableArray Evidence); internal sealed record AITimelineClaim( string Claim, AIEvidenceLevel EvidenceLevel, ImmutableArray Evidence); internal sealed record AIMachineReadableClaims( ImmutableArray UnitClaims, ImmutableArray EventClaims, ImmutableArray TimelineClaims) { public static AIMachineReadableClaims Empty { get; } = new( ImmutableArray.Empty, ImmutableArray.Empty, ImmutableArray.Empty); } internal sealed record AIValidationIssue( AIValidationSeverity Severity, AIValidationIssueKind Kind, string Message, string? UnitId = null, TimeSpan? Time = null); internal sealed record AIValidationResult( AIMachineReadableClaims Claims, ImmutableArray Issues) { public static AIValidationResult Empty { get; } = new(AIMachineReadableClaims.Empty, ImmutableArray.Empty); public bool RequiresRevision => Issues.Any(i => i.Severity is AIValidationSeverity.Contradiction or AIValidationSeverity.Fatal); public bool HasIssues => !Issues.IsEmpty; } internal static class AIAnalysisValidation { private static readonly Regex _jsonFenceRegex = new( @"```(?:json)?\s*(\{[\s\S]*?\})\s*```", RegexOptions.IgnoreCase | RegexOptions.Compiled); public static AIValidationResult ValidateMachineReadableClaims(string response) { var issues = ImmutableArray.CreateBuilder(); var json = ExtractJsonObject(response); if (json is null || string.IsNullOrWhiteSpace(json)) { issues.Add(new AIValidationIssue( AIValidationSeverity.Warning, AIValidationIssueKind.MissingMachineReadableClaims, "AI 未输出机器可读声明,无法进行自动验证。")); return new AIValidationResult(AIMachineReadableClaims.Empty, issues.ToImmutable()); } var claims = ParseClaims(json, issues); if (claims is null) { return new AIValidationResult(AIMachineReadableClaims.Empty, issues.ToImmutable()); } ValidateClaimSelfConsistency(claims, issues); return new AIValidationResult(claims, issues.ToImmutable()); } public static string FormatIssues(ImmutableArray issues) { if (issues.IsEmpty) { return "未发现机器可读声明问题。"; } var sb = new StringBuilder(); foreach (var issue in issues) { var unitText = string.IsNullOrWhiteSpace(issue.UnitId) ? string.Empty : $" UnitId={issue.UnitId}"; sb.AppendLine($"[{issue.Severity}/{issue.Kind}]{unitText} {issue.Message}"); } return sb.ToString().TrimEnd(); } private static string? ExtractJsonObject(string response) { var markerIndex = response.LastIndexOf("[机器可读声明]", StringComparison.OrdinalIgnoreCase); var searchText = markerIndex >= 0 ? response.Substring(markerIndex) : response; var matches = _jsonFenceRegex.Matches(searchText); if (matches.Count > 0) { return matches[matches.Count - 1].Groups[1].Value; } var start = searchText.LastIndexOf('{'); var end = searchText.LastIndexOf('}'); if (start >= 0 && end > start) { return searchText.Substring(start, end - start + 1); } return null; } private static AIMachineReadableClaims? ParseClaims( string json, ImmutableArray.Builder issues) { try { using var doc = JsonDocument.Parse(json, new JsonDocumentOptions { AllowTrailingCommas = true, CommentHandling = JsonCommentHandling.Skip }); var root = doc.RootElement; if (root.ValueKind != JsonValueKind.Object) { issues.Add(new AIValidationIssue( AIValidationSeverity.Warning, AIValidationIssueKind.InvalidMachineReadableClaims, "机器可读声明不是 JSON object。")); return null; } return new AIMachineReadableClaims( ReadUnitClaims(root), ReadSimpleClaims(root, "eventClaims") .Select(c => new AIEventClaim(c.Claim, c.EvidenceLevel, c.Evidence)) .ToImmutableArray(), ReadSimpleClaims(root, "timelineClaims") .Select(c => new AITimelineClaim(c.Claim, c.EvidenceLevel, c.Evidence)) .ToImmutableArray()); } catch (JsonException ex) { issues.Add(new AIValidationIssue( AIValidationSeverity.Warning, AIValidationIssueKind.InvalidMachineReadableClaims, $"机器可读声明 JSON 解析失败:{ex.Message}")); return null; } } private static ImmutableArray ReadUnitClaims(JsonElement root) { if (!TryGetArray(root, "unitClaims", out var unitClaims)) { return ImmutableArray.Empty; } var result = ImmutableArray.CreateBuilder(); foreach (var item in unitClaims.EnumerateArray()) { if (item.ValueKind != JsonValueKind.Object) { continue; } result.Add(new AIUnitClaim( ReadFlexibleString(item, "unitId"), ReadFlexibleString(item, "player"), ReadFlexibleString(item, "claim"), ReadEvidenceLevel(item), ReadStringArray(item, "evidence"), ReadStringArray(item, "alternatives"), ReadStringArray(item, "needsConfirmation"))); } return result.ToImmutable(); } private sealed record SimpleClaim( string Claim, AIEvidenceLevel EvidenceLevel, ImmutableArray Evidence); private static ImmutableArray ReadSimpleClaims(JsonElement root, string propertyName) { if (!TryGetArray(root, propertyName, out var claims)) { return ImmutableArray.Empty; } var result = ImmutableArray.CreateBuilder(); foreach (var item in claims.EnumerateArray()) { if (item.ValueKind != JsonValueKind.Object) { continue; } result.Add(new SimpleClaim( ReadFlexibleString(item, "claim"), ReadEvidenceLevel(item), ReadStringArray(item, "evidence"))); } return result.ToImmutable(); } private static void ValidateClaimSelfConsistency( AIMachineReadableClaims claims, ImmutableArray.Builder issues) { foreach (var claim in claims.UnitClaims) { if (string.IsNullOrWhiteSpace(claim.UnitId)) { issues.Add(new AIValidationIssue( AIValidationSeverity.Warning, AIValidationIssueKind.InvalidMachineReadableClaims, "Unit claim 缺少 unitId。")); } if (string.IsNullOrWhiteSpace(claim.Claim)) { issues.Add(new AIValidationIssue( AIValidationSeverity.Warning, AIValidationIssueKind.InvalidMachineReadableClaims, "Unit claim 缺少 claim。", claim.UnitId)); } if (claim.EvidenceLevel is AIEvidenceLevel.Confirmed or AIEvidenceLevel.HighlyLikely && claim.Evidence.IsEmpty) { issues.Add(new AIValidationIssue( AIValidationSeverity.Warning, AIValidationIssueKind.MissingEvidence, "高置信 UnitId 推测缺少 evidence。", claim.UnitId)); } if (claim.EvidenceLevel is AIEvidenceLevel.Possible or AIEvidenceLevel.Uncertain && claim.Alternatives.IsEmpty && claim.NeedsConfirmation.IsEmpty) { issues.Add(new AIValidationIssue( AIValidationSeverity.WeakEvidence, AIValidationIssueKind.MissingAlternative, "低置信 UnitId 推测应提供 alternatives 或 needsConfirmation。", claim.UnitId)); } } } private static bool TryGetArray(JsonElement root, string propertyName, out JsonElement array) { if (root.TryGetProperty(propertyName, out array) && array.ValueKind == JsonValueKind.Array) { return true; } array = default; return false; } private static AIEvidenceLevel ReadEvidenceLevel(JsonElement item) { var value = ReadFlexibleString(item, "evidenceLevel"); return NormalizeEvidenceLevel(value); } private static AIEvidenceLevel NormalizeEvidenceLevel(string value) { value = value.Trim().Replace("_", "").Replace("-", "").Replace(" ", ""); return value.ToLowerInvariant() switch { "confirmed" or "确定" => AIEvidenceLevel.Confirmed, "highlylikely" or "high" or "高度可能" => AIEvidenceLevel.HighlyLikely, "possible" or "可能" => AIEvidenceLevel.Possible, "ruledout" or "excluded" or "已排除" => AIEvidenceLevel.RuledOut, _ => AIEvidenceLevel.Uncertain, }; } private static string ReadFlexibleString(JsonElement item, string propertyName) { if (!item.TryGetProperty(propertyName, out var value)) { return string.Empty; } return value.ValueKind switch { JsonValueKind.String => value.GetString() ?? string.Empty, JsonValueKind.Number => value.GetRawText(), JsonValueKind.True => "true", JsonValueKind.False => "false", _ => string.Empty, }; } private static ImmutableArray ReadStringArray(JsonElement item, string propertyName) { if (!item.TryGetProperty(propertyName, out var value)) { return ImmutableArray.Empty; } if (value.ValueKind == JsonValueKind.String) { var text = value.GetString(); return string.IsNullOrWhiteSpace(text) ? ImmutableArray.Empty : ImmutableArray.Create(text!); } if (value.ValueKind != JsonValueKind.Array) { return ImmutableArray.Empty; } var result = new List(); foreach (var element in value.EnumerateArray()) { var text = element.ValueKind == JsonValueKind.String ? element.GetString() : element.GetRawText(); if (!string.IsNullOrWhiteSpace(text)) { result.Add(text!); } } return result.ToImmutableArray(); } } }