mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-08-06 01:42:12 +00:00
Some checks failed
Build and Release / Determine run mode (push) Has been cancelled
Build and Release / Read metadata (push) Has been cancelled
Build and Release / Sync Flatpak repo (push) Has been cancelled
Build and Release / Collect Flatpak artifacts (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Prepare & create release (push) Has been cancelled
Build and Release / Publish release (push) Has been cancelled
346 lines
16 KiB
C#
346 lines
16 KiB
C#
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Text.RegularExpressions;
|
|
|
|
using HtmlAgilityPack;
|
|
|
|
namespace AIStudio.Assistants.VisualBriefing;
|
|
|
|
public sealed partial class VisualBriefingArtifactService
|
|
{
|
|
/// <summary>
|
|
/// Matches the version-independent artifact header at the start of standalone HTML.
|
|
/// </summary>
|
|
private static readonly Regex HEADER_REGEX = HeaderRegex();
|
|
|
|
/// <summary>
|
|
/// Matches the version-independent artifact header at the start of standalone HTML.
|
|
/// </summary>
|
|
[GeneratedRegex(@"\A<!doctype html>\n<!--MWAI_VISUAL_BRIEFING_HEADER:(?<value>[A-Za-z0-9+/=]+)-->\n", RegexOptions.CultureInvariant)]
|
|
private static partial Regex HeaderRegex();
|
|
|
|
/// <summary>
|
|
/// Matches the generated presentation stylesheet.
|
|
/// </summary>
|
|
private static readonly Regex STYLE_REGEX = StyleRegex();
|
|
|
|
/// <summary>
|
|
/// Matches the generated presentation stylesheet.
|
|
/// </summary>
|
|
[GeneratedRegex("""<style\s+id="mwai-briefing-style">(?<value>[\s\S]*?)</style>""", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
|
|
private static partial Regex StyleRegex();
|
|
|
|
/// <summary>
|
|
/// Matches the embedded declarative runtime.
|
|
/// </summary>
|
|
private static readonly Regex RUNTIME_REGEX = RuntimeRegex();
|
|
|
|
/// <summary>
|
|
/// Matches the embedded declarative runtime.
|
|
/// </summary>
|
|
[GeneratedRegex("""<script\s+id="mwai-briefing-runtime">(?<value>[\s\S]*?)</script>""", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
|
|
private static partial Regex RuntimeRegex();
|
|
|
|
/// <summary>
|
|
/// Matches the optional embedded chart runtime.
|
|
/// </summary>
|
|
private static readonly Regex ECHARTS_REGEX = EChartsRegex();
|
|
|
|
/// <summary>
|
|
/// Matches the optional embedded chart runtime.
|
|
/// </summary>
|
|
[GeneratedRegex("""<script\s+id="mwai-echarts-runtime">(?<value>[\s\S]*?)</script>""", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
|
|
private static partial Regex EChartsRegex();
|
|
|
|
/// <summary>
|
|
/// Reads an intact standalone artifact without applying current compiler or runtime rules.
|
|
/// </summary>
|
|
public static bool TryParse(string html, out VisualBriefingArtifactParts parts, out string issue)
|
|
{
|
|
parts = null!;
|
|
issue = string.Empty;
|
|
|
|
if (string.IsNullOrWhiteSpace(html))
|
|
{
|
|
issue = "The briefing file is empty.";
|
|
return false;
|
|
}
|
|
|
|
if (!html.EndsWith("</html>", StringComparison.Ordinal))
|
|
{
|
|
issue = "The briefing document wrapper is invalid or incomplete.";
|
|
return false;
|
|
}
|
|
|
|
var headerMatch = HEADER_REGEX.Match(html);
|
|
if (!headerMatch.Success)
|
|
{
|
|
issue = "The briefing artifact header is missing or misplaced.";
|
|
return false;
|
|
}
|
|
|
|
VisualBriefingExportManifest? exportManifest;
|
|
try
|
|
{
|
|
var json = Encoding.UTF8.GetString(Convert.FromBase64String(headerMatch.Groups["value"].Value));
|
|
using var headerDocument = JsonDocument.Parse(json);
|
|
exportManifest = HasDuplicateProperties(headerDocument.RootElement)
|
|
? null
|
|
: headerDocument.RootElement.Deserialize<VisualBriefingExportManifest>(JSON_OPTIONS);
|
|
}
|
|
catch (Exception exception) when (exception is FormatException or JsonException)
|
|
{
|
|
issue = "The briefing artifact header is invalid.";
|
|
return false;
|
|
}
|
|
|
|
if (!ValidateHeader(exportManifest, out issue))
|
|
return false;
|
|
|
|
var documentHash = exportManifest!.DocumentHash;
|
|
exportManifest.DocumentHash = DOCUMENT_HASH_PLACEHOLDER;
|
|
var placeholderHeader = $"<!doctype html>\n<!--{HEADER_MARKER}{EncodeHeader(exportManifest)}-->\n";
|
|
exportManifest.DocumentHash = documentHash;
|
|
var placeholderDocument = placeholderHeader + html[headerMatch.Length..];
|
|
var computedDocumentHash = VisualBriefingHashing.Compute(placeholderDocument);
|
|
if (!string.Equals(computedDocumentHash, documentHash, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
issue = "The briefing document hash does not match its contents.";
|
|
return false;
|
|
}
|
|
|
|
var document = new HtmlDocument();
|
|
document.LoadHtml(html);
|
|
|
|
var htmlNode = FindUniqueNode(document, "//html");
|
|
var headNode = FindUniqueNode(document, "//head");
|
|
var bodyNode = FindUniqueNode(document, "//body");
|
|
var dataNode = FindUniqueElementById(document, DATA_ELEMENT_ID);
|
|
var rootNode = FindUniqueElementById(document, "mwai-briefing-root");
|
|
var footerNode = FindUniqueElementById(document, "mwai-static-footer");
|
|
var headerNodes = FindNodes(document.DocumentNode, "//*[@id='mwai-static-header']")?.ToArray() ?? [];
|
|
var generatedStyleNode = FindUniqueElementById(document, "mwai-briefing-style");
|
|
var protectedStyleNode = FindUniqueElementById(document, "mwai-protected-style");
|
|
var runtimeNode = FindUniqueElementById(document, "mwai-briefing-runtime");
|
|
var echartsNode = FindUniqueElementById(document, "mwai-echarts-runtime");
|
|
var styleMatch = STYLE_REGEX.Match(html);
|
|
var runtimeMatch = RUNTIME_REGEX.Match(html);
|
|
var echartsMatch = ECHARTS_REGEX.Match(html);
|
|
|
|
if (htmlNode is null || headNode is null || bodyNode is null || dataNode is null || rootNode is null ||
|
|
footerNode is null || generatedStyleNode is null || protectedStyleNode is null || runtimeNode is null ||
|
|
headerNodes.Length > 1 ||
|
|
(headerNodes.Length == 1 &&
|
|
(!headerNodes[0].Name.Equals("header", StringComparison.OrdinalIgnoreCase) || headerNodes[0].ParentNode != bodyNode)) ||
|
|
!styleMatch.Success || !runtimeMatch.Success || (echartsNode is not null) != echartsMatch.Success)
|
|
{
|
|
issue = "The briefing envelope is incomplete or ambiguous.";
|
|
return false;
|
|
}
|
|
|
|
var scriptNodes = FindNodes(document.DocumentNode, "//script")?.ToArray() ?? [];
|
|
var styleNodes = FindNodes(document.DocumentNode, "//style")?.ToArray() ?? [];
|
|
if (scriptNodes.Any(node => node.Id is not DATA_ELEMENT_ID and not "mwai-echarts-runtime" and not "mwai-briefing-runtime") ||
|
|
scriptNodes.Count(node => node.Id == DATA_ELEMENT_ID) != 1 ||
|
|
scriptNodes.Count(node => node.Id == "mwai-briefing-runtime") != 1 ||
|
|
scriptNodes.Count(node => node.Id == "mwai-echarts-runtime") > 1 ||
|
|
styleNodes.Length != 2 ||
|
|
styleNodes.Count(node => node.Id == "mwai-briefing-style") != 1 ||
|
|
styleNodes.Count(node => node.Id == "mwai-protected-style") != 1 ||
|
|
!string.Equals(dataNode.GetAttributeValue("type", string.Empty), "application/json", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
issue = "The briefing contains unknown or duplicated executable resources.";
|
|
return false;
|
|
}
|
|
|
|
var bodyChildren = FindNodes(document.DocumentNode, "//body/*")?.ToArray() ?? [];
|
|
var allowedBodyIds = new HashSet<string>(StringComparer.Ordinal)
|
|
{
|
|
DATA_ELEMENT_ID,
|
|
"mwai-static-header",
|
|
"mwai-briefing-root",
|
|
"mwai-static-footer",
|
|
"mwai-echarts-runtime",
|
|
"mwai-briefing-runtime",
|
|
};
|
|
if (bodyChildren.Any(node => !allowedBodyIds.Contains(node.Id)) ||
|
|
bodyChildren.Select(node => node.Id).Distinct(StringComparer.Ordinal).Count() != bodyChildren.Length)
|
|
{
|
|
issue = "The briefing body contains elements outside the stable artifact envelope.";
|
|
return false;
|
|
}
|
|
|
|
JsonElement data;
|
|
try
|
|
{
|
|
using var parsedData = JsonDocument.Parse(dataNode.InnerText);
|
|
data = parsedData.RootElement.Clone();
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
issue = "The briefing data block is invalid.";
|
|
return false;
|
|
}
|
|
|
|
var template = CanonicalizeTemplate(rootNode.InnerHtml);
|
|
var css = styleMatch.Groups["value"].Value.Trim();
|
|
var runtime = runtimeMatch.Groups["value"].Value;
|
|
var echarts = echartsMatch.Success ? echartsMatch.Groups["value"].Value : null;
|
|
parts = new(exportManifest, data, template, css, runtime, echarts, documentHash);
|
|
|
|
var cspNodes = FindNodes(document.DocumentNode, "//meta[@http-equiv='Content-Security-Policy']")?.ToArray() ?? [];
|
|
var actualCsp = cspNodes.Length == 1
|
|
? cspNodes[0].GetAttributeValue("content", string.Empty)
|
|
: string.Empty;
|
|
if (!string.Equals(actualCsp, GetContentSecurityPolicy(parts), StringComparison.Ordinal))
|
|
{
|
|
parts = null!;
|
|
issue = "The briefing Content Security Policy is missing or inconsistent with its embedded scripts.";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads an intact artifact and additionally applies the current semantic compiler contract.
|
|
/// </summary>
|
|
internal static bool TryParseForRecompile(string html, out VisualBriefingArtifactParts parts, out string issue)
|
|
{
|
|
if (!TryParse(html, out parts, out issue))
|
|
return false;
|
|
|
|
if (parts.ExportManifest.SchemaVersion != VisualBriefingVersions.SCHEMA)
|
|
{
|
|
parts = null!;
|
|
issue = "The briefing data schema is not compatible with the current compiler.";
|
|
return false;
|
|
}
|
|
|
|
issue = ValidateProtectedData(parts.ExportManifest, parts.Data);
|
|
if (!string.IsNullOrEmpty(issue))
|
|
{
|
|
parts = null!;
|
|
return false;
|
|
}
|
|
|
|
issue = ValidateGeneratedParts(
|
|
null,
|
|
parts.Data,
|
|
parts.TemplateHtml,
|
|
parts.Css,
|
|
!string.IsNullOrWhiteSpace(parts.EChartsScript));
|
|
if (!string.IsNullOrEmpty(issue))
|
|
{
|
|
parts = null!;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates stable artifact-header fields without imposing current runtime or schema versions.
|
|
/// </summary>
|
|
private static bool ValidateHeader(VisualBriefingExportManifest? exportManifest, out string issue)
|
|
{
|
|
issue = string.Empty;
|
|
if (exportManifest is null ||
|
|
exportManifest.ArtifactVersion != VisualBriefingVersions.ARTIFACT ||
|
|
exportManifest.SchemaVersion <= 0 ||
|
|
exportManifest.RuntimeVersion <= 0 ||
|
|
exportManifest.BriefingId == Guid.Empty ||
|
|
exportManifest.RevisionId == Guid.Empty ||
|
|
string.IsNullOrWhiteSpace(exportManifest.Name) ||
|
|
string.IsNullOrWhiteSpace(exportManifest.AIStudioVersion) ||
|
|
string.IsNullOrWhiteSpace(exportManifest.RuntimeAIStudioVersion) ||
|
|
exportManifest.DocumentHash.Length != 64 ||
|
|
!exportManifest.DocumentHash.All(Uri.IsHexDigit) ||
|
|
exportManifest.TargetLanguage is CommonLanguages.OTHER && string.IsNullOrWhiteSpace(exportManifest.CustomTargetLanguage) ||
|
|
exportManifest.ProtectionLevel is VisualBriefingProtectionLevel.OTHER && string.IsNullOrWhiteSpace(exportManifest.CustomProtectionLevel))
|
|
{
|
|
issue = "The briefing artifact header contains invalid or unsupported metadata.";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds exactly one node for an XPath expression.
|
|
/// </summary>
|
|
private static HtmlNode? FindUniqueNode(HtmlDocument document, string xpath)
|
|
{
|
|
var nodes = FindNodes(document.DocumentNode, xpath)?.ToArray() ?? [];
|
|
return nodes.Length == 1 ? nodes[0] : null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds exactly one element by ID.
|
|
/// </summary>
|
|
private static HtmlNode? FindUniqueElementById(HtmlDocument document, string id)
|
|
{
|
|
var nodes = FindNodes(document.DocumentNode, $"//*[@id='{id}']")?.ToArray() ?? [];
|
|
return nodes.Length == 1 ? nodes[0] : null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates current protected data needed for recompilation.
|
|
/// </summary>
|
|
private static string ValidateProtectedData(VisualBriefingExportManifest exportManifest, JsonElement data)
|
|
{
|
|
if (!data.TryGetProperty("_mwai", out var protectedData) ||
|
|
protectedData.ValueKind is not JsonValueKind.Object ||
|
|
!protectedData.TryGetProperty("schemaVersion", out var schemaVersion) ||
|
|
schemaVersion.ValueKind is not JsonValueKind.Number ||
|
|
!schemaVersion.TryGetInt32(out var parsedSchemaVersion) ||
|
|
parsedSchemaVersion != VisualBriefingVersions.SCHEMA ||
|
|
!protectedData.TryGetProperty("runtimeVersion", out var runtimeVersion) ||
|
|
runtimeVersion.ValueKind is not JsonValueKind.Number ||
|
|
!runtimeVersion.TryGetInt32(out var parsedRuntimeVersion) ||
|
|
parsedRuntimeVersion != exportManifest.RuntimeVersion ||
|
|
!protectedData.TryGetProperty("aiStudioVersion", out var aiStudioVersion) ||
|
|
aiStudioVersion.ValueKind is not JsonValueKind.String ||
|
|
!string.Equals(aiStudioVersion.GetString(), exportManifest.AIStudioVersion, StringComparison.Ordinal) ||
|
|
!protectedData.TryGetProperty("assets", out var protectedAssets) ||
|
|
protectedAssets.ValueKind is not JsonValueKind.Object ||
|
|
data.TryGetProperty("assets", out _))
|
|
return "The protected briefing data block is incomplete or inconsistent.";
|
|
|
|
var protectedAssetProperties = protectedAssets.EnumerateObject().ToArray();
|
|
if (protectedAssetProperties.Any(property =>
|
|
property.Value.ValueKind is not JsonValueKind.String ||
|
|
!property.Value.GetString()!.StartsWith("data:image/", StringComparison.Ordinal)) ||
|
|
protectedAssetProperties.Select(property => property.Name).Distinct(StringComparer.Ordinal).Count() != protectedAssetProperties.Length)
|
|
return "The protected embedded asset map contains invalid or duplicated entries.";
|
|
|
|
if (!protectedData.TryGetProperty("assetMetadata", out var assetMetadata) ||
|
|
assetMetadata.ValueKind is not JsonValueKind.Object)
|
|
return "The protected visual asset metadata is missing.";
|
|
|
|
var metadataProperties = assetMetadata.EnumerateObject().ToArray();
|
|
if (metadataProperties.Length != protectedAssetProperties.Length ||
|
|
metadataProperties.Any(property =>
|
|
!protectedAssets.TryGetProperty(property.Name, out _) ||
|
|
property.Value.ValueKind is not JsonValueKind.Object ||
|
|
!property.Value.TryGetProperty("description", out var description) ||
|
|
description.ValueKind is not JsonValueKind.String ||
|
|
string.IsNullOrWhiteSpace(description.GetString()) ||
|
|
!property.Value.TryGetProperty("altText", out var altText) ||
|
|
altText.ValueKind is not JsonValueKind.String ||
|
|
string.IsNullOrWhiteSpace(altText.GetString())))
|
|
return "The protected visual asset metadata is invalid or incomplete.";
|
|
|
|
if (!protectedData.TryGetProperty("footer", out var footer) ||
|
|
footer.ValueKind is not JsonValueKind.Object)
|
|
return "The protected briefing footer data is missing.";
|
|
|
|
string[] footerFields = ["createdWith", "models", "createdAt", "authors", "protection"];
|
|
return footerFields.Any(field =>
|
|
!footer.TryGetProperty(field, out var value) ||
|
|
value.ValueKind is not JsonValueKind.String ||
|
|
string.IsNullOrWhiteSpace(value.GetString()))
|
|
? "The protected briefing footer data is incomplete."
|
|
: string.Empty;
|
|
}
|
|
} |