zcbot/windows-node/Zcbot.WindowsNode/Application/JobMonitorService.cs

190 lines
7.8 KiB
C#

using System.Text.Json;
namespace Zcbot.WindowsNode;
internal interface IJobMonitorService
{
IReadOnlyList<JobDisplaySnapshot> ReadSnapshots(int limit = 50);
}
internal sealed class JobMonitorService(string jobsDirectory) : IJobMonitorService
{
public IReadOnlyList<JobDisplaySnapshot> ReadSnapshots(int limit = 50)
{
if (!Directory.Exists(jobsDirectory))
{
return [];
}
var snapshots = new List<JobDisplaySnapshot>();
foreach (var requestPath in Directory.EnumerateFiles(
jobsDirectory, "request.json", SearchOption.AllDirectories))
{
try
{
using var requestDocument = JsonDocument.Parse(File.ReadAllBytes(requestPath));
var root = requestDocument.RootElement;
if (!JobInboxStore.TryReadGuid(root, "job_id", out var jobId))
{
continue;
}
var jobDirectory = Directory.GetParent(
Directory.GetParent(requestPath)!.FullName)!.FullName;
var acceptedAt = ReadDate(root, "accepted_at")
?? new DateTimeOffset(File.GetCreationTimeUtc(requestPath));
var capability = ReadString(root, "capability", "unknown");
var title = ReadDisplayTitle(root);
var inputFilename = root.TryGetProperty("input_transfers", out var transfers)
&& transfers.ValueKind == JsonValueKind.Array
? string.Join(", ", transfers.EnumerateArray()
.Select(item => ReadString(item, "filename", "-")))
: "-";
var state = JobInboxStore.ReadState(Path.Combine(jobDirectory, "state.json"));
var terminal = JobInboxStore.ReadTerminal(Path.Combine(jobDirectory, "terminal.json"));
var uploadPath = Path.Combine(jobDirectory, "upload-complete.json");
var uploadComplete = File.Exists(uploadPath);
var cloudTerminalPath = Path.Combine(jobDirectory, "cloud-terminal.json");
var cloudTerminal = JobInboxStore.ReadTerminal(cloudTerminalPath);
var stage = state?.Stage ?? "accepted";
var progress = state?.Progress ?? 0;
var detail = state?.Detail ?? "任务已由本机接收";
var updatedAt = state is not null
&& state.UpdatedAt != DateTimeOffset.MinValue
&& state.UpdatedAt > acceptedAt
? state.UpdatedAt
: acceptedAt;
if (terminal is JsonElement terminalValue)
{
var terminalStatus = ReadString(terminalValue, "status", "failed");
if (terminalStatus == "succeeded" && !uploadComplete)
{
stage = "uploading_outputs";
progress = Math.Max(progress, 90);
detail = state?.Stage == "uploading_outputs"
? state.Detail
: "软件执行完成,等待上传结果";
}
else
{
stage = terminalStatus;
progress = terminalStatus == "succeeded" ? 100 : progress;
detail = TerminalDetail(terminalValue, terminalStatus, detail);
}
updatedAt = LatestWrite(updatedAt, Path.Combine(jobDirectory, "terminal.json"));
}
if (uploadComplete)
{
stage = "succeeded";
progress = 100;
detail = File.Exists(Path.Combine(jobDirectory, "workspace-result.json"))
? "预览已上传,工程保存在本机工作区"
: "结果已上传并由云端确认";
updatedAt = LatestWrite(updatedAt, uploadPath);
}
else if (cloudTerminal is JsonElement cloudValue)
{
var cloudStatus = ReadString(cloudValue, "status", "failed");
stage = "cloud_terminal";
progress = 100;
detail = cloudStatus == "cancelled"
? "云端任务已取消,本地生成的结果仍保留"
: "云端任务已终止,本地生成的结果仍保留";
updatedAt = LatestWrite(updatedAt, cloudTerminalPath);
}
snapshots.Add(new JobDisplaySnapshot(
jobId,
capability,
title,
inputFilename,
stage,
Math.Clamp(progress, 0, 100),
detail,
acceptedAt,
updatedAt,
uploadComplete));
}
catch (Exception exception) when (
exception is JsonException or IOException or UnauthorizedAccessException)
{
}
}
return snapshots
.OrderByDescending(item => item.IsActive)
.ThenByDescending(item => item.UpdatedAt)
.Take(Math.Max(1, limit))
.ToArray();
}
private static string TerminalDetail(JsonElement terminal, string status, string fallback)
{
if (terminal.TryGetProperty("error", out var error))
{
var detail = ReadString(error, "detail", "");
if (!string.IsNullOrWhiteSpace(detail))
{
return detail;
}
}
return status switch
{
"succeeded" => "软件任务执行成功",
"cancelled" => "任务已取消",
"failed" => "任务执行失败",
_ => fallback,
};
}
private static DateTimeOffset LatestWrite(DateTimeOffset current, string path)
{
var writtenAt = new DateTimeOffset(File.GetLastWriteTimeUtc(path));
return writtenAt > current ? writtenAt : current;
}
private static string ReadString(JsonElement value, string name, string fallback) =>
value.TryGetProperty(name, out var property)
&& property.ValueKind == JsonValueKind.String
&& !string.IsNullOrWhiteSpace(property.GetString())
? property.GetString()!
: fallback;
private static string ReadDisplayTitle(JsonElement root)
{
const string fallback = "未命名任务";
if (root.TryGetProperty("request_summary", out var summary)
&& summary.ValueKind == JsonValueKind.Object)
{
var title = ReadString(summary, "title", fallback);
if (title != fallback)
{
return title;
}
}
if (root.TryGetProperty("request", out var request)
&& request.ValueKind == JsonValueKind.Object
&& request.TryGetProperty("operation", out var operation)
&& operation.ValueKind == JsonValueKind.Object)
{
foreach (var action in operation.EnumerateObject())
{
if (action.Value.ValueKind != JsonValueKind.Object)
{
continue;
}
var title = ReadString(action.Value, "title", fallback);
if (title != fallback)
{
return title;
}
}
}
return fallback;
}
private static DateTimeOffset? ReadDate(JsonElement value, string name) =>
value.TryGetProperty(name, out var property)
&& property.ValueKind == JsonValueKind.String
&& DateTimeOffset.TryParse(property.GetString(), out var parsed)
? parsed
: null;
}