Repository Wiki
BeyondDimension/SteamTools

账号切换(GameAccount)

账号切换(GameAccount)是 Watt Toolkit(SteamTools)的多平台游戏账号快速切换插件。它将"退出当前账号 → 切换登录凭据 → 重新拉起平台进程"这一整套繁琐操作封装为一键切换,并支持为每个账号创建带自定义 URL 协议的桌面快捷方式,实现"双击即登录"。

Purpose and Scope

本页覆盖 GameAccount 插件端到端能力:

  • 插件装配与生命周期(Plugin、DI 注册、配置注册、ViewModel 预热)
  • 平台切换器的接口契约 IPlatformSwitcher 及其两个注册实现(BasicPlatformSwitcher、SteamPlatformSwitcher)
  • 设置持久化(GameAccountSettings_ / IPartialGameAccountSettings / AccountRemarks)
  • Windows 平台的 URL 协议注册与登录快捷方式生成

有意留给兄弟页面的内容:

  • 插件框架本身的加载、MEF 导出与启动流程 → 见 Plugin Framework 相关页面
  • 平台账号实体(IAccount、PlatformAccount)与 Steam 登录数据读取的底层细节 → 属于平台服务层
  • 主程序设置系统(IOptionsMonitor 管线)的整体设计 → 见 Settings 相关页面

Overview

Watt Toolkit 是一个插件化客户端,每个功能以独立程序集(BD.WTTS.Client.Plugins.GameAccount)形式存在。GameAccount 插件的职责边界非常清晰:

职责承载者
在主窗口注入"用户快速切换"菜单页Plugin.GetMenuTabItems() → GameAccountPage
为每个游戏平台提供账号切换实现IPlatformSwitcher 的多实现注册
保存账号别名(备注)等用户偏好GameAccountSettings_(JSON 持久化)
让操作系统识别 steam:// 类自定义协议并唤起客户端CreateSystemProtocol(Windows 注册表)
为单账号生成一键登录桌面快捷方式CreateLoginShortcut(经 IPC 委托给系统服务)

插件描述原文很好地概括了产品定位:"可支持自行添加多平台账号快速切换功能,Steam 可自动读取账号信息,其它平台请手动添加账号信息"。也就是说 Steam 平台走自动化路径(SteamPlatformSwitcher,可枚举本机已登录用户),其他平台走手动录入路径(BasicPlatformSwitcher)。

关键常量

插件身份在全局常量文件中定义,用于插件去重与稳定标识:

csharp
public const string GameAccount = "GameAccount";

Source: AssemblyInfo.Constants.cs

csharp
public const string GameAccountId = "00000000-0000-0000-0000-000000000002";

Source: AssemblyInfo.Constants.cs

Architecture

Loading diagram...

架构要点解读:

  1. 插件与宿主解耦:Plugin 通过 MEF [CompositionExport(typeof(IPlugin))] 被宿主发现(仅在桌面平台条件编译下生效,见 #if (WINDOWS || MACCATALYST || MACOS || LINUX) && !(IOS || ANDROID)),移动端不会加载此插件。
  2. 多实现策略模式:ConfigureRequiredServices 向容器注册两个 IPlatformSwitcher 单例。调用方(ViewModel)以 IEnumerable<IPlatformSwitcher> 形式注入全部实现,按 PlatformAccount 分发到对应切换器 —— 新增平台支持无需修改既有代码。
  3. 设置走主程序管线:插件不自建存储,而是把 GameAccountSettings_ 交给宿主的配置系统(IOptionsMonitor<T> + JSON 文件),再以 IPartialGameAccountSettings 接口暴露"部分设置"视图,避免插件直接依赖完整设置类型。
  4. 系统能力下沉到 IPC:创建 .lnk 快捷方式这类需要 COM/系统 API 的操作不在插件进程内完成,而是经 IPlatformService.IPCRoot 委托,保证插件层纯托管、可测试。

插件装配与生命周期

Plugin 类是插件的唯一装配入口,完整实现如下:

csharp
1public sealed class Plugin : PluginBase<Plugin>, IPlugin 2{ 3 const string moduleName = AssemblyInfo.GameAccount; 4 5 public override Guid Id => Guid.Parse(AssemblyInfo.GameAccountId); 6 7 public sealed override string Name => Strings.UserFastChange; 8 9 public sealed override string UniqueEnglishName => moduleName; 10 11 public sealed override string Description => "可支持自行添加多平台账号快速切换功能,Steam 可自动读取账号信息,其它平台请手动添加账号信息"; 12 13 protected sealed override string? AuthorOriginalString => null; 14 15 public sealed override object? Icon => Resources.userswitcher; 16}

Source: Plugin.cs

生命周期各阶段按宿主调用顺序:

1. 菜单项注入 — GetMenuTabItems()

csharp
1public override IEnumerable<MenuTabItemViewModel>? GetMenuTabItems() 2{ 3 yield return new MenuTabItemViewModel(this, nameof(Strings.UserFastChange)) 4 { 5 PageType = typeof(GameAccountPage), 6 IsResourceGet = true, 7 IconKey = Icon, 8 }; 9}

Source: Plugin.cs

使用 yield return 延迟生成菜单项;IsResourceGet = true 表示标题 Strings.UserFastChange 需要走本地化资源解析而非字面字符串,保证多语言下菜单名正确。

2. 服务注册 — ConfigureRequiredServices(IServiceCollection, Startup)

csharp
1public override void ConfigureRequiredServices(IServiceCollection services, Startup startup) 2{ 3 services.AddSingleton<IPartialGameAccountSettings>(s => 4 s.GetRequiredService<IOptionsMonitor<GameAccountSettings_>>().CurrentValue); 5 6 services.AddSingleton<IPlatformSwitcher, BasicPlatformSwitcher>() 7 .AddSingleton<IPlatformSwitcher, SteamPlatformSwitcher>(); 8}

Source: Plugin.cs

两个值得注意的设计意图:

  • IPartialGameAccountSettings 用工厂委托而非直接注册类型:它桥接到 IOptionsMonitor<GameAccountSettings_> 的 CurrentValue。这是"部分设置(Partial Settings)"模式 —— 主程序通过 src/BD.WTTS.Client/Settings/Abstractions/IPartialGameAccountSettings.cs 暴露插件设置的子集,插件自身(如 ChangeUserRemark 的默认实现)可以读写该设置而不需要引用整个设置模型。
  • 连缀两次 AddSingleton<IPlatformSwitcher, ...>:DI 容器允许多次注册同一接口。任何注入 IEnumerable<IPlatformSwitcher> 的消费者都会拿到两个实例,按平台特征自行挑选执行者。

3. 配置注册 — GetConfiguration(bool)

csharp
1public override IEnumerable<(Action<IServiceCollection>? @delegate, bool isInvalid, string name)>? GetConfiguration(bool directoryExists) 2{ 3 yield return GetConfiguration<GameAccountSettings_>(directoryExists); 4}

Source: Plugin.cs

directoryExists 参数允许宿主在配置目录尚不存在时跳过或创建配置;isInvalid 标志用于上报加载失败的配置项。

4. ViewModel 预热 — OnInitializeAsync()

csharp
1public override ValueTask OnInitializeAsync() 2{ 3 IViewModelManager.Instance.Get<GameAccountPageViewModel>(); 4 return default; 5}

Source: Plugin.cs

在插件初始化阶段就通过 IViewModelManager.Instance.Get<T>() 强制构造 GameAccountPageViewModel。WHY:账号列表(尤其 Steam 的本地登录用户枚举)是 I/O 密集操作,提前在启动期完成数据加载,用户首次点开页面时无需等待,避免 UI 首帧空白。GameAccountPageViewModel 拆分为 GameAccountPageViewModel.cs 与 GameAccountPageViewModel.props.cs 两个部分类文件,后者承载属性通知逻辑。

平台切换器契约 — IPlatformSwitcher

这是整个插件的核心抽象。接口方法全部围绕"账号切换生命周期"组织:

csharp
1public interface IPlatformSwitcher 2{ 3 ValueTask<bool> SwapToAccount(IAccount? account, PlatformAccount platform); 4 ValueTask<bool> ClearCurrentLoginUser(PlatformAccount platform); 5 ValueTask<bool> KillPlatformProcess(PlatformAccount platform); 6 bool RunPlatformProcess(PlatformAccount platform, bool isAdmin); 7 ValueTask NewUserLogin(PlatformAccount platform); 8 ValueTask<bool> CurrnetUserAdd(string name, PlatformAccount platform); 9 string GetCurrentAccountId(PlatformAccount platform); 10 bool SetPlatformPath(PlatformAccount platform); 11 Task<bool> DeleteAccountInfo(IAccount account, PlatformAccount platform); 12 Task<IEnumerable<IAccount>?> GetUsers(PlatformAccount platform, Action? refreshUsers = null); 13}

Source: IPlatformSwitcher.cs

按职责分组理解这 10 个方法:

分组方法说明
切换主流程SwapToAccount切到指定账号(account 可为 null 表示切到"无账号/访客"态)
会话清理ClearCurrentLoginUser、KillPlatformProcess清空当前登录态、终止平台进程
进程管理RunPlatformProcess(platform, isAdmin)以(可选)管理员权限拉起平台客户端;同步返回 bool
账号枚举GetUsers(platform, refreshUsers)列出平台已知账号;refreshUsers 回调用于触发外部刷新(如重读本地数据后通知 UI)
账号增删NewUserLogin、CurrnetUserAdd(name, platform)、DeleteAccountInfo新登录、手动添加当前用户、删除账号信息
环境探测GetCurrentAccountId、SetPlatformPath读取当前登录账号 ID、配置平台安装路径

注意:方法名 CurrnetUserAdd 中的拼写(Currnet)是源码中的真实拼写,文档保持原样,二次开发时请照抄接口名。

接口默认实现(C# Default Interface Methods)

接口为三个方法提供了默认实现,这是该插件的重要设计 —— 通用逻辑上收到契约层,避免 BasicPlatformSwitcher 与 SteamPlatformSwitcher 重复编写:

ChangeUserRemark — 账号别名持久化

csharp
1void ChangeUserRemark(IAccount account) 2{ 3 if (!string.IsNullOrEmpty(account.AccountId)) 4 GameAccountSettings.AccountRemarks.Add($"{account.PlatformName}-{account.AccountId}", account.AliasName); 5 else 6 Toast.Show(ToastIcon.Error, AppResources.Error_AccountIdIsEmpty); 7}

Source: IPlatformSwitcher.cs

存储键为复合键 "{PlatformName}-{AccountId}",值为用户自定义别名 AliasName。WHY:复合键天然隔离不同平台的同名 AccountId,且查询时无需二次过滤。当 AccountId 为空时以 Toast 向用户报错 Error_AccountIdIsEmpty 而非抛异常,保持 UI 层无崩溃。GameAccountSettings.AccountRemarks 静态访问器来自 using static BD.WTTS.Settings.Abstractions.IGameAccountSettings;,是设置模型上的一个静态字典视图。

CreateSystemProtocol — Windows URL 协议注册

csharp
1public void CreateSystemProtocol(string targetPath) 2{ 3#if WINDOWS 4 using var key = Registry.ClassesRoot.CreateSubKey(Constants.CUSTOM_URL_SCHEME_NAME); 5 key.SetValue("URL Protocol", ""); 6 using var shellKey = key.CreateSubKey("shell"); 7 using RegistryKey openKey = shellKey.CreateSubKey("open"); 8 using RegistryKey commandKey = openKey.CreateSubKey("command"); 9 commandKey.SetValue("", "\"" + targetPath + "\" \"%1\""); 10#endif 11}

Source: IPlatformSwitcher.cs

该段在注册表 HKEY_CLASSES_ROOT\<CUSTOM_URL_SCHEME_NAME> 下构建标准的 URL Protocol 结构:shell\open\command 的默认值被写成 "targetPath" "%1"。WHY:注册后,操作系统会把形如 scheme://... 的链接启动请求转发给 Watt Toolkit 可执行文件,并把完整 URL 作为第一个参数(%1)传入 —— 这正是"一键登录快捷方式"能工作的前提。所有 RegistryKey 均用 using 释放句柄;非 Windows 平台下方法体为空(编译期裁剪),保证跨平台可编译。

CreateLoginShortcut — 快捷方式创建(IPC 下沉)

csharp
1public async Task<bool> CreateLoginShortcut( 2 string pathLink, 3 string targetPath, 4 string? arguments, 5 string? description, 6 string? hotkey, 7 string? iconLocation, 8 string? workingDirectory, 9 CancellationToken cancellationToken = default) 10{ 11#if WINDOWS 12 var s = await IPlatformService.IPCRoot.Instance; 13 s.CreateShortcut(pathLink, targetPath, arguments, description, hotkey, iconLocation, workingDirectory); 14 return true; 15#else 16 await Task.CompletedTask; 17 return false; 18#endif 19}

Source: IPlatformSwitcher.cs

参数与 Windows IShellLink 的 .lnk 字段一一对应(目标、参数、描述、热键、图标、工作目录)。注意两点:

  • 委托而非自实现:真正的 COM 调用发生在 IPlatformService.IPCRoot 暴露的系统服务中。插件层只持有可空参数与调用语义,把平台差异(如需要提升权限的 COM 初始化)隔离在宿主侧。
  • 非 Windows 直接返回 false:调用方可据此禁用相关 UI 按钮,而不是靠异常传递失败。
  • 虽然签名中有 cancellationToken,当前实现未消费它(快捷方式创建是快速本地操作)。

核心切换流程

一键切换的完整时序(以 Steam 账号 A → 账号 B 为例):

Loading diagram...

流程顺序的设计意图:先杀进程再写凭据。若在平台进程存活时改写登录数据,会被进程回写覆盖或因文件锁失败,因此 KillPlatformProcess 必须先于凭据写入执行;随后 RunPlatformProcess 以新身份拉起客户端。快捷方式链路则把"协议注册"与"快捷方式生成"拆为两步,前者只需做一次(系统级),后者可按账号任意生成多个。

设置与数据模型

GameAccountSettings_ 与 JSON Source Generation

csharp
[JsonSourceGenerationOptions(WriteIndented = true, IgnoreReadOnlyProperties = true)] [JsonSerializable(typeof(GameAccountSettings_))] internal partial class GameAccountSettingsContext : JsonSerializerContext

Source: GameAccountSettings.cs

设置模型通过 JsonSerializerContext 源生成器完成序列化:WriteIndented = true 保证配置文件人类可读、便于用户手工修改;IgnoreReadOnlyProperties = true 避免只读属性参与序列化。相比反射式序列化,源生成在 AOT/裁剪(Trimming)场景下是必需的,这正是 Watt Toolkit 多平台发布(含移动端 AOT)所需。

设置文件组织

文件角色
Settings/Abstractions/IGameAccountSettings.BaseType.cs设置基础类型契约(分部接口)
Settings/Abstractions/IGameAccountSettings.cs设置契约(含 AccountRemarks 静态视图)
Settings/GameAccountSettings.cs具体实现 GameAccountSettings_ + GameAccountSettingsContext
src/BD.WTTS.Client/Settings/Abstractions/IPartialGameAccountSettings.cs主程序侧的"部分设置"接口,桥接插件设置到主程序设置管线

IAccount 相关的核心字段(从代码使用处可确认):AccountId、AliasName、PlatformName,三者共同构成账号的身份与显示信息,其中 PlatformName-AccountId 是持久化别名字典的键。

API Reference

IPlatformSwitcher 方法一览

方法签名要点返回 / 行为
SwapToAccount(IAccount? account, PlatformAccount platform)ValueTask<bool>;切换成功返回 true
ClearCurrentLoginUser(PlatformAccount platform)ValueTask<bool>;清空当前登录态
KillPlatformProcess(PlatformAccount platform)ValueTask<bool>;终止平台进程
RunPlatformProcess(PlatformAccount platform, bool isAdmin)bool(同步);isAdmin 决定是否提权启动
NewUserLogin(PlatformAccount platform)ValueTask;发起新用户登录
CurrnetUserAdd(string name, PlatformAccount platform)ValueTask<bool>;手动添加当前用户
GetCurrentAccountId(PlatformAccount platform)string;当前登录账号 ID
ChangeUserRemark(IAccount account) — 接口默认实现void;写 AccountRemarks,AccountId 为空时 Toast 报错
SetPlatformPath(PlatformAccount platform)bool;设置平台安装路径
DeleteAccountInfo(IAccount account, PlatformAccount platform)Task<bool>;删除账号信息
GetUsers(PlatformAccount platform, Action? refreshUsers = null)Task<IEnumerable<IAccount>?>;可传刷新回调
CreateSystemProtocol(string targetPath) — 默认实现void;仅 Windows 写注册表
CreateLoginShortcut(string pathLink, string targetPath, string? arguments, string? description, string? hotkey, string? iconLocation, string? workingDirectory, CancellationToken cancellationToken = default) — 默认实现Task<bool>;Windows 经 IPC 创建 .lnk,其他平台 false

Source: IPlatformSwitcher.cs

Plugin 生命周期 API

成员类型 / 签名说明
IdGuid(只读属性重写)固定为 00000000-0000-0000-0000-000000000002
Namestring本地化名 Strings.UserFastChange
UniqueEnglishNamestring"GameAccount"
Descriptionstring中文功能描述
Iconobject?Resources.userswitcher 资源
GetMenuTabItems()IEnumerable<MenuTabItemViewModel>?注入主菜单页
ConfigureRequiredServices(IServiceCollection, Startup)voidDI 注册
GetConfiguration(bool)IEnumerable<(delegate, isInvalid, name)>?配置文件注册
OnInitializeAsync()ValueTask预热 ViewModel

Failure Modes、边界与并发

依据源码可确认的行为:

  • 空 AccountId 防御:ChangeUserRemark 在 AccountId 为空时用 Toast.Show(ToastIcon.Error, ...) 提示并静默跳过持久化,不会抛异常导致页面崩溃。
  • 平台裁剪:CreateSystemProtocol 与 CreateLoginShortcut 均以 #if WINDOWS 包裹实现体。非 Windows 下前者为空操作、后者直接返回 false,调用方应以返回值驱动 UI 可用性。
  • 进程-凭据竞态:切换流程必须先 KillPlatformProcess 再写入登录数据(见核心流程),否则会被存活进程覆盖/锁定 —— 实现新平台切换器时务必保持此顺序。
  • 注册表写入权限:CreateSystemProtocol 写 Registry.ClassesRoot(HKCR)。在未提权环境下可能因权限不足抛异常,宿主需在调用前保证以管理员身份运行或捕获异常降级。
  • IPC 失败路径:CreateLoginShortcut 依赖 IPlatformService.IPCRoot.Instance 的可用性;当前实现未捕获 IPC 异常,若 IPC 服务不可用异常会向上传播到调用方。
  • 设置热更新:IPartialGameAccountSettings 注册为 IOptionsMonitor<GameAccountSettings_>.CurrentValue 的快照。若配置文件被外部修改,IOptionsMonitor 会触发重载,消费方读取的是重载后的值。

扩展点:新增平台切换器

为新的游戏平台(如 Origin、Uplay 等)添加账号切换的推荐路径:

  1. 新建 XxxPlatformSwitcher : IPlatformSwitcher(或派生自 BasicPlatformSwitcher 复用通用逻辑),实现 10 个核心方法;
  2. 在 Plugin.ConfigureRequiredServices 中追加一行 services.AddSingleton<IPlatformSwitcher, XxxPlatformSwitcher>();
  3. 复用接口默认实现 ChangeUserRemark / CreateSystemProtocol / CreateLoginShortcut 即可获得别名持久化与一键登录快捷方式能力,无需重写。

得益于多实现注册 + IEnumerable<IPlatformSwitcher> 分发的设计,新增平台不需要修改 ViewModel 或任何既有切换器 —— 这是典型的开闭原则落地。

Sources

(2 files)
src/BD.WTTS.Client.Plugins.GameAccount/Plugins
src/BD.WTTS.Client.Plugins.GameAccount/Services