MainWindow.xaml.cs 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771
  1. using System.Collections.Generic;
  2. using System.Globalization;
  3. using System.Windows;
  4. using System.Windows.Controls;
  5. using System.Windows.Media;
  6. using System.Windows.Media.Animation;
  7. using System.Windows.Threading;
  8. using QuickIP.Client.Models;
  9. using QuickIP.Client.Services;
  10. namespace QuickIP.Client;
  11. public partial class MainWindow : Window
  12. {
  13. private readonly NetworkAdapterService _networkAdapterService = new();
  14. private readonly PasswordStoreService _passwordStoreService = new();
  15. private readonly NetworkConfigurationService _networkConfigurationService = new();
  16. private readonly DiscoveryService _discoveryService = new();
  17. private readonly AgentApiService _agentApiService = new();
  18. private readonly AdminPrivilegeService _adminPrivilegeService = new();
  19. private IReadOnlyList<AdapterInfo> _adapters = [];
  20. private string _connectedBaseAddress = string.Empty;
  21. private string _connectedLocalIPv4 = string.Empty;
  22. private bool _configValidated;
  23. private bool _suppressConfigChangeHandling;
  24. private bool _isShowingPassword;
  25. private bool _isBusy;
  26. private bool _suppressPasswordSync;
  27. private CancellationTokenSource? _statusMessageCts;
  28. public MainWindow()
  29. {
  30. InitializeComponent();
  31. Loaded += MainWindow_OnLoaded;
  32. }
  33. private async void MainWindow_OnLoaded(object sender, RoutedEventArgs e)
  34. {
  35. await LoadInitialStateAsync();
  36. }
  37. private async Task LoadInitialStateAsync()
  38. {
  39. AdminStateTextBlock.Text = _adminPrivilegeService.IsAdministrator()
  40. ? "管理员状态:当前已以管理员身份运行,可执行本机网卡切换。"
  41. : "管理员状态:当前不是管理员运行,切换到维护网络会失败。";
  42. _adapters = _networkAdapterService.GetEthernetAdapters();
  43. await _networkAdapterService.ProbeMaintenanceReachabilityAsync(_adapters);
  44. _adapters = _adapters
  45. .OrderByDescending(adapter => adapter.RecommendationScore)
  46. .ThenBy(adapter => adapter.Name)
  47. .ToList();
  48. AdapterComboBox.ItemsSource = _adapters;
  49. var recommendedAdapter = _networkAdapterService.GetRecommendedAdapter(_adapters);
  50. var savedPassword = _passwordStoreService.LoadPassword();
  51. if (!string.IsNullOrWhiteSpace(savedPassword))
  52. {
  53. PasswordBox.Password = savedPassword;
  54. PasswordTextBox.Text = savedPassword;
  55. }
  56. if (recommendedAdapter is not null)
  57. {
  58. AdapterComboBox.SelectedItem = recommendedAdapter;
  59. UpdateAdapterDetails(recommendedAdapter);
  60. }
  61. else if (_adapters.Count > 0)
  62. {
  63. AdapterComboBox.SelectedIndex = 0;
  64. UpdateAdapterDetails(_adapters[0]);
  65. }
  66. AppendLog("客户端已加载连接页。", true);
  67. ClearRemoteDetails();
  68. UpdateButtonStates();
  69. }
  70. private void AdapterComboBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
  71. {
  72. if (AdapterComboBox.SelectedItem is not AdapterInfo adapter)
  73. {
  74. UpdateAdapterDetails(null);
  75. ClearRemoteDetails();
  76. SetStatus("请选择一块有线网卡。", false);
  77. UpdateButtonStates();
  78. return;
  79. }
  80. UpdateAdapterDetails(adapter);
  81. ClearRemoteDetails();
  82. SetStatus(adapter.HasLink
  83. ? $"已选择 {adapter.RecommendationLabel} 网卡,可切换到维护网络。{adapter.RecommendationReason}"
  84. : "当前网卡未检测到链路,请检查网线连接。", true);
  85. UpdateButtonStates();
  86. }
  87. private async void SwitchMaintenanceButton_OnClick(object sender, RoutedEventArgs e)
  88. {
  89. if (!_adminPrivilegeService.IsAdministrator())
  90. {
  91. SetStatus("当前程序未以管理员身份运行,无法修改本机网卡。", true);
  92. MessageBox.Show(this, "请以管理员身份运行客户端后再切换到维护网络。", "需要管理员权限", MessageBoxButton.OK, MessageBoxImage.Warning);
  93. return;
  94. }
  95. if (AdapterComboBox.SelectedItem is not AdapterInfo adapter)
  96. {
  97. SetStatus("请先选择一块网卡。", true);
  98. return;
  99. }
  100. PersistPasswordIfNeeded();
  101. if (adapter.IsReachableToMaintenance)
  102. {
  103. SetStatus("当前网卡已经可以直接访问 169.254.100.2,无需切换到维护网络。", true);
  104. return;
  105. }
  106. SetBusyState(true);
  107. SetStatus("正在切换到维护网络,请稍候。", true);
  108. await Dispatcher.InvokeAsync(() => { }, System.Windows.Threading.DispatcherPriority.Render);
  109. try
  110. {
  111. await _networkConfigurationService.ConfigureMaintenanceNetworkAsync(adapter);
  112. SetStatus("已切换到维护网络。", true);
  113. await RefreshAdaptersAsync(adapter.Id);
  114. }
  115. catch (Exception ex)
  116. {
  117. SetStatus($"切换维护网络失败:{ex.Message}", true);
  118. MessageBox.Show(this, ex.Message, "切换维护网络失败", MessageBoxButton.OK, MessageBoxImage.Error);
  119. }
  120. finally
  121. {
  122. SetBusyState(false);
  123. }
  124. }
  125. private async void RefreshAdaptersButton_OnClick(object sender, RoutedEventArgs e)
  126. {
  127. SetBusyState(true);
  128. try
  129. {
  130. await RefreshAdaptersAsync();
  131. SetStatus("已刷新本机网卡和管理口探测结果。", true);
  132. }
  133. catch (Exception ex)
  134. {
  135. SetStatus($"刷新本机网卡失败:{ex.Message}", true);
  136. MessageBox.Show(this, ex.Message, "刷新失败", MessageBoxButton.OK, MessageBoxImage.Error);
  137. }
  138. finally
  139. {
  140. SetBusyState(false);
  141. }
  142. }
  143. private async void DiscoverConnectButton_OnClick(object sender, RoutedEventArgs e)
  144. {
  145. if (string.IsNullOrWhiteSpace(GetCurrentPassword()))
  146. {
  147. SetStatus("请输入管理密码。", true);
  148. MessageBox.Show(this, "请先在右侧“管理密码(必填)”区域输入密码。", "缺少管理密码", MessageBoxButton.OK, MessageBoxImage.Information);
  149. return;
  150. }
  151. PersistPasswordIfNeeded();
  152. SetBusyState(true);
  153. SetStatus("正在检查当前网卡是否可直接访问管理口。", true);
  154. await Dispatcher.InvokeAsync(() => { }, System.Windows.Threading.DispatcherPriority.Render);
  155. try
  156. {
  157. if (AdapterComboBox.SelectedItem is AdapterInfo adapter && adapter.IsReachableToMaintenance)
  158. {
  159. var directResult = await _agentApiService.CheckHealthAsync("http://169.254.100.2:48888", GetCurrentPassword(), adapter.IPv4Address);
  160. if (directResult.Success)
  161. {
  162. _connectedBaseAddress = "http://169.254.100.2:48888";
  163. _connectedLocalIPv4 = adapter.IPv4Address;
  164. SetStatus("连接成功,无需切换本机网卡。", true);
  165. OpenDeviceDetailsWindow(_connectedBaseAddress, _connectedLocalIPv4);
  166. return;
  167. }
  168. if (directResult.StatusCode == 401)
  169. {
  170. SetStatus("该网卡可以直连管理口,但管理密码错误。", true);
  171. MessageBox.Show(this, "当前网卡已经可以直连 Agent,但密码校验失败,请确认密码是否正确。", "密码错误", MessageBoxButton.OK, MessageBoxImage.Warning);
  172. return;
  173. }
  174. if (directResult.StatusCode == 403)
  175. {
  176. SetStatus("该网卡可以直连管理口,但 Agent 拒绝访问,请确认远端是否还是旧版本。", true);
  177. MessageBox.Show(this, "当前网卡已经可以直连 Agent,但请求被拒绝。请确认 Linux 端是否运行的是最新版本 Agent。", "访问被拒绝", MessageBoxButton.OK, MessageBoxImage.Warning);
  178. return;
  179. }
  180. SetStatus($"该网卡虽可建立连接,但直连 HTTP 校验失败:{directResult.Message}。正在尝试设备发现。", true);
  181. }
  182. SetStatus("当前网卡无法完成直连校验,正在发现设备,请稍候。", true);
  183. var selectedAdapter = AdapterComboBox.SelectedItem as AdapterInfo;
  184. var device = await _discoveryService.DiscoverAsync(selectedAdapter?.IPv4Address ?? string.Empty);
  185. if (device is null)
  186. {
  187. SetStatus("未发现设备。如果当前网卡不可达,请先切换到维护网络。", true);
  188. return;
  189. }
  190. SetStatus("已发现设备,正在验证连接。", true);
  191. var discoveredResult = await _agentApiService.CheckHealthAsync($"http://{device.Lan2Ip}:48888", GetCurrentPassword(), selectedAdapter?.IPv4Address ?? string.Empty);
  192. if (discoveredResult.Success)
  193. {
  194. _connectedBaseAddress = $"http://{device.Lan2Ip}:48888";
  195. _connectedLocalIPv4 = selectedAdapter?.IPv4Address ?? string.Empty;
  196. SetStatus("连接成功。", true);
  197. OpenDeviceDetailsWindow(_connectedBaseAddress, _connectedLocalIPv4);
  198. }
  199. else
  200. {
  201. SetStatus($"设备已发现,但 HTTP 验证失败:{discoveredResult.Message}", true);
  202. }
  203. }
  204. catch (Exception ex)
  205. {
  206. ClearRemoteDetails();
  207. SetStatus($"连接失败:{ex.Message}", true);
  208. MessageBox.Show(this, ex.Message, "连接失败", MessageBoxButton.OK, MessageBoxImage.Error);
  209. }
  210. finally
  211. {
  212. SetBusyState(false);
  213. }
  214. }
  215. private void PersistPasswordIfNeeded()
  216. {
  217. var password = GetCurrentPassword();
  218. if (!string.IsNullOrWhiteSpace(password))
  219. {
  220. _passwordStoreService.SavePassword(password);
  221. return;
  222. }
  223. }
  224. private string GetCurrentPassword()
  225. {
  226. return _isShowingPassword ? PasswordTextBox.Text : PasswordBox.Password;
  227. }
  228. private void UpdateButtonStates()
  229. {
  230. var adapter = AdapterComboBox.SelectedItem as AdapterInfo;
  231. var hasAdapter = adapter is not null;
  232. RefreshAdaptersButton.IsEnabled = !_isBusy;
  233. SwitchMaintenanceButton.IsEnabled = !_isBusy && hasAdapter;
  234. DiscoverConnectButton.IsEnabled = !_isBusy && hasAdapter && adapter!.HasLink;
  235. ReloadInterfaceConfigButton.IsEnabled = RemoteTargetInterfaceComboBox.SelectedItem is RemoteInterfaceInfo;
  236. ValidateConfigButton.IsEnabled = RemoteTargetInterfaceComboBox.SelectedItem is RemoteInterfaceInfo;
  237. ApplyConfigButton.IsEnabled = _configValidated && RemoteTargetInterfaceComboBox.SelectedItem is RemoteInterfaceInfo;
  238. }
  239. private async Task RefreshAdaptersAsync(string? selectedAdapterId = null)
  240. {
  241. _adapters = _networkAdapterService.GetEthernetAdapters();
  242. await _networkAdapterService.ProbeMaintenanceReachabilityAsync(_adapters);
  243. _adapters = _adapters
  244. .OrderByDescending(adapter => adapter.RecommendationScore)
  245. .ThenBy(adapter => adapter.Name)
  246. .ToList();
  247. AdapterComboBox.ItemsSource = _adapters;
  248. var selected = selectedAdapterId is null
  249. ? _networkAdapterService.GetRecommendedAdapter(_adapters)
  250. : _adapters.FirstOrDefault(adapter => adapter.Id == selectedAdapterId) ?? _networkAdapterService.GetRecommendedAdapter(_adapters);
  251. if (selected is not null)
  252. {
  253. AdapterComboBox.SelectedItem = selected;
  254. UpdateAdapterDetails(selected);
  255. }
  256. }
  257. private void UpdateAdapterDetails(AdapterInfo? adapter)
  258. {
  259. if (adapter is null)
  260. {
  261. AdapterProbeTextBlock.Text = "-";
  262. return;
  263. }
  264. AdapterProbeTextBlock.Text = $"{adapter.ProbeStatus} / {adapter.ProbeReason}";
  265. }
  266. private async Task LoadRemoteDetailsAsync(string baseAddress, string localIPv4)
  267. {
  268. ClearRemoteDetails();
  269. RemoteDetailsBorder.Visibility = Visibility.Visible;
  270. SetStatus("正在读取设备信息。", true);
  271. var device = await _agentApiService.GetDeviceInfoAsync(baseAddress, GetCurrentPassword(), localIPv4);
  272. if (device is not null)
  273. {
  274. RemoteDeviceIdTextBlock.Text = device.DeviceId;
  275. RemoteHostnameTextBlock.Text = device.Hostname;
  276. RemoteOsVersionTextBlock.Text = device.OSVersion;
  277. RemoteAgentVersionTextBlock.Text = device.AgentVersion;
  278. }
  279. SetStatus("正在读取 Linux 接口列表。", true);
  280. var interfaces = await _agentApiService.GetInterfacesAsync(baseAddress, GetCurrentPassword(), localIPv4);
  281. if (interfaces is not null)
  282. {
  283. RemoteSummaryTextBlock.Text = $"当前管理接口:{interfaces.ManagementInterface};建议目标接口:{interfaces.SuggestedTargetInterface};{(interfaces.RequiresTargetSelection ? "需要手动选择目标接口。" : "已自动识别建议目标接口。")}";
  284. var suggested = interfaces.Interfaces.FirstOrDefault(item => item.SystemName == interfaces.SuggestedTargetInterface)
  285. ?? interfaces.Interfaces.FirstOrDefault(item => item.IsSuggestedTarget)
  286. ?? interfaces.Interfaces.FirstOrDefault(item => !item.IsManagementInterface);
  287. RemoteTargetInterfaceComboBox.ItemsSource = interfaces.Interfaces;
  288. if (suggested is not null)
  289. {
  290. RemoteTargetInterfaceComboBox.SelectedItem = suggested;
  291. await LoadRemoteInterfaceConfigAsync(suggested.SystemName);
  292. }
  293. SetStatus("已加载设备信息和 Linux 接口列表。", true);
  294. return;
  295. }
  296. RemoteSummaryTextBlock.Text = "设备已连接,但暂时无法读取 Linux 接口列表。";
  297. }
  298. private void OpenDeviceDetailsWindow(string baseAddress, string localIPv4)
  299. {
  300. var window = new DeviceDetailsWindow(baseAddress, localIPv4, GetCurrentPassword())
  301. {
  302. Owner = this,
  303. };
  304. window.ShowDialog();
  305. }
  306. private void ClearRemoteDetails()
  307. {
  308. RemoteDetailsBorder.Visibility = Visibility.Collapsed;
  309. RemoteDeviceIdTextBlock.Text = "-";
  310. RemoteHostnameTextBlock.Text = "-";
  311. RemoteOsVersionTextBlock.Text = "-";
  312. RemoteAgentVersionTextBlock.Text = "-";
  313. RemoteTargetInterfaceComboBox.ItemsSource = null;
  314. RemoteConfigInterfaceTextBlock.Text = "-";
  315. RemoteConfigIpTextBlock.Text = "-";
  316. RemoteConfigGatewayTextBlock.Text = "-";
  317. RemoteConfigDnsTextBlock.Text = "-";
  318. NewIpTextBox.Text = string.Empty;
  319. NewMaskTextBox.Text = string.Empty;
  320. NewGatewayTextBox.Text = string.Empty;
  321. NewDnsTextBox.Text = string.Empty;
  322. ConfigValidationTextBlock.Text = "读取目标接口当前配置后,可在此修改并校验。";
  323. ApplyTaskStatusTextBlock.Text = "尚未提交配置任务。";
  324. _configValidated = false;
  325. RemoteSummaryTextBlock.Text = "连接成功后,这里会显示 Linux 管理接口和建议目标接口。";
  326. UpdateButtonStates();
  327. }
  328. private async void RemoteTargetInterfaceComboBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
  329. {
  330. if (RemoteTargetInterfaceComboBox.SelectedItem is not RemoteInterfaceInfo selected || string.IsNullOrWhiteSpace(_connectedBaseAddress))
  331. {
  332. return;
  333. }
  334. await LoadRemoteInterfaceConfigAsync(selected.SystemName);
  335. }
  336. private async Task LoadRemoteInterfaceConfigAsync(string interfaceName)
  337. {
  338. SetStatus($"正在读取目标接口 {interfaceName} 当前配置。", true);
  339. var result = await _agentApiService.GetInterfaceConfigAsync(_connectedBaseAddress, GetCurrentPassword(), _connectedLocalIPv4, interfaceName);
  340. if (!result.Success || result.Data is null)
  341. {
  342. RemoteConfigInterfaceTextBlock.Text = interfaceName;
  343. RemoteConfigIpTextBlock.Text = "读取失败";
  344. RemoteConfigGatewayTextBlock.Text = "读取失败";
  345. RemoteConfigDnsTextBlock.Text = "读取失败";
  346. SetStatus($"读取目标接口 {interfaceName} 配置失败:{result.Message}", true);
  347. return;
  348. }
  349. var config = result.Data;
  350. RemoteConfigInterfaceTextBlock.Text = config.Interface;
  351. RemoteConfigIpTextBlock.Text = string.IsNullOrWhiteSpace(config.IP) ? "无" : $"{config.IP}/{config.Prefix}";
  352. RemoteConfigGatewayTextBlock.Text = string.IsNullOrWhiteSpace(config.Gateway) ? "无" : config.Gateway;
  353. RemoteConfigDnsTextBlock.Text = config.DnsSummary;
  354. _suppressConfigChangeHandling = true;
  355. NewIpTextBox.Text = config.IP;
  356. NewMaskTextBox.Text = PrefixToMask(config.Prefix);
  357. NewGatewayTextBox.Text = config.Gateway;
  358. NewDnsTextBox.Text = config.Dns.FirstOrDefault() ?? string.Empty;
  359. _suppressConfigChangeHandling = false;
  360. _configValidated = false;
  361. ConfigValidationTextBlock.Text = "已回填目标接口当前配置,可直接修改后校验。";
  362. ApplyTaskStatusTextBlock.Text = "尚未提交配置任务。";
  363. UpdateButtonStates();
  364. SetStatus($"已读取目标接口 {interfaceName} 当前配置。", true);
  365. }
  366. private async void ReloadInterfaceConfigButton_OnClick(object sender, RoutedEventArgs e)
  367. {
  368. if (RemoteTargetInterfaceComboBox.SelectedItem is not RemoteInterfaceInfo selected)
  369. {
  370. return;
  371. }
  372. await LoadRemoteInterfaceConfigAsync(selected.SystemName);
  373. }
  374. private async void ValidateConfigButton_OnClick(object sender, RoutedEventArgs e)
  375. {
  376. if (RemoteTargetInterfaceComboBox.SelectedItem is not RemoteInterfaceInfo selected)
  377. {
  378. return;
  379. }
  380. var request = BuildConfigRequest(selected.SystemName);
  381. if (request is null)
  382. {
  383. return;
  384. }
  385. SetStatus($"正在校验目标接口 {selected.SystemName} 的新配置。", true);
  386. var result = await _agentApiService.ValidateInterfaceConfigAsync(_connectedBaseAddress, GetCurrentPassword(), _connectedLocalIPv4, request);
  387. _configValidated = result.Success && result.Data?.Valid == true;
  388. if (result.Data is not null)
  389. {
  390. var warnings = result.Data.Warnings.Count > 0 ? $" 警告:{string.Join(";", result.Data.Warnings)}" : string.Empty;
  391. var errors = result.Data.Errors.Count > 0 ? $" 错误:{string.Join(";", result.Data.Errors)}" : string.Empty;
  392. ConfigValidationTextBlock.Text = result.Success ? $"校验通过。{warnings}" : $"校验失败。{errors}{warnings}";
  393. }
  394. else
  395. {
  396. ConfigValidationTextBlock.Text = $"校验失败:{result.Message}";
  397. }
  398. ApplyTaskStatusTextBlock.Text = _configValidated ? "配置已通过校验,可提交应用。" : "当前配置尚未通过校验。";
  399. UpdateButtonStates();
  400. }
  401. private async void ApplyConfigButton_OnClick(object sender, RoutedEventArgs e)
  402. {
  403. if (RemoteTargetInterfaceComboBox.SelectedItem is not RemoteInterfaceInfo selected)
  404. {
  405. return;
  406. }
  407. var request = BuildConfigRequest(selected.SystemName);
  408. if (request is null)
  409. {
  410. return;
  411. }
  412. var confirmMessage = $"将要把以下配置应用到接口 {selected.SystemName}:\n\n" +
  413. $"IP:{request.IP}/{request.Prefix}\n" +
  414. $"网关:{(string.IsNullOrWhiteSpace(request.Gateway) ? "无" : request.Gateway)}\n" +
  415. $"DNS:{(request.Dns.Count == 0 ? "无" : string.Join(", ", request.Dns))}\n\n" +
  416. "请确认是否继续。";
  417. if (MessageBox.Show(this, confirmMessage, "确认应用配置", MessageBoxButton.OKCancel, MessageBoxImage.Question) != MessageBoxResult.OK)
  418. {
  419. return;
  420. }
  421. SetStatus($"正在提交目标接口 {selected.SystemName} 的配置任务。", true);
  422. var applyResult = await _agentApiService.ApplyInterfaceConfigAsync(_connectedBaseAddress, GetCurrentPassword(), _connectedLocalIPv4, request);
  423. if (!applyResult.Success || applyResult.Data is null)
  424. {
  425. ApplyTaskStatusTextBlock.Text = $"提交配置任务失败:{applyResult.Message}";
  426. return;
  427. }
  428. ApplyTaskStatusTextBlock.Text = $"配置任务已提交:{applyResult.Data.TaskId},正在轮询状态。";
  429. await PollTaskAsync(applyResult.Data.TaskId);
  430. }
  431. private async Task PollTaskAsync(string taskId)
  432. {
  433. var transientFailureCount = 0;
  434. for (var i = 0; i < 20; i++)
  435. {
  436. await Task.Delay(1000);
  437. var result = await _agentApiService.GetTaskAsync(_connectedBaseAddress, GetCurrentPassword(), _connectedLocalIPv4, taskId);
  438. if (!result.Success || result.Data is null)
  439. {
  440. if (result.StatusCode is null)
  441. {
  442. transientFailureCount++;
  443. ApplyTaskStatusTextBlock.Text = $"任务 {taskId} 轮询中,检测到短暂断连,正在重试({transientFailureCount})。";
  444. continue;
  445. }
  446. ApplyTaskStatusTextBlock.Text = $"读取任务状态失败:{result.Message}";
  447. return;
  448. }
  449. transientFailureCount = 0;
  450. var task = result.Data;
  451. ApplyTaskStatusTextBlock.Text = $"任务 {task.TaskId} / {task.Status} / {task.Step} / {task.Detail}";
  452. if (task.Status is "success" or "failed" or "rolled_back")
  453. {
  454. if (RemoteTargetInterfaceComboBox.SelectedItem is RemoteInterfaceInfo selected)
  455. {
  456. await LoadRemoteInterfaceConfigAsync(selected.SystemName);
  457. }
  458. return;
  459. }
  460. }
  461. ApplyTaskStatusTextBlock.Text = $"任务 {taskId} 轮询超时,请稍后手动刷新。";
  462. }
  463. private RemoteInterfaceConfig? BuildConfigRequest(string interfaceName)
  464. {
  465. if (string.IsNullOrWhiteSpace(NewIpTextBox.Text))
  466. {
  467. ConfigValidationTextBlock.Text = "IP 地址不能为空。";
  468. return null;
  469. }
  470. if (!TryMaskToPrefix(NewMaskTextBox.Text, out var prefix))
  471. {
  472. ConfigValidationTextBlock.Text = "子网掩码格式不正确。";
  473. return null;
  474. }
  475. var dns = string.IsNullOrWhiteSpace(NewDnsTextBox.Text)
  476. ? Array.Empty<string>()
  477. : new[] { NewDnsTextBox.Text.Trim() };
  478. return new RemoteInterfaceConfig
  479. {
  480. Interface = interfaceName,
  481. IP = NewIpTextBox.Text.Trim(),
  482. Prefix = prefix,
  483. Gateway = NewGatewayTextBox.Text.Trim(),
  484. Dns = dns,
  485. };
  486. }
  487. private static string PrefixToMask(int prefix)
  488. {
  489. if (prefix < 0 || prefix > 32)
  490. {
  491. return string.Empty;
  492. }
  493. var mask = prefix == 0 ? 0u : uint.MaxValue << (32 - prefix);
  494. return string.Join('.', new[]
  495. {
  496. (mask >> 24) & 255,
  497. (mask >> 16) & 255,
  498. (mask >> 8) & 255,
  499. mask & 255,
  500. });
  501. }
  502. private static bool TryMaskToPrefix(string maskText, out int prefix)
  503. {
  504. prefix = 0;
  505. if (string.IsNullOrWhiteSpace(maskText))
  506. {
  507. return false;
  508. }
  509. var parts = maskText.Trim().Split('.');
  510. if (parts.Length != 4)
  511. {
  512. return false;
  513. }
  514. uint mask = 0;
  515. foreach (var part in parts)
  516. {
  517. if (!byte.TryParse(part, out var octet))
  518. {
  519. return false;
  520. }
  521. mask = (mask << 8) | octet;
  522. }
  523. var seenZero = false;
  524. for (var i = 31; i >= 0; i--)
  525. {
  526. var bit = (mask & (1u << i)) != 0;
  527. if (bit && seenZero)
  528. {
  529. return false;
  530. }
  531. if (bit)
  532. {
  533. prefix++;
  534. }
  535. else
  536. {
  537. seenZero = true;
  538. }
  539. }
  540. return true;
  541. }
  542. private void ConfigInputChanged_OnChanged(object sender, TextChangedEventArgs e)
  543. {
  544. if (_suppressConfigChangeHandling)
  545. {
  546. return;
  547. }
  548. _configValidated = false;
  549. ConfigValidationTextBlock.Text = "配置内容已变更,请重新点击“校验配置”。";
  550. ApplyTaskStatusTextBlock.Text = "当前配置尚未通过校验。";
  551. UpdateButtonStates();
  552. }
  553. private void SetStatus(string message, bool addLog)
  554. {
  555. ApplyStatusMessageStyle(message);
  556. StatusTextBlock.Text = message;
  557. StatusMessageBorder.Opacity = 0;
  558. StatusMessageBorder.Visibility = Visibility.Visible;
  559. StatusMessageBorder.BeginAnimation(OpacityProperty, new DoubleAnimation(1, TimeSpan.FromMilliseconds(160)));
  560. _statusMessageCts?.Cancel();
  561. _statusMessageCts = new CancellationTokenSource();
  562. _ = HideStatusMessageAsync(_statusMessageCts.Token);
  563. if (addLog)
  564. {
  565. AppendLog(message, false);
  566. }
  567. }
  568. private async Task HideStatusMessageAsync(CancellationToken cancellationToken)
  569. {
  570. try
  571. {
  572. await Task.Delay(3000, cancellationToken);
  573. await Dispatcher.InvokeAsync(() =>
  574. {
  575. var animation = new DoubleAnimation(0, TimeSpan.FromMilliseconds(200));
  576. animation.Completed += (_, _) =>
  577. {
  578. if (!cancellationToken.IsCancellationRequested)
  579. {
  580. StatusMessageBorder.Visibility = Visibility.Collapsed;
  581. }
  582. };
  583. StatusMessageBorder.BeginAnimation(OpacityProperty, animation);
  584. });
  585. }
  586. catch (TaskCanceledException)
  587. {
  588. }
  589. }
  590. private void ApplyStatusMessageStyle(string message)
  591. {
  592. var (background, foreground) = GetStatusMessageBrushes(message);
  593. StatusMessageBorder.Background = background;
  594. StatusTextBlock.Foreground = foreground;
  595. }
  596. private static (Brush Background, Brush Foreground) GetStatusMessageBrushes(string message)
  597. {
  598. if (ContainsAny(message, "失败", "错误", "拒绝", "超时", "不能为空", "不正确", "无法"))
  599. {
  600. return (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#B91C1C")), Brushes.White);
  601. }
  602. if (ContainsAny(message, "未发现", "请", "重试", "警告", "需要"))
  603. {
  604. return (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#C2410C")), Brushes.White);
  605. }
  606. if (ContainsAny(message, "成功", "已切换", "已刷新", "已读取", "已加载", "已发现", "已提交", "已回填"))
  607. {
  608. return (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#047857")), Brushes.White);
  609. }
  610. return (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#111827")), Brushes.White);
  611. }
  612. private static bool ContainsAny(string message, params string[] markers)
  613. {
  614. return markers.Any(marker => message.Contains(marker, StringComparison.Ordinal));
  615. }
  616. private void AppendLog(string message, bool isInitial)
  617. {
  618. var prefix = DateTime.Now.ToString("HH:mm:ss", CultureInfo.InvariantCulture);
  619. EventLogListBox.Items.Add($"[{prefix}] {message}");
  620. if (!isInitial)
  621. {
  622. EventLogListBox.ScrollIntoView(EventLogListBox.Items[^1]);
  623. }
  624. }
  625. private void SetBusyState(bool isBusy)
  626. {
  627. _isBusy = isBusy;
  628. AdapterComboBox.IsEnabled = !isBusy;
  629. RefreshAdaptersButton.IsEnabled = !isBusy;
  630. PasswordBox.IsEnabled = !isBusy;
  631. PasswordTextBox.IsEnabled = !isBusy;
  632. TogglePasswordVisibilityButton.IsEnabled = !isBusy;
  633. SwitchMaintenanceButton.IsEnabled = !isBusy && AdapterComboBox.SelectedItem is AdapterInfo;
  634. DiscoverConnectButton.IsEnabled = !isBusy && AdapterComboBox.SelectedItem is AdapterInfo adapter && adapter.HasLink;
  635. }
  636. private void TogglePasswordVisibilityButton_OnClick(object sender, RoutedEventArgs e)
  637. {
  638. _isShowingPassword = !_isShowingPassword;
  639. if (_isShowingPassword)
  640. {
  641. PasswordTextBox.Text = PasswordBox.Password;
  642. PasswordBox.Visibility = Visibility.Collapsed;
  643. PasswordTextBox.Visibility = Visibility.Visible;
  644. TogglePasswordVisibilityButton.Content = "🙈";
  645. PasswordTextBox.Focus();
  646. PasswordTextBox.CaretIndex = PasswordTextBox.Text.Length;
  647. return;
  648. }
  649. PasswordBox.Password = PasswordTextBox.Text;
  650. PasswordTextBox.Visibility = Visibility.Collapsed;
  651. PasswordBox.Visibility = Visibility.Visible;
  652. TogglePasswordVisibilityButton.Content = "👁";
  653. PasswordBox.Focus();
  654. }
  655. private void PasswordBox_OnPasswordChanged(object sender, RoutedEventArgs e)
  656. {
  657. if (_suppressPasswordSync)
  658. {
  659. return;
  660. }
  661. _suppressPasswordSync = true;
  662. PasswordTextBox.Text = PasswordBox.Password;
  663. _suppressPasswordSync = false;
  664. }
  665. private void PasswordTextBox_OnTextChanged(object sender, TextChangedEventArgs e)
  666. {
  667. if (_suppressPasswordSync)
  668. {
  669. return;
  670. }
  671. _suppressPasswordSync = true;
  672. PasswordBox.Password = PasswordTextBox.Text;
  673. _suppressPasswordSync = false;
  674. }
  675. }