901 lines
32 KiB
C#
901 lines
32 KiB
C#
using System.Diagnostics;
|
||
using CodexProviderSync.Core;
|
||
|
||
namespace CodexProviderSync.App;
|
||
|
||
public sealed class MainForm : Form
|
||
{
|
||
private readonly CodexSyncService _syncService = new();
|
||
private readonly SettingsService _settingsService = new();
|
||
|
||
private readonly ComboBox _codexHomeCombo = new() { Dock = DockStyle.Fill, DropDownStyle = ComboBoxStyle.DropDown };
|
||
private readonly Button _browseButton = new() { Text = "Browse...", AutoSize = true };
|
||
private readonly Button _refreshButton = new() { Text = "Refresh", AutoSize = true };
|
||
private readonly RichTextBox _statusBox = new()
|
||
{
|
||
Dock = DockStyle.Fill,
|
||
ReadOnly = true,
|
||
WordWrap = false,
|
||
Font = new Font("Consolas", 10F),
|
||
BackColor = SystemColors.Window
|
||
};
|
||
private readonly ListView _providerList = new()
|
||
{
|
||
Dock = DockStyle.Fill,
|
||
View = View.Details,
|
||
FullRowSelect = true,
|
||
HideSelection = false,
|
||
MultiSelect = false
|
||
};
|
||
private readonly TextBox _manualProviderText = new() { Dock = DockStyle.Fill };
|
||
private readonly Button _addProviderButton = new() { Text = "添加", AutoSize = true };
|
||
private readonly Button _removeProviderButton = new() { Text = "删除手动项", AutoSize = true };
|
||
private readonly Label _selectedProviderValue = new()
|
||
{
|
||
AutoSize = false,
|
||
Dock = DockStyle.Fill,
|
||
Height = 32,
|
||
Font = new Font(SystemFonts.DefaultFont, FontStyle.Bold),
|
||
TextAlign = ContentAlignment.MiddleLeft
|
||
};
|
||
private readonly CheckBox _updateConfigCheck = new()
|
||
{
|
||
AutoSize = true,
|
||
Margin = new Padding(0, 4, 8, 0)
|
||
};
|
||
private readonly CheckBox _restoreConfigCheck = new() { Text = "恢复配置文件(config.toml)", Checked = false, AutoSize = true };
|
||
private readonly CheckBox _restoreDatabaseCheck = new() { Text = "恢复线程数据库(SQLite)", Checked = true, AutoSize = true };
|
||
private readonly CheckBox _restoreSessionsCheck = new() { Text = "恢复会话文件元数据(rollout)", Checked = true, AutoSize = true };
|
||
private readonly Label _updateConfigLabel = new()
|
||
{
|
||
AutoSize = false,
|
||
Dock = DockStyle.Fill,
|
||
TextAlign = ContentAlignment.MiddleLeft,
|
||
Text = "同步时改写 config.toml"
|
||
};
|
||
private readonly NumericUpDown _backupRetentionInput = new()
|
||
{
|
||
Minimum = 1,
|
||
Maximum = 100000,
|
||
Value = AppConstants.DefaultBackupRetentionCount,
|
||
Width = 72
|
||
};
|
||
private readonly Button _executeButton = new() { Text = "立即同步", Dock = DockStyle.Fill, Height = 40 };
|
||
private readonly Button _restoreButton = new() { Text = "恢复备份", Dock = DockStyle.Fill, Height = 40 };
|
||
private readonly Button _openBackupButton = new() { Text = "打开备份目录", Dock = DockStyle.Fill, Height = 40 };
|
||
private readonly Button _pruneBackupsButton = new() { Text = "清理旧备份", Dock = DockStyle.Fill, Height = 40 };
|
||
private readonly Label _busyLabel = new() { AutoSize = true, ForeColor = Color.DarkGreen, Text = "Ready" };
|
||
private readonly Label _warningLine1 = new()
|
||
{
|
||
AutoSize = false,
|
||
Dock = DockStyle.Fill,
|
||
BackColor = Color.FromArgb(255, 244, 214),
|
||
ForeColor = Color.FromArgb(120, 53, 15),
|
||
TextAlign = ContentAlignment.MiddleLeft,
|
||
Text = "执行前先关闭 Codex CLI / App"
|
||
};
|
||
private readonly Label _warningLine2 = new()
|
||
{
|
||
AutoSize = false,
|
||
Dock = DockStyle.Fill,
|
||
BackColor = Color.FromArgb(255, 244, 214),
|
||
ForeColor = Color.FromArgb(120, 53, 15),
|
||
TextAlign = ContentAlignment.MiddleLeft,
|
||
Text = "以及 app-server / 相关终端"
|
||
};
|
||
private readonly TextBox _logBox = new()
|
||
{
|
||
Dock = DockStyle.Fill,
|
||
ReadOnly = true,
|
||
Multiline = true,
|
||
ScrollBars = ScrollBars.Both,
|
||
Font = new Font("Consolas", 10F),
|
||
BackColor = SystemColors.Window
|
||
};
|
||
|
||
private AppSettings _settings = new();
|
||
private StatusSnapshot? _currentStatus;
|
||
private bool _loadingSettings;
|
||
|
||
public MainForm()
|
||
{
|
||
Text = "Codex Provider Sync";
|
||
MinimumSize = new Size(1180, 760);
|
||
StartPosition = FormStartPosition.CenterScreen;
|
||
|
||
_providerList.Columns.Add("Provider", 180);
|
||
_providerList.Columns.Add("来源", 180);
|
||
_providerList.Columns.Add("当前", 70);
|
||
_providerList.Columns.Add("手动", 70);
|
||
_providerList.Columns.Add("已保存", 70);
|
||
|
||
BuildLayout();
|
||
WireEvents();
|
||
}
|
||
|
||
protected override async void OnLoad(EventArgs e)
|
||
{
|
||
base.OnLoad(e);
|
||
await LoadStateAsync();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Brings the running form back to the foreground. Invoked by the
|
||
/// single-instance focus broker when a second copy of CodexProviderSync
|
||
/// is launched and asks the first copy to take focus.
|
||
/// </summary>
|
||
public new void BringToFront()
|
||
{
|
||
if (InvokeRequired)
|
||
{
|
||
BeginInvoke(new Action(BringToFront));
|
||
return;
|
||
}
|
||
if (WindowState == FormWindowState.Minimized)
|
||
{
|
||
WindowState = FormWindowState.Normal;
|
||
}
|
||
Show();
|
||
Activate();
|
||
TopMost = true;
|
||
TopMost = false;
|
||
Focus();
|
||
}
|
||
|
||
protected override void OnFormClosing(FormClosingEventArgs e)
|
||
{
|
||
PersistUiState();
|
||
base.OnFormClosing(e);
|
||
}
|
||
|
||
private void BuildLayout()
|
||
{
|
||
TableLayoutPanel root = new()
|
||
{
|
||
Dock = DockStyle.Fill,
|
||
ColumnCount = 1,
|
||
RowCount = 3,
|
||
Padding = new Padding(12)
|
||
};
|
||
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||
root.RowStyles.Add(new RowStyle(SizeType.Percent, 68));
|
||
root.RowStyles.Add(new RowStyle(SizeType.Percent, 32));
|
||
|
||
root.Controls.Add(BuildTopPanel(), 0, 0);
|
||
root.Controls.Add(BuildMainPanel(), 0, 1);
|
||
root.Controls.Add(BuildLogPanel(), 0, 2);
|
||
|
||
Controls.Add(root);
|
||
}
|
||
|
||
private Control BuildTopPanel()
|
||
{
|
||
GroupBox group = new()
|
||
{
|
||
Text = "Codex Home",
|
||
Dock = DockStyle.Top,
|
||
AutoSize = true,
|
||
AutoSizeMode = AutoSizeMode.GrowAndShrink
|
||
};
|
||
|
||
TableLayoutPanel panel = new()
|
||
{
|
||
Dock = DockStyle.Top,
|
||
ColumnCount = 4,
|
||
AutoSize = true,
|
||
Padding = new Padding(10)
|
||
};
|
||
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
||
panel.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
|
||
panel.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
|
||
panel.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
|
||
|
||
Label recentLabel = new()
|
||
{
|
||
Text = "最近使用会自动保留在下拉列表中",
|
||
AutoSize = true,
|
||
ForeColor = SystemColors.GrayText,
|
||
Anchor = AnchorStyles.Left
|
||
};
|
||
|
||
panel.Controls.Add(_codexHomeCombo, 0, 0);
|
||
panel.Controls.Add(_browseButton, 1, 0);
|
||
panel.Controls.Add(_refreshButton, 2, 0);
|
||
panel.Controls.Add(recentLabel, 3, 0);
|
||
group.Controls.Add(panel);
|
||
return group;
|
||
}
|
||
|
||
private Control BuildMainPanel()
|
||
{
|
||
TableLayoutPanel main = new()
|
||
{
|
||
Dock = DockStyle.Fill,
|
||
ColumnCount = 3,
|
||
RowCount = 1,
|
||
Margin = new Padding(0, 12, 0, 12)
|
||
};
|
||
main.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50));
|
||
main.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50));
|
||
main.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 380));
|
||
|
||
main.Controls.Add(BuildStatusGroup(), 0, 0);
|
||
main.Controls.Add(BuildProviderGroup(), 1, 0);
|
||
main.Controls.Add(BuildActionGroup(), 2, 0);
|
||
return main;
|
||
}
|
||
|
||
private Control BuildStatusGroup()
|
||
{
|
||
GroupBox group = new() { Text = "当前状态", Dock = DockStyle.Fill };
|
||
group.Controls.Add(_statusBox);
|
||
return group;
|
||
}
|
||
|
||
private Control BuildProviderGroup()
|
||
{
|
||
GroupBox group = new() { Text = "Provider 列表", Dock = DockStyle.Fill };
|
||
TableLayoutPanel panel = new()
|
||
{
|
||
Dock = DockStyle.Fill,
|
||
ColumnCount = 1,
|
||
RowCount = 3,
|
||
Padding = new Padding(8)
|
||
};
|
||
panel.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
|
||
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||
|
||
FlowLayoutPanel addPanel = new()
|
||
{
|
||
Dock = DockStyle.Fill,
|
||
FlowDirection = FlowDirection.LeftToRight,
|
||
AutoSize = true,
|
||
WrapContents = false
|
||
};
|
||
addPanel.Controls.Add(new Label { Text = "手动添加:", AutoSize = true, Margin = new Padding(0, 8, 8, 0) });
|
||
_manualProviderText.Width = 220;
|
||
addPanel.Controls.Add(_manualProviderText);
|
||
addPanel.Controls.Add(_addProviderButton);
|
||
|
||
FlowLayoutPanel hintPanel = new()
|
||
{
|
||
Dock = DockStyle.Fill,
|
||
FlowDirection = FlowDirection.LeftToRight,
|
||
AutoSize = true,
|
||
WrapContents = false
|
||
};
|
||
hintPanel.Controls.Add(_removeProviderButton);
|
||
hintPanel.Controls.Add(new Label
|
||
{
|
||
Text = "Refresh 时会把扫描到的新 Provider 自动并入持久化列表",
|
||
AutoSize = true,
|
||
Margin = new Padding(12, 8, 0, 0),
|
||
ForeColor = SystemColors.GrayText
|
||
});
|
||
|
||
panel.Controls.Add(_providerList, 0, 0);
|
||
panel.Controls.Add(addPanel, 0, 1);
|
||
panel.Controls.Add(hintPanel, 0, 2);
|
||
group.Controls.Add(panel);
|
||
return group;
|
||
}
|
||
|
||
private Control BuildActionGroup()
|
||
{
|
||
GroupBox group = new() { Text = "执行", Dock = DockStyle.Fill, MinimumSize = new Size(420, 0) };
|
||
TableLayoutPanel panel = new()
|
||
{
|
||
Dock = DockStyle.Fill,
|
||
ColumnCount = 1,
|
||
RowCount = 11,
|
||
Padding = new Padding(12)
|
||
};
|
||
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||
panel.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
|
||
|
||
panel.Controls.Add(new Label { Text = "目标 Provider", AutoSize = true }, 0, 0);
|
||
panel.Controls.Add(_selectedProviderValue, 0, 1);
|
||
panel.Controls.Add(BuildUpdateConfigPanel(), 0, 2);
|
||
panel.Controls.Add(BuildWarningPanel(), 0, 3);
|
||
panel.Controls.Add(BuildBackupRetentionPanel(), 0, 4);
|
||
panel.Controls.Add(_executeButton, 0, 5);
|
||
panel.Controls.Add(BuildRestoreOptionsPanel(), 0, 6);
|
||
panel.Controls.Add(_restoreButton, 0, 7);
|
||
panel.Controls.Add(_openBackupButton, 0, 8);
|
||
panel.Controls.Add(_pruneBackupsButton, 0, 9);
|
||
panel.Controls.Add(_busyLabel, 0, 10);
|
||
|
||
group.Controls.Add(panel);
|
||
return group;
|
||
}
|
||
|
||
private Control BuildUpdateConfigPanel()
|
||
{
|
||
TableLayoutPanel panel = new()
|
||
{
|
||
Dock = DockStyle.Fill,
|
||
ColumnCount = 2,
|
||
Margin = new Padding(0, 2, 0, 8)
|
||
};
|
||
panel.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
|
||
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
||
panel.Controls.Add(_updateConfigCheck, 0, 0);
|
||
panel.Controls.Add(_updateConfigLabel, 1, 0);
|
||
return panel;
|
||
}
|
||
|
||
private Control BuildWarningPanel()
|
||
{
|
||
Panel panel = new()
|
||
{
|
||
Dock = DockStyle.Fill,
|
||
Height = 86,
|
||
Padding = new Padding(10, 8, 10, 8),
|
||
Margin = new Padding(0, 4, 0, 8),
|
||
BackColor = Color.FromArgb(255, 244, 214)
|
||
};
|
||
TableLayoutPanel textLayout = new()
|
||
{
|
||
Dock = DockStyle.Fill,
|
||
ColumnCount = 1,
|
||
RowCount = 2,
|
||
BackColor = Color.FromArgb(255, 244, 214),
|
||
Margin = new Padding(0),
|
||
Padding = new Padding(0)
|
||
};
|
||
textLayout.RowStyles.Add(new RowStyle(SizeType.Percent, 50));
|
||
textLayout.RowStyles.Add(new RowStyle(SizeType.Percent, 50));
|
||
textLayout.Controls.Add(_warningLine1, 0, 0);
|
||
textLayout.Controls.Add(_warningLine2, 0, 1);
|
||
panel.Controls.Add(textLayout);
|
||
return panel;
|
||
}
|
||
|
||
private Control BuildBackupRetentionPanel()
|
||
{
|
||
TableLayoutPanel panel = new()
|
||
{
|
||
Dock = DockStyle.Fill,
|
||
ColumnCount = 3,
|
||
Margin = new Padding(0, 2, 0, 8),
|
||
AutoSize = true
|
||
};
|
||
panel.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
|
||
panel.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
|
||
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
||
|
||
panel.Controls.Add(new Label
|
||
{
|
||
Text = "自动保留最近",
|
||
AutoSize = true,
|
||
Anchor = AnchorStyles.Left,
|
||
Margin = new Padding(0, 6, 8, 0)
|
||
}, 0, 0);
|
||
panel.Controls.Add(_backupRetentionInput, 1, 0);
|
||
panel.Controls.Add(new Label
|
||
{
|
||
Text = "份备份",
|
||
AutoSize = true,
|
||
Anchor = AnchorStyles.Left,
|
||
Margin = new Padding(8, 6, 0, 0)
|
||
}, 2, 0);
|
||
|
||
return panel;
|
||
}
|
||
|
||
private Control BuildRestoreOptionsPanel()
|
||
{
|
||
TableLayoutPanel panel = new()
|
||
{
|
||
Dock = DockStyle.Fill,
|
||
ColumnCount = 1,
|
||
Margin = new Padding(0, 0, 0, 6),
|
||
AutoSize = true
|
||
};
|
||
panel.RowCount = 4;
|
||
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
||
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||
|
||
panel.Controls.Add(new Label
|
||
{
|
||
Text = "恢复内容",
|
||
AutoSize = true,
|
||
Anchor = AnchorStyles.Left,
|
||
Margin = new Padding(0, 2, 0, 4)
|
||
}, 0, 0);
|
||
panel.Controls.Add(_restoreConfigCheck, 0, 1);
|
||
panel.Controls.Add(_restoreDatabaseCheck, 0, 2);
|
||
panel.Controls.Add(_restoreSessionsCheck, 0, 3);
|
||
return panel;
|
||
}
|
||
|
||
private Control BuildLogPanel()
|
||
{
|
||
GroupBox group = new() { Text = "执行日志", Dock = DockStyle.Fill };
|
||
group.Controls.Add(_logBox);
|
||
return group;
|
||
}
|
||
|
||
private void WireEvents()
|
||
{
|
||
_browseButton.Click += async (_, _) => await BrowseCodexHomeAsync();
|
||
_refreshButton.Click += async (_, _) => await RefreshStatusAsync();
|
||
_addProviderButton.Click += async (_, _) => await AddManualProviderAsync();
|
||
_removeProviderButton.Click += async (_, _) => await RemoveManualProviderAsync();
|
||
_backupRetentionInput.ValueChanged += async (_, _) => await PersistBackupRetentionAsync();
|
||
_executeButton.Click += async (_, _) => await ExecuteSyncOrSwitchAsync();
|
||
_restoreButton.Click += async (_, _) => await RestoreBackupAsync();
|
||
_openBackupButton.Click += (_, _) => OpenBackupFolder();
|
||
_pruneBackupsButton.Click += async (_, _) => await PruneBackupsAsync();
|
||
_providerList.SelectedIndexChanged += (_, _) => UpdateSelectionLabel();
|
||
_codexHomeCombo.Leave += async (_, _) => await PersistHomeSelectionAsync();
|
||
}
|
||
|
||
private async Task LoadStateAsync()
|
||
{
|
||
_loadingSettings = true;
|
||
_settings = await _settingsService.LoadAsync();
|
||
ApplyWindowBounds(_settings.WindowBounds);
|
||
ReloadRecentHomes();
|
||
_codexHomeCombo.Text = _settings.LastCodexHome ?? AppConstants.DefaultCodexHome();
|
||
_backupRetentionInput.Value = Math.Max(_backupRetentionInput.Minimum, Math.Min(_backupRetentionInput.Maximum, _settings.BackupRetentionCount));
|
||
AppendLog($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 已加载设置: {_settingsService.SettingsPath}");
|
||
_loadingSettings = false;
|
||
await RefreshStatusAsync();
|
||
}
|
||
|
||
private async Task RefreshStatusAsync()
|
||
{
|
||
string codexHome = CurrentCodexHome();
|
||
await RunBusyAsync("Refreshing...", () => RefreshStatusCoreAsync(codexHome));
|
||
}
|
||
|
||
private async Task BrowseCodexHomeAsync()
|
||
{
|
||
using FolderBrowserDialog dialog = new()
|
||
{
|
||
Description = "选择 .codex 目录",
|
||
UseDescriptionForTitle = true,
|
||
InitialDirectory = Directory.Exists(CurrentCodexHome()) ? CurrentCodexHome() : AppConstants.DefaultCodexHome(),
|
||
ShowNewFolderButton = false
|
||
};
|
||
|
||
if (dialog.ShowDialog(this) != DialogResult.OK)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_codexHomeCombo.Text = dialog.SelectedPath;
|
||
await PersistHomeSelectionAsync();
|
||
await RefreshStatusAsync();
|
||
}
|
||
|
||
private async Task PersistHomeSelectionAsync()
|
||
{
|
||
string codexHome = CurrentCodexHome();
|
||
if (string.IsNullOrWhiteSpace(codexHome))
|
||
{
|
||
return;
|
||
}
|
||
|
||
_settings = _settingsService.RecordCodexHome(_settings, codexHome);
|
||
_settings = _settingsService.UpdateState(_settings, SelectedProvider(), _settings.LastBackupDirectory, CaptureWindowBounds(), CurrentBackupRetentionCount());
|
||
await _settingsService.SaveAsync(_settings);
|
||
ReloadRecentHomes();
|
||
}
|
||
|
||
private async Task PersistBackupRetentionAsync()
|
||
{
|
||
if (_loadingSettings)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_settings = _settingsService.UpdateState(
|
||
_settings,
|
||
SelectedProvider(),
|
||
_settings.LastBackupDirectory,
|
||
CaptureWindowBounds(),
|
||
CurrentBackupRetentionCount());
|
||
await _settingsService.SaveAsync(_settings);
|
||
AppendLog($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 已更新自动保留数: {CurrentBackupRetentionCount()}");
|
||
}
|
||
|
||
private async Task AddManualProviderAsync()
|
||
{
|
||
string provider = _manualProviderText.Text.Trim();
|
||
if (string.IsNullOrWhiteSpace(provider))
|
||
{
|
||
MessageBox.Show(this, "请输入要添加的 Provider ID。", Text, MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return;
|
||
}
|
||
|
||
_settings = _settingsService.AddManualProvider(_settings, provider);
|
||
await _settingsService.SaveAsync(_settings);
|
||
_manualProviderText.Clear();
|
||
ReloadProviderList();
|
||
SelectProvider(provider);
|
||
AppendLog($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 已添加手动 Provider: {provider}");
|
||
}
|
||
|
||
private async Task RemoveManualProviderAsync()
|
||
{
|
||
string? provider = SelectedProvider();
|
||
if (string.IsNullOrWhiteSpace(provider))
|
||
{
|
||
MessageBox.Show(this, "请先选择要删除的 Provider。", Text, MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return;
|
||
}
|
||
|
||
_settings = _settingsService.RemoveManualProvider(_settings, provider);
|
||
await _settingsService.SaveAsync(_settings);
|
||
ReloadProviderList();
|
||
SelectProvider(_currentStatus?.CurrentProvider.Provider);
|
||
AppendLog($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 已删除手动 Provider: {provider}");
|
||
}
|
||
|
||
private async Task ExecuteSyncOrSwitchAsync()
|
||
{
|
||
string? provider = SelectedProvider();
|
||
if (string.IsNullOrWhiteSpace(provider))
|
||
{
|
||
MessageBox.Show(this, "请先选择目标 Provider。", Text, MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return;
|
||
}
|
||
|
||
if (!ConfirmCodexClosed("执行同步前,请先关闭已打开的 Codex CLI、Codex App、app-server 和相关终端。是否继续?"))
|
||
{
|
||
return;
|
||
}
|
||
|
||
await RunBusyAsync("Executing...", async () =>
|
||
{
|
||
string codexHome = CurrentCodexHome();
|
||
int backupRetentionCount = CurrentBackupRetentionCount();
|
||
SyncResult result = _updateConfigCheck.Checked
|
||
? await Task.Run(async () => await _syncService.RunSwitchAsync(codexHome, provider, backupRetentionCount))
|
||
: await Task.Run(async () => await _syncService.RunSyncAsync(codexHome, provider: provider, keepCount: backupRetentionCount));
|
||
|
||
_settings = _settingsService.UpdateState(_settings, provider, result.BackupDir, CaptureWindowBounds(), backupRetentionCount);
|
||
await _settingsService.SaveAsync(_settings);
|
||
AppendLog($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 执行完成");
|
||
AppendLog(TextFormatter.FormatSyncResult(result, _updateConfigCheck.Checked ? "已切换并同步" : "已同步"));
|
||
AppendLog(string.Empty);
|
||
await RefreshStatusCoreAsync(codexHome);
|
||
SelectProvider(provider);
|
||
});
|
||
}
|
||
|
||
private async Task RestoreBackupAsync()
|
||
{
|
||
string backupRoot = _currentStatus?.BackupRoot ?? AppConstants.DefaultBackupRoot(CurrentCodexHome());
|
||
string initialBackupDir = Directory.Exists(_settings.LastBackupDirectory)
|
||
? _settings.LastBackupDirectory!
|
||
: backupRoot;
|
||
|
||
using FolderBrowserDialog dialog = new()
|
||
{
|
||
Description = "选择要恢复的 backup 目录",
|
||
UseDescriptionForTitle = true,
|
||
InitialDirectory = initialBackupDir,
|
||
ShowNewFolderButton = false
|
||
};
|
||
|
||
if (dialog.ShowDialog(this) != DialogResult.OK)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (!_restoreConfigCheck.Checked && !_restoreDatabaseCheck.Checked && !_restoreSessionsCheck.Checked)
|
||
{
|
||
MessageBox.Show(this, "请至少选择一种要恢复的内容。", Text, MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
return;
|
||
}
|
||
|
||
string restoreTargets = string.Join("、", new[]
|
||
{
|
||
_restoreConfigCheck.Checked ? "配置文件(config.toml)" : null,
|
||
_restoreDatabaseCheck.Checked ? "线程数据库(SQLite)" : null,
|
||
_restoreSessionsCheck.Checked ? "会话文件元数据(rollout)" : null
|
||
}.Where(static item => item is not null));
|
||
|
||
DialogResult confirm = MessageBox.Show(
|
||
this,
|
||
$"确认恢复以下备份?{Environment.NewLine}{Environment.NewLine}{dialog.SelectedPath}{Environment.NewLine}{Environment.NewLine}将覆盖当前的: {restoreTargets}。",
|
||
Text,
|
||
MessageBoxButtons.OKCancel,
|
||
MessageBoxIcon.Warning);
|
||
if (confirm != DialogResult.OK)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (!ConfirmCodexClosed("恢复 backup 前,请先关闭已打开的 Codex CLI、Codex App、app-server 和相关终端。是否继续?"))
|
||
{
|
||
return;
|
||
}
|
||
|
||
await RunBusyAsync("Restoring...", async () =>
|
||
{
|
||
string codexHome = CurrentCodexHome();
|
||
RestoreResult result = await Task.Run(async () => await _syncService.RunRestoreAsync(
|
||
codexHome,
|
||
dialog.SelectedPath,
|
||
new RestoreBackupOptions
|
||
{
|
||
RestoreConfig = _restoreConfigCheck.Checked,
|
||
RestoreDatabase = _restoreDatabaseCheck.Checked,
|
||
RestoreSessions = _restoreSessionsCheck.Checked
|
||
}));
|
||
_settings = _settingsService.UpdateState(_settings, SelectedProvider(), dialog.SelectedPath, CaptureWindowBounds(), CurrentBackupRetentionCount());
|
||
await _settingsService.SaveAsync(_settings);
|
||
AppendLog($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 恢复完成");
|
||
AppendLog(TextFormatter.FormatRestoreResult(result));
|
||
AppendLog(string.Empty);
|
||
await RefreshStatusCoreAsync(codexHome);
|
||
});
|
||
}
|
||
|
||
private void OpenBackupFolder()
|
||
{
|
||
string path = _currentStatus?.BackupRoot ?? AppConstants.DefaultBackupRoot(CurrentCodexHome());
|
||
Directory.CreateDirectory(path);
|
||
Process.Start(new ProcessStartInfo
|
||
{
|
||
FileName = path,
|
||
UseShellExecute = true
|
||
});
|
||
}
|
||
|
||
private async Task PruneBackupsAsync()
|
||
{
|
||
if (!ConfirmBackupPrune())
|
||
{
|
||
return;
|
||
}
|
||
|
||
await RunBusyAsync("Cleaning backups...", async () =>
|
||
{
|
||
string codexHome = CurrentCodexHome();
|
||
BackupPruneResult result = await Task.Run(async () => await _syncService.RunPruneBackupsAsync(codexHome, CurrentBackupRetentionCount()));
|
||
AppendLog($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 旧备份清理完成");
|
||
AppendLog(TextFormatter.FormatBackupPruneResult(result));
|
||
AppendLog(string.Empty);
|
||
await RefreshStatusCoreAsync(codexHome);
|
||
});
|
||
}
|
||
|
||
private void ReloadRecentHomes()
|
||
{
|
||
string currentText = _codexHomeCombo.Text;
|
||
_codexHomeCombo.BeginUpdate();
|
||
_codexHomeCombo.Items.Clear();
|
||
foreach (string home in _settings.RecentCodexHomes)
|
||
{
|
||
_codexHomeCombo.Items.Add(home);
|
||
}
|
||
_codexHomeCombo.EndUpdate();
|
||
if (!string.IsNullOrWhiteSpace(currentText))
|
||
{
|
||
_codexHomeCombo.Text = currentText;
|
||
}
|
||
}
|
||
|
||
private void ReloadProviderList()
|
||
{
|
||
_providerList.BeginUpdate();
|
||
_providerList.Items.Clear();
|
||
|
||
if (_currentStatus is not null)
|
||
{
|
||
foreach (ProviderOption option in _syncService.BuildProviderOptions(_currentStatus, _settings))
|
||
{
|
||
ListViewItem item = new(option.Id)
|
||
{
|
||
Tag = option.Id
|
||
};
|
||
item.SubItems.Add(TextFormatter.FormatProviderSources(option));
|
||
item.SubItems.Add(option.IsCurrentProvider ? "Yes" : string.Empty);
|
||
item.SubItems.Add(option.IsManual ? "Yes" : string.Empty);
|
||
item.SubItems.Add(option.IsSaved ? "Yes" : string.Empty);
|
||
_providerList.Items.Add(item);
|
||
}
|
||
}
|
||
|
||
_providerList.EndUpdate();
|
||
SelectProvider(_settings.LastSelectedProvider ?? _currentStatus?.CurrentProvider.Provider);
|
||
UpdateSelectionLabel();
|
||
}
|
||
|
||
private void SelectProvider(string? provider)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(provider))
|
||
{
|
||
return;
|
||
}
|
||
|
||
_providerList.SelectedItems.Clear();
|
||
foreach (ListViewItem item in _providerList.Items)
|
||
{
|
||
if (string.Equals(item.Tag as string, provider, StringComparison.Ordinal))
|
||
{
|
||
item.Selected = true;
|
||
item.Focused = true;
|
||
item.EnsureVisible();
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
private void UpdateSelectionLabel()
|
||
{
|
||
string? provider = SelectedProvider();
|
||
_selectedProviderValue.Text = string.IsNullOrWhiteSpace(provider) ? "未选择" : provider;
|
||
}
|
||
|
||
private string? SelectedProvider()
|
||
{
|
||
return _providerList.SelectedItems.Count == 0 ? null : _providerList.SelectedItems[0].Tag as string;
|
||
}
|
||
|
||
private string CurrentCodexHome()
|
||
{
|
||
string text = _codexHomeCombo.Text.Trim();
|
||
return string.IsNullOrWhiteSpace(text) ? AppConstants.DefaultCodexHome() : text;
|
||
}
|
||
|
||
private int CurrentBackupRetentionCount()
|
||
{
|
||
return Decimal.ToInt32(_backupRetentionInput.Value);
|
||
}
|
||
|
||
private void PersistUiState()
|
||
{
|
||
try
|
||
{
|
||
_settings = _settingsService.RecordCodexHome(_settings, CurrentCodexHome());
|
||
_settings = _settingsService.UpdateState(_settings, SelectedProvider(), _settings.LastBackupDirectory, CaptureWindowBounds(), CurrentBackupRetentionCount());
|
||
_settingsService.Save(_settings);
|
||
}
|
||
catch
|
||
{
|
||
// Ignore shutdown persistence failures.
|
||
}
|
||
}
|
||
|
||
private async Task RefreshStatusCoreAsync(string codexHome)
|
||
{
|
||
_currentStatus = await Task.Run(async () => await _syncService.GetStatusAsync(codexHome));
|
||
_settings = _settingsService.RecordCodexHome(_settings, _currentStatus.CodexHome);
|
||
_settings = _settingsService.MergeDetectedProviders(_settings, _syncService.ExtractDetectedProviderIds(_currentStatus));
|
||
_settings = _settingsService.UpdateState(_settings, SelectedProvider(), _settings.LastBackupDirectory, CaptureWindowBounds(), CurrentBackupRetentionCount());
|
||
await _settingsService.SaveAsync(_settings);
|
||
|
||
_statusBox.Text = TextFormatter.FormatStatus(_currentStatus);
|
||
ReloadRecentHomes();
|
||
ReloadProviderList();
|
||
_codexHomeCombo.Text = _currentStatus.CodexHome;
|
||
AppendLog($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 已刷新: {_currentStatus.CodexHome}");
|
||
}
|
||
|
||
private WindowBoundsState CaptureWindowBounds()
|
||
{
|
||
Rectangle bounds = WindowState == FormWindowState.Normal ? Bounds : RestoreBounds;
|
||
return new WindowBoundsState
|
||
{
|
||
X = bounds.X,
|
||
Y = bounds.Y,
|
||
Width = bounds.Width,
|
||
Height = bounds.Height,
|
||
Maximized = WindowState == FormWindowState.Maximized
|
||
};
|
||
}
|
||
|
||
private void ApplyWindowBounds(WindowBoundsState? bounds)
|
||
{
|
||
if (bounds is null || bounds.Width < 800 || bounds.Height < 600)
|
||
{
|
||
Size = new Size(1280, 820);
|
||
return;
|
||
}
|
||
|
||
StartPosition = FormStartPosition.Manual;
|
||
Bounds = new Rectangle(bounds.X, bounds.Y, bounds.Width, bounds.Height);
|
||
if (bounds.Maximized)
|
||
{
|
||
WindowState = FormWindowState.Maximized;
|
||
}
|
||
}
|
||
|
||
private async Task RunBusyAsync(string stateText, Func<Task> action)
|
||
{
|
||
SetBusy(true, stateText);
|
||
try
|
||
{
|
||
await action();
|
||
}
|
||
catch (Exception error)
|
||
{
|
||
AppendLog($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 错误: {error}");
|
||
MessageBox.Show(this, error.Message, Text, MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
}
|
||
finally
|
||
{
|
||
SetBusy(false, "Ready");
|
||
}
|
||
}
|
||
|
||
private void SetBusy(bool busy, string stateText)
|
||
{
|
||
UseWaitCursor = busy;
|
||
_busyLabel.Text = stateText;
|
||
_busyLabel.ForeColor = busy ? Color.DarkOrange : Color.DarkGreen;
|
||
_browseButton.Enabled = !busy;
|
||
_refreshButton.Enabled = !busy;
|
||
_addProviderButton.Enabled = !busy;
|
||
_removeProviderButton.Enabled = !busy;
|
||
_updateConfigCheck.Enabled = !busy;
|
||
_backupRetentionInput.Enabled = !busy;
|
||
_restoreConfigCheck.Enabled = !busy;
|
||
_restoreDatabaseCheck.Enabled = !busy;
|
||
_restoreSessionsCheck.Enabled = !busy;
|
||
_executeButton.Enabled = !busy;
|
||
_restoreButton.Enabled = !busy;
|
||
_openBackupButton.Enabled = !busy;
|
||
_pruneBackupsButton.Enabled = !busy;
|
||
_providerList.Enabled = !busy;
|
||
_manualProviderText.Enabled = !busy;
|
||
_codexHomeCombo.Enabled = !busy;
|
||
}
|
||
|
||
private void AppendLog(string message)
|
||
{
|
||
if (_logBox.TextLength > 0)
|
||
{
|
||
_logBox.AppendText(Environment.NewLine);
|
||
}
|
||
|
||
_logBox.AppendText(message);
|
||
_logBox.SelectionStart = _logBox.TextLength;
|
||
_logBox.ScrollToCaret();
|
||
}
|
||
|
||
private bool ConfirmCodexClosed(string message)
|
||
{
|
||
return MessageBox.Show(
|
||
this,
|
||
message,
|
||
Text,
|
||
MessageBoxButtons.OKCancel,
|
||
MessageBoxIcon.Warning) == DialogResult.OK;
|
||
}
|
||
|
||
private bool ConfirmBackupPrune()
|
||
{
|
||
string message =
|
||
$"确认清理旧备份?{Environment.NewLine}{Environment.NewLine}" +
|
||
$"将只保留最近 {CurrentBackupRetentionCount()} 份受本工具管理的备份。{Environment.NewLine}" +
|
||
"被删除的旧备份无法直接恢复。";
|
||
|
||
return MessageBox.Show(
|
||
this,
|
||
message,
|
||
Text,
|
||
MessageBoxButtons.OKCancel,
|
||
MessageBoxIcon.Warning) == DialogResult.OK;
|
||
}
|
||
}
|