Repository Wiki
BeyondDimension/SteamTools

库存挂卡与 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 的实现思路是:

  1. 依赖本机 Steam 客户端:插件描述明确"需要启动 Steam 客户端并登录对应账号",VM 在启动挂卡前会连续校验 Steam 进程、客户端登录态、Web 登录态以及 SteamId 一致性(见下文 Core Flow)。
  2. Web 会话与客户端会话双轨:通过 ISteamSessionService + ISecureStorage 持久化 Web 登录态(CurrentSteamUserKey),同时通过 SteamConnectService.Current 读取客户端登录用户。
  3. 算法可配置:运行规则(IdleRule,默认 FastMode)、运行顺序(IdleSequentital)、并行游戏数、切换间隔、黑名单等全部由源生成的设置接口 ISteamIdleSettings 暴露,改动即时生效(VM 订阅了 IdleRule / IdleSequentital 变更)。

「ASF 集成」则是把 ArchiSteamFarm 以进程内方式接入 WattToolkit:插件在宿主检测到 Steam 环境(startup.HasSteam)时注册 AddArchiSteamFarmService,并为 ASF 的 WebBrowser 提供统一代理/Cookie/连接数策略的 SocketsHttpHandler,使 ASF 的网络行为与 WattToolkit 全局代理设置一致。

Architecture

Loading diagram...

上图要点(均可在源码中定位):

  • 两个插件类都继承 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

两个插件的身份都来自全局常量文件,保证跨版本稳定:

csharp
public const string ArchiSteamFarmPlus = "ArchiSteamFarmPlus";

Source: AssemblyInfo.Constants.cs

csharp
public const string ArchiSteamFarmPlusId = "00000000-0000-0000-0000-000000000004";

Source: AssemblyInfo.Constants.cs

SteamIdleCard 与 SteamIdleCardId 遵循同一模式定义在同一常量文件中(挂卡插件同样通过 AssemblyInfo.SteamIdleCard / AssemblyInfo.SteamIdleCardId 读取,见下文 Plugin.cs)。

SteamIdleCard 插件入口

csharp
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 插件入口

csharp
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 提示并中断:

csharp
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

校验链的设计意图(为什么按这个顺序):

  1. 进程存在 > 客户端已登录 > Web 会话可用 > 账号一致性。前两道是廉价的本机检查(ISteamService.IsRunningSteamProcess、SteamConnectService.Current.IsConnectToSteam),失败时无需任何网络请求即可快速失败;只有全部通过才去尝试 LoginSteam() 建 Web 会话。
  2. 双重身份体系:Steam 客户端用户(SteamConnectService.Current.CurrentSteamUser?.SteamId64)与 Web 会话用户(SteamLoginState.SteamId)必须一致,否则挂卡调度会作用于错误账号。
  3. 遥测埋点:TracepointHelper.TrackEvent("IdleCardRun") 记录启动事件,用于统计该功能使用情况。
  4. RunLoaingState / RunState 两个状态标志把"加载中"与"运行中"分开,避免在切换瞬间重复触发。

登录与注销的会话生命周期

构造函数中的 LoginSteamCommand 同时承担登录与注销两条路径;注销时显式清理安全存储中的会话并从会话服务移除:

csharp
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)。

设置变更的即时响应

csharp
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 联动刷新。

时序图:开始挂卡

Loading diagram...

数据模型与配置

ISteamIdleSettings(源生成配置接口)

配置由包 BD.Common.Settings.V4.SourceGenerator.Tools 源生成,接口与默认值成对出现,VM 直接静态订阅:

csharp
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

配置选项一览(含默认值)

选项类型默认值说明
IdleTimeTimeSpanTimeSpan.FromMinutes(6)挂卡状态更新时间
IdleRuleIdleRuleIdleRule.FastMode运行规则(默认快速模式)
IdleSequentitalIdleSequentitalIdleSequentital.Default运行顺序
MaxIdleCountint30最大并行运行游戏数量
MinRunTimedouble2最少游戏时间(小时)
SwitchTimedouble5000自动切换游戏时间间隔(毫秒)
RefreshBadgesTimedouble6自动刷新徽章数据时间间隔(分钟)
BlacklistAppListDictionary<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 复用:

csharp
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 门控:

csharp
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 与连接上限策略保持一致:

csharp
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 决定两个插件的加载集合与菜单顺序:

csharp
GameList, ArchiSteamFarmPlus, Authenticator,

Source: PluginsCore.cs

csharp
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)
PriorityRunIdleIdleApp立即优先挂指定游戏
ToggleBlacklistIdleIdleApp将游戏加入/移出挂卡黑名单(写入 BlacklistAppList)
IdleManualRunNext–手动触发切换到下一游戏
NavAppToSteamViewCommanduint appid打开 Steam 商店页(STEAM_NAVGAME_URL)
OpenLinkUrlCommandstring url系统浏览器打开链接

定义见 IdleCardPageViewModel.cs。

VM 依赖注入方式为服务单例直取(ISteamService.Instance、ISteamIdleCardService.Instance、ISteamSessionService.Instance),并用 AsyncLock 字段串行化并发触发的异步操作(IdleCardPageViewModel.cs)。

Failure Modes、边界与并发

  • Steam 进程未运行:IsRunningSteamProcess == false → Toast SteamNotRuning,不做任何网络调用(快速失败)。
  • 客户端未登录:IsConnectToSteam == false → Toast Idle_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 扩展持久化黑名单。

Sources

(4 files)
src/BD.WTTS.Client.Plugins.ArchiSteamFarmPlus/Plugins
src/BD.WTTS.Client.Plugins.SteamIdleCard/Plugins
src/BD.WTTS.Client.Plugins.SteamIdleCard/Settings/Abstractions
src/BD.WTTS.Client.Plugins.SteamIdleCard/UI/ViewModels