MQTT环境触发制冷实战:本地状态机与防抖设计
1. 从一个温度到了风扇自己转的需求说起夏天最烦的事情不是热是热的时候人不在家、或者半夜懒得起来开风扇。我最初做这个项目的动机特别朴素家里有个小书房朝西下午三四点太阳一晒温度能比客厅高五六度但人往往在客厅或者出门了等想起来的时候书房已经闷成蒸笼。市面上带温控的智能插座不少但要么依赖厂商云、要么联动逻辑死板要么就是温度高于X就开这种一刀切完全不考虑湿度、变化趋势和滞后。MQTT Climate-Triggered Cooling这个标题拆开看核心就三件事MQTT负责消息的订阅与发布Climate指温湿度这类环境量的采集与判断Triggered Cooling是最终的执行动作——触发制冷设备。它不是一个具体的成品而是一套环境数据驱动设备动作的自动化范式。适合谁来参考三类人一是刚接触 MQTT、想找个真实场景练手的嵌入式或物联网初学者二是家里已经有若干智能设备、想自己写联动逻辑的折腾党三是做小型机房、温室、宠物房、酒柜这类需要环境保障场景的从业者逻辑是通用的只是阈值和执行器不同。我前后迭代了三版第一版用现成的云平台规则引擎第二版改成自己写订阅端做判断第三版才把滞后、防抖、失联保护这些工程细节补齐。下面我把整套思路、选型理由、踩过的坑和最终跑稳的方案完整讲一遍。你不需要完全照抄我的硬件逻辑拿走换成你自己的传感器和执行器一样能用。2. 为什么把判断逻辑放在本地而不是云平台规则里2.1 云平台规则引擎的便利与它的天花板一开始我用的是某云平台的设备联动功能传感器上报温度平台侧配置一条规则温度大于28度打开插座。五分钟就配好了看起来很美。但用了两周问题就来了。第一规则引擎的判断周期是固定的通常最短也要一分钟轮询一次而且它看的是最近一次上报值如果传感器上报间隔是30秒那平台拿到的数据其实是有延迟的。第二我想加连续三次超过28度才触发这种防抖逻辑平台规则里根本表达不了只能靠持续时间这种粗糙条件凑合。第三也是最要命的一旦家里网络抖动或者平台侧服务波动规则就静默失效了你根本不知道它没执行。这里就引出一个关键判断环境触发的实时性和可靠性要求决定了判断逻辑应该放在离数据源最近的地方。MQTT 的发布订阅模型天然适合这件事——传感器只管往一个主题发消息判断端只管订阅这个主题两者解耦谁也不依赖谁的实现。你把判断端跑在本地一台常开的小主机树莓派、旧笔记本、软路由、甚至一块 ESP32上网络断了它照样能基于最后状态做保守处理这比云端的黑盒规则可控得多。2.2 MQTT 在这个场景里到底扮演什么角色很多人一上来就纠结用 MQTT 还是 HTTP其实这俩解决的不是一回事。HTTP 是请求-响应你要主动去问传感器现在多少度问一次答一次MQTT 是发布-订阅传感器有变化就主动推订阅方被动接收。环境监测这种场景数据是周期性、单向、多消费者的——温度数据既要给制冷逻辑用又要给手机 App 显示还要存进数据库画曲线。用 HTTP 你得轮询三次用 MQTT 一次发布三个订阅者各取所需。具体到主题设计我建议按home/room/sensor/type这种层级来比如home/study/climate/temperature和home/study/climate/humidity。层级化的好处是订阅时可以用通配符home/study/climate/#一次订阅该房间所有环境量将来加个co2或者lux不用改订阅端代码。这里有个新手常犯的错主题名用中文或者带空格某些客户端会出问题老老实实用小写英文加斜杠。2.3 判断端放本地后谁来做大脑判断端我最终选的是 Python 脚本跑在树莓派上用paho-mqtt这个库。为什么不用 Node.js 或者 Go因为 Python 的生态在数据处理和快速迭代上最省事paho-mqtt的 API 也足够简单一个on_message回调就能接住所有消息。如果你更熟悉 JavaScriptmqtt.js一样能做逻辑完全一致。核心不在于语言而在于状态机怎么设计——这是整个项目真正的技术含量所在下一节展开。3. 温度触发的核心不是大于28就开而是状态机3.1 单阈值判断为什么一定会抖动假设你设温度大于28度开风扇小于28度关风扇。现实是温度在28度附近会来回跳27.9、28.1、27.8、28.2……风扇就会疯狂开关继电器咔咔响压缩机如果是空调更是会被搞坏。这就是阈值抖动是所有基于模拟量的控制都要面对的第一个问题。解决办法是引入回差Hysteresis也叫滞回区间。开和关用两个不同的阈值温度升到28度才开但要降到26度才关。中间这2度就是缓冲区温度在28和26之间波动时风扇保持当前状态不变。这个2度怎么定取决于你的执行器惯性和环境热惯性。风扇这种即开即停的1到2度够了空调因为制冷有延迟、停机后余冷还在回差要设大一点3到4度比较稳。我书房用的是循环扇回差设了1.5度实测很稳。3.2 用代码把状态机写清楚下面是我判断端的核心逻辑骨架用 Python 写的注释里标了每个设计点的意图import paho.mqtt.client as mqtt import time # 阈值配置开阈值和关阈值分开形成回差 TEMP_ON 28.0 # 高于此值触发制冷 TEMP_OFF 26.5 # 低于此值停止制冷 HYSTERESIS TEMP_ON - TEMP_OFF # 防抖连续N次超阈值才动作避免单次异常值误触发 DEBOUNCE_COUNT 3 class ClimateController: def __init__(self): self.current_temp None self.cooling_on False self.over_count 0 # 连续超上限计数 self.under_count 0 # 连续低于下限计数 self.last_msg_time time.time() def on_temperature(self, temp): self.current_temp temp self.last_msg_time time.time() if not self.cooling_on: # 当前是关的判断要不要开 if temp TEMP_ON: self.over_count 1 if self.over_count DEBOUNCE_COUNT: self.turn_on() self.over_count 0 else: self.over_count 0 # 没超就清零必须是连续 else: # 当前是开的判断要不要关 if temp TEMP_OFF: self.under_count 1 if self.under_count DEBOUNCE_COUNT: self.turn_off() self.under_count 0 else: self.under_count 0 def turn_on(self): self.cooling_on True publish_command(ON) print(f[{time.strftime(%H:%M:%S)}] 触发制冷当前温度 {self.current_temp}) def turn_off(self): self.cooling_on False publish_command(OFF) print(f[{time.strftime(%H:%M:%S)}] 停止制冷当前温度 {self.current_temp})这段代码里有两个容易被忽略但极其重要的点。第一over_count和under_count在条件不满足时必须清零否则就不是连续N次而是累计N次防抖就失效了。第二开和关的判断是互斥的用if not self.cooling_on / else分开避免同一时刻既想开又想关的逻辑冲突。3.3 防抖次数和上报频率要匹配DEBOUNCE_COUNT 3意味着要连续三次超阈值才动作。如果你的传感器每30秒上报一次那就是要持续90秒高温才开风扇这个响应速度对书房场景可以接受但对机房这种要求快速响应的就太慢了。反过来如果传感器每2秒上报一次3次才6秒防抖效果又太弱一个瞬时热源飘过就触发了。我的经验是防抖窗口的总时长控制在1到3分钟比较合理。算法是DEBOUNCE_COUNT × 上报间隔。30秒上报配3次是90秒5秒上报配12次是60秒你按这个公式调。另外传感器本身最好在固件侧做一次滑动平均把原始ADC读数的毛刺先滤掉这样上报出来的值本身就干净判断端压力小很多。4. 从传感器到执行器整条链路的搭建细节4.1 传感器选型与数据质量我用的是 DHT22 和 SHT30 两种都试过。DHT22 便宜但精度一般温度±0.5度湿度±2%到5%而且采样频率不能太高官方建议不低于2秒一次否则读数会漂。SHT30 贵一些但精度高温度±0.3度、I2C接口、响应快长期稳定性也好。如果你只是做风扇联动DHT22 够用如果要做精密环境控制直接上 SHT30 或者 BME280还能顺便测气压。这里有个血泪教训DHT22 千万别接在发热元件旁边。我第一版把传感器和 ESP32 主板挤在一个小盒子里结果测出来的温度永远比实际高3度因为主板自己发热。后来把传感器用延长线引出来离主板至少10厘米数据立刻就准了。传感器要放在能代表目标区域真实环境的位置别图省事塞在设备盒里。4.2 执行器继电器、红外还是直接控制执行器分三种情况。最简单的是智能插座继电器风扇插在智能插座上判断端通过 MQTT 发指令给插座控制通断。这种方式零改造但只能控制开关不能调速。第二种是红外发射如果你的风扇或空调是红外遥控的用红外发射模块模拟遥控信号能控制开关、风速、模式灵活性高很多但需要先抓取遥控码。第三种是直接控制比如用继电器接风扇的供电线或者用 PWM 调速模块控制最精细但需要动电路有安全风险。我最终用的是红外方案因为书房那台风扇支持遥控红外发射模块几块钱抓码用现成的库跑一遍就拿到了。红外的好处是非侵入不破坏原设备坏处是没有状态反馈——你发了开的指令但设备到底开没开你不知道。所以我在判断端加了一个指令去重逻辑如果当前状态已经是开就不重复发开指令避免红外信号堆积。4.3 主题设计与消息格式约定整条链路的主题我这样规划主题方向载荷示例说明home/study/climate/temperature传感器发布27.6纯数值单位摄氏度home/study/climate/humidity传感器发布58.2纯数值单位百分比home/study/cooling/command判断端发布ON/OFF执行指令home/study/cooling/state执行端发布ON/OFF实际状态回执home/study/climate/status判断端发布JSON心跳与当前决策状态载荷格式上我踩过一个坑一开始温度直接发27.6这种裸数值简单是简单但后来想加时间戳和传感器ID就没地方放。后来改成 JSON{value: 27.6, ts: 1690000000, src: sht30}扩展性好了很多。但 JSON 的解析开销比裸数值大在 ESP32 这种资源紧张的设备上要权衡。我的做法是传感器到判断端用裸数值省资源判断端到执行端和状态上报用 JSON 保信息量。5. 那些让我熬夜排查的坑5.1 消息丢失与 retained 消息的妙用MQTT 默认的 QoS 0 是最多一次发出去就不管了网络一抖消息就没了。我遇到过判断端重启后传感器还在发但判断端因为订阅建立晚了几秒错过了那几帧导致它以为没有数据一直不动作。解决办法有两个一是把传感器上报的 QoS 提到 1至少一次保证消息能到达二是给传感器主题设置retained 标志这样 broker 会保留每个主题的最后一条消息新订阅者一连上立刻就能收到当前值不用干等下一次上报。retained 这个特性在环境监测里简直是神器。判断端启动的瞬间就能拿到现在多少度而不是等30秒后的下一帧。但要注意retained 消息只保留最后一条而且如果你发了空载荷payload 为空到某个主题会清除该主题的 retained 消息。这个特性可以用来做设备离线标记但别误操作把正常数据清了。5.2 判断端假死心跳与看门狗判断端跑在树莓派上理论上很稳但我遇到过两次它假死——进程还在但不再处理消息了。原因一次是网络栈卡住一次是某个异常没被捕获导致回调线程挂了。这种问题最阴险因为从外面看一切正常但制冷逻辑已经失效了。我的对策是加双向心跳。判断端每隔30秒往home/study/climate/status发一条带时间戳的心跳同时监控自己最后一次收到传感器消息的时间。如果超过5分钟没收到任何温度数据就判定为数据源异常主动发一条告警可以发到手机通知并且保持当前执行状态不变——注意这里不能盲目关掉制冷因为可能是传感器坏了而不是真的不热了保守策略是维持现状并告警让人来判断。# 心跳与失联检测 def check_health(self): now time.time() # 超过300秒没收到温度数据判定数据源异常 if now - self.last_msg_time 300: if not self.alerted: publish_alert(传感器数据超时请检查设备) self.alerted True else: self.alerted False5.3 断电恢复后的状态一致性还有一个场景停电了恢复供电后风扇可能还插在插座上处于上次的状态但判断端重启后cooling_on默认是False它以为风扇是关的。如果实际风扇是开的就会出现明明不热但风扇还在转或者很热但判断端以为已经开了不发指令的错乱。解决思路是启动时先同步真实状态。如果执行器支持状态回执比如智能插座能查开关状态判断端启动后先订阅home/study/cooling/state拿到真实状态再初始化self.cooling_on。如果执行器不支持回执比如红外那就启动时无条件发一次关指令把状态归零再重新开始判断。这个归零动作虽然粗暴但能保证逻辑起点是确定的。6. 让这套逻辑更耐用的几个进阶思路6.1 用体感温度而不是干球温度单纯看温度其实不够准。同样是28度湿度40%和湿度80%体感完全不一样后者闷得多。所以我后来把湿度也纳入判断用简化的**热指数Heat Index**公式算一个体感温度用体感温度去触发。公式不复杂网上有现成的近似算法输入温度和相对湿度输出体感值。这样在潮湿天气里可能27度就触发了干燥天气里29度才触发更符合人的真实感受。6.2 分时段策略白天和夜间不同阈值夜里人对温度的耐受度高一些而且风扇噪音影响睡眠所以我把阈值做成随时间变化的。晚上11点到早上7点开阈值提高到29度关阈值降到26度减少夜间启停次数。实现上就是判断当前小时数动态选一组阈值。这个改动很小但体验提升明显尤其是风扇不会半夜反复启停吵人。6.3 数据留痕把环境曲线存下来判断端顺手把每条温度消息写进本地 SQLite 或者时序数据库比如 InfluxDB时间长了就能看出规律书房每天几点最热、周末和工作日有没有差异、开风扇后温度多久降下来。这些数据反过来能帮你优化阈值。我用 SQLite 存了三个月的数据画出来发现下午三点到五点是绝对高峰于是把防抖窗口在这个时段调短响应更快。数据留痕这件事做的时候觉得多余回头看全是价值。6.4 手动优先别忘了留个人工开关自动化最怕的就是人想干预但干预不了。我加了一个手动主题home/study/cooling/manual往这个主题发ON或OFF可以强制覆盖自动判断并且设置一个手动优先时长比如30分钟内自动逻辑不插手30分钟后恢复自动。这样临时想开风扇或者想关掉不用去改代码或者拔插头。这个设计思路来自工业控制里的手动/自动切换非常实用。7. 我在这套方案里最看重的三个原则折腾完这三版我最大的体会是环境触发类项目的难点从来不在连上和发消息而在判断得准、执行得稳、异常时不出乱子。MQTT 只是管道真正决定项目好不好用的是管道两端的状态管理。第一个原则是任何基于模拟量的判断都必须有回差没有回差的阈值控制迟早会抖动这是物理世界的规律不是代码能绕过去的。第二个原则是防抖和响应速度要平衡防抖窗口太短会误触发太长会迟钝用次数×间隔去量化它别凭感觉设。第三个原则是永远假设会失联、会断电、会假死然后为每种异常设计一个保守的默认行为——数据没了就维持现状加告警重启了就归零再同步这些兜底逻辑才是项目能长期无人值守运行的关键。如果你刚开始做我建议先用 DHT22 加一块 ESP32 把传感器发消息、电脑订阅打印这条最小链路跑通确认 MQTT 的发布订阅你理解了再往上加判断逻辑和执行器。别一上来就追求全自动先把每一段单独验证最后拼起来出问题的时候你才知道是哪一段的锅。这套东西我跑了小半年除了那次传感器被我塞盒子里导致读数偏高之外没再出过需要半夜爬起来处理的问题算是达到了设好就不用管的状态。

相关新闻

ESP32-P4模拟U盘实战:高速USB MSC与Flash存储设计

ESP32-P4模拟U盘实战:高速USB MSC与Flash存储设计

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

2026/10/9 1:09:47 阅读更多 →
RISC-V中断系统四大模块:PLIC、CLINT、AIA与ECLIC实战解析

RISC-V中断系统四大模块:PLIC、CLINT、AIA与ECLIC实战解析

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

2026/10/9 1:08:46 阅读更多 →
基于S32K144的CAN Bootloader开发实战:从Flash分区到固件升级全解析

基于S32K144的CAN Bootloader开发实战:从Flash分区到固件升级全解析

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

2026/10/9 1:08:46 阅读更多 →

最新新闻

浏览器扩展端侧AI推理:WebGPU+ONNX Runtime实战架构

浏览器扩展端侧AI推理:WebGPU+ONNX Runtime实战架构

1. 这不是“把模型塞进浏览器”那么简单:端侧AI在扩展环境里的真实战场“现代浏览器扩展环境下的端侧 AI 推理系统架构与工程实现规范”——这个标题里没有一个词是虚的,每个字都踩在当下前端工程最硬的几块石头上。我从去年开始带团队落地三个真实商用级…

2026/10/9 7:00:47 阅读更多 →
MiMo-V2.6:面向自我改进的工业级强化学习架构

MiMo-V2.6:面向自我改进的工业级强化学习架构

1. 这不是一篇“读论文就完事”的笔记,而是一次对强化学习边界的实地勘探“MiMo-V2.6 - Scaling Reinforcement Learning Towards Self-Improvement”这个标题里藏着三个关键信号:MiMo(Multi-Model,多模型协同)、V2.6&…

2026/10/9 7:00:47 阅读更多 →
Vue过滤器指南:原理、使用场景及面试避坑

Vue过滤器指南:原理、使用场景及面试避坑

最近这道面试题出现的频率不低,尤其面试 Vue 相关岗位时,冷不丁就会被问到:“说说 Vue 过滤器是什么?有哪些使用场景?”很多人第一反应是“用过”,但真让展开讲讲,又容易和计算属性、方法混在一…

2026/10/9 7:00:47 阅读更多 →
人工智能数学基础:梯度下降、优化器与正则化的核心原理与实战

人工智能数学基础:梯度下降、优化器与正则化的核心原理与实战

手头这本《人工智能数学基础》翻到第十八章的时候,我其实松了一口气——前面十几章的线性代数、微积分、概率论铺了那么多,总算开始收网了。这一章讲的内容,很多人可能会觉得“不就是优化算法和正则化嘛”,但真等到训练模型训练不…

2026/10/9 7:00:47 阅读更多 →
WorkBuddy实战:搭建Excel VBA模板母版-副本自动同步系统

WorkBuddy实战:搭建Excel VBA模板母版-副本自动同步系统

我把手头几张各写各的VBA模板文档,用WorkBuddy理成了母版-副本自动同步的总控台。前几周我一直在维护一套Excel进销存模板,里面有报价单、对账单、领料单,每张表里都塞了一段VBA逻辑,逻辑还互相抄。最崩溃的一次是改完报价单的客户…

2026/10/9 7:00:47 阅读更多 →
C语言数据类型与变量:内存机制、指针结构与实战避坑指南

C语言数据类型与变量:内存机制、指针结构与实战避坑指南

C语言的数据类型和变量,看上去是每本教材开篇就讲的基础,但我在实际带项目、看别人代码、甚至帮人排查问题的时候发现,很多人恰恰是栽在这些“基础”上。指针用得晕、结构体定义不明白、类型转换出bug、变量作用域一锅粥——这些问题十有八九…

2026/10/9 6:59:46 阅读更多 →

日新闻

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API这个话题,隔三差五就会在群里被翻出来讨论一次。上周还有个同事线上处理一个订单超时问题,排查到最后发现是ZonedDateTime序列化后时区丢了,用户在下单当天晚上看到的时间整整差了8个小时。这类问题几乎每个做Java开发的人都遇到过…

2026/10/9 0:00:49 阅读更多 →
EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

前几个月我手头有好几台机器需要互相访问:办公室台式机、家里 NAS、还有一台云主机。如果只是偶尔传个文件倒还好,问题是工作场景经常要在几处环境之间来回切换,每次都先登录跳板机再层层代理,实在折腾。我先后试过端口映射、自建…

2026/10/9 0:00:49 阅读更多 →
AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent 这个词在过去一年里被反复提及,但真正动手搭过一套能跑起来的 Agent 系统的人都知道,从"知道它是什么"到"让它稳定干活"之间隔着一整套工程决策。我前后参与过几个 Agent 项目的落地,从最初用现成框架拼装&…

2026/10/9 0:01:50 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

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

2026/10/8 15:26:32 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

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

2026/10/8 15:26:40 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

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

2026/10/8 10:10:36 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/8 21:13:17 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/8 15:26:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/9 6:17:20 阅读更多 →