MCP逆向工程实战:解析JSON-RPC握手与工具调用
2026年10月8日的热榜我扫了一眼排在 GitHub Trending 首位的不是什么新发布的大模型框架也不是又一个花哨的前端组件库而是一个和“逆向工程 MCP”直接相关的项目。很多开发者点进榜单可能第一反应是MCP 不是已经有官方 SDK 和一堆服务体系了吗怎么还需要逆向实际上“逆向”这个动作在协议生态里从来不是考古它解决的是“接入一个没有文档的服务器、排查一个说不清原因的连接失败、或者在安全审计里搞清楚某个工具到底能被调用到什么程度”这类非常现实的问题。今天这篇就把这件事拆开聊清楚MCP 到底是什么样的协议、逆向它要关注哪几个关键节点、以及你自己动手时最常踩的坑。1. 先聊聊这次热榜的“反常”1.1 什么叫“逆向工程 MCP”先说定义。MCP 是模型上下文协议的缩写它的目标是让 AI 应用聊天机器人、IDE、自动化脚本和外部工具、数据源之间有一个统一的 Channel 通道。本身它是一个开放协议开放性意味着理论上你只要遵循 JSON-RPC 2.0 的消息格式就能接入。但现实世界里很多 MCP 服务端并不是白纸黑字写得清清楚楚的有的是内部版本对协议做了裁剪有的是老服务端只实现了部分能力有的是商业产品文档缺失但服务在跑。所谓逆向工程就是拿一个已经存在的 MCP 服务端通过抓取它收到的消息、观察它回包格式、反复探测它的能力边界最后把它的通信规律还原出来写一个自己的客户端或者适配层。这次热榜上的项目能登顶本质上说明了一个趋势单纯“能跑通官方示例”已经不再稀缺稀缺的是在复杂的真实环境里把 MCP 服务端“降服”的能力。1.2 热度为什么集中在现在MCP 生态这两年进入了一个爆发期服务端数量增长非常猛。但从“数量多”到“质量高”之间有一道坎协议版本在演进能力定义在扩展不同服务端实现细节也参差不齐。早几年大家关注的是“有没有 MCP 支持”现在关注的是“这个 MCP 服务端到底能干什么、干到什么程度、我能不能安全地把它接入自己的系统”。这种关注点的迁移恰好把“逆向工程”推到了聚光灯下。你自己部署过就会明白很多服务端看起来遵循协议但实际行为可能和预期有偏差。没有逆向和探测的手段你连问题出在哪里都定位不了。热榜第一说明大量开发者正在被这种“协议黑盒”困扰需要一套可复用的方法论。1.3 谁会第一时间用到这类项目如果你平时只调用官方 SDK对 HTTP 请求细节不敏感那这类项目对你的直接价值可能没那么大。但有三类人一定会盯着它做安全审计的开发者。他们需要确认某个 MCP 服务端暴露出的工具不会造成越权操作、命令注入或者数据泄露。做内部工具集成的开发者。业务系统用的是私有 MCP 服务端厂商只给了二进制没给协议文档这时候只能反向探测。客户端的框架开发者。需要让自家客户端兼容尽可能多的服务端就一定要理解协议实现的边界而不能只停留在标准文档层面。我也是从第二条路径进入这个领域的。当时手头一个内部工具只有服务端二进制官方文档写了个大概实际跑起来却总是报错。逼到墙角之后才认真开始分析协议最后发现所谓“兼容 MCP”和自己原以为的完全不是一回事。2. MCP 协议基础拆解逆向的对象到底长什么样2.1 MCP 的核心角色与连接模型逆向之前得先确定你在看地图上的哪个位置。MCP 的模型其实很简单概念上有三层MCP 服务端持有工具、资源、提示词的一方。它可能是本地子进程也可能是一个远程 HTTP 服务。MCP 客户端发起连接、调用工具的一方。它通常嵌在你的 AI 应用或 agent 框架里。传输层连接客户端和服务端的通道。常见的传输方式有两种一种是基于标准输入输出的本地通道一种是基于 HTTP 的远程通道。理解这个连接模型对于逆向最大的帮助是你知道了该去哪里抓消息。如果服务端是本地子进程你就去抓它的 stdout 和 stderr如果服务端是远程服务你就去抓它的 HTTP 请求和响应。抓到了消息逆向就完成了一半。2.2 JSON-RPC 消息体长什么样MCP 本身不发明新的编码协议消息格式采用 JSON-RPC 2.0。这个选择对我们来说是福音因为 JSON-RPC 足够简单一个消息就三个关键字段jsonrpc固定值 2.0method方法名比如 initialize、tools/listparams参数对象除了 method 之外的重点都在这里另外它会区分“请求”“响应”“通知”。请求带 id响应会回同一个 id通知则没有 id 也不期待响应。刚开始逆向的人容易忽略这一点把通知当成请求去等回包然后就卡死了。举个例子一个标准的工具调用请求长这样{ jsonrpc: 2.0, id: 3, method: tools/call, params: { name: fetch_url, arguments: { url: https://example.com } } }你只要能把这类消息从通道里完整捕获下来就能知道服务端具体提供了哪些能力、参数结构是什么、返回数据结构是什么。这是整个逆向工作的地基。2.3 核心消息链路initialize 到 tools/call规范化 MCP 服务端在业务请求之前会有一段流程必须做对客户端先发initialize声明自己支持的协议版本和 capabilities。服务端回一个 initialize 响应里面包含它自己的协议版本、capabilities 和 serverInfo。客户端收到后要补发一条notifications/initialized通知告诉服务端“我已经准备好进入工作状态”。然后客户端才能请求tools/list获取工具列表。用户要执行某个功能时客户端再发tools/call。这个顺序极其重要。很多“连不上”“调用失败”的问题本质不是最后一步工具调用出了问题而是握手没有完整走完。逆向工程时你首先要做的不是翻江倒海地找工具定义而是先把握手的状态机跑通。我在几个项目里都验证过一个经验只要 initialize 和 initialized 这一步对了后面大概率顺风顺水这一步不对后面全是玄学。3. 实操亲手复刻一个 MCP 客户端3.1 第二步准备探测环境要做逆向先要建立一个可控的观测环境。我会分三步准备一个可以随时启停的服务端二进制方便反复重启观察行为差异。准备一个调试客户端它不依赖任何 SDK只是一个能收发 JSON-RPC 消息的脚本。准备一份日志输出把每个消息的收发内容按时间顺序记录下来。不用急着写完整工具只需要原样输出过。我的做法是先用 netcat 或者简单的 Python 脚本搭一个“录音机”把进出消息全部保存成 JSONL 文件。每条记录都带上时间戳、方向客户端发送还是服务端返回、消息 ID。这样后面不管是分析还是回放都有证据可查。如果你连服务端进程的输出都抓不到那就要顺着传输层去找。本地进程看 stdout/stderr远程服务看 HTTP 流量和 Network 面板。3.2 第三步模拟一次完整的协议握手这一步直接用代码说话。我用 Python 写了一个最小客户端目标是跑通 initialize 并进入可调用状态。import json import subprocess server_cmd [./mock-mcp-server] proc subprocess.Popen( server_cmd, stdinsubprocess.PIPE, stdoutsubprocess.PIPE, stderrsubprocess.PIPE, ) def send_request(req_id, method, params): payload { jsonrpc: 2.0, id: req_id, method: method, params: params, } raw json.dumps(payload) \n proc.stdin.write(raw.encode()) proc.stdin.flush() line proc.stdout.readline() return json.loads(line) # 第一步initialize first send_request( 1, initialize, { protocolVersion: 2025-03-26, capabilities: {}, clientInfo: {name: debug-client, version: 0.0.1}, }, ) print(first) # 第二步通知服务端已初始化 notice { jsonrpc: 2.0, method: notifications/initialized, params: {}, } raw json.dumps(notice) \n proc.stdin.write(raw.encode()) proc.stdin.flush()这里有几个经验点protocolVersion不要填死一个值。先填一个宽松的版本然后看服务端返回什么。真正的兼容性处理以服务端返回的 version 为准。initialize响应的capabilities很关键它告诉你这个服务端支持哪些能力比如是否支持资源订阅、是否支持提示词、是否支持日志。通知消息不能带id带了反而可能被服务端忽略或者当作非法请求。3.3 第四步读取和分析工具清单握手跑通之后下一步就是问服务端“你有哪些工具可以用”。这对应的方法是tools/listtools send_request(2, tools/list, {}) for tool in tools.get(result, {}).get(tools, []): print(tool[name], tool.get(description, )) print(json.dumps(tool.get(inputSchema, {}), ensure_asciiFalse, indent2))工具列表返回的格式看起来像这样{ name: fetch_url, description: 抓取指定网址正文并转换为 Markdown, inputSchema: { type: object, properties: { url: { type: string } }, required: [url] } }inputSchema是你逆向服务端能力最重要的信息源。它本质是一个 JSON Schema 子集定义了工具接受什么参数、哪些必填、哪些选填、参数类型是什么。拿到这份清单后你基本就能列出服务端完整的外表面。一个容易忽视的细节是有些服务端返回的工具列表是动态生成的你明明在文档里看到某工具但它可能只在特定配置下才出现。遇到这种情况你的客户端逻辑里就不应该硬编码工具列表而是每次启动后实时拉取。3.4 第五步调用工具并捕获错误工具调用就是发tools/call参数里带上工具名和参数列表。这里真正需要逆向的是错误行为。同一个工具参数合法与非法回包结构可能完全不同有的服务端返回错误码有的服务端直接抛一个包含堆栈的字符串有的则什么都不返回只把错误写进 stderr。resp send_request( 3, tools/call, { name: fetch_url, arguments: {url: not-a-valid-url}, }, ) print(json.dumps(resp, ensure_asciiFalse, indent2))如果你连这个请求都返回了说明你的消息结构在基本面上是对的。顺着这个路径继续深入可以用不同参数组合探测更细的行为。老实说这就是最原始的“黑盒测试”不需要读服务端源码只需要不断调整输入、观察输出、总结规律。4. 逆向过程的关键决策不是所有服务端都值得“硬刚”4.1 先判断是协议问题还是传输问题逆向时最忌讳一上来就钻到消息格式里抠细节。我在实践中会先做一个快速体检先确认传输层通不通再确认握手流程能不能完成最后才去看具体的方法调用。传输层的隐性坑是最多的本地 stdio 服务端可能因为启动参数不够而报错退出。远程 HTTP 服务端可能带版本路径比如根路径并不是实际的服务入口。有些服务端需要先鉴权返回 401 后通过特殊请求头才放行。还有一部分服务端默认监听的是 SSE 事件流而你的客户端读的却是普通 HTTP 请求两者对不上必然失败。我见过很多同事调了一整天 MCP 协议最后发现只是请求头里少了一个Accept字段。所以排障顺序一定是“传输层优先协议层次之”。4.2 复用现成库还是自己写协议骨架做逆向工程时很多人会想与其自己造轮子不如直接用官方的 SDK。这个想法在日常开发里是对的但在协议分析阶段恰恰是个坑。SDK 封装程度太高它会帮你处理很多“默认行为”而这些“默认行为”恰好会掩盖服务端的真实响应。你想看原始数据却被 SDK 内部逻辑包装了一层等于雾里看花。更合理的做法是分析阶段用自己写的最小骨架脚本记录原始请求/响应。验证阶段用现成库跑同样的流程看是否得到一致结果。生产阶段再回到成熟库把最小骨架的“心智模型”套上去。这样既没有丢掉对协议的全盘控制又在最终落地上获得了稳定性的保障。4.3 安全意识要提前部署逆向工程 MCP 服务端不只是要拿到“能不能通”还要评估“通了之后能干什么”。MCP 服务端对外暴露的每个工具本质上都是一个远程调用接口。在连入一个未知来源的服务端时安全确认非常关键。讲的几个问题值得仔细思考工具列表里是否包含执行 shell 命令的能力。某个工具是否允许传入任意路径去读写文件。工具调用过程中是否对参数做了校验。服务端是否可能主动推送对我们不利的资源。这些信息全部可以通过逆向过程收集。比如拿着工具清单扫一眼就能识别出高风险工具再通过构造恶意参数去探测服务端是否做了防御。这种安全扫描越早做后续接入正式环境的风险就越低。5. 常见问题与排查技巧实录5.1 问题速查表这一块是纯经验干货。我把过去几次逆向 MCP 服务端遇到的典型问题整理成了一个表格方便你直接对照。现象可能原因处理建议初始化请求发出去后没有响应传输层连错端口/路径或者服务端启动失败先用 curl 或原始 socket 验证通道握手返回协议版本不匹配服务端只支持旧版本而你传了太新的协议号改成服务端返回的 protocolVersion 再重试通知消息被忽略通知里带了 id服务端不认确保通知消息不包含 idtools/list 返回空数组服务端配置没加载任何工具或需要特定鉴权检查服务端的启动配置和环境变量tools/call 返回错误码却看不到堆栈错误信息被写进 stderr 而不是消息体同时捕获 stderr并和请求消息按时间戳对齐HTTP 模式请求返回 404服务端点不在根路径可能在 /mcp 或 /api/mcp 之下多试几个常规路径结合服务端文档消息内容过大被截断默认的读取行数或缓冲区太小改用按长度读取或增大缓冲区服务端响应正常但客户端一直超时对方在原地等待一个不需要请求体的长连接检查是否还需要发 keepalive 通知5.2 最隐蔽的一个坑消息解析的边界本地 stdio 通道有个特别容易踩的坑它以换行符作为消息边界但某些服务端的日志信息会直接混杂在 stdout 里输出。如果你的客户端每读到一行就做 JSON 解析读到日志行时就会直接报错。我处理这一类问题的办法是增加一层“解析容错”先把每一条原始行原样保存再做 JSON 校验如果不能解析就把它视为旁路日志单独存储而不是中断整个流程。这样做的好处是不会丢失消息同时还能保留完整的日志上下文。后期排查问题时非常舒服。5.3 关于“降级兼容”的思考逆向协议时你可能会遇到服务器返回某些你还看不懂的字段比如未来的版本扩展能力。我的原则是不认识的字段保留原样不要丢掉回传时尽量只改必需字段其他字段全部透传。协议是活的你没法保证某个你忽略的字段在后面某个环节不是关键。让服务端认为你是它的同类更利于把整个流程跑完。6. 让热榜项目真正可用的两个补强方向6.1 增加协议审计日志如果你准备把这类逆向项目纳入自己的工具链我强烈建议你给客户端加一个协议审计日志模块。有什么用当你接入的不是一个服务端而是成百上千个服务端时快速定位问题最快的方式就是对比日志。你只需要记录五个维度的数据请求和响应方向消息 ID 和 method耗时是否报错原始消息摘要有了这份日志你就不用再靠猜了。我自己在监控里把这些日志接上之后售后问题的定位时间直接缩短了一半以上。6.2 动态输出一份服务端能力报告第二个补强方向是自动生成能力报告。通过遍历 initialize、tools/list、resources/list、prompts/list 四组请求把服务端公布的能力整理成结构化 JSON。然后把这份 JSON 作为后续测试的基线。当服务端升级后再跑一次报告diff 一下就知道它改了什么。这个习惯帮我避免了无数次“升级后突然白屏”的尴尬局面。其实这个方法不算特殊就是把逆向过程中手动观察的动作固化成了自动化脚本。你前期做得越规范后面维护起来越省心。7. 最后分享一个我用逆向方法时悟到的习惯先说结论永远不要只满足于“跑通”。跑通只是一个起点你真正要弄清楚的是“它为什么这次能跑通”。如果某次集成莫名其妙就成功了我反而会警惕会回头把服务端的原始协议版本、能力列表、消息边界再复查一遍直到能准确解释每个关键步骤为止。这个习惯帮我规避过很多隐性风险。因为有太多“成功接入了 MCP”的项目其实只是官方示例里最简单的路径根本没有覆盖真实生产环境的版本差异、鉴权差异、错误处理差异。而通过逆向工程把这些差异摸清之后你对系统的理解深度会完全不一样。遇到问题时别人靠猜你靠的是对通信规律整体的把握这就是热榜项目背后最值得迁移的能力。

相关新闻

ARPG战斗框架实战:基于虚幻GAS的技能与属性系统设计

ARPG战斗框架实战:基于虚幻GAS的技能与属性系统设计

1. 为什么ARPG战斗框架需要GAS 做ARPG的人迟早会撞上一堵墙:战斗逻辑越写越乱。最开始你可能只是想让角色挥一刀,扣个血,播个特效。写着写着,暴击、闪避、霸体、护盾、持续伤害、属性克制、技能连携、冷却缩减、buff叠加规则全涌进…

2026/10/12 14:39:03 阅读更多 →
Aspose.Words版本合集怎么选?19.5等三版引用与激活避坑指南

Aspose.Words版本合集怎么选?19.5等三版引用与激活避坑指南

简介:该合集面向Java开发人员,提供aspose-words-for-java 19.5、18.10等三个版本的jar包,可在JDK 6环境下直接导入Eclipse使用,解决Word文档转PDF、生成与处理文档等场景需求。经测试的破解版本无水印、无文件大小限制&#xff0c…

2026/10/13 15:50:27 阅读更多 →
51单片机+8×8点阵写贪吃蛇:从硬件接线到C语言实现全攻略

51单片机+8×8点阵写贪吃蛇:从硬件接线到C语言实现全攻略

如果你学过C语言,又刚好有点单片机基础,那“51单片机 8*8点阵 贪吃蛇”这个组合,绝对是最值得上手的一个练手项目。它不复杂,但GPIO控制、动态扫描、按键输入、状态机设计全都能练到。我前前后后做过三版,也帮同学排…

2026/10/12 14:39:03 阅读更多 →

最新新闻

基于Matlab GUI的轮轨接触几何计算程序设计与实现

基于Matlab GUI的轮轨接触几何计算程序设计与实现

做车辆动力学的人,应该都有过被轮轨接触几何支配的恐惧。不管你是在算临界速度、评估脱轨安全性,还是在调悬挂参数,第一关都得把轮轨接触几何算清楚。左右轮滚动圆半径差、接触角、等效锥度,这些量直接决定你后面的动力学仿真靠不…

2026/10/13 16:51:39 阅读更多 →
Winform轻量级可交互FlowChart流程图实现方案

Winform轻量级可交互FlowChart流程图实现方案

简介:这是一份基于WinForm平台实现的轻量级流程图绘制工具源码,面向C#初学者与小型项目开发者,解决快速构建可视化业务流程图的需求。资源以FlowChart.Net开源库为基础深度定制,功能简洁实用,适合嵌入教学演示、内部管…

2026/10/13 16:51:39 阅读更多 →
SIP软电话源码实战:从编译到NAT穿透与信令定制

SIP软电话源码实战:从编译到NAT穿透与信令定制

简介:本资源是一份基于SIP协议的软电话完整源代码工程,面向通信协议开发初学者、VoIP系统学习者及嵌入式/网络应用开发者,旨在帮助理解SIP信令交互、媒体协商与实时语音通信的核心实现逻辑。压缩包共302个文件,含45个C源文件&…

2026/10/13 16:51:39 阅读更多 →
测试工程师成长路径:从功能测试到测试架构师的能力进阶指南

测试工程师成长路径:从功能测试到测试架构师的能力进阶指南

前阵子我面试一位五年经验的测试工程师,简历上写着“熟悉自动化测试”,结果我随口问了一句“接口测试的断言除了状态码还会验什么”,他支支吾吾说了半天,最后落点在“看返回结果对不对”。这个场景不是个例,聊测试工程…

2026/10/13 16:51:39 阅读更多 →
MySQL课件精讲:从E-R图到候选码的数据库设计核心

MySQL课件精讲:从E-R图到候选码的数据库设计核心

简介:这套《MySql数据库原理及应用》课件汇总共1个文件(pptx格式),压缩包大小13.29MB,覆盖整本教材的教学内容。资源面向数据库初学人员、高校师生及自学MySQL的读者,系统讲解数据库系统基础知识与MySQL应用…

2026/10/13 16:51:39 阅读更多 →
AI工程化能力三支柱:数据智能、模型架构与系统交付

AI工程化能力三支柱:数据智能、模型架构与系统交付

1. 这不是“学AI”,而是构建一套可迁移、能落地的工程化能力体系很多人点开“AI培训学什么”这类标题,第一反应是找速成班、抄代码、背模型名字——结果学了三个月,连数据怎么清洗都卡在pandas报错上;或者花两万块报了“大模型实战…

2026/10/13 16:50:38 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

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

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

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

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

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

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

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

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

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

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 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/12 21:05:09 阅读更多 →
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/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练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/12 18:11:17 阅读更多 →