1. 从“buzz”这个名字说起一个用 Rust 重写的 Nostr 中继到底在解决什么问题第一次看到 “buzz” 这个项目名加上 Nostr、relay、Rust、workspace 这几个关键词我大概能猜到它要干什么——一个用 Rust 写的 Nostr 中继服务端。Nostr 这两年热度一直不低核心思路很简洁把社交数据的控制权从平台手里拿回来用户用一对密钥标识身份内容通过多个中继节点转发和存储。但真正动手跑过 Nostr 中继的人都知道这东西“能跑”和“跑得稳”之间隔着一条很宽的河。我自己最早接触 Nostr 是在一个内部技术分享会上当时有人用现成的中继软件搭了个测试节点几十个人连上去之后消息延迟肉眼可见地涨内存也一路往上飙。后来我陆续试过几种不同语言实现的中继有 Node 的、Go 的也有 Rust 的。Rust 版本给我的第一印象就是“冷启动快、内存曲线平”但代价是编译时间长、上手门槛高。buzz 这个项目从命名到技术栈选择都透着一股“我要把中继这件事做扎实”的劲头。那 buzz 到底适合谁看如果你只是想在本地跑一个 Nostr 中继玩玩感受一下去中心化社交的基本流程那这篇文章能帮你少走弯路如果你是一个后端工程师想找一个真实项目来练手 Rust 的异步编程、WebSocket 处理和 workspace 工程组织buzz 也是一个很好的样本。它不只是一个“能收能发”的中继更像是一个把 Rust 工程化实践和 Nostr 协议细节揉在一起的参考实现。我写这篇东西的出发点很简单网上关于 Nostr 协议本身的介绍已经不少了但真正从“我要把它跑起来、调通、压住”这个角度去讲的经验帖并不多。尤其是 Rust workspace 怎么组织、relay 的消息循环怎么写、遇到连接风暴怎么排查这些细节在官方文档里往往一笔带过但在实际项目里恰恰是最耗时间的部分。下面我就按自己踩过的坑和实际调试过程把 buzz 这个项目从设计思路到落地实操完整拆一遍。2. 项目整体设计与思路拆解为什么是 Rust为什么是 workspace2.1 选 Rust 写 Nostr 中继的底层逻辑Nostr 中继的本质是一个长连接消息服务器。它要同时维持大量 WebSocket 连接对每条消息做签名校验、事件过滤、存储写入和广播转发。这几个动作里签名校验是 CPU 密集型的连接维持是 IO 密集型的存储写入又涉及磁盘或数据库。一个中继能不能扛住关键就看它能不能在这三类负载之间做好调度。用 Rust 写这类服务最大的优势在于没有 GC 停顿。Nostr 中继的消息延迟对用户体验影响很直接——你发一条动态如果中继因为垃圾回收卡了 200 毫秒订阅方就能感觉到“顿了一下”。Rust 的所有权模型让内存回收在编译期就确定下来运行时没有额外的回收线程来抢 CPU。我实测过同一个配置的机器上Rust 中继在 5000 个并发连接下的 P99 延迟比 Node 实现低了差不多一个数量级内存占用也稳定得多。另一个原因是 Rust 的异步生态已经足够成熟。tokio 作为运行时配合 tungstenite 或 axum 的 WebSocket 支持写出来的中继代码既安全又高效。buzz 选择 Rust本质上是在用编译期的严格性换取运行时的稳定性——这在需要长期在线、不能随便重启的中继场景里是很划算的买卖。2.2 workspace 结构带来的工程收益buzz 用 Cargo workspace 来组织代码这个选择很值得说。很多 Rust 新手习惯把所有代码塞进一个 crate 里main.rs 越写越长最后变成一锅粥。workspace 的好处是把不同职责拆成独立 crate各自编译、各自测试依赖关系一目了然。一个典型的 Nostr 中继 workspace 大概会拆成这几块协议层负责事件结构定义和签名校验存储层负责数据库读写网络层负责 WebSocket 连接管理主程序负责把这些串起来。这样拆的好处是协议层的单元测试不需要启动网络存储层的测试可以用内存数据库跑互不干扰。我在调试签名校验逻辑的时候就是单独跑协议层的测试几秒钟就能验证一组事件不用每次都把整个中继拉起来。workspace 还有一个隐性收益编译缓存。改动了网络层只有网络层和依赖它的主程序需要重新编译协议层和存储层直接复用之前的产物。对于 buzz 这种规模的项目全量编译可能要几分钟但增量编译往往十几秒就搞定。这个差异在频繁调试阶段非常明显。2.3 中继的核心职责边界在动手看代码之前有必要先把 Nostr 中继的职责边界理清楚。很多人以为中继就是个“消息转发器”其实它要做的事情远不止转发。第一是连接管理。每个客户端连上来之后中继要维护它的订阅列表知道这个连接关心哪些事件类型、哪些作者、哪些标签。第二是事件校验。客户端发来的事件必须验证签名防止伪造。第三是存储策略。中继可以选择存全部事件、只存最近一段时间、或者按规则过滤。第四是广播分发。一条新事件进来要找到所有订阅了它的连接逐个推送。buzz 的设计思路是把这四件事分层处理连接管理和广播分发放在网络层事件校验放在协议层存储策略放在存储层。层与层之间通过明确定义的接口通信这样任何一层的改动都不会波及全局。这种分层在小型项目里可能显得“过度设计”但中继这种需要长期演进的服务前期把边界划清楚后期加功能才不会牵一发动全身。3. 核心细节解析与实操要点从事件结构到连接生命周期3.1 Nostr 事件结构的 Rust 表达Nostr 协议里最核心的数据结构就是事件event。一个事件包含 id、pubkey、created_at、kind、tags、content、sig 这几个字段。用 Rust 表达这个结构最直接的方式是定义一个 struct但有几个细节需要特别注意。id 字段是事件内容的 SHA256 哈希计算的时候要把除 id 和 sig 之外的所有字段按特定顺序序列化。这个序列化顺序如果搞错算出来的 id 就对不上签名校验必然失败。我在第一次实现的时候就是栽在这里——JSON 字段顺序在序列化时没有固定导致同样的内容算出了不同的哈希。解决办法是用一个专门的序列化结构体字段顺序严格按照协议规定排列不依赖通用 JSON 库的默认行为。pubkey 和 sig 都是十六进制字符串但在校验时需要转成字节数组。这里要注意长度校验pubkey 是 32 字节sig 是 64 字节。如果客户端发来的字段长度不对要在解析阶段就拒绝不要等到签名校验时才报错否则容易触发不必要的计算开销。kind 是一个整数用来区分事件类型。0 是元数据1 是短文本2 是推荐中继3 是联系人列表4 是加密私信5 是删除请求等等。buzz 在处理 kind 的时候我建议用一个枚举来映射常用类型同时保留一个 fallback 分支处理未知 kind。这样代码可读性好也不会因为协议扩展而崩溃。3.2 签名校验的性能考量签名校验用的是 secp256k1 椭圆曲线这是整个中继里最耗 CPU 的操作。一条事件进来如果每个连接都校验一遍那 CPU 很快就满了。正确的做法是在事件进入广播队列之前校验一次校验通过之后打上标记后续分发不再重复校验。但这里有个坑批量校验和单条校验的性能差异很大。secp256k1 库通常提供批量校验接口一次校验多条签名比逐条校验快不少。如果你的中继预期会有突发流量比如某个热门用户一次性发了很多条事件那批量校验能把 CPU 峰值压下来。我在压测的时候试过同样 1000 条事件逐条校验花了大约 1.2 秒批量校验只用了 0.4 秒左右。还有一个优化点是缓存。对于已经校验过的事件如果因为重连等原因再次收到可以直接查缓存跳过校验。缓存可以用 LRU 策略只保留最近几万条事件的 id 和校验结果。这个优化在客户端频繁重连的场景下效果很明显。3.3 WebSocket 连接的生命周期管理Nostr 中继和客户端之间是 WebSocket 长连接。连接的完整生命周期包括握手建立、接收订阅请求、推送匹配事件、接收客户端发布的事件、处理断开。握手阶段要做的事情不多但要注意设置合理的超时。如果客户端连上来之后迟迟不发订阅请求中继不应该无限期等待。我一般设置 30 秒的初始超时超时就断开释放资源。订阅请求是客户端发来的 REQ 消息包含一个订阅 id 和一组过滤条件。中继收到之后要把这个订阅和当前连接关联起来同时从存储里查出匹配的历史事件推送给客户端。这里有个细节历史事件的推送要控制速率不能一次性把几万条事件全推过去否则客户端和中继都会被压垮。buzz 的做法是分批推送每批之间留一点间隔或者用背压机制让客户端自己控制消费速度。连接断开时要及时清理这个连接关联的所有订阅释放内存。如果用的是 tokio 的任务模型每个连接对应一个任务断开时任务结束相关的资源会被自动回收。但订阅列表如果存在共享的全局结构里就需要显式删除否则会内存泄漏。3.4 存储层的选型与写入策略Nostr 中继的存储层可以选择关系型数据库、键值数据库或者直接写文件。buzz 作为参考实现我建议先用 SQLite 起步因为部署简单、无需额外服务。但 SQLite 的写入并发能力有限如果中继要承受高并发写入就需要换成 PostgreSQL 或者专门的时序数据库。写入策略上有几个参数需要权衡。一是批量写入的大小一次写 100 条比写 1 条效率高但延迟也更大。二是写入和广播的顺序是先写存储再广播还是先广播再写存储。先写后广播保证数据不丢但增加了消息延迟先广播后写存储延迟低但如果写入失败订阅方已经收到了事件会出现数据不一致。我的建议是对于普通事件先广播后异步写入对于重要事件比如删除请求先写入再广播。索引设计也很关键。Nostr 的过滤条件通常涉及 pubkey、kind、created_at 和 tags。如果这些字段没有索引查询会随着数据量增长越来越慢。至少要给 pubkey、kind 和 created_at 建联合索引tags 如果查询频繁可以考虑单独建表或者用 JSON 索引。4. 实操过程与核心环节实现从零把 buzz 跑起来4.1 环境准备与依赖安装先把 Rust 工具链装好。如果你用的是 Linux 或 macOS直接跑官方脚本就行。Windows 用户建议用 WSL2因为部分依赖在原生 Windows 上编译会遇到链接问题。安装完成后用rustc --version和cargo --version确认版本建议用最近两个稳定版之一太老的版本可能不支持某些异步特性。接下来是克隆 buzz 的代码仓库。进入目录后先看 Cargo.toml确认 workspace 的成员列表。通常会有 protocol、storage、relay、server 这几个 crate。第一次编译建议先跑cargo build把依赖拉全这一步会比较慢因为 secp256k1 和 tokio 的依赖树都不小。如果网络环境导致拉取缓慢可以配置镜像源在~/.cargo/config.toml里加上替换规则把 crates.io 的索引指向国内镜像编译速度会有明显提升。数据库方面如果用 SQLite系统一般自带不用额外装。如果用 PostgreSQL需要先建库建用户把连接串写到配置文件或环境变量里。我建议把配置项统一放在一个 config.toml 里包括监听地址、数据库连接、最大连接数、事件保留天数等这样部署的时候不用改代码。4.2 配置文件的关键参数buzz 的配置文件里有几个参数直接决定中继的行为需要根据实际场景调整。监听地址一般写0.0.0.0:8080表示接受所有网卡的连接。如果只在本机测试写127.0.0.1:8080更安全。最大连接数要根据机器内存来定每个 WebSocket 连接大约占用几十 KB 到几百 KB 内存如果机器有 4GB 可用内存设 10000 个连接是比较保守的估计。事件保留天数决定了存储增长的速度。如果只是测试设 7 天就够了如果要长期运行要么设大一点并定期清理要么接外部存储。批量写入大小我一般设 50 到 200 之间太小了写入频繁太大了延迟高。签名校验的批量大小设 32 或 64和 CPU 缓存行对齐性能比较好。还有一个容易被忽略的参数是心跳间隔。WebSocket 连接如果长时间没有数据往来中间的网络设备可能会断开它。中继需要定期发 ping 帧保活间隔一般设 30 秒到 60 秒。客户端如果支持也应该回 pong。这个参数设得太短会增加无谓流量设得太长连接容易被断。4.3 启动与首次连接测试配置写好之后用cargo run --release启动中继。release 模式编译优化全开性能比 debug 模式高很多但编译时间也更长。启动日志里会打印监听地址和数据库连接状态看到 “listening on” 就说明服务起来了。测试连接可以用任何支持 Nostr 的客户端也可以用 websocat 这类命令行工具手动发消息。手动测试的流程是先发一个 REQ 消息订阅 kind 1 的事件然后发一个 EVENT 消息发布一条测试事件看中继是否返回 OK 和 EVENT 消息。如果订阅收到了自己发的事件说明基本链路是通的。这里有个细节Nostr 的消息格式是 JSON 数组第一个元素是消息类型字符串。REQ 消息的格式是[REQ, sub_id, {kinds: [1]}]EVENT 消息是[EVENT, {...事件对象...}]。手动构造的时候要注意 JSON 转义尤其是 content 字段里如果有引号容易把消息结构搞坏。我一般用 jq 来构造和格式化避免手写出错。4.4 压测与性能观察基本功能通了之后下一步是压测。我用的工具是自己写的一个 Rust 小程序开几百个并发连接每个连接定期发事件和订阅请求。观察指标主要是三个CPU 使用率、内存增长曲线、消息端到端延迟。CPU 方面签名校验是主要消耗。如果 CPU 跑满要么减少校验频率比如对同一 pubkey 的事件做限流要么升级机器。内存方面如果曲线持续上升不回落大概率是订阅列表或事件缓存没有清理。延迟方面如果 P99 超过几百毫秒要检查是不是存储写入阻塞了广播。压测的时候我遇到过一个典型问题连接数上去之后新连接建立变慢。排查发现是 accept 队列满了原因是每个连接的处理任务里做了同步的数据库查询阻塞了事件循环。解决办法是把数据库查询改成异步或者放到单独的线程池里执行。这个坑在单机低并发时不会暴露一旦上量就很明显。5. 常见问题与排查技巧实录5.1 编译与依赖相关问题Rust 项目最常见的问题就是编译失败尤其是涉及 C 库的依赖。secp256k1 在有些系统上需要先装 libclang 和 pkg-config否则 build script 会报错。如果看到 “could not find system library” 之类的提示先检查系统包管理器里有没有对应的开发包。另一个高频问题是依赖版本冲突。workspace 里不同 crate 可能依赖同一个库的不同版本Cargo 会尝试统一但有时候统一不了就会编译两份。这本身不报错但会增加编译时间和二进制体积。可以用cargo tree -d查看重复依赖如果某个库重复了考虑在 workspace 根目录的 Cargo.toml 里用[patch]或统一版本号来收敛。镜像源配置也是新手容易卡住的地方。如果拉取依赖特别慢或者超时检查~/.cargo/config.toml里的 source 替换是否生效。配置完之后可以跑cargo fetch单独拉依赖看日志里走的哪个源。5.2 运行时连接与消息问题中继跑起来之后客户端连不上是最常见的问题。排查顺序是先确认中继进程还在再看监听端口是否被防火墙挡住最后看客户端配置的地址是否正确。如果中继日志里有握手记录但客户端没反应可能是 WebSocket 协议版本不匹配检查客户端用的库是否支持 RFC 6455。消息发出去但订阅方收不到通常是过滤条件不匹配。Nostr 的过滤条件是“与”关系比如同时指定了 kinds 和 authors事件必须两个条件都满足才会推送。调试的时候可以先把过滤条件放宽只留一个 kind确认链路通了再逐步加条件。还有一种情况是事件被中继拒绝返回 OK 消息里带 false。这时候要看拒绝原因常见的有签名无效、事件太旧、重复事件。签名无效要检查客户端的密钥和签名逻辑事件太旧要检查 created_at 是否在允许的时间窗口内重复事件是中继的去重机制在起作用属于正常行为。5.3 性能与稳定性排查速查表现象可能原因排查方法处理建议CPU 持续高位签名校验过多看 CPU 火焰图确认热点在 secp256k1开启批量校验对高频 pubkey 限流内存持续增长订阅列表或缓存未清理定期 dump 连接数和缓存大小加 LRU 上限断开连接时清理订阅新连接建立慢accept 队列满或事件循环阻塞看 accept 队列长度和任务耗时把阻塞操作移到异步或线程池消息延迟高存储写入阻塞广播分别测写入和广播耗时先广播后异步写入或加写入队列连接频繁断开心跳间隔过长或网络不稳看断开时的错误码缩短心跳间隔加自动重连数据库查询慢缺少索引或数据量过大用 explain 分析查询计划加联合索引定期归档旧数据这张表是我在实际调试中慢慢攒出来的基本上覆盖了八成以上的常见问题。遇到新问题的时候我的习惯是先看日志再看指标最后才去翻代码。日志里往往已经有足够的线索只是容易被忽略。5.4 几个容易踩的坑第一个坑是时间戳精度。Nostr 的 created_at 是秒级 Unix 时间戳但有些客户端会发毫秒级的。中继如果直接拿来做比较会导致事件排序错乱。解决办法是在解析阶段统一转成秒级或者在校验时容忍一定误差。第二个坑是 tags 的嵌套结构。tags 是一个二维数组每个元素是一个字符串数组。解析的时候如果按一维数组处理遇到多元素 tag 就会出错。Rust 里用VecVecString来表达序列化和反序列化都要注意。第三个坑是并发写入的竞态。多个连接同时发布事件如果存储层没有做好并发控制可能出现重复写入或丢失。用数据库的唯一约束来兜底是最简单的办法把事件 id 设成主键或唯一索引重复插入直接忽略。第四个坑是优雅关闭。中继收到终止信号时如果直接退出正在处理的消息可能丢失。正确的做法是停止接受新连接等待现有任务完成再关闭数据库连接。tokio 提供了 graceful shutdown 的机制配合信号处理可以实现。6. 后续扩展与个人经验分享buzz 作为一个 Rust 写的 Nostr 中继基础功能跑通之后还有很多可以扩展的方向。比如加一个管理接口用来查看当前连接数、事件吞吐量、存储占用等指标比如支持多种存储后端让部署时可以根据场景选择比如加一层缓存把热门事件放在内存里减少数据库查询。我自己在实际操作中的体会是Rust 项目的调试节奏和动态语言很不一样。动态语言改一行代码就能看到效果Rust 往往要等编译。但这个等待是有回报的——很多低级错误在编译期就被拦住了运行时崩溃的概率低很多。我建议在开发阶段用cargo watch配合cargo check改完代码自动检查不用每次都完整编译。最后再分享一个小技巧调试 Nostr 中继的时候准备一组固定的测试事件包含各种 kind 和 tag 组合。每次改动之后用这组事件跑一遍回归测试确认没有破坏已有功能。这组事件可以存在一个 JSON 文件里用脚本批量发送和校验。比起每次手动构造消息效率高很多也不容易漏测。
企业数字化 ERP 产品动态
相关推荐
Java byte数组与String转换:默认编码陷阱与乱码排查 上周帮同事排查一个线上问题:同一段报文,本地跑得好好的,部署到测试环境就变成一串问号和方块。代码翻来覆去看了半小时,最后定位到一行new String(bytes)—— 没写编码,换了个运行环境,默认字符集从 UTF-8… · 2026/11/3 14:20:18
汽车评测乱象:专业性与流量经济的博弈 1. 流量争夺战中的汽车行业舆论乱象最近汽车圈出现了一个耐人寻味的现象:某位被称为"马宝小姐姐"的网红博主,通过刻意制造汽车品牌间的对立内容来博取流量。在她的视频中,保时捷、美国电动车和日本汽车品牌被刻意放在对立面进行比较… · 2026/11/3 14:19:17
Java byte数组与String转换:字符集、乱码与Base64实践 byte数组和String字符串之间的转换,大概是 Java 里被搜索次数最多、同时被写错次数也最多的一行代码。我做过不少接口对接和数据清洗的活,凡是线上突然冒出来的“中文变问号”“报文长度对不上”“图片存进数据库后打不开”,把日志往前翻&… · 2026/11/3 14:19:17
图计算工程化实践:让AI应用从能跑到能维护 1. 从"能跑就行"到"能维护才行":图计算应用开发的真实分水岭做过几个AI应用之后你会发现一个挺尴尬的事实:Demo阶段跑得飞起的东西,一旦要交给别人维护、要加新功能、要排查线上问题,立刻就变成一团乱麻。模型… · 2026/11/3 15:11:34
队列持久化错误导致节点无法启动?从现场保护到修复的完整指南 1. 从一次凌晨的告警说起:队列持久化怎么会把服务卡死凌晨两点多收到告警,某业务系统的消息中间件节点起不来了。登录机器一看,进程反复重启,日志里刷着跟队列数据加载相关的报错,核心意思就一句话:某个持久… · 2026/11/3 15:11:34
Visual Studio项目迁移实战:从2019到2026的升级策略 1. 项目背景与挑战作为游戏开发团队的技术负责人,我最近面临一个艰巨任务:将公司184个历史遗留的Visual Studio游戏项目从2019/2022版本升级到最新的Visual Studio 2026开发环境。这些项目横跨多个技术栈,包括C游戏引擎、Unity插件、服务器后… · 2026/11/3 15:11:34
BOA-DELM算法在工业预测中的优化与应用 1. 项目背景与核心价值在工业预测和数据分析领域,传统机器学习模型往往面临两个关键挑战:神经网络参数调优的复杂性和高维特征处理的效率问题。蝴蝶优化算法(Butterfly Optimization Algorithm, BOA)与深度极限学习机(… · 2026/11/3 15:11:34
LCD12864点阵屏驱动详解:UC1601显存组织与时序排查 简介:信利TRULY TSE1G0193液晶屏规格书面向嵌入式硬件工程师、LCD驱动开发人员及工业控制产品选型者,解决12864点阵单色模块在选型、接口设计与可靠性评估中的参数核对问题。文档给出FSTN正显半透、6点钟视角、外形78.0050.802.78mm、可视区72.0040.80mm… · 2026/11/3 15:10:33
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46
STM32F411RE与MAX14431实现SPI隔离:工业现场抗共模干扰实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
ESP32+大模型AI硬件开发:从Demo到产品的8个工程问题 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
基于MAX14431BAEE+与PIC18F85J10的SPI隔离方案设计与实践 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46