news 2026/9/4 4:25:00

CefSharp多账号浏览器环境隔离与指纹修改实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CefSharp多账号浏览器环境隔离与指纹修改实战指南

简介:本资源是一套基于C#与CEFSharp实现多账号并发登录的完整工程实践方案,面向Web自动化、爬虫开发及安全测试领域的中高级开发者,解决多账户Cookie隔离、浏览器指纹混淆及反反爬集成等核心难题。压缩包含873个文件,主体为344个C#源码文件(含MultiAccount.csproj等核心项目结构)、174个Chromium资源pak文件、114个C++头文件及52个动态链接库,整体达376.59MB,完整保留了CEF环境初始化、独立IRequestContext配置、JS注入式指纹修改(UserAgent/Canvas/ WebGL等)及自动化加购逻辑等关键模块。已有4438人学习下载,提供可直接编译运行的Visual Studio解决方案(含sln、csproj、config)、调试符号pdb、资源文件resx与日志log,目录组织清晰,便于快速定位多实例浏览器创建、Cookie上下文绑定与指纹对抗代码段。

1. 项目概述与核心需求解析

最近在做一个C#的桌面客户端项目,里面需要嵌入一个浏览器组件来操作一些Web应用。需求听起来挺简单,但实际做起来坑不少:用户需要在这个客户端里同时登录多个账号,比如管理几个不同的社交媒体或者电商后台。直接用系统自带的WebBrowser控件或者一个普通的CefSharp实例,你会发现所有账号的Cookie都混在一起,A账号的操作会影响到B账号,甚至直接导致串号、被封号。这背后的核心问题,就是浏览器环境的隔离。

所以,这个项目的目标很明确:基于CefSharp,实现一套机制,让多个浏览器实例能完全独立运行,每个实例拥有自己独立的Cookie存储、独立的本地存储(LocalStorage、IndexedDB),甚至能修改一部分浏览器指纹信息,让每个账号的浏览环境看起来都像是一台独立的、干净的电脑。这不仅仅是“多开几个窗口”那么简单,它涉及到Chromium嵌入式框架的进程模型、资源隔离策略以及一些底层的浏览器行为模拟。

2. CefSharp基础与多实例架构设计

2.1 为什么选择CefSharp?

在C#的桌面开发里,嵌入浏览器的主流选择有几个:古老的WebBrowser(IE内核)、WebView2(基于Edge Chromium)、以及CefSharp。WebBrowser太老且兼容性差,首先排除。WebView2是微软的新宠,依赖系统运行时,更轻量,但在多实例深度隔离和指纹修改的灵活性上,目前还是CefSharp更胜一筹。CefSharp本质上是将开源的Chromium Embedded Framework (CEF)封装成了.NET可用的控件,它提供了对底层Chromium几乎完全的控制能力,这正是我们实现精细隔离和修改所必需的。

2.2 理解CefSharp的进程模型:Browser vs. Render Process

要实现真正的隔离,必须理解CefSharp(或者说Chromium)的多进程架构。默认情况下,一个CefSharp应用启动时,会有一个主进程(Browser Process),负责窗口管理、网络请求、Cookie存储等。每个标签页或浏览器实例对应一个渲染进程(Render Process),负责解析HTML、执行JavaScript。但是,默认的Cookie和本地存储是Browser Process管理的,所有Render Process共享。这就是为什么简单创建多个ChromiumWebBrowser控件,Cookie还是会串。

解决方案的核心在于CefSettings中的CachePathRootCachePath,以及为每个浏览器实例指定唯一的RequestContextRequestContext是请求上下文,它管理着网络堆栈的配置,包括Cookie存储、缓存、权限设置等。通过为每个需要隔离的账号创建独立的RequestContext(并指向不同的磁盘缓存目录),我们就能在进程层面实现资源的物理隔离。

2.3 多账号隔离的架构设计思路

我的设计思路是:“一进程一上下文,一账号一目录”

  1. 独立缓存目录:为每个账号预先分配一个唯一的、隔离的磁盘文件夹。这个文件夹将存放该账号所有的Cookie、LocalStorage、缓存文件等。
  2. 独立请求上下文:每个ChromiumWebBrowser实例都绑定一个独立的CefRequestContext,并将该上下文的缓存路径指向步骤1中的独立目录。
  3. 浏览器实例管理:在UI层(如WPF的TabControl),每个Tab项内嵌一个独立的ChromiumWebBrowser控件,每个控件都使用步骤2中创建的独立上下文进行初始化。
  4. 指纹修改集成:指纹修改的操作,主要通过在这个独立的RequestContext上设置偏好(Preferences)、或在页面加载时注入JavaScript来修改navigator等对象属性实现。

这样,从磁盘存储到内存中的网络上下文,每个账号的浏览环境都是完全沙盒化的。即使两个实例访问同一个网站,它们之间也看不到对方的登录状态和数据。

3. 核心实现:Cookie与本地存储的深度隔离

3.1 初始化CefSharp与全局设置

首先,必须在App启动时(比如App.xaml.csOnStartup中)初始化CEF。这里的关键是设置RootCachePath,并启用缓存。

public partial class App : Application { protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); var settings = new CefSettings(); // 设置一个总的根缓存目录,子目录将在这里面创建 settings.RootCachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "MyApp", "CefCache"); // 启用缓存,这对于Cookie和本地存储的持久化至关重要 settings.CachePath = "cache"; // 关闭GPU加速,有时能避免一些渲染进程的奇怪问题 settings.CefCommandLineArgs.Add("disable-gpu", "1"); settings.CefCommandLineArgs.Add("disable-gpu-compositing", "1"); // 必须设置为单进程模式吗?不,我们采用多进程,但通过上下文隔离。 // settings.CefCommandLineArgs.Add("single-process", "1"); // 调试时可开,生产环境不建议。 // 其他必要设置 settings.LogSeverity = LogSeverity.Disable; // 禁用日志,减少IO settings.WindowlessRenderingEnabled = true; // WPF需要这个 Cef.Initialize(settings, performDependencyCheck: true, browserProcessHandler: null); } }

注意RootCachePathCachePath的区别。RootCachePath是所有缓存数据的根目录,而CachePath是相对于RootCachePath的子路径。通常我们只需设置RootCachePath,然后为每个实例的RequestContext指定完整的独立路径。

3.2 创建独立请求上下文与浏览器实例

这是隔离的核心。我们创建一个工厂方法,用于生成一个完全独立的浏览器实例。

public ChromiumWebBrowser CreateIsolatedBrowser(string accountId, string initialUrl = "about:blank") { // 1. 为该账号创建独立的缓存目录 string cachePath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "MyApp", "IsolatedProfiles", accountId // 使用账号ID作为目录名 ); if (!Directory.Exists(cachePath)) { Directory.CreateDirectory(cachePath); } // 2. 创建请求上下文设置 var contextSettings = new RequestContextSettings { CachePath = cachePath, // 关键:指定独立缓存路径 PersistSessionCookies = true, // 持久化会话Cookie PersistUserPreferences = true // 持久化用户偏好 }; // 3. 创建独立的请求上下文 var requestContext = new CefRequestContext(contextSettings); // 4. 创建浏览器实例,并绑定独立上下文 var browser = new ChromiumWebBrowser(initialUrl) { RequestContext = requestContext // 关键:关联独立上下文 }; // 5. (可选)在此上下文中设置一些初始偏好,例如禁用WebRTC以降低指纹追踪 var preferences = requestContext.GetAllPreferences(true); // 可以通过 preferences.SetValue("webrtc.ip_handling_policy", "disable_non_proxied_udp") 等方式修改 // 但更复杂的指纹修改通常通过JS注入实现。 return browser; }

在实际的WPF窗口中,你可以这样使用:

public partial class MainWindow : Window { private Dictionary<string, ChromiumWebBrowser> _browserInstances = new(); public MainWindow() { InitializeComponent(); Loaded += MainWindow_Loaded; } private void MainWindow_Loaded(object sender, RoutedEventArgs e) { // 模拟创建两个账号的浏览器 var browserForAccountA = CreateIsolatedBrowser("Account_A", "https://login.example.com"); var browserForAccountB = CreateIsolatedBrowser("Account_B", "https://login.example.com"); // 将浏览器控件添加到不同的TabItem中 tabAccountA.Content = browserForAccountA; tabAccountB.Content = browserForAccountB; // 保存引用以便后续管理 _browserInstances["Account_A"] = browserForAccountA; _browserInstances["Account_B"] = browserForAccountB; } }

3.3 验证隔离效果

如何验证Cookie真的隔离了?一个简单的测试方法是:

  1. Account_A的浏览器中登录一个网站(如Gitee)。
  2. 然后在Account_B的浏览器中访问同一个网站。
  3. 如果Account_B显示未登录状态,说明隔离成功。你还可以检查两个账号对应的缓存目录,会发现里面生成了完全不同的CookiesLocal Storage等数据库文件。

实操心得CachePath目录的权限很重要。如果程序没有写入权限,会导致Cookie无法持久化,变成“内存Cookie”,浏览器关闭就消失。确保你的程序有在LocalApplicationData下创建和写入文件的权限。另外,首次为某个RequestContext指定一个全新的CachePath时,浏览器启动可能会稍慢,因为它需要初始化新的存储结构。

4. 浏览器指纹修改的原理与实现

4.1 什么是浏览器指纹?

浏览器指纹是指网站通过JavaScript收集你浏览器和系统的各种属性信息,如用户代理(User-Agent)、屏幕分辨率、时区、安装的字体列表、WebGL渲染器、Canvas图像哈希等。这些信息组合起来,几乎可以唯一标识一台设备。即使你清除了Cookie,网站通过指纹也能认出你。我们的目标就是让多个CefSharp实例报告不同的指纹信息,增加区分的难度。

4.2 修改指纹的两种主要途径

途径一:通过CefSettings和命令行参数修改(全局或上下文级)

这种方法主要修改一些基础且稳定的属性。

  • User-Agent: 这是最基础的指纹。可以在创建ChromiumWebBrowser时直接设置BrowserSettings,或者通过CefSettings.CefCommandLineArgs添加--user-agent参数。但更精细的做法是在RequestContext中通过SetPreference来设置。
    // 在创建RequestContext后设置 var preferences = requestContext.GetAllPreferences(true); // 注意:修改User-Agent可能影响网站兼容性 preferences.SetValue("user_agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 MyCustomAgent"); preferences.Commit();
  • 语言和时区: 可以通过命令行参数模拟。
    settings.CefCommandLineArgs.Add("--lang", "zh-CN"); // 时区需要在JS注入中修改,命令行参数影响有限。
  • 禁用特性: 禁用一些可能暴露硬件信息的特性。
    settings.CefCommandLineArgs.Add("--disable-features", "WebRtcHideLocalIpsWithMdns"); // 禁用WebRTC的mDNS // 在RequestContext的偏好中禁用WebRTC preferences.SetValue("webrtc.ip_handling_policy", "disable_non_proxied_udp");

途径二:通过JavaScript注入修改(页面级)

这是修改指纹最灵活、最强大的方式,主要针对通过navigatorscreenwindow等对象暴露的属性。我们可以在页面加载开始时(FrameLoadStartLoadEnd事件中)执行JS代码来覆盖这些属性。

browser.FrameLoadStart += (sender, args) => { // 确保在主框架加载开始时注入 if (args.Frame.IsMain) { // 构建指纹修改脚本 string fingerprintScript = @" // 覆盖navigator属性 Object.defineProperty(navigator, 'userAgent', { get: () => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' }); Object.defineProperty(navigator, 'platform', { get: () => 'Win64' }); Object.defineProperty(navigator, 'language', { get: () => 'zh-CN' }); Object.defineProperty(navigator, 'languages', { get: () => ['zh-CN', 'zh', 'en-US', 'en'] }); Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 4 // 修改逻辑核心数 }); // 覆盖screen属性(需谨慎,可能影响页面布局) Object.defineProperty(screen, 'width', { get: () => 1920 }); Object.defineProperty(screen, 'height', { get: () => 1080 }); Object.defineProperty(screen, 'colorDepth', { get: () => 24 }); // 修改时区(这个比较棘手,只能修改返回值,系统时区很难变) Date.prototype.getTimezoneOffset = function() { return -480; }; // 模拟东八区 // 注意:Intl.DateTimeFormat().resolvedOptions().timeZone 这个属性很难被简单覆盖 // Canvas指纹干扰 - 注入噪声 const originalGetContext = HTMLCanvasElement.prototype.getContext; HTMLCanvasElement.prototype.getContext = function(type, attributes) { const context = originalGetContext.call(this, type, attributes); if (type === '2d') { const originalFillText = context.fillText; context.fillText = function(text, x, y, maxWidth) { // 在绘制文本时添加极微小的随机偏移,改变最终Canvas哈希 x += (Math.random() - 0.5) * 0.1; y += (Math.random() - 0.5) * 0.1; originalFillText.call(context, text, x, y, maxWidth); }; } return context; }; "; // 执行脚本 args.Frame.ExecuteJavaScriptAsync(fingerprintScript); } };

4.3 指纹修改的注意事项与局限性

  1. 属性可枚举性: 简单的赋值(如navigator.userAgent = 'new value')对很多只读属性无效。必须使用Object.defineProperty来重新定义属性的getter。
  2. 执行时机: 必须在网站自己的脚本执行之前完成注入,否则网站可能已经读取并缓存了原始值。FrameLoadStart是一个相对较早的时机,但对于一些单页应用(SPA),可能还需要监听后续的导航事件。
  3. 兼容性风险: 修改userAgent可能导致某些网站CSS或JS功能异常。修改screen尺寸可能导致响应式布局错乱。务必针对目标网站进行充分测试。
  4. 对抗升级: 网站的指纹脚本也在不断升级,会检测属性是否被篡改(例如,检查Object.getOwnPropertyDescriptor(navigator, 'userAgent'))。更高级的对抗需要更复杂的JS混淆和模拟。
  5. 硬件指纹: WebGL渲染器、音频上下文指纹等非常底层,通过JS注入很难完美修改,通常需要更底层的CEF C++接口甚至修改Chromium源码,这超出了CefSharp常规使用的范围。

踩坑记录:我曾尝试大规模修改字体列表指纹,发现非常困难。网站可以通过document.fonts.check()或测量字符宽度来探测可用字体。简单的覆盖navigator.pluginsnavigator.mimeTypes列表也容易被检测出数组长度不一致。对于高安全要求的场景,指纹修改更像是一场“军备竞赛”,需要持续研究和调整策略。

5. 高级功能与性能优化

5.1 账号配置的持久化管理

我们不可能每次启动都手动创建账号。需要一套管理机制。

  1. 配置文件: 为每个账号创建一个JSON配置文件,存储账号ID、缓存路径、自定义的指纹配置(如特定的User-Agent字符串、屏幕分辨率等)。
  2. 管理器类: 创建一个BrowserProfileManager类,负责加载所有账号配置,并在程序启动时批量创建对应的CefRequestContextChromiumWebBrowser实例。
  3. 状态保存: 监听浏览器关闭事件,将每个浏览器实例最后访问的URL保存到配置中,实现会话恢复。

5.2 资源占用与内存优化

每个独立的RequestContext都意味着一个独立的存储和网络栈,会占用额外的内存和磁盘空间。当同时打开几十个甚至上百个实例时,资源消耗会非常可观。

  • 内存优化
    • 禁用不必要的功能: 在CefSettingsBrowserSettings中关闭不需要的功能,如JavascriptOpenWindowsJavascriptCloseWindowsImageLoading(如果不需要显示图片)。
    • 释放闲置实例: 对于不活动的Tab,可以考虑将对应的ChromiumWebBrowser控件从视觉树卸载,并调用browser.Dispose()释放底层CefBrowser,但保留RequestContext和缓存目录。需要时再重新创建浏览器实例并加载之前的URL。
  • 磁盘优化
    • 定期清理: 实现一个清理任务,定期删除长时间未使用的账号缓存目录。
    • 使用内存盘(RAM Disk): 对于追求极致速度且数据可丢失的场景,可以将CachePath指向内存盘。但这会显著增加内存消耗。

5.3 自动化与防检测策略

单纯的环境隔离和指纹修改可能还不够,一些高级风控会检测自动化行为。

  • 模拟人类操作: 集成自动化框架(如Puppeteer Sharp,但需适配CefSharp)时,在点击、输入等操作之间加入随机延迟和移动轨迹。
  • WebDriver属性: 通过JS注入,覆盖navigator.webdrivernavigator.chromewindow.chrome等属性,使其返回undefinedfalse,以隐藏自动化痕迹。
  • CDP(Chrome DevTools Protocol)覆盖: 一些检测会通过Chrome DevTools Protocol来探查。CefSharp暴露了IBrowserGetHost().GetDevToolsUrl(),但默认不开启。非必要时不要开启DevTools远程调试。

6. 常见问题与调试技巧实录

6.1 问题排查清单

问题现象可能原因排查步骤与解决方案
Cookie不保存,关闭程序就丢失1.CachePath路径权限不足。
2.RequestContextSettings.PersistSessionCookies未设置为true
3. 浏览器进程异常退出。
1. 检查缓存目录是否存在且可写。以管理员身份运行一次试试。
2. 确保创建RequestContextSettings时设置了持久化选项。
3. 在程序退出时,确保调用Cef.Shutdown()前给浏览器足够时间保存数据。
多个实例间Cookie仍然串扰1. 多个ChromiumWebBrowser实例共用了同一个RequestContext(或默认的全局上下文)。
2. 缓存目录路径重复。
1. 检查每个ChromiumWebBrowserRequestContext属性是否确实是独立创建的实例。
2. 打印或记录每个实例的CachePath,确保它们完全不同。
修改的User-Agent不生效1. JS注入时机太晚,网站已读取旧值。
2. 通过SetPreference设置的User-Agent对某些请求(如iframe、XHR)不生效。
3. 网站从HTTP请求头中读取User-Agent。
1. 尝试在FrameLoadStart甚至更早的OnBeforeBrowser事件中注入JS。
2. 结合使用命令行参数--user-agent和JS覆盖。
3. CefSharp的请求拦截器(IRequestHandler)可以修改请求头,这是最彻底的方法。
程序崩溃,特别是多实例时1. CEF未正确初始化或关闭。
2. 多线程访问CefSharp对象不当。
3. 资源(内存、句柄)耗尽。
1. 确保Cef.Initialize()在主UI线程且程序启动时只调用一次。Cef.Shutdown()在程序退出时调用。
2. 所有对ChromiumWebBrowser对象的操作(除了构造函数)都必须在UI线程上(Dispatcher.Invoke)。
3. 使用任务管理器监控进程内存和GDI对象数。优化方案见5.2节,及时释放闲置实例。
指纹修改被网站检测到1. 覆盖的属性存在不一致(如navigator.languageAccept-Language头不符)。
2. 只覆盖了部分属性,网站检查了未覆盖的属性(如deviceMemory)。
3. JS覆盖的代码本身存在特征。
1. 确保所有相关的属性和HTTP头都同步修改。
2. 使用更全面的指纹修改库(如headless-chrome-crawler中的一些方案)作为参考,查漏补缺。
3. 对注入的JS代码进行混淆和压缩,减少其可识别性。

6.2 实用的调试技巧

  1. 启用CEF日志: 在开发阶段,将CefSettings.LogSeverity设置为LogSeverity.InfoLogSeverity.Error,并指定LogFile路径。日志对于排查初始化失败、上下文创建问题非常有用。
  2. 使用开发者工具: 为每个ChromiumWebBrowser实例启用开发者工具。可以在代码中调用browser.ShowDevTools()。在DevTools的Console中,直接输入navigator.userAgent等命令,可以实时验证你的JS注入是否生效。
  3. 检查缓存目录: 直接去磁盘上查看为每个账号生成的缓存目录。你会看到CookiesLocal StorageCache等文件夹。如果隔离成功,不同账号的目录下文件应该完全不同。
  4. 网络请求分析: 使用Fiddler或Charles等抓包工具,监控不同浏览器实例发出的网络请求。重点查看请求头中的CookieUser-Agent等字段,确认它们是否按预期隔离和修改。

6.3 关于“遇见网络环境不好怎么办”的思考

这个热词虽然不直接关联代码,但在多账号管理场景下至关重要。当需要管理上百个账号,且每个都有独立IP需求时,单纯的浏览器隔离不够,需要搭配代理IP。

  • 在CefSharp中配置代理: 可以通过RequestContext设置代理。
    var contextSettings = new RequestContextSettings { CachePath = cachePath }; var requestContext = new CefRequestContext(contextSettings); // 设置代理服务器 var preferences = requestContext.GetAllPreferences(true); // 格式为 `scheme=proxy://host:port` preferences.SetValue("proxy.server", "http=127.0.0.1:8080;https=127.0.0.1:8080"); preferences.SetValue("proxy.bypass_list", "<local>"); // 绕过本地地址 preferences.Commit();
  • 动态切换代理: 为每个RequestContext配置不同的代理,实现账号与IP的一一绑定。这需要有一个可靠的代理IP池,并在创建浏览器实例时动态分配。
  • 代理的稳定性: 网络环境不好时,代理IP可能失效。需要在代码中加入代理检测和自动切换的逻辑,例如,通过浏览器实例加载一个检测IP的网页,如果失败,则通知代理池更换IP并重启该实例的浏览器(或刷新页面)。

实现CefSharp下的多账号隔离和指纹修改,是一个从理解原理到小心实践的过程。它没有银弹,每一个环节——从目录权限、上下文生命周期到指纹对抗的细节——都需要仔细测试。这套方案为我管理的多个自动化运营项目提供了稳定的基础环境,希望这些踩坑经验和实现细节也能帮助你构建出更健壮、更隐蔽的多账号浏览器解决方案。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/4 4:24:19

健身营养踩坑频发,怎么甄别靠谱本土龙头企业

全民健身带动健身营养赛道快速扩容&#xff0c;但行业乱象同样突出。不少品牌重营销投放&#xff0c;直接照搬海外配方&#xff0c;出现蛋白含量虚标、原料来源模糊等问题&#xff1b;大量品牌完全依赖外部代工&#xff0c;缺少完整品控链条&#xff0c;批次品质波动大&#xf…

作者头像 李华
网站建设 2026/9/4 4:24:19

YOLO车辆计数数据集构建与模型训练全流程实战

简介&#xff1a;本资源是面向计算机视觉初学者与YOLO系列算法实践者的专业目标检测数据集&#xff0c;专为车辆多类别计数任务设计&#xff0c;涵盖卡车、公交车、汽车、自行车、拖拉机五类常见道路交通工具&#xff0c;可直接用于YOLOv5/v7/v8/v9/v10/v11等主流版本的模型训练…

作者头像 李华
网站建设 2026/9/4 4:23:56

MATLAB双目相机标定与图像校正:从原理到三维重建的完整实践

简介&#xff1a;本资源是一套面向计算机视觉初学者与工程实践者的MATLAB双目视觉标定与校正完整实现方案&#xff0c;聚焦双目相机内外参标定、畸变校正、极线对齐、立体匹配预处理等核心环节&#xff0c;适用于机器人导航、三维测量及自动驾驶等场景的算法验证与教学实践。压…

作者头像 李华
网站建设 2026/9/4 4:21:30

STM32F4电子时钟实战:从硬件选型到软件架构与驱动开发

简介&#xff1a;本资源是一套基于STM32F4系列微控制器的数码管电子时钟完整嵌入式开发工程&#xff0c;面向嵌入式初学者、课程设计学生及STM32爱好者&#xff0c;解决从硬件驱动、RTC时间管理到动态扫描显示的一体化实践难题。压缩包共241个文件&#xff0c;包含44个C源文件&…

作者头像 李华
网站建设 2026/9/4 4:21:19

STM32+NRF905无线通信实战:从原理图到稳定收发

简介&#xff1a;本资源是一套基于STM32平台实现nRF905无线射频通信的完整毕业设计级开发包&#xff0c;面向电子信息、嵌入式系统方向的本科生及初学者&#xff0c;解决低功耗短距离无线数据传输的硬件设计与软件驱动开发问题&#xff0c;适用于课程设计、毕设选题及工程实训。…

作者头像 李华
网站建设 2026/9/4 4:21:14

秋叶ComfyUI整合包:一键解决AI绘画环境配置与跨平台兼容难题

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华