ONVIF Java SDK 实战:设备发现、鉴权与取流避坑指南
简介这份 ONVIF JAVA SDK 面向从事网络摄像头接入、安防平台开发及物联网视频集成的 Java 工程师帮助开发者快速理解并实现 ONVIF 协议的核心交互流程。压缩包共 1157 个文件以 1147 个 Java 源码为主体辅以 5 个 jar 依赖库、Gradle 构建脚本与批处理文件整体约 2.32MB结构紧凑、便于导入 IDE 直接编译调试。SDK 覆盖 ONVIF 协议的全部功能Demo 中演示了设备发现、媒体配置等核心能力复杂功能需结合源码自行扩展。ONVIF 本质是 UDP 加 HTTP 加 XML 的组合技术门槛不高难点在于字段繁多读者可借助完整源码逐层梳理报文结构与调用链路掌握设备发现、能力协商与媒体流地址获取等关键环节为二次开发或自研协议栈打下基础。目前已有 656 人学习下载适合具备一定 Java 与网络编程基础、希望深入理解 ONVIF 实现细节的开发者参考。1. 从一堆 WSDL 到能跑的设备发现这份 ONVIF JAVA SDK 到底解决了什么手里有个网络摄像头想用 Java 接进来做实时预览、云台控制、录像回放结果翻遍官方文档只找到一堆 WSDL 和 SOAP 报文示例连个能直接mvn compile的工程都没有——这是很多做安防集成、视频平台对接的开发者都遇到过的开局。ONVIF 协议本身不复杂本质就是基于 SOAP 的 Web Service设备发现走 WS-Discovery能力查询走 GetCapabilities媒体流走 GetStreamUriPTZ 走 ContinuousMove。但真要从零手写这些 SOAP 请求光是命名空间和鉴权头就能耗掉两三天。这份 ONVIF JAVA SDK 就是把这些底层 SOAP 交互封装成 Java 可调用接口的资源包拿到手能直接跑设备发现、拉 RTSP 地址、控制云台适合做视频监控平台对接、NVR 集成、边缘设备管理的后端开发者。它不解决摄像头硬件问题也不替代 RTSP 播放器它解决的是「Java 侧怎么跟 ONVIF 设备说上话」这一段。2. 拆开 SDK 看结构核心类、依赖与设备发现链路2.1 资源包里有什么先认清模块边界拿到压缩包解压后通常能看到几个关键部分lib或 Maven 坐标下的核心 jar、src里的示例代码、以及若干 WSDL 文件。核心 jar 里一般包含设备管理、媒体、PTZ、事件、录像回放这几个服务代理类。常见做法是 SDK 把 ONVIF 的Device、Media、PTZ、Imaging等 service 分别封装成独立的客户端类每个类内部持有 SOAP 连接和鉴权信息。示例代码里通常有一个DiscoveryTest或DeviceManagerTest用来演示怎么发 WS-Discovery 组播包找设备。需要先确认的依赖有三块一是 SOAP 引擎老版本 SDK 多用 Axis2 或 CXF新一点的用 JAX-WS RI二是 WS-Discovery 的实现有些 SDK 自己写了组播收发有些依赖ws-discovery第三方库三是 XML 解析通常 JDK 自带的够用。如果 SDK 基于 Axis2那axis2-kernel、axis2-adb、axis2-transport-http这几个 jar 必须齐缺一个就会在运行时抛ClassNotFoundException而且报错信息往往指向 WSDL 解析失败容易误判成网络问题。提示先看pom.xml或lib目录里的 jar 清单确认 SOAP 引擎版本。Axis2 1.7.x 和 1.8.x 在SOAPEnvelope处理上有差异混用会导致MustUnderstand头解析异常。2.2 设备发现WS-Discovery 的组播逻辑与代码落地ONVIF 设备发现不走单播走的是 WS-Discovery 标准客户端往239.255.255.250:3702发一个Probe组播包设备收到后单播回ProbeMatch里面带设备服务地址通常是http://ip/onvif/device_service。SDK 一般把这个过程封装成一个discoverDevices(timeout)方法。下面是一段典型的调用代码// 创建发现客户端指定超时时间毫秒 DeviceDiscovery discovery new DeviceDiscovery(); // 发起 WS-Discovery 探测等待 3000ms 收集响应 ListDeviceInfo devices discovery.discoverDevices(3000); for (DeviceInfo dev : devices) { // 设备服务地址后续所有 SOAP 请求都发往这个 endpoint System.out.println(Endpoint: dev.getEndpoint()); // 设备类型标识用于区分摄像头/NVR/编码器 System.out.println(Type: dev.getType()); // 设备可能返回多个 XAddr取 device_service 那个 System.out.println(Scopes: dev.getScopes()); }这段代码的逻辑是discoverDevices内部起一个 UDP socket 绑定随机端口加入组播组发送 Probe 报文然后在超时窗口内循环receive每收到一个 ProbeMatch 就解析出XAddrs和Scopes封装成DeviceInfo。参数上超时时间设 3000ms 是常见值设太短比如 500ms在设备多的网段会漏设备设太长比如 10000ms会拖慢启动。如果发现结果为空先确认运行机器是否在同一网段、防火墙是否放行 UDP 3702 入站、以及网卡是否支持组播。有些云服务器默认不开组播这种情况只能改用单播直连已知 IP。2.3 从发现到取流GetCapabilities 与 GetStreamUri 的衔接发现到设备只是第一步接下来要拿 RTSP 地址。流程是先用设备服务地址创建DeviceClient调GetCapabilities拿到 Media 服务的 XAddr再用 Media 服务调GetProfiles拿到媒体配置 token最后用 token 调GetStreamUri拿 RTSP URL。代码大致如下// 用发现到的 endpoint 创建设备客户端 DeviceClient deviceClient new DeviceClient(deviceEndpoint, username, password); // 获取能力集里面包含 Media、PTZ、Events 等服务的地址 Capabilities caps deviceClient.getCapabilities(); String mediaXAddr caps.getMediaXAddr(); // 创建媒体客户端 MediaClient mediaClient new MediaClient(mediaXAddr, username, password); // 获取所有媒体配置主码流、子码流通常各一个 profile ListProfile profiles mediaClient.getProfiles(); String token profiles.get(0).getToken(); // 用 profile token 换取 RTSP 地址 StreamUri uri mediaClient.getStreamUri(token, RTP-Unicast, RTSP); System.out.println(RTSP URL: uri.getUri());这里的关键参数是GetStreamUri的StreamSetupTransport选RTP-Unicast还是RTP-MulticastProtocol选RTSP还是HTTP。绝大多数摄像头支持RTP-Unicast RTSP。如果返回的 URI 里带?参数那是设备厂商附加的鉴权信息不要手动去掉。拿到 RTSP 地址后Java 侧通常用javacv或gstreamer拉流那部分不在这个 SDK 范围内SDK 只负责把地址要出来。3. 鉴权、PTZ 与事件订阅三个最容易翻车的实操点3.1 WS-Security UsernameToken 鉴权摘要模式与明文模式的切换ONVIF 设备的鉴权走 WS-Security 的UsernameToken分两种模式PasswordText明文和PasswordDigest摘要。大部分设备默认或只支持PasswordDigest但有些老设备只认明文。SDK 里通常有一个SecurityConfig或AuthHeader类来配置。如果鉴权失败设备返回的是 SOAP Fault错误码可能是NotAuthorized或ter:NotAuthorized但 Axis2 有时会把它包成AxisFault信息不直观。常见做法是先试PasswordDigest失败再切PasswordText。摘要模式的计算方式是Base64(SHA1(nonce created password))其中nonce是随机数created是 UTC 时间戳。如果设备时间和服务器时间差超过 5 分钟摘要校验会失败报Unauthorized。这是最容易被忽略的坑摄像头没接 NTP时间飘了怎么调代码都不通。解决办法是登录摄像头 Web 界面校准时间或者在 SDK 里允许关闭时间戳校验不推荐仅调试用。// 配置鉴权摘要模式 SecurityConfig config new SecurityConfig(); config.setUsername(admin); config.setPassword(admin123); // 默认走 PasswordDigest若设备不支持则改为 false config.setUseDigest(true); // 设置时间偏移容忍秒调试时可放宽 config.setTimeOffsetTolerance(300); DeviceClient client new DeviceClient(endpoint, config);参数timeOffsetTolerance不是所有 SDK 都有如果没有就只能靠校准设备时间。useDigest设为 false 时密码在 SOAP 头里是 Base64 编码的明文抓包可见生产环境不建议。3.2 PTZ 控制ContinuousMove 的速度参数与坐标系PTZ 控制是问得最多的功能之一。ONVIF 的 PTZ 服务提供ContinuousMove、RelativeMove、AbsoluteMove、GotoPreset等操作。ContinuousMove最常用参数是一个PTZSpeed向量包含PanTilt的 x、y 和Zoom的 x取值范围通常是 -1.0 到 1.0。但不同厂商对速度的响应曲线不一样有的 0.1 就很快有的 1.0 才慢慢动。// 创建 PTZ 客户端地址从 GetCapabilities 的 PTZ XAddr 获取 PTZClient ptzClient new PTZClient(ptzXAddr, username, password); // 获取 PTZ 配置 token ListPTZConfig configs ptzClient.getConfigurations(); String ptzToken configs.get(0).getToken(); // 向左上方向持续移动速度 0.5 PTZSpeed speed new PTZSpeed(); speed.setPanTilt(0.5f, 0.5f); // x: 左右y: 上下 speed.setZoom(0.0f); // 不变焦 ptzClient.continuousMove(ptzToken, speed); // 停止移动必须显式调用否则设备会一直转 ptzClient.stop(ptzToken, true, true);逻辑说明continuousMove发出后设备立即开始动直到收到stop或超时。很多新手忘了调stop结果摄像头转到底卡住血泪经验。参数上PanTilt的 x 正值向右、负值向左y 正值向上、负值向下但有些厂商反过来需要实测。stop的两个布尔参数分别控制 pan/tilt 和 zoom 是否停止一般都给 true。如果设备不支持连续移动会返回ActionNotSupported这时改用RelativeMove。3.3 事件订阅PullPoint 与 Basic Notification 的选型ONVIF 事件有两种模式Basic Notification设备主动推送到客户端指定的 HTTP 地址和PullPoint客户端定期拉取。SDK 通常两种都封装了。Basic Notification需要客户端有一个公网可达的 HTTP 服务适合服务端部署PullPoint由客户端主动拉适合内网或 NAT 环境。// 创建事件客户端 EventClient eventClient new EventClient(eventXAddr, username, password); // 创建 PullPoint 订阅超时 600 秒 PullPointSubscription sub eventClient.createPullPointSubscription(600); // 拉取事件等待最多 30 秒 ListEvent events sub.pullMessages(30); for (Event e : events) { // 事件主题如 tns1:RuleEngine/CellMotionDetector/Motion System.out.println(Topic: e.getTopic()); // 事件时间 System.out.println(Time: e.getUtcTime()); }createPullPointSubscription的 timeout 参数是订阅有效期到期后需要续订或重建。pullMessages的 timeout 是单次拉取的等待时间设太短会频繁空轮询设太长会延迟事件响应。常见值是 30 到 60 秒。如果拉不到事件先确认设备是否开启了对应的事件规则比如移动侦测很多摄像头默认关闭需要在 Web 界面里手动开。4. 避坑与排查设备发现不到、鉴权失败、取流为空怎么办4.1 发现不到设备组播被拦或网卡选错现象discoverDevices返回空列表但摄像头 Web 界面能打开。原因通常是运行环境不支持组播或者 SDK 绑定了错误的网卡。多网卡机器上SDK 默认可能绑到虚拟网卡比如 Docker 的docker0组播包发不出去。解决办法在 SDK 里指定网卡接口或者改用单播直连。如果 SDK 没提供网卡配置接口可以用System.setProperty(java.net.preferIPv4Stack, true)强制 IPv4有时能绕过 IPv6 组播问题。4.2 鉴权失败时间偏移与 nonce 重放现象SOAP 请求返回NotAuthorized但用户名密码确认无误。原因一是设备时间与服务器时间差超过 5 分钟摘要校验失败原因二是 nonce 被设备判定为重放同一 nonce 短时间重复使用。解决办法先校准设备时间确保 NTP 同步再检查 SDK 是否每次请求生成新 nonce如果 SDK 缓存了 nonce需要改成每次新建。有些 SDK 的SecurityConfig有nonceGenerator接口可以注入自定义实现。4.3 取流地址为空profile token 选错或编码格式不支持现象getStreamUri返回空 URI 或抛异常。原因一是profiles列表里第一个不是主码流可能是子码流或某个未配置的 profile原因二是StreamSetup的Protocol设成了设备不支持的HTTP。解决办法遍历所有 profile打印encoding和resolution选分辨率最高的那个Protocol统一用RTSP。如果设备只支持RTP-Unicast不要选RTP-Multicast。4.4 PTZ 不动token 错误或设备未使能现象continuousMove调用成功但摄像头不动。原因一是 PTZ token 拿错了getConfigurations返回的 token 可能不是当前激活的原因二是设备 PTZ 功能未使能有些摄像头需要在 Web 界面里开启「云台控制」。解决办法用getStatus查当前 PTZ 状态确认 token 对应的是活动配置如果设备有多个 PTZ 配置逐个试。另外部分设备要求先调gotoHomePosition初始化再发移动指令。4.5 事件拉取超时订阅过期或规则未触发现象pullMessages一直返回空或超时。原因一是订阅已过期设备侧已销毁原因二是没有配置事件规则设备根本不产生事件。解决办法在拉取循环里捕获SubscriptionExpired异常自动重建订阅同时登录设备 Web 界面确认移动侦测等规则已启用并绑定了通知。5. 进阶把 SDK 包成 Spring Boot Starter 与多设备并发管理单台设备调通之后实际项目里往往是几十上百台。直接new DeviceClient每台一个实例线程池一开就爆。我一般会做一层封装用ConcurrentHashMapString, DeviceSession缓存设备会话每个 session 持有 device/media/ptz 三个客户端配合定时任务做保活和重连。下面是一个简化的会话管理代码Component public class DeviceSessionManager { // 设备 endpoint - 会话线程安全 private final ConcurrentHashMapString, DeviceSession sessions new ConcurrentHashMap(); // 获取或创建会话 public DeviceSession getSession(String endpoint, String user, String pass) { return sessions.computeIfAbsent(endpoint, ep - { DeviceSession s new DeviceSession(); s.deviceClient new DeviceClient(ep, user, pass); // 初始化时拉取能力集缓存 media/ptz 地址 Capabilities caps s.deviceClient.getCapabilities(); s.mediaClient new MediaClient(caps.getMediaXAddr(), user, pass); s.ptzClient new PTZClient(caps.getPtzXAddr(), user, pass); return s; }); } // 定时保活每 60 秒调一次 GetSystemDateAndTime Scheduled(fixedDelay 60000) public void keepAlive() { sessions.forEach((ep, s) - { try { s.deviceClient.getSystemDateAndTime(); } catch (Exception e) { // 失败则移除下次请求重建 sessions.remove(ep); } }); } }这段代码的关键点computeIfAbsent保证同一 endpoint 只创建一个会话keepAlive用GetSystemDateAndTime这个轻量接口做心跳失败就剔除避免死连接堆积。参数上心跳间隔 60 秒是经验值太短会增加设备负担太长则故障发现慢。如果设备数量超过 200建议按网段分片每个分片一个线程池避免单线程轮询成为瓶颈。另一个进阶用法是把 SDK 包成 Spring Boot Starter通过ConfigurationProperties暴露用户名密码、超时时间、心跳间隔等参数业务代码只注入DeviceSessionManager即可。这样换设备品牌或升级 SDK 版本时改动面小。验证方法也简单写一个CommandLineRunner启动时自动发现设备、拉取 RTSP 地址、打印出来能跑通就说明整条链路没问题。从那以后我每次接新摄像头都强制先跑一遍发现、鉴权、取流、PTZ 这四步的最小闭环确认全通再写业务逻辑省得后面在业务代码里排查协议问题。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

前端风控参数if-match生成逻辑拆解:从抓包到算法还原

前端风控参数if-match生成逻辑拆解:从抓包到算法还原

去年年底接了一个某查查平台的接口调试任务,需要在合规的前提下对接它的数据服务。调试过程中发现一个很有意思的现象:搜索接口的请求体里躺着一个名为 if-match 的参数,这个参数看起来像是一个普通的头字段,但每次请求的值都不一…

2026/10/17 18:54:15 阅读更多 →
基于知识图谱的鱼类病害防治知识管理系统:从本体设计到查询推理的工程实践

基于知识图谱的鱼类病害防治知识管理系统:从本体设计到查询推理的工程实践

简介:这份资源是面向计算机相关专业本科毕业生与毕业设计指导教师的任务书文档,围绕「基于知识图谱的鱼类病害防治知识管理系统」课题展开,适用于选题参考、任务书模板借鉴与毕设流程梳理。压缩包内仅含1个doc文件,大小约61KB&…

2026/10/17 18:53:14 阅读更多 →
Codex写ORM查询为什么数据越多越慢?用N+1优化接口性能

Codex写ORM查询为什么数据越多越慢?用N+1优化接口性能

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

2026/10/17 18:53:14 阅读更多 →

最新新闻

React Native 应用迁移 OpenHarmony 实战:数学练习应用开发与调试

React Native 应用迁移 OpenHarmony 实战:数学练习应用开发与调试

这两年我一直有意把一些跨平台项目往 OpenHarmony 上搬,刚好遇到一个适合练手的场景:数学练习。大概背景是,我手上有一份用 React Native 写的口算练习 Demo,之前在 iOS 和 Android 上都跑得很顺,后来一次技术交流中看…

2026/10/17 19:45:55 阅读更多 →
srcset与sizes实战:彻底搞懂响应式图片加载优化

srcset与sizes实战:彻底搞懂响应式图片加载优化

很多做前端的同学,工作了两三年,srcset这个名字听得很熟,但真到项目里要优化图片加载时,却总是绕开它走。问起来,答案基本是“响应式图片嘛,让不同屏幕加载不同尺寸的图”,再深入一点就说不清了…

2026/10/17 19:45:55 阅读更多 →
教育Agent实践:从个性化学习路径到自动批改的全流程落地

教育Agent实践:从个性化学习路径到自动批改的全流程落地

把一个班级四十个人的错题逐题看完,再给每个人画出“下一条该做的题”,这个工作量换成任何一位老师都需要加班到深夜。我两年前开始做“教育 Agent”这个方向时,目标很直接:能不能让系统自己完成“诊断—推荐—批改—反馈”这个循…

2026/10/17 19:45:55 阅读更多 →
iOS列表视频播放全解:AVPlayer架构、自动播放与性能优化

iOS列表视频播放全解:AVPlayer架构、自动播放与性能优化

简介:面向iOS中高级开发者的列表视频播放示例工程,模仿网易视频和今日头条的浏览交互,解决滚动列表内视频预览、点击全屏播放、切换后自动暂停等常见需求。工程基于列表控件与系统视频框架,覆盖自定义视频单元格、渲染图层、单元格…

2026/10/17 19:44:54 阅读更多 →
小学生C++竞赛初赛备考全攻略:题型拆解与高频失分点避坑

小学生C++竞赛初赛备考全攻略:题型拆解与高频失分点避坑

简介:2024年第三十一届厦门市小学生计算机C语言竞赛(初赛)试题,是一份面向小学高年级信息学初学者的完整真题试卷。全卷共四大题、45小题,满分100分,限时90分钟,内容涵盖计算机基础知识、C语法、…

2026/10/17 19:44:54 阅读更多 →
2025生成式AI流量趋势:代码补全爆发与搜索替代路径

2025生成式AI流量趋势:代码补全爆发与搜索替代路径

简介:《2025年生成式AI全球行业趋势报告》是一份面向投资者、产品经理、市场研究员和AI从业者的行业数据盘点。报告基于权威投资者情报热力图,以2025年5月9日截止的数据为节点,重点分析近12周全球生成式AI各细分方向的流量同比增长情况。内容…

2026/10/17 19:44:54 阅读更多 →

日新闻

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

简介:《DeepSeek使用方法.pdf》是一份面向DeepSeek入门及进阶用户的完整使用指南,重点围绕V3与R1两款模型的能力差异与适用场景展开,帮助读者根据任务类型与成本选择合适的模型。资源共1个PDF文件,压缩包大小约311KB,篇…

2026/10/17 0:00:25 阅读更多 →
200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

简介:这是一份聚焦DeepSeek提示词优化的《200Deepseek润色指令》PDF文档,共收录两百余条可直接套用的润色指令,涵盖语法修正、风格统一、逻辑梳理、表达精炼等维度,并针对商务写作、学术论文、广告文案等场景给出差异化建议。文档…

2026/10/17 0:00:25 阅读更多 →
OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

简介:本资源是一份基于OpenCV的本科课程设计完整报告,面向计算机视觉初学者、图像处理学习者及高校通信/信息类专业学生,解决物体智能计数与检测结果结构化记录的实际问题。项目覆盖图像预处理、轮廓检测、边界框绘制、轨迹栏交互式参数调节、…

2026/10/17 0:00:25 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/16 21:35:51 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/17 1:44:59 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/16 15:04:24 阅读更多 →

月新闻

我发现了一个新思路:用 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/16 21:54:38 阅读更多 →
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/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练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/16 14:23:36 阅读更多 →