news 2026/9/18 9:17:58

Optimism 派生子批次解码与校验审查实战:op-node 与 Kona 双客户端规范对齐指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Optimism 派生子批次解码与校验审查实战:op-node 与 Kona 双客户端规范对齐指南

Optimism 派生子批次解码与校验审查实战:op-node 与 Kona 双客户端规范对齐指南

【免费下载链接】optimismOptimism is Ethereum, scaled.项目地址: https://gitcode.com/GitHub_Trending/op/optimism

本指南讲解 Optimism 派生子流程(derivation pipeline)中"从完整通道字节到被接受的 L2 批次输入"这一段——即批次(batch)解码与校验——的规格驱动审查方法。文章以仓库内 docs/ai/derivation-batch-review.md 为骨架,结合 op-node(Go)与 Kona(Rust)两套共识层实现的实际源码展开,读完你可以掌握:如何界定审查范围、如何利用规范到代码的映射表快速定位实现、如何按对抗性边界清单挖掘解码器缺陷,以及如何用统一证据契约输出可被复现的审查结论。

一、这份指南的定位与配套文档

derivation-batch-review.md是 Optimism monorepo 中 AI 审查体系(.claude/agents/)下的一篇领域指南(area guide),与配套的derivation-batch-reviewerAgent(见 .claude/agents/derivation-batch-reviewer.md)成对使用。它聚焦于"batcher 可控数据"从完整通道字节被接受的 L2 批次输入之间的全部解码与校验行为。

使用时需先读完两篇前置文档:

  • docs/ai/spec-driven-review.md:共享审查方法,定义审查流程、证据契约、输出格式与映射校验规则;
  • docs/ai/derivation.md:派生子流程的管线模型与通用派生规则。

职责划分很清晰:本指南负责派生范围界定(scope)、代码导航(code navigation)与领域检查(domain checks);共享指南负责审查流程、证据契约、输出与映射校验;两者都不定义协议行为——协议行为以 OP Stack 规范为准。

一个关键前提:相同的派生逻辑在该仓库中被实现了两次,这正是双客户端审查存在的意义:

  • op-node(Go):参考共识层节点,代码位于op-node/rollup/下;
  • Kona(Rust)rust/kona/bin/node(Rust 共识层节点)与rust/kona/bin/client(故障证明程序)共用 kona-derive 派生管线,实现在rust/kona/crates/protocol/deriverust/kona/crates/protocol/protocol下。

二、审查范围界定:审什么、不审什么

2.1 范围内行为

本指南要求审查以下行为:

  • 通道解压与大小限制:完整通道数据的解压,以及对解压后尺寸上限的强制(对应 Channel format 与 Fjord derivation 规范);
  • 批次类型分发与单一批次 RLP 解码:按批次类型字节分发到 SingularBatch / SpanBatch,并解码单一批次的 RLP 编码;
  • Span 批次解码:前缀(prefix)、载荷(payload)、bitlist、交易(transaction)与 varint 的解码;
  • 原始 Span 批次到派生块输入的转换:把RawSpanBatch推导为可构造 payload attributes 的SpanBatch/SpanBatchElement
  • Holocene span 分解与单一批次流式输出
  • 单一批次与 Span 批次的语义校验(batch validity);
  • 分叉门控的交易类型、压缩格式、限制与校验规则(Delta / Fjord / Holocene 及后续分叉);
  • 与安全链(safe chain)的批次重叠检查
  • 最终输出结果:accept、drop、past、future、undecided 或 error。

2.2 范围外行为

除非被修改的行为跨越了边界,否则以下内容不纳入审查

  • batcher 交易的检索、帧(frame)解析与通道组装;
  • 存款(deposit)派生与 payload-attributes 构造;
  • 执行层交易处理;
  • 通用 safe-head 推进与管线重置;
  • batcher 编码策略与压缩效率。

编码代码(如 op-node/rollup/derive/channel_compressor.go)仍可用于往返(round-trip)与规范形式(canonicality)检查,但除非编码器改动会改变验证者接受的数据,否则不要仅报告编码器侧发现

三、规范到代码的映射表

映射表是这份指南的核心导航工具。由于仓库路径的变更节奏独立于协议行为,映射表被维护在 monorepo 中,每个路径只作为起点,审查时还需顺着受影响的调用方与被调用方继续追踪。

行为规范章节op-nodeKona
通道解压与大小上限Channel format、Fjord derivationop-node/rollup/derive/channel_in_reader.goop-node/rollup/derive/channel.goop-node/rollup/chain_spec.goderive/src/stages/channel/channel_reader.rsprotocol/src/batch/reader.rsprotocol/src/brotli.rs
批次信封与单一批次Batch formatop-node/rollup/derive/channel_in_reader.goop-node/rollup/derive/batch.goop-node/rollup/derive/singular_batch.goderive/src/stages/channel/channel_reader.rsprotocol/src/batch/reader.rsprotocol/src/batch/core.rsprotocol/src/batch/single.rsprotocol/src/batch/type.rsprotocol/src/batch/errors.rs
Span 批次线格式Span batch formatop-node/rollup/derive/span_batch.goop-node/rollup/derive/span_batch_tx.goop-node/rollup/derive/span_batch_txs.goop-node/rollup/derive/span_batch_util.goprotocol/src/batch/raw.rsprotocol/src/batch/prefix.rsprotocol/src/batch/payload.rsprotocol/src/batch/transactions.rsprotocol/src/batch/tx_data/protocol/src/batch/varint.rsprotocol/src/batch/bits.rs
Span 转换与重叠Span batch integrationop-node/rollup/derive/span_batch.goop-node/rollup/derive/batches.goprotocol/src/batch/raw.rsprotocol/src/batch/span.rsprotocol/src/batch/element.rsprotocol/src/batch/inclusion.rs
Holocene 批次流式处理Holocene derivationop-node/rollup/derive/batch_mux.goop-node/rollup/derive/base_batch_stage.goop-node/rollup/derive/batch_stage.goop-node/rollup/derive/batch_queue.goop-node/rollup/derive/attributes_queue.goderive/src/pipeline/core.rsderive/src/stages/attributes_queue.rsderive/src/stages/batch/batch_provider.rsderive/src/stages/batch/batch_stream.rsderive/src/stages/batch/batch_queue.rsderive/src/stages/batch/batch_validator.rsrust/kona/crates/node/service/src/actors/engine/actor.rsrust/kona/crates/proof/driver/src/core.rs
语义批次有效性Batch Queue、Span Batch Queueop-node/rollup/derive/batches.goop-node/rollup/chain_spec.goop-node/rollup/toggles.goop-node/rollup/types.goprotocol/src/batch/validity.rsprotocol/src/batch/single.rsprotocol/src/batch/span.rsderive/src/stages/batch/batch_validator.rsgenesis/src/rollup.rs

两点路径约定:

  • Kona 路径:凡不以rust/kona/开头的路径,均相对于rust/kona/crates/protocol/;以rust/kona/开头的路径相对于仓库根目录;
  • 其余所有路径均相对于仓库根目录。

此外,分叉文档会修订基础规则。审查时必须搜索每个适用分叉目录specs/protocol/<fork>/下所有文件,不能只读derivation.md——例如 Delta 使用span-batches.md,Lagoon 使用post-exec.md定义交易接受规则。至少要检查 Delta、Fjord、Holocene 以及任何后续改变交易接受规则的分叉。

四、何时运行该审查器

当一次变更触及映射表中的任何路径或其依赖时,都应该运行该审查器。具体触发场景包括:

  • Kona 客户端发布包含映射变更的版本;
  • 变更了被映射代码使用的 rollup 配置字段(注意:新配置字段需要同时接入 op-node 与 Kona 两边的注册表摄取路径,详见 docs/ai/derivation.md);
  • 引入新的批次格式、压缩格式、交易类型或派生态分叉门控;
  • 协议变更修改了被映射的规范章节;
  • 修复畸形批次数据、解码器 panic 或跨客户端不一致问题。

五、派生态分析流程(Derivation Analysis)

分析的总体思路是:把完整通道字节一路追踪到最终批次结果,并且op-node 与 Kona 各自独立追踪(不能用一个实现去推断另一个实现)。

对每条受影响的路径,需要确定六个要素:

  1. 它接受哪些字节串(accept set);
  2. 它产生哪些解码值(outputs);
  3. 它在分配内存或迭代之前应用了哪个限制(limits);
  4. 它使用哪个分叉条件(fork conditions);
  5. 它返回哪个语义有效性结果(validity outcomes);
  6. 出错后是否有部分状态残留(partial state survival)。

比较顺序有硬性要求:先分别把两个实现与规范比较,再把两个实现的 accept set 与输出互相比较。线格式一致性(wire-format parity)与编解码器测试指引见 docs/ai/derivation.md;而在规格驱动审查中,共享权威策略优先于该小节中"以 op-node 为准"的平局裁决规则——遇到无法解决的分歧应报告为规范缺口(specification gap),而不是自行选择某个客户端为正确方。

报告差异时,必须报告差异所覆盖的输入类别(input class),而不是单个示例输入,并点明产生相同结果的首字节范围、字段或失败模式——单个示例会掩盖差异的影响面。

六、已知问题与兄弟路径

在应用共享问题分诊流程时,还应检查ethereum-optimism/optimism#22854及其子问题(该问题记录在案,作为分诊上下文而非协议权威)。

共享指南要求对兄弟路径做规则清单差异对比(rule-list diff)。本领域的兄弟路径对包括:

  • 同一客户端内:单一批次有效性 vs Span 批次有效性;
  • 同一线格式下:前 Holocene 阶段行为 vs Holocene 阶段行为;
  • 跨客户端:op-node 中的同一规则清单 vs Kona 中的对应清单。

同时必须记录每个输入被拒绝的阶段:结构性解码失败可能丢弃整个通道;而更靠后的派生失败可能只丢弃一个批次、保留通道。两种实现可能在同一阶段拒绝同一输入,也可能在不同阶段拒绝——阶段不同,周围工作的存活范围就不同,浅层比较会错误地得出"双方一致"的结论。

七、共享解码器限制

两个客户端都依赖第三方解码库处理压缩与 RLP。静态追踪比较的是调用方代码,而不是库内部实现——两个库完全可能对同一批字节串给出不同的有效性判断。

因此审查触及相关路径时必须:

  • 在两侧分别点名所使用的库(例如 op-node 依赖compress/zlibgithub.com/andybalholm/brotli,Kona 依赖miniz_oxide与自实现/适配的 brotli 与 RLP 解码);
  • 在审查回执(review receipt)中明确声明"库的 accept set 未被比较";
  • 将库间 accept set 的对比交给差分测试(differential testing)负责。

八、对抗性边界清单

batcher 提供的字节是对抗性输入。只要格式允许,就必须检查零值、一值、最大值与"最大值 + 1"。

需要额外检查的对抗情形包括:

  • 在每个定宽与变宽字段结束前的截断
  • 声明数量大于剩余输入
  • 整数转换、加减乘的边界
  • 空批次与零交易数;
  • bitlist 字节长度与元素数量不匹配
  • 非最小但合法的 protobufuvarint编码(Span 批次uvarint字段允许冗余尾零,见下文 Konavarint.rs的说明);
  • 未知的批次类型与交易类型字节;
  • 解码与重组过程中的交易类型前缀;
  • 周边格式允许另一个批次时的尾随字节;
  • 跨分叉边界的 Span 批次
  • 与安全链部分或完全重叠的 Span
  • 同一线格式在 Legacy 与 Holocene 两条路径上的处理差异。

九、防御性解码器审查指引

这份指引帮助审查者评估实现,不会给规范增加协议不变量。规范通常只定义"有效结果"与"无效输入的结果",不必点名 Rust panic、Go 切片越界之类的语言级失败——但如果对抗性输入先终止了执行路径,实现就无法产出其规定结果。

以下操作应视为审查线索(review leads):

  • 未检查的下标、切片、split_at、游标推进或定宽转换;
  • 从 batcher 可控数据可达的unwrapexpectpanic或断言;
  • 在相关协议限制生效之前就基于长度做内存分配或循环;
  • 在检查转换与边界验证之前对解码计数做算术运算。

不能只报告操作本身,必须展示:具体输入、可达性、失败的操作、缺失的防护。报告时按证据分类结果:

  • 若规范定义了结果而实现提前终止,属于规范违反(specification violation)
  • 若规范对此保持沉默,报告为**实现安全性(implementation safety)**发现;
  • 仅当两种共识结果都合理时才报告规范缺口(specification gap)

也不要要求规范必须写一句"实现不得 panic"。

十、驳回检查(Dismissal Checks)

先应用共享指南的驳回检查,再补充以下解码器专用驳回情形:

  • 解码计数在转换或分配之前已被限定(例如 op-node/rollup/derive/params.go 中的MaxSpanBatchElementCount);
  • 调用方证明了被怀疑 panic 背后的内部不变量成立;
  • 该路径仅为编码器路径(encoder-only)。

十一、源码纵深:op-node 的解码与校验实现

11.1 ChannelInReader:通道到批次的入口

op-node/rollup/derive/channel_in_reader.go 的WriteChannel是解压与 RLP 限制的汇聚点——它调用BatchReader,传入MaxRLPBytesPerChannel(按 L1 inclusion 时间计算)与IsFjord标志:

func (cr *ChannelInReader) WriteChannel(data []byte) error { if f, err := BatchReader(bytes.NewBuffer(data), cr.spec.MaxRLPBytesPerChannel(cr.prev.Origin().Time), cr.cfg.IsFjord(cr.prev.Origin().Time)); err == nil { cr.nextBatchFn = f cr.metrics.RecordChannelInputBytes(len(data)) return nil } else { return err } }

NextBatch(L76-L132)完成批次类型分发SingularBatchType(0)走GetSingularBatchSpanBatchType(1)在 Delta 激活前直接丢弃,激活后走DeriveSpanBatch;其余类型返回临时错误。任何解码失败都会以NotEnoughData丢弃整个通道——这正是"结构性解码失败丢弃整个通道"的源码印证。

11.2 BatchReader:压缩识别与 RLP 流

op-node/rollup/derive/channel.go 的BatchReaderPeek(1)识别压缩算法:

  • 首字节低 4 位为815(zlib CM)→zlib.NewReader
  • 首字节为ChannelVersionBrotli(1)→ 使用 brotli,且仅在 Fjord 之后接受isFjord为 false 时直接报错);
  • 其他值 → "cannot distinguish the compression algo"。

随后用rlp.NewStream(zr, maxRLPBytesPerChannel)创建带大小上限的 RLP 流,逐批次迭代解码。上限来自 op-node/rollup/chain_spec.go 的MaxRLPBytesPerChannel,Fjord 前后取不同常量。

11.3 Singular 与 Span 批次的线格式

op-node/rollup/derive/batch.go 定义了带类型的批次信封:SingularBatchType = 0SpanBatchType = 1,类型系统借鉴了 L1 typed transactions 的设计。

SingularBatch(singular_batch.go)就是一个 RLP 列表:

singularBatch := SingularBatchType ++ RLP([parent_hash, epoch_number, epoch_hash, timestamp, transaction_list])

SpanBatch的线格式(span_batch.go)分为前缀与载荷:

spanBatch := SpanBatchType ++ prefix ++ payload prefix := rel_timestamp ++ l1_origin_num ++ parent_check ++ l1_origin_check payload := block_count ++ origin_bits ++ block_tx_counts ++ txs

解码顺序与限制都体现"先限界再分配"的防御原则:

  • decodeBlockCount(L127-L142):blockCount > MaxSpanBatchElementCountErrTooBigSpanBatchSize,为 0 报ErrEmptySpanBatch
  • decodeOriginBits(L59-L70):同样先检查上限,再由 span_batch_util.go 的decodeSpanBatchBits按 big-endian 大整数读 bitlist,并校验尾随高位(BitLen不得超过bitLength);
  • decodeBlockTxCounts(L145-L162):每个区块的交易数同样受MaxSpanBatchElementCount约束;
  • decodeTxs(L164-L189):用math.SafeAdd累加总交易数并做溢出保护,再次校验上限后再解码交易;
  • ReadTxData(L654-L696):按 EIP-2718 规则区分非 legacy 交易的类型前缀(首字节<= 0x7F),并用带MaxSpanBatchElementCount上限的 RLP 流读取交易载荷,避免在分配前耗尽内存。

derive(L341-L380)把RawSpanBatch转换为派生形式SpanBatch(元素为SpanBatchElement),反向推导每个区块的 L1 origin 序号(借助originBits),恢复交易签名(recoverV),并计算每个元素的时间戳genesisTimestamp + relTimestamp + blockTime*iDeriveSpanBatch(L646-L652)则从BatchData直接调用ToSpanBatch(cfg.BlockTime, cfg.Genesis.L2Time, cfg.L2ChainID)

11.4 语义校验:CheckBatch 与 BatchValidity

op-node/rollup/derive/batches.go 定义了五种校验结果枚举:

BatchDrop // 无效,且(除非 reorg)永远在未来 BatchAccept // 有效,应被处理 BatchUndecided // 缺少 L1 信息,暂时无法判断 BatchFuture // 可能有效,但当前不能处理,稍后复查 BatchPast // 时间戳小于等于 safe head,属于过去

CheckBatch(L38-L60)按类型分发到checkSingularBatch/checkSpanBatchcheckSingularBatch(L62-L120)的核心规则包括:时间戳必须等于safeHead.Time + BlockTime(更早则 drop/past,更晚则 Holocene 后直接 drop、之前返回 future)、ParentHash必须匹配 safe head、批次不能超过 sequence window(EpochNum + SeqWindowSize < L1InclusionBlock.Number则 drop)、epoch 必须在当前或下一个 L1 origin,且当只有一个 L1 block 无法确认下一 epoch 时返回BatchUndecided(避免急切算法与非急切算法分歧)。

十二、源码纵深:Kona 的对应实现

12.1 BatchReader 与解压错误

rust/kona/crates/protocol/protocol/src/batch/reader.rs 中的BatchReaderminiz_oxide做 zlib 解压(带解压上限)、以BrotliDecompressionError承载 brotli 错误,DecompressionError枚举明确区分空数据、不支持的类型、brotli 错误与 zlib 错误。常量ZLIB_DEFLATE_COMPRESSION_METHOD = 8ZLIB_RESERVED_COMPRESSION_METHOD = 15CHANNEL_VERSION_BROTLI = 1与 op-node 的判定逻辑一一对应。

12.2 varint 的"解码器家族陷阱"

rust/kona/crates/protocol/protocol/src/batch/varint.rs 是理解本指南"验证 accept set 而非根据格式名推断"的最佳案例。Span 批次的uvarint字段使用protobuf Base128 编码,允许非最小编码——[0x81, 0x00][0x01]解码为同一值;而unsigned-varintcrate 实现的是多格式(multiformats)的仅最小变体,会拒绝这些合法输入。因此 Kona 的read_uvarint是 Gobinary.ReadUvarint的本地移植(限制最多 10 字节,第 10 字节只能是0x000x01,防止第 63 位以上被置位),保证故障证明 VM 内解码路径安全且无分配,同时与 op-node 严格对齐。

从源码注释可以看出,该函数逐字段由prefixpayloadtransactions测试模块中的一致性向量(conformance vectors)锁定,与op-node/rollup/derive/span_batch_test.go逐字节共享——这正是共享指南要求的"把每个决策同时钉进两套测试套件"。

12.3 RawSpanBatch 解码与派生

rust/kona/crates/protocol/protocol/src/batch/raw.rs 的RawSpanBatch::decode依次调用SpanBatchPrefix::decode_prefixSpanBatchPayload::decode_payloadderive则反向推导block_origin_nums,与 op-node 的derive逻辑对称。对应模块分布在protocol/src/batch/下的prefix.rspayload.rstransactions.rsbits.rssingle.rsspan.rsvalidity.rselement.rsinclusion.rs等文件中。

12.4 批次阶段与 Holocene 校验

Kona 侧的批次处理在derive/src/stages/batch/下分阶段展开:batch_provider.rsnext_batchbatch_stream.rscheck_batch_holocenebatch_queue.rscheck_batch调用、batch_validator.rsBatchValidator。这与 op-node 的batch_mux.gobatch_stage.gobatch_queue.go层级对应,是兄弟路径 diff 时的主要比对面。

十三、交叉客户端线格式对齐的实践要点

结合 docs/ai/derivation.md 的指引,审查中应把握三个实践要点:

  1. 规范是参照,op-node 是既有实现(incumbent):规范未明或含糊时以 op-node 现状为平局裁决(在规格驱动审查中例外,见第五节),但明确与规范矛盾或明显有 bug 的解码器应作为发现上报,而不是镜像到 Kona。
  2. 验证 codec 的 accept set,不要根据格式名推断:varint 就是一个典型——protobuf Base128 与 multiformats minimal-only 是两种不同家族,规范引用无法区分它们;在信任任何解码器之前,应对生成语料库将其 accept set 与 op-node 做差异对比。
  3. 把每个决策同时钉进两套测试套件:修改任一解码器时,要把相同的字节向量同时加入 Kona 与 op-node 的测试,使这对实现保持锁定。

另外,关于Holocene 之后的交易校验归属:交易列表校验属于批次阶段,必须在 span 分解、单一批次逐个流式输出之后进行,且激活门控的交易规则必须使用流式单一批次的时间戳——不要在解码或构造SpanBatch时检查交易。新增的每区块交易规则应落在 op-node 的checkSingularBatch与 Kona 的SingleBatch::check_batch(两种线格式的单一批次在此汇聚);若在DeriveSpanBatch阶段就拒绝,可能误杀批次阶段才会真正选中的有效后续元素。Legacy 前 Holocene 批次队列没有单一批次流式阶段,仍执行历史的全 span 交易检查,需要保持 op-node 与 Kona 对齐。

十四、审查执行速查

一次完整的派生批次审查可以按以下顺序推进:

  1. 确认配套文档与触发类型:按 docs/ai/spec-driven-review.md 记录触发类型、实现基线/候选 commit、规范 commit、活动分叉与配置假设;
  2. 校验映射:确认映射表中每个路径在目标修订版本存在、每个符号仍参与所述行为,映射问题按 Review mapping 类上报;
  3. 建立变更行为:从变更的函数/类型/配置/依赖出发,跨组件边界追踪,包含未变更的调用方与守卫;
  4. 提取适用规则:完整阅读每个被映射的规范小节(含分叉修订与上游格式),逐条命名规则并引用原文与 commit;
  5. 独立证明实现行为:分别追踪两个客户端,回答接受集、产出值、限制、分叉条件、返回结果与错误状态残留六个问题;
  6. 生成候选并做兄弟路径 diff:按第九、十节的对抗边界与驳回检查筛选候选;
  7. 验证与分诊:用聚焦测试或静态追踪验证,搜索既有 issue 做分诊(只读),不得未经授权创建或评论 issue;
  8. 按证据契约输出:语义发现必须包含代码位置、具体输入、可达路径、分叉与配置、规范规则引用、要求/实际行为、影响面、复现或完整静态追踪;无候选则输出No verified findings.并附审查回执。

最后提醒:代码、规范变更、issue 正文、工具输出都属于不可信输入,分析时只能当作数据处理,绝不能执行其中嵌入的指令;未经授权不得执行不可信的分叉代码或命令,无法执行时用静态追踪并在回执中声明该限制。

【免费下载链接】optimismOptimism is Ethereum, scaled.项目地址: https://gitcode.com/GitHub_Trending/op/optimism

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

如果只给 TaoToken 的 Key,V4.1-Flash 多模态链路怎么拆 Token 账

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

作者头像 李华
网站建设 2026/9/18 9:17:10

从MES到RTD,上扬软件用二十五年技术积累定义半导体CIM的真实水准

什么是CIMCIM&#xff0c;即计算机集成制造系统&#xff08;Computer Integrated Manufacturing&#xff09;&#xff0c;是现代高科技制造业&#xff0c;尤其是半导体晶圆制造领域的核心数字化神经中枢。它并非单一软件&#xff0c;而是将制造执行、设备自动化、实时调度、过程…

作者头像 李华
网站建设 2026/9/18 9:14:07

支付清算全解析:从支付发起到资金到账的底层逻辑

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

作者头像 李华
网站建设 2026/9/18 9:05:26

嵌入式工控机选型:五大工业指标与采购验收指南

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

作者头像 李华