返回提交历史
Modified
XFEEDToolBox/ViewModels/AutomaticTradeViewModel.cs
+20
-3
Modified
XFEEDToolBox/ViewModels/ToolboxViewModel.cs
+12
-1
Modified
XFEEDToolBox/ViewModels/TradePlanExecutionViewModel.cs
+6
-1
Modified
XFEEDToolBox/Views/PlannerPage.xaml
+1
-1
Modified
docs/TRADE-WORK-COORDINATION.md
+9
-0
XFEstudio/XFEEDToolBox
Fix trade page cancellation for externally started workflows
e9c511e
代码差异
5 个文件
+48
-6
@@ -12,6 +12,7 @@ public partial class ToolboxViewModel
12
12
{
13
13
private sealed record AutomaticTradeMembership(string CommanderId,int ProcessId,DateTimeOffset ObservedAt,OriginalCommunityGoalRead Read);
14
14
private CancellationTokenSource? automaticTradeStop;
15
private string? automaticTradeConfigurationPath;
15
16
internal async Task RunAutomaticTradeAsync(string configurationPath)
16
17
{
17
18
if(RunActive||automaticTradeStop is not null)throw new InvalidOperationException("工具箱已有跑商流程。");
@@ -19,6 +20,7 @@ public partial class ToolboxViewModel
19
20
??throw new InvalidDataException("自动跑商配置为空。");configuration.Validate();
20
21
Directory.CreateDirectory(configuration.WorkingDirectory);
21
22
using var lease=new FileStream(Path.Combine(configuration.WorkingDirectory,"workflow.lock"),FileMode.OpenOrCreate,FileAccess.ReadWrite,FileShare.None);
23
automaticTradeConfigurationPath=Path.GetFullPath(configurationPath);
22
24
using var stop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token);automaticTradeStop=stop;RunActive=true;
23
25
var token=stop.Token;var root=configuration.WorkingDirectory;var phase="preflight";var initial=runtime.Player;
24
26
var completedVisits=0;var completedBatches=0;decimal net=0;string? activeJourney=null;NativeTradeCommandDriver? driver=null;
@@ -36,7 +38,8 @@ public partial class ToolboxViewModel
36
38
}
37
39
async Task State(string value,string? issue=null)
38
40
{
39
phase=value;RunStatus=issue is null?"自动跑商中":"需要处理";RunMessage=issue??value;ActualProfit=$"{net:N0} Cr";ProfitLabel="本次已确认净利润";runtime.Log("自动跑商",issue??value);
41
phase=value;RunStatus=value switch{"stopped"=>"已取消","stopped-pending-reconciliation"=>"已取消,待核对",_=>issue is null?"自动跑商中":"需要处理"};
42
RunMessage=issue??(value=="stopped"?"自动跑商已取消,已确认交易和恢复检查点已保留。":value);ActualProfit=$"{net:N0} Cr";ProfitLabel="本次已确认净利润";runtime.Log("自动跑商",RunMessage);
40
43
var payload=new{at=DateTimeOffset.UtcNow,phase,issue,executor="XFEEDToolBox",agentControlsGame=false,scheduledTask=false,completedVisits,completedBatches,activeJourney,
41
44
runtime.Player.SystemName,runtime.Player.StationName,runtime.Player.Docked,runtime.Player.CargoCount,netProfit=net};
42
45
var path=Path.Combine(root,"state.json");await File.WriteAllTextAsync(path+".tmp",JsonSerializer.Serialize(payload,ToolboxSettings.JsonOptions));File.Move(path+".tmp",path,true);
@@ -264,8 +267,16 @@ public partial class ToolboxViewModel
264
267
await File.WriteAllTextAsync(Path.Combine(activeJourney,"leg.json"),JsonSerializer.Serialize(leg,ToolboxSettings.JsonOptions),token);
265
268
await CompleteJourney(activeJourney,leg,membershipClient);destinationQuotes=await Quotes(configuration.Destination);
266
269
}
270
token.ThrowIfCancellationRequested();
271
}
272
catch(Exception error)when(token.IsCancellationRequested)
273
{
274
var unresolved=Directory.EnumerateFiles(root,"*.pending.json",SearchOption.AllDirectories)
275
.Any(path=>!File.Exists(path.Replace(".pending.json",".confirmed.json",StringComparison.Ordinal)));
276
await File.WriteAllTextAsync(Path.Combine(root,"cancellation-"+DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()+".json"),
277
JsonSerializer.Serialize(new{at=DateTimeOffset.UtcNow,phase,unresolved,error=error.ToString()},ToolboxSettings.JsonOptions));
278
await State(unresolved?"stopped-pending-reconciliation":"stopped",unresolved?"跑商已取消;已提交请求尚待核对,不会重复下单。":null);
267
279
}
268
catch(OperationCanceledException)when(token.IsCancellationRequested){await State("stopped");}
269
280
catch(Exception error)
270
281
{
271
282
await File.WriteAllTextAsync(Path.Combine(root,"failure-"+DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()+".json"),
@@ -274,7 +285,13 @@ public partial class ToolboxViewModel
274
285
}
275
286
finally
276
287
{
277
if(driver is not null)await driver.ReleaseAllAsync();automaticTradeStop=null;RunActive=false;
288
try{if(driver is not null)await driver.ReleaseAllAsync();}
289
catch(Exception error)
290
{
291
await File.WriteAllTextAsync(Path.Combine(root,"release-failure-"+DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()+".json"),error.ToString());
292
await State("requires-recovery","释放跑商控制失败:"+error.Message);ShowError(error.Message);
293
}
294
finally{automaticTradeStop=null;RunActive=false;RefreshTradePlanCommands();CalculateCommand.NotifyCanExecuteChanged();}
278
295
}
279
296
}
280
297
}
@@ -186,6 +186,7 @@ public partial class ToolboxViewModel : ViewModelBase, IDisposable
186
186
{ var values=next.ToArray();return current.SequenceEqual(values)?current:values; }
187
187
private void OnRunChanged(RunProgress progress) => dispatcher.TryEnqueue(() =>
188
188
{
189
if (automaticTradeStop is not null) return;
189
190
lastRun = progress;
190
191
RunActive = progress.State is RunState.Running or RunState.Paused;
191
192
RunStatus = progress.State switch { RunState.Running => "执行中", RunState.Paused => "已暂停", RunState.Completed => "已完成", RunState.Stopped => "已停止", RunState.Failed => "需要处理", _ => "等待路线" };
@@ -480,7 +481,17 @@ public partial class ToolboxViewModel : ViewModelBase, IDisposable
480
481
}
481
482
[RelayCommand] private void PauseRun() => runtime.Runner.Pause();
482
483
[RelayCommand] private void ResumeRun() { try { runtime.Runner.Resume(); } catch (Exception e) { ShowError(e.Message); } }
483
[RelayCommand] private void StopRun() {automaticTradeStop?.Cancel();runtime.Runner.Stop();}
484
[RelayCommand] private void StopRun()
485
{
486
if (automaticTradeStop is { } stop)
487
{
488
RunStatus = "正在取消";
489
RunMessage = "正在停止跑商,核对已提交请求并释放控制。";
490
stop.Cancel();
491
return;
492
}
493
runtime.Runner.Stop();
494
}
484
495
[RelayCommand] private void PreviousMarketPage() { marketPage = Math.Max(0, marketPage - 1); Interlocked.Increment(ref marketRequest); OnDataChanged(RuntimeChange.Markets); }
485
496
[RelayCommand] private void NextMarketPage() { if (MarketRows.Count == 150) marketPage++; Interlocked.Increment(ref marketRequest); OnDataChanged(RuntimeChange.Markets); }
486
497
[RelayCommand] private void CopyDestination()
@@ -5,7 +5,7 @@ namespace XFEEDToolBox.ViewModels;
5
5
6
6
public partial class ToolboxViewModel
7
7
{
8
private bool CanExecuteTradePlan() => SelectedStart is not null && CanCalculate() && !NavigationActive
8
private bool CanExecuteTradePlan() => (SelectedStart is not null || automaticTradeConfigurationPath is not null) && CanCalculate() && !NavigationActive
9
9
&& !DemoMode && !Environment.GetCommandLineArgs().Contains("--offline-preview");
10
10
11
11
partial void OnSelectedStartChanged(StationLiveRow? value) => RefreshTradePlanCommands();
@@ -20,6 +20,11 @@ public partial class ToolboxViewModel
20
20
private async Task ExecuteTradePlanAsync(CancellationToken cancellationToken)
21
21
{
22
22
if (!CanExecuteTradePlan()) return;
23
if (automaticTradeConfigurationPath is { } configurationPath)
24
{
25
await RunAutomaticTradeAsync(configurationPath);
26
return;
27
}
23
28
ExecutionModeIndex = 2;
24
29
Plan = null;
25
30
SelectedLeg = null;
@@ -35,7 +35,7 @@
35
35
<StackPanel Spacing="14">
36
36
<Grid ColumnSpacing="16"><Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
37
37
<StackPanel Spacing="6"><TextBlock Text="本次执行" Style="{StaticResource Eyebrow}"/><TextBlock Text="{x:Bind ViewModel.RunStatus, Mode=OneWay}" FontSize="23" FontWeight="SemiBold"/></StackPanel>
38
<Button x:Name="ExecuteTradePlanButton" Grid.Column="1" Content="自动执行" AutomationProperties.Name="自动执行跑商路线" Style="{StaticResource PrimaryButton}" MinWidth="132" VerticalAlignment="Center" controls:AsyncAction.Command="{x:Bind ViewModel.ExecuteTradePlanCommand}" controls:AsyncAction.CancelCommand="{x:Bind ViewModel.StopRunCommand}"/>
38
<Button x:Name="ExecuteTradePlanButton" Grid.Column="1" Content="自动执行" AutomationProperties.Name="自动执行跑商路线" AutomationProperties.AutomationId="ExecuteTradePlanButton" Style="{StaticResource PrimaryButton}" MinWidth="132" VerticalAlignment="Center" controls:AsyncAction.Command="{x:Bind ViewModel.ExecuteTradePlanCommand}" controls:AsyncAction.CancelCommand="{x:Bind ViewModel.StopRunCommand}" controls:AsyncAction.IsRunning="{x:Bind ViewModel.RunActive, Mode=OneWay}"/>
39
39
</Grid>
40
40
<TextBlock Text="{x:Bind ViewModel.RunMessage, Mode=OneWay}" TextWrapping="Wrap"/>
41
41
<ProgressBar Value="{x:Bind ViewModel.RunPercent, Mode=OneWay}" Maximum="100"/>
@@ -2,6 +2,15 @@
2
2
3
3
更新:2026-10-24(Asia/Shanghai)。最新状态以紧接下方的记录为准,后文保留历史故障及验收过程。
4
4
5
## 跑商页取消已完成实际验证;新采购失败单独保留(2026-10-24 21:54)
6
7
- 用户要求以最终目标和实际验收作为交付标准,不把源码修改、构建通过或阶段进度称为完成;本次具体目标是运行中直接点跑商页取消,保留工具箱窗口。
8
- 根因是自动流程从工具箱入口启动时,按钮只看 ExecuteTradePlanCommand.IsRunning,没有绑定真正的 RunActive。现同一个按钮绑定实际运行状态;独立流程不会被旧 Runner 的回调覆盖;取消过程中明确显示正在取消,结束后显示已取消。再次点击自动执行继续同一配置和检查点。
9
- Release 构建零警告、零错误。实际工具箱 PID90544/startTicks639284460207594112,在同一个窗口中连续通过两次启动/取消;状态均为 stopped,工作锁释放,按钮恢复启用。13:40:24Z 至13:41:30Z的验证区间内没有新采购、销售、维护订单或离站,未核对订单为0。原图、执行区放大图、中文OCR、UIA与行为回执交叉核验,两个场景的视觉gate均pass,证据 work/visual/trade-cancel/。
10
- 取消不重放已提交请求;保留原始回执、账本和恢复检查点。有未核对订单时显示已取消,待核对;释放控制异常保留失败原因,运行状态仍能正确收尾。没有改原生ABI、使用键鼠、创建定时任务或调用子智能体。
11
- 上述验证结束后,21:42出现新采购操作668aa9c4b307410a8aa94b9aacf68134:原生state=4/error=7,mutationsSent=1、clientApplied=false、reconciliationRequired=true;客户端货舱0、钱包68785438,但这些不足以证明服务器未成交。原单和 pending 文件完整保留,未删除、补写成功或重发。该交易故障不是取消按钮验证成功的证据,也不能把取消修复称为跑商已恢复。
12
- 21:46:30原始Undocked、随后LTT18046/Phanes的FSDJump、21:51:30 Leiber Installation停靠及21:51:52 ShipyardSwap确认当前已变为ShipID7。后台原图与原生当前星系读取核对Phanes。此前把停靠状态8归因于打开站务界面、要求返回驾驶舱,依据不足,已撤回;不得为解决此问题直接把8加入可交易停靠状态。当前旧Corsair配置保持requires-recovery,不自动接管换船后的航行或重放该采购。
13
5
14
## 新流程两批已全部结算,新增确认净19145877 Cr(2026-10-24 21:15)
6
15
7
16
- 第二批 `journey-350a1809f44d4218ae0fd8bdc6159510` 已完整passed:采购7f3a87ac3b244bfea48e0acaee767ab5的192t铀全部售出,唯一售出2961988d665543bf9be27e3fcccbbb6f。售出后只读市场状态一次IPC超时,已保留原回执;恢复只完成剩余服务及余额核对,绝不重买/重卖。只读同generation状态轮询现在在原35秒上限内重试已知瞬态,不重发交易或服务请求。