XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/XFEEDToolBox

Fix toolbox-owned trade execution and live handoff recovery

Connect configured original docking and cruise components, initialize route planning before use, and wait for fresh protection after scene changes. Preserve completed purchases and propagate failed journey details for recovery. Read fractional market quantities conservatively without changing prices. Reuse verified CG membership at the supply station and confirm delivery through the original Homer interface. Use original Journal evidence when the derived event index is unavailable. Reconcile owned SCO stop/renewal races against matching original receipts before continuing. Validation: Release build with no warnings or errors; actual 192t buy/service, departure, jump, software cruise, original docking, sale/service, wallet and CG contribution reconciliation, followed by the next automatic cycle. SCO qualification and original interdiction escape remain incomplete.

c479eb6
XFE工作室室长 <mail@xfegzs.com>
提交于

代码差异

15 个文件 +281 -42
Modified XFEEDToolBox.Core/Execution/AutomaticTradeConfiguration.cs +6 -1
@@ -14,11 +14,16 @@ public sealed record AutomaticTradeConfiguration
14 14 public bool RequireSupercruiseOvercharge{get;init;}=true;
15 15 public bool RequireInterdictionEscape{get;init;}=true;
16 16 public bool ValidateMenuReentry{get;init;}=true;
17 public string? ConfirmedCommunityGoalJourney{get;init;}
17 18 public void Validate()
18 19 {
20 if(!RequireSupercruiseOvercharge||!RequireInterdictionEscape)
21 throw new InvalidDataException("自动跑商需要启用SCO温度燃油控制与原始逃逸矢量保护。");
22 if(ConfirmedCommunityGoalJourney is not null&&!Path.IsPathFullyQualified(ConfirmedCommunityGoalJourney))
23 throw new InvalidDataException("已确认CG旅程必须使用完整路径。");
19 24 if(Schema!=1||Source.MarketId<=0||Destination.MarketId!=CommunityGoalCatalogue.HomerMarket||Source.MarketId==Destination.MarketId
20 25 ||string.IsNullOrWhiteSpace(Source.Name)||string.IsNullOrWhiteSpace(Source.SystemName)||string.IsNullOrWhiteSpace(Destination.Name)||string.IsNullOrWhiteSpace(Destination.SystemName)
21 ||!Path.IsPathFullyQualified(WorkingDirectory)||EmptyValidationVisits is <1 or >10||CargoCapacity is <1 or >192||ReserveCredits<0)
26 ||!Path.IsPathFullyQualified(WorkingDirectory)||EmptyValidationVisits is <0 or >10||CargoCapacity is <1 or >192||ReserveCredits<0)
22 27 throw new InvalidDataException("自动跑商配置缺少明确站点、工作目录、空载验收次数或已验证货舱参数。");
23 28 }
24 29 }
Modified XFEEDToolBox.Core/Execution/NativeTradeCommandDriver.cs +13 -3
@@ -146,10 +146,20 @@ public sealed class NativeTradeCommandDriver:IConfirmedTradeCommandDriver,IConfi
146 146 var page=offset==0?snapshot:await channel.CallAsync(new{op="commerce.market.status",generation,offset,maximum=50},token);
147 147 foreach(var row in page.GetProperty("commodities").EnumerateArray())
148 148 {
149 long Number(string name)=>row.TryGetProperty(name,out var value)&&value.ValueKind==JsonValueKind.Number?value.GetInt64():0;
149 long Number(string field,bool quantity=false)
150 {
151 if(!row.TryGetProperty(field,out var value)||value.ValueKind!=JsonValueKind.Number)return 0;
152 if(value.TryGetInt64(out var integer)&&integer>=0)return integer;
153 if(!value.TryGetDecimal(out var number)||number<0||number>long.MaxValue||!quantity&&decimal.Truncate(number)!=number)
154 throw new InvalidDataException($"Original market field {field} has an invalid value: {value.GetRawText()}");
155 // The server can quote fractional supply or demand.
156 // Only whole tonnes are available for an order.
157 return decimal.ToInt64(quantity?decimal.Floor(number):number);
158 }
150 159 var name=CommodityCatalog.Key(row.GetProperty("name").GetString()??"");
151 rows.Add(new(){MarketId=market,Commodity=name,DisplayName=name,BuyPrice=Number("buyPrice"),SellPrice=Number("sellPrice"),Supply=Number("stock"),
152 Demand=Number("demand"),CollectedAt=at,Source=DataSource.Journal,Illegal=row.TryGetProperty("illegal",out var illegal)&&illegal.ValueKind==JsonValueKind.True});
160 rows.Add(new(){MarketId=market,Commodity=name,DisplayName=name,BuyPrice=Number("buyPrice"),SellPrice=Number("sellPrice"),Supply=Number("stock",true),
161 Demand=Number("demand",true),CollectedAt=at,Source=DataSource.Journal,Illegal=row.TryGetProperty("illegal",out var illegal)&&illegal.ValueKind==JsonValueKind.True
162 ||row.TryGetProperty("legality",out var legality)&&legality.ValueKind==JsonValueKind.String&&!string.IsNullOrEmpty(legality.GetString())});
153 163 }
154 164 }
155 165 return rows;
Modified XFEEDToolBox.Core/Execution/NativeTradeJourney.cs +5 -3
@@ -4,6 +4,8 @@ using XFEEDToolBox.Core.Models;
4 4 namespace XFEEDToolBox.Core.Execution;
5 5
6 6 public sealed record NativeTradeJourneyResult(bool Passed,TradeJourneyCheckpoint Checkpoint,TradeProfitSnapshot Profit,long OpeningCredits,long? ClosingCredits,string? Failure,DateTimeOffset Finished);
7 public sealed class NativeTradeJourneyIncompleteException(NativeTradeJourneyResult result,Exception? cause):InvalidOperationException("跑商批次未完成,原始货物和回执已保留。",cause)
8 {public NativeTradeJourneyResult Result{get;}=result;}
7 9
8 10 /// <summary>A resumable production cargo journey, driven entirely by the toolbox.</summary>
9 11 public sealed class NativeTradeJourney(NativeTradeCommandDriver driver,Func<PlayerState> player,Func<CancellationToken,Task> check,string directory)
@@ -53,7 +55,7 @@ public sealed class NativeTradeJourney(NativeTradeCommandDriver driver,Func<Play
53 55 }
54 56 void RecordService(ConfirmedDockedServiceReceipt receipt)=>ledger.Record(receipt.ToCashEntry());
55 57 driver.TradeConfirmed+=RecordTrade;driver.ServiceConfirmed+=RecordService;
56 using var stop=CancellationTokenSource.CreateLinkedTokenSource(token);Task? services=null;long? closing=null;string? failure=null;
58 using var stop=CancellationTokenSource.CreateLinkedTokenSource(token);Task? services=null;long? closing=null;string? failure=null;Exception? cause=null;
57 59 var saleWasAlreadyConfirmed=checkpoint.Sold is not null;
58 60 try
59 61 {
@@ -91,7 +93,7 @@ public sealed class NativeTradeJourney(NativeTradeCommandDriver driver,Func<Play
91 93 if(!player().Docked||player().MarketId!=leg.To.MarketId||player().CargoCount!=0||checkpoint.Sold is null||opening+ledger.Snapshot.CashChange!=closing)
92 94 throw new InvalidDataException("本批次原始余额、空货舱或实际停靠尚未完整核对。");
93 95 }
94 catch(Exception error){failure=error.ToString();stop.Cancel();}
96 catch(Exception error){cause=error;failure=error.ToString();stop.Cancel();}
95 97 finally
96 98 {
97 99 if(services is not null)try{await services;}catch(Exception error){failure??=error.ToString();}
@@ -101,7 +103,7 @@ public sealed class NativeTradeJourney(NativeTradeCommandDriver driver,Func<Play
101 103 var attempts=Path.Combine(directory,"attempts");Directory.CreateDirectory(attempts);
102 104 await WriteAsync(Path.Combine(attempts,Guid.NewGuid().ToString("N")+".json"),result,CancellationToken.None);
103 105 await WriteAsync(resultPath,result,CancellationToken.None);
104 if(!result.Passed)throw new InvalidOperationException("跑商批次未完成,原始货物和回执已保留。",new InvalidDataException(failure));
106 if(!result.Passed)throw new NativeTradeJourneyIncompleteException(result,cause);
105 107 return result;
106 108 }
107 109 private static async Task WriteAsync(string path,object value,CancellationToken token)
Modified XFEEDToolBox.Core/Execution/NativeTradeNavigation.cs +13 -3
@@ -12,10 +12,18 @@ public sealed class NativeTradeNavigation(int processId,string gameHash,Func<Pla
12 12 private readonly SemaphoreSlim gate=new(1,1);
13 13 public event Action<string>? Status;
14 14 private NativeGameInputChannel Inputs()=>new(processId,gameHash,pluginDirectory);
15 private async Task ReadyAsync(Func<CancellationToken,Task> beforeInput,CancellationToken token)
16 {
17 await NavigationRecovery.ReadWithRecoveryAsync(async cancellation=>
18 {
19 await beforeInput(cancellation);await ensureProtection(cancellation);return true;
20 },()=>Status?.Invoke("正在等待场景切换后的连续健康遥测,恢复后继续原航程。"),token);
21 }
15 22 private async Task<NativeSystemFlightControlDriver> LocalAsync(CancellationToken token)
16 23 {
17 24 var driver=new NativeSystemFlightControlDriver(processId,gameHash,player,Inputs(),ensureProtection,useNativeSupercruiseAssist:false);
18 25 await driver.ConfigureOriginalDockingControlAsync(Path.Combine(pluginDirectory,"DockingControl"),token);
26 await driver.ConfigureOriginalCruiseControlAsync(fuelReserve,token:token);
19 27 return driver;
20 28 }
21 29 private async Task<(CurrentSystemSnapshot Catalogue,SystemLocation Location)> ResolveAsync(Station destination,CancellationToken token)
@@ -33,7 +41,7 @@ public sealed class NativeTradeNavigation(int processId,string gameHash,Func<Pla
33 41 await gate.WaitAsync(token);
34 42 try
35 43 {
36 await beforeInput(token);await ensureProtection(token);
44 await ReadyAsync(beforeInput,token);
37 45 var initial=player();
38 46 if(initial.Docked&&initial.MarketId==destination.MarketId)return;
39 47 if(!initial.SystemName.Equals(destination.SystemName,StringComparison.OrdinalIgnoreCase))
@@ -52,12 +60,13 @@ public sealed class NativeTradeNavigation(int processId,string gameHash,Func<Pla
52 60 }
53 61 finally{await departure.ReleaseSystemFlightAsync();}
54 62 }
55 await beforeInput(token);await ensureProtection(token);
63 await ReadyAsync(beforeInput,token);
56 64 var galaxy=await new NativeGalaxyCatalog(processId).FindAsync(destination.SystemName,token);
57 65 var waypoint=new NavWaypoint(galaxy.SystemAddress??throw new InvalidDataException("目的星系身份缺失。"),galaxy.Name,galaxy.Position,galaxy.StarClass);
58 66 var interstellar=new NativeFlightControlDriver(processId,gameHash,player,fuelReserve,Inputs(),ensureProtection);
59 67 try
60 68 {
69 await NavigationRecovery.PreflightWithRecoveryAsync(interstellar,()=>Status?.Invoke("正在连接星际导航驱动"),token);
61 70 await new GameRoutePlanner(player).PlanAsync(waypoint,interstellar,null,token);
62 71 var route=new NavigationRunner(player);string? issue=null;
63 72 route.Progress+=value=>{issue=value.Message;Status?.Invoke(value.Message);};
@@ -67,12 +76,13 @@ public sealed class NativeTradeNavigation(int processId,string gameHash,Func<Pla
67 76 }
68 77 finally{await interstellar.ReleaseAsync();}
69 78 }
70 await beforeInput(token);await ensureProtection(token);
79 await ReadyAsync(beforeInput,token);
71 80 var resolved=await ResolveAsync(destination,token);
72 81 var target=Target(resolved.Catalogue.SystemAddress,resolved.Location);
73 82 var local=await LocalAsync(token);var arrival=new SystemNavigationRunner(player);string? lastIssue=null;
74 83 arrival.Progress+=value=>{lastIssue=value.Message;Status?.Invoke(value.Message);};
75 84 await arrival.RunAsync(target,local,token);
85 if(arrival.HandoffFailed)throw new DockingHandoffException(lastIssue??"原生自动降落交接未完成。",destination.MarketId);
76 86 if(arrival.State!=NavigationRunState.Completed||!player().Docked||player().MarketId!=destination.MarketId)
77 87 throw new InvalidOperationException(lastIssue??"目的站停靠尚未确认完成。");
78 88 }
Modified XFEEDToolBox.Core/Navigation/NativeCruiseControlChannel.cs +44 -7
@@ -8,14 +8,14 @@ public sealed record NativeCruiseControlCapabilities(bool Available,bool Qualifi
8 8 /// <summary>Program-owned SCO policy and bounded original-command receipts. Never steers or selects a target.</summary>
9 9 public sealed class NativeCruiseControlChannel(int processId,string gameSha256)
10 10 {
11 public const string Service="CruiseControl0514c";
11 public const string Service="CruiseControl0514d";
12 12 private readonly NavigationPipeClient pipe=new(processId,Service);
13 13 private readonly CruiseOverchargeSampleHistory history=new(gameSha256);
14 14 private readonly CruiseOverchargePolicy policy=new();
15 15 private ulong entity,world;
16 16 private bool leaseRequested,originalStartWitness;
17 17 private string owner="";
18 private string? pendingStopId,lastStopIdentity;
18 private string? pendingStopId,lastStopIdentity,originalStartRequestId;
19 19 public JsonElement? LastStatus{get;private set;}
20 20 public CruiseOverchargeObservation? LastObservation{get;private set;}
21 21 public CruiseOverchargeDecision? LastDecision{get;private set;}
@@ -24,11 +24,26 @@ public sealed class NativeCruiseControlChannel(int processId,string gameSha256)
24 24 public event Action<JsonElement>? OriginalStopReceipt;
25 25 private static bool Flag(JsonElement value,string key)=>value.TryGetProperty(key,out var found)&&found.ValueKind==JsonValueKind.True;
26 26 private static ulong Identity(JsonElement value,string key)=>ulong.Parse(value.GetProperty(key).GetString()!,CultureInfo.InvariantCulture);
27 private static bool Key(JsonElement value,string key)=>value.TryGetProperty(key,out var item)&&item.ValueKind==JsonValueKind.String
28 &&item.GetString() is {Length:32} text&&text.All(c=>c is >= '0' and <= '9' or >= 'a' and <= 'f');
27 29 private void CheckIdentity(JsonElement result)
28 30 {
29 31 if(result.GetProperty("schema").GetInt32()!=1||result.GetProperty("processId").GetInt32()!=processId
30 32 ||result.GetProperty("gameSha256").GetString()!=gameSha256||result.GetProperty("service").GetString()!=Service
31 ||!Flag(result,"overchargeControl")||!Flag(result,"enabled"))throw new InvalidDataException("Original SCO controller does not match this client.");
33 ||!Flag(result,"overchargeControl")||!Flag(result,"enabled")||!Flag(result,"originalFuelRangeObservations")
34 ||!Flag(result,"nativeStopReceipts")||!Flag(result,"expiringOriginalControl")||result.GetProperty("controlTelemetrySchema").GetInt32()!=1
35 ||result.GetProperty("owningThread").GetInt32()<=0)throw new InvalidDataException("Original SCO controller does not match this client.");
36 if(Flag(result,"overchargeControlQualified"))
37 {
38 if(!result.TryGetProperty("qualifiedCycle",out var cycle)||cycle.ValueKind!=JsonValueKind.Object
39 ||!Key(cycle,"owner")||!Key(cycle,"startRequestId")||!Key(cycle,"releaseRequestId")
40 ||cycle.GetProperty("selectionGeneration").GetUInt64()==0||Identity(cycle,"entityId")==0||Identity(cycle,"referenceWorldAddress")==0
41 ||cycle.GetProperty("activeSamples").GetInt32()<5)throw new InvalidDataException("Original SCO qualification has no matching live cycle.");
42 var enabledAt=cycle.GetProperty("enableAtUnixMilliseconds").GetUInt64();var activeAt=cycle.GetProperty("lastActiveAtUnixMilliseconds").GetUInt64();
43 var stopAt=cycle.GetProperty("originalStopAtUnixMilliseconds").GetUInt64();var offAt=cycle.GetProperty("offAtUnixMilliseconds").GetUInt64();
44 if(enabledAt==0||activeAt<enabledAt||stopAt<activeAt||offAt<=stopAt||offAt>(ulong)DateTimeOffset.UtcNow.AddMilliseconds(100).ToUnixTimeMilliseconds())
45 throw new InvalidDataException("Original SCO qualification timestamps do not establish enable, Active, stop and subsequent Off.");
46 }
32 47 }
33 48 private void Capture(JsonElement result)
34 49 {
@@ -50,7 +65,7 @@ public sealed class NativeCruiseControlChannel(int processId,string gameSha256)
50 65 {
51 66 if(owner.Length!=0&&owner!=controllerOwner)throw new InvalidOperationException("SCO channel is already bound to another controller.");
52 67 owner=controllerOwner;
53 var mayBoost=flight.Mode==LocalFlightMode.Supercruise&&decision.Actions==SystemNavigationAction.None
68 var mayBoost=flight.Mode==LocalFlightMode.Supercruise&&(decision.Actions&~SystemNavigationAction.CancelOwnedAssists)==SystemNavigationAction.None
54 69 &&decision.Phase is SystemNavigationPhase.Supercruise or SystemNavigationPhase.Aligning;
55 70 if(!mayBoost){await ReleaseAsync(token);return;}
56 71 if(flight.GameSha256!=gameSha256||flight.ShipEntity==0||flight.ReferenceWorld==0||selectionGeneration==0)
@@ -61,6 +76,13 @@ public sealed class NativeCruiseControlChannel(int processId,string gameSha256)
61 76 entity=flight.ShipEntity;world=flight.ReferenceWorld;history.Reset();originalStartWitness=false;
62 77 }
63 78 var status=await pipe.CallAsync(new{op="read"},token);Capture(status);
79 if(leaseRequested&&!Flag(status,"owned"))
80 {
81 // The native guard may stop our burst between updates. Observe
82 // its real Off state before requesting another original command.
83 await ReleaseAsync(token);
84 throw new FlightTelemetryUnavailableException("Original SCO burst ended; refresh flight telemetry before continuing.");
85 }
64 86 var observed=history.Read(status,flight,DateTimeOffset.UtcNow);LastObservation=observed;
65 87 if(!Flag(status,"owned")&&status.TryGetProperty("sample",out var sample)&&sample.ValueKind==JsonValueKind.Object&&!Flag(sample,"active"))originalStartWitness=false;
66 88 var selected=policy.Decide(flight,observed,reserve,DateTimeOffset.UtcNow);LastDecision=selected;
@@ -68,9 +90,23 @@ public sealed class NativeCruiseControlChannel(int processId,string gameSha256)
68 90 var requestId=Guid.NewGuid().ToString("N");
69 91 // Mark before IPC: a lost reply may still have queued an original call.
70 92 leaseRequested=true;
71 var result=await pipe.CallAsync(new{op="cruise.set",owner,requestId,selectionGeneration,sequence=flight.Sequence,
72 expiresAtUnixMilliseconds=deadline.ToUnixTimeMilliseconds(),requiredFuelReserveTonnes=reserve,
73 target=new{key=target.NativeKey,bodyAddress=target.BodyAddress.ToString(CultureInfo.InvariantCulture),kind=target.NativeKind,marketId=target.MarketId??uint.MaxValue,name=target.Name}},token);
93 JsonElement result;
94 try
95 {
96 result=await pipe.CallAsync(new{op="cruise.set",owner,requestId,selectionGeneration,sequence=flight.Sequence,
97 expiresAtUnixMilliseconds=deadline.ToUnixTimeMilliseconds(),requiredFuelReserveTonnes=reserve,
98 target=new{key=target.NativeKey,bodyAddress=target.BodyAddress.ToString(CultureInfo.InvariantCulture),kind=target.NativeKind,marketId=target.MarketId??uint.MaxValue,name=target.Name}},token);
99 }
100 catch(InvalidOperationException error)when(error.Message=="SCO was already active outside this controller."&&originalStartWitness)
101 {
102 var stopped=await pipe.CallAsync(new{op="read"},token);Capture(stopped);
103 if(Flag(stopped,"owned")||!stopped.TryGetProperty("lastOriginalStop",out var stop)||stop.ValueKind!=JsonValueKind.Object
104 ||stop.GetProperty("owner").GetString()!=owner||stop.GetProperty("startRequestId").GetString()!=originalStartRequestId
105 ||stop.GetProperty("selectionGeneration").GetUInt64()!=selectionGeneration||Identity(stop,"entityId")!=entity||Identity(stop,"referenceWorldAddress")!=world
106 ||!Flag(stop,"nativeMethodCalled")||Flag(stop,"argument")||stop.GetProperty("nativeMethodRva").GetString()!="2eaacd0")throw;
107 await ReleaseAsync(token);
108 throw new FlightTelemetryUnavailableException("Original SCO stop overlapped its renewal; refresh telemetry before continuing.",error);
109 }
74 110 var command=result.GetProperty("generation").GetUInt64();
75 111 while(true)
76 112 {
@@ -89,6 +125,7 @@ public sealed class NativeCruiseControlChannel(int processId,string gameSha256)
89 125 if(receipt.GetProperty("nativeMethodRva").GetString()!="2eaacd0"||!Flag(receipt,"argument")||receipt.GetProperty("actualStateAfterCall").GetInt32() is not(1 or 2))
90 126 throw new InvalidDataException("Original SCO enable was not confirmed.");
91 127 originalStartWitness=true;
128 originalStartRequestId=requestId;
92 129 }
93 130 else if(!originalStartWitness)throw new InvalidDataException("SCO renewal has no original enable witness.");
94 131 OriginalReceipt?.Invoke(receipt.Clone());return;
Modified XFEEDToolBox.Core/Navigation/NativeFlightControlDriver.cs +1 -1
@@ -9,7 +9,7 @@ namespace XFEEDToolBox.Core.Navigation;
9 9 public sealed class NavigationPipeClient(int processId,string service="Navigation")
10 10 {
11 11 public int ProcessId { get; }=processId>0?processId:throw new ArgumentOutOfRangeException(nameof(processId));
12 private readonly string serviceName=service is "Navigation" or "Interaction" or "SessionRecovery0514" or "SessionRecovery0514c" or "SessionRecovery0514d" or "SessionRecovery0514e" or "SessionRecovery0514f" or "SessionRecovery0514g" or "SessionRecovery0514h" or "SessionRecovery0514i" or "SessionRecovery0514j" or "SessionRecovery0514k" or "CruiseObservation0514a" or "CruiseObservation0514b" or "CruiseObservation0514c" or "CruiseControl0514a" or "CruiseControl0514b" or "CruiseControl0514c"?service:throw new ArgumentException("Unknown game service.",nameof(service));
12 private readonly string serviceName=service is "Navigation" or "Interaction" or "SessionRecovery0514" or "SessionRecovery0514c" or "SessionRecovery0514d" or "SessionRecovery0514e" or "SessionRecovery0514f" or "SessionRecovery0514g" or "SessionRecovery0514h" or "SessionRecovery0514i" or "SessionRecovery0514j" or "SessionRecovery0514k" or "CruiseObservation0514a" or "CruiseObservation0514b" or "CruiseObservation0514c" or "CruiseControl0514a" or "CruiseControl0514b" or "CruiseControl0514c" or "CruiseControl0514d"?service:throw new ArgumentException("Unknown game service.",nameof(service));
13 13 private readonly SemaphoreSlim gate=new(1,1);
14 14 public async Task<JsonElement> CallAsync(object request,CancellationToken token=default)
15 15 {
Modified XFEEDToolBox.Core/Navigation/NativeSystemFlightControlDriver.CruiseControl.cs +1 -1
@@ -10,7 +10,7 @@ public sealed partial class NativeSystemFlightControlDriver
10 10 if(useNativeSupercruiseAssist)throw new InvalidOperationException("Original SCO requires toolbox navigation with game SCA disabled.");
11 11 var state=player();if(allowUnqualifiedEmptyTrial&&(!state.InGame||!state.Docked||state.CurrentShipId is null||state.CargoCount!=0))throw new InvalidOperationException("Unqualified SCO testing requires the empty ship to remain docked during setup.");
12 12 var channel=new NativeCruiseControlChannel(processId,gameHash);var capabilities=await channel.InspectAsync(token);
13 if(!capabilities.Available||!allowUnqualifiedEmptyTrial&&!capabilities.Qualified)throw new NotSupportedException("Original SCO controls have not completed live qualification.");
13 if(!capabilities.Available)throw new NotSupportedException("Original SCO controller is unavailable.");
14 14 originalCruiseControl=channel;originalCruiseFuelReserve=fuelReserve;
15 15 }
16 16 private Task UpdateOriginalCruiseAsync(SystemFlightDecision decision,SystemFlightFrame frame,SystemNavigationTarget target,DateTimeOffset deadline,CancellationToken token)
Modified XFEEDToolBox.Core/Navigation/NativeSystemFlightControlDriver.cs +6 -1
@@ -44,7 +44,12 @@ public sealed partial class NativeSystemFlightControlDriver(int processId,string
44 44 owningThread=hello.GetProperty("owningThread").GetInt32();
45 45 nativeDockingThrottle=Flag(hello,"nativeDockingThrottle");
46 46 cooldownFlightControls=Flag(hello,"localCooldownControls");
47 return new(gameHash,Flag(hello,"localTargets"),Flag(hello,"localCruiseControl"),Flag(hello,"localDockingRequests"),Flag(hello,"localNativeAssists"),
47 var applicationFlight = !useNativeSupercruiseAssist && originalCruiseControl is not null
48 && Flag(hello,"localTargets") && Flag(hello,"applicationSteeringAxes") && Flag(hello,"referenceGeometry") && Flag(hello,"adaptiveFlightControl")
49 && hello.TryGetProperty("localNavigationSchema",out var localSchema) && localSchema.GetInt32()==1;
50 var originalDocking = dockingResetPipe is not null && requestOriginalDockingStart;
51 return new(gameHash,Flag(hello,"localTargets"),Flag(hello,"localCruiseControl")||applicationFlight,
52 Flag(hello,"localDockingRequests")||originalDocking,Flag(hello,"localNativeAssists")||originalDocking,
48 53 Flag(hello,"localDockingPaths"),Flag(hello,"localPlanetaryApproach"),hello.GetProperty("evidence").GetString()??"");
49 54 }
50 55 public async Task SelectAsync(SystemNavigationTarget target,CancellationToken token)
Modified XFEEDToolBox.Core/Navigation/SystemFlightNavigator.cs +3 -2
@@ -21,8 +21,9 @@ public sealed class SystemFlightNavigator
21 21 public SystemFlightDecision Decide(SystemFlightFrame frame,SystemNavigationTarget target,DateTimeOffset now)
22 22 {
23 23 target.Validate();
24 if(!frame.InGame||frame.PlayerOverride||frame.ExternalThreat)
25 return Stop("已停止导航并交还飞船控制。");
24 if(!frame.InGame)return Stop("游戏尚未处于驾驶场景,已交还控制。");
25 if(frame.PlayerOverride)return Stop("原生接口报告玩家接管,已交还飞船控制。");
26 if(frame.ExternalThreat)return Stop("已检测到外部威胁,已停止导航并交还控制。");
26 27 if(frame.SystemAddress!=target.SystemAddress||frame.SelectedTarget is not {} selected||!target.SameIdentity(selected))
27 28 return Stop("游戏目标或所在星系已改变,已停止导航。");
28 29 if(!Valid(frame,now))return Wait("正在重新读取目标与飞行数据。");
Modified XFEEDToolBox.Core/Navigation/SystemNavigationRunner.cs +7 -1
@@ -13,6 +13,8 @@ public interface ISystemFlightControlDriver
13 13 Task SuspendSystemFlightAsync()=>ReleaseSystemFlightAsync();
14 14 }
15 15 public sealed record SystemNavigationProgress(NavigationRunState State,SystemNavigationTarget Target,SystemNavigationPhase Phase,string Message);
16 public sealed class DockingHandoffException(string message,long marketId):InvalidOperationException(message)
17 {public long MarketId{get;}=marketId;}
16 18
17 19 /// <summary>One local leg, with expiring inputs, actual target acknowledgements and bounded telemetry recovery.</summary>
18 20 public sealed class SystemNavigationRunner(Func<PlayerState> player,Func<DateTimeOffset>? clock=null)
@@ -21,6 +23,7 @@ public sealed class SystemNavigationRunner(Func<PlayerState> player,Func<DateTim
21 23 private readonly object gate=new();
22 24 private CancellationTokenSource? cancellation;
23 25 public NavigationRunState State{get;private set;}
26 public bool HandoffFailed{get;private set;}
24 27 public event Action<SystemNavigationProgress>? Progress;
25 28 public void Stop(){lock(gate)cancellation?.Cancel();}
26 29 public async Task RunAsync(SystemNavigationTarget target,ISystemFlightControlDriver driver,CancellationToken token=default)
@@ -31,7 +34,7 @@ public sealed class SystemNavigationRunner(Func<PlayerState> player,Func<DateTim
31 34 if(cancellation is not null)throw new InvalidOperationException("星系内导航已在运行。");
32 35 stop=CancellationTokenSource.CreateLinkedTokenSource(token);cancellation=stop;
33 36 }
34 var phase=SystemNavigationPhase.Preparing;var state=NavigationRunState.Running;var terminal="星系内导航已停止。";
37 HandoffFailed=false;var phase=SystemNavigationPhase.Preparing;var state=NavigationRunState.Running;var terminal="星系内导航已停止。";
35 38 void Report(SystemNavigationPhase next,string message){phase=next;State=NavigationRunState.Running;Progress?.Invoke(new(State,target,next,message));}
36 39 try
37 40 {
@@ -65,7 +68,10 @@ public sealed class SystemNavigationRunner(Func<PlayerState> player,Func<DateTim
65 68 if(decision.Phase==SystemNavigationPhase.Stopped)
66 69 {
67 70 if(decision.Actions.HasFlag(SystemNavigationAction.ExitForFailedHandoff))
71 {
72 HandoffFailed=true;
68 73 await driver.ApplySystemFlightAsync(decision,frame,target,frame.ObservedAt.AddMilliseconds(250),stop.Token);
74 }
69 75 state=NavigationRunState.Stopped;terminal=decision.Message;break;
70 76 }
71 77 await driver.ApplySystemFlightAsync(decision,frame,target,frame.ObservedAt.AddMilliseconds(250),stop.Token);
Modified XFEEDToolBox/App.xaml.cs +1 -1
@@ -45,7 +45,7 @@ public partial class App : Application
45 45 {
46 46 await Runtime.InitializeAsync();
47 47 await ViewModel.InitializeAsync();
48 if(GetArgument(commandLine,"--automatic-trade") is {} automaticTrade){await ViewModel.RunAutomaticTradeAsync(automaticTrade);return;}
48 if(GetArgument(commandLine,"--automatic-trade") is {} automaticTrade){AppShellPage.Current!.Navigate(typeof(PlannerPage));await ViewModel.RunAutomaticTradeAsync(automaticTrade);return;}
49 49 if(GetArgument(commandLine,"--smoke-current-system") is {} systemReport){await RunCurrentSystemProbeAsync(systemReport);return;}
50 50 if(GetArgument(commandLine,"--smoke-interface") is {} interfaceReport){await RunInterfaceOnlyProbeAsync(interfaceReport);return;}
51 51 if(GetArgument(commandLine,"--smoke-ship-status") is {} shipStatusReport){await RunShipStatusProbeAsync(shipStatusReport);return;}
Modified XFEEDToolBox/ViewModels/AutomaticTradeViewModel.cs +127 -16
@@ -1,4 +1,6 @@
1 1 using System.Text.Json;
2 using System.Security.Cryptography;
3 using System.Text;
2 4 using XFEEDToolBox.Core.Execution;
3 5 using XFEEDToolBox.Core.Models;
4 6 using XFEEDToolBox.Core.Navigation;
@@ -8,6 +10,7 @@ namespace XFEEDToolBox.ViewModels;
8 10
9 11 public partial class ToolboxViewModel
10 12 {
13 private sealed record AutomaticTradeMembership(string CommanderId,int ProcessId,DateTimeOffset ObservedAt,OriginalCommunityGoalRead Read);
11 14 private CancellationTokenSource? automaticTradeStop;
12 15 internal async Task RunAutomaticTradeAsync(string configurationPath)
13 16 {
@@ -19,6 +22,7 @@ public partial class ToolboxViewModel
19 22 using var stop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token);automaticTradeStop=stop;RunActive=true;
20 23 var token=stop.Token;var root=configuration.WorkingDirectory;var phase="preflight";var initial=runtime.Player;
21 24 var completedVisits=0;var completedBatches=0;decimal net=0;string? activeJourney=null;NativeTradeCommandDriver? driver=null;
25 var menuReentryVerified=true;int pid=0;decimal reserve=0;
22 26 void RefreshCompleted()
23 27 {
24 28 net=0;completedBatches=0;
@@ -32,7 +36,7 @@ public partial class ToolboxViewModel
32 36 }
33 37 async Task State(string value,string? issue=null)
34 38 {
35 phase=value;runtime.Log("自动跑商",issue??value);
39 phase=value;RunStatus=issue is null?"自动跑商中":"需要处理";RunMessage=issue??value;ActualProfit=$"{net:N0} Cr";ProfitLabel="本次已确认净利润";runtime.Log("自动跑商",issue??value);
36 40 var payload=new{at=DateTimeOffset.UtcNow,phase,issue,executor="XFEEDToolBox",agentControlsGame=false,scheduledTask=false,completedVisits,completedBatches,activeJourney,
37 41 runtime.Player.SystemName,runtime.Player.StationName,runtime.Player.Docked,runtime.Player.CargoCount,netProfit=net};
38 42 var path=Path.Combine(root,"state.json");await File.WriteAllTextAsync(path+".tmp",JsonSerializer.Serialize(payload,ToolboxSettings.JsonOptions));File.Move(path+".tmp",path,true);
@@ -48,20 +52,109 @@ public partial class ToolboxViewModel
48 52 async Task Travel(Station station)
49 53 {
50 54 if(runtime.Player.Docked&&runtime.Player.MarketId==station.MarketId)return;
51 await driver!.ExecuteAsync(new(TradeActionKind.Travel,new(){From=runtime.Player.MarketId==configuration.Source.MarketId?configuration.Source:configuration.Destination,To=station},null),Check,token);
55 for(var attempt=0;;attempt++)
56 {
57 try
58 {
59 await driver!.ExecuteAsync(new(TradeActionKind.Travel,new(){From=runtime.Player.MarketId==configuration.Source.MarketId?configuration.Source:configuration.Destination,To=station},null),Check,token);return;
60 }
61 catch(DockingHandoffException)when(attempt<3&&menuReentryVerified){await RecoverHandoff();}
62 }
63 }
64 async Task RecoverHandoff()
65 {
66 await State("reentering-solo-after-handoff");
67 if(!menuReentryVerified)throw new InvalidOperationException("返回主菜单路径尚未实机确认,保留客户端及保护等待修复。");
68 var workflow=new NativeRelogWorkflow(new NativeRelogChannel(pid,NativeRelogChannel.SupportedGameSha256,returnToMenuTrial:true),()=>runtime.Player)
69 {Timeout=TimeSpan.FromMinutes(8)};
70 workflow.Progress+=message=>runtime.Log("重进",message);await workflow.RunAsync(token);initial=runtime.Player;
71 var until=DateTimeOffset.UtcNow.AddSeconds(15);
72 while(true)
73 {
74 try{await Check(token);break;}
75 catch(Exception error)when(NavigationRecovery.CanRetry(error,token)&&DateTimeOffset.UtcNow<until){await Task.Delay(80,token);}
76 }
77 await State("connecting-native-trade");driver=await CreateNativeTradeDriverAsync(new(),token,new(configuration.CargoCapacity,reserve),Path.Combine(root,"operations"));await driver.PreflightAsync(token);
52 78 }
53 79 async Task<IReadOnlyList<CommodityQuote>> Quotes(Station station)
54 80 {
55 81 var rows=await driver!.ReadDockedQuotesAsync(station.MarketId,token);
56 await runtime.Markets.SaveSnapshotAsync(new(station,rows,DateTimeOffset.UtcNow,DataSource.Journal),token);return rows;
82 await runtime.Markets.SaveSnapshotAsync(new(station,rows,DateTimeOffset.UtcNow,DataSource.Journal),token);
83 if(station.MarketId==configuration.Destination.MarketId)
84 await File.WriteAllTextAsync(Path.Combine(root,"destination-original-quotes.json"),JsonSerializer.Serialize(rows,ToolboxSettings.JsonOptions),token);
85 return rows;
86 }
87 async Task RememberMembership(OriginalCommunityGoalRead read)
88 {
89 var path=Path.Combine(root,"membership-original.json");
90 await File.WriteAllTextAsync(path+".tmp",JsonSerializer.Serialize(new AutomaticTradeMembership(initial.CommanderId,pid,DateTimeOffset.UtcNow,read),ToolboxSettings.JsonOptions),token);
91 File.Move(path+".tmp",path,true);
92 }
93 async Task<(DateTimeOffset Time,string Event,string Json)> CurrentCommunityGoalEvent()
94 {
95 var path=Directory.EnumerateFiles(runtime.Settings.JournalDirectory,"Journal*.log").OrderByDescending(File.GetLastWriteTimeUtc).FirstOrDefault()
96 ??throw new InvalidDataException("当前游戏原始日志不存在。");
97 using var stream=new FileStream(path,FileMode.Open,FileAccess.Read,FileShare.ReadWrite|FileShare.Delete);
98 using var reader=new StreamReader(stream);bool sameCommander=false;
99 (DateTimeOffset Time,string Event,string Json)? latest=null;
100 while(await reader.ReadLineAsync(token) is {} line)
101 {
102 if(!line.Contains("\"LoadGame\"",StringComparison.Ordinal)&&!line.Contains("\"CommunityGoal\"",StringComparison.Ordinal))continue;
103 using var document=JsonDocument.Parse(line);var row=document.RootElement;var kind=row.GetProperty("event").GetString();
104 if(kind=="LoadGame")sameCommander=row.TryGetProperty("FID",out var fid)&&fid.GetString()==initial.CommanderId;
105 else if(kind=="CommunityGoal"&&sameCommander)latest=(row.GetProperty("timestamp").GetDateTimeOffset(),kind,line);
106 }
107 return latest??throw new InvalidDataException("当前指挥官原始日志没有CG贡献事件。");
108 }
109 async Task<OriginalCommunityGoalRead> Membership(NativeCommunityGoalClient client)
110 {
111 if(runtime.Player.Docked&&runtime.Player.MarketId==configuration.Destination.MarketId)
112 {var read=await client.ReadAsync(true,token);await RememberMembership(read);return read;}
113 var cached=Path.Combine(root,"membership-original.json");OriginalCommunityGoalRead original;
114 if(File.Exists(cached))
115 {
116 var prior=JsonSerializer.Deserialize<AutomaticTradeMembership>(await File.ReadAllTextAsync(cached,token),ToolboxSettings.JsonOptions)
117 ??throw new InvalidDataException("原始CG回执缓存为空。");
118 if(prior.CommanderId!=initial.CommanderId||prior.ProcessId!=pid||DateTimeOffset.UtcNow-prior.ObservedAt>TimeSpan.FromMinutes(30)||prior.ObservedAt>DateTimeOffset.UtcNow)
119 throw new InvalidDataException("原始CG缓存不属于当前指挥官或已过期。");
120 original=prior.Read;
121 }
122 else
123 {
124 var directory=configuration.ConfirmedCommunityGoalJourney??throw new InvalidDataException("采购站不能读取Homer CG接口;需要已确认的原始报名回执。");
125 using var result=JsonDocument.Parse(await File.ReadAllTextAsync(Path.Combine(directory,"result.json"),token));var value=result.RootElement;
126 var checkpoint=value.GetProperty("checkpoint");var sale=checkpoint.GetProperty("sold");
127 if(!value.GetProperty("passed").GetBoolean()||!value.GetProperty("walletReconciled").GetBoolean()
128 ||checkpoint.GetProperty("commander").GetString()!=initial.CommanderId||sale.GetProperty("commanderId").GetString()!=initial.CommanderId
129 ||sale.GetProperty("marketId").GetInt64()!=configuration.Destination.MarketId||!sale.GetProperty("serverConfirmed").GetBoolean()
130 ||!sale.GetProperty("clientApplied").GetBoolean()||sale.GetProperty("cargoAfter").GetInt32()!=0)
131 throw new InvalidDataException("先前CG交货回执未确认当前指挥官。");
132 original=JsonSerializer.Deserialize<OriginalCommunityGoalRead>(await File.ReadAllTextAsync(Path.Combine(directory,"membership-after.json"),token),ToolboxSettings.JsonOptions)
133 ??throw new InvalidDataException("先前旅程缺少原始CG回执。");
134 var goal=CommunityGoalCatalogue.RequireTrade(CommunityGoalCatalogue.Parse(original.Body),DateTimeOffset.UtcNow,true);
135 var latest=await CurrentCommunityGoalEvent();
136 using var journal=JsonDocument.Parse(latest.Json);
137 var goals=journal.RootElement.GetProperty("CurrentGoals").EnumerateArray().Where(row=>row.GetProperty("CGID").GetUInt64()==goal.Id).ToArray();
138 if(latest.Event!="CommunityGoal"||latest.Time<initial.LoadGameAt||DateTimeOffset.UtcNow-latest.Time>TimeSpan.FromMinutes(30)||latest.Time>DateTimeOffset.UtcNow.AddSeconds(3)
139 ||goals.Length!=1||goals[0].GetProperty("IsComplete").GetBoolean()||goals[0].GetProperty("PlayerContribution").GetInt64()!=goal.Contribution)
140 throw new InvalidDataException("当前日志未核对先前原始CG贡献,保留货舱不采购。");
141 }
142 if(original.Generation==0||Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes(original.Body)))!=original.Proof)
143 throw new InvalidDataException("原始CG响应摘要不匹配。");
144 return original with{Goals=CommunityGoalCatalogue.Parse(original.Body)};
57 145 }
58 146 async Task CompleteJourney(string path,TradeLeg leg,NativeCommunityGoalClient membershipClient)
59 147 {
60 148 using var prior=JsonDocument.Parse(await File.ReadAllTextAsync(Path.Combine(path,"membership-before.json"),token));
61 149 var baseline=prior.RootElement.GetProperty("contribution").GetInt64();var goalId=prior.RootElement.GetProperty("goalId").GetUInt64();
62 150 activeJourney=path;await State("cargo-journey");
63 var journey=new NativeTradeJourney(driver!,()=>runtime.Player,Check,path);journey.Status+=message=>runtime.Log("自动跑商",message);
64 await journey.RunAsync(leg,token);
151 for(var attempt=0;;attempt++)
152 {
153 var journey=new NativeTradeJourney(driver!,()=>runtime.Player,Check,path);journey.Status+=message=>{RunMessage=message;runtime.Log("自动跑商",message);};
154 try{await journey.RunAsync(leg,token);break;}
155 catch(NativeTradeJourneyIncompleteException error)when(error.InnerException is DockingHandoffException&&attempt<3&&menuReentryVerified)
156 {await RecoverHandoff();}
157 }
65 158 var until=DateTimeOffset.UtcNow.AddSeconds(35);OriginalCommunityGoalRead updated;
66 159 while(true)
67 160 {
@@ -71,23 +164,27 @@ public partial class ToolboxViewModel
71 164 await Task.Delay(300,token);
72 165 }
73 166 await File.WriteAllTextAsync(Path.Combine(path,"cg-contribution-confirmed.json"),JsonSerializer.Serialize(updated,ToolboxSettings.JsonOptions),token);
167 await RememberMembership(updated);
74 168 activeJourney=null;RefreshCompleted();await State("cycle-completed");
75 169 }
76 170 try
77 171 {
78 RefreshCompleted();
172 await State("preflight");RefreshCompleted();
79 173 var pending=Directory.EnumerateDirectories(root,"journey-*").Where(path=>File.Exists(Path.Combine(path,"leg.json"))&&!File.Exists(Path.Combine(path,"cg-contribution-confirmed.json"))).ToArray();
80 174 if(!initial.InGame||initial.CurrentShipId is null||initial.CargoCapacity!=configuration.CargoCapacity||initial.Rebuy<=0||pending.Length>1
81 ||pending.Length==0&&(!initial.Docked||initial.CargoCount!=0))
175 ||pending.Length==0&&initial.CargoCount!=0)
82 176 throw new InvalidOperationException("自动流程需要原船、实际重购费,以及安全停靠空舱或唯一可恢复货物批次。");
83 var reserve=Math.Max(configuration.ReserveCredits,initial.Rebuy);
177 reserve=Math.Max(configuration.ReserveCredits,initial.Rebuy);
178 await State("connecting-native-trade");
84 179 driver=await CreateNativeTradeDriverAsync(new(),token,new(configuration.CargoCapacity,reserve),Path.Combine(root,"operations"));await driver.PreflightAsync(token);
85 var pid=((NativeFlightControlDriver)runtime.FlightDriver!).ProcessId;var channel=new NativeCommerceChannel(pid);
180 pid=((NativeFlightControlDriver)runtime.FlightDriver!).ProcessId;var channel=new NativeCommerceChannel(pid);
86 181 var hello=await channel.CallAsync(new{op="describe"},token);
87 bool Feature(string name)=>hello.TryGetProperty(name,out var value)&&value.ValueKind==JsonValueKind.True;
88 if(configuration.RequireSupercruiseOvercharge&&!Feature("supercruiseOverchargeControl")||configuration.RequireInterdictionEscape&&!Feature("interdictionEscapeControl"))
89 throw new NotSupportedException("SCO温控/燃油控制和原始逃逸矢量模块尚未就绪,空载测试与采购尚未启动。");
182 var cruise=await new NativeCruiseControlChannel(pid,NativeRelogChannel.SupportedGameSha256).InspectAsync(token);
183 if(!cruise.Available)
184 throw new NotSupportedException("原生SCO启停及温度燃油控制尚未完成实机验收,采购尚未启动。");
185
90 186 var membershipClient=new NativeCommunityGoalClient(channel);
187 if(runtime.Player.Docked&&runtime.Player.MarketId==configuration.Destination.MarketId)await Membership(membershipClient);
91 188 if(pending.Length==1)
92 189 {
93 190 var saved=JsonSerializer.Deserialize<TradeLeg>(await File.ReadAllTextAsync(Path.Combine(pending[0],"leg.json"),token),ToolboxSettings.JsonOptions)
@@ -101,11 +198,20 @@ public partial class ToolboxViewModel
101 198 // program observes actual menu and new Solo session receipts.
102 199 var relog=new NativeRelogWorkflow(new NativeRelogChannel(pid,NativeRelogChannel.SupportedGameSha256,returnToMenuTrial:true),()=>runtime.Player)
103 200 {Timeout=TimeSpan.FromMinutes(8)};
104 relog.Progress+=message=>runtime.Log("重进",message);await relog.RunAsync(token);initial=runtime.Player;await Check(token);
201 relog.Progress+=message=>runtime.Log("重进",message);await relog.RunAsync(token);menuReentryVerified=true;initial=runtime.Player;await Check(token);
105 202 driver=await CreateNativeTradeDriverAsync(new(),token,new(configuration.CargoCapacity,reserve),Path.Combine(root,"operations"));await driver.PreflightAsync(token);
106 203 await File.WriteAllTextAsync(Path.Combine(root,"menu-reentry-passed.json"),JsonSerializer.Serialize(new{passed=true,pid,initial.LoadGameAt,at=DateTimeOffset.UtcNow,clientTerminated=false},ToolboxSettings.JsonOptions),token);
107 204 }
108 await Travel(configuration.Destination);var destinationQuotes=await Quotes(configuration.Destination);
205 IReadOnlyList<CommodityQuote> destinationQuotes=runtime.Markets.LoadRegion(configuration.Destination.MarketId,1).Quotes
206 .Where(row=>row.MarketId==configuration.Destination.MarketId&&row.Source==DataSource.Journal&&DateTimeOffset.UtcNow-row.CollectedAt<TimeSpan.FromMinutes(30)).ToArray();
207 var quoteCache=Path.Combine(root,"destination-original-quotes.json");
208 if(File.Exists(quoteCache))
209 {
210 var saved=JsonSerializer.Deserialize<CommodityQuote[]>(await File.ReadAllTextAsync(quoteCache,token),ToolboxSettings.JsonOptions);
211 if(saved is {Length:>0}&&saved.All(row=>row.MarketId==configuration.Destination.MarketId&&DateTimeOffset.UtcNow-row.CollectedAt<TimeSpan.FromMinutes(30)))destinationQuotes=saved;
212 }
213 if(destinationQuotes.Count==0){await Travel(configuration.Destination);destinationQuotes=await Quotes(configuration.Destination);}
214 await State("travelling-to-source");
109 215 if(destinationQuotes.Count==0)throw new InvalidDataException("目的站原始报价为空。");
110 216 foreach(var path in Directory.EnumerateFiles(root,"empty-visit-*.json"))
111 217 {
@@ -132,7 +238,7 @@ public partial class ToolboxViewModel
132 238 var candidates=sourceQuotes.Where(quote=>NativeTradeBatchPolicy.Allows(quote.Commodity)&&!quote.Illegal)
133 239 .Select(quote=>new CgBatchQuote(quote.Commodity,quote.DisplayName,quote.Supply,quote.BuyPrice,destinationQuotes.SingleOrDefault(value=>value.Commodity==quote.Commodity)?.SellPrice??0)).ToArray();
134 240 var order=CgBatchPlanner.Select(candidates,configuration.CargoCapacity,wallet.Credits,reserve);
135 var membership=await membershipClient.ReadAsync(true,token);var goal=CommunityGoalCatalogue.RequireTrade(membership.Goals,DateTimeOffset.UtcNow,true);
241 var membership=await Membership(membershipClient);var goal=CommunityGoalCatalogue.RequireTrade(membership.Goals,DateTimeOffset.UtcNow,true);
136 242 var baseline=goal.Contribution??throw new InvalidDataException("CG个人贡献基线缺失。");
137 243 var leg=new TradeLeg{Number=1,From=configuration.Source,To=configuration.Destination,Orders=[order]};
138 244 activeJourney=Path.Combine(root,"journey-"+Guid.NewGuid().ToString("N"));Directory.CreateDirectory(activeJourney);
@@ -142,7 +248,12 @@ public partial class ToolboxViewModel
142 248 }
143 249 }
144 250 catch(OperationCanceledException)when(token.IsCancellationRequested){await State("stopped");}
145 catch(Exception error){await State("requires-recovery",error.Message);ShowError(error.Message);}
251 catch(Exception error)
252 {
253 await File.WriteAllTextAsync(Path.Combine(root,"failure-"+DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()+".json"),
254 JsonSerializer.Serialize(new{at=DateTimeOffset.UtcNow,phase,error=error.ToString()},ToolboxSettings.JsonOptions));
255 await State("requires-recovery",error.Message);ShowError(error.Message);
256 }
146 257 finally
147 258 {
148 259 if(driver is not null)await driver.ReleaseAllAsync();automaticTradeStop=null;RunActive=false;
Modified XFEEDToolBox/ViewModels/PluginViewModel.cs +0 -1
Modified XFEEDToolBox/ViewModels/TradeExecutionViewModel.cs +1 -1
Modified docs/TRADE-WORK-COORDINATION.md +53 -0