1. 先搞清楚 Fuego 到底是个什么东西我最早接触 Fuego 是在一个需要给多块不同架构的开发板做回归测试的场景里。当时手头有五六种板子架构不一有的跑在本地串口上有的只能远程登录操作每次改动一点驱动或者内核配置就得挨个手工敲命令验证一遍累且容易漏。后来用了 Fuego才算把这堆重复劳动收拢成一套可复用、可批量跑的测试集合。Fuego 是一个面向嵌入式 Linux 设备的自动化测试框架核心定位是把测试用例组织起来统一调度到目标板上执行并把结果集中收集、比对、展示。它本身不产出测试内容而是提供一套跑测试的基础设施测试用例的组织方式、执行调度、日志采集、结果判定、以及一个基于网页的结果查看界面。你可以把它理解成一个测试管家——你告诉它有哪些板子、要跑哪些测试剩下的分发、执行、收日志、判断通过与否它来操心。这套东西解决的问题很明确嵌入式测试的碎片化。板子多、架构杂、连接方式各异、日志散落各处、结果靠人眼看。Fuego 用统一的目录结构和约定式的脚本接口把这些差异抹平了一部分让写一次测试到处跑变得可行。它适合谁呢我认为主要是三类人一是维护多款硬件、需要频繁做冒烟和回归的嵌入式测试工程师二是做 BSP 或系统集成、要验证镜像基本功能的开发人员三是想把手工验证流程固化成可持续执行的自动化流程的团队。这篇文章聚焦的是 Fuego 里最容易让人卡住、也最能体现它设计思路的一件事给框架添加一个新的测试。为什么单独拎这件事出来讲因为 Fuego 的测试不是随便丢个脚本进去就能跑的它有一套自己的约定——测试目录里的 spec 文件、测试脚本、解析脚本、判据文件这几样东西各司其职少一个或者写错一处测试就跑不起来或者结果永远是通过。我见过太多人第一次加测试时脚本明明本地能跑一放进框架就各种报错问题往往就出在对这套约定的理解不到位上。所以下面我会按整体设计思路 → 核心文件逐个拆解 → 完整实操落地 → 踩坑排查的顺序展开把我实际加测试时踩过的、以及帮别人排查过的问题都摊开讲。读完你应该能独立地给自己的板子加一个能跑、能判、结果可信的测试用例。2. 添加测试前必须吃透的设计约定2.1 为什么 Fuego 要用目录 约定文件这种笨办法第一次看 Fuego 的测试目录时我的反应是这也太原始了。一个测试就是fuego_test目录下的一个子目录里面塞着fuego_test.sh、spec.json老版本是spec、parser.py、criteria.json这么几个文件全靠文件名和约定来协作没有任何强类型的接口定义。这种设计放在今天的眼光看确实土但用久了会发现它是有意为之。嵌入式环境的现实是目标板上的工具链可能是精简过的 busybox可能没有 Python可能连 bash 都是裁剪版。如果框架要求测试必须写成某种特定语言、依赖某个运行时那很多板子根本跑不了。Fuego 的做法是把框架侧和目标侧解耦测试脚本fuego_test.sh是跑在目标板上的只要求最基本的 shell 环境而parser.py和判据处理是跑在主机上的主机环境相对完整可以用 Python。这个分工是整个框架的基石理解了这一点后面所有为什么这个文件要这么写就都顺了。另一个考量是可移植性和可维护性。测试用例以纯文本文件形式存在意味着它可以被版本管理、被复制、被改造。你想基于现有测试改一个新变体直接复制目录改名就行不需要动框架代码。这种约定优于配置的思路牺牲了一部分类型安全和错误提示换来了极低的接入门槛和极强的可移植性——对嵌入式这种环境差异巨大的领域来说这笔交易是划算的。2.2 四个核心文件各自扮演什么角色在动手之前得先在心里建立起这张分工表。我把它整理成了下面这个对照建议加测试时对着看文件运行位置职责写错的典型后果spec.json主机框架读取声明测试的元信息名称、依赖、超时、是否需要重新构建等测试不显示、参数不生效、超时被误判fuego_test.sh目标板真正在板子上执行的测试逻辑测试报错、卡死、无输出parser.py主机把测试输出解析成结构化结果测量值/判据结果为空、指标解析不出来criteria.json主机定义通过/失败的门槛永远通过或永远失败这里最容易误解的是spec.json和criteria.json的价值。很多人觉得这俩就是走个形式随便填填。实际上spec.json里的超时设置、依赖声明直接决定了测试在调度层面会不会被误杀criteria.json里的判据则决定了跑完了和跑对了之间的区别。我后面会专门讲这两块的细节。另外要强调一个顺序问题Fuego 的执行流是主机调度 → 板子执行脚本 → 输出回传主机 → parser 解析 → criteria 判定。这个链条意味着你在fuego_test.sh里echo出来的东西最终要能被parser.py认出来。两边必须约定好输出格式这是新手最常断裂的地方。我见过有人脚本里打印了一堆漂亮的中文日志parser 里却按固定英文关键字去匹配结果解析出来一片空白还以为测试没跑。2.3 三种常见的测试类型决定你的脚本怎么写Fuego 里的测试大致可以分成几类不同类型对应不同的脚本结构提前想清楚你属于哪类能省很多返工第一类是功能验证型跑一个程序看它退出码和输出是否符合预期。这类最简单脚本基本就是调用、检查返回值。第二类是基准性能型跑一个负载采集耗时、吞吐之类的数值这类必须输出可被 parser 提取的数值通常还要在criteria.json里设阈值。第三类是交互/长时型比如需要串口交互、需要持续跑一段时间采集状态。这类要特别注意超时设置和清理逻辑否则容易把板子挂住。你要加的测试属于哪一类直接决定了fuego_test.sh里要不要做数值输出、parser.py里要不要写提取逻辑、criteria.json要不要设门槛。我个人的建议是第一次加测试先挑一个最简单的功能验证型跑通全流程把整条链路摸熟再去碰复杂的。别一上来就搞性能采集出了问题你分不清是脚本错、parser 错还是判据错。3. 核心文件逐个拆解与实操要点3.1 spec.json别把它当成注释文件spec.json是框架读取测试元信息的地方。它的字段不多但每一个都有实际作用我挑几个关键的讲。testName是这个测试的唯一标识框架界面和命令行里都用它来引用测试。这里有个坑名字里别用空格和特殊字符否则在命令行传参和结果目录生成时容易出问题。我一般用下划线分隔的小写名比如my_gpio_check。timeout是单次测试执行的超时秒数。这个值一定要结合你的测试实际耗时来设宁可略宽也别卡太死。设太短的后果是测试还在跑就被框架强杀报告里显示超时失败你会以为是板子问题查半天。设太长的问题相反测试真挂了框架要等很久才回收批量跑时特别浪费时间。我的经验值是本地手工跑三次取最大耗时再乘以 1.5 到 2 作为超时。needs_output之类的字段涉及输出处理的开关具体名称随版本略有差异加测试时最好直接参考同目录下已有的、能正常工作的测试的 spec 文件照着改是最稳妥的。新版 Fuego 有spec.json老版本可能是无扩展名的spec格式是类似的键值对。动手前先确认你用的 Fuego 版本用的是哪种格式这个细节不对测试根本不会被识别。还有一个容易忽略的点spec 里可以声明测试依赖比如是否需要先构建、是否依赖某个工具存在。如果你的测试需要先在主机上编译一个程序再推送到板子那构建逻辑和依赖声明就得配合好fuego_test.sh里的推送步骤。这块我建议初期直接跳过先做纯脚本型测试等熟了再上需要编译的。注意修改 spec 后最好清一下测试的构建缓存再重新跑。Fuego 有时会缓存测试元信息改了不生效会让人怀疑人生。3.2 fuego_test.sh跑在板子上的那部分要贫瘠一点这个脚本是整个测试的核心执行体但它跑在目标板上环境可能非常简陋。写的时候要时刻提醒自己这里不是你的开发机。具体来说有几条硬性习惯。第一脚本开头别假设有 bash。很多精简系统只有 busybox 的 sh写#!/bin/bash可能直接找不到解释器。稳妥的写法是#!/bin/sh然后避免使用 bash 独有的语法比如[[ ]]、数组、local在某些裁剪环境里也可能不可用。我一般写测试脚本时会把语法限制在 POSIX sh 的子集里这样兼容性最好。第二注意工作目录。脚本执行时当前目录不一定是测试目录所有对测试资源的引用要么用绝对路径要么在脚本开头显式cd到已知位置。我踩过的坑是脚本里用相对路径引用一个数据文件本地测好好的在框架里跑就找不到文件原因是框架从一个不同的目录启动脚本。第三输出要克制且规范。因为输出最终要被parser.py解析你echo的每一行都要么是给人看的要么是能被机器认出的结构化信息。我的做法是给机器看的用固定前缀比如RESULT: valuexxx给人看的就正常打印。别把两者混在一起也别打印一大堆无关信息否则 parser 的匹配逻辑很容易被误触发。第四一定要处理退出码和清理。脚本结束时的退出码框架会看非零通常意味着执行层面出了问题。如果你的测试中途创建了临时文件、启动了后台进程记得在脚本末尾清掉否则残留在板子上会影响下一次测试。我给脚本加清理逻辑的方式是尽量把可能失败的地方包起来确保清理代码一定能被执行到。3.3 parser.py主机侧的翻译官别写得太脆parser.py的角色是把板子回传的原始输出翻译成框架能理解的结构化数据。它跑在主机上可以用完整的 Python 环境这一点比脚本宽松多了。但它有一个隐性的脆弱点它依赖输出格式。板子上的输出一旦变了哪怕只是多了个空行、换了个措辞parser 就可能匹配不上。所以写 parser 的核心原则是匹配要宽、提取要稳、缺失要有兜底。具体讲匹配用正则时别锁死整行用关键字加宽松的数值模式比如rRESULT:\s*value([0-9.])这种容许前后有空格、有换行。提取不到值时不要让它静默返回空要么给出明确提示要么用一个合理的默认值并打日志这样排查时你能立刻知道是没提取到而不是值就是空。另一个实践要点是 parser 里处理多组测量值的方式。有的测试一次跑下来会产生好几个指标parser 要能分别提取并各自命名这样在结果界面上才能分开显示、分开和判据比较。我建议指标名就用有意义的英文短名跟criteria.json里的键一一对应两边名字对不上是最常见的低级错误。3.4 criteria.json把跑完了升级成跑对了判据文件是决定结果可信度的地方。没有它测试跑完只能说明执行没报错不能说明功能正确。criteria.json的本质是给 parser 提取出来的每个指标定一个通过门槛框架拿实际值和门槛比给出通过或失败。写判据时最需要想清楚的是门槛怎么来。比如你测一个操作的耗时门槛不能拍脑袋定得基于实测数据留出合理裕量。我的方法是先在几块不同的板子上各跑若干次记录分布取一个明显宽松于正常波动、但又能抓住明显劣化的值。门槛定太严稍微有点系统负载波动就误报失败定太松真出问题了也发现不了。这个平衡点要靠数据说话不能靠感觉。还有一点要提醒判据的键名必须和 parser 输出的指标名严格对应。哪怕大小写差一个字母框架也可能认为该指标没有判据或者判据没有对应指标结果要么恒过要么恒失败。这个坑我替别人排查过不止一次症状就是测试结果永远是同一个值最后发现是键名对不上。4. 从零加一个测试的完整落地过程4.1 准备工作先手工跑通再进框架这一步很多人跳过然后付出代价。我的铁律是任何测试先在目标板上手工完整跑通确认命令、依赖、输出都符合预期再考虑往框架里搬。原因很简单框架会引入额外变量工作目录、环境变量、超时、输出重定向如果基础命令本身就有问题你在框架里排查等于同时面对两个未知数。具体做法登录到目标板手工执行你打算写进fuego_test.sh的那串命令观察输出、耗时、返回值。把实际输出抄下来这就是你后面写 parser 匹配规则的依据。同时记下这次的真实耗时用来定spec.json里的超时。别忘了在不同状态下各跑几次冷启动、有负载时看看耗时和输出的稳定性这直接决定你的判据门槛定多宽。4.2 建立测试目录与骨架文件准备工作完成后到 Fuego 的测试目录下创建你的测试子目录。目录名建议和测试名一致便于对应。然后在里面放上四个骨架文件。这一步我建议不要从空白开始而是从同类型、已能正常工作的测试目录复制一份过来改能省掉大量格式细节上的来回。复制过来之后按顺序改先改spec.json的名字和超时再改fuego_test.sh里的执行逻辑然后改parser.py的匹配规则和指标名最后改criteria.json的键名和门槛。改的过程中每改完一个文件可以先不急着全链路跑而是在主机上单独验证一下语法比如python直接执行一下 parser看有没有语法错误。这种小步验证能避免一次性冒出五六个错误。4.3 编写执行脚本的关键片段执行脚本的结构我一般这么组织开头做环境确认和变量定义中间是主测试逻辑最后是清理。下面是一个简化后的骨架注意它只用 POSIX sh 语法#!/bin/sh # 基本环境确认缺工具就尽早失败别跑到一半才报错 if ! command -v some_tool /dev/null 21; then echo ERROR: some_tool not found exit 1 fi # 主逻辑 TMPFILE/tmp/my_test_$$.log some_tool --run $TMPFILE 21 RC$? # 输出可被 parser 提取的结构化结果 VALUE$(grep -o metric[0-9.]* $TMPFILE | head -n 1 | cut -d -f2) echo RESULT: value${VALUE} # 清理 rm -f $TMPFILE exit $RC这段骨架里有几个设计意图值得说。开头查工具是为了让失败尽早暴露避免脚本跑到后面才因为缺命令莫名失败用$$做临时文件名后缀是为了避免并发跑多个测试时互相覆盖把结构化输出单独用固定前缀打印是为了让 parser 匹配时不容易误伤最后清临时文件是为了不给板子留垃圾。这几个点看着琐碎但在批量、并发、长期跑的场景下每一个都能省掉不少麻烦。4.4 编写解析脚本与对齐判据parser 这边我对应的写法是围绕固定前缀做宽松匹配并对缺失做兜底import re def parse(text): result {} m re.search(rRESULT:\s*value([0-9.]), text) if m: result[value] float(m.group(1)) else: # 明确记录缺失而不是静默返回空 print(WARN: value not found in output) result[value] None return result这里的门道在于re.search而不是re.match因为输出可能有多行目标信息不一定在开头正则里用\s*容许冒号后有多余空格兜底分支打警告而不是直接崩这样你在结果日志里能立刻看到是解析失败。判据文件里就对应写value这个键和它的门槛。两边键名严格一致这一步做完整条链路的接口就对齐了。4.5 跑第一次并读结果第一次跑别急着批量就单独跑这一个测试然后重点看三个地方一是执行日志确认脚本确实在板子上跑起来了二是 parser 的输出确认指标被正确提取三是判据比对结果确认通过/失败符合预期。这三处任何一处不对都回到对应文件去修。我第一次加测试时就是在这里发现 parser 提取出的值一直是空回头一看是脚本里 echo 的前缀和 parser 里匹配的前缀差了一个字符。这种错误在框架层面看不出来只能靠这三处日志逐段核对。5. 常见问题与排查速查5.1 那些看起来像板子故障、其实是配置的问题加测试过程中遇到的大部分疑难杂症根子不在硬件而在配置和格式。我把高频问题整理成了下面这张表基本覆盖了我自己踩过的和帮别人处理过的情况现象大概率原因处理方向测试列表里看不到新测试目录名或 spec 格式不被识别核对 spec 文件名与版本是否匹配目录名无特殊字符测试一启动就超时失败超时设太短或脚本卡在等待输入放宽超时检查脚本是否误读了标准输入结果始终为空parser 匹配规则与输出格式不一致对照板子原始输出放宽正则结果恒定通过或恒定失败判据键名与指标名不匹配逐字核对两边键名时好时坏偶发失败门槛过严或存在未清理的残留状态复核门槛裕量补清理逻辑脚本报找不到文件相对路径问题改用绝对路径或在开头显式 cd5.2 我总结的几条实操心得第一条也是最值钱的一条永远保留一次完整的原始输出。不管 parser 写得多好第一次跑的时候都该把板子回传的原始文本完整看一眼。这一步能提前发现 80% 的解析问题因为你会直观看到输出长什么样和你的正则想当然以为的样子对不对得上。我后来养成习惯在脚本里把结合流重定向到一个日志文件parser 解析的对象就是那份原始日志出了问题直接翻原始日志一目了然。第二条清理逻辑不是可选项。测试在板子上留下的临时文件、后台进程、占用的设备节点如果不清第一次可能没事批量跑或者第二天再跑就会出各种奇怪的失败。我吃过一次亏测试里启动的后台进程没杀干净结果后续测试抢同一个设备节点全都失败排查了很久才发现是上一个测试的残留。第三条别在第一次就把门槛定死。先用一个非常宽松的判据让全流程跑通确认链路正常再逐步收紧门槛到合适值。这样你能区分链路问题和门槛问题不至于混在一起查。这个顺序听起来简单但真能省掉大量返工。第四条脚本里的每个echo都要有理由。要么给人看要么给机器解析别为了看起来详细打印一堆无用信息。输出越干净parser 越不容易误判日志也越好读。这个习惯在测试数量涨到几十上百个的时候价值会成倍放大。5.3 后续扩展的思路当你把一个最简单的功能测试跑通之后扩展方向其实很自然需要采数值就加 parser 提取逻辑和判据门槛升级成性能型测试需要多组参数就考虑用循环在脚本里跑多轮并分别输出结构化结果需要先构建就先补上主机侧的构建步骤和依赖声明。每一步都是在已有骨架上加一块而不是推倒重来这也是这套约定式设计真正好用的地方——它把复杂的自动化测试拆成了几个各管一段、边界清楚的简单文件。我在实际使用中越来越觉得Fuego 这类框架的价值不在它有多先进而在它足够朴素、足够可拆解让人能在资源受限的嵌入式环境里把自动化测试这件事真正落地。加测试的过程本质上就是在和这套约定对话你尊重它的分工它就给你一个能跑、能判、能复现的结果。
企业数字化 ERP 产品动态
相关推荐
小学C++真题暴露的运算符优先级与布尔逻辑认知断层 简介:本资源为2024年信息素养大赛C算法创意实践挑战赛小学组初赛真题解析文档,专为小学阶段编程初学者设计,聚焦C基础语法、运算符优先级、条件与循环结构、布尔逻辑及简单算法建模能力训练,助力学生夯实编程根基、提升竞赛解题思… · 2026/11/3 7:17:54
全能AI学术平台:一站式解决文献管理、写作、绘图与代码的科研工作流 做科研的人,浏览器里大概都存着好几个AI工具。找文献的、做综述的、改语法的、画流程图的、补代码的,一个比一个看着厉害。我也属于那种爱尝鲜的,从通用大模型到学术专用AI,基本都注册过一轮。试到后面发现一个挺尴尬的问题&#… · 2026/11/3 7:16:54
ObjectMapper从入门到精通:序列化配置、泛型与性能优化实战 做后端开发这些年,序列化这块踩过的坑真不少。早期用原生手写拼JSON,字段一多就乱套;后来换成Jackson,一开始也只是停留在writeValueAsString和readValue两个方法上,直到项目里遇到泛型反序列化、日期格式错乱、循环引… · 2026/11/3 12:03:00
端口镜像+Wireshark定位空载口SSDP异常流量 简介:围绕Wireshark抓包定位网络故障的实战型docx文档,面向网络运维、系统集成与网络安全初学者及一线工程师,重点解决VLAN端口流量突增、异常报文排查和攻击源定位等问题。压缩包内共1个docx文件,大小约385KB,以案例讲… · 2026/11/3 12:03:00
制造buzz:从话题钩子到用户自传播的完整实战拆解 buzz这个词,在营销圈里被反复念叨,但大多数人对它的理解其实很模糊。有人说它是“刷屏”,有人说它是“热度”,还有人干脆把它等同于“追热点”。我做了这么多年增长和品牌运营,越来越觉得,buzz本质上不是某… · 2026/11/3 12:03:00
MATLAB多无人机协同目标搜索与围捕仿真:概率地图与势场收拢 调试这套仿真的头一个星期,我做的最多的事情不是写算法,而是盯着那五条轨迹发愣:它们要么在搜索阶段互相撞在一起,要么在围捕阶段绕着目标画出一个歪歪扭扭的多边形,然后集体停在某个角落不动了。项目标题里"多无… · 2026/11/3 12:03:00
端口流量异常排查实战:Wireshark抓包、镜像与SSDP泛洪定位 简介:这是一份面向网络运维与故障排查人员的实战案例文档,围绕Wireshark抓包处理网络故障展开,重点解决VLAN内端口流量突增、AP空载却出现大量数据包等异常场景。资源包共1个docx文件,大小385KB,内容以锐捷交换机为案例… · 2026/11/3 12:03:00
.NET Native AOT编译技术解析与实战优化 1. Native AOT编译的本质与价值 在传统.NET开发中,JIT(Just-In-Time)编译是运行时性能的核心瓶颈之一。当应用启动时,CLR需要将IL代码实时编译为机器码,这个过程不仅消耗CPU资源,还会导致明显的启动延迟。而… · 2026/11/3 12:01:57
双极步进电机控制实战: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