using System.Globalization; using System.Diagnostics; using System.Reflection; using System.Runtime.InteropServices; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; using Microsoft.Win32; static class DwgDraftApi { public static IServiceCollection AddDwgDraft(this IServiceCollection services) { services.AddSingleton(); return services; } public static WebApplication MapDwgDraftApi(this WebApplication app) { var group = app.MapGroup("/api/dwg-draft"); group.MapGet("/installation", (DwgDraftService dwg) => Results.Ok(dwg.DiscoverInstallation())); group.MapGet("/bridge", (DwgDraftService dwg) => Results.Ok(dwg.ReadBridgeStatus())); group.MapPost("/extract", (DwgDraftExtractRequest request, DwgDraftService dwg) => { try { return Results.Ok(dwg.Extract(request)); } catch (Exception ex) { return Results.Problem($"AutoCAD DWG 提取失败:{ex.Message}", statusCode: 500); } }); group.MapPost("/open", (DwgDraftExtractRequest request, DwgDraftService dwg) => { try { return Results.Ok(dwg.Extract(request)); } catch (Exception ex) { return Results.Problem($"AutoCAD DWG 打开/提取失败:{ex.Message}", statusCode: 500); } }); group.MapGet("/latest-extraction", (DwgDraftService dwg) => { try { return Results.Ok(dwg.ReadLatestExtraction()); } catch (Exception ex) { return Results.Problem($"读取 DWG 提取结果失败:{ex.Message}", statusCode: 500); } }); group.MapGet("/latest-extraction/category/{category}", (string category, DwgDraftService dwg) => { try { var result = dwg.ReadCategoryExtraction(category); return result.Found ? Results.Ok(result) : Results.NotFound(result); } catch (Exception ex) { return Results.Problem($"读取 DWG 分类提取结果失败:{ex.Message}", statusCode: 500); } }); return app; } } sealed class DwgDraftService { const string DefaultProgId = "AutoCAD.Application.23.1"; static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower }; static readonly string[] ProgIdCandidates = [ "AutoCAD.Application.23.1", "AutoCAD.Application.23", "AutoCAD.Application" ]; readonly AppPaths _paths; readonly object _lock = new(); public DwgDraftService(AppPaths paths) { _paths = paths; } public DwgDraftInstallation DiscoverInstallation() { var progIds = Registry.ClassesRoot.GetSubKeyNames() .Where(name => name.StartsWith("AutoCAD.Application", StringComparison.OrdinalIgnoreCase)) .OrderBy(name => name, StringComparer.OrdinalIgnoreCase) .Select(ReadProgIdInfo) .ToList(); var exeCandidates = new[] { @"D:\Program Files\Autodesk\AutoCAD 2020\acad.exe", @"C:\Program Files\Autodesk\AutoCAD 2020\acad.exe", @"C:\Program Files\Autodesk\AutoCAD 2021\acad.exe", @"C:\Program Files\Autodesk\AutoCAD 2022\acad.exe" }; return new DwgDraftInstallation { Found = progIds.Count > 0 || exeCandidates.Any(File.Exists), DefaultProgId = DefaultProgId, ProgIds = progIds, AcadExeCandidates = exeCandidates.Where(File.Exists).ToList(), Notes = [ "DWG 通过 AutoCAD COM 读取,后续二维图查错以 AutoCAD 路径为主。", "AutoCAD COM 调用可能在软件忙碌时返回 RPC_E_CALL_REJECTED,后端已对属性读取和集合遍历做重试。" ] }; } public DwgDraftBridgeStatus ReadBridgeStatus() { var bridgeDir = ResolveBridgeDir(); var heartbeatPath = Path.Combine(bridgeDir, "heartbeat.json"); var alive = IsBridgeAlive(heartbeatPath); JsonElement? heartbeat = null; if (File.Exists(heartbeatPath)) { using var doc = JsonDocument.Parse(File.ReadAllText(heartbeatPath)); heartbeat = doc.RootElement.Clone(); } return new DwgDraftBridgeStatus { Ok = true, Alive = alive, BridgeDir = bridgeDir, HeartbeatPath = heartbeatPath, LastWriteTimeUtc = File.Exists(heartbeatPath) ? File.GetLastWriteTimeUtc(heartbeatPath) : null, Heartbeat = heartbeat, Message = alive ? "AutoCAD DWG bridge is online." : "AutoCAD DWG bridge is not online." }; } public DwgDraftExtractionResult Extract(DwgDraftExtractRequest request) { lock (_lock) { return RunExternalExtractor(request); } } DwgDraftExtractionResult RunExternalExtractor(DwgDraftExtractRequest request) { TraceExternalExtractor("start"); ApplyDrawingPathBase64(request); if (string.IsNullOrWhiteSpace(request.DrawingPath)) throw new ArgumentException("drawing_path 不能为空。", nameof(request)); var drawingPath = Path.GetFullPath(request.DrawingPath.Trim().Trim('"')); TraceExternalExtractor($"drawing_path={drawingPath}"); if (!File.Exists(drawingPath)) throw new FileNotFoundException($"DWG 文件不存在:{drawingPath}"); var progId = string.IsNullOrWhiteSpace(request.ProgId) ? ResolveProgId() : request.ProgId.Trim(); TraceExternalExtractor($"prog_id={progId}"); var outputDir = ResolveOutputDir(request.OutputDir); Directory.CreateDirectory(outputDir); TraceExternalExtractor($"output_dir={outputDir}"); var extractorDll = Path.Combine( _paths.Agent4Root, "tools", "drawing-diagnostics", "AutoCadDwgExtractor", "bin", "Debug", "net10.0", "AutoCadDwgExtractor.dll"); TraceExternalExtractor($"extractor_dll={extractorDll}; exists={File.Exists(extractorDll)}"); if (!File.Exists(extractorDll)) throw new FileNotFoundException($"AutoCAD DWG 提取器尚未编译:{extractorDll}"); if (request.UseBridge && TryRunBridgeExtractor(request, drawingPath, progId, outputDir, out var bridgeResult)) { return bridgeResult; } var startInfo = new ProcessStartInfo { FileName = "dotnet", WorkingDirectory = _paths.Agent4Root, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true }; startInfo.ArgumentList.Add(extractorDll); startInfo.ArgumentList.Add("--drawing-path"); startInfo.ArgumentList.Add(drawingPath); startInfo.ArgumentList.Add("--output-dir"); startInfo.ArgumentList.Add(outputDir); startInfo.ArgumentList.Add("--prog-id"); startInfo.ArgumentList.Add(progId); startInfo.ArgumentList.Add("--visible"); startInfo.ArgumentList.Add(request.Visible ? "true" : "false"); startInfo.ArgumentList.Add("--close-after-extract"); startInfo.ArgumentList.Add(request.CloseAfterExtract ? "true" : "false"); startInfo.ArgumentList.Add("--max-entities"); startInfo.ArgumentList.Add((request.MaxEntities <= 0 ? 100_000 : request.MaxEntities).ToString(CultureInfo.InvariantCulture)); if (!string.IsNullOrWhiteSpace(request.MainViewSeedMode)) { startInfo.ArgumentList.Add("--main-view-seed-mode"); startInfo.ArgumentList.Add(request.MainViewSeedMode.Trim()); } if (!string.IsNullOrWhiteSpace(request.ReuseExtractionDir)) { startInfo.ArgumentList.Add("--reuse-extraction-dir"); startInfo.ArgumentList.Add(Path.GetFullPath(request.ReuseExtractionDir.Trim().Trim('"'))); } TraceExternalExtractor("process_start"); using var process = Process.Start(startInfo) ?? throw new InvalidOperationException("Failed to start AutoCadDwgExtractor."); TraceExternalExtractor($"process_started pid={process.Id}"); var stdout = process.StandardOutput.ReadToEnd(); var stderr = process.StandardError.ReadToEnd(); process.WaitForExit(); TraceExternalExtractor($"process_exit exit_code={process.ExitCode}; stdout_len={stdout.Length}; stderr_len={stderr.Length}"); if (process.ExitCode != 0) throw new InvalidOperationException($"AutoCadDwgExtractor failed. exit={process.ExitCode}\n{stdout}\n{stderr}"); var manifestPath = Path.Combine(outputDir, "manifest.json"); TraceExternalExtractor($"manifest={manifestPath}; exists={File.Exists(manifestPath)}"); if (!File.Exists(manifestPath)) throw new FileNotFoundException($"AutoCadDwgExtractor did not write manifest: {manifestPath}\n{stdout}\n{stderr}"); using var doc = JsonDocument.Parse(File.ReadAllText(manifestPath)); var root = doc.RootElement; var entityCount = root.TryGetProperty("entity_count", out var countElement) && countElement.ValueKind == JsonValueKind.Number ? countElement.GetInt32() : 0; var typeCounts = new Dictionary(StringComparer.OrdinalIgnoreCase); if (root.TryGetProperty("type_counts", out var typeCountsElement) && typeCountsElement.ValueKind == JsonValueKind.Object) { foreach (var item in typeCountsElement.EnumerateObject()) { if (item.Value.ValueKind == JsonValueKind.Number) typeCounts[item.Name] = item.Value.GetInt32(); } } return new DwgDraftExtractionResult { Ok = true, Source = "AutoCAD COM external extractor", ProgId = progId, Started = root.TryGetProperty("started_auto_cad", out var startedElement) && startedElement.ValueKind is JsonValueKind.True, DrawingPath = drawingPath, ExportDir = outputDir, ManifestPath = manifestPath, EntityCount = entityCount, TypeCounts = typeCounts, CategoryFiles = ReadCategoryFiles(root, outputDir), Message = $"已通过 AutoCAD COM 外部提取器提取 DWG:{Path.GetFileName(drawingPath)}" }; } bool TryRunBridgeExtractor( DwgDraftExtractRequest request, string drawingPath, string progId, string outputDir, out DwgDraftExtractionResult result) { result = new DwgDraftExtractionResult(); var bridgeDir = ResolveBridgeDir(); var heartbeatPath = Path.Combine(bridgeDir, "heartbeat.json"); if (!IsBridgeAlive(heartbeatPath)) { TraceExternalExtractor($"bridge_not_available heartbeat={heartbeatPath}"); return false; } var jobsDir = Path.Combine(bridgeDir, "jobs"); var resultsDir = Path.Combine(bridgeDir, "results"); Directory.CreateDirectory(jobsDir); Directory.CreateDirectory(resultsDir); var jobId = $"{DateTime.UtcNow:yyyyMMddHHmmssfff}-{Guid.NewGuid():N}"; var jobPath = Path.Combine(jobsDir, $"{jobId}.request.json"); var tempJobPath = jobPath + ".tmp"; var resultPath = Path.Combine(resultsDir, $"{jobId}.result.json"); var job = new DwgBridgeJob { JobId = jobId, DrawingPath = drawingPath, OutputDir = outputDir, ProgId = progId, Visible = request.Visible, CloseAfterExtract = request.CloseAfterExtract, MaxEntities = request.MaxEntities <= 0 ? 100_000 : request.MaxEntities, MainViewSeedMode = request.MainViewSeedMode, ReuseExtractionDir = string.IsNullOrWhiteSpace(request.ReuseExtractionDir) ? "" : Path.GetFullPath(request.ReuseExtractionDir.Trim().Trim('"')) }; File.WriteAllText(tempJobPath, JsonSerializer.Serialize(job, JsonOptions)); File.Move(tempJobPath, jobPath, overwrite: true); TraceExternalExtractor($"bridge_job_submitted job_id={jobId}; bridge_dir={bridgeDir}"); var timeout = TimeSpan.FromSeconds(request.BridgeTimeoutSeconds <= 0 ? 600 : request.BridgeTimeoutSeconds); var deadline = DateTime.UtcNow.Add(timeout); while (DateTime.UtcNow < deadline) { if (File.Exists(resultPath)) { var bridgeResult = JsonSerializer.Deserialize(File.ReadAllText(resultPath), JsonOptions) ?? throw new InvalidOperationException($"Invalid bridge result: {resultPath}"); TraceExternalExtractor($"bridge_job_result job_id={jobId}; ok={bridgeResult.Ok}; exit={bridgeResult.ExitCode}"); if (!bridgeResult.Ok) { throw new InvalidOperationException( $"AutoCAD bridge extraction failed. job={jobId}; exit={bridgeResult.ExitCode}\n{bridgeResult.Error}\n{bridgeResult.Stdout}\n{bridgeResult.Stderr}"); } var manifestPath = string.IsNullOrWhiteSpace(bridgeResult.ManifestPath) ? Path.Combine(outputDir, "manifest.json") : bridgeResult.ManifestPath; if (!File.Exists(manifestPath)) throw new FileNotFoundException($"AutoCAD bridge did not write manifest: {manifestPath}"); result = ReadExternalManifest( manifestPath, outputDir, drawingPath, progId, "AutoCAD COM bridge extractor", $"Read DWG through AutoCAD bridge: {Path.GetFileName(drawingPath)}"); return true; } Thread.Sleep(500); } throw new TimeoutException($"AutoCAD bridge extraction timed out. job={jobId}; timeout={timeout.TotalSeconds:0}s"); } bool IsBridgeAlive(string heartbeatPath) { if (!File.Exists(heartbeatPath)) return false; return DateTime.UtcNow - File.GetLastWriteTimeUtc(heartbeatPath) <= TimeSpan.FromSeconds(30); } string ResolveBridgeDir() { var configured = Environment.GetEnvironmentVariable("AGENT4_DWG_BRIDGE_DIR"); if (!string.IsNullOrWhiteSpace(configured)) return Path.GetFullPath(configured.Trim().Trim('"')); return Path.Combine(_paths.RuntimeDir, "dwg-draft", "bridge"); } DwgDraftExtractionResult ReadExternalManifest( string manifestPath, string outputDir, string drawingPath, string progId, string source, string message) { using var doc = JsonDocument.Parse(File.ReadAllText(manifestPath)); var root = doc.RootElement; var entityCount = root.TryGetProperty("entity_count", out var countElement) && countElement.ValueKind == JsonValueKind.Number ? countElement.GetInt32() : 0; var typeCounts = new Dictionary(StringComparer.OrdinalIgnoreCase); if (root.TryGetProperty("type_counts", out var typeCountsElement) && typeCountsElement.ValueKind == JsonValueKind.Object) { foreach (var item in typeCountsElement.EnumerateObject()) { if (item.Value.ValueKind == JsonValueKind.Number) typeCounts[item.Name] = item.Value.GetInt32(); } } return new DwgDraftExtractionResult { Ok = true, Source = source, ProgId = progId, Started = root.TryGetProperty("started_auto_cad", out var startedElement) && startedElement.ValueKind is JsonValueKind.True, DrawingPath = drawingPath, ExportDir = outputDir, ManifestPath = manifestPath, EntityCount = entityCount, TypeCounts = typeCounts, CategoryFiles = ReadCategoryFiles(root, outputDir), Message = message }; } void TraceExternalExtractor(string message) { try { var dir = Path.Combine(_paths.RuntimeDir, "dwg-draft"); Directory.CreateDirectory(dir); File.AppendAllText( Path.Combine(dir, "extractor-dispatch.log"), $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {message}{Environment.NewLine}"); } catch { // Diagnostics must not affect extraction. } } DwgDraftExtractionResult ExtractOnSta(DwgDraftExtractRequest request) { ApplyDrawingPathBase64(request); if (string.IsNullOrWhiteSpace(request.DrawingPath)) throw new ArgumentException("drawing_path 不能为空。", nameof(request)); var drawingPath = Path.GetFullPath(request.DrawingPath.Trim().Trim('"')); if (!File.Exists(drawingPath)) throw new FileNotFoundException($"DWG 文件不存在:{drawingPath}"); var progId = string.IsNullOrWhiteSpace(request.ProgId) ? ResolveProgId() : request.ProgId.Trim(); var outputDir = ResolveOutputDir(request.OutputDir); Directory.CreateDirectory(outputDir); var app = ConnectOrCreate(progId, request.Visible, out var started); object? doc = null; try { WaitForQuiescent(app, TimeSpan.FromSeconds(20)); doc = OpenDocument(app, drawingPath); WaitForQuiescent(app, TimeSpan.FromSeconds(20)); var extraction = ExtractDocument(doc, request.MaxEntities <= 0 ? 100_000 : request.MaxEntities); var manifest = WriteExtractionFiles(outputDir, drawingPath, progId, started, extraction); if (request.CloseAfterExtract) TryInvoke(doc, "Close", [false]); return new DwgDraftExtractionResult { Ok = true, Source = "AutoCAD COM", ProgId = progId, Started = started, DrawingPath = drawingPath, ExportDir = outputDir, ManifestPath = Path.Combine(outputDir, "manifest.json"), EntityCount = extraction.EntitiesCommon.Count, TypeCounts = extraction.TypeCounts, CategoryFiles = manifest.CategoryFiles, Message = $"已通过 AutoCAD COM 提取 DWG:{Path.GetFileName(drawingPath)}" }; } finally { if (doc != null) ReleaseCom(doc); ReleaseCom(app); } } DwgDraftDocumentExtraction ExtractDocument(object doc, int maxEntities) { var extraction = new DwgDraftDocumentExtraction(); ExtractSpace(doc, "ModelSpace", extraction, maxEntities); ExtractSpace(doc, "PaperSpace", extraction, maxEntities); return extraction; } void ExtractSpace(object doc, string spaceName, DwgDraftDocumentExtraction extraction, int maxEntities) { var space = GetProperty(doc, spaceName); if (space == null) return; try { var count = ToInt(GetProperty(space, "Count")) ?? 0; if (string.Equals(spaceName, "ModelSpace", StringComparison.OrdinalIgnoreCase)) extraction.ModelSpaceCount = count; else extraction.PaperSpaceCount = count; for (var i = 0; i < count && extraction.EntitiesCommon.Count < maxEntities; i++) { var entity = GetCollectionItem(space, i); if (entity == null) continue; try { ExtractEntity(entity, spaceName, i, extraction); } finally { ReleaseCom(entity); } } } finally { ReleaseCom(space); } } void ExtractEntity(object entity, string spaceName, int index, DwgDraftDocumentExtraction extraction) { var objectName = ReadString(entity, "ObjectName"); if (string.IsNullOrWhiteSpace(objectName)) objectName = ReadString(entity, "EntityName"); if (string.IsNullOrWhiteSpace(objectName)) objectName = entity.GetType().Name; extraction.TypeCounts[objectName] = extraction.TypeCounts.TryGetValue(objectName, out var current) ? current + 1 : 1; var common = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["space"] = spaceName, ["index"] = index, ["object_name"] = objectName, ["handle"] = ReadString(entity, "Handle"), ["layer"] = ReadString(entity, "Layer"), ["linetype"] = ReadString(entity, "Linetype"), ["color"] = ToInt(GetProperty(entity, "Color")), ["lineweight"] = ToInt(GetProperty(entity, "Lineweight")) }; extraction.EntitiesCommon.Add(common); var details = ExtractDetails(entity, objectName); var item = Merge(common, details); if (IsDimension(objectName)) { extraction.Dimensions.Add(item); var tolerance = ExtractDimensionalTolerance(common, details); if (tolerance != null) extraction.DimensionalTolerances.Add(tolerance); } else if (IsGeometry(objectName)) { extraction.Geometry.Add(item); } else if (IsAnnotation(objectName)) { extraction.Annotations.Add(item); } else { extraction.UnknownEntities.Add(item); } var text = details.TryGetValue("text", out var t) ? Convert.ToString(t, CultureInfo.InvariantCulture) ?? "" : ""; var blockName = details.TryGetValue("effective_name", out var b) ? Convert.ToString(b, CultureInfo.InvariantCulture) ?? "" : ""; if (LooksLikeRoughness(text, blockName)) extraction.Roughness.Add(Merge(common, details)); if (LooksLikeGeometricTolerance(text, blockName)) extraction.GeometricTolerances.Add(Merge(common, details)); if (LooksLikeDatum(text, blockName)) extraction.Datums.Add(Merge(common, details)); } static Dictionary ExtractDetails(object entity, string objectName) { var details = new Dictionary(StringComparer.OrdinalIgnoreCase); if (objectName.Contains("Line", StringComparison.OrdinalIgnoreCase)) { AddPoint(details, "start", GetProperty(entity, "StartPoint")); AddPoint(details, "end", GetProperty(entity, "EndPoint")); } else if (objectName.Contains("Circle", StringComparison.OrdinalIgnoreCase)) { AddPoint(details, "center", GetProperty(entity, "Center")); var radius = ToDouble(GetProperty(entity, "Radius")); details["radius"] = radius; details["diameter"] = radius == null ? null : radius * 2; } else if (objectName.Contains("Arc", StringComparison.OrdinalIgnoreCase)) { AddPoint(details, "center", GetProperty(entity, "Center")); details["radius"] = ToDouble(GetProperty(entity, "Radius")); details["start_angle"] = ToDouble(GetProperty(entity, "StartAngle")); details["end_angle"] = ToDouble(GetProperty(entity, "EndAngle")); } else if (objectName.Contains("Spline", StringComparison.OrdinalIgnoreCase)) { AddPoint(details, "start", GetProperty(entity, "StartPoint")); AddPoint(details, "end", GetProperty(entity, "EndPoint")); details["degree"] = ToInt(GetProperty(entity, "Degree")); details["closed"] = ToBool(GetProperty(entity, "Closed")); } else if (objectName.Contains("Text", StringComparison.OrdinalIgnoreCase)) { var raw = ReadString(entity, "TextString"); details["text"] = CleanAcadText(raw); details["raw_text"] = raw; details["height"] = ToDouble(GetProperty(entity, "Height")); details["rotation"] = ToDouble(GetProperty(entity, "Rotation")); AddPoint(details, "insertion_point", GetProperty(entity, "InsertionPoint")); } else if (objectName.Contains("Dimension", StringComparison.OrdinalIgnoreCase)) { var textOverride = ReadString(entity, "TextOverride"); details["measurement"] = ToDouble(GetProperty(entity, "Measurement")); details["text_override"] = textOverride; details["text"] = CleanAcadText(textOverride); var missingPointFields = new List(); AddPointFromProperties(details, "text_position", entity, missingPointFields, "TextPosition"); AddPointFromProperties(details, "xline1_point", entity, missingPointFields, "XLine1Point", "ExtLine1Point", "ExtensionLine1Point"); AddPointFromProperties(details, "xline2_point", entity, missingPointFields, "XLine2Point", "ExtLine2Point", "ExtensionLine2Point"); AddPointFromProperties(details, "dim_line_point", entity, missingPointFields, "DimLinePoint", "DimLineLocation"); AddPointFromProperties(details, "center", entity, missingPointFields, "Center"); AddPointFromProperties(details, "chord_point", entity, missingPointFields, "ChordPoint"); AddPointFromProperties(details, "far_chord_point", entity, missingPointFields, "FarChordPoint"); if (missingPointFields.Count > 0) details["dimension_point_missing_fields"] = missingPointFields; } else if (objectName.Contains("BlockReference", StringComparison.OrdinalIgnoreCase)) { details["name"] = ReadString(entity, "Name"); details["effective_name"] = ReadString(entity, "EffectiveName"); details["rotation"] = ToDouble(GetProperty(entity, "Rotation")); details["x_scale_factor"] = ToDouble(GetProperty(entity, "XScaleFactor")); details["y_scale_factor"] = ToDouble(GetProperty(entity, "YScaleFactor")); AddPoint(details, "insertion_point", GetProperty(entity, "InsertionPoint")); var attributes = ReadBlockAttributes(entity); if (attributes.Count > 0) { details["attributes"] = attributes; details["text"] = string.Join(" ", attributes.Select(a => a.TryGetValue("value", out var value) ? value : null)); } } else if (objectName.Contains("Hatch", StringComparison.OrdinalIgnoreCase)) { details["pattern_name"] = ReadString(entity, "PatternName"); details["pattern_scale"] = ToDouble(GetProperty(entity, "PatternScale")); } else if (objectName.Contains("RasterImage", StringComparison.OrdinalIgnoreCase)) { details["image_file"] = ReadString(entity, "ImageFile"); } return details; } static List> ReadBlockAttributes(object entity) { var result = new List>(); var hasAttributes = ToBool(GetProperty(entity, "HasAttributes")) ?? false; if (!hasAttributes) return result; var raw = TryInvoke(entity, "GetAttributes", []); if (raw == null) return result; if (raw is Array array) { foreach (var item in array) { if (item == null) continue; result.Add(new Dictionary { ["tag"] = ReadString(item, "TagString"), ["value"] = CleanAcadText(ReadString(item, "TextString")), ["raw_value"] = ReadString(item, "TextString") }); ReleaseCom(item); } } return result; } static Dictionary? ExtractDimensionalTolerance( Dictionary common, Dictionary details) { var raw = details.TryGetValue("text_override", out var rawValue) ? Convert.ToString(rawValue, CultureInfo.InvariantCulture) ?? "" : ""; var text = details.TryGetValue("text", out var textValue) ? Convert.ToString(textValue, CultureInfo.InvariantCulture) ?? "" : ""; if (!DimensionalToleranceParser.LooksLikeTolerance(raw, text)) return null; var measurement = details.TryGetValue("measurement", out var measurementValue) ? ToDouble(measurementValue) : null; var objectName = common.TryGetValue("object_name", out var objectNameValue) ? Convert.ToString(objectNameValue, CultureInfo.InvariantCulture) ?? "" : ""; var parsed = DimensionalToleranceParser.Parse(text, raw, measurement, objectName); var result = Merge(Merge(common, details), parsed); result["measurement"] = measurementValue; result["numeric_values"] = ExtractNumbers(text); return result; } DwgDraftManifest WriteExtractionFiles( string outputDir, string drawingPath, string progId, bool started, DwgDraftDocumentExtraction extraction) { var files = new List { WriteCategory(outputDir, "entities-common", "entities-common.json", extraction.EntitiesCommon), WriteCategory(outputDir, "geometry", "geometry.json", extraction.Geometry), WriteCategory(outputDir, "annotations", "annotations.json", extraction.Annotations), WriteCategory(outputDir, "dimensions", "dimensions.json", extraction.Dimensions), WriteCategory(outputDir, "dimensional-tolerances", "dimensional-tolerances.json", extraction.DimensionalTolerances), WriteCategory(outputDir, "roughness", "roughness.json", extraction.Roughness), WriteCategory(outputDir, "geometric-tolerances", "geometric-tolerances.json", extraction.GeometricTolerances), WriteCategory(outputDir, "datums", "datums.json", extraction.Datums), WriteCategory(outputDir, "unknown-entities", "unknown-entities.json", extraction.UnknownEntities) }; var manifest = new DwgDraftManifest { Ok = true, Schema = "agent4.dwg-draft.extraction.v1", Source = "AutoCAD COM", ProgId = progId, StartedAutoCad = started, DrawingPath = drawingPath, ExportedAtUtc = DateTime.UtcNow, ModelSpaceCount = extraction.ModelSpaceCount, PaperSpaceCount = extraction.PaperSpaceCount, EntityCount = extraction.EntitiesCommon.Count, TypeCounts = extraction.TypeCounts, CategoryFiles = files }; WriteJson(Path.Combine(outputDir, "manifest.json"), manifest); return manifest; } public DwgDraftLatestExtractionResult ReadLatestExtraction() { var path = ResolveManifestPath(""); if (!File.Exists(path)) { return new DwgDraftLatestExtractionResult { Ok = false, Found = false, ExportDir = Path.GetDirectoryName(path) ?? "", ManifestPath = path, Message = "DWG extraction manifest was not found. Call POST /api/dwg-draft/extract first." }; } using var doc = JsonDocument.Parse(File.ReadAllText(path)); var root = doc.RootElement.Clone(); var files = ReadCategoryFiles(root, Path.GetDirectoryName(path) ?? ""); return new DwgDraftLatestExtractionResult { Ok = true, Found = true, ExportDir = Path.GetDirectoryName(path) ?? "", ManifestPath = path, LastWriteTimeUtc = File.GetLastWriteTimeUtc(path), Manifest = root, CategoryFiles = files, Message = "Read the latest AutoCAD DWG extraction manifest." }; } public DwgDraftCategoryExtractionResult ReadCategoryExtraction(string category) { if (string.IsNullOrWhiteSpace(category)) throw new ArgumentException("category is required.", nameof(category)); var manifestPath = ResolveManifestPath(""); if (!File.Exists(manifestPath)) { return new DwgDraftCategoryExtractionResult { Ok = false, Found = false, Category = category, ManifestPath = manifestPath, Message = "DWG extraction manifest was not found." }; } using var manifestDoc = JsonDocument.Parse(File.ReadAllText(manifestPath)); var exportDir = Path.GetDirectoryName(manifestPath) ?? ""; var files = ReadCategoryFiles(manifestDoc.RootElement, exportDir); var selected = files.FirstOrDefault(file => string.Equals(file.Category, category, StringComparison.OrdinalIgnoreCase) || string.Equals(Path.GetFileNameWithoutExtension(file.File), category, StringComparison.OrdinalIgnoreCase)); if (selected == null || !File.Exists(selected.FullPath)) { return new DwgDraftCategoryExtractionResult { Ok = false, Found = false, Category = category, ManifestPath = manifestPath, ExportDir = exportDir, AvailableCategories = files.Select(file => file.Category).ToList(), Message = $"DWG extraction category was not found: {category}" }; } using var categoryDoc = JsonDocument.Parse(File.ReadAllText(selected.FullPath)); return new DwgDraftCategoryExtractionResult { Ok = true, Found = true, Category = selected.Category, ManifestPath = manifestPath, ExportDir = exportDir, CategoryPath = selected.FullPath, LastWriteTimeUtc = File.GetLastWriteTimeUtc(selected.FullPath), ItemCount = selected.ItemCount, Data = categoryDoc.RootElement.Clone(), AvailableCategories = files.Select(file => file.Category).ToList(), Message = $"Read DWG extraction category: {selected.Category}" }; } string ResolveOutputDir(string requested) { if (!string.IsNullOrWhiteSpace(requested)) return Path.GetFullPath(requested.Trim().Trim('"')); var configured = Environment.GetEnvironmentVariable("AGENT4_DWG_EXPORT_DIR"); if (!string.IsNullOrWhiteSpace(configured)) return Path.GetFullPath(configured.Trim().Trim('"')); return Path.Combine(_paths.RuntimeDir, "dwg-draft", "latest-extraction"); } static void ApplyDrawingPathBase64(DwgDraftExtractRequest request) { if (string.IsNullOrWhiteSpace(request.DrawingPathBase64)) return; try { request.DrawingPath = Encoding.UTF8.GetString(Convert.FromBase64String(request.DrawingPathBase64.Trim())); } catch (Exception ex) { throw new ArgumentException($"drawing_path_base64 不是有效的 UTF-8 Base64 路径:{ex.Message}", nameof(request)); } } string ResolveManifestPath(string requestedDir) { var dir = ResolveOutputDir(requestedDir); return Path.Combine(dir, "manifest.json"); } static DwgDraftCategoryFile WriteCategory(string outputDir, string category, string file, object data) { var path = Path.Combine(outputDir, file); WriteJson(path, data); var itemCount = data is System.Collections.ICollection collection ? collection.Count : 0; return new DwgDraftCategoryFile { Category = category, File = file, FullPath = path, ItemCount = itemCount, Exists = true }; } static void WriteJson(string path, object data) { Directory.CreateDirectory(Path.GetDirectoryName(path) ?? "."); File.WriteAllText(path, JsonSerializer.Serialize(data, JsonOptions)); } static List ReadCategoryFiles(JsonElement manifest, string exportDir) { if (!manifest.TryGetProperty("category_files", out var filesElement) || filesElement.ValueKind != JsonValueKind.Array) return []; var result = new List(); foreach (var item in filesElement.EnumerateArray()) { var category = item.TryGetProperty("category", out var categoryElement) ? categoryElement.GetString() ?? "" : ""; var file = item.TryGetProperty("file", out var fileElement) ? fileElement.GetString() ?? "" : ""; var itemCount = item.TryGetProperty("item_count", out var countElement) && countElement.ValueKind == JsonValueKind.Number ? countElement.GetInt32() : 0; if (string.IsNullOrWhiteSpace(category) || string.IsNullOrWhiteSpace(file)) continue; var fullPath = Path.GetFullPath(Path.Combine(exportDir, file)); var root = Path.GetFullPath(exportDir); if (!fullPath.StartsWith(root, StringComparison.OrdinalIgnoreCase)) continue; result.Add(new DwgDraftCategoryFile { Category = category, File = file, FullPath = fullPath, ItemCount = itemCount, Exists = File.Exists(fullPath) }); } return result; } static string ResolveProgId() { foreach (var progId in ProgIdCandidates) { if (Type.GetTypeFromProgID(progId, throwOnError: false) != null) return progId; } return DefaultProgId; } static object ConnectOrCreate(string progId, bool visible, out bool started) { var app = TryGetActiveComObject(progId, out _); started = false; if (app == null) { var type = Type.GetTypeFromProgID(progId, throwOnError: true) ?? throw new InvalidOperationException($"找不到 AutoCAD COM ProgID:{progId}"); app = Activator.CreateInstance(type) ?? throw new InvalidOperationException($"创建 AutoCAD COM 对象失败:{progId}"); started = true; } TrySetProperty(app, "Visible", visible); return app; } static object OpenDocument(object app, string drawingPath) { var documents = GetProperty(app, "Documents") ?? throw new InvalidOperationException("AutoCAD.Application.Documents 不可用。"); try { var doc = TryInvoke(documents, "Open", [drawingPath, true]) ?? TryInvoke(documents, "Open", [drawingPath]) ?? throw new InvalidOperationException($"AutoCAD 打开 DWG 失败:{drawingPath}"); return doc; } finally { ReleaseCom(documents); } } static void WaitForQuiescent(object app, TimeSpan timeout) { var deadline = DateTime.UtcNow + timeout; while (DateTime.UtcNow < deadline) { try { var state = TryInvoke(app, "GetAcadState", []); if (state == null) return; var isQuiescent = ToBool(GetProperty(state, "IsQuiescent")) ?? true; ReleaseCom(state); if (isQuiescent) return; } catch { return; } Thread.Sleep(250); } } static object? GetProperty(object obj, string name) => RetryCom(() => { try { return obj.GetType().InvokeMember( name, BindingFlags.GetProperty, binder: null, target: obj, args: null, culture: CultureInfo.InvariantCulture); } catch (TargetInvocationException ex) when (IsRetryableCom(ex.InnerException)) { throw; } catch (COMException ex) when (IsRetryableCom(ex)) { throw; } catch { return null; } }); static object? GetCollectionItem(object obj, int index) => RetryCom(() => { foreach (var flags in new[] { BindingFlags.GetProperty, BindingFlags.InvokeMethod }) { try { return obj.GetType().InvokeMember( "Item", flags, binder: null, target: obj, args: [index], culture: CultureInfo.InvariantCulture); } catch (TargetInvocationException ex) when (IsRetryableCom(ex.InnerException)) { throw; } catch (COMException ex) when (IsRetryableCom(ex)) { throw; } catch { // Try the next dispatch style. } } return null; }); static object? TryInvoke(object obj, string name, object?[] args) => RetryCom(() => { try { return obj.GetType().InvokeMember( name, BindingFlags.InvokeMethod, binder: null, target: obj, args: args, culture: CultureInfo.InvariantCulture); } catch (TargetInvocationException ex) when (IsRetryableCom(ex.InnerException)) { throw; } catch (COMException ex) when (IsRetryableCom(ex)) { throw; } catch { return null; } }); static void TrySetProperty(object obj, string name, object? value) { RetryCom(() => { try { obj.GetType().InvokeMember( name, BindingFlags.SetProperty, binder: null, target: obj, args: [value], culture: CultureInfo.InvariantCulture); } catch { // Optional property. } return (object?)null; }); } static T RunSta(Func action) { T? result = default; Exception? error = null; var thread = new Thread(() => { try { result = action(); } catch (Exception ex) { error = ex; } }); thread.SetApartmentState(ApartmentState.STA); thread.Start(); thread.Join(); if (error != null) throw error; return result!; } static object? RetryCom(Func action) { Exception? last = null; for (var attempt = 0; attempt < 120; attempt++) { try { return action(); } catch (TargetInvocationException ex) when (IsRetryableCom(ex.InnerException)) { last = ex.InnerException; Thread.Sleep(100); } catch (COMException ex) when (IsRetryableCom(ex)) { last = ex; Thread.Sleep(100); } } if (last != null) throw last; return action(); } static bool IsRetryableCom(Exception? ex) { if (ex is not COMException com) return false; return unchecked((uint)com.HResult) is 0x80010001 or 0x8001010A; } static Dictionary Merge( Dictionary common, Dictionary details) { var result = new Dictionary(common, StringComparer.OrdinalIgnoreCase); foreach (var item in details) result[item.Key] = item.Value; return result; } static bool IsGeometry(string objectName) { return objectName.Contains("Line", StringComparison.OrdinalIgnoreCase) || objectName.Contains("Circle", StringComparison.OrdinalIgnoreCase) || objectName.Contains("Arc", StringComparison.OrdinalIgnoreCase) || objectName.Contains("Spline", StringComparison.OrdinalIgnoreCase) || objectName.Contains("Hatch", StringComparison.OrdinalIgnoreCase) || objectName.Contains("Wipeout", StringComparison.OrdinalIgnoreCase) || objectName.Contains("RasterImage", StringComparison.OrdinalIgnoreCase); } static bool IsDimension(string objectName) => objectName.Contains("Dimension", StringComparison.OrdinalIgnoreCase); static bool IsAnnotation(string objectName) { return objectName.Contains("Text", StringComparison.OrdinalIgnoreCase) || objectName.Contains("BlockReference", StringComparison.OrdinalIgnoreCase); } static bool LooksLikeRoughness(string text, string blockName) { var merged = text + " " + blockName; return Regex.IsMatch(merged, @"\bR[az]\s*\d", RegexOptions.IgnoreCase) || merged.Contains("粗糙", StringComparison.OrdinalIgnoreCase) || merged.Contains("其余", StringComparison.OrdinalIgnoreCase) || merged.Contains("rough", StringComparison.OrdinalIgnoreCase); } static bool LooksLikeGeometricTolerance(string text, string blockName) { var merged = text + " " + blockName; return merged.Contains("位置度", StringComparison.OrdinalIgnoreCase) || merged.Contains("同轴度", StringComparison.OrdinalIgnoreCase) || merged.Contains("平行度", StringComparison.OrdinalIgnoreCase) || merged.Contains("垂直度", StringComparison.OrdinalIgnoreCase) || merged.Contains("圆跳动", StringComparison.OrdinalIgnoreCase) || merged.Contains("geotol", StringComparison.OrdinalIgnoreCase) || Regex.IsMatch(merged, @"[⊥∥⌖◎⌯⌭]"); } static bool LooksLikeDatum(string text, string blockName) { var merged = text + " " + blockName; return merged.Contains("基准", StringComparison.OrdinalIgnoreCase) || merged.Contains("datum", StringComparison.OrdinalIgnoreCase); } static List ExtractNumbers(string text) { return Regex.Matches(text, @"[+-]?\d+(?:\.\d+)?") .Select(match => double.TryParse(match.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var value) ? value : (double?)null) .Where(value => value != null) .Select(value => value!.Value) .ToList(); } static string CleanAcadText(string text) { if (string.IsNullOrWhiteSpace(text)) return ""; var value = text.Replace("%%C", "Φ", StringComparison.OrdinalIgnoreCase) .Replace("%%D", "°", StringComparison.OrdinalIgnoreCase) .Replace("%%P", "±", StringComparison.OrdinalIgnoreCase); value = Regex.Replace(value, @"\\A\d+;", ""); value = Regex.Replace(value, @"\\H[^;]+;", ""); value = Regex.Replace(value, @"\\S([^;]+)\^([^;]*);", "$1/$2"); value = Regex.Replace(value, @"\\S([^;]+);", "$1"); value = Regex.Replace(value, @"[{}]", ""); value = value.Replace(@"\P", " "); return Regex.Replace(value, @"\s+", " ").Trim(); } static void AddPoint(Dictionary details, string name, object? value) { var point = ToPoint(value); if (point != null) details[name] = point; } static void AddPointFromProperties( Dictionary details, string name, object entity, List missingFields, params string[] propertyNames) { foreach (var propertyName in propertyNames) { var point = ToPoint(GetProperty(entity, propertyName)); if (point == null) continue; details[name] = point; details[$"{name}_source_property"] = propertyName; return; } missingFields.Add(name); } static List? ToPoint(object? value) { if (value is Array array) { var result = new List(); foreach (var item in array) { if (ToDouble(item) is { } number) result.Add(number); } return result.Count > 0 ? result : null; } return null; } static string ReadString(object obj, string name) { return Convert.ToString(GetProperty(obj, name), CultureInfo.InvariantCulture) ?? ""; } static int? ToInt(object? value) { try { return value == null ? null : Convert.ToInt32(value, CultureInfo.InvariantCulture); } catch { return null; } } static double? ToDouble(object? value) { try { return value == null ? null : Convert.ToDouble(value, CultureInfo.InvariantCulture); } catch { return null; } } static bool? ToBool(object? value) { try { return value == null ? null : Convert.ToBoolean(value, CultureInfo.InvariantCulture); } catch { return null; } } static void ReleaseCom(object obj) { try { if (Marshal.IsComObject(obj)) Marshal.FinalReleaseComObject(obj); } catch { // Best-effort COM cleanup. } } static DwgDraftProgIdInfo ReadProgIdInfo(string progId) { var clsid = ReadDefaultValue($@"{progId}\CLSID"); return new DwgDraftProgIdInfo { ProgId = progId, Description = ReadDefaultValue(progId), Clsid = clsid, LocalServer32 = string.IsNullOrWhiteSpace(clsid) ? "" : ReadDefaultValue($@"CLSID\{clsid}\LocalServer32") }; } static string ReadDefaultValue(string subKey) { using var key = Registry.ClassesRoot.OpenSubKey(subKey); return key?.GetValue("")?.ToString() ?? ""; } static object? TryGetActiveComObject(string progId, out string error) { error = ""; try { var hr = CLSIDFromProgID(progId, out var clsid); if (hr < 0) Marshal.ThrowExceptionForHR(hr); return GetActiveObject(ref clsid, IntPtr.Zero); } catch (Exception ex) { error = ex.Message; return null; } } [DllImport("ole32.dll", CharSet = CharSet.Unicode)] static extern int CLSIDFromProgID(string progId, out Guid clsid); [DllImport("oleaut32.dll", PreserveSig = false)] [return: MarshalAs(UnmanagedType.Interface)] static extern object GetActiveObject(ref Guid clsid, IntPtr reserved); } sealed class DwgDraftExtractRequest { public string DrawingPath { get; set; } = ""; public string DrawingPathBase64 { get; set; } = ""; public string ProgId { get; set; } = ""; public bool Visible { get; set; } = true; public bool CloseAfterExtract { get; set; } public string OutputDir { get; set; } = ""; public int MaxEntities { get; set; } = 100_000; public string MainViewSeedMode { get; set; } = ""; public string ReuseExtractionDir { get; set; } = ""; public bool UseBridge { get; set; } = true; public int BridgeTimeoutSeconds { get; set; } = 600; } sealed class DwgBridgeJob { public string JobId { get; set; } = ""; public string DrawingPath { get; set; } = ""; public string OutputDir { get; set; } = ""; public string ProgId { get; set; } = ""; public bool Visible { get; set; } = true; public bool CloseAfterExtract { get; set; } public int MaxEntities { get; set; } = 100_000; public string MainViewSeedMode { get; set; } = ""; public string ReuseExtractionDir { get; set; } = ""; } sealed class DwgBridgeJobResult { public bool Ok { get; set; } public string JobId { get; set; } = ""; public int ExitCode { get; set; } public string Stdout { get; set; } = ""; public string Stderr { get; set; } = ""; public string OutputDir { get; set; } = ""; public string ManifestPath { get; set; } = ""; public string Error { get; set; } = ""; } sealed class DwgDraftBridgeStatus { public bool Ok { get; set; } public bool Alive { get; set; } public string BridgeDir { get; set; } = ""; public string HeartbeatPath { get; set; } = ""; public DateTime? LastWriteTimeUtc { get; set; } public JsonElement? Heartbeat { get; set; } public string Message { get; set; } = ""; } sealed class DwgDraftDocumentExtraction { public int ModelSpaceCount { get; set; } public int PaperSpaceCount { get; set; } public Dictionary TypeCounts { get; set; } = new(StringComparer.OrdinalIgnoreCase); public List> EntitiesCommon { get; set; } = new(); public List> Geometry { get; set; } = new(); public List> Annotations { get; set; } = new(); public List> Dimensions { get; set; } = new(); public List> DimensionalTolerances { get; set; } = new(); public List> Roughness { get; set; } = new(); public List> GeometricTolerances { get; set; } = new(); public List> Datums { get; set; } = new(); public List> UnknownEntities { get; set; } = new(); } sealed class DwgDraftInstallation { public bool Found { get; set; } public string DefaultProgId { get; set; } = ""; public List ProgIds { get; set; } = new(); public List AcadExeCandidates { get; set; } = new(); public List Notes { get; set; } = new(); } sealed class DwgDraftProgIdInfo { public string ProgId { get; set; } = ""; public string Description { get; set; } = ""; public string Clsid { get; set; } = ""; public string LocalServer32 { get; set; } = ""; } sealed class DwgDraftExtractionResult { public bool Ok { get; set; } public string Source { get; set; } = ""; public string ProgId { get; set; } = ""; public bool Started { get; set; } public string DrawingPath { get; set; } = ""; public string ExportDir { get; set; } = ""; public string ManifestPath { get; set; } = ""; public int EntityCount { get; set; } public Dictionary TypeCounts { get; set; } = new(StringComparer.OrdinalIgnoreCase); public List CategoryFiles { get; set; } = new(); public string Message { get; set; } = ""; } sealed class DwgDraftManifest { public bool Ok { get; set; } public string Schema { get; set; } = ""; public string Source { get; set; } = ""; public string ProgId { get; set; } = ""; public bool StartedAutoCad { get; set; } public string DrawingPath { get; set; } = ""; public DateTime ExportedAtUtc { get; set; } public int ModelSpaceCount { get; set; } public int PaperSpaceCount { get; set; } public int EntityCount { get; set; } public Dictionary TypeCounts { get; set; } = new(StringComparer.OrdinalIgnoreCase); public List CategoryFiles { get; set; } = new(); } sealed class DwgDraftCategoryFile { public string Category { get; set; } = ""; public string File { get; set; } = ""; public string FullPath { get; set; } = ""; public int ItemCount { get; set; } public bool Exists { get; set; } } sealed class DwgDraftLatestExtractionResult { public bool Ok { get; set; } public bool Found { get; set; } public string ExportDir { get; set; } = ""; public string ManifestPath { get; set; } = ""; public DateTime? LastWriteTimeUtc { get; set; } public JsonElement? Manifest { get; set; } public List CategoryFiles { get; set; } = new(); public string Message { get; set; } = ""; } sealed class DwgDraftCategoryExtractionResult { public bool Ok { get; set; } public bool Found { get; set; } public string Category { get; set; } = ""; public string ExportDir { get; set; } = ""; public string ManifestPath { get; set; } = ""; public string CategoryPath { get; set; } = ""; public DateTime? LastWriteTimeUtc { get; set; } public int ItemCount { get; set; } public JsonElement? Data { get; set; } public List AvailableCategories { get; set; } = new(); public string Message { get; set; } = ""; }