using System.Diagnostics; using System.Text; using System.Text.Json; using XFEEDToolBox.Core.Data; using XFEEDToolBox.Core.Execution; using XFEEDToolBox.Core.Models; namespace XFEEDToolBox.Core.Native; public sealed record NativeBridgeDescription(int ProtocolVersion, int ProcessId, string ImageSha256, IReadOnlyList Capabilities, string Status); /// Offline research only. Compiled into the test harness, never into the shipping application. public sealed class NativeBridgeClient : IAsyncDisposable { private readonly SemaphoreSlim gate = new(1, 1); private Process? process; public NativeBridgeDescription? Description { get; private set; } public bool Connected => process is { HasExited: false }; public event Action? Diagnostic; public async Task ConnectAsync(string python, string hostScript, GameEnvironment environment, CancellationToken token = default) { if (environment.ProfileMode != "isolated-offline") throw new InvalidOperationException("原生研究仅限已识别的隔离离线副本,禁止连接在线客户端。当前发布版只使用文件与公共行情读取。"); if (environment.ProcessId is null || environment.ImageSha256.Length != 64) throw new InvalidOperationException("无法唯一识别游戏进程及映像版本。"); if (Connected && Description?.ProcessId == environment.ProcessId && Description.ImageSha256 == environment.ImageSha256) return; if (process != null) await DisposeAsync(); var start = new ProcessStartInfo(python) { UseShellExecute = false, CreateNoWindow = true, RedirectStandardInput = true, RedirectStandardOutput = true, RedirectStandardError = true, StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8 }; foreach (var arg in new[] { "-u", hostScript, "--pid", environment.ProcessId.Value.ToString(), "--image", environment.Executable, "--sha256", environment.ImageSha256 }) start.ArgumentList.Add(arg); start.Environment["PYTHONIOENCODING"] = "utf-8"; process = Process.Start(start) ?? throw new InvalidOperationException("原生桥接进程无法启动。"); process.ErrorDataReceived += (_, e) => { if (e.Data is { Length: > 0 } text) Diagnostic?.Invoke(text[..Math.Min(300, text.Length)]); }; process.BeginErrorReadLine(); try { var result = await CallAsync("describe", null, token); Description = JsonSerializer.Deserialize(result.GetRawText(), ToolboxSettings.JsonOptions) ?? throw new InvalidDataException("原生桥接未返回版本信息。"); if (Description.ProtocolVersion != 1 || Description.ProcessId != environment.ProcessId || Description.ImageSha256 != environment.ImageSha256) throw new InvalidDataException("原生桥接身份或协议版本不匹配。"); } catch { await DisposeAsync(); throw; } } public async Task CallAsync(string method, object? parameters = null, CancellationToken token = default) { await gate.WaitAsync(token); try { if (!Connected) throw new InvalidOperationException("原生桥接未连接。"); using var timeout = CancellationTokenSource.CreateLinkedTokenSource(token); timeout.CancelAfter(TimeSpan.FromSeconds(12)); var id = Guid.NewGuid().ToString("N"); var request = JsonSerializer.Serialize(new { id, method, parameters }, ToolboxSettings.JsonOptions).ReplaceLineEndings(""); await process!.StandardInput.WriteLineAsync(request.AsMemory(), timeout.Token); await process.StandardInput.FlushAsync(timeout.Token); var line = await process.StandardOutput.ReadLineAsync(timeout.Token) ?? throw new IOException("原生桥接已断开。"); using var doc = JsonDocument.Parse(line); var root = doc.RootElement; if (root.Text("id") != id) throw new InvalidDataException("原生指令关联编号不匹配;不自动重试交易。"); if (!root.Flag("ok")) throw new InvalidOperationException(root.Text("error", "原生指令被拒绝。")); return root.Get("result").Clone(); } finally { gate.Release(); } } public async ValueTask DisposeAsync() { if (process == null) return; try { if (!process.HasExited) { process.StandardInput.Close(); using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(3)); await process.WaitForExitAsync(timeout.Token); } } catch (OperationCanceledException) { if (!process.HasExited) process.Kill(true); } finally { process.Dispose(); process = null; Description = null; } } } public sealed class NativeTradeDriver(NativeBridgeClient bridge) : ITradeCommandDriver { public Task PreflightAsync(CancellationToken token) { var description = bridge.Description ?? throw new InvalidOperationException("请先连接原生桥接。"); var missing = new[] { "trade.buy", "trade.sell", "navigation.travel", "station.services" }.Except(description.Capabilities).ToArray(); if (missing.Length > 0) throw new InvalidOperationException("原生入口尚未验证:" + string.Join("、", missing) + "。观察接入不能作为自动跑商已完成的证明。"); return Task.CompletedTask; } public async Task ServiceDockedAsync(DockedServiceRequest request,Func beforeInput,CancellationToken token) { await beforeInput(token); await bridge.CallAsync("station.services",new{marketId=request.MarketId,reservedCargo=request.ReservedCargo},token); } public async Task ExecuteAsync(TradeAction action, Func beforeInput, CancellationToken token) { await beforeInput(token); var method = action.Kind switch { TradeActionKind.Buy => "trade.buy", TradeActionKind.Sell => "trade.sell", _ => "navigation.travel" }; await bridge.CallAsync(method, new { sourceMarketId = action.Leg.From.MarketId, destinationMarketId = action.Leg.To.MarketId, system = action.Leg.To.SystemName, commodity = action.Order?.Commodity, quantity = action.Order?.Quantity, expectedPrice = action.Kind == TradeActionKind.Buy ? action.Order?.BuyPrice : action.Order?.SellPrice }, token); } public async Task ReleaseAllAsync() { if (bridge.Connected) try { await bridge.CallAsync("commands.cancel"); } catch (IOException) { } catch (InvalidOperationException) { } } }