Repository navigation
Conversation
…nto CN # Conflicts: # src/XIVLauncher/Windows/ViewModel/Main/Flows/GameInjectionFlow.cs
AtmoOmen
left a comment
There was a problem hiding this comment.
请勿让代理配置绕开原有的配置体系,目前整个代理系统都游离于其他系统之外。
|
配置已并入主配置、入口已移入 VM、保存即生效 |
AtmoOmen
left a comment
There was a problem hiding this comment.
- 不要包含“西红柿炒鸡蛋(没有红烧肉版)”式注释。
- 测试内容没有什么意义,请删除。
…apshot 拆分独立文件并精简注释
There was a problem hiding this comment.
逻辑错误
- 下拉框条目名不会随"显示名称"修改刷新。
ProxySettingsWindowViewModel.cs中ProxySelectItem是 record,DisplayName是只读计算属性:
public sealed record ProxySelectItem(ProxyProfile? Profile)
{
public string DisplayName =>
Profile is null ? "不使用代理" : Profile.DisplayName;
}record 不实现 INotifyPropertyChanged,而 RebuildSelection() 只在构造函数、CreateProfile()、DeleteSelectedProfile() 里被调用。XAML 用 DisplayMemberPath="DisplayName" 绑定 SelectionItems,因此用户改完"显示名称"以后,条目下拉框仍显示旧名字,只有再新增或删除条目才会刷新。
ProxyConfigSnapshot.IsDisabled里与ToSnapshot()重复的一半判断不可达。ProxyProfile.ToSnapshot()已经做了同等校验:
if (ProxyType == ProxyType.None)
return null;
var host = ProxyHost.Trim();
if (string.IsNullOrWhiteSpace(host) || ProxyPort is < 1 or > 65535)
return null;而 ProxyConfigSnapshot 又写了一遍:
public bool IsDisabled =>
string.IsNullOrWhiteSpace(Host)
|| Port is < 1 or > 65535
|| string.IsNullOrWhiteSpace(Type)
|| string.Equals(Type, "None", StringComparison.OrdinalIgnoreCase);在这次改动的全部调用路径中,ProxyConfigSnapshot 只由 ProxyProfile.ToSnapshot() 构造,所以 Host 为空、端口越界、Type 为空或为 "None" 四种情况都已经被上一层拦掉,这几个分支不会成立。
- 密码解密失败的清理写了两遍,其中一遍必然不生效。
ProxyProfile.GetPassword()在 catch 中直接清空字段:
catch
{
ProxyPasswordEncrypted = string.Empty;
return null;
}LauncherSettingsV3.SanitizeProxySettings() 再判一次并再清一次:
if (!string.IsNullOrWhiteSpace(profile.ProxyPasswordEncrypted) && profile.GetPassword() == null)
{
Log.Warning("[LauncherSettingsV3] 代理密码解密失败, 已清空密码字段: {ProfileName}", profile.DisplayName);
profile.ProxyPasswordEncrypted = string.Empty;
}条件里的 profile.GetPassword() 求值时已经把 ProxyPasswordEncrypted 清空,紧接着的赋值不会产生任何变化,这段实际只起到记日志的作用。
-
GamePatchMetadataClient与GamePatchInstaller从new HttpClient()换成显式SocketsHttpHandler后,Cookie 处理语义变了。 原先private readonly HttpClient client = new();内部用的是HttpClientHandler,其UseCookies默认为true;换成new SocketsHttpHandler { UseProxy = true, Proxy = ... }后,SocketsHttpHandler.CookieContainer默认为null,不再处理Set-Cookie。这两个类只 GETSdoInfos.CLIENT_ALL_FILES_LIST_URL与SdoInfos.REMOTE_VERSION_URL,不使用 Cookie,所以这次改动没有实际后果;同一次改动中的另外两处则显式保留了该语义——Launcher.MockHttpClient用new HttpClientHandler { UseCookies = true, Proxy = ... },LoginChannelContext保留了CookieContainer。 -
ConfirmButton_OnClick里Close()是多余的。
DialogResult = true;
Close();给 DialogResult 赋值本身就会关闭窗口。
- 代理对象与
HttpClient之间存在时序耦合,但没有约束保证。XLProxyProvider.Current是可变的静态属性,只在StartupOrchestrator.InitializeSettingsAsync中被写一次:
XLProxyProvider.Apply(context.Settings.ProxySettings.ToSnapshot());所有 HttpClient 都必须在这次调用之后构造才能拿到代理。但 GameFileDownloader、GamePatchMetadataClient、GamePatchInstaller 仍是字段初始化器 Proxy = XLProxyProvider.Current。目前这几处对象都在登录、打补丁阶段才创建,MainWindowViewModel.cs 第 70 行的 Launcher = new(); 也在启动流程之后,所以当前没有失效路径;一旦有 HttpClient 在 InitializeSettingsAsync 之前构造,就会静默退回直连。
-
XLProxyProvider.BuildWebProxy声明为public,但改动中没有本类以外的调用点,只有Apply内部调用它。 -
"不使用代理"存在两条独立表达路径。
SelectionItems[0]是ProxySelectItem(null)(显示"不使用代理"),ProxyTypeOptions里也有一项new(ProxyType.None, "不使用代理")。前者走Save里Settings.ActiveProfileId = profile?.Id置 null,后者走ProxyProfile.ToSnapshot()返回 null。两条路径最终结果相同,但界面上同时存在两个名为"不使用代理"的选项。
界面绘制与项目风格的一致性
- 窗口基类与同类设置窗口不同。
ProxySettingsWindow.xaml是普通<Window>,只有系统标题栏;项目里的设置类窗口都是components:ChromeWindow加components:TitleBar:SettingsWindow.xaml、AdvancedSettingsWindow.xaml(第 1、33 行)、ChangelogWindow.xaml、CompanionAppSetupWindow.xaml、AccountDeviceProfileWindow.xaml。ChromeWindow.cs的注释写明了它的作用:
/// Base window that removes the visible OS frame while keeping the native
/// caption style. Windows therefore still plays its own show/hide/minimize/
/// maximize animation (no custom, flash-prone opacity animation is needed),
/// and the window keeps Aero Snap, native resizing, the DWM drop shadow and
/// Windows 11 rounded corners.
新窗口反而采用了注释里被点名的旧做法:<Window.Triggers><EventTrigger RoutedEvent="Window.Loaded"><BeginStoryboard Storyboard="{StaticResource WindowLoadAnimation}" />,配合 Grid x:Name="WindowAnimationLayer" Opacity="0"。项目里保留这套写法的是 CaptchaInputWindow、GameClientFileTaskWindow、LoadingDialog 这类任务对话框,而不是从设置窗口进入的子窗口。
-
卡片参数与相邻窗口不一致。 新窗口用
materialDesign:Card Margin="0,0,0,16" UniformCornerRadius="8"加<StackPanel Margin="20">;AccountDeviceProfileWindow.xaml用UniformCornerRadius="12" ClipContent="True" Background="{DynamicResource MaterialDesign.Brush.Surface}" materialDesign:ElevationAssist.Elevation="Dp2"加<StackPanel Margin="24,20">;SettingsWindow.xaml的SettingCardStyle是UniformCornerRadius=8、Background=Surface、Elevation=Dp0、Padding=18。 -
标题区比例失衡。 新窗口的头部是:
<materialDesign:PackIcon Kind="Network" Width="36" Height="36" VerticalAlignment="Center"
Foreground="{DynamicResource MaterialDesign.Brush.Primary}"
Margin="0,0,16,0" />
<TextBlock Foreground="{DynamicResource MaterialDesign.Brush.Foreground}"
Text="网络代理" VerticalAlignment="Center" FontWeight="SemiBold" />TextBlock 没有绑定文本样式,字号继承窗口上的 TextElement.FontSize="14"。AdvancedSettingsWindow.xaml 的同类头部是 32×32 图标加 Style="{StaticResource MaterialDesignHeadline5TextBlock}"。
-
按钮与底部操作栏没有复用邻近窗口的参数。 新窗口的"新增/删除"直接用
Style="{StaticResource MaterialDesignOutlinedButton}",AccountDeviceProfileWindow.xaml对应位置用CardButtonStyle(materialDesign:ButtonAssist.CornerRadius=6、Height=36、Padding=14,0)。底部操作栏Padding="24,16",AccountDeviceProfileWindow.xaml是Padding="28,16"。 -
详情表单同时用了外置标签和框内 Hint。 例如:
<TextBlock Text="代理主机" Foreground="{DynamicResource MaterialDesign.Brush.ForegroundLight}" />
<TextBox Margin="0,4,0,16"
Text="{Binding SelectedProfile.ProxyHost, UpdateSourceTrigger=PropertyChanged}"
materialDesign:HintAssist.Hint="例如 127.0.0.1"
Style="{StaticResource MaterialDesignOutlinedTextBox}" />AccountDeviceProfileWindow.xaml 的同类字段(设备 ID、MAC 地址、主机名等)只有 HintAssist.Hint,没有外置标签。
DarkIntegerUpDownStyle是逐条复制过来的。 新窗口内的样式与AccountDeviceProfileWindow.xaml第 88-105 行的同名样式在 8 个Setter(Background、Foreground、BorderBrush、BorderThickness、Height、FontSize、Padding)和IsEnabled触发器上完全一致。同时端口控件又在元素上写了Padding="4",覆盖掉样式里的Padding="10,0":
<xctk:IntegerUpDown Margin="0,4,0,16" Width="140" Minimum="1" Maximum="65535"
Value="{Binding SelectedProfile.ProxyPort, UpdateSourceTrigger=PropertyChanged}"
Style="{StaticResource DarkIntegerUpDownStyle}"
Padding="4" />交互与用户心智
-
设置入口落在"账号安全"分组里。 新增项插在
SettingsWindow.xaml第 594-608 行,位置上属于第 527 行的Text="账号安全"卡片(下一张卡片标题"伴随程序"在第 600 行)。该卡片原有内容是RequireDeviceProfileSetupForNewAccountLogin、RequireDeviceProfileSetupForQRCodeLogin与"共享设备信息",都是设备信息相关项。 -
密码要重新输入才能保住,切条目就丢。 窗口 code-behind 订阅
PropertyChanged,切换条目时清空密码框:
viewModel.PropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(ProxySettingsWindowViewModel.SelectedItem))
ProxyPasswordBox.Password = string.Empty;
};密码只在 ConfirmButton_OnClick 里通过 ViewModel.Save(ProxyPasswordBox.Password) 落进条目,且 Save 只在输入非空时调用 SetPassword。因此用户输入密码后切到另一条看一眼再切回来,输入内容已经没了,此时 hint 会显示 PasswordHint 的"先设置"分支,即"未设置"。
-
"清除密码"与"未设置"提示词。
PasswordHint在没有已存密码时返回"未设置",这个字符串被当作materialDesign:HintAssist.Hint使用,表达的是状态而不是输入提示;有密码时返回"留空保持当前密码",才是输入提示。两种语义共用一个位置。 -
窗口里直接展示配置文件完整路径。
ProxySettingsWindowViewModel把ConfigFilePath = Paths.GetConfigPath()传给界面,ProxySettingsWindow.xaml用<TextBlock Text="{Binding ConfigFilePath}" Opacity="0.58" TextWrapping="Wrap" />显示,紧邻的下一行又用文字说明"配置随启动器配置保存在 Roaming 目录"。用户在同一屏里同时看到绝对路径和"Roaming 目录"这类表述。 -
改名后下拉框不更新(同逻辑问题第 1 条的使用侧表现)。 用户在"显示名称"里改成新名字,条目下拉框仍显示旧名字,只有点"新增"或"删除"才会刷新。
-
提交方式只有"确定"。 修改不落盘的唯一路径是关闭窗口,依赖窗口构造函数里的深拷贝:
var settings = App.Settings.ProxySettings.DeepClone();
var viewModel = new ProxySettingsWindowViewModel(settings);
DataContext = viewModel;AdvancedSettingsWindow 同样只有"确认"按钮,这一点一致;差别在于删除条目有 CustomMessageBox 二次确认,清除密码没有。
重复造轮子
XLHttpClientFactory新增的代理参数只有一个调用点,另有三处手写了同类 handler。 这次改动给工厂加了参数:
public static HttpClient Create
(
TimeSpan connectTimeout,
int maxConnectionsPerServer,
DecompressionMethods automaticDecompression,
IWebProxy? proxy = null
)全项目 7 处 XLHttpClientFactory.Create 调用(XLHttpClientFileDownloader、NetworkEnvironmentService、DotNetRuntimeManager、HttpClientWithProgress、DalamudAssetManager、DalamudUpdater、LoginArea)中只有 LoginArea 传了 XLProxyProvider.Current。而 GameFileDownloader、GamePatchMetadataClient、GamePatchInstaller 各自展开了一份 new SocketsHttpHandler { UseProxy = true, Proxy = XLProxyProvider.Current }。工厂里那份 handler 还额外配置了 ConnectTimeout、MaxConnectionsPerServer、EnableMultipleHttp2Connections、KeepAlivePingDelay/Timeout/Policy、PooledConnectionLifetime、PooledConnectionIdleTimeout、Expect100ContinueTimeout、ResponseDrainTimeout,并套了一层 Http11FallbackHandler,手写的三处都没有这些。DCTravelClient、LoginChannelContext、Launcher 用的是 HttpClientHandler(需要 CookieContainer),工厂不覆盖这种情形。
- 条目管理界面整块照搬了
AccountDeviceProfileWindow的"设备信息预设"卡片。 相同的materialDesign:PackIcon Kind="FormatListBulleted"标题行、相同的右上"新增/删除"按钮组(Kind="Plus"、Kind="DeleteOutline")、相同的ComboBox(HintAssist.Hint="选择…") + 16 宽间隔 + TextBox(Hint="显示名称")双列布局:
<ColumnDefinition Width="*" />
<ColumnDefinition Width="16" />
<ColumnDefinition Width="*" />AccountDeviceProfileWindow.xaml 第 225-246 行是同一段结构,只是 Hint 文案是"选择预设"。详情卡片的做法也一样:<StackPanel Margin="20" IsEnabled="{Binding HasSelectedProfile}"> 对应 AccountDeviceProfileWindow.xaml 的 <StackPanel Margin="24,20" IsEnabled="{Binding CanEditDeviceDetails}">。
-
DarkIntegerUpDownStyle重复定义第二份(详见风格一节第 6 条),位置在ProxySettingsWindow.xaml的窗口资源里,而Xaml/Components/下已有共享资源字典可放这类样式。 -
密码加解密另起了一套。
ProxyProfile用 DPAPI 自建:
private static readonly byte[] PasswordEntropy = "XIVLauncherCN.ProxyPassword"u8.ToArray();
private static string EncryptPassword(string plainPassword) =>
Convert.ToBase64String(ProtectedData.Protect(Encoding.UTF8.GetBytes(plainPassword), PasswordEntropy, DataProtectionScope.CurrentUser));项目里已有两套加解密实现:XIVLauncher.Account/Cred/EncryptionHelper.cs(Aegis256 + Argon2id,EncryptString/DecryptString)与 ICredProvider 体系(CredentialManager 基于 Windows 凭据管理器、WindowsHello)。代理密码不能直接套用后者的一个客观原因是加载顺序:StartupOrchestrator.cs 第 129 行 LauncherSettingsV3.Load(...) 触发 SanitizeProxySettings() 解密,第 164 行才执行 context.AccountManager.InitializeCredProviderAsync(...),凭据提供者在代理密码需要解密时尚未初始化。
- 代理类型在两处各定义一次,中间靠字符串往返。
ProxyType枚举在XIVLauncher/Settings/ProxyType.cs,ProxyConfigSnapshot.Type是string,由ProxyType.ToString()得到,再在XLProxyProvider.BuildWebProxy里映射回来:
var scheme = config.Type.Trim().ToLowerInvariant() switch
{
"http" => "http",
"https" => "https",
"socks5" => "socks5",
_ => null
};ProxyConfigSnapshot.IsDisabled 里判断 string.Equals(Type, "None", ...),ProxySettingsWindowViewModel 里又有一份 ProxyTypeOptions("不使用代理"、"HTTP"、"HTTPS"、"SOCKS5")。
ProxySettings.DeepClone() 用 JsonSerializer.Serialize/Deserialize 默认选项做深拷贝,而 LauncherSettingsV3 有自己的 JsonOptions(IncludeFields = true、PropertyNameCaseInsensitive = true、WriteIndented = true),两处序列化配置并存。
No description provided.