账号切换(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)。
关键常量
插件身份在全局常量文件中定义,用于插件去重与稳定标识:
public const string GameAccount = "GameAccount";Source: AssemblyInfo.Constants.cs
public const string GameAccountId = "00000000-0000-0000-0000-000000000002";Source: AssemblyInfo.Constants.cs
Architecture
架构要点解读:
- 插件与宿主解耦:
Plugin通过 MEF[CompositionExport(typeof(IPlugin))]被宿主发现(仅在桌面平台条件编译下生效,见#if (WINDOWS || MACCATALYST || MACOS || LINUX) && !(IOS || ANDROID)),移动端不会加载此插件。 - 多实现策略模式:
ConfigureRequiredServices向容器注册两个IPlatformSwitcher单例。调用方(ViewModel)以IEnumerable<IPlatformSwitcher>形式注入全部实现,按PlatformAccount分发到对应切换器 —— 新增平台支持无需修改既有代码。 - 设置走主程序管线:插件不自建存储,而是把
GameAccountSettings_交给宿主的配置系统(IOptionsMonitor<T>+ JSON 文件),再以IPartialGameAccountSettings接口暴露"部分设置"视图,避免插件直接依赖完整设置类型。 - 系统能力下沉到 IPC:创建
.lnk快捷方式这类需要 COM/系统 API 的操作不在插件进程内完成,而是经IPlatformService.IPCRoot委托,保证插件层纯托管、可测试。
插件装配与生命周期
Plugin 类是插件的唯一装配入口,完整实现如下:
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()
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)
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)
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()
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
这是整个插件的核心抽象。接口方法全部围绕"账号切换生命周期"组织:
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 — 账号别名持久化
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 协议注册
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 下沉)
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 为例):
流程顺序的设计意图:先杀进程再写凭据。若在平台进程存活时改写登录数据,会被进程回写覆盖或因文件锁失败,因此 KillPlatformProcess 必须先于凭据写入执行;随后 RunPlatformProcess 以新身份拉起客户端。快捷方式链路则把"协议注册"与"快捷方式生成"拆为两步,前者只需做一次(系统级),后者可按账号任意生成多个。
设置与数据模型
GameAccountSettings_ 与 JSON Source Generation
[JsonSourceGenerationOptions(WriteIndented = true, IgnoreReadOnlyProperties = true)]
[JsonSerializable(typeof(GameAccountSettings_))]
internal partial class GameAccountSettingsContext : JsonSerializerContextSource: 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
| 成员 | 类型 / 签名 | 说明 |
|---|---|---|
Id | Guid(只读属性重写) | 固定为 00000000-0000-0000-0000-000000000002 |
Name | string | 本地化名 Strings.UserFastChange |
UniqueEnglishName | string | "GameAccount" |
Description | string | 中文功能描述 |
Icon | object? | Resources.userswitcher 资源 |
GetMenuTabItems() | IEnumerable<MenuTabItemViewModel>? | 注入主菜单页 |
ConfigureRequiredServices(IServiceCollection, Startup) | void | DI 注册 |
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 等)添加账号切换的推荐路径:
- 新建
XxxPlatformSwitcher : IPlatformSwitcher(或派生自BasicPlatformSwitcher复用通用逻辑),实现 10 个核心方法; - 在
Plugin.ConfigureRequiredServices中追加一行services.AddSingleton<IPlatformSwitcher, XxxPlatformSwitcher>(); - 复用接口默认实现
ChangeUserRemark/CreateSystemProtocol/CreateLoginShortcut即可获得别名持久化与一键登录快捷方式能力,无需重写。
得益于多实现注册 + IEnumerable<IPlatformSwitcher> 分发的设计,新增平台不需要修改 ViewModel 或任何既有切换器 —— 这是典型的开闭原则落地。