发布-订阅模式实战:系统解耦、消息中间件与故障排查全解析

做后端开发的这几年,我遇到最多的设计问题之一就是系统之间怎么通信。早期项目里,A系统的数据一变,就得硬编码调用B系统、C系统的接口,改一个接口就要联动改好几个调用方。中间只要有一个系统升级,整个调用链就跟着紧张。后来真正把发布-订阅用起来,才算是把这个问题彻底理顺了。

发布-订阅的核心思想,是把消息的生产者和消费者彻底拆开:生产者只负责把消息发到某个“主题”上,不用关心谁会去读;消费者按需订阅自己关心的主题,自己去拉消息。双方互不知道对方的存在,自然也就谈不上硬编码依赖。这篇文章不打算讲教科书式的概念,而是从我实际踩坑、落地的角度出发,把发布-订阅的核心机制、代码实现、以及最常遇到的故障排查方法一次讲清楚。无论你是正在做系统拆分、微服务改造,还是只是想让自己模块之间的通信更清爽,这篇都值得看完。

1. 发布-订阅的底层逻辑与场景判断

1.1 从一次订单重构说起

假设有一个电商系统,用户下单成功后,系统至少要干这几件事:扣减库存、给用户发通知、给会员体系累积积分、把订单数据推给数据分析。

如果按点对点的写法,订单服务里会写类似这样的代码:

def create_order(order_data): # 保存订单 save_order(order_data) # 调库存服务 inventory_service.reduce_stock(order_data["items"]) # 调通知服务 notify_service.send_message(order_data["user"], "下单成功") # 调积分服务 points_service.add_points(order_data["user"], 100) # 调数据分析 analytics_service.push(order_data)

这种写法的致命问题,不是代码长,而是耦合。每一次新增下游,比如要增加一个订单同步到财务系统的需求,就得回来改订单服务的代码。而且下单是强主链路,任何一个下游系统响应慢或者超时,用户的订单操作就会被拖慢。库存服务一抖动,用户连下单都成功不了,这显然不合理。

用发布-订阅改造以后,订单服务只做一件事:在自己的事务里,把“订单已创建”这个事件发到一个订单主题上。库存、通知、积分、数据分析各自订阅这个主题,自己去拉消息。新增财务同步?财务系统自己订阅就行,订单服务一行代码都不用动。库存服务挂了,最多是消息在队列里等它回来,用户该下单下单,完全不受影响。

这就是发布-订阅最核心的价值:划清边界,削峰填谷,让上下游各自演进。真正用起来之后,你会发现架构里很多“说不清谁该调谁”的问题,最后都能归到“你发布事件,我订阅事件”这八个字上。

1.2 它和点对点队列、观察者模式到底差在哪

很多人把发布-订阅和消息队列、观察者模式混在一起,搞不清边界。其实它们的差别很实际:

观察者模式是最早的“发布订阅雏形”,但它基本限定在单机、进程内,Subject直接持有Observer引用,消息传递是同步的。比如前端框架里的事件绑定,就是典型的观察者模式。它解决的是代码层面的解耦,不解决服务之间的通信问题。

点对点队列(Point-to-Point)虽然也有消息中间件的参与,但一条消息从入队到出队,最终只会被一个消费者消费。典型场景是任务分发,比如一批短信发送任务,多个worker各自领任务,每个任务只执行一次。这种模式保证“竞争消费”,但不适合做广播通知。

发布-订阅的核心特征是广播:一个主题上的一条消息,可以被多个订阅方各自收到一份,互不影响。如果把主题比作一个公共频道,点对点队列是“一个任务给一个人干”,发布订阅是“一条新闻所有关注的人都看到”。

维度观察者模式点对点队列发布-订阅
通信范围进程内跨进程/跨机器跨进程/跨机器
消息去向绑定观察者一条消息一个消费者一条消息所有订阅方
通信方式同步为主异步异步
持久化能力无支持支持
适用场景UI事件绑定、状态流转任务分发、负载均衡广播通知、事件驱动

我自己的经验是,接手一个架构时先判断它要解决的到底是“任务分配”还是“事件广播”,这决定了你上消息中间件还是上事件总线。用错模式,后面维护成本会翻倍。

2. 核心机制拆解:主题、订阅关系与消息流转

2.1 四个基础角色

一次标准的发布订阅通信,包含四个角色:

生产者(Producer):负责发布消息的一方,只需要知道往哪个主题发,不感知下游是谁。

消费者(Consumer):负责订阅主题,消费消息的一方。多个消费者可以组成一个消费者组,组内竞争消费,组间广播。

主题(Topic):消息的逻辑分类,相当于一个消息容器。所有消息都发到对应的主题上,消费者只拉自己订阅的主题。

消息代理(Broker):承接消息存储、转发、分发的中间层。它承担“媒婆”的职责,把生产者和消费者隔离开。

我用一个生活类比来理解这四个角色:主题是电台的节目频道,生产者是主持人,只负责在频道里说话,消费者是收音机前的听众,选择自己喜欢的频道收听。中间负责信号发射和传输的,就是消息代理。主持人不知道有多少听众在听,听众也不关心主持人背后怎么生产内容。

在实际系统中,消费者不是直接“收到”消息的,而是主动去代理那里拉取(Pull)。这和很多人的直觉相反。大多数消息中间件采用的模式是消费者通过长轮询来拉消息,而不是代理推给消费者。拉模式的好处是消费者可以控制处理速度,按自己的能力消费,天然就对消费速度有调节作用。推模式最大的问题在于,消费者处理能力弱时,代理全速推送会导致消费端内存被打爆,处理速度跟不上就只能丢消息。

2.2 主题的粒度与订阅关系的设计

主题粒度设计得好不好,直接决定系统后续好不好维护。

我见过最糟糕的做法,是把所有业务事件全发到同一个主题上,消费者拉下来以后自己判断类型。这样做的后果是:一个消费者必须处理全站所有类型的事件,逻辑上越来越臃肿,而且不同团队之间会互相干扰。正确做法应该是按业务域划分主题,比如订单主题、支付主题、用户主题,一个主题里再通过事件类型(Event Type)区分具体的业务动作,比如OrderCreated、PaymentSucceeded。

另外,订阅关系一般分为两种:一种是广播订阅,同一主题下的所有消费者都能收到全量消息;另一种是分组订阅,多个消费者实例组成一个Group,一条消息只会被全局其中一个实例消费。很多消息中间件支持的实际上是分组订阅的模式,通过多个Group实现“广播”的效果。这个区别一定要弄清楚,否则你会在并发场景下看到很诡异的行为:明明起了两个消费者实例,结果只有一台在消费,另一台完全闲着,就是因为它们被分到了同一个Group里。

我遇到过这样一个问题:运营系统发一个公告,想广播给所有用户端,结果两个消费者实例同时消费这个主题,把公告内容一人一半分掉了,用户端有的收到有的没收到。排查到最后才发现是消费组的语义理解错了。所以设计订阅关系时,一定要先弄清楚“你是要大家各拿一份,还是要把任务分开各干一部分”。

2.3 可靠性机制:确认、重试与死信

发消息容易,难的是在故障条件下保证消息不丢、不重复、最终被处理。这里有三道闸门需要理解。

第一道是生产端确认。发送消息时,代理返回成功确认才算真正写入;如果没有确认,生产者需要决定是否重发。但重发会引入重复风险,这里就涉及“至少一次”还是“至多一次”的语义选择。

第二道是消费端确认。消费者拿到消息处理后,主动向代理发送ack确认,代理才会把这条消息标记为已消费。如果处理失败,消费者不回ack,消息会被重新投递。这个机制是“不丢消息”的基石,但代价是消费端必须做好幂等,否则重试就会变成重复处理。

第三道是死信队列。当一条消息重试了多次仍然失败,说明它可能是一条“问题消息”,不应该无限重试拖死后面的正常消息。通常的实践是把它丢进一个单独的死信主题,等人工排查后决定是修复重投还是丢弃。我处理过的线上事故里,有不少都是靠死信队列里的原始消息找回现场证据的。比如某条订单数据一直处理失败,如果没有死信机制,它就会反复被消费,然后在业务日志里留下大片大片的报错。

3. 动手实操:从代码到生产方案

3.1 先写一个轻量事件总线

如果系统还没到分布式规模,只是几个服务之间要做解耦,那么自己实现一个事件总线就够了。我曾经在模块间通信的初期这么干过,成本低,逻辑直观。

先展示核心实现,用Python写一个进程内的事件总线:

import threading import collections from typing import Callable, Dict, List class EventBus: def __init__(self): self._subscribers: Dict[str, List[Callable]] = {} self._lock = threading.Lock() def subscribe(self, event_type: str, handler: Callable): with self._lock: if event_type not in self._subscribers: self._subscribers[event_type] = [] self._subscribers[event_type].append(handler) def unsubscribe(self, event_type: str, handler: Callable): with self._lock: if event_type in self._subscribers: self._subscribers[event_type].remove(handler) def publish(self, event_type: str, data=None): handlers = [] with self._lock: handlers = list(self._subscribers.get(event_type, [])) for handler in handlers: try: handler(data) except Exception as e: # 实际项目里这里要接入统一异常上报 print(f"handler error: {e}")

这个实现有几个要点值得说明一下。第一,publish的时候先把订阅者列表拷贝出来再遍历,是为了避免在处理过程中有人动态取消订阅导致遍历出错。第二,每个handler的异常在内部捕获掉,是防止一个订阅者出问题把整个发布链路打断,实际项目里还应该引入统一的异常上报机制。

当然,这种进程内总线的局限也很明显:没有持久化、没有跨机器的能力、进程重启消息就丢了。所以它只能用在单体架构的内部模块解耦,一旦你要做服务级别的通信,就得换真正的消息中间件。

3.2 接入消息中间件的典型流程

生产环境我做过的落地方式,基本都遵循一套固定流程。

第一步,确定主题命名规范。主题名要能看出业务域,比如order-event、payment-event,不要用order、data这种含糊的名字。

第二步,确定消息体结构。建议统一用包含消息ID、事件类型、产生时间、业务数据的JSON结构,给未来排查留好线索。

第三步,确定消费方式。如果下游有多个系统要响应同一个事件,每个系统使用独立的消费者组;如果只是希望多个实例分摊压力,就共用一个消费者组。

第四步,配置ack和重试策略。消费失败时不应该无限重试,一般控制在3到5次,超过就进死信队列。

这里我用代码展示一个消费者端的基本姿势(伪代码,具体API看你用的中间件版本):

def handle_order_created(message): order_id = message.data['order_id'] try: process_order(order_id) message.ack() except RetriableError: message.requeue(delay=30) except Exception: message.reject(dead_letter=True)

这段逻辑非常实用。RetriableError代表这次失败是暂时性的,比如下游数据库连接抖动,可以延迟重试;其他异常说明可能消息本身有问题,直接进死信都比反复重试消耗资源强。我在多个项目里都用这套分类策略,线上消息处理的稳定性提升非常明显。

3.3 参数与配置的实战心得

配置消息中间件时,有几个参数是我每次都要反复检查和调优的:

批量拉取大小。消费者每次拿走多少条消息,不是越大越好。太大会导致单批处理时间变长,消息在内存里积压;太小又会造成网络往返过多。日常项目里,批量拉取大小定位在几十条比较平衡。

并发消费者数。一个主题下的消费者实例数量,决定了消费的并行度。注意消费者实例数乘以单实例的处理能力,要能覆盖业务高峰的数据量,最好留出20%到30%的余量。

消费超时与重试间隔。消费超时时间设置要大于业务处理的最长时间,否则会发生消息明明在处理,却被代理认为超时又重新投递,造成重复处理。我之前就栽过这个坑:一个批量任务处理要40秒,超时时间设置成了30秒,结果同一批数据每次都被投递了两三遍。

注意:消费超时时间必须结合业务最坏情况来设置,宁可大一点也不要因为超时触发大量重复投递。重复消息比慢消息更难排查。

另外,重试间隔也不能设置得太短。我见过有人把重试间隔设成3秒,结果下游服务故障持续了5分钟,期间所有消息都在疯狂重试,光重试流量就把下游又打垮了一轮。合理的做法是使用指数退避策略,比如第一次重试延迟10秒,第二次30秒,第三次1分钟,给下游恢复留出时间窗口。

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

4.1 消息丢失:查三个环节

消息丢失是发布订阅场景下最头疼的问题。排查的时候,我按“生产端、中间件、消费端”三个环节逐个排查。

生产端:确认这条消息发送时是否收到了确认响应。很多消息丢失的根源,是生产者在发送时没有确认发送成功,或者发送失败后没有重发机制。看生产日志里有没有报错信息,是最快的定位方式。

中间件:确认消息是否落盘,主题是否有过期时间。如果主题的保存策略设得太短,还没等消费者拉走,消息就可能被清掉。特别是存量很大的时候,主题的存储策略清理逻辑超乎寻常地快,需要仔细核对配置。

消费端:确认消费者是否有ack。如果消费者为了省事,拉下来消息直接处理,处理完又忘记ack,代理就会反复投递同一条消息,从日志上看像是“丢了”,其实是“卡住了”。消费日志里查最近有没有反复处理同一条消息,能快速判断是不是这个原因。

另外还有一个很容易被忽视的场景:消息被写进了错误的分区。很多消息中间件支持根据业务ID把消息哈希到固定分区,如果某些消息没有拿到分区键,就会全部堆到默认分区,造成消费乱序或者部分消息一直不被匹配的消费者处理。排查时需要确认消息的分区分布是否均匀。

4.2 重复消费:幂等是唯一的解药

只要用的是至少一次语义,重复消费就一定会发生。这不是bug,而是机制的一部分。消费者宕机、ack超时、网络抖动,都会导致同一条消息被投递多次。

应对重复消费的唯一方法,就是让处理逻辑幂等。常见的做法有几种:

给消息带上全局唯一ID,消费方根据这个ID查重表,处理过就不再处理。很多系统中都用一张消息去重表来实现这个目标。

利用业务天然的唯一键来做幂等,比如订单号、支付流水号,在数据库里建唯一索引,重复插入直接报冲突,catch住即可。

用状态机约束:一条数据只有在特定状态才能执行特定操作,状态更新使用条件更新,确保重复执行不会带来重复效果。

我在做财务对账系统时,经历过一次典型的重复消费事故:某个回调事件被重复投递,导致一笔账被记了两次。没有幂等设计,排查过程极其痛苦。那之后我把所有消费逻辑都默认当成“会重复发生”来设计,每次都是先查一次,再操作一次,宁多一次查询,不冒重复风险。

还有一个细节经验:幂等表本身要跟业务操作放在同一个事务里。如果先查重、再处理业务、最后写幂等记录,中间任意一步失败都会留下漏洞。正确的姿势是业务数据更新和幂等标记写入在同一次数据库事务里完成,这样才能保证一致性。

4.3 消息积压:先处理再深究

消息积压实操上,我用的是先恢复再排查的思路。

第一步,扩容消费者实例。消费者组里加实例,是最快的横向扩容手段。如果部署在容器里,直接在消费组里多起两三个实例就能立竿见影。前提是下游的数据库、接口等还扛得住并发增长,否则积压没解决,先把下游打垮。

第二步,检查消费逻辑有没有阻塞点。很多时候积压不是消费能力不够,而是消费逻辑里有一个慢调用卡住了,比如调了一个超时需要30秒的外部接口。这种时候要给外部调用加超时和熔断,别让一个慢依赖拖垮整条消费链路。

第三步,积累积压量特别大时,可以先暂停一部分业务无关的消息订阅,优先处理核心业务数据。等积压恢复正常,再把订阅拉起来。这种策略需要在业务侧和团队间达成一致,不是技术层面能单独决定的,但是救命的手段。

我印象最深的一次线上事故,就是业务做了一波大促活动,订单量是平时的几十倍,结果消费端数据库连接池被打满。当时先做了实例扩容,又紧急调整了消息批量拉取的策略,把一批拉取的消息数量降下来,让数据库有喘息时间,这才慢慢把积压消化掉。复盘时发现,真正的根因是消费逻辑里一个分组查询没有加索引,数据量一大就把数据库拖慢了。所以说,遇到积压不要只想到扩容,一条慢SQL带来的积压,扩容再多也没用。

关于积压还有一个被忽略的点:消费者实例数并不能无限增加。消费者组里实例多了之后,每个实例分到的分区数量会减少。当实例数超过分区数时,多出来的实例就完全空闲,不会帮助消费。所以在扩容前先看清楚自己的主题有几个分区,再决定起多少个消费者实例。

写这篇文章的时候,我回想起自己从单模块调用、到写事件总线、再到底层接消息中间件这一路踩坑过来的过程。发布订阅这套模式,技术实现上不算难,难的是对业务边界的理解和事件粒度的设计。我个人的经验是,开始不用追求一步到位,先从最笨的消息广播做起,把核心链路跑通,再逐步引入分组、死信、幂等这些机制,系统会一步步变得像样。最后再分享一个小技巧,所有线上关于消息的排查,都要第一时间把原始消息内容完整留档,这是你事后还原现场最重要的依据,也是我在接手几个维护了多年的旧系统之后反复确认过的教训。

相关新闻

C#纯托管读取PCD点云:格式解析、避坑指南与二次开发

C#纯托管读取PCD点云:格式解析、避坑指南与二次开发

简介:一套C#读取点云数据的示例工程,面向C#初级开发者与三维视觉学习者,演示如何从文本文件中逐行读取点云数据,并将大量离散的三维坐标点解析为Point3D对象,适用于3D重建、机器视觉、自动驾驶等场景的入门实践。压缩包…

2026/10/21 4:32:49 阅读更多 →
项目标题‘rea’解析:为何模糊缩写无法支撑技术内容创作

项目标题‘rea’解析:为何模糊缩写无法支撑技术内容创作

项目标题是“rea”,但提供的输入内容中,项目正文为空、关键词未列出、摘要描述缺失,且网络搜索部分为纯空行(),无任何实际信息支撑。作为一名从业十多年、每天要拆解几十个真实项目标题的资深博主&#xff…

2026/10/21 4:31:49 阅读更多 →
语雀笔记导出神器,有查看权限即可一键导出!(支持1:1还原,PDF和Word导出)

语雀笔记导出神器,有查看权限即可一键导出!(支持1:1还原,PDF和Word导出)

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

2026/10/21 4:31:49 阅读更多 →

最新新闻

手写JSON解析器:从词法分析到语法树的编译原理实战

手写JSON解析器:从词法分析到语法树的编译原理实战

1. 从一段JSON文本到内存对象:解析器到底在做什么很多人第一次接触编译原理,都是从"写一个JSON解析器"开始的。这个选择很聪明——JSON的语法足够简单,简单到一页纸就能写完文法;但它又足够完整,完整到能覆盖…

2026/10/21 5:11:04 阅读更多 →
Java俄罗斯方块源码解析:游戏主循环、碰撞检测与消行实现

Java俄罗斯方块源码解析:游戏主循环、碰撞检测与消行实现

简介:这是一份面向Java初学者与游戏编程爱好者的俄罗斯方块完整源码,以Java语言实现,适合用来理解经典游戏的核心逻辑与面向对象设计。源码围绕游戏主面板、方块形状、流程控制器与程序入口等关键类展开,涵盖游戏循环、方块生成与…

2026/10/21 5:11:04 阅读更多 →
NLP与无人船:从自然语言指令到自主靠泊的完整技术链路

NLP与无人船:从自然语言指令到自主靠泊的完整技术链路

去年我带着组里的几位同学把一个挺有意思的项目从零做到了仿真验证——给一艘无人船加一套自然语言操控链路,让操作员可以直接说“慢速驶向测试场北侧码头,左舷靠泊”或者“绕开右前方漂浮物,停到A区泊位”,船载系统自己去完成意图…

2026/10/21 5:11:04 阅读更多 →
鸿蒙+Flutter跨平台实战:桌面图标焕新方案全解析

鸿蒙+Flutter跨平台实战:桌面图标焕新方案全解析

桌面图标焕新大师这个名字,听起来像某个手机美化App的宣传语,但它本质上是一个很典型的跨平台实战项目:把视觉设计、资源管理、系统桌面机制和Flutter渲染能力串在一起,最终在鸿蒙设备上完成“一键切换桌面图标风格”的完整闭环。…

2026/10/21 5:11:04 阅读更多 →
从ReAct到龙虾架构:AI Agent编排演进与工程实践

从ReAct到龙虾架构:AI Agent编排演进与工程实践

这两年AI Agent架构的文章我看了不下几十篇,名字一个比一个唬人:ReAct、Plan-and-Execute、Reflexion、多Agent系统、图式编排、龙虾架构……但真能把“从A到B是怎么一步步走出来”讲明白的,确实不多。尤其对小程序员来说,最痛苦的…

2026/10/21 5:11:04 阅读更多 →
AI原生知识库重构:从传统检索到语义网络的实践解析

AI原生知识库重构:从传统检索到语义网络的实践解析

做了三年传统知识库,我一直在琢磨一个问题:资料越存越多,知识却越来越难找,团队知道的东西永远比系统记录的多。传统知识库的把式——目录分类、关键词搜索、标签管理——本质上是在用二维平面的方式梳理立体复杂的真实知识&#…

2026/10/21 5:10:03 阅读更多 →

日新闻

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

这段时间硬件圈和AI圈都在传一个事:AMD锐龙AI Max PRO 400系列把最高192GB统一内存搬进了PC形态的产品里,按宣传口径,本地跑320B参数的大家伙不再是纸面概念。我第一反应倒不是“内存真大”,而是这话背后的含金量——它把过去只能…

2026/10/21 0:00:11 阅读更多 →
AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

1. 从“封号重开”这个行为说起:用户到底在追求什么“封号重开”这四个字,放在任何产品语境里都带着一股决绝的味道。一个账号从零开始积累使用记录、调教偏好、沉淀工作流,背后是大量的时间和精力成本。正常情况下,没有人愿意主动…

2026/10/21 0:00:11 阅读更多 →
ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

1. 为什么在ROS里硬刚YOLOv3不是“炫技”,而是工程落地的必经之路我第一次在某高校机器人实验室看到那台搭载Jetson TX2的移动底盘原地打转、反复识别错垃圾桶时,就意识到:单纯跑通darknet_ros demo和真正让YOLOv3在ROS里“干活”&#xff0c…

2026/10/21 0:00:11 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/20 20:43:59 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/21 4:27:17 阅读更多 →

月新闻

我发现了一个新思路:用 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/20 10:02:08 阅读更多 →
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/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练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/20 9:50:45 阅读更多 →