feat: enhance task management with execution ID support and add callback service

This commit is contained in:
sladro 2026-03-02 14:18:58 +08:00
parent 660c3bcd6f
commit a0c89cac52
15 changed files with 366 additions and 20 deletions

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
@ -80,6 +80,7 @@
<Private>False</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Buffers, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>packages\System.Buffers.4.5.1\lib\netstandard1.1\System.Buffers.dll</HintPath>
</Reference>
@ -143,6 +144,7 @@
<Compile Include="Services\RevitService.cs" />
<Compile Include="Services\DocumentService.cs" />
<Compile Include="Services\TaskManager.cs" />
<Compile Include="Services\TaskCallbackService.cs" />
<Compile Include="Services\ShellAnalyzer.cs" />
<Compile Include="Services\ShellOptimizer.cs" />
<!-- Common -->
@ -154,4 +156,4 @@
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
</Project>

View File

@ -196,7 +196,7 @@ namespace RevitHttpControl.Controllers
}
// 创建异步任务
var taskId = TaskManager.Instance.CreateTask();
var taskId = TaskManager.Instance.CreateTask(request.ExecutionId);
TaskManager.Instance.SetTaskRunning(taskId);
// 异步执行导出操作

View File

@ -1,4 +1,4 @@
using System;
using System;
using System.Net;
using System.Net.Http;
using System.Web.Http;
@ -150,7 +150,7 @@ namespace RevitHttpControl.Controllers
}
// 创建异步任务
var taskId = TaskManager.Instance.CreateTask();
var taskId = TaskManager.Instance.CreateTask(request.ExecutionId);
TaskManager.Instance.SetTaskRunning(taskId);
// 异步执行打开文件操作

View File

@ -1,4 +1,4 @@
using System;
using System;
using System.Net;
using System.Net.Http;
using System.Web.Http;
@ -126,7 +126,7 @@ namespace RevitHttpControl.Controllers
}
// 创建异步任务
var taskId = TaskManager.Instance.CreateTask();
var taskId = TaskManager.Instance.CreateTask(request.ExecutionId);
TaskManager.Instance.SetTaskRunning(taskId);
// 异步执行薄壳优化

View File

@ -1,4 +1,4 @@
using System;
using System;
using System.Net;
using System.Net.Http;
using System.Web.Http;
@ -114,7 +114,7 @@ namespace RevitHttpControl.Controllers
}
// 创建异步任务
var taskId = TaskManager.Instance.CreateTask();
var taskId = TaskManager.Instance.CreateTask(request.ExecutionId);
TaskManager.Instance.SetTaskRunning(taskId);
// 异步执行统计操作

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace RevitHttpControl.Models
{
@ -8,6 +9,12 @@ namespace RevitHttpControl.Models
/// </summary>
public class ExportIfcRequest
{
/// <summary>
/// 外部执行ID可选用于批处理回调
/// </summary>
[JsonProperty("execution_id")]
public string ExecutionId { get; set; }
/// <summary>
/// 输出文件路径(可选,如果不指定则自动生成)
/// </summary>

View File

@ -1,4 +1,5 @@
using System;
using System;
using Newtonsoft.Json;
namespace RevitHttpControl.Models
{
@ -7,6 +8,12 @@ namespace RevitHttpControl.Models
/// </summary>
public class OpenFileRequest
{
/// <summary>
/// 外部执行ID可选用于批处理回调
/// </summary>
[JsonProperty("execution_id")]
public string ExecutionId { get; set; }
/// <summary>
/// 文件路径
/// </summary>

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace RevitHttpControl.Models
{
@ -75,12 +76,25 @@ namespace RevitHttpControl.Models
/// <summary>
/// 文件大小(字节)
/// </summary>
[JsonProperty("fileSize")]
public long FileSizeBytes { get; set; }
/// <summary>
/// 文件大小(格式化显示)
/// </summary>
public string FileSizeDisplay { get; set; }
/// <summary>
/// 面片数
/// </summary>
[JsonProperty("polygonCount")]
public long PolygonCount { get; set; }
/// <summary>
/// 模型特征数
/// </summary>
[JsonProperty("featureCount")]
public int FeatureCount { get; set; }
}
/// <summary>

View File

@ -1,8 +1,9 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace RevitHttpControl.Models
{
@ -38,6 +39,12 @@ namespace RevitHttpControl.Models
/// </summary>
public class ShellExecuteRequest
{
/// <summary>
/// 外部执行ID可选用于批处理回调
/// </summary>
[JsonProperty("execution_id")]
public string ExecutionId { get; set; }
/// <summary>
/// 优化模式
/// </summary>
@ -54,6 +61,12 @@ namespace RevitHttpControl.Models
/// </summary>
public class ShellCustomExecuteRequest
{
/// <summary>
/// 外部执行ID可选用于批处理回调
/// </summary>
[JsonProperty("execution_id")]
public string ExecutionId { get; set; }
/// <summary>
/// 要删除的类别ID列表BuiltInCategory 的 int 值)
/// </summary>

View File

@ -1,4 +1,5 @@
using System;
using System;
using Newtonsoft.Json;
namespace RevitHttpControl.Models
{
@ -7,6 +8,12 @@ namespace RevitHttpControl.Models
/// </summary>
public class StatsRequest
{
/// <summary>
/// 外部执行ID可选用于批处理回调
/// </summary>
[JsonProperty("execution_id")]
public string ExecutionId { get; set; }
/// <summary>
/// 统计类型
/// </summary>

View File

@ -1,4 +1,5 @@
using System;
using System;
using Newtonsoft.Json;
namespace RevitHttpControl.Models
{
@ -7,6 +8,11 @@ namespace RevitHttpControl.Models
/// </summary>
public class TaskStatusResponse
{
/// <summary>
/// 外部执行ID用于批处理回调
/// </summary>
public string ExecutionId { get; set; }
/// <summary>
/// 任务ID
/// </summary>
@ -84,4 +90,31 @@ namespace RevitHttpControl.Models
/// </summary>
public string StatusUrl { get; set; }
}
}
/// <summary>
/// 批处理任务结果回调请求体
/// </summary>
public class TaskResultCallbackRequest
{
[JsonProperty("execution_id")]
public string ExecutionId { get; set; }
[JsonProperty("software_id")]
public string SoftwareId { get; set; }
[JsonProperty("status")]
public string Status { get; set; }
[JsonProperty("error_message")]
public string ErrorMessage { get; set; }
[JsonProperty("result")]
public object Result { get; set; }
[JsonProperty("finished_at")]
public string FinishedAt { get; set; }
[JsonProperty("token")]
public string Token { get; set; }
}
}

View File

@ -428,7 +428,9 @@ namespace RevitHttpControl.Services
IsCentralFile = doc.IsWorkshared,
Status = doc.IsModified ? "已修改" : "已保存",
FileSizeBytes = fileSizeBytes,
FileSizeDisplay = fileSizeDisplay
FileSizeDisplay = fileSizeDisplay,
PolygonCount = CalculatePolygonCountFast(doc, maxElements: 200, maxMilliseconds: 1200),
FeatureCount = CalculateFeatureCount(doc)
};
// 2. 快速统计主要构件(批量获取避免多次调用)
@ -609,6 +611,137 @@ namespace RevitHttpControl.Services
}
}
/// <summary>
/// 计算模型面片总数(基于可三角化几何)
/// </summary>
/// <param name="doc">Revit文档</param>
/// <returns>面片总数</returns>
private static long CalculatePolygonCountFast(Document doc, int maxElements, int maxMilliseconds)
{
long totalTriangles = 0;
var processedElements = 0;
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
try
{
var options = new Options
{
ComputeReferences = false,
IncludeNonVisibleObjects = false,
DetailLevel = ViewDetailLevel.Medium
};
var elements = new FilteredElementCollector(doc)
.WhereElementIsNotElementType()
.Where(e => e.Category != null && e.Category.CategoryType == CategoryType.Model);
foreach (var element in elements)
{
if (processedElements >= maxElements || stopwatch.ElapsedMilliseconds >= maxMilliseconds)
{
break;
}
GeometryElement geometry = null;
try
{
geometry = element.get_Geometry(options);
}
catch
{
continue;
}
if (geometry == null)
continue;
totalTriangles += CountTrianglesFromGeometry(geometry);
processedElements++;
}
}
catch
{
// 忽略计算异常并返回已累计值
}
return totalTriangles;
}
/// <summary>
/// 统计模型特征数(按模型类别去重)
/// </summary>
/// <param name="doc">Revit文档</param>
/// <returns>模型特征数</returns>
private static int CalculateFeatureCount(Document doc)
{
try
{
return new FilteredElementCollector(doc)
.WhereElementIsNotElementType()
.Where(e => e.Category != null && e.Category.CategoryType == CategoryType.Model)
.Select(e => e.Category.Id.IntegerValue)
.Distinct()
.Count();
}
catch
{
return 0;
}
}
/// <summary>
/// 递归统计几何中的三角面片数量
/// </summary>
/// <param name="geometry">几何对象集合</param>
/// <returns>三角面片数量</returns>
private static long CountTrianglesFromGeometry(GeometryElement geometry)
{
long triangles = 0;
foreach (var obj in geometry)
{
if (obj is Solid solid && solid.Faces != null && solid.Faces.Size > 0)
{
foreach (Face face in solid.Faces)
{
try
{
var mesh = face.Triangulate();
if (mesh != null)
triangles += mesh.NumTriangles;
}
catch
{
// 忽略单个面三角化失败
}
}
continue;
}
if (obj is Mesh meshObj)
{
triangles += meshObj.NumTriangles;
continue;
}
if (obj is GeometryInstance instance)
{
try
{
var instanceGeometry = instance.GetInstanceGeometry();
if (instanceGeometry != null)
triangles += CountTrianglesFromGeometry(instanceGeometry);
}
catch
{
// 忽略单个实例解析失败
}
}
}
return triangles;
}
/// <summary>
/// 获取文档文件大小(字节)
/// </summary>

View File

@ -0,0 +1,100 @@
using System;
using System.Configuration;
using System.Net.Http;
using System.Text;
using System.Threading;
using Newtonsoft.Json;
using RevitHttpControl.Models;
namespace RevitHttpControl.Services
{
/// <summary>
/// 批处理任务结果回调服务
/// </summary>
public static class TaskCallbackService
{
private const string DefaultEndpointPath = "/api/v1/plugin-callbacks/task-result";
private const string DefaultSoftwareId = "revit";
private static readonly TimeSpan[] RetryDelays =
{
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(3),
TimeSpan.FromSeconds(5)
};
public static void NotifyTaskResult(TaskStatusResponse task)
{
if (task == null || (task.Status != TaskStatus.Completed && task.Status != TaskStatus.Failed))
{
return;
}
var baseAddress = ConfigurationManager.AppSettings["PluginCallbackBaseUrl"];
var token = ConfigurationManager.AppSettings["PluginCallbackToken"];
var softwareId = ConfigurationManager.AppSettings["PluginSoftwareId"];
if (string.IsNullOrWhiteSpace(baseAddress) || string.IsNullOrWhiteSpace(token))
{
return;
}
if (string.IsNullOrWhiteSpace(softwareId))
{
softwareId = DefaultSoftwareId;
}
var callbackUrl = $"{baseAddress.TrimEnd('/')}{DefaultEndpointPath}";
var payload = new TaskResultCallbackRequest
{
ExecutionId = task.ExecutionId,
SoftwareId = softwareId,
Status = task.Status == TaskStatus.Completed ? "success" : "failed",
ErrorMessage = task.Status == TaskStatus.Failed
? (string.IsNullOrWhiteSpace(task.ErrorMessage) ? "UNKNOWN_ERROR" : task.ErrorMessage)
: null,
Result = task.Status == TaskStatus.Completed ? (task.Result ?? new { }) : new { },
FinishedAt = (task.CompletedAt ?? DateTime.UtcNow).ToString("o"),
Token = token
};
SendWithRetry(callbackUrl, payload);
}
private static void SendWithRetry(string callbackUrl, TaskResultCallbackRequest payload)
{
var json = JsonConvert.SerializeObject(payload);
using (var httpClient = new HttpClient())
{
if (Post(httpClient, callbackUrl, json))
{
return;
}
foreach (var delay in RetryDelays)
{
Thread.Sleep(delay);
if (Post(httpClient, callbackUrl, json))
{
return;
}
}
}
}
private static bool Post(HttpClient httpClient, string callbackUrl, string json)
{
try
{
using (var content = new StringContent(json, Encoding.UTF8, "application/json"))
using (var response = httpClient.PostAsync(callbackUrl, content).GetAwaiter().GetResult())
{
return response.IsSuccessStatusCode;
}
}
catch
{
return false;
}
}
}
}

View File

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
@ -46,17 +46,21 @@ namespace RevitHttpControl.Services
/// 创建新任务
/// </summary>
/// <returns>任务ID</returns>
public Guid CreateTask()
public Guid CreateTask(string executionId = null)
{
// 检查是否需要清理过期任务
CheckAndCleanupIfNeeded();
var taskId = Guid.NewGuid();
var effectiveExecutionId = string.IsNullOrWhiteSpace(executionId)
? taskId.ToString()
: executionId;
var task = new TaskStatusResponse
{
TaskId = taskId,
Status = TaskStatus.Pending,
CreatedAt = DateTime.UtcNow
CreatedAt = DateTime.UtcNow,
ExecutionId = effectiveExecutionId
};
_tasks.TryAdd(taskId, task);
@ -100,6 +104,7 @@ namespace RevitHttpControl.Services
task.Status = TaskStatus.Completed;
task.Result = result;
task.CompletedAt = DateTime.UtcNow;
NotifyTaskResultInBackground(task);
}
}
@ -115,9 +120,29 @@ namespace RevitHttpControl.Services
task.Status = TaskStatus.Failed;
task.ErrorMessage = errorMessage;
task.CompletedAt = DateTime.UtcNow;
NotifyTaskResultInBackground(task);
}
}
/// <summary>
/// 后台触发任务结果回调,避免阻塞调用线程
/// </summary>
/// <param name="task">任务状态</param>
private static void NotifyTaskResultInBackground(TaskStatusResponse task)
{
System.Threading.Tasks.Task.Run(() =>
{
try
{
TaskCallbackService.NotifyTaskResult(task);
}
catch
{
// 回调失败不影响主流程
}
});
}
/// <summary>
/// 取消任务
/// </summary>

View File

@ -1,5 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<appSettings>
<add key="PluginCallbackBaseUrl" value="" />
<add key="PluginCallbackToken" value="" />
<add key="PluginSoftwareId" value="revit" />
</appSettings>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
@ -12,4 +17,4 @@
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
</configuration>