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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Collections.ObjectModel;
using XFEEDToolBox.Core.Presentation;
using System.Diagnostics;
using System.Text.Json;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.UI.Dispatching;
using Windows.ApplicationModel.DataTransfer;
using Windows.Storage.Pickers;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Execution;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Planning;
using XFEEDToolBox.Core.Services;
using XFEEDToolBox.Core.Native;
using XFEEDToolBox.Views;
using XFEEDToolBox.Core.Session;
using XFEEDToolBox.Services;

namespace XFEEDToolBox.ViewModels;

public partial class ToolboxViewModel : ViewModelBase, IDisposable
{
    private readonly ToolboxRuntime runtime;
    private readonly DispatcherQueue dispatcher;
    private readonly DispatcherQueueTimer timer;
    private bool initialized, refreshing, revertingDemo;
    private int dirty, marketPage;
    private long? lastMarketFilterId;
    private long marketRequest;
    private readonly CancellationTokenSource lifetime = new();
    private RunProgress? lastRun;
    private GameEnvironment? gameEnvironment;
    private readonly GameSessionController sessions;
    private WindowsGlobalHotkeys? hotkeys;
    private CancellationTokenSource? sessionStop;
    public PlayerState CurrentPlayer => DemoMode ? DemoData.Player : runtime.Player;
    public string AppVersionLabel=>typeof(App).Assembly.GetName().Version is {} version?$"XFE ED TOOLBOX / {version.Major}.{version.Minor}.{version.Build}":"XFE ED TOOLBOX";

    [ObservableProperty] public partial bool DemoMode { get; set; }
    [ObservableProperty] public partial bool IsBusy { get; set; }
    [ObservableProperty] public partial bool RunActive { get; set; }
    [ObservableProperty] public partial bool SessionBusy { get; set; }
    [ObservableProperty, NotifyPropertyChangedFor(nameof(StartingGame),nameof(RestartingGame),nameof(ReloggingGame))]
    public partial SessionOperation? ActiveSessionOperation { get; set; }
    public bool StartingGame => ActiveSessionOperation == SessionOperation.Start;
    public bool RestartingGame => ActiveSessionOperation == SessionOperation.Restart;
    public bool ReloggingGame => ActiveSessionOperation == SessionOperation.Relog;
    [ObservableProperty, NotifyPropertyChangedFor(nameof(HasSessionMessage))] public partial string SessionMessage { get; set; } = "";
    public bool HasSessionMessage => !string.IsNullOrWhiteSpace(SessionMessage);
    [ObservableProperty] public partial bool HotkeysEnabled { get; set; }
    [ObservableProperty] public partial string RelogHotkey { get; set; } = "";
    [ObservableProperty] public partial string RestartHotkey { get; set; } = "";
    [ObservableProperty] public partial string HotkeyStatus { get; set; } = "全局快捷键未启用。";
    [ObservableProperty] public partial bool HasError { get; set; }
    [ObservableProperty] public partial string ErrorMessage { get; set; } = "";
    [ObservableProperty] public partial string StatusText { get; set; } = "正在读取本机数据";
    [ObservableProperty] public partial string Commander { get; set; } = "等待 Journal";
    [ObservableProperty] public partial string Ship { get; set; } = "—";
    [ObservableProperty] public partial string CreditsText { get; set; } = "—";
    [ObservableProperty] public partial string CargoText { get; set; } = "—";
    [ObservableProperty] public partial string MaterialTotalText { get; set; } = "—";
    [ObservableProperty] public partial string MaterialKindsText { get; set; } = "等待材料快照";
    [ObservableProperty] public partial string PlayerSnapshotText { get; set; } = "正在读取本机指挥官数据";
    [ObservableProperty] public partial string BalanceNote { get; set; } = "现金由 LoadGame 与后续交易事件推导";
    [ObservableProperty] public partial string SystemName { get; set; } = "等待位置数据";
    [ObservableProperty] public partial string StationName { get; set; } = "—";
    [ObservableProperty] public partial string DockingText { get; set; } = "未连接游戏";
    [ObservableProperty] public partial string CargoNote { get; set; } = "货舱数据待更新";
    [ObservableProperty] public partial string CoverageText { get; set; } = "0 个站点";
    [ObservableProperty] public partial string QuoteCountText { get; set; } = "0";
    [ObservableProperty] public partial string LatestText { get; set; } = "尚无行情";
    [ObservableProperty] public partial string SourceNote { get; set; } = "EDDN 增量行情 + 本地 Journal";
    [ObservableProperty] public partial string EddnCountText { get; set; } = "0";
    [ObservableProperty] public partial string MarketSearch { get; set; } = "";
    [ObservableProperty] public partial int MarketSortIndex { get; set; }
    [ObservableProperty] public partial bool OnlyCurrentStation { get; set; }
    [ObservableProperty] public partial string MarketPageText { get; set; } = "第 1 页";
    public LiveCollection<MarketLiveRow,MarketRow,(long,string)> MarketRows { get; } = new(x => (x.Station.MarketId,x.Quote.Commodity),x => new(x));
    public LiveCollection<StationLiveRow,Station,long> Stations { get; } = new(x => x.MarketId,x => new(x));
    [ObservableProperty] public partial int InventoryTab { get; set; }
    [ObservableProperty] public partial string InventorySearch { get; set; } = "";
    public LiveCollection<InventoryLiveRow,InventoryItem,(string,string,long?)> InventoryRows { get; } = new(x => (x.Group,x.Key,x.MissionId),x => new(x));
    public LiveCollection<ShipLiveRow,ShipRecord,long> Ships { get; } = new(x => x.ShipId,x => new(x));
    [ObservableProperty] public partial ShipLiveRow? SelectedShip { get; set; }
    public LiveCollection<ModuleLiveRow,FittedModule,string> FittedModules { get; } = new(x => x.Slot,x => new(x));
    public LiveCollection<StoredModuleLiveRow,StoredModule,long> StoredModules { get; } = new(x => x.StorageSlot,x => new(x));
    public LiveCollection<ReadCheckLiveRow,ReadVerificationItem,string> ReadChecks { get; } = new(x => x.Category,x => new(x));
    [ObservableProperty] public partial string FleetSummary { get; set; } = "等待船队与装配数据";
    [ObservableProperty] public partial string SelectedShipSummary { get; set; } = "选择一艘飞船查看最后记录的配置。";
    [ObservableProperty] public partial string CurrentShipSummary { get; set; } = "等待 Loadout";
    [ObservableProperty] public partial string CockpitSummary { get; set; } = "等待 Status.json";
    [ObservableProperty] public partial string ReadSummary { get; set; } = "正在核对读取覆盖范围";
    [ObservableProperty] public partial string ReadReportPath { get; set; } = "验收报告保存在工具箱自己的数据目录。";
    public LiveCollection<ActivityLiveRow,ActivityEntry,long> Activity { get; } = new(x => x.Id,x => new(x));

    [ObservableProperty] public partial StationLiveRow? SelectedStart { get; set; }
    [ObservableProperty] public partial double Cash { get; set; } = 1000000;
    [ObservableProperty] public partial double Reserve { get; set; } = 100000;
    [ObservableProperty] public partial double Hold { get; set; } = 64;
    [ObservableProperty] public partial double JumpRange { get; set; } = 20;
    [ObservableProperty] public partial double MaxDistance { get; set; } = 60;
    [ObservableProperty] public partial double Radius { get; set; } = 500;
    [ObservableProperty] public partial double MaxLegs { get; set; } = 4;
    [ObservableProperty] public partial double MaxAgeHours { get; set; } = 6;
    [ObservableProperty] public partial double MaxArrival { get; set; } = 10000;
    [ObservableProperty] public partial double JumpMinutes { get; set; } = 1;
    [ObservableProperty] public partial double DockMinutes { get; set; } = 4;
    [ObservableProperty] public partial double CostPerJump { get; set; } = 100;
    [ObservableProperty] public partial double CostPerMinute { get; set; }
    [ObservableProperty] public partial bool Loop { get; set; } = true;
    [ObservableProperty] public partial bool IncludePlanetary { get; set; }
    [ObservableProperty] public partial bool IncludeCarriers { get; set; }
    [ObservableProperty] public partial bool AllowUnknown { get; set; }
    [ObservableProperty] public partial int RiskIndex { get; set; } = 1;
    [ObservableProperty] public partial int ObjectiveIndex { get; set; }
    [ObservableProperty] public partial int PadIndex { get; set; } = 2;
    [ObservableProperty] public partial RoutePlan? Plan { get; set; }
    [ObservableProperty] public partial TradeLeg? SelectedLeg { get; set; }
    [ObservableProperty] public partial string PlanProfit { get; set; } = "—";
    [ObservableProperty] public partial string PlanHourly { get; set; } = "—";
    [ObservableProperty] public partial string PlanDuration { get; set; } = "—";
    [ObservableProperty] public partial string PlanInfo { get; set; } = "选择起点和约束,计算你的下一趟航程。";
    [ObservableProperty] public partial string PlanWarnings { get; set; } = "路线估计基于已知行情,出发前需确认游戏中的报价和航线。";

    [ObservableProperty] public partial int ExecutionModeIndex { get; set; }
    [ObservableProperty] public partial string ExecutionHint { get; set; } = "模拟演练只展示步骤,不向游戏发送输入。";
    [ObservableProperty] public partial string RunStatus { get; set; } = "等待路线";
    [ObservableProperty] public partial string RunMessage { get; set; } = "输入投入资金并规划路线,选择方案后开始执行。";
    [ObservableProperty] public partial string ActualProfit { get; set; } = "0 Cr";
    [ObservableProperty] public partial string ProfitLabel { get; set; } = "已确认净利润";
    [ObservableProperty] public partial string ProfitDetails { get; set; } = "按实际成交和停靠服务回执更新。";
    [ObservableProperty] public partial double RunPercent { get; set; }
    [ObservableProperty] public partial string NextDestination { get; set; } = "—";
    [ObservableProperty] public partial string JournalDirectory { get; set; } = "";
    [ObservableProperty] public partial string EddnEndpoint { get; set; } = "";
    [ObservableProperty] public partial bool EddnOnStartup { get; set; }
    [ObservableProperty] public partial string ImportSystemName { get; set; } = "";
    [ObservableProperty] public partial bool ImportNearby { get; set; } = true;
    [ObservableProperty] public partial string ImportStatus { get; set; } = "可按星系补全,或导入 Spansh JSON / GZip 快照。";
    [ObservableProperty] public partial string EnvironmentText { get; set; } = "正在自动读取本机舰船与游戏配置";
    public string AcceptanceMode => "数据只读接入 · 会话操作由主页按钮或已启用的快捷键触发";
    public string DataDirectory => runtime.DataDirectory;

    internal bool OfflinePreview{get;}
    public ToolboxViewModel(ToolboxRuntime runtime, DispatcherQueue dispatcher,bool offlinePreview=false)
    {
        galaxyLifetimeToken=lifetime.Token;
        OfflinePreview=offlinePreview;
        this.runtime = runtime; this.dispatcher = dispatcher;
        systemNavigator=runtime.FlightAutomation.Local;systemNavigator.Progress+=OnSystemFlightProgress;
        sessions = new(new WindowsGameSessionDriver(() => runtime.Player, () => PluginGameExecutable,
            () => PluginRelogReady, ReportRelogProgress, EnsureNativeFlightProtectionAsync), () => RunActive||NavigationActive||SystemFlightActive||NavigationRouteBusy);
        sessions.Progress += p => dispatcher.TryEnqueue(() => { if (p.Message.Length > 0) { SessionMessage = p.Message; runtime.Log("会话", p.Message); } });
        JournalDirectory = runtime.Settings.JournalDirectory; EddnEndpoint = runtime.Settings.EddnEndpoint;
        EddnOnStartup = runtime.Settings.ConnectEddnOnStart;
        HotkeysEnabled = runtime.Settings.Hotkeys.Enabled; RelogHotkey = runtime.Settings.Hotkeys.Relog; RestartHotkey = runtime.Settings.Hotkeys.Restart;
        InitializePluginSettings();
        InitializeTranslationSettings();
        InitializeProfile();
        InitializeNavigation();
        InitializeMarketCacheSettings();
        NavigationRouteOptions=runtime.Settings.NavigationRouteOptions;
        var r = runtime.Settings.Planner;
        Reserve = (double)r.ReserveCredits; Hold = r.CargoCapacity; JumpRange = r.LadenJumpRangeLy;
        MaxDistance = r.MaxLegDistanceLy; Radius = r.SearchRadiusLy; MaxLegs = r.MaxLegs; MaxAgeHours = r.MaxMarketAgeMinutes / 60.0;
        MaxArrival = r.MaxArrivalDistanceLs; Loop = r.Loop; RiskIndex = (int)r.Risk; ObjectiveIndex = (int)r.Objective;
        PadIndex = Math.Max(0, (int)r.RequiredPad - 1); IncludePlanetary = r.IncludePlanetary; IncludeCarriers = r.IncludeCarriers; AllowUnknown = r.AllowUnknownMetadata;
        JumpMinutes = r.MinutesPerJump; DockMinutes = r.DockingMinutes; CostPerJump = (double)r.CostPerJump; CostPerMinute = (double)r.CostPerMinute;
        InitializeTradePlanningOptions(r);
        InitializeTradeRegionScope();
        runtime.DataChanged += OnDataChanged;
        runtime.RunChanged += OnRunChanged;
        runtime.LiveJournalEvent+=OnShieldJournal;
        timer = dispatcher.CreateTimer(); timer.Interval = TimeSpan.FromMilliseconds(200); timer.IsRepeating=false;
        timer.Tick += async (_, _) =>
        {
            if(!initialized || lifetime.IsCancellationRequested)return;
            if(refreshing){timer.Start();return;}
            var changes=(RuntimeChange)Interlocked.Exchange(ref dirty,0);
            if(changes!=RuntimeChange.None)await RefreshDataAsync(changes);
            if(Volatile.Read(ref dirty)!=0)timer.Start();
        };
    }

    private void OnRuntimeChanged() => OnDataChanged(RuntimeChange.All);
    private void OnDataChanged(RuntimeChange changes)
    {
        if(lifetime.IsCancellationRequested)return;
        Interlocked.Or(ref dirty,(int)changes);
        dispatcher.TryEnqueue(()=>{if(initialized&&!timer.IsRunning&&!lifetime.IsCancellationRequested)timer.Start();});
    }
    private static IReadOnlyList<T> StableSequence<T>(IReadOnlyList<T> current,IEnumerable<T> next)
    { var values=next.ToArray();return current.SequenceEqual(values)?current:values; }
    private void OnRunChanged(RunProgress progress) => dispatcher.TryEnqueue(() =>
    {
        if (automaticTradeStop is not null) return;
        lastRun = progress;
        RunActive = progress.State is RunState.Running or RunState.Paused;
        RunStatus = progress.State switch { RunState.Running => "执行中", RunState.Paused => "已暂停", RunState.Completed => "已完成", RunState.Stopped => "已停止", RunState.Failed => "需要处理", _ => "等待路线" };
        RunMessage = progress.Message;
        PauseRunCommand.NotifyCanExecuteChanged();ResumeRunCommand.NotifyCanExecuteChanged();
        ActualProfit = $"{progress.ActualProfit:N0} Cr";
        TradeConfirmedProfit=progress.IsSimulation?"0 Cr":ActualProfit;
        if(!progress.IsSimulation&&progress.State==RunState.Completed&&!plannedRoundCounted){CompletedTradeRounds++;plannedRoundCounted=true;}
        ProfitLabel = progress.IsSimulation ? "模拟预计净收益" : "已确认净利润";
        var costs = progress.Accounting;
        ProfitDetails = progress.IsSimulation ? "规划估计值,未发生实际交易。"
            : $"燃油 {costs.Fuel:N0} · 维修 {costs.Repairs:N0} · 补给 {costs.Rearm:N0} · 其他支出 {costs.OtherExpenses:N0} Cr\n在途货物成本 {costs.CargoInvestment:N0} Cr · 现金变动 {costs.CashChange:+#,0;-#,0;0} Cr";
        RunPercent = progress.State == RunState.Completed ? 100 : progress.TotalLegs == 0 ? 0 : (progress.Leg - 1d) / progress.TotalLegs * 100;
        NextDestination = Plan?.Legs.ElementAtOrDefault(Math.Max(0, progress.Leg - 1))?.To.DisplayName ?? "—";
        RefreshTradePlanCommands(); CalculateCommand.NotifyCanExecuteChanged();
    });

    public async Task InitializeAsync()
    {
        initialized = true;
        Interlocked.Exchange(ref dirty,0);
        await RefreshAsync();
        await LoadGalaxyCacheAsync();
        // If the saved destination is already reached, present the route the
        // player has subsequently plotted in game. This never replots the game.
        if(CurrentPlayer.SystemName.Equals(NavigationDestination.Trim(),StringComparison.OrdinalIgnoreCase)
            &&CurrentPlayer.Navigation.Route.LastOrDefault() is {} plotted&&plotted.SystemAddress!=CurrentPlayer.Navigation.CurrentSystemAddress)
            NavigationDestination=plotted.Name;
        RefreshNavigation(CurrentPlayer);
        await RefreshUpgradeListAsync();
        UsePlayerValues();
        if(!OfflinePreview)
        {
            await ReadEnvironmentAsync();
            await ReadPluginStatusAsync();
            await ReadTranslationAsync();
            await RefreshWalletAsync();
            await InitializeAutomaticTradeAsync();
        }
        if(runtime.Settings.ShieldZeroRestartEnabled&&sessionAutomationAllowed)ShieldZeroRestartEnabled=true;
        StartGameCommand.NotifyCanExecuteChanged(); RelogGameCommand.NotifyCanExecuteChanged(); RestartGameCommand.NotifyCanExecuteChanged();
    }

    partial void OnDemoModeChanged(bool value)
    {
        if (revertingDemo) return;
        if (RunActive || SessionBusy) { ShowError("请先停止执行或会话操作,再切换数据模式。"); revertingDemo = true; DemoMode = !value; revertingDemo = false; return; }
        SetTradeCandidates([]); SelectedStart = null; marketPage = 0;
        UsePlayerValues(); OnRuntimeChanged();
        StartGameCommand.NotifyCanExecuteChanged(); RelogGameCommand.NotifyCanExecuteChanged(); RestartGameCommand.NotifyCanExecuteChanged();
    }
    partial void OnMarketSearchChanged(string value) { CancelMarketRead();marketPage = 0; Interlocked.Increment(ref marketRequest); OnDataChanged(RuntimeChange.Markets); }
    partial void OnOnlyCurrentStationChanged(bool value) { CancelMarketRead();marketPage = 0; Interlocked.Increment(ref marketRequest); OnDataChanged(RuntimeChange.Markets); }
    partial void OnMarketSortIndexChanged(int value) { CancelMarketRead();marketPage = 0; Interlocked.Increment(ref marketRequest); OnDataChanged(RuntimeChange.Markets); }
    partial void OnInventorySearchChanged(string value) => OnDataChanged(RuntimeChange.Player);
    partial void OnInventoryTabChanged(int value) => OnDataChanged(RuntimeChange.Player);
    partial void OnSelectedShipChanged(ShipLiveRow? value)
    {
        FittedModules.Apply(value?.Modules ?? []);
        SelectedShipSummary = value == null ? "没有飞船装配记录。" : $"{value.DisplayName} · {value.Ident}\n{value.Location} · {value.ConfigurationStatus}\n货舱 {value.CargoCapacity?.ToString() ?? "—"} t · 日志最大跳距 {value.MaxJumpRange?.ToString("F2") ?? "—"} ly · 重购 {value.Rebuy?.ToString("N0") ?? "—"} Cr\n最大跳距不是满载跳距,规划时仍需按载重校准。";
    }
    partial void OnPlanChanged(RoutePlan? value)
    {
        NotifyTradePlanningSummary();
        RefreshTradePlanCommands();
        if (RunActive) return;
        RunStatus = value?.Legs.Count > 0 ? "待执行" : "等待路线";
        RunMessage = value?.Legs.Count > 0 ? TradeHasExistingCargo?"方案已选定;请先处理现有货物,再开始自动执行。":"已选择路线,点击开始执行后启动。" : "输入投入资金并规划路线,再选择方案开始执行。";
        NextDestination = value?.Legs.FirstOrDefault()?.To.DisplayName ?? "—";
    }
    partial void OnExecutionModeIndexChanged(int value) => ExecutionHint = value switch
    {
        0 => "模拟演练只展示步骤,不向游戏发送输入。",
        1 => "你在游戏中操作;工具按 Journal 自动确认买入、到站、卖出并推进路线。",
        _ => "自动执行由工具箱完成采购、航行、出售及站内维护,并核对实际回执。"
    };

    public void ShowError(string message) => dispatcher.TryEnqueue(() => { ErrorMessage = message; HasError = true; });

    private const string MarketCacheFailureMessage="本地商品缓存恢复失败,行情暂不可用。飞船状态与导航继续更新,原始缓存和诊断记录已保留。";
    [RelayCommand] private Task RefreshAsync() => RefreshDataAsync(RuntimeChange.All);
    private async Task RefreshDataAsync(RuntimeChange changes)
    {
        if(!initialized)return;
        if(refreshing){OnDataChanged(changes);return;}
        refreshing=true;
        try
        {
            var demo=DemoMode;var p=CurrentPlayer;
            if(OnlyCurrentStation&&lastMarketFilterId!=p.MarketId)changes|=RuntimeChange.Markets;
            if((changes&RuntimeChange.Markets)!=0)
            {
                try
                {
                var search=MarketSearch;var current=OnlyCurrentStation;var page=marketPage;var sort=(MarketSort)MarketSortIndex;var request=Volatile.Read(ref marketRequest);
                var searchToken=BeginMarketRead();
                var data=await Task.Run(()=>
                {
                    if(demo)
                    {
                        var book=DemoData.CreateBook();
                        var rows=book.Quotes.Select(q=>new MarketRow(book.Stations.First(s=>s.MarketId==q.MarketId),q)).Where(x=>(!current||x.Station.MarketId==p.MarketId)&&(x.Name.Contains(search,StringComparison.OrdinalIgnoreCase)||x.Location.Contains(search,StringComparison.OrdinalIgnoreCase))).Skip(page*150).Take(150).ToArray();
                        return(Rows:(IReadOnlyList<MarketRow>)rows,Stations:book.Stations,Count:(Stations:(long)book.Stations.Count,Quotes:(long)book.Quotes.Count,Latest:(DateTimeOffset?)book.Quotes.Max(q=>q.CollectedAt)));
                    }
                    var rowsLive=current&&p.MarketId is null?[]:runtime.Markets.Search(search,current?p.MarketId:null,page*150,sort:sort,token:searchToken);
                    searchToken.ThrowIfCancellationRequested();
                    var stations=runtime.Markets.GetStations();
                    if(p.MarketId is {} id&&!stations.Any(s=>s.MarketId==id)&&runtime.Markets.GetStation(id) is {} origin)stations=new[]{origin}.Concat(stations).ToArray();
                    return(Rows:rowsLive,Stations:stations,Count:runtime.Markets.Statistics());
                },lifetime.Token);
                if(demo!=DemoMode){OnRuntimeChanged();return;}
                if(request!=Volatile.Read(ref marketRequest)){OnDataChanged(changes);return;}
                lastMarketFilterId=p.MarketId;
                MarketRows.Apply(data.Rows,MarketSortIndex!=0);
                MarketPageText=$"第 {marketPage+1} 页 · 本页 {MarketRows.Count} 条";
                var oldStart=SelectedStart?.MarketId;var oldDestination=AutomaticDestination?.MarketId;
                var selectedStations=new[]{SelectedStart?.Record,AutomaticDestination?.Record,automaticTradeParameters?.Source,automaticTradeParameters?.Destination}.OfType<Station>();
                Stations.Apply(data.Stations.Concat(selectedStations));
                SelectedStart=Stations.FirstOrDefault(s=>s.MarketId==(oldStart??p.MarketId));
                AutomaticDestination=Stations.FirstOrDefault(s=>s.MarketId==oldDestination);
                NotifyTradePlanningSummary();
                CoverageText=$"{data.Count.Stations:N0} 个已知站点";QuoteCountText=data.Count.Quotes.ToString("N0");
                LatestText=data.Count.Latest is {} at?at.ToLocalTime().ToString("MM-dd HH:mm:ss"):"尚无行情";
                RefreshMarketPresentation();
                if(ErrorMessage==MarketCacheFailureMessage){ErrorMessage="";HasError=false;}
                }
                catch(OperationCanceledException)when(!lifetime.IsCancellationRequested)
                {OnDataChanged(RuntimeChange.Markets);}
                catch(TimeoutException)
                {MarketQueryStatus="查询超时,仍显示上次有效结果。可限定当前站点或输入完整商品名后重试。";}
                catch(Microsoft.Data.Sqlite.SqliteException error)when(error.SqliteErrorCode is 11 or 26)
                {
                    runtime.Log("商品缓存",error.ToString());
                    try
                    {
                        await Task.Run(()=>runtime.Markets.RecoverAsync(lifetime.Token),lifetime.Token);
                        MarketPageText="商品缓存已恢复,正在重新读取";
                        OnDataChanged(RuntimeChange.Markets);
                    }
                    catch(Exception recoveryError)when(recoveryError is not OperationCanceledException)
                    {
                        MarketPageText="商品缓存恢复失败";
                        runtime.Log("商品缓存恢复",recoveryError.ToString());
                        ShowError(MarketCacheFailureMessage);
                    }
                }
            }
            if((changes&RuntimeChange.Player)!=0)
            {
                RefreshTradePlanningPlayer(p);
                OnPropertyChanged(nameof(AutomaticShipSummary));
                RefreshRelogAvailability();
                Commander=p.Commander;RefreshProfileName();Ship=string.IsNullOrEmpty(p.Ship)?"舰船数据未知":Core.Localization.ChineseNames.Ship(p.Ship);
                RefreshProfileData(p);
                CreditsText=p.Credits?.ToString("N0")??"—";CargoText=$"{p.CargoCount:N0} / {p.CargoCapacity:N0} t";
                MaterialTotalText=p.Materials.Sum(m=>m.Count).ToString("N0");MaterialKindsText=$"{p.Materials.Count} 种已持有材料";
                PlayerSnapshotText=$"本机状态 · {p.UpdatedAt.ToLocalTime():MM-dd HH:mm:ss}";
                BalanceNote=p.CreditsReliable?"Journal 余额 · 交易后自动更新":"余额可能不完整 · 重新登录游戏可校准";
                SystemName=string.IsNullOrWhiteSpace(p.SystemName)?"等待位置数据":p.SystemName;
                StationName=string.IsNullOrEmpty(p.StationName)?"深空 / 航行中":p.StationName;
                DockingText=demo?"内部界面测试":!p.InGame?"游戏会话已结束":p.Docked?"已停靠":"航行中";
                CargoNote=$"可用货舱 {p.FreeCargo:N0} t · 任务货物保留";
                var inventory=InventoryTab switch{1=>p.Materials,2=>p.Locker,3=>p.Backpack,_=>p.Inventory};
                InventoryRows.Apply(inventory.Where(i=>i.Name.Contains(InventorySearch,StringComparison.OrdinalIgnoreCase)||i.Key.Contains(InventorySearch,StringComparison.OrdinalIgnoreCase)).OrderBy(i=>i.Name));
                RefreshInventoryPresentation();
                var selectedId=SelectedShip?.ShipId??p.CurrentShipId;
                Ships.Apply(p.Ships);StoredModules.Apply(p.StoredModules);
                SelectedShip=Ships.FirstOrDefault(x=>x.ShipId==selectedId)??Ships.FirstOrDefault();
                OnSelectedShipChanged(SelectedShip);
                FleetSummary=$"{Ships.Count} 艘已知飞船 · {Ships.Count(s=>s.ConfigurationAt!=null)} 艘有历史装配 · {StoredModules.Count} 个寄存模块";
                var activeShip=p.Ships.FirstOrDefault(x=>x.ShipId==p.CurrentShipId);
                CurrentShipSummary=activeShip is null?"当前飞船装配未读取":$"当前:{activeShip.DisplayName} · #{activeShip.ShipId}\n{activeShip.ConfigurationStatus}";
                CockpitSummary=p.Status is {} cockpit?cockpit.FuelText+"\n"+cockpit.PipsText+$"\nFlags {cockpit.Flags} · Flags2 {cockpit.Flags2}":"尚未读取座舱状态";
                RefreshEngineering(p);
                await RefreshUpgradeListAsync();
                RefreshNavigation(p);
                if(ImportSystemName.Length==0&&p.SystemName.Length>0)ImportSystemName=p.SystemName;
            }
            if((changes&(RuntimeChange.Player|RuntimeChange.Markets))!=0)
            {
                ReadChecks.Apply(ReadAcceptance.Build(p,demo?null:runtime.Journal.LocalMarket));
                ReadSummary=$"已解析 {runtime.Journal.EventsRead:N0} 条 Journal 事件 · 检测到变化时更新\n{runtime.Journal.DirectoryPath}\n每一项时间均为游戏最后写出时间,不代表全部数据当前有效。";
            }
            if((changes&(RuntimeChange.Connection|RuntimeChange.Markets))!=0)
            {
                EddnCountText=(runtime.Eddn?.MessagesReceived??0).ToString("N0");StatusText=runtime.Eddn?.Status??"EDDN 未连接 · 本地数据可用";
                SourceNote="本地 Market + EDDN 增量行情 + Spansh 快照";
            }
            if((changes&RuntimeChange.Activity)!=0)Activity.Apply(runtime.Activity);
            if((changes&RuntimeChange.GameEvents)!=0&&GameLogPageVisible&&gameLogPage==0)await RefreshGameEventsAsync();
            if((changes&(RuntimeChange.Player|RuntimeChange.Markets|RuntimeChange.Connection))!=0)RefreshDashboard(p);
            if((changes&(RuntimeChange.Player|RuntimeChange.Markets))!=0)await RefreshGalaxyAsync(p,(changes&RuntimeChange.Markets)!=0);
        }
        catch(OperationCanceledException)when(lifetime.IsCancellationRequested){}
        catch(Exception e){runtime.Log("界面数据刷新",e.ToString());ShowError(e.Message);}
        finally{refreshing=false;}
    }

    [RelayCommand]
    private void UsePlayerValues()
    {
        RefreshTradePlanningPlayer(CurrentPlayer,force:true);
    }

    partial void OnSessionBusyChanged(bool value)
    {
        StartGameCommand.NotifyCanExecuteChanged(); RelogGameCommand.NotifyCanExecuteChanged(); RestartGameCommand.NotifyCanExecuteChanged();
        RefreshTradePlanCommands(); CalculateCommand.NotifyCanExecuteChanged();
        StartNavigationCommand.NotifyCanExecuteChanged();
        StartRemoteJourneyCommand.NotifyCanExecuteChanged();
        UpdateSystemControlAvailability();
    }
    partial void OnRunActiveChanged(bool value) { RefreshTradeExecutionTimer(value);RefreshTradePlanCommands();NotifyTradePlanningSummary(); PauseRunCommand.NotifyCanExecuteChanged();ResumeRunCommand.NotifyCanExecuteChanged();StartGameCommand.NotifyCanExecuteChanged(); RelogGameCommand.NotifyCanExecuteChanged(); RestartGameCommand.NotifyCanExecuteChanged();StartNavigationCommand.NotifyCanExecuteChanged();UpdateSystemControlAvailability(); }
    private bool CanManageSession() => initialized && !DemoMode && !RunActive && !SessionBusy&&!NavigationActive&&!SystemFlightActive&&!NavigationRouteBusy;
    private bool CanRelog() => CanManageSession() && sessions.CanRelog;
    [RelayCommand(CanExecute = nameof(CanManageSession))] private Task StartGameAsync() => RunSessionAsync(SessionOperation.Start);
    [RelayCommand(CanExecute = nameof(CanRelog))] private Task RelogGameAsync() => RunSessionAsync(SessionOperation.Relog);
    [RelayCommand(CanExecute = nameof(CanManageSession))] private Task RestartGameAsync() => RunSessionAsync(SessionOperation.Restart);
    [RelayCommand] private void CancelSession() => sessionStop?.Cancel();
    public void InitializeHotkeys(bool activate)
    {
        sessionAutomationAllowed=activate;
        hotkeys = new(WinRT.Interop.WindowNative.GetWindowHandle(App.MainWindow), operation => dispatcher.TryEnqueue(() =>
        {
            if (!CanManageSession()) { SessionMessage = "当前状态不能执行会话操作。"; return; }
            if (operation == SessionOperation.Relog && !sessions.CanRelog) { SessionMessage = RelogAvailabilityText; return; }
            _ = RunSessionAsync(operation);
        }));
        if (!activate) { HotkeyStatus = "隐藏界面检查:未注册任何用户快捷键。"; return; }
        try { hotkeys.Apply(runtime.Settings.Hotkeys); UpdateHotkeyStatus(); }
        catch (Exception e) { HotkeyStatus = e.Message; ShowError(e.Message); }
    }
    private void UpdateHotkeyStatus()
    {
        var s = hotkeys?.Settings ?? new();
        HotkeyStatus = !s.Enabled ? "全局快捷键已关闭。" : $"当前生效:重进 {(s.Relog.Length > 0 ? s.Relog : "未绑定")} · 大退 {(s.Restart.Length > 0 ? s.Restart : "未绑定")}。";
    }
    [RelayCommand] private async Task SaveHotkeysAsync()
    {
        if (hotkeys is null) return;
        var previous = hotkeys.Settings;
        try
        {
            hotkeys.Apply(new(HotkeysEnabled, RelogHotkey, RestartHotkey));
            await runtime.SaveSettingsAsync(runtime.Settings with { Hotkeys = hotkeys.Settings });
            RelogHotkey = hotkeys.Settings.Relog; RestartHotkey = hotkeys.Settings.Restart; UpdateHotkeyStatus();
        }
        catch (Exception e)
        {
            try { hotkeys.Apply(previous); } catch (Exception restore) { HotkeyStatus = "快捷键恢复失败:" + restore.Message; }
            HotkeyStatus = "未应用本次修改:" + e.Message; ShowError(e.Message);
        }
    }
    private async Task RunSessionAsync(SessionOperation operation)
    {
        if (!CanManageSession()) return;
        ActiveSessionOperation=operation;SessionBusy = true; HasError = false;
        using var operationStop = CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token);
        sessionStop = operationStop;
        try { await sessions.RunAsync(operation, operationStop.Token); }
        catch (OperationCanceledException) { SessionMessage = operation == SessionOperation.Relog ? "已取消后续重进操作,已提交给游戏的菜单操作仍会完成。" : "会话操作已取消。"; }
        catch (Exception e) { ShowError(e.Message); }
        finally { sessionStop = null; SessionBusy = false; ActiveSessionOperation=null; }
    }
    private bool CanCalculate() => !RunActive && !SessionBusy && !IsBusy&&!TradeRegionBusy&&TradeShipPadKnown;
    private bool CanImportData() => !IsBusy;
    partial void OnIsBusyChanged(bool value)
    {
        OnPropertyChanged(nameof(TradeSelectionEnabled));
        RefreshTradePlanCommands();
        CalculateCommand.NotifyCanExecuteChanged();ImportSystemCommand.NotifyCanExecuteChanged();
        ImportSpanshCommand.NotifyCanExecuteChanged();ImportCaptureCommand.NotifyCanExecuteChanged();
    }
    [RelayCommand(IncludeCancelCommand = true, CanExecute = nameof(CanCalculate))]
    private async Task CalculateAsync(CancellationToken token)
    {
        IsBusy = true; HasError = false;
        var progress=BeginTradePlanningProgress(token);
        try
        {
            RefreshTradePlanningPlayer(CurrentPlayer);
            if(!double.IsFinite(Radius))Radius=runtime.Settings.MarketCacheRadiusLy;
            await PrepareTradeRegionForPlanningAsync(token);
            var request = BuildRequest();
            var inputVersion=Volatile.Read(ref tradePlanningInputVersion);
            var demo = DemoMode;
            SetTradeCandidates([]);TradeEmptyTitle="正在比较路线";TradeEmptyDescription="正在核对范围内的行情、供需与飞船约束。";
            TradePlanningStatus="正在比较可行路线与预计小时收益…";
            IReadOnlyList<CommunityGoalInfo> goals=[];
            if(request.CommunityGoalSupplyOnly)
            {
                ApplyTradePlanningProgress(new("核对 CG 供货条件",0,null,"只读确认有效活动、交付站与接收商品;未确认时不推荐普通路线。"));
                goals=await ReadTradeCommunityGoalsForPlanningAsync(demo,token);
            }
            var book=demo?DemoData.CreateBook():await Task.Run(()=>runtime.Markets.LoadPlanningRegion(request,token:token,progress:progress),token);
            var result = await Task.Run(() => new TradeRoutePlanner().CalculateCandidatesWithDiagnostics(book, request,3,token,progress,goals), token);
            Interlocked.Increment(ref tradePlanningOperation);
            if(!request.CommunityGoalSupplyOnly&&result.Candidates.Count>0)
            {
                ApplyTradePlanningProgress(new("核对CG活动目录",0,null,"最多等待三秒;活动未知不影响常规市场利润的比较。"));
                goals=await ReadTradeCommunityGoalsForPlanningAsync(demo,token);
            }
            else if(request.CommunityGoalSupplyOnly&&result.Candidates.Count>0
                &&(tradeCommunityGoalVerifiedAt is not {} readAt||DateTimeOffset.UtcNow-readAt>TimeSpan.FromMinutes(2)))
                goals=await ReadTradeCommunityGoalsForPlanningAsync(demo,token);
            token.ThrowIfCancellationRequested();
            if (demo != DemoMode) return;
            if(inputVersion!=Volatile.Read(ref tradePlanningInputVersion))
            {SetTradePlanningIssue("资料已更新,请重新规划","飞船、货舱、位置或规划参数已在计算期间变化,请按当前数据重新规划。");return;}
            SetTradeCandidates(result.Candidates,result.Diagnostics,goals);
            runtime.Log("规划",JsonSerializer.Serialize(result.Diagnostics,ToolboxSettings.JsonOptions));
            if (!demo) await runtime.SaveSettingsAsync(runtime.Settings with { Planner = request });
        }
        catch (OperationCanceledException) { SetTradePlanningIssue("规划已取消","本次规划已取消,点击规划路线可重新比较。"); }
        catch (Exception e) { SetTradePlanningIssue("暂时无法生成方案",e.Message);ShowError(e.Message); }
        finally { EndTradePlanningProgress();IsBusy = false; }
    }

    private PlannerRequest BuildRequest()
    {
        var player=CurrentPlayer;
        var defaults=runtime.Settings.Planner;
        static double Optional(double value,double fallback)=>double.IsFinite(value)?value:fallback;
        var cash=Optional(Cash,(double)(player.Credits??0));var reserve=Optional(Reserve,(double)player.Rebuy);
        var investment=Optional(TradeInvestment,Math.Max(0,cash-reserve));
        if(investment<=0||investment>Math.Max(0,cash-reserve))throw new ArgumentException("投入资金须大于零,且不能超过留出运营与保险资金后的可用余额。");
        var shipPad=ShipPadPolicy.RequiredPad(player.Ship);
        if(shipPad==LandingPad.Unknown)throw new ArgumentException("当前飞船尺寸尚未识别,请等待真实飞船数据后再规划。");
        var request = new PlannerRequest
        {
            StartMarketId = player.MarketId??0, Credits = (decimal)cash, ReserveCredits = Math.Max((decimal)reserve,(decimal)cash-(decimal)investment),
            InvestmentCredits=investmentFollowsBalance?null:(decimal)investment,
            CargoCapacity = checked((int)Optional(Hold,player.CargoCapacity)),OccupiedCargo=player.CargoCount,LadenJumpRangeLy = Optional(JumpRange,player.MaxJumpRange??defaults.LadenJumpRangeLy),JumpRangeIsEstimate=true,
            CurrentPosition=player.Position,CurrentSystemName=player.SystemName,CurrentMarketId=player.MarketId,MaxLegDistanceLy = Optional(MaxDistance,defaults.MaxLegDistanceLy),
            SearchRadiusLy = Optional(Radius,runtime.Settings.MarketCacheRadiusLy),SearchCenterPosition=EffectiveTradeRegionCenter,SearchCenterName=TradeRegionCenterText.Trim().Length==0?player.SystemName:resolvedTradeRegionName,
            MaxLegs = checked((int)Optional(MaxLegs,defaults.MaxLegs)), Loop = Loop,CommunityGoalSupplyOnly=TradePlanningModeIndex==1,
            CommunityGoalCommodityFilter=TradePlanningModeIndex==1?TradeSearchConstraints.ParseCommodityFilter(TradeCommoditySearchText):[],
            SourceMarketIdFilter=tradeSourceSearchMarketId,DestinationMarketIdFilter=tradeDestinationSearchMarketId,
            SourceSystemFilter=TradeSourceSystemText?.Trim()??"",SourceStationFilter=TradeSourceSearchStation?.Record.Name??TradeSourceSearchText?.Trim()??"",
            DestinationSystemFilter=TradeDestinationSystemText?.Trim()??"",DestinationStationFilter=TradeDestinationSearchStation?.Record.Name??TradeDestinationSearchText?.Trim()??"",
            Risk = RiskIndex is >=0 and <=2?(RiskTolerance)RiskIndex:defaults.Risk, Objective = ObjectiveIndex is >=0 and <=2?(RouteObjective)ObjectiveIndex:defaults.Objective,
            ActualShipType=player.Ship,ShipRequiredPad=shipPad,AllowedSourceKinds=SelectedTradeSourceKinds==0?StationSourceKinds.AllKnown:SelectedTradeSourceKinds,
            RequiredPad = (LandingPad)Math.Max((int)shipPad,PadIndex is >=0 and <=2?PadIndex+1:(int)shipPad), IncludePlanetary = true,
            IncludeCarriers = true, AllowUnknownMetadata = AllowUnknown,
            MaxMarketAgeMinutes = checked((int)(Optional(MaxAgeHours,defaults.MaxMarketAgeMinutes/60d) * 60)), MaxArrivalDistanceLs = Optional(MaxArrival,defaults.MaxArrivalDistanceLs),
            MinutesPerJump = Optional(JumpMinutes,defaults.MinutesPerJump), DockingMinutes = Optional(DockMinutes,defaults.DockingMinutes), CostPerJump = (decimal)Optional(CostPerJump,(double)defaults.CostPerJump), CostPerMinute = (decimal)Optional(CostPerMinute,(double)defaults.CostPerMinute)
        };
        request.Validate(); return request;
    }

    private bool CanStartPlan() => Plan?.Legs.Count > 0 && !RunActive && !SessionBusy&&!NavigationActive;
    [RelayCommand(CanExecute = nameof(CanStartPlan))]
    private async Task StartPlanAsync(CancellationToken cancellationToken=default)
    {
        using var stop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token,cancellationToken);var token=stop.Token;
        try
        {
            if (Plan == null) return;
            if (DemoMode && ExecutionModeIndex != 0) throw new InvalidOperationException("演示沙盘仅支持模拟演练。");
            if (ExecutionModeIndex is < 0 or > 2) throw new InvalidOperationException("请选择有效的执行方式。");
            if(ExecutionModeIndex==2&&(!CurrentPlayer.Docked||CurrentPlayer.MarketId!=Plan.Legs[0].From.MarketId))
                throw new InvalidOperationException("请先停靠 "+Plan.Legs[0].From.DisplayName+" 再执行此完整路线;前往起点的耗时目前只计入估算。");
            BeginPlannedTradeMetrics();
            var nativeDriver=ExecutionModeIndex==2?await CreateNativeTradeDriverAsync(Plan,token):null;
            token.ThrowIfCancellationRequested();
            AppShellPage.Current?.Navigate(typeof(PlannerPage));
            RunActive = true;
            await runtime.Runner.RunAsync(Plan, (ExecutionMode)ExecutionModeIndex, nativeDriver, token);
        }
        catch(OperationCanceledException){runtime.Log("执行","执行已取消。");}
        catch (Exception e) { ShowError(e.Message); }
        finally { RunActive = false; RefreshTradePlanCommands(); CalculateCommand.NotifyCanExecuteChanged(); }
    }
    private bool CanPauseRun()=>automaticTradeStop is null&&runtime.Runner.State==RunState.Running;
    private bool CanResumeRun()=>automaticTradeStop is null&&runtime.Runner.State==RunState.Paused;
    [RelayCommand(CanExecute=nameof(CanPauseRun))] private void PauseRun() => runtime.Runner.Pause();
    [RelayCommand(CanExecute=nameof(CanResumeRun))] private void ResumeRun() { try { runtime.Runner.Resume(); } catch (Exception e) { ShowError(e.Message); } }
    [RelayCommand] private void StopRun()
    {
        if (automaticTradeStop is { } stop)
        {
            RunStatus = "正在取消";
            RunMessage = "正在停止跑商,核对已提交请求并释放控制。";
            stop.Cancel();
            return;
        }
        runtime.Runner.Stop();
    }
    [RelayCommand] private void PreviousMarketPage() { CancelMarketRead();marketPage = Math.Max(0, marketPage - 1); Interlocked.Increment(ref marketRequest); OnDataChanged(RuntimeChange.Markets); }
    [RelayCommand] private void NextMarketPage() { CancelMarketRead();if (MarketRows.Count == 150) marketPage++; Interlocked.Increment(ref marketRequest); OnDataChanged(RuntimeChange.Markets); }
    [RelayCommand] private void CopyDestination()
    {
        var leg = Plan?.Legs.ElementAtOrDefault(Math.Max(0, (lastRun?.Leg ?? 1) - 1));
        if (leg == null) return;
        var data = new DataPackage(); data.SetText(leg.To.SystemName); Clipboard.SetContent(data);
        runtime.Log("导航", $"已复制目的星系:{leg.To.SystemName}");
    }
    [RelayCommand] private async Task ExportPlanAsync()
    {
        try
        {
            if (Plan == null) throw new InvalidOperationException("请先计算路线。");
            var picker = new FileSavePicker { SuggestedFileName = $"ED-Trade-{DateTime.Now:yyyyMMdd-HHmm}" };
            picker.FileTypeChoices.Add("路线 JSON", new[] { ".json" }); InitializePicker(picker);
            if (await picker.PickSaveFileAsync() is { } file) await File.WriteAllTextAsync(file.Path, JsonSerializer.Serialize(Plan, ToolboxSettings.JsonOptions));
        }
        catch (Exception e) { ShowError(e.Message); }
    }
    [RelayCommand] private async Task ExportReadReportAsync()
    {
        try
        {
            if (DemoMode) throw new InvalidOperationException("请切换到实时接入后导出真实数据验收报告。");
            var p = runtime.Player;
            var cache = runtime.Markets.Statistics();
            var report = new { generatedAt = DateTimeOffset.UtcNow, mode = "external-read-only", journalDirectory = runtime.Journal.DirectoryPath,
                eventsRead = runtime.Journal.EventsRead, checks = ReadAcceptance.Build(p, runtime.Journal.LocalMarket),
                environment = gameEnvironment, player = p with { CommanderId = "" },
                localMarket = runtime.Journal.LocalMarket, publicMarketCache = new { cache.Stations, cache.Quotes, cache.Latest },
                limits = new[] { "未使用过的远程飞船可能只有清单而没有装配", "历史装配不等于实时配置", "公共行情不覆盖所有站点的即时价格", "本报告不证明零封禁风险" } };
            var folder = Path.Combine(runtime.DataDirectory, "reports"); Directory.CreateDirectory(folder);
            var path = Path.Combine(folder, $"read-acceptance-{DateTime.Now:yyyyMMdd-HHmmss}.json");
            await File.WriteAllTextAsync(path, JsonSerializer.Serialize(report, ToolboxSettings.JsonOptions));
            ReadReportPath = path; runtime.Log("验收", "已导出只读数据报告(不含账号认证信息)。");
        }
        catch (Exception e) { ShowError(e.Message); }
    }
    [RelayCommand] private async Task SaveSettingsAsync()
    {
        try
        {
            if (!Uri.TryCreate(EddnEndpoint, UriKind.Absolute, out var uri) || uri.Scheme != "tcp" || uri.Port <= 0) throw new ArgumentException("请输入有效的 EDDN TCP 地址。");
            await runtime.SaveSettingsAsync(runtime.Settings with { JournalDirectory = JournalDirectory.Trim(), EddnEndpoint = EddnEndpoint.Trim(), ConnectEddnOnStart = EddnOnStartup });
            OnRuntimeChanged();
        }
        catch (Exception e) { ShowError(e.Message); }
    }
    [RelayCommand] private async Task ToggleEddnAsync()
    {
        try { if (runtime.Eddn == null) runtime.StartEddn(); else await runtime.StopEddnAsync(); }
        catch (Exception e) { ShowError(e.Message); }
    }
    [RelayCommand(IncludeCancelCommand = true,CanExecute=nameof(CanImportData))] private async Task ImportSystemAsync(CancellationToken token)
    {
        IsBusy = true;
        try
        {
            var progress = new Progress<ImportProgress>(p => ImportStatus = $"{p.Message} · {p.Systems} 星系 / {p.Stations} 站点 / {p.Quotes:N0} 报价");
            var result = ImportNearby ? await runtime.Spansh.ImportNearbyAsync(ImportSystemName.Trim(), 20, progress, token) : await runtime.Spansh.ImportSystemAsync(ImportSystemName.Trim(), progress, token);
            ImportStatus = $"已处理 {result.Systems} 个星系、{result.Stations} 个站点;更新 {result.Quotes:N0} 条报价。保留源数据时间。";
            runtime.Log("导入", ImportStatus); OnRuntimeChanged();
        }
        catch (OperationCanceledException) { ImportStatus = "导入已取消,已完成的数据保留。"; }
        catch (Exception e) { ImportStatus = "导入未完成"; ShowError(e.Message); }
        finally { IsBusy = false; }
    }
    [RelayCommand(CanExecute=nameof(CanImportData))] private Task ImportSpanshAsync(CancellationToken token)=>ImportFileAsync("spansh",token);
    [RelayCommand(CanExecute=nameof(CanImportData))] private Task ImportCaptureAsync(CancellationToken token)=>ImportFileAsync("capture",token);
    private async Task ImportFileAsync(string kind, CancellationToken token)
    {
        IsBusy = true;
        try
        {
            var picker = new FileOpenPicker();
            foreach (var ext in kind == "capture" ? new[] { ".zip" } : new[] { ".json", ".gz" }) picker.FileTypeFilter.Add(ext);
            InitializePicker(picker);
            if (await picker.PickSingleFileAsync().AsTask(token) is not { } file) return;
            token.ThrowIfCancellationRequested();
            var progress = new Progress<ImportProgress>(p => ImportStatus = $"{p.Message} · {p.Stations} 站点 / {p.Quotes:N0} 报价");
            var result = kind == "capture" ? await Task.Run(() => CaptureImporter.ImportAsync(file.Path, runtime.Markets, progress, token), token) : await Task.Run(() => runtime.Spansh.ImportFileAsync(file.Path, progress: progress, token: token), token);
            ImportStatus = $"完成:{result.Stations} 个站点,{result.Quotes:N0} 条报价。"; runtime.Log("导入", ImportStatus); OnRuntimeChanged();
        }
        catch (OperationCanceledException) { ImportStatus = "导入已取消,已完成的数据保留。"; }
        catch (Exception e) { ShowError(e.Message); }
        finally { IsBusy = false; }
    }
    [RelayCommand] private async Task ChooseJournalAsync()
    {
        var picker = new FolderPicker(); picker.FileTypeFilter.Add("*"); InitializePicker(picker);
        if (await picker.PickSingleFolderAsync() is { } folder) JournalDirectory = folder.Path;
    }
    [RelayCommand] private async Task ReadEnvironmentAsync(CancellationToken cancellationToken=default)
    {
        using var stop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token,cancellationToken);var token=stop.Token;
        try
        {
            gameEnvironment = await GameEnvironmentReader.ReadAsync(runtime.Player, token: token);
            EnvironmentText = gameEnvironment.Summary + "\n" + string.Join("\n", gameEnvironment.Diagnostics);
        }
        catch(OperationCanceledException){EnvironmentText="环境读取已取消。";}
        catch (Exception e) { ShowError(e.Message); }
    }
    [RelayCommand] private void OpenDataDirectory() => Process.Start(new ProcessStartInfo(runtime.DataDirectory) { UseShellExecute = true });
    private static void InitializePicker(object picker) => WinRT.Interop.InitializeWithWindow.Initialize(picker, WinRT.Interop.WindowNative.GetWindowHandle(App.MainWindow));
    public void Dispose()
    {
        runtime.FlightAutomation.Progress-=OnFlightAutomationProgress;runtime.FlightAutomation.Stop();
        runtime.Navigator.Progress-=OnNavigationProgress;runtime.Navigator.Stop();
        systemNavigator.Progress-=OnSystemFlightProgress;systemNavigator.Stop();systemFlightStop?.Cancel();currentSystemTimer?.Stop();currentSystemSource?.Dispose();
        if(shipProtection is {} protection)_=protection.DisposeAsync();
        hotkeys?.Dispose(); sessionStop?.Cancel();
        runtime.LiveJournalEvent-=OnShieldJournal;
        walletTimer?.Stop();
        tradeExecutionTimer?.Stop();
        shipTelemetryTimer?.Stop();
        timer.Stop(); runtime.DataChanged -= OnDataChanged; runtime.RunChanged -= OnRunChanged;
        lifetime.Cancel(); lifetime.Dispose();
        galaxyStreaming.Dispose();galaxyCellStreaming.Dispose();if(nativeGalaxy is not null&&galaxyNativeProgressHandler is not null)nativeGalaxy.RegionProgress-=galaxyNativeProgressHandler;
        galaxyHttp.Dispose();
    }
}