前言 完整项目地址: https://github.com/chr233/XinjingdailyBot 因为项目之前是使用运行时反射来完成服务的动态注册，而源生成器是在编译前完成代码生成并参与编译，完全没有运行时的性能开销 运行时反射 - 旧的方案 标记特性 首先创建一个 Attribute, 用来标注需要扫描的服务类 namespace XinjingdailyBot.Infrastructure.Attribute; /// /// 标记服务 /// 1、如果服务是本身 直接在类上使用[AppService] /// 2、如果服务是接口 在类上使用 [AppService(ServiceType = typeof(实现接口))] /// [AttributeUsage(AttributeTargets.Class, Inherited = false)] public sealed class AppServiceAttribute : System.Attribute { /// /// 服务声明周期 /// 不给默认值的话注册的是AddSingleton /// public LifeTime ServiceLifetime { get; set; } = LifeTime.Scoped; /// /// 指定服务类型 /// public Type? ServiceType { get; set; } /// /// 是否可以从第一个接口获取服务类型 /// public bool InterfaceServiceType { get; set; } /// /// 标记服务 /// /// public AppServiceAttribute(LifeTime serviceLifetime) { ServiceLifetime = serviceLifetime; } /// /// 标记服务 /// /// /// public AppServiceAttribute(Type? serviceType, LifeTime serviceLifetime) { ServiceLifetime = serviceLifetime; ServiceType = serviceType; } /// /// 标记服务 /// /// /// /// public AppServiceAttribute(Type? serviceType, LifeTime serviceLifetime, bool interfaceServiceType) { ServiceLifetime = serviceLifetime; ServiceType = serviceType; InterfaceServiceType = interfaceServiceType; } } /// /// 生命周期 /// public enum LifeTime { /// /// 瞬时 /// Transient, /// /// 范围 /// Scoped, /// /// 单例 /// Singleton } 基于反射的服务扫描器 然后再主项目启动的时候使用反射扫描添加了该 Attribute 的所有 Class 并注册服务 using System.Diagnostics.CodeAnalysis; using System.Reflection; using XinjingdailyBot.Infrastructure.Attribute; namespace XinjingdailyBot.WebAPI.Extensions; /// /// 动态注册服务扩展 /// public static class AppServiceExtensions { private static readonly NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger(); /// /// 注册引用程序域中所有有AppService标记的类的服务 /// /// [RequiresUnreferencedCode("不兼容剪裁")] public static void AddAppService(this IServiceCollection services) { var cls = new string[] { "XinjingdailyBot.Repository", "XinjingdailyBot.Service", "XinjingdailyBot.Command", "XinjingdailyBot.WebAPI", }; foreach (var item in cls) { var assembly = Assembly.Load(item); Register(services, assembly); } } /// /// 动态注册服务 /// /// /// [RequiresUnreferencedCode("不兼容剪裁")] private static void Register(IServiceCollection services, Assembly assembly) { string? name = assembly.GetName().Name; _logger.Debug($"===== 注册 {name} 中的服务 ====="); uint count = 0; foreach (var type in assembly.GetTypes()) { var serviceAttribute = type.GetCustomAttribute<AppServiceAttribute>(); if (serviceAttribute != null) { count++; var serviceType = serviceAttribute.ServiceType; //情况1 适用于依赖抽象编程，注意这里只获取第一个 if (serviceType == null && serviceAttribute.InterfaceServiceType) { serviceType = type.GetInterfaces().FirstOrDefault(); } //情况2 不常见特殊情况下才会指定ServiceType，写起来麻烦 if (serviceType == null) { serviceType = type; } var lifetime = serviceAttribute.ServiceLifetime; switch (lifetime) { case LifeTime.Singleton: services.AddSingleton(serviceType, type); break; case LifeTime.Scoped: services.AddScoped(serviceType, type); break; case LifeTime.Transient: services.AddTransient(serviceType, type); break; default: services.AddTransient(serviceType, type); break; } _logger.Debug($"{lifetime} - {serviceType}"); } } _logger.Debug($"===== 注册了 {count} 个服务 ====="); } } 也不是不能用，就是感觉不够完美，而且因为使用了反射的相关特性，无法简单地启用程序集裁剪和应用 AOT，所有就想到利用 .Net 5.0 新加入的源生成器特性来实现相同的功能，避免使用反射特性和降低运行时的额外开销 源生成器 - 新的方案 源生成器官方文档 因为我的项目是按照命名空间分成了几个子项目，而源生成器只能分析当前项目的语法树，如果用文件 IO 的方式读取本地 .cs 文件也是可以实现的，但是会显示警告，因为官方并不推荐在源生成器中使用文件 IO，这会降低编译速度，而且服务类也不是经常新增，每次编译都扫描一遍也是完全没必要的 综上，我的解决方案是写一个 Powershell 脚本来扫描解决方案中的所有服务，并保存到一个文件中，然后在源生成器中读取该文件来生成服务注册的代码 服务扫描脚本 首先是服务扫描脚本 scan_service.ps1 # # @Author : Chr_ # @Date : 2024-01-18 14:25:15 # @LastEditors : Chr_ # @LastEditTime : 2024-01-18 20:15:18 # @Description : 生成依赖服务信息 # # 工作目录 $workDir = (Get-Location).Path; $outputPath = Join-Path -Path (Join-Path -Path $workDir -ChildPath "XinjingdailyBot.WebAPI") -ChildPath "Properties"; # 包名列表 $libNames = "XinjingdailyBot.Model", "XinjingdailyBot.Repository", "XinjingdailyBot.Interface", "XinjingdailyBot.Service", "XinjingdailyBot.Tasks", "XinjingdailyBot.Command"; # 输出路径 $appServicePath = Join-Path -Path $outputPath -ChildPath "appService.json"; # 扫描源文件列表 function GetSourceFileList { param ( [string]$Path ) $result = @(); # 去除 bin obj 目录 $dirs = Get-ChildItem -Path $Path -Directory -Exclude "bin", "obj" # 遍历所有 .cs 文件 foreach ($dir in $dirs) { $files = Get-ChildItem -Path $dir -Filter "*.cs" -File -Recurse # 遍历子目录文件 foreach ($file in $files) { $result += $file } } $files = Get-ChildItem -Path $Path -Filter "*.cs" -File # 遍历所有 .cs 文件 foreach ($file in $files) { $result += $file } Write-Output $result $result; } # 读取命名空间 function GetNamespace { param ( [string] $Content ) if ($Content -match "namespace\s+([^;{]+)") { $Matches[1]; } } # 读取类名/接口名 function GetClassName { param ( [string] $Content ) if ($Content -match "(?:class|interface|record)\s+([^({:\s]+)") { $Matches[1]; } } # 扫描AppServiceAttribute function ScanAppServiceAttribute { param ( [string] $Content ) if ($Content -match "\[AppService(?:Attribute)?\(([^\]]+)\)\]") { $result = @{} $arguments = $Matches[1].Split(","); foreach ($argument in $arguments) { if ($argument -match "LifeTime\.(Singleton|Transient|Scoped)") { $result["LifeTime"] = $Matches[1]; } elseif ($argument -match "typeof\(([^()]+)\)") { $result["Interface"] = $Matches[1]; } elseif ($argument -match "true|false") { $result["AutoInterface"] = $Matches[1]; } else { Write-Output $argument; } } $result; } } # 类全名表 $clsNamespaces = @{}; $appServiceAttributes = @{}; foreach ($libName in $libNames) { # 库文件路径 $libPath = Join-Path -Path $workDir -ChildPath $libName; # 获取所有源文件路径 $filePaths = GetSourceFileList -Path $libPath; # 读取Service信息和类命名空间; foreach ($filePath in $filePaths) { $fileContent = Get-Content -Path $filePath.FullName -Raw; $namespace = GetNameSpace -Content $fileContent; if ($null -eq $namespace -or $namespace -eq "" ) { continue; } $clsName = GetClassName -Content $fileContent; if ($null -eq $namespace -or $namespace -eq "" ) { continue; } $clsNamespaces[$clsName] = $namespace; $appService = ScanAppServiceAttribute -Content $fileContent; if ($null -ne $appService) { $appServiceAttributes[$clsName] = $appService; } } } $services = $appServiceAttributes.Clone(); # 生成服务信息 foreach ($service in $services.Keys) { $serviceInfo = $appServiceAttributes[$service]; $namespace = $clsNamespaces[$service]; $serviceInfo["Class"] = "$namespace.$service"; $interface = $serviceInfo["Interface"]; if ($null -ne $interface) { $ns = $clsNamespaces[$interface]; $serviceInfo["Interface"] = "$ns.$interface"; } $appServiceAttributes[$service] = $serviceInfo; } Write-Output "扫描到 $($appServiceAttributes.Count) 个服务"; $appServiceAttributes | ConvertTo-Json | Out-File -FilePath $appServicePath Write-Output "文件保存至 $appServicePath" 这个脚本的功能是遍历定义的子项目文件，然后扫描所有类和接口，以及所有带有 AppServiceAttribute 特性的服务类，最后生成一个 appService.json 运行脚本生成文件以后记得修改文件生成操作，不然在分析器里这个文件是不可见的 创建源生成器 跟创建类库一样，但是需要编辑 .csproj 文件，完整文件如下 <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules> <IsRoslynComponent>true</IsRoslynComponent> <GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn> <NoWarn>CS1570;CS1587</NoWarn> </PropertyGroup> <ItemGroup> <None Include="..\.editorconfig" Link=".editorconfig" /> </ItemGroup> <ItemGroup> <PackageReference Include="Microsoft.CodeAnalysis.Analyzers"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> <PackageReference Include="Microsoft.CodeAnalysis.Common"> <PrivateAssets>all</PrivateAssets> </PackageReference> <PackageReference Include="Microsoft.CodeAnalysis.CSharp"> <PrivateAssets>all</PrivateAssets> </PackageReference> <PackageReference Include="Newtonsoft.Json"> <GeneratePathProperty>true</GeneratePathProperty> <PrivateAssets>all</PrivateAssets> </PackageReference> </ItemGroup> <ItemGroup> <Compile Update="Templates.Designer.cs"> <DesignTime>True</DesignTime> <AutoGen>True</AutoGen> <DependentUpon>Templates.resx</DependentUpon> </Compile> </ItemGroup> <ItemGroup> <EmbeddedResource Update="Templates.resx"> <Generator>ResXFileCodeGenerator</Generator> <LastGenOutput>Templates.Designer.cs</LastGenOutput> </EmbeddedResource> </ItemGroup> <ItemGroup> <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" /> </ItemGroup> <Target Name="GetDependencyTargetPaths"> <ItemGroup> <TargetPathWithTargetPlatformMoniker Include="$(PKGNewtonsoft_Json)\lib\netstandard2.0\Newtonsoft.Json.dll" IncludeRuntimeDependency="false" /> </ItemGroup> </Target> </Project> 我所做的修改如下 将目标框架改为 netstandard2.0，目前源生成器必须用该版本 添加 EnforceExtendedAnalyzerRules 和 IsRoslynComponent 项，第一个是排除别的分析器，第二个是定义该项目是 Roslyn 组件，添加了以后会有额外的调试配置 添加 GetTargetPathDependsOn 项，意思是从定义的路径中寻找依赖库 添加 Microsoft.CodeAnalysis.Analyzers 和 Microsoft.CodeAnalysis.Common 和 Microsoft.CodeAnalysis.CSharp 库，原样从微软文档拷来的，生成器必须的依赖 添加 Newtonsoft.Json 库（可选），我使用了 Newtonsoft.Json 作为 Json 反序列化库，因此还需要额外配置引用信息（因为源生成器相当于是编译期的扩展，如果不做额外处理会报找不到Dll的错），需要在下面定义 <ItemGroup> <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" /> </ItemGroup> <Target Name="GetDependencyTargetPaths"> <ItemGroup> <TargetPathWithTargetPlatformMoniker Include="$(PKGNewtonsoft_Json)\lib\netstandard2.0\Newtonsoft.Json.dll" IncludeRuntimeDependency="false" /> </ItemGroup> </Target> 然后再主项目中添加项目引用，并修改引用的属性，将被引用项目类型改成分析器，然后排除所有的生成资产 </ItemGroup> <ProjectReference Include="..\XinjingdailyBot.Generator\XinjingdailyBot.Generator.csproj"> <OutputItemType>Analyzer</OutputItemType> <ReferenceOutputAssembly>false</ReferenceOutputAssembly> </ProjectReference> </ItemGroup> 服务注册源生成器 代码模板存放在 Templates.resx 中，内容如图 可以根据输出的结果反推 using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; using Newtonsoft.Json; using System.Text; using XinjingdailyBot.Generator.Data; namespace XinjingdailyBot.Generator; [Generator] internal sealed class AppServiceGenerator : ISourceGenerator { const string InputFileName = "appService.json"; const string OutoutFileName = "GeneratedAppServiceExtensions.g.cs"; /// <inheritdoc/> public void Initialize(GeneratorInitializationContext context) { // 无需处理 } /// <inheritdoc/> public void Execute(GeneratorExecutionContext context) { try { var fileText = context.AdditionalFiles.Where(static x => x.Path.EndsWith(InputFileName)).FirstOrDefault() ?? throw new FileNotFoundException("缺少配置文件, 请使用 scan_service.ps1 生成"); ProcessSettingsFile(fileText, context); } catch (Exception e) { context.ReportDiagnostic( Diagnostic.Create( "XJB_01", nameof(AppServiceGenerator), $"生成注入代码失败，{e.Message}", defaultSeverity: DiagnosticSeverity.Error, severity: DiagnosticSeverity.Error, isEnabledByDefault: true, warningLevel: 0)); } } /// /// 生成文件 /// /// /// private void ProcessSettingsFile(AdditionalText xmlFile, GeneratorExecutionContext context) { var text = xmlFile.GetText(context.CancellationToken)?.ToString() ?? throw new FileLoadException("文件读取失败"); var json = JsonConvert.DeserializeObject<AppServiceData>(text) ?? throw new FileLoadException("文件读取失败"); var sb = new StringBuilder(); sb.AppendLine(Templates.AppServiceHeader); foreach (var kv in json) { var entry = kv.Value; var lifeTime = entry.LifeTime?.ToLowerInvariant() switch { "singleton" or "scoped" or "transient" => entry.LifeTime, _ => null, }; if (string.IsNullOrEmpty(lifeTime)) { continue; } if (!string.IsNullOrEmpty(entry.Class)) { if (string.IsNullOrEmpty(entry.Interface)) { sb.AppendLine(string.Format(Templates.AppServiceContent1, lifeTime, entry.Class)); } else { sb.AppendLine(string.Format(Templates.AppServiceContent2, lifeTime, entry.Interface, entry.Class)); } } } sb.AppendLine(Templates.AppServiceFooter); context.AddSource(OutoutFileName, SourceText.From(sb.ToString(), Encoding.UTF8)); } } 生成的代码示例，为避免使用 using，这里使用完整类名引用 using Microsoft.Extensions.DependencyInjection; namespace XinjingdailyBot.WebAPI.Extensions; /// /// 动态注册服务扩展 /// public static class GeneratedAppServiceExtensions { private static readonly NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger(); /// /// 注册引用程序域中所有有AppService标记的类的服务 /// /// public static void AddAppServiceGenerated(this IServiceCollection services) { services.AddTransient<XinjingdailyBot.Repository.OldPostRepository>(); services.AddScoped<XinjingdailyBot.Interface.Bot.Common.IDispatcherService, XinjingdailyBot.Service.Bot.Common.DispatcherService>(); services.AddSingleton<XinjingdailyBot.Repository.TagRepository>(); services.AddSingleton<XinjingdailyBot.Interface.Bot.Common.IChannelService, XinjingdailyBot.Service.Bot.Common.ChannelService>(); services.AddTransient<XinjingdailyBot.Interface.Data.IAttachmentService, XinjingdailyBot.Service.Data.AttachmentService>(); services.AddScoped<XinjingdailyBot.Command.ReviewCommand>(); services.AddScoped<XinjingdailyBot.Interface.Bot.Handler.IJoinRequestHandler, XinjingdailyBot.Service.Bot.Handler.JoinRequestHandler>(); services.AddTransient<XinjingdailyBot.Interface.Data.IBanRecordService, XinjingdailyBot.Service.Data.BanRecordService>(); services.AddSingleton<XinjingdailyBot.Repository.RejectReasonRepository>(); services.AddScoped<XinjingdailyBot.Interface.Bot.Common.IUpdateService, XinjingdailyBot.Service.Bot.Common.UpdateService>(); services.AddSingleton<XinjingdailyBot.Interface.Bot.Handler.IForwardMessageHandler, XinjingdailyBot.Service.Bot.Handler.ForwardMessageHandler>(); services.AddScoped<XinjingdailyBot.Command.CommonCommand>(); services.AddSingleton<XinjingdailyBot.Repository.LevelRepository>(); services.AddSingleton<XinjingdailyBot.Interface.Bot.Handler.ICommandHandler, XinjingdailyBot.Service.Bot.Handler.CommandHandler>(); services.AddTransient<XinjingdailyBot.Interface.Data.IDialogueService, XinjingdailyBot.Service.Data.DialogueService>(); services.AddSingleton<XinjingdailyBot.Interface.Data.IPostService, XinjingdailyBot.Service.Data.PostService>(); services.AddTransient<XinjingdailyBot.Interface.Data.IChannelOptionService, XinjingdailyBot.Service.Data.ChannelOptionService>(); services.AddSingleton<XinjingdailyBot.Interface.Bot.Handler.IChannelPostHandler, XinjingdailyBot.Service.Bot.Handler.ChannelPostHandler>(); services.AddSingleton<XinjingdailyBot.Interface.Data.IUserService, XinjingdailyBot.Service.Data.UserService>(); services.AddTransient<XinjingdailyBot.Interface.Helper.IImageHelperService, XinjingdailyBot.Service.Helper.ImageHelperService>(); services.AddSingleton<XinjingdailyBot.Interface.Data.INameHistoryService, XinjingdailyBot.Service.Data.NameHistoryService>(); services.AddScoped<XinjingdailyBot.Command.NormalCommand>(); services.AddSingleton<XinjingdailyBot.Interface.Data.IMediaGroupService, XinjingdailyBot.Service.Data.MediaGroupService>(); services.AddScoped<XinjingdailyBot.Command.ObsoleteCommand>(); services.AddScoped<XinjingdailyBot.Command.PostCommand>(); services.AddTransient<XinjingdailyBot.Interface.Data.IAdvertisePostService, XinjingdailyBot.Service.Data.AdvertisePostsService>(); services.AddTransient<XinjingdailyBot.Interface.Data.IAdvertiseService, XinjingdailyBot.Service.Data.AdvertiseService>(); services.AddSingleton<XinjingdailyBot.Repository.GroupRepository>(); services.AddTransient<XinjingdailyBot.Interface.Helper.IHttpHelperService, XinjingdailyBot.Service.Helper.HttpHelperService>(); services.AddTransient<XinjingdailyBot.Interface.Helper.ITextHelperService, XinjingdailyBot.Service.Helper.TextHelperService>(); services.AddSingleton<XinjingdailyBot.Interface.Bot.Handler.IMessageHandler, XinjingdailyBot.Service.Bot.Handler.MessageHandler>(); services.AddTransient<XinjingdailyBot.Interface.Data.ICmdRecordService, XinjingdailyBot.Service.Data.CmdRecordService>(); services.AddSingleton<XinjingdailyBot.Interface.Bot.Handler.IInlineQueryHandler, XinjingdailyBot.Service.Bot.Handler.InlineQueryHandler>(); services.AddTransient<XinjingdailyBot.Repository.PostRepository>(); services.AddSingleton<XinjingdailyBot.Interface.Data.IUserTokenService, XinjingdailyBot.Service.Data.UserTokenService>(); services.AddTransient<XinjingdailyBot.Interface.Helper.IMarkupHelperService, XinjingdailyBot.Service.Helper.MarkupHelperService>(); services.AddScoped<XinjingdailyBot.Command.AdminCommand>(); services.AddScoped<XinjingdailyBot.Command.SuperCommand>(); services.AddTransient<XinjingdailyBot.Interface.Data.IReviewStatusService, XinjingdailyBot.Service.Data.ReviewStatusService>(); services.AddSingleton<XinjingdailyBot.Interface.Bot.Handler.IGroupMessageHandler, XinjingdailyBot.Service.Bot.Handler.GroupMessageHandler>(); } } 调试源生成器 首先删除源生成器项目的所有调试目标，然后新增 Roslyn Component 目标，并在右边将 Target Project 设为主项目，然后将启动项目改为生成器 然后在生成器的 Initialize 方法中开启调试器 public void Initialize(GeneratorInitializationContext context) { // 无需处理 if (!Debugger.IsAttached) { Debugger.Launch(); } } 这样就可以动态调试生成器代码了 实际使用 将启动项目改为主功能，去掉源生成器的调试代码，然后重新生成一遍解决方案，如果一切正常，将会在分析器中看到生成的代码文件 如果后续有新的服务，重新执行 scan_service.ps1 即可更新配置文件
【油猴脚本教程】P1 开发者工具的使用 & JavaScript 基础教程
P1 开发者工具的使用 与 JavaScript 基础教程 教程索引 视频链接 (上) 视频链接 (下) 开发者工具的使用 F12 或者 菜单 -> 更多工具 -> 开发者工具 调出 切换中文 更改语言后重新打开开发者工具生效 或者 界面简介 元素 HTML 原文 HTML节点的属性 控制台 在任意Tab按ESC可以在下方调出 源代码 浏览网站中的所有资源 调试代码 网络 抓包工具 应用 查看本地储存的内容 Cookie Session LocalStorage JavaScript 基础教程 一些 JavaScript 学习资源 MDN 文档： https://developer.mozilla.org/zh-CN/docs/Web/JavaScript 廖雪峰： https://www.liaoxuefeng.com/wiki/1022910821149312 菜鸟教程： https://www.runoob.com/js/js-tutorial.html uTools 插件 基本特性 单线程 弱类型 / 动态类型 大小写敏感 ' ," 通用 不强制使用分号 (建议统一) 语法很灵活 运行时: v8, NodeJS 基础语法 注释 //单行注释 /* 多行注释 */ 变量与常量 赋值操作符 var let const //定义变量/常量 var a = 1; var b; let c = 1; let d; const e = 4; //a,b,c,d,e 1 undefined 1 undefined 4 console.log("a,b,c,d,e",a,b,c,d,e); //Uncaught SyntaxError: Missing initializer in const declaration const f; //赋新值 a = 2; c = 3; //Uncaught TypeError: Assignment to constant variable. e = 1; //重复定义 //undefined var a = 6; let c = 6; //Uncaught SyntaxError: Identifier 'a' has already been declared let a = 6; //Uncaught SyntaxError: Identifier 'a' has already been declared const a = 6; //常量覆盖 const e = 6; //e 6 console.log("e",e); //在子域中覆盖(ES6引入) var ts = 1; let he = 2; { var ts = "aaa"; let he = "yyy"; //ts,he aaa yyy console.log("ts,he",ts,he); } //ts,he aaa 2 console.log("ts,he",ts,he); var 定义一个 变量 let 定义一个仅在作用域内生效的 变量 const 定义一个 常量，一旦定义无法修改，但是可以覆盖定义 使用相同的操作符可以覆盖定义同名的变量，但是不建议这么使用 使用 let 覆盖定义时不影响父作用域的同名变量 强烈推荐优先使用 let 定义变量，如果值不需要改变，建议使用 const 定义成常量 函数提升 //函数提升 foo(); // "bar" function foo() { console.log("bar"); } //这种写法的函数不会被提升 bar(); // 类型错误：bar 不是一个函数 var bar = function() { console.log("bar2"); }; 作用域 { } 定义代码作用域 (ES6的内容) 使用 let 定义的变量，在子域可以访问父域的变量，但是父域不能访问子域的变量 函数中定义的内容外部无法访问 子域中定义的函数，父域无法访问 //顶级作用域(最外层的作用域) let x = 114; { //子作用域 let b = 514; //x,b 114 514 console.log("x,b", x, b); } //函数内部其实也是一种作用域 function xbz() { let c = 233; //x,c 114 233 console.log("x,c", x, c); //Uncaught ReferenceError: b is not defined console.log("b", b); } xbz(); //匿名函数同理 (() => { let d = 3.14; //x,d 114 3.14 console.log("x,d", x, d); //Uncaught ReferenceError: b is not defined console.log("b", b); })(); //另一种写法 (function() { let d = 3.14; //x,d 114 3.14 console.log("x,d", x, d); //Uncaught ReferenceError: b is not defined console.log("b", b); })(); //x 114 console.log("x", x); //Uncaught ReferenceError: b is not defined console.log("b", b); //Uncaught ReferenceError: c is not defined console.log("c", c); //Uncaught ReferenceError: d is not defined console.log("d", d); 一模一样的代码，只是使用 var 而不是 let ，可访问性略有区别 //顶级作用域(最外层的作用域) var x = 114; { //子作用域 var b = 514; //x,b 114 514 console.log("x,b", x, b); } //函数内部其实也是一种作用域 function xbz() { var c = 233; //x,c 114 233 console.log("x,c", x, c); //b 514 console.log("b", b); } xbz(); //匿名函数同理 (() => { var d = 3.14; //x,d 114 3.14 console.log("x,d", x, d); //b 514 console.log("b", b); })(); //x, 114 console.log("x", x); //b, 514 console.log("b", b); //Uncaught ReferenceError: c is not defined console.log("c", c); //Uncaught ReferenceError: d is not defined console.log("d", d); 数据类型 基础类型 数值 整数 浮点数(小数) NaN (跟任何值比较都是false) 字符串 " " ' ' ` ` (模板字符串，可以用 ${ } 直接引用程序中的变量) let a = 1; //'a=1' `a=${a}` 布尔 true false 对象 object Array 数组 [ xxx, xxx ] new Array(xxx, xxx) 属性： length 元素个数 方法： at 获取指定索引的元素，找不到返回 underfined，等效于 Array[index] contact 拼接另一个可迭代元素，不影响原数组 indexOf 查找元素位置，索引从0开始，找不到返回-1 includes 是否包含元素，返回布尔 join 使用分隔符将列表元素合并成字符串 push 在末尾添加元素，返回数组长度 pop 移除末尾元素，返回被移除的元素 unshift 在数组首位添加元素，返回数组长度 shift 移除首位元素，返回被移除的元素 slice 切片，不影响原数组，返回切片后的新数组 some 传入lambda表达式，如果列表中含有符合表达式的元素，返回true，反之返回false filter 传入lambda表达式，返回一个包含符合表达式的元素的新列表 find 传入lambda表达式，返回第一个符合表达式的元素，没有匹配则返回 undefined sort 传入lambda表达式，返回排序后的数组 forEach 传入lambda表达式，将列表的每一个元素代入执行 字典 {"key": value} 示例 let dict = {}; //添加键值对 dict["a"] = 1; dict.b = 2; //{a: 1, b: 2} console.log(dict); //一个字典中键名不会重复 dict.b = 3; //{a: 1, b: 3} console.log(dict); //读取键值对, 不存在的键名返回 underfined //1 console.log(dict.a); //3 console.log(dict["b"]); //undefined console.log(dict["c"]); Map 字典 new Map(); Map.set(key,value); 属性 size 键值对数量 方法 set 添加键值对 get 通过键名获取键值, 不存在时返回 underfined clear 清空字典 has 是否含有指定的键名 delete 删除指定键值对 keys 返回键名的可迭代对象 forEach 传入lambda表达式，将字典的每一个键名和键值一起代入执行 Set 集合 new Set() 集合中元素是唯一的，可以用来去重，内部元素无序，无法通过索引访问 属性 size 键值对数量 方法 add 添加元素 delete 删除元素 clear 清空集合 has 是否有指定元素 entries 返回集合的可迭代对象 keys 返回集合的可迭代对象 forEach 传入lambda表达式，将集合中的每一个元素代入执行 函数 function 获取变量类型 typeof(xx) 数据类型转换 Boolean(any) //任意类型转布尔 String() //所有类型都可以用这个转成字符串 Number() //任意类型转数值类型，转换失败返回NaN parseFloat(any) //转换失败返回NaN parseInt(any) //转换失败返回NaN object.toString() //基础类型没有这个方法 变量比较的坑 == 与 === ( = 是赋值操作符) == 比较操作符，自动隐式类型转换 === 绝对等于，不允许自动类型转换 NaN 跟任意值比较结果都是 false 推荐优先使用 === 判断是否相等 一些被认为是 false 的值 false underfined null 0 NaN "" 执行流 if 条件判断 if( /* 表达式 */ ) { //表达式运算结果转换为布尔为 true 时执行该块 } else if( /* 表达式2 */ ) //也可以使用 else if 连续判断多个表达式 { //可以存在任意和 else if 子句，也可以省略 //表达式2运算结果转换为布尔为 true 时执行该块 } else { //else 子句可以省略 //表达式1和表达式2的运算结果转换为布尔都为 false 时执行该块 } //示例 let a = 500; if(a > 514){ console.log(1); } else if(a < 114){ console.log(2); } else{ console.log("啊啊啊"); } //"啊啊啊" switch 分支 switch ( /* 表达式 */ ) { case /* 值1 */: //表达式计算结果等于值1时执行从case到第一个break之间的语句 [break;]//不写break的话会case语句 case /* 值2 */: //表达式计算结果等于值2时执行从case到第一个break之间的语句 [break;] //可以存在任意个case default: //只能写在最后, 可以省略 //当表达式的值不满足任意一个case, 或者没有遇到break跳出时执行这里的语句 [break;]//可以写break,但是没有必要 } //示例 for(let x=0; x<5; x++) { switch (x){ case 1: console.log('1'); case 2: case 4: console.log('2 or 4'); break; default: console.log('default'); } } /* default 1 2 or 4 2 or 4 default 2 or 4 */ for 循环 for 最通用的计数循环 for(/* 初值设定，只执行一次 */ ; /* 表达式，满足条件则执行循环 */ ; /* 每次循环后执行 */){ //循环执行这里的语句 } //例子 for (let i = 0; i < 5; i++) { console.log(i); } //0 1 2 3 4 const list = [1,2,3,4,5]; for (let i=0; i < list.length; i++){ console.log(list[i]); } //1 2 3 4 5 for...in 自动枚举对象的 可枚举属性 ，适用于枚举 字典 ，一般不适用于 数组 和 集合 不能在循环体内修改迭代对象 //错误示范 const list = [9,8,7,6,5]; for(let x in list){ console.log(x); } //0 1 2 3 4 //获取的实际上是元素的下标 const set = new Set([1,2,3,4,5]); for(let x in set){ console.log(x); } //无输出 //为什么会得到这样的结果？ //对字典使用则会得到预期的结果 const dict = {'a':1,'b':2,'c':3}; for(let x in dict){ console.log(x); } //a b c for...in 循环还可以用来迭代 对象 的 属性 for...of 循环 自动枚举可迭代对象的每一个元素 ，适用于任何可迭代对象，字典 不可直接迭代 不能在循环体内修改迭代对象 const list = [9,8,7,6,5]; for(let x of list){ console.log(x); } //9 8 7 6 5 const set = new Set([1,2,3,4,5]); for(let x of set){ console.log(x); } //1 2 3 4 5 //错误示例 const dict = {'a':1,'b':2,'c':3}; for(let x of dict){ console.log(x); } //TypeError: dict is not iterable forEach 循环 严格意义上来说是属于部分可迭代对象的原型方法 ( Prototype )，而不是通用操作符 需要传入一个函数，执行过程就是把可迭代对象的每个元素代入函数中执行，因为每次循环都相当于调用函数，因此效率远不如上面的几种 for 循环 const list = ['a', 'b', 'c']; list.forEach(x => console.log(x)); //a b c //相当于 list.forEach(function(x) { console.log(x); }); 执行速度 for > for...in / for...of >>> forEach while 循环 while 类似 for 循环，满足循环条件就会一直执行循环体 while(/* 循环条件为 true 则进入循环 */) { /* 循环执行体 */ } //例子 let x = 3; while(x--){ console.log(x); } //2 1 0 //要注意避免出现死循环 do...while 在执行循环体后才会检查循环条件，也就是说至少会执行一次循环体 do { /* 循环执行体 */ } while(/* 循环条件为 true 则进入循环 */) //例子 let x = 3; do { console.log(x); } while(x--); //3 2 1 0 循环操作 continue 跳过执行当前循环体，进行下一轮循环 //跳过执行部分循环 let x = 0; while(x++ < 10) { if(x % 3 == 0){ continue; } console.log(x); } //1 2 4 5 7 8 10 //使用 label 自由控制外部循环 var i = 0; var j = 10; checkiandj: while (i < 4) { console.log('i',i); i += 1; checkj: while (j > 4) { console.log('j',j); j -= 1; if ((j % 2) == 0) { continue checkj; } console.log(j + ' 是奇数。'); } console.log('i = ' + i); console.log('j = ' + j); } break 结束当前循环体 //结束循环 const a = [1,2,3,4,5]; for (i = 0; i < a.length; i++) { if (a[i] == 3) { break; } console.log(a[i]); } //1 2 //也可以指定 label 终止外部循环 let x = 0; let z = 0 labelCancelLoops: while (true) { console.log("外部循环: " + x); x += 1; z = 1; while (true) { console.log("内部循环: " + z); z += 1; if (z === 3 && x === 3) { break labelCancelLoops; } else if (z === 3) { break; } } } try-catch 错误捕获 在 try 块中的代码如果执行遇到错误，会被 catch 块捕获，而不会影响外部代码继续执行 try { //使用 throw 可以手动引发异常 throw '114514异常'; } catch (error) { console.error(error); } //控制台输出了异常消息 //还可以使用 finally 块，不论是否遇到错误，跳出 try...catch 块前都会执行 finally 块的代码 try { //使用 throw 可以手动引发异常 throw '114514异常'; } finally { console.log('finally') } //finally 和 catch 块都是可选的，也可以一起用，但是不能单独使用 try 块 try { //使用 throw 可以手动引发异常 throw '114514异常'; } catch (error) { console.error(error); } finally { console.log('finally') } 运算操作符 算数运算 摆烂了，看MDN吧： https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Expressions_and_Operators + 加 ++ 自增 - 减 -- 自减 * 乘 / 除 ** 指数 % 取余数 位运算 & 按位与 | 按位或 ^ 按位异或 逻辑运算 && 逻辑与 || 逻辑或 简化赋值运算 x += y => x = x + y x -= y => x = x - y x *= y => x = x * y x /= y => x = x / y x %= y => x = x % y x **= y => x = x ** y x <<= y => x = x << y x >>= y => x = x >> y x &= y => x = x & y x |= y => x = x | y x ^= y => x = x ^ y 解构赋值 //可迭代元素的解构，相当于把不同下标的元素赋值给定义的变量 const list = ['1','2','3']; let [t1, t2, t3, t4] = list; console.log(t1,t2,t3,t4); //1 2 3 undefined //如果元素不够分配，未分配到的元素不会被赋值 //使用 ... 可以让某个变量接受剩余元素 const list = ['1','2','3','4','5']; let [t1, t2, ...t3] = list; console.log(t1,t2,t3); //1 2 (3) ['3', '4', '5'] //注意，t3变量是一个数组 //也可以对字典使用解构，但是需要使用 {} 而不是 [] const dict = {aa:1,bb:2,cc:3}; let {bb,aa,cc,dd} = dict; console.log(aa,bb,cc,dd); //1 2 3 undefined //字典的解构赋值是通过 key 而不是变量索引来获取的，因此变量的顺序无关紧要 //如果变量和key名字不一致也可以指定别名 const dict = {aa:1,bb:2,cc:3}; let {aa:s1,bb:s2,cc:s3} = dict; console.log(s1,s2,s3); //1 2 3 三目运算 表达式 ? 值1 : 值2 当表达式成立，三目运算的结果为值1，当表达式不成立，三目运算的结果为值2 let x = false ? 1 : 2; console.log(x); //2 let x = true ? 1 : 2; console.log(x); //1 浏览器对象 Window window 是浏览器对象，访问属性或者方法的时候可以省略 window 常用属性 document HTML文档对象，页面上的元素都可以通过这个对象获取 console 控制台对象，用于打印日志 location 地址对象，读取网页链接或者控制跳转 localStorage 访问本地储存数据 screen 获取显示器分辨率 scrollX , scrollY 水平/垂直滚动条位置 常用方法 alert 弹出提示框，会阻塞 JS 运行 confirm 弹出确认提示框，会阻塞 JS 运行 blur 将焦点移出当前窗口 close 关闭窗口 eval 执行字符串形式的 JS 语句 find 查找指定字符串，并聚焦到字符串的位置 focus 激活当前窗口 open 打开新窗口 (标签页) openDialog 打开新的对话框窗口 print 显示打印对话框 prompt 弹出输入框，会阻塞 JS 运行 scroll 控制滚动条滚动到指定位置 stop 停止加载 文档对象 Document Document 接口是 HTML 页面的另一种形式，或者叫 DOM 树，通过 window.document 可以获取当前窗口的 DOM 树，通过 Document 对象可以很方便的添加或者修改 DOM 元素 Document 对象也继承了 Node 对象的属性和方法 常用属性 body 返回 节点 head 返回 节点 cookie 读取或者设置 cookie title 获取或者设置网页标题 URL 返回地址栏链接 常用方法 createElement 指定标签名，创建一个新的元素 getElementsByClassName 指定类名查找元素 getElementById 指定ID查找元素 querySelector 通过CSS选择器查找第一个匹配元素 querySelectorAll 通过CSS选择器查找所有匹配元素 evaluate 执行XPath表达式 hasFocus 返回当前文档是否拥有焦点 文档元素对象 Element Element 对象相当于HTML中的Tag，比如 Element 对象也继承了 Node 对象的属性和方法 常用属性 attributes 标签属性集合 classList 标签 class 属性列表 (按空格分隔) className 标签 class 属性的值 id 标签的 id 属性 innerHTML DOM 元素内部的 HTML 文本 innerText DOM 元素内部的文本 tagName 标签自身的名称，例如 div a span 等 常用方法 addEventListener 添加事件监听器，需要指定事件名和回调函数，触发特定事件后自动调用回调函数 dispatchEvent 手动触发指定的事件 getAttribute 获取标签的指定属性 文档节点对象 Node 网页中的所有 Node 一起构成了 DOM 树，对节点的修改会反应到网页中 常用属性 childNodes 返回该节点的所有子节点 firstChild 返回第一个子节点，没有返回 null lastChild 返回最后一个子节点，没有返回 null nodeName 返回节点对应的名称，比如 #document nodeType 返回节点类型对应的值，具体定义参考 MDN parentNode 返回父节点 parentElement 返回父元素 textContent 返回节点内所有子节点的文本内容 常用方法 appendChild 将指定节点作为子节点添加到当前节点的末尾 cloneNode 克隆指定节点 hasChildNodes 判断当前节点是否有子节点 insertBefore 在当前节点新增子节点，插入在指定节点之前 Document Element 与 Node Document 即 DOM 树，包含了网页中的所有 DOM 对象，既包含 Element 也包含 Node Element 标签元素，类似与 xxx，它继承 Node 的属性和方法，同时具有自己特有的属性和方法 Node 节点，是 DOM 树的最基础单位，比如 xxx 这一个 Element，就包含一个 A Node （即 ）和一个 Text Node（即 xxx） 一个 Element 可以包含任意个 Element 元素 和 Node 元素
前言 本脚本仅供学习交流之用，使用者如何使用与本文作者无关。 AutoJS下载 & 教程 [post cid="1235" /] 脚本 大部分沿用之前的代码，初步测试没有问题，遇到问题欢迎留言反馈 //JD不定时热更新活动页，请关注脚本更新 //遇到问题请在发布页留言： //https://blog.chrxw.com/archives/2021/01/18/1455.html //Ver 1.8 2021.01.23 //By Chr_(chr@chrxw.com) //========================= //更新说明 //重写了任务完成判断方式,防止偶尔报错 //优化数组访问,应该不会再越界报错了 //修复若干bug //新支持浏览频道任务 //修复加购和参加会员任务的问题（需要打开开关） //修复出现限时任务时报错的问题 //========================= //功能配置 //自动加入会员（设为true启用) let auto_join_vip = false; //自动完成加购任务（设为true启用) let auto_add_cart = false; //延时时间倍率（倍数越大延时越久) let sleep_t = 1.0; //========================= auto.waitFor(); console.show(); toast('\n脚本开始运行\n请切换到任务页\nBy Chr_'); for (let i = 0; i < 10; i++) { //等待切换到任务页 if (className("android.view.View").text("我的爆竹").exists() || className("android.view.View").text("集爆竹").exists()) { break; } rsleep(1); } if (!className("android.view.View").textStartsWith("邀请好友助力").exists()) { advclick(className("android.view.View").text("集爆竹").findOnce()); } toast("\n即将开始工作\n中途需退出请按【音量+】终止"); sleep(1000); for (; ;) { let flag = false; let skip = false; let tasks = className("android.view.View").textStartsWith("邀请好友助力").findOnce().parent().parent().children(); for (let i = 0; i < tasks.length; i++) { if (tasks[i].childCount() == 5) { tasks[i].child(4).click(); rsleep(1); advback(); skip = true; } } if (skip) { continue; } for (let i = 0; i < tasks.length; i++) { if (tasks[i].length < 4) { for (let j = 0; j < tasks[i].childCount(); j++) { log(tasks[i].child(j).text()); } continue; } let desc; let title; try { desc = tasks[i].child(2).text(); title = tasks[i].child(1).text(); } catch (e) { log(e); continue; } let btn = tasks[i].child(3); log(i, desc); if (desc.search("8秒") != -1) { if (checks(btn, title)) { advclick(btn); view(); flag = true; } } else if (desc.search("浏览可得") != -1) { if (checks(btn, title)) { advclick(btn); view2(); flag = true; } } else if (desc.search("关注频道") != -1) { if (checks(btn, title)) { advclick(btn); view3(); flag = true; } } else if (auto_join_vip && desc.search("成功入会可得") != -1) { if (checks(btn, title)) { advclick(btn); view3(); flag = true; } } else if (auto_add_cart && desc.search("5个") != -1) { if (checks(btn, title)) { advclick(btn); view4(); flag = true; } } if (flag) { break; } } if (!flag) { toast("\n脚本运行完毕\n模拟浏览任务全部完成（大概）"); console.hide(); break; } } //检查任务是否完成 function checks(btn, title) { let match = title.match(/(\d+)\/(\d+)/); if (match) { let a = Number(match[1]); let b = Number(match[2]); log(a, b, a < b); return a < b; } else { return false; } } //模拟浏览（浏览会场任务） function view() { rsleep(3); className("android.widget.Image").text("vk image").findOne(5000); rslide(6); rslideR(1); rsleep(3); advback(); } //模拟浏览（直接返回） function view2() { rsleep(3); back(); rsleep(3); } //模拟浏览（开卡） function view3() { rsleep(2); let btn = className("android.view.View").text("确认授权并加入店铺会员").findOne(5000); if (btn) { advclick(btn); rsleep(2); } else { rsleep(1); } advback(); } //模拟浏览（浏览商品、加购任务） function view4() { rsleep(3); let add_cart = true; let count = 4; if (className("android.view.View").text("任意浏览以下5个商品").exists()) { add_cart = false; count = 4; } else { add_cart = true; count = 5; } log(add_cart ? "加购模式" : "浏览模式"); for (let i = 0; i < 5; i++) { let prices = className("android.view.View").textMatches("^¥[0-9]+\.[0-9][0-9]").untilFind(); log(i, prices[i].text()); let good = prices[i].parent().parent(); if (good.childCount() != count) { log('已完成，跳过'); continue; } if (!add_cart) { advclick(good); rsleep(2); rslide(1); className("android.widget.ImageView").desc("返回").click(); } else { good.child(4).click(); rsleep(2); rslide(1); className("android.widget.ImageView").desc("返回").click(); } rslide(1); rsleep(2); } advback(); rsleep(3); } //随机延时 function rsleep(s) { while (s--) { let t = random(900, 1200) * sleep_t; sleep(t); } } //模拟返回 function advback() { if (className("android.view.View").text("集爆竹").exists()) { return; } for (let i = 0; i < 3; i++) { if (!className("android.widget.ImageView").desc("返回").exists()) { rslideR(1); rsleep(2); continue; } className("android.widget.ImageView").desc("返回").click(); rsleep(5); if (className("android.view.View").text("集爆竹").exists()) { return; } } log("定位不到返回按钮，模拟返回键"); back(); } //随机划屏 function rslide(i) { while (i--) { let x1 = random(300, 800); let y1 = random(1200, 1900); let x2 = random(300, 800); let y2 = random(1000, 1200); swipe(x1, y1, x2, y2, 300); rsleep(1); } } //随机划屏，反向 function rslideR(i) { while (i--) { let x1 = random(300, 800); let y1 = random(900, 1300); let x2 = random(300, 800); let y2 = random(1500, 1900); swipe(x1, y1, x2, y2, 300); rsleep(1); } } //模拟点击 function advclick(uiobject) { let rect = uiobject.bounds(); click(rect.centerX(), rect.centerY()); } function test() { let tasks = className("android.view.View").textStartsWith("邀请好友助力").findOnce().parent().parent().children(); for (let i = 0; i < tasks.length; i++) { for (let j = 0; j < tasks[i].childCount(); j++) { log(tasks[i].child(j).text()); } } }
前言 开源地址：Github ，蓝奏云镜像：链接 (密码:cg84) 配置文件说明 [auto] # 可选配置 # 如果配置了下面的值,脚本将会自动代入配置的值 # 如果没有配置,脚本将会要求你输入对应项目 # Steam64位ID,只能为64位ID steamid = 0 # 如果有多个账号,也可以这这么配置 # steamid = [11111,22222,33333] [other] # 其他设置 # 是否在运行结束后显示等待屏幕, 如果是在终端中运行可以禁用 # true: 显示等待屏幕, false: 不显示等待屏幕,直接退出 wait_screen = false [itad] # itad模块设置,用于获取价格信息 # API Token, 必填 # 申请地址: https://new.isthereanydeal.com/apps/mine/ token = "" # 价格区域 # 设置值参考详见附录1 country = "CN" region = "cn" # 货币符号 currency_symbol = '¥' [keylol] # keylol模块设置,用于获取游戏标签,卡牌等信息 # 是否启用keylol模块,禁用的话将使用itad模块替代 # true:启用, false:禁用 enable = false [steam] # Steam网页端语言选项 # schinese代表简中, english代表英文 language = "schinese" # 是否使用小尺寸的游戏图片,改为false将会使用大一些的图片 small_game_pic = true [net] # 代理设置,只支持http协议(不支持socks5) # 如果使用全局模式应该不需要设置 # 非空值: 启用代理, false: 禁用代理 proxy = false # proxy = 'http://127.0.0.1:1081' [output] #输出配置 columns = [''] # 直接在控制台显示 console = true # 打包出了点问题暂时不可用 # markdown格式的输出文件(Typora等软件可以打开) markdown = true # xlsx格式的输出文件(Excel可以打开) xlsx = true # 论坛使用的BBCODE bbcode = true # 导出Json,方便二次开发 json = true [sort] # 排序设置 ################### # 排序功能暂未实装 # ################### # 使用数字表示排序方式,负数表示逆向排序 # 例如: # 1: appid 从小到大 # -1: appid 从大到小 (以下类似) # 0: 不排序 (按照Steam返回的顺序输出) # 1: appid 从小到大 # 2: 史低 -> 近史低 -> 未达史低 ,没有价格的项目始终放在最下面 # 3: 折扣幅度 从小到大 ,没有价格的项目始终放在最下面 # 4: 现价 从低到高 ,没有价格的项目始终放在最下面 # 5: 原价 从低到高 ,没有价格的项目始终放在最下面 # 6: 好评率 从低到高 # 7: 类型 游戏 -> 软件 -> DLC -> 视频 # [0-7]:选择排序依据,如果排名相同会按照appid从小到大排序 index = 1 [filter] # 过滤器设置 # 错误的配置有可能得到意外的结果 # 类似于白名单,【过滤掉】的意思是【删掉】 # 任意规则的值设置为 -1 代表禁用该条规则 # -----其他过滤器--------------------------------------- # true:过滤掉 没有 卡牌的游戏, false/-1:禁用 has_card = -1 # -----价格过滤器--------------------------------------- # 如果游戏未发售,视为 没有价格 # 如果价格获取出错,视为 没有价格 # 免费游戏价格视为 0,但是既不算有价格也不算没有价格 # false:过滤掉 免费 的游戏, true/-1:禁用 price_free = -1 # false:过滤掉 有价格 的游戏, true/-1:禁用 price_set = -1 # false:过滤掉 没有价格 的游戏, true/-1:禁用 price_noset = -1 # 下面的价格过滤器只对有价格的游戏有效,忽略没有价格和免费的游戏 # 任意数值:过滤掉当前价格 低于 设定值的游戏, -1:禁用 price_higher = -1 # 任意数值:过滤掉当前价格 高于 设定值的游戏, -1:禁用 price_lower = -1 # -----折扣过滤器--------------------------------------- # 折扣过滤器只对有价格的游戏有效,忽略没有价格和免费的游戏 # 折扣计算方法: (原价 - 现价) / 原价 * 100 # 免费游戏和没有价格的游戏折扣视为 0 # 未打折折扣视为 0, 限免折扣视为 100 # false:过滤掉 未打折 的游戏, true/-1:禁用 discount_no = -1 # 下面的折扣过滤器只对有折扣的游戏有效,忽略未打折的游戏 # [1-100]:过滤掉当前折扣 低于 设定值的游戏, -1:禁用 discount_higher = -1 # [1-100]:过滤掉当前折扣 高于 设定值的游戏, -1:禁用 discount_lower = -1 # 近史低条件: (现价-史低价) / 原价 <= 5% # 一个游戏只有一种状态,要么是史低,要么是近史低,要么是非史低 # false:过滤掉 未达到 史低或近史低的游戏, true/-1:禁用 discount_not_lowest = -1 # false:过滤掉 达到 史低的游戏, true/-1:禁用 discount_is_lowest = -1 # false:过滤掉 达到 近史低的游戏, true/-1:禁用 discount_almost_lowest = -1 # -----评测过滤器--------------------------------------- # 评价等级说明: # 0:评测数量不足 (未发售和预售游戏会显示这个) # 1->4:差评如潮 -> 特别差评 -> 差评 -> 多半差评 # 5:褒贬不一 # 6->9:多半好评 -> 好评 -> 特别好评 -> 好评如潮 # false:过滤掉 评测数量不足(等级0) 的游戏, true/-1: 禁用 review_no_score = -1 # 下面的过滤器都会 忽略 评测数量不足 的游戏 # [1-9]:过滤掉评价等级 低于 设定值的游戏, 0和-1:禁用 review_score_higher = -1 # [1-9]:过滤掉评价等级 高于 设定值的游戏, 0和-1:禁用 review_score_lower = -1 # [0-100]:过滤掉好评率 低于 设定值的游戏, -1:禁用 review_percent_higher = -1 # [0-100]:过滤掉好评率 高于 设定值的游戏, -1:禁用 review_percent_lower = -1 # 任意数值:过滤掉总评价数 低于 设定值的游戏, -1:禁用 review_total_higher = -1 # 任意数值:过滤掉总评价数 高于 设定值的游戏, -1:禁用 review_total_lower = -1 # -----平台过滤器--------------------------------------- # true: 过滤掉 不支持 Windows的游戏, false: 不过滤, -1: 禁用 platform_windows = -1 # true: 过滤掉 不支持 Mac的游戏, false: 不过滤, -1: 禁用 platform_mac = -1 # true: 过滤掉 不支持 Linux的游戏, false: 不过滤, -1: 禁用 platform_linux = -1 # PS: Steam上真的有不支持Windows的游戏吗? # -----标签过滤器--------------------------------------- # 游戏标签名称由Steam语言决定,不同的语言会获取到不同的标签 # false: 过滤掉 没有 标签的游戏, true/-1:禁用 tags_enpty = -1 # 下面的过滤器都会 忽略 没有 标签的游戏 # 过滤规则示例: # 游戏A: '射击','动漫','动作','弹幕射击' # 游戏B: '视觉小说','动漫','女性主角','冒险','独立' # 游戏C: '休闲','冒险','独立' # tags_include = ['动漫'] -> 过滤后 -> 游戏A,游戏B # tags_include = ['动漫','射击'] -> 过滤后 -> 游戏A # tags_exclude = ['休闲'] -> 过滤后 -> 游戏C # tags_exclude = ['冒险','独立'] -> 过滤后 -> 游戏B,游戏C # 可以只写标签名称的一部分,只要标签中包含设置值就算匹配 # 如果设置了多个标签,则需要每个设置值都被匹配才算通过匹配 # 错误的配置可能得到空结果,例如: # tags_include = ['射击游戏'] -> 过滤后 -> 无匹配 # 得到空结果的原因是没有游戏有'射击游戏'这个标签 # 任意文本:过滤掉标签 不匹配 规则的游戏, -1: 禁用 tags_include = -1 # tags_include = ['标签1'] # tags_include = ['标签1','标签2'] # 任意文本:过滤掉标签 匹配 规则的游戏, -1: 禁用 tags_exclude = -1 # tags_exclude = ['标签1'] # tags_exclude = ['标签1','标签2'] # 可以设置更多标签,比如['标签1','标签2','标签3','标签4'] 国家区域代码和货币单位参考 区域 货币 Region Country 欧洲 € eu1 AL, AD, AT, BE, DK, FI, FR, DE, IE, LI, LU, MK, NL, SE, CH 欧洲 € eu2 BA, BG, HR, CY, CZ, GR, HU, IT, MT, MC, ME, NO, PL, PT, RO, SM, RS, SK, SI, ES, VA, EE, LV, LT" 英国 £ uk GB 美国 $ us US 加拿大 $ ca CA 巴西 R$ br2 BR 澳大利亚 A$ au2 AU 俄罗斯 py6 ru RU 土耳其 £T tr TR 中国 ¥ cn CN 过滤器配置参考 未完待续
前言 x> 双十一活动已经过期，本文仅作存档 本脚本仅供学习交流之用，使用者如何使用与本文作者无关。 AutoJS下载 & 教程 [post cid="1235" /] 全民营业脚本 i> 如果遇到问题请在下方评论 脚本 //遇到问题请在 https://chrxw.com 留言 //Ver 0.6 2020.10.31 //By Chr_(chr@chrxw.com) //========================= //功能配置 //自动加入会员（设为false禁用) var auto_join_vip = true; //自动完成加购任务（设为false禁用) var auto_add_cart = true; //========================= auto.waitFor(); // console.show(); toast('\n脚本开始运行\n请切换到任务页'); for (var i = 0; i < 10; i++) { //等待切换到任务页 if (className("android.view.View").text("收取金币").exists() || className("android.view.View").text("立即解锁").exists()) { log('break'); break; } rsleep(1); } if (!className("android.view.View").textStartsWith("邀请好友助力").exists()) { advclick(className("android.view.View").text("领金币").findOnce()); } toast("\n即将开始工作\n中途需退出请按【音量+】终止"); sleep(2000); for (; ;) { var flag = false; var objs = text("去完成").find(); for (var i = 0; i < 10; i++) { if (objs[i] != null) { obj = objs[i].parent(); if (obj.children() >= 8) { break; } } } var tasks = obj.children(); for (var i = 2; i < tasks.length; i += 4) { var txt = tasks[i].text(); log(txt, i); if (txt.search("8秒") != -1) { var btn = tasks[i + 1]; if (btn.text() == "去完成") { advclick(btn); view(); flag = true; } } else if (txt.search("浏览可得") != -1) { var btn = tasks[i + 1]; if (btn.text() == "去完成") { advclick(btn); view2(); flag = true; } } else if (auto_join_vip && txt.search("成功入会可得") != -1) { var btn = tasks[i + 1]; if (btn.text() == "去完成") { advclick(btn); view3(); flag = true; } } else if (auto_add_cart && txt.search("5个") != -1) { var btn = tasks[i + 1]; if (btn.text() == "去完成") { advclick(btn); view4(); flag = true; } } if (flag) { break; } } if (!flag) { toast("\n脚本运行完毕\n模拟浏览任务全部完成（大概）"); break; } } //模拟浏览（浏览会场任务） function view() { rsleep(3); rslide(10); // className("android.widget.Image").text("vk image").findOne(5000); rslideR(1); rsleep(3); advback(); } //模拟浏览（直接返回） function view2() { rsleep(2); back(); rsleep(2); } //模拟浏览（开卡） function view3() { rsleep(2); var btn = className("android.view.View").text("确认授权并加入店铺会员").findOne(5000); log(btn); if (btn) { advclick(btn); className("android.view.View").text("恭喜您已集齐所有会员卡").findOne(2000); rsleep(2); advback(); } } //模拟浏览（浏览商品、加购任务） function view4() { rsleep(3); var add_cart = true; var count = 4; if (className("android.view.View").text("任意浏览以下5个商品").exists()) { var add_cart = false; var count = 4; } else { var add_cart = true; var count = 5; } log(add_cart ? "加购模式" : "浏览模式"); for (var i = 0; i < 5; i++) { var prices = className("android.view.View").textMatches("^¥[0-9]+\.[0-9][0-9]").untilFind(); log(i, prices[i].text()); var good = prices[i].parent().parent(); if (good.childCount() > count) { log('已完成，跳过'); continue; } if (!add_cart) { advclick(good); rsleep(2); rslide(1); className("android.widget.ImageView").desc("返回").click(); } else { good.child(4).click(); } rslide(1); rsleep(2); } advback(); rsleep(3); } //随机延时 function rsleep(s) { while (s--) { sleep(random(900, 1200)); } } //模拟返回 function advback() { for (var i = 0; i < 3; i++) { if (!className("android.widget.ImageView").desc("返回").exists()) { rslideR(1); rsleep(2); continue; } className("android.widget.ImageView").desc("返回").click(); rsleep(5); if (className("android.view.View").text("领金币").exists()) { return; } } log("定位不到返回按钮，模拟返回键"); back(); } //随机划屏 function rslide(i) { while (i--) { var x1 = random(200, 900); var y1 = random(1200, 1900); var x2 = random(200, 900); var y2 = random(1000, 1200); swipe(x1, y1, x2, y2, 300); rsleep(1); } } //随机划屏，反向 function rslideR(i) { while (i--) { var x1 = random(200, 900); var y1 = random(900, 1300); var x2 = random(200, 900); var y2 = random(1500, 1900); swipe(x1, y1, x2, y2, 300); rsleep(1); } } function advclick(uiobject) { var rect = uiobject.bounds(); // var x = random(rect.left, rect.right); // var y = random(rect.top, rect.bottom); click(rect.centerX(), rect.centerY()); } 说明 加了几个功能开关，可以禁用部分功能。 修正了几个影响使用的bug。 支持浏览，开卡，加购等任务。 修复无法退出的bug。 修复了无法运行的bug。 每日逛店脚本 活动页最下面每日逛店也有少量金币，不过受益比较感人，就不整合了，需要的自取 //遇到问题请在 https://chrxw.com 留言 //Ver 0.1 2020.10.27 //By Chr_(chr@chrxw.com) auto.waitFor(); toast('\n脚本开始运行\n请切换到任务页'); className("android.view.View").textStartsWith("宝箱随机藏在以下店铺内").waitFor(); for (var k = 0; k < 10; k++) { count = className("android.view.View").text("寻宝箱 领金币").findOne().parent().child(2).childCount(); if (count > 0) { toast("\n即将开始工作\n中途需退出请按【音量+】终止"); for (var i = 0; i < 20; i++) { var shops = className("android.view.View").text("寻宝箱 领金币").findOne().parent().child(2).children(); shops[i].click(); rsleep(4); if (className("android.view.View").text("每日签到 (0/1)").findOne(100) && className("android.view.View").text("去完成").findOne(100)) { toast('达到每日上限，任务结束'); back(); break; } back(); } toast('工作完成,总计', i, '个店铺'); break; } else { toast('获取不到店铺列表，请手动划到活动页最下面'); } } //随机延时 function rsleep(s) { while (s--) { sleep(random(900, 1200)); } } 热爱时光机脚本 脚本 //遇到问题请在 https://chrxw.com 留言 //Ver 0.1 2020.11.1 //By Chr_(chr@chrxw.com) //========================= //功能配置 //========================= auto.waitFor(); //console.show(); // toast('\n脚本开始运行\n请切换到品牌狂欢城任务页'); className("android.view.View").text("热爱时光机").waitFor(); // toast("\n即将开始工作\n中途需退出请按【音量+】终止"); // sleep(2000); for (; ;) { if (!className("android.view.View").textStartsWith("邀请好友一起玩").exists()) { get_task_btn().click(); rsleep(2); } var flag = false; var objs = text("去完成").find(); for (var i = 0; i < objs.size(); i++) { if (objs[i] != null) { obj = objs[i].parent(); if (obj.children().size() == 2) { break; } } } var tasks = obj.parent().parent().children(); for (var i = 0; i < tasks.length; i++) { var txt = tasks[i].child(1).text(); var btn = tasks[i].child(3).child(0); if (txt.search("超级") != -1) { if (btn.text() == "去完成") { log(1, txt); btn.click(); rsleep(2); view2(); flag = true; } } else if (txt.search("大牌") != -1) { if (btn.text() == "去完成") { log(2, txt); btn.click(); rsleep(2); view3(); flag = true; } } else if (txt.search("精选") != -1) { if (btn.text() == "去完成") { log(3, txt); btn.click(); view(); flag = true; } // 有bug，暂时不能使用 // } else if (txt.search("同城") != -1) { // if (btn.text() == "去完成") { // log(3, txt); // btn.click(); // view(); // flag = true; // } } if (flag) { break; } } if (!flag) { toast("\n脚本运行完毕\n模拟浏览任务全部完成（大概）"); break; } } //模拟浏览（直接返回） function view() { rsleep(2); advback(); rsleep(2); } //模拟浏览（主页浏览任务1） function view2() { var task = className("android.view.View").textStartsWith("逛超级品牌立得100").findOnce(1); if (task) { var str = task.text(); var nums = str.match(/\d+/g); var curr = nums[1]; var max = nums[2]; while (curr < max) { toastLog('逛超级品牌任务', curr); className("android.view.View").textStartsWith("逛超级品牌立得100").findOnce(1).parent().child(2).child(0).child(1).click(); view(); curr++; } } } //模拟浏览（主页浏览任务2） function view3() { var task = className("android.view.View").textStartsWith("逛大牌品牌立得50").findOnce(1); if (task) { var str = task.text(); var nums = str.match(/\d+/g); var curr = nums[1]; var max = nums[2]; toastLog(nums) while (curr < max) { toastLog('逛大牌品牌任务' + curr); className("android.view.View").textStartsWith("逛大牌品牌立得50").findOnce(1).parent().child(1).child(1).click(); view(); curr++; } } } //随机延时 function rsleep(s) { while (s--) { sleep(random(900, 1200)); } } //模拟返回 function advback() { for (var i = 0; i < 3; i++) { if (!className("android.widget.ImageView").desc("返回").exists()) { rslideR(1); rsleep(2); continue; } className("android.widget.ImageView").desc("返回").click(); rsleep(5); if (className("android.view.View").text("热爱时光机").exists()) { return; } } log("定位不到返回按钮，模拟返回键"); back(); } //随机划屏 function rslide(i) { while (i--) { var x1 = random(200, 900); var y1 = random(1200, 1900); var x2 = random(200, 900); var y2 = random(1000, 1200); swipe(x1, y1, x2, y2, 300); rsleep(1); } } //随机划屏，反向 function rslideR(i) { while (i--) { var x1 = random(200, 900); var y1 = random(900, 1300); var x2 = random(200, 900); var y2 = random(1500, 1900); swipe(x1, y1, x2, y2, 300); rsleep(1); } } //高级点击 function advclick(uiobject) { var rect = uiobject.bounds(); // var x = random(rect.left, rect.right); // var y = random(rect.top, rect.bottom); click(rect.centerX(), rect.centerY()); } //获取任务按钮 function get_task_btn() { var tips = className("android.view.View").textMatches("剩余\\d+能量").findOnce(); var btn = tips.parent().parent().child(4); return (btn); } 说明 此为第一版，可能存在bug，如有遇到请在此反馈。
前言 DirectoryLister是一款非常小巧的PHP文件浏览器，适合搭建一个简单的个人文件站，之前的教程使用的版本为2.7.1，现在的最新版本为3.5.0，新增了文件夹说明、文件搜索等功能。 我的示例站点：链接 安装步骤 环境需求 PHP版本为 7.2 及以上 安装PHP的 zip、DOM、Fileinfo 扩展，前两个宝塔面板自带，只需安装Fileinfo 下载源码 官方网站 GitHub 本地镜像 @> 本站只提供3.5.0版本下载，最新版本请从官方渠道下载 安装 下载好以后，解压到网站根目录，将 .env.example 文件重命名为 .env 文件，然后用编辑器打开 @> Windows下如果无法重命名的话，可以使用命令 rename .env.example .env !> 如果修改了 .env 文件但是配置不生效，解禁PHP的 putenv 函数即可 .env 配置文件说明 # 调试模式开关 APP_DEBUG=false # 语言设置(zh-CN即简体中文) APP_LANGUAGE=zh-CN # # 深色模式开关(蓝白/紫黑) DARK_MODE=false # 是否在文件列表下方显示Readme.md文件(如果存在Readme.md文件) DISPLAY_READMES=true # 在文件列表上方显示Readme.md文件(需要DISPLAY_READMES=true) READMES_FIRST=false # 是否允许将整个目录打包成ZIP下载(搜索栏左侧会显示打包下载按钮) ZIP_DOWNLOADS=true # # Google Analyitcs设置(要使用的话设置成资源ID即可) GOOGLE_ANALYTICS_ID=false # GOOGLE_ANALYTICS_ID=UA-XXXXXXXXX-X # # MATOMO_ANALYTICS设置 MATOMO_ANALYTICS_URL=false MATOMO_ANALYTICS_ID=false # # 排序设置，可选值如下，排序方式区别见下 # type, natural, name, accessed, changed, modified SORT_ORDER=type # 逆序排序开关 REVERSE_SORT=false # # 隐藏app目录 HIDE_APP_FILES=true # 隐藏.git目录 HIDE_VCS_FILES=true # # 时间格式 DATE_FORMAT="Y-m-d H:i:s" # 时区设置（Asia/Shanghai即上海时间） TIMEZONE="Asia/Shanghai" # # 可以计算Hash的文件的最大尺寸(单位是字节Bytes) MAX_HASH_SIZE=1000000000 # # 主页标题，显示在网页左上角(默认为“目录起始页”) HOME_TEXT="文件下载" # 页面缓存设置，设为false禁用缓存(默认值为"app/cache/views") VIEW_CACHE="app/cache/views" 如果能正确显示网页，说明安装成功了: 定制 i> 修改后需要清除页面缓存后才能看到效果，默认缓存目录为 app/cache/ ，删掉整个目录即可 网站标题优化 将 Home 修改成中文 文件路径为 app\src\Controllers\DirectoryController.php 改成想要的值，比如“主页” 定制标题格式 文件位置在 app\views\layouts\app.twig {{ title }} 是占位符，用来显示当前目录的名称，其他的文本都可以任意改，如果使用特殊符号记得转义 常用符号转义值：" ",& &,< <,> > EXE图标显示空白解决方法 打开app\config\icons.php 把所有 fas fa window 替换成 fab fa windows 即可 @> 原因是新版的 Font Awesome 貌似需要Pro才有这个图标…… 中文字体优化 修改版 app.css 下载: 链接 文件路径为 app\assets\app.css 隐藏特定的文件 在网站根目录下新建一个 .hidden 文本文件，在里面写入希望隐藏的文件名或者路径名即可，支持正则表达式 页面自定义 页眉模板文件：app/views/components/header.twig 页脚模板文件：app/views/components/footer.twig CSS文件：app/assets/app.css 本地化文件：app/translations/zh-CN.yaml