新增 OTA WatchDog 编排与车队健康/报警后端。
覆盖包库回传、任务下发、CDM 任务同步与报警采集,并为包/任务 ID 与上传文件名加上路径安全校验。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MiGu.Server.Launcher;
|
||||
|
||||
namespace MiGu.Server.Ota;
|
||||
|
||||
public sealed class OtaVehicleSource
|
||||
{
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
private readonly SimpleLiteOptions _sl;
|
||||
private readonly InternalTokenStoreAccessor _token;
|
||||
private readonly ILogger<OtaVehicleSource> _log;
|
||||
|
||||
public OtaVehicleSource(
|
||||
IHttpClientFactory httpFactory,
|
||||
IOptions<SimpleLiteOptions> sl,
|
||||
InternalTokenStoreAccessor token,
|
||||
ILogger<OtaVehicleSource> log)
|
||||
{
|
||||
_httpFactory = httpFactory;
|
||||
_sl = sl.Value;
|
||||
_token = token;
|
||||
_log = log;
|
||||
}
|
||||
|
||||
public async Task<List<OtaVehicleRow>> ListCarsAsync(CancellationToken ct)
|
||||
{
|
||||
var port = _sl.ProjectionPort > 0 ? _sl.ProjectionPort : 8222;
|
||||
var cars = await TryProjectionAsync(port, ct);
|
||||
if (cars.Count == 0)
|
||||
cars = await TryAgvListAsync(port, ct);
|
||||
return cars;
|
||||
}
|
||||
|
||||
private async Task<List<OtaVehicleRow>> TryProjectionAsync(int port, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var client = _httpFactory.CreateClient();
|
||||
client.Timeout = TimeSpan.FromSeconds(8);
|
||||
using var req = new HttpRequestMessage(HttpMethod.Get, $"http://127.0.0.1:{port}/projection/cars");
|
||||
var token = _token.Token;
|
||||
if (!string.IsNullOrEmpty(token))
|
||||
req.Headers.TryAddWithoutValidation("X-Platform-Internal-Token", token);
|
||||
using var resp = await client.SendAsync(req, ct);
|
||||
if (!resp.IsSuccessStatusCode) return new();
|
||||
var text = await resp.Content.ReadAsStringAsync(ct);
|
||||
return ParseCars(text);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogDebug(ex, "projection/cars failed");
|
||||
return new();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<List<OtaVehicleRow>> TryAgvListAsync(int port, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var client = _httpFactory.CreateClient();
|
||||
client.Timeout = TimeSpan.FromSeconds(8);
|
||||
using var resp = await client.GetAsync($"http://127.0.0.1:{port}/api/agv/list", ct);
|
||||
if (!resp.IsSuccessStatusCode) return new();
|
||||
var text = await resp.Content.ReadAsStringAsync(ct);
|
||||
using var doc = JsonDocument.Parse(text);
|
||||
var root = doc.RootElement;
|
||||
var arr = root.ValueKind == JsonValueKind.Array ? root
|
||||
: root.TryGetProperty("data", out var d) ? d
|
||||
: root.TryGetProperty("items", out var i) ? i
|
||||
: default;
|
||||
if (arr.ValueKind != JsonValueKind.Array) return new();
|
||||
var list = new List<OtaVehicleRow>();
|
||||
foreach (var el in arr.EnumerateArray())
|
||||
{
|
||||
var id = GetStr(el, "agv_id", "id", "carId") ?? "";
|
||||
var name = GetStr(el, "agv_name", "name") ?? id;
|
||||
var ip = GetStr(el, "agv_ip", "ip");
|
||||
var state = GetStr(el, "status", "state");
|
||||
if (string.IsNullOrEmpty(id) && string.IsNullOrEmpty(ip)) continue;
|
||||
list.Add(new OtaVehicleRow
|
||||
{
|
||||
Id = string.IsNullOrEmpty(id) ? ip! : id,
|
||||
Name = name,
|
||||
Ip = ip,
|
||||
State = state
|
||||
});
|
||||
}
|
||||
return list;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogDebug(ex, "agv/list failed");
|
||||
return new();
|
||||
}
|
||||
}
|
||||
|
||||
private static List<OtaVehicleRow> ParseCars(string text)
|
||||
{
|
||||
using var doc = JsonDocument.Parse(text);
|
||||
var root = doc.RootElement;
|
||||
var arr = root.ValueKind == JsonValueKind.Array ? root
|
||||
: root.TryGetProperty("cars", out var c) ? c
|
||||
: root.TryGetProperty("items", out var i) ? i
|
||||
: root.TryGetProperty("data", out var d) ? d
|
||||
: default;
|
||||
if (arr.ValueKind != JsonValueKind.Array) return new();
|
||||
var list = new List<OtaVehicleRow>();
|
||||
foreach (var el in arr.EnumerateArray())
|
||||
{
|
||||
var id = GetStr(el, "id", "carId", "rawId") ?? "";
|
||||
var name = GetStr(el, "name") ?? id;
|
||||
var ip = GetStr(el, "ip");
|
||||
var state = GetStr(el, "state", "status");
|
||||
var group = GetStr(el, "group");
|
||||
if (string.IsNullOrEmpty(id)) continue;
|
||||
list.Add(new OtaVehicleRow { Id = id, Name = name, Ip = ip, State = state, Group = group });
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private static string? GetStr(JsonElement el, params string[] names)
|
||||
{
|
||||
foreach (var n in names)
|
||||
{
|
||||
if (el.TryGetProperty(n, out var p) && p.ValueKind == JsonValueKind.String)
|
||||
return p.GetString();
|
||||
if (el.TryGetProperty(n, out p) && p.ValueKind is JsonValueKind.Number)
|
||||
return p.ToString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>避免 Ota 层直接依赖 Auth 命名空间循环;薄包装 InternalTokenStore。</summary>
|
||||
public sealed class InternalTokenStoreAccessor
|
||||
{
|
||||
private readonly Auth.InternalTokenStore _store;
|
||||
public InternalTokenStoreAccessor(Auth.InternalTokenStore store) => _store = store;
|
||||
public string Token => _store.Token;
|
||||
}
|
||||
Reference in New Issue
Block a user