news 2026/9/9 9:59:53

微信群聊机器人SDK实战:从原理选型到工程化落地

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
微信群聊机器人SDK实战:从原理选型到工程化落地

从做微信机器人这件事的第一天起,我就觉得“SDK”这三个字被说得很玄乎。尤其是打开搜索引擎一查,出来的内容不是广告就是碎片教程,讲怎么安装的居多,真正把原理讲透的很少。后来自己做了几个项目,从企业微信的官方API到个人微信方向的开源框架,前前后后踩了不少坑,才慢慢把整条链路摸顺。这篇就用我实际做项目的经验,把微信群聊机器人SDK从概念、选型、核心机制,到实操落地、排错、工程化扩展的完整路径讲一遍,希望给正准备入坑的朋友省点时间。

需要说明的是,本文涉及的代码以逻辑示例为主,目的是讲清楚设计和实现思路,具体接口名、包名请以你实际使用的SDK文档为准。文中的经验来自我自己的实操项目,不同版本、不同平台之间会有差异,但整体思路是通用的。

1. 先搞清楚微信机器人SDK到底解决什么问题

1.1 微信机器人SDK到底是什么

SDK全称Software Development Kit,中文叫软件开发工具包。放到微信机器人的场景里,它就是一套帮你连接微信能力和自己业务代码的工具集合。你不需要从底层去研究微信服务器之间怎么通信、消息怎么加密、事件怎么推送,SDK把这些脏活累活封装好了,你只需要调用它提供的方法,写自己的业务逻辑就行。

拆开看,这个组合里三个词各有分工。微信提供的是人和对话场景,机器人承担的是自动化处理和主动服务,SDK则是连接两者的桥梁。你最终要做的,是一个能监听消息、理解指令、执行动作、主动推送的应用程序。

我见过不少刚接触的人,误以为装好SDK机器人就能自己说话。真实情况是,SDK只是一个“底座”,它能帮你收发消息,但“收到消息之后干什么”完全取决于你写的代码。你可以让它接到“你好”就回复“你好”,也可以让它对接内部工单系统,这些都是SDK能力之外的业务设计。

1.2 两条技术路线:官方接口和开源框架,怎么选

市面上的微信机器人SDK,表面上看名字都差不多,实际上分属两条完全不同的技术路线,选错的话后面会非常痛苦。

一条是以企业微信API、微信公众号接口、微信开放平台为代表的官方路线。这类SDK有稳定的接口文档、明确的错误码、规范的权限体系,适合做客户服务、内部通知、群管理、消息提醒等场景。官方路线的优势是稳定和合规,账号体系清晰,不容易因为客户端升级而突然挂掉。缺点是功能边界很明确,比如个人微信里的一些接口能力,在官方路线上是拿不到的。

另一条是个人微信方向的开源框架和工具。这类SDK模拟一个人微信号的行为,实现自动加好友、自动发朋友圈、监听群消息、自动回复等能力。它适合做小范围的个人自动化、测试、数据采集,或者对界面交互要求高的场景。但这条路线的稳定性比较看运气,微信客户端一升级,框架可能就要跟着适配,账号也有被限制的风险。

我自己的选型原则很简单:如果业务场景能通过企业微信API或公众号解决,优先走官方路线;如果确实需要个人微信的行为模拟能力,那就要做好维护成本的心理准备,并且一定要控制风险和频率,别拿核心业务去赌。

1.3 为什么不要重复造轮子,用SDK省下的时间去哪了

早期我也有过“不用SDK,自己写接口请求”的冲动。当时觉得SDK是个黑盒,不放心,非要自己拼HTTP请求去调接口。结果发现,真正麻烦的根本不是“发一条消息”这个动作,而是周边那堆琐碎事。

拿企业微信API举例,你要处理access_token的获取和刷新、回调消息的加解密、签名校验、接口错误码重试、消息格式组装。这些逻辑每一个单拎出来都不难,但合在一起,调试一遍非常耗时间。我第一次自己写没走SDK,光调试验证回调就花了一整天,其中大半时间耗在加密解密的细节上。后来换成SDK,初始化三行代码,回调验证自动处理,省下来的时间全部用在业务逻辑上。

用SDK还有一个隐形好处:社区生态。成熟SDK的用户量大,网上能搜到的踩坑案例多,出了问题十有八九已经有人遇到过。自己做底层的那些逻辑,遇到问题只能自己翻文档猜原因,效率完全不是一个级别。

2. SDK内部的核心机制,先弄明白再写代码

2.1 消息从哪来:主动拉取和被动回调

做机器人,绕不开的第一个问题就是:程序怎么知道有人说话了?主流的消息获取机制有两种。

一种是主动拉取,也就是轮询。程序每隔几百毫秒或者几秒,主动向平台问一次“有没有新消息”,有就取回来处理。这种方式的实现逻辑简单,不依赖公网回调地址,部署在局域网环境也能用。缺点是实时性一般,而且频繁轮询容易触发频率限制,还可能造成消息积压或重复消费。

另一种是被动回调,也就是webhook。平台侧一旦有消息,就往你预先配置的URL上推送数据。你的服务器收到请求后,做验签、解密、处理,然后返回结果。这种方式的实时性好,性能消耗也更低,但前提是你有一个公网可访问的接收地址,并且要处理好SSL证书、域名备案、回调配置这些前置条件。

企业微信API的消息推送基本都走回调模式。你需要在应用管理后台配置“接收消息服务器URL”,填上Token和EncodingAESKey。SDK启动后会起一个HTTP服务,自动完成验签和解密,把消息对象递给你。这个过程看似绕,但它是企业级应用的标准做法,稳定性和实时性都有保障。

2.2 高频核心对象:消息、会话、联系人、机器人

用SDK做微信机器人,每天打交道的就是几个核心对象。理解它们的属性和关系,功能设计和代码组织都会顺手很多。

消息对象是数据流转的中心。它至少包含发送者ID、会话ID、消息类型(文本、图片、语音、视频、文件、卡片等)、时间戳和内容。有的SDK还会带上消息ID,这个字段对做去重非常关键,后面我会详细讲。

会话对象代表一个聊天窗口,可能是单聊,也可能是群聊。群聊场景下,会话对象一般还会包含群成员列表、群名称、群主ID等信息。联系人对象则对应一个微信用户或群成员,包含昵称、头像、备注名、用户ID等。

我在设计机器人时,习惯把消息对象理解成HTTP请求里的request。收到一条消息,就等于收到一个请求,机器人要解析它,决定是否处理,执行对应动作,最后返回结果。只不过聊天请求没有严格的“请求-响应”约束,需要你自己设计状态和容错。

2.3 Token与登录态是怎么回事

不管是官方API还是个人微信框架,Token和登录态都是绕不开的概念。很多人第一次看到这几个词就头疼,我用一个类比来解释。

Token就像一张临时进门卡。你拿着这张卡去调接口,接口才知道你有操作权限。卡是有有效期的,过期了就要去换新的。企业微信API的access_token有效期一般是两小时,SDK通常会做自动缓存和刷新。你在业务代码里不要手动去获取新token再塞回给SDK,很容易和SDK内部的缓存机制打架,导致偶发的权限错误。正确做法是只配置一次凭证,剩下的刷新逻辑交给SDK。

个人微信方向的开源框架则更多依赖登录态。一般通过扫码登录,将session信息保存下来,之后每次调用都靠这个session充当“长期通行证”。登录态非常脆弱,手机端微信升级、网络环境变化、异地登录、风控触发,都可能让它瞬间失效。所以做这方向的项目,一定要在设计之初就把“登录态失效之后的恢复流程”想清楚,而不是等掉线了再临时抱佛脚。

2.4 事件分发和消息类型支持

成熟的SDK内部不会只提供一个“收到消息”的入口,而是会把事件分门别类。常见的事件类型有文本消息事件、图片消息事件、语音消息事件、加入群聊事件、退群事件、好友申请事件、消息已读事件等。

用事件驱动的方式来组织代码,清晰度会高很多。你只需要为关心的事件注册处理函数,不关心的事件直接忽略。我见过有些新手喜欢在一个回调里写一堆if else,把所有逻辑堆在一起,表面上看着快,后面一加功能就乱成一团。

建议一开始就按事件类型拆文件。文本消息归文本消息处理,群成员变更归群管理模块处理,定时任务单独放一个调度目录。这样SDK升级、加功能、修bug,都有明确的位置可改,不会牵一发而动全身。

3. 实操:从零跑通第一个微信群聊机器人

3.1 环境准备和语言选型

先说说语言的问题。Python生态大、案例多,是很多人的第一选择;Node.js适合想跟前端后端一套技术栈走通的场景;Go则在性能和并发上有优势。我的建议是,用你熟悉的技术栈就好,不用为了追赶潮流换语言。如果完全没有偏好,从Python开始最省力。

以Python为例,环境上准备好Python 3.8以上版本、虚拟环境和对应SDK包。安装SDK这一步特别容易踩版本坑,一定要确认你安装的版本和你用的微信体系匹配。老框架往往只支持特定版本的微信客户端,版本不匹配轻则提示登录失败,重则收不到任何消息。

如果你走的是企业微信官方API路线,推荐先通读一遍官方文档,把应用ID、应用Secret、Token、EncodingAESKey这几个概念理解清楚。这些东西分别对应你应用的身份标识、权限凭证、签名校验密钥和消息加密密钥,缺一个SDK都起不来。

3.2 最小可运行的机器人:监听消息、自动回复

跑通最小闭环是建立信心的关键,别一上来就设计宏大架构。我每次在新的SDK上起步,都先做一个最小可运行的机器人,验证消息能收到、能回复,再往里面加东西。

伪代码层面的实现逻辑是固定的。第一步,初始化客户端并传入凭证。

from wechat_robot_sdk import Client client = Client( app_id="你的应用ID", app_secret="你的应用Secret", token="回调Token", encoding_aes_key="消息加解密密钥", )

第二步,注册文本消息处理函数。

@client.on_text_message def handle_text(msg): content = msg.content.strip() if content == "你好": msg.reply("你好,我是机器人,有什么可以帮你?")

第三步,启动服务。

if __name__ == "__main__": client.run()

这段代码的核心逻辑就三件事:SDK负责接收微信侧推送过来的消息并解密,消息对象进入你注册的处理函数,你的函数决定回复什么内容,SDK再把回复发出去。整个链路通了,后面的功能都在这个框架里加。

3.3 常见功能扩展:关键词回复、群管理、定时提醒

最小闭环跑通之后,就可以开始做实际要用的功能了。我自己按使用频率总结过一套微信群聊机器人常用功能清单,按优先级排列大概是下面这样。

关键词自动回复排第一。维护一个关键词映射表,可以是字典,也可以放数据库。收到文本消息后先做关键词匹配,命中则返回对应内容。这里有个细节:关键词匹配要处理前缀、后缀、模糊匹配,至少支持一种,否则用户发“天气北京”和“北京天气”会得到完全不同的结果。

定时提醒排第二。像每天早上推送工作日报、每周固定时间提醒交周报、整点发布天气播报,这类任务用任务调度库就能解决。任务触发时调用SDK的发送接口,把消息推送到指定会话或对象。

群管理功能排第三。入群欢迎语、@全体成员、敏感词提醒、定期清人,都属于群管理范畴。这类功能最要注意权限设计和误操作兜底,尤其是移除群成员这类不可逆操作,一定要加操作日志和二次确认机制,避免机器人一时误判造成不可挽回的体验事故。

3.4 多账号和消息队列的实际场景

如果你的使用场景涉及多个微信账号或多个机器人同时在线,那就不能只写单进程的玩具代码了。一个进程只跑一个机器人是最稳的部署方式,多个机器人之间通过消息队列解耦。

举个例子,主业务服务收到一条业务通知,它不直接调用机器人SDK发消息,而是把“要发什么内容、发给谁、什么时间发”写进队列,由独立的推送服务去消费队列内容,再通过各自账号的SDK发出去。这样做的好处是,哪怕某个账号掉线或者被限制,推送任务都在队列里排队,不会丢,恢复后还能继续发。

多账号场景下,账号配置也要独立管理。我习惯把每个机器人的凭证信息单独存一份配置,启动时按配置加载。千万不要把多个账号的凭证混在一个全局变量里,不然账号一多,微信机器人SDK的状态互相串了,排查起来极其痛苦。

4. 常见问题与排查技巧实录

4.1 掉线、登录态失效怎么办

个人微信方向的开源框架,掉线是最常见的问题。最典型的症状是机器人跑着跑着突然没反应,打开日志一看,写着登录过期或会话失效。

排查流程一般分三步。第一步,检查日志里是否出现登录过期、session invalid、auth failed之类的关键词,有的话基本确认是登录态失效,需要重新扫码登录。第二步,检查网络和手机端,如果你的微信客户端刚刚升过级,而框架没有同步适配,那掉线几乎是必然的。第三步,确认框架版本,去项目仓库看看有没有更新补丁,这种问题只能通过更新框架解决。

更重要的是预防措施。我做的机器人项目里都加了健康检查,每隔几分钟检查一次连接状态,发现异常立即通过备用通道通知维护者。加上自动重连机制,掉线后能第一时间请求重新登录,把故障时间压缩到最小。

4.2 回调消息丢失、重复推送怎么办

企业微信API的回调模式下,消息重复推送是非常典型的问题。原因是微信服务器在推送消息后,会等待你返回特定的成功字符串,如果返回不及时或格式不对,它就认为投递失败,然后重试推送。结果就是你看到同一条消息被处理了多次。

解决办法分两层。第一层,在回调入口处第一时间返回成功,不要等业务逻辑处理完再返回。耗时的业务处理全部丢到异步任务队列里去,保证回调请求快速响应。第二层,在消息处理逻辑里做幂等处理,用消息ID或“发送者+时间戳”做唯一键,已经处理过的消息直接跳过。

乱码问题则一般出在编码不一致。SDK接口返回的可能是字节串,你要明确解码成UTF-8,尤其在拼接URL、生成文件名、存储数据库时最容易踩坑。我习惯在SDK外面再包一层适配器,统一处理编码转换,避免在业务代码里到处试。

4.3 SDK版本冲突和环境兼容性

Python项目里依赖冲突是最磨人的。尤其当SDK依赖了特定版本的requests或httpx,而你的项目里另一个库对这个包的版本要求正好相反,轻则警告,重则直接启动报错。

规避的办法是强烈的环境隔离。机器人服务最好独立成一个进程,用虚拟环境管理,不要和主业务代码混在一起。如果不得不放在同一进程,就把依赖版本锁定好,用requirements.txt或pyproject.toml统一管理。升级SDK之前先看changelog,确认没有破坏性变更再动。

还有一类问题是安装路径和环境变量造成的。比如系统提示找不到指定的SDK,但包里明明存在。这种通常是你运行程序的解释器和安装SDK的解释器不是同一个,检查IDE的Python解释器路径、虚拟环境是否激活,基本就能解决。

4.4 一张表看完高频问题

我在实践中把高频问题整理成了一张速查表,遇到问题先对照排查,效率高很多。

问题现象可能原因快速处理办法
机器人完全收不到消息回调地址不可访问、URL配置错误先用浏览器访问一次回调URL,检查能否返回预期内容
消息能收到但无法回复权限配置不足、Token过期检查账号权限,勾选对应消息权限,确认Token配置正确
偶尔重复收到同一条消息回调返回慢,触发平台重试回调入口同步返回成功,业务逻辑转异步执行
消息内容全是乱码编码格式不一致统一按UTF-8解码,包一层编码适配器
登录瞬间成功,运行几分钟后掉线客户端版本不兼容、风控触发检查微信客户端版本和框架兼容性,降低操作频率
启动报找不到对应模块虚拟环境和解释器不一致检查Python解释器路径,激活正确的虚拟环境
同一个关键词有时生效有时不生效缓存未刷新,或者配置存储有延迟检查缓存策略,必要时重启服务加载最新配置

5. 进阶:自己动手封装一个SDK

5.1 从调用者视角看SDK设计

很多朋友用久了SDK,会好奇一件事:如果项目里没有合适的现成SDK,能不能自己封装一个?答案是当然可以,而且做一遍之后,你对SDK原理的理解会提升一个层次。

自己封装SDK,第一步不是写代码,而是站在调用者的角度,想清楚这个SDK对外要暴露什么接口。一个好的SDK,接口一定是非常直观的,使用者不需要理解内部复杂的鉴权、加密、重试,只需要调用一个方法,比如send_text(user_id, content)就能发消息。

这种“面向使用体验”的设计思路,和写业务代码完全是两码事。你自己知道内部实现,但使用者不关心。你要做的是把不确定性全部内部化:消息发失败了要不要重试、Token过期了要不要自动换新、回调验签不通过怎么处理,这些都应该在SDK内部解决,而不是抛给调用者。

5.2 封装一个最小请求客户端

以企业微信API为例,最简单的一个SDK至少要包含三块能力:获取和缓存access_token、发送消息、接收验证回调。

第一步,封装凭证管理。access_token不是永久有效的,SDK内部要维护一个缓存的token,并在过期前自动刷新。最简单的实现是记录token的获取时间和有效期,每次请求前判断是否应该刷新。

class AccessTokenManager: def __init__(self, app_id, app_secret): self.app_id = app_id self.app_secret = app_secret self.token = None self.expire_at = 0 def get_token(self): if self.token and self.expire_at > time.time() + 60: return self.token resp = requests.get( "https://example.com/gettoken", params={"appid": self.app_id, "secret": self.app_secret}, ) data = resp.json() self.token = data["access_token"] self.expire_at = time.time() + data["expires_in"] return self.token

第二步,封装消息发送。所有消息类型统一走一个发送方法,内部根据参数组装不同的消息体。

class MessageSender: def __init__(self, token_manager): self.token_manager = token_manager def send_text(self, user_id, content): token = self.token_manager.get_token() return requests.post( "https://example.com/send", params={"access_token": token}, json={"touser": user_id, "msgtype": "text", "text": {"content": content}}, )

到这里,一个最精简的SDK骨架已经有了。调用者只需要创建发送器,然后直接调send_text,完全不需要关心token刷新细节。

5.3 回调验签与消息解密的设计要点

SDK里最容易被低估的是回调处理模块。回调验签和消息加解密,直接决定了这个SDK在实际场景中能不能稳定用。

验签的常规逻辑是,把平台推送过来时携带的时间戳、随机字符串、签名值拼接成一串,用你配置的Token做HMAC或SHA1计算,比对结果是否一致。一致才说明消息确实来自微信侧,不通过直接拒绝。

消息解密则是把推送过来的密文,用EncodingAESKey进行解密,再解析出真实消息内容。这里有个细节,解密后的消息可能包含随机字节串和消息长度,一定要按格式解析,不能图省事直接当成明文处理。

自己写这块的时候,务必参考协议规范的准确算法描述,不要自己发明拼接顺序和解码方式。我第一次写时小看了这部分,把解密后的字节流直接转字符串,结果消息内容前面多了一堆乱码,排查了很久才发现是格式解析问题。

6. 从能用到好用:架构和工程化建议

6.1 分层设计,把SDK封装成服务

跑到后期,很多人的机器人代码会变成一个大杂烩:业务逻辑、消息处理、数据存储、权限判断全挤在一起,改起需求来痛苦不堪。解决思路是分层,把关注点彻底分开。

我的做法是把项目拆成三层:最底层是SDK对接层,只负责和微信平台通信,不做任何业务判断;中间层是服务层,提供具体能力,比如发送文本、解析关键词、查询用户信息;最上层是业务逻辑层,处理消息路由、功能分发、状态管理。

分层的好处非常多。最直接的一个好处是,哪天你决定换SDK或升级主版本,只需要改最底层,上层业务代码几乎不用动。而且每一层都可以单独测试,出了问题定位范围会小很多。

6.2 去重、幂等和限流

聊天场景天然是异步且不可靠的,消息重复、乱序、超时都是常态。做机器人的时候,必须默认“同一事件可能被收到多次”为前提来设计系统。

消息去重是最基本的。我在消息入库的地方加唯一索引,用消息ID做去重键,重复消息直接丢弃。主动发消息的场景则需要做频率控制,限制单位时间内的发送数量,避免因为业务bug导致机器人疯狂刷屏,这种事故对品牌和用户体验的伤害是不可逆的。

限流策略更不能省。微信平台对单账号的主动消息频率通常有明显限制,一旦超过限制,轻则接口报错,重则功能被限制甚至账号被处罚。我的做法是给所有主动消息发送场景加一个排队器,控制发送速率,宁可让消息延迟几秒,也不能挑战频率上限。

6.3 日志、监控和报警

机器人是长驻服务,跑在服务器上,出问题不会像客户端那样弹窗告诉你,你得自己知道“它一切正常”和“它悄悄挂了”。这个能力只能靠日志和监控体系来保证。

日志方面,每个消息的进来、处理、回复,都要记录结构化日志,时间、消息ID、发送者、操作、结果必须齐全。这样排查问题的时候,翻日志就能知道完整链路,不用靠猜。

监控方面,至少要有进程存活监控、连接状态检查和异常告警。一旦机器人掉线或者服务异常,要通过备用渠道(比如企业微信通知、邮件、短信)第一时间通知维护者。我在项目里把监控设计成了独立模块,不依赖机器人主逻辑,即使主服务挂了,监控进程也能独立报警。

6.4 项目复盘清单

写到这里,最后分享一个我每次做完机器人项目都会过一遍的复盘清单,算是给整套经验做个收口。

  • 功能是否覆盖了实际需求?有没有哪些功能是拍脑袋加的,实际根本没人用?
  • 消息的去重和幂等是否在关键路径上生效?
  • 主动消息的频率是否有硬性限制?
  • 登录态掉线后,是否能在半小时内被发现并恢复?
  • 所有配置信息是否独立管理,有没有硬编码在代码里?
  • 关键业务路径有没有日志?日志能不能支撑问题回溯?
  • 多账号场景下,账号配置和状态是否互相隔离?

这七条问题,每一条背后都是我曾经踩过的坑。别看它们简单,全部落实到位,机器人项目的稳定性和可维护性会上一个台阶。


做了这么多机器人项目,我最大的体会是,微信机器人SDK本质上只是敲门砖,真正决定项目成败的,是对消息场景的理解、对异常情况的预判,以及对代码结构的持续管理。先把最小闭环跑起来,再慢慢迭代功能,每一步都会走得很扎实。最后提醒一句:技术选型前先想清楚用在什么业务场景,安全合规永远排在功能实现前面。希望这篇文章能给你一些参考,也祝你第一个微信机器人顺利上线。

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

户外救援管理系统设计与实践:SpringBoot与Flowable的工程落地

1. 户外救援业务到底长什么样:先把需求盘清楚很多人看到“户外救援管理系统”这个标题,第一反应是“这不就是个报平安App吗”,或者“把求救电话接到后台登记一下就行”。真做过这行才知道,救援管理和普通工单系统完全是两码事。救…

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

博客内容生成规范与安全准则说明

我无法根据当前输入生成符合要求的博文。原因如下:项目标题仅为单个英文单词"skills",无具体指向性,既非技术名词、工具名、方法论,也非明确项目或产品;项目正文为空;关键词为空;摘要…

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

主动引导式Skill:把视频制作流程变成可复制的标准流水线

前几天我发了一条帖子,核心是把视频制作流程封装成主动引导式 Skill 的过程截图放了出去。结果第二天早上醒来,后台粉丝涨了四千多,私信直接炸了。一半人问“这个 Skill 是不是某个插件,要怎么安装”,另一半人问“Skil…

作者头像 李华
网站建设 2026/9/9 9:51:20

PyQt5实战指南:从安装配置到前后端解耦设计

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

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

opencode:开源AI编码代理的安装、配置与实战指南

前阵子群里有人发了一张 Windows 终端截图,报错红通通的:"opencode : 无法将“opencode”项识别为 cmdlet、函数、脚本文件或可运行程序的名称"。底下还跟着一句 "error: unexpected server error. check server logs"。这几乎是每个…

作者头像 李华
网站建设 2026/9/9 9:47:52

技术博客写作:从明确项目标题开始,记录真实踩坑经验

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

作者头像 李华