库存挂卡与 ASF 集成(SteamIdleCard / ArchiSteamFarmPlus)
WattToolkit(SteamTools)通过两个独立的桌面端插件实现「库存挂卡」与「ASF(ArchiSteamFarm)集成」两大能力:BD.WTTS.Client.Plugins.SteamIdleCard 提供 Steam 游戏快速挂卡(掉落集换式卡牌)功能,BD.WTTS.Client.Plugins.ArchiSteamFarmPlus 提供内嵌 ASF 控制台功能实现。两者均遵循宿主的 PluginBase<T> / IPlugin 插件契约,由 PluginsCore 统一发现、注册与排序。
Purpose and Scope
本页覆盖以下内容,并以真实源码为依据逐层展开:
- SteamIdleCard 插件:插件标识与元数据、菜单页注册、按需服务注入(
AddSteamAccountService/AddSteamIdleCardService)、配置模型ISteamIdleSettings的全部选项与默认值,以及IdleCardPageViewModel中"启动/停止挂卡"的真实控制流(前置校验链)。 - ArchiSteamFarmPlus 插件:插件标识与元数据、
startup.HasSteam门控下的AddArchiSteamFarmService注入、为 ASF(ArchiSteamFarm.Web.WebBrowser)定制HttpMessageHandler的CreateHttpHandler设计。 - 两个插件在宿主中的注册与排序:
PluginsCore中的插件清单与菜单排序权重(ArchiSteamFarmPlus=65、SteamIdleCard=75),以及AssemblyInfo.Constants中的模块名与固定 GUID。
不在本页展开、留给兄弟页面的主题:
- 插件系统本身的发现/加载机制(MEF
CompositionExport、PluginBase<T>生命周期细节)——属于宿主插件框架主题。 ISteamIdleCardService/ISteamAccountService在 BD.SteamClient 服务层的具体实现(挂卡调度算法、Steam Web API 调用细节)——属于 Steam 客户端服务层主题。- ASF 本体(ArchiSteamFarm 库)内部的挂卡机器人逻辑——属于上游开源库,本页只覆盖 WattToolkit 对它的宿主集成层。
Overview
「库存挂卡」指让 Steam 客户端"模拟游玩"仍可掉落集换式卡牌的游戏,从而刷完剩余掉落卡数(TotalCardsRemaining → DroppedCardsCount)。WattToolkit 的实现思路是:
- 依赖本机 Steam 客户端:插件描述明确"需要启动 Steam 客户端并登录对应账号",VM 在启动挂卡前会连续校验 Steam 进程、客户端登录态、Web 登录态以及 SteamId 一致性(见下文 Core Flow)。
- Web 会话与客户端会话双轨:通过
ISteamSessionService+ISecureStorage持久化 Web 登录态(CurrentSteamUserKey),同时通过SteamConnectService.Current读取客户端登录用户。 - 算法可配置:运行规则(
IdleRule,默认FastMode)、运行顺序(IdleSequentital)、并行游戏数、切换间隔、黑名单等全部由源生成的设置接口ISteamIdleSettings暴露,改动即时生效(VM 订阅了IdleRule/IdleSequentital变更)。
「ASF 集成」则是把 ArchiSteamFarm 以进程内方式接入 WattToolkit:插件在宿主检测到 Steam 环境(startup.HasSteam)时注册 AddArchiSteamFarmService,并为 ASF 的 WebBrowser 提供统一代理/Cookie/连接数策略的 SocketsHttpHandler,使 ASF 的网络行为与 WattToolkit 全局代理设置一致。
Architecture
上图要点(均可在源码中定位):
- 两个插件类都继承
PluginBase<Plugin>并实现IPlugin,通过[CompositionExport(typeof(IPlugin))](仅桌面平台编译)导出给宿主插件系统。 PluginsCore负责把两个插件纳入插件集合并决定菜单顺序:Authenticator => 55, ArchiSteamFarmPlus => 65, SteamIdleCard => 75,即界面上 ASF 页排在挂卡页之前。- 挂卡插件自己不实现挂卡算法,而是通过
ConfigureDemandServices注入ISteamAccountService与ISteamIdleCardService(实现位于 BD.SteamClient 服务层),ViewModel 通过ISteamIdleCardService.Instance等单例入口消费。 - ASF 插件的核心价值在宿主适配层:菜单页
MainFramePage承载 ASF 控制台 UI,CreateHttpHandler为 ASF 统一网络策略。
插件注册与元数据
模块名与固定 ID
两个插件的身份都来自全局常量文件,保证跨版本稳定:
public const string ArchiSteamFarmPlus = "ArchiSteamFarmPlus";Source: AssemblyInfo.Constants.cs
public const string ArchiSteamFarmPlusId = "00000000-0000-0000-0000-000000000004";Source: AssemblyInfo.Constants.cs
SteamIdleCard 与 SteamIdleCardId 遵循同一模式定义在同一常量文件中(挂卡插件同样通过 AssemblyInfo.SteamIdleCard / AssemblyInfo.SteamIdleCardId 读取,见下文 Plugin.cs)。
SteamIdleCard 插件入口
1#if (WINDOWS || MACCATALYST || MACOS || LINUX) && !(IOS || ANDROID)
2[CompositionExport(typeof(IPlugin))]
3#endif
4public sealed class Plugin : PluginBase<Plugin>, IPlugin
5{
6 const string moduleName = AssemblyInfo.SteamIdleCard;
7
8 public override Guid Id => Guid.Parse(AssemblyInfo.SteamIdleCardId);
9
10 public override string Name => Strings.SteamIdleCard;
11
12 public sealed override string UniqueEnglishName => moduleName;
13
14 public sealed override string Description => "Steam 游戏快速挂卡,需要启动 Steam 客户端并登录对应账号,支持多种算法逻辑挂卡,掉卡速度更快。";
15
16 public sealed override object? Icon => Resources.card;Source: Plugin.cs
设计意图:
- 平台条件编译:
CompositionExport仅在桌面端(Windows/macOS/Linux,排除 iOS/Android)生效——挂卡功能强依赖本机 Steam 客户端,移动端没有意义。 UniqueEnglishName固定为模块名常量:作为插件目录/配置的稳定键,不受本地化显示名影响。Description直接说明运行前提("需要启动 Steam 客户端并登录对应账号"),与后文 VM 的前置校验链一一对应。
ArchiSteamFarmPlus 插件入口
1public sealed class Plugin : PluginBase<Plugin>, IPlugin
2{
3 const string moduleName = AssemblyInfo.ArchiSteamFarmPlus;
4
5 public override Guid Id => Guid.Parse(AssemblyInfo.ArchiSteamFarmPlusId);
6
7 public override string Name => BDStrings.ArchiSteamFarmPlus;
8
9 public sealed override string UniqueEnglishName => moduleName;
10
11 public sealed override string Description => moduleName + " 控制台功能实现";
12
13 public sealed override object? Icon => Resources.asf;
14
15 public override IEnumerable<MenuTabItemViewModel>? GetMenuTabItems()
16 {
17 yield return new MenuTabItemViewModel(this, nameof(BDStrings.ArchiSteamFarmPlus))
18 {
19 PageType = typeof(MainFramePage),
20 IsResourceGet = true,
21 IconKey = Icon
22 };
23 }Source: Plugin.cs
ASF 插件只注册一个菜单标签页 MainFramePage——ASF 的全部功能都在该页面承载的"控制台"中呈现,插件本身不再拆分子页面。
Core Flow:启动挂卡的真实控制流
IdleCardPageViewModel.IdleRunStartOrStop_Click() 是"开始/停止挂卡"按钮的真实入口。启动前依次通过四道校验,任一失败即以 Toast 提示并中断:
1/// <summary>
2/// 启动或停止挂卡
3/// </summary>
4public async Task IdleRunStartOrStop_Click()
5{
6 if (!SteamTool.IsRunningSteamProcess)
7 {
8 Toast.Show(ToastIcon.Warning, Strings.SteamNotRuning);
9 return;
10 }
11
12 if (!SteamConnectService.Current.IsConnectToSteam) // 是否登录 Steam 客户端
13 {
14 Toast.Show(ToastIcon.Warning, Strings.Idle_NeedLoginSteam);
15 return;
16 }
17
18 if (!IsLogin)
19 {
20 IsLoaing = true;
21 if (!await LoginSteam()) // 登录 Steam Web
22 {
23 IsLoaing = false;
24 Toast.Show(ToastIcon.Warning, Strings.Idle_NeedLoginSteam);
25 return;
26 }
27 else
28 IsLoaing = false;
29 }
30
31 if (!RunLoaingState)
32 {
33 RunLoaingState = true;
34
35 if (!RunState)
36 {
37 TracepointHelper.TrackEvent("IdleCardRun");
38
39 if (SteamLoginState.Success && SteamLoginState.SteamId != (ulong?)SteamConnectService.Current.CurrentSteamUser?.SteamId64)Source: IdleCardPageViewModel.cs
校验链的设计意图(为什么按这个顺序):
- 进程存在 > 客户端已登录 > Web 会话可用 > 账号一致性。前两道是廉价的本机检查(
ISteamService.IsRunningSteamProcess、SteamConnectService.Current.IsConnectToSteam),失败时无需任何网络请求即可快速失败;只有全部通过才去尝试LoginSteam()建 Web 会话。 - 双重身份体系:Steam 客户端用户(
SteamConnectService.Current.CurrentSteamUser?.SteamId64)与 Web 会话用户(SteamLoginState.SteamId)必须一致,否则挂卡调度会作用于错误账号。 - 遥测埋点:
TracepointHelper.TrackEvent("IdleCardRun")记录启动事件,用于统计该功能使用情况。 RunLoaingState/RunState两个状态标志把"加载中"与"运行中"分开,避免在切换瞬间重复触发。
登录与注销的会话生命周期
构造函数中的 LoginSteamCommand 同时承担登录与注销两条路径;注销时显式清理安全存储中的会话并从会话服务移除:
1else //注销登录
2{
3 await ISecureStorage.Instance.RemoveAsync(ISteamSessionService.CurrentSteamUserKey);
4 steamSession.RemoveSession(SteamLoginState.SteamId.ToString());
5 SteamLoginState = new();
6 IsLogin = false;
7}Source: IdleCardPageViewModel.cs
登录成功后立即 LoadBadges() 拉取徽章数据并 SteamAppsSort() 排序,使页面首屏即有可挂卡列表(见构造函数 L36-L54)。
设置变更的即时响应
1SteamIdleSettings.IdleRule.Subscribe(async _ => { await IdleRuleChange(); }, false);
2SteamIdleSettings.IdleSequentital.Subscribe(async _ => { await IdleSequentitalChance(); }, false);
3
4this.WhenAnyValue(x => x.TotalCardsRemaining, y => y.DroppedCardsCount)
5 .Subscribe(_ =>
6 {
7 this.RaisePropertyChanged(nameof(DropCardsCount));
8 });Source: IdleCardPageViewModel.cs
用户在设置面板切换运行规则/顺序时无需重启挂卡任务:设置项是响应式的(源生成设置接口的 Subscribe 能力),VM 立刻重算调度策略;DropCardsCount 则是 TotalCardsRemaining - DroppedCardsCount 派生属性,通过 WhenAnyValue 联动刷新。
时序图:开始挂卡
数据模型与配置
ISteamIdleSettings(源生成配置接口)
配置由包 BD.Common.Settings.V4.SourceGenerator.Tools 源生成,接口与默认值成对出现,VM 直接静态订阅:
1public partial interface ISteamIdleSettings
2{
3 static ISteamIdleSettings? Instance
4 => Ioc.Get_Nullable<IOptionsMonitor<ISteamIdleSettings>>()?.CurrentValue;
5
6 /// <summary>
7 /// 挂卡状态更新时间
8 /// </summary>
9 TimeSpan IdleTime { get; set; }
10
11 /// <summary>
12 /// 运行规则
13 /// </summary>
14 IdleRule IdleRule { get; set; }
15
16 /// <summary>
17 /// 运行顺序
18 /// </summary>
19 IdleSequentital IdleSequentital { get; set; }
20
21 /// <summary>
22 /// 最大并行运行游戏数量
23 /// </summary>
24 int MaxIdleCount { get; set; }
25
26 /// <summary>
27 /// 最少游戏时间 hours
28 /// </summary>
29 double MinRunTime { get; set; }
30
31 /// <summary>
32 /// 自动切换游戏时间间隔 ms
33 /// </summary>
34 double SwitchTime { get; set; }
35
36 /// <summary>
37 /// 自动刷新徽章数据时间间隔 min
38 /// </summary>
39 double RefreshBadgesTime { get; set; }
40
41 /// <summary>
42 /// 挂卡黑名单游戏列表
43 /// </summary>
44 Dictionary<uint, string?>? BlacklistAppList { get; set; }Source: ISteamIdleSettings.cs
配置选项一览(含默认值)
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
IdleTime | TimeSpan | TimeSpan.FromMinutes(6) | 挂卡状态更新时间 |
IdleRule | IdleRule | IdleRule.FastMode | 运行规则(默认快速模式) |
IdleSequentital | IdleSequentital | IdleSequentital.Default | 运行顺序 |
MaxIdleCount | int | 30 | 最大并行运行游戏数量 |
MinRunTime | double | 2 | 最少游戏时间(小时) |
SwitchTime | double | 5000 | 自动切换游戏时间间隔(毫秒) |
RefreshBadgesTime | double | 6 | 自动刷新徽章数据时间间隔(分钟) |
BlacklistAppList | Dictionary<uint, string?>? | [] | 挂卡黑名单游戏列表(appid → 游戏名) |
默认值定义见 ISteamIdleSettings.cs(
DefaultIdleTime~DefaultBlacklistAppList)。
设计意图说明:
MaxIdleCount = 30而非 1:Steam 允许同一账号同时"游玩"少量游戏,WattToolBar 放开到 30 并配合SwitchTime(5s)轮转,是其"掉卡速度更快"卖点在配置层的体现;用户可通过黑名单与MinRunTime收紧行为。RefreshBadgesTime单位是分钟、SwitchTime单位是毫秒:两者作用层次不同(徽章全量数据 vs 单游戏切换),源码注释以中文显式标注单位避免误用。BlacklistAppList键为uint(appid):与IdleApp模型及ToggleBlacklistIdle命令的入参类型一致,直接可做集合运算。
服务注入
挂卡插件通过 ConfigureDemandServices 注册两个服务,且都传入带 CookieContainer 的 SocketsHttpHandler 工厂,保证会话 Cookie 复用:
1public override void ConfigureDemandServices(IServiceCollection services, Startup startup)
2{
3 services.AddSteamAccountService(c => new SocketsHttpHandler() { CookieContainer = c });
4 services.AddSteamIdleCardService(c => new SocketsHttpHandler() { CookieContainer = c });
5}
6
7public override IEnumerable<(Action<IServiceCollection>? @delegate, bool isInvalid, string name)>? GetConfiguration(bool directoryExists)
8{
9 yield return GetConfiguration<SteamIdleSettings_>(directoryExists);
10}Source: Plugin.cs
ASF 插件的服务注册则被 startup.HasSteam 门控:
1public override void ConfigureDemandServices(IServiceCollection services, Startup startup)
2{
3 if (startup.HasSteam)
4 {
5 // ASF Service
6 services.AddArchiSteamFarmService();
7 }
8}Source: Plugin.cs
只有在宿主检测到 Steam 环境时才加载 ASF 服务——ASF 的所有机器人/IPC 依赖都比较重,按需加载可避免无关环境白白付出启动成本。
ArchiSteamFarmPlus 的网络适配层
ASF 插件为 ArchiSteamFarm.Web.WebBrowser 提供了统一的 HttpMessageHandler 构造函数 CreateHttpHandler,使 ASF 的 HTTP 行为与 WattToolkit 的全局代理、Cookie 与连接上限策略保持一致:
1/// <summary>
2/// 用于 <see cref="ArchiSteamFarm"/> 的 <see cref="HttpMessageHandler"/>
3/// </summary>
4static HttpMessageHandler? CreateHttpHandler(CreateHttpHandlerArgs args)
5{
6 var proxy = args.Item4;
7 var useProxy = GeneralHttpClientFactory.UseWebProxy(proxy);
8 var setMaxConnectionsPerServer = !(args.Item5 < 1); // https://github.com/dotnet/runtime/blob/v6.0.0/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandler.cs#L157
9#if NETCOREAPP2_1_OR_GREATER
10 {
11 var handler = new SocketsHttpHandler()
12 {
13 AllowAutoRedirect = args.Item1,
14 AutomaticDecompression = args.Item2,
15 CookieContainer = args.Item3,
16 };
17 if (useProxy)
18 {
19 handler.Proxy = proxy;
20 handler.UseProxy = true;
21 }
22 if (setMaxConnectionsPerServer)
23 {
24 handler.MaxConnectionsPerServer = args.Item5;
25 }
26 return handler;
27 }Source: Plugin.cs
参数通过源文件头部的 CreateHttpHandlerArgs 元组类型传入(bool AllowAutoRedirect、DecompressionMethods、CookieContainer、IWebProxy?、int MaxConnectionsPerServer,见 Plugin.cs)。
设计意图:
setMaxConnectionsPerServer = !(args.Item5 < 1):当上游未指定连接上限(0 或负数)时不设置MaxConnectionsPerServer,让 .NET 使用其内部默认值;源码内直接注释指向 dotnet/runtime 的实现行,说明作者精确对齐了运行时默认行为,避免无谓地限制 ASF 的并发抓取能力。- 代理复用
GeneralHttpClientFactory.UseWebProxy:与 WattToolkit 其它网络组件共用同一代理判定逻辑,用户在 WattToolkit 里配置的加速/代理对 ASF 同样生效。 - Android 分支:同文件后续有
#elif ANDROID分支走PlatformHttpMessageBuilder.CreateAndroidClientHandler,保持移动端网络栈平台原生(该分支属于桌面插件编译组合的防御性代码)。 - 目前该委托在
ConfigureRequiredServices中处于注释状态(//ArchiSteamFarm.Web.WebBrowser.CreateHttpHandlerDelegate = CreateHttpHandler;,见 Plugin.cs),属于预留的接入点——实现保留但未启用。
宿主中的注册与排序
PluginsCore 决定两个插件的加载集合与菜单顺序:
GameList,
ArchiSteamFarmPlus,
Authenticator,Source: PluginsCore.cs
Authenticator => 55,
ArchiSteamFarmPlus => 65,
SteamIdleCard => 75,Source: PluginsCore.cs
排序权重直接决定侧边菜单顺序:Authenticator(55) → ArchiSteamFarmPlus(65) → SteamIdleCard(75)。两个插件都通过 InternalsVisibleTo 获得访问宿主内部类型的权限(InternalsVisibleTo.cs),并在设计时宿主 BD.WTTS.Client.Avalonia.Designer.HostApp 的 .csproj 中被引用(BD.WTTS.Client.Avalonia.Designer.HostApp.csproj)以支持 XAML 预览。
ViewModel 命令一览(IdleCardPageViewModel)
| 命令 | 参数 | 语义 |
|---|---|---|
LoginSteamCommand | – | 首次进入自动执行;登录或注销 Web 会话 |
IdleRunStartOrStop | – | 启动/停止挂卡(前置校验链见 Core Flow) |
PriorityRunIdle | IdleApp | 立即优先挂指定游戏 |
ToggleBlacklistIdle | IdleApp | 将游戏加入/移出挂卡黑名单(写入 BlacklistAppList) |
IdleManualRunNext | – | 手动触发切换到下一游戏 |
NavAppToSteamViewCommand | uint appid | 打开 Steam 商店页(STEAM_NAVGAME_URL) |
OpenLinkUrlCommand | string url | 系统浏览器打开链接 |
VM 依赖注入方式为服务单例直取(ISteamService.Instance、ISteamIdleCardService.Instance、ISteamSessionService.Instance),并用 AsyncLock 字段串行化并发触发的异步操作(IdleCardPageViewModel.cs)。
Failure Modes、边界与并发
- Steam 进程未运行:
IsRunningSteamProcess == false→ ToastSteamNotRuning,不做任何网络调用(快速失败)。 - 客户端未登录:
IsConnectToSteam == false→ ToastIdle_NeedLoginSteam。 - Web 会话缺失/失效:自动尝试
LoginSteam();失败同样 Toast 提示并中止。 - 账号不匹配:
SteamLoginState.SteamId != CurrentSteamUser?.SteamId64时不启动调度(客户端登录的账号与 Web 会话账号必须是同一人)。 - 重复触发/加载态:
IsLoaing与RunLoaingState双状态防止登录或启动过程被并发重入;页面首次激活(Activation+IsFirstActivation)自动执行LoginSteamCommand。 - 注销路径的清理:显式
RemoveAsync(CurrentSteamUserKey)+RemoveSession(...),避免残留会话导致的账号串扰。 - 设置热切换:
IdleRule/IdleSequentital变更即时生效,无需停止任务。
Performance & Operations
- 遥测:
TracepointHelper.TrackEvent("IdleCardRun")上报启动事件(IdleCardPageViewModel.cs)。 - 轮转与刷新节奏可调:
SwitchTime(5s 默认)控制单游戏切换,RefreshBadgesTime(6min 默认)控制徽章全量刷新,IdleTime(6min 默认)控制状态更新。 - Cookie 复用:两个 Steam 服务共享注入的
CookieContainer,减少重复登录/2FA 挑战。 - ASF 按需加载:
startup.HasSteam门控降低无 Steam 环境的启动开销。
Extension Points
- 新增运行规则/顺序:扩展
Enums/IdleRule.cs/Enums/IdleSequentital.cs并在IdleRuleChange()/IdleSequentitalChance()中处理新分支(实现于服务层与IdleCardPageViewModel.props.cs)。 - ASF 网络策略接管:启用
ArchiSteamFarm.Web.WebBrowser.CreateHttpHandlerDelegate = CreateHttpHandler(当前被注释,属预留钩子)。 - 黑名单扩展:
BlacklistAppList为Dictionary<uint, string?>,可直接由ToggleBlacklistIdle命令驱动的 UI 扩展持久化黑名单。
Related Links
- AssemblyInfo.Constants.cs / #L146 — 模块名与插件 GUID 常量
- PluginsCore.cs / #L499 — 插件集合与菜单排序
- BD.WTTS.Client.Plugins.SteamIdleCard/Plugins/Plugin.cs — 挂卡插件入口
- BD.WTTS.Client.Plugins.SteamIdleCard/Settings/Abstractions/ISteamIdleSettings.cs — 挂卡配置契约
- BD.WTTS.Client.Plugins.SteamIdleCard/UI/ViewModels/IdleCardPageViewModel.cs — 挂卡页 VM 与控制流
- BD.WTTS.Client.Plugins.ArchiSteamFarmPlus/Plugins/Plugin.cs — ASF 插件入口与网络适配