100行代码手写Hermes式智能体框架并接入飞书机器人

1. 为什么我要自己动手写一个智能体框架

1.1 从“玩具”到“工具”的转折点

市面上开源的智能体框架我几乎都翻过一遍。LangChain 生态庞大,但依赖链太长,一个简单的对话循环要引入几十个包;AutoGen 的多智能体协作很惊艳,可一旦要接入企业内部系统,配置复杂度直线上升。我真正想要的东西其实很朴素:一个能跑在单进程里、依赖极少、逻辑透明、方便接入飞书这类协作平台的轻量级智能体内核。

Hermes 式智能体的核心思路给了我很大启发——它不追求大而全,而是把“感知-决策-执行-反馈”这个循环做得极其干净。所谓 Hermes 式,我理解就是像一个信使一样,在用户意图和实际工具之间做高效、可靠的消息传递与任务编排。它不需要庞大的编排引擎,核心就是一个循环加上一套工具注册机制。

这个项目解决的核心问题是:让一个开发者能在半小时内理解全部代码,并且在一小时内把它接入自己的飞书机器人。适合谁参考?适合那些被重型框架折磨过、想回归本质的中级开发者,也适合刚接触智能体概念、想通过读源码来理解原理的初学者。100 行核心代码不是噱头,是我反复删减后的结果——每一行都承担明确职责,没有一行是为了“看起来专业”而存在的。

1.2 飞书作为接入端的独特价值

选择飞书作为首个接入平台,原因很实际。飞书的开放平台文档清晰,机器人创建流程简单,Webhook 和事件订阅两种模式都支持得很好。更重要的是,飞书的消息卡片交互能力很强,智能体返回的结构化内容可以直接渲染成可点击的卡片,用户体验比纯文本好一个档次。

从技术角度看,飞书的事件回调机制天然适合智能体的异步处理模式。用户发一条消息,飞书服务器推送到我的回调地址,智能体处理后通过 API 回复。这个链路里,我的核心代码只需要关心“收到消息→处理→返回结果”这三步,平台相关的鉴权、签名校验、消息格式转换都可以封装在适配层里。这样一来,核心的智能体逻辑和平台接入逻辑就彻底解耦了,以后想接入其他协作平台,只需要换一个适配器。

注意:飞书机器人有两种消息接收模式——长连接和 Webhook。长连接适合本地开发调试,不需要公网地址;Webhook 适合生产部署,但需要处理签名校验和重试机制。我建议开发阶段用长连接,上线前再切 Webhook。

2. 核心架构设计与关键取舍

2.1 为什么是 100 行——删减的艺术

很多人看到“100 行”第一反应是“肯定功能很弱”。实际上,这 100 行只包含最核心的智能体循环,不包含飞书适配层、不包含具体工具实现、不包含配置文件。如果把所有东西都算上,大概 400 行左右。但核心循环确实只有 100 行,因为我把所有能剥离的东西都剥离了。

具体来说,我删掉了这些东西:内置的对话历史管理(改用外部字典简单存储)、复杂的提示词模板引擎(直接用 f-string)、多模型路由(只保留一个 OpenAI 兼容接口)、向量数据库检索(需要时再手动加)。每删掉一个东西,我都会问自己:没有它,智能体还能完成基本任务吗?答案是能,那就删。

这种做法的好处是调试极其方便。当智能体行为不符合预期时,我不需要在一堆抽象层里找问题,直接看那 100 行循环就能定位。坏处是扩展性需要自己动手,但我觉得这反而是优点——扩展什么、怎么扩展,完全由业务需求决定,而不是被框架的设计绑架。

2.2 核心循环的四个阶段

智能体的核心循环我拆成四个阶段,每个阶段对应一个函数:

  • 感知阶段:接收用户输入,做最基本的清洗和格式化。这里不做复杂的 NLP 处理,因为大模型本身就能理解自然语言,过度预处理反而可能丢失信息。
  • 决策阶段:把用户输入、工具描述、历史上下文拼成一个提示词,发给大模型,让模型决定是直接回答还是调用工具。这里的关键是工具描述的格式要足够清晰,模型才能准确选择。
  • 执行阶段:如果模型返回了工具调用请求,就解析参数、执行对应函数、拿到结果。执行时要加超时和异常捕获,避免一个工具卡死整个循环。
  • 反馈阶段:把工具执行结果再喂给模型,让模型生成最终的自然语言回复。这个阶段可能需要多轮,直到模型不再请求调用工具为止。

这四个阶段用一个 while 循环串起来,设置最大迭代次数防止死循环。实测下来,大部分任务 2-3 轮就能完成,复杂任务也很少超过 5 轮。

2.3 工具注册机制的设计

工具注册我用了最朴素的字典结构,而不是装饰器或者类继承。原因很简单:字典一目了然,新人接手时看一眼就知道有哪些工具、每个工具的参数是什么。

每个工具用一个大模型能理解的描述字符串来定义,包括工具名称、功能说明、参数列表和每个参数的类型与含义。这个描述直接拼进提示词里,模型根据描述来决定调用哪个工具。我试过用 JSON Schema 格式,效果也不错,但字符串描述更灵活,可以加入自然语言的解释。

工具函数的签名我统一成def tool_name(params: dict) -> str,参数从字典里取,返回值统一是字符串。这样做的好处是执行器不需要关心每个工具的具体签名,统一调用即可。坏处是类型检查弱,但智能体场景下,参数正确性由模型保证,人工检查反而多余。

提示:工具描述的质量直接决定模型调用的准确率。我踩过的坑是描述太简短,模型经常选错工具。后来我把每个工具的描述写成一段完整的话,包括“什么时候该用”和“什么时候不该用”,准确率明显提升。

3. 飞书接入的完整实操流程

3.1 创建飞书机器人应用

第一步是在飞书开放平台创建应用。进入开发者后台,选择“创建企业自建应用”,填写应用名称和描述。创建完成后,在“凭证与基础信息”页面拿到 App ID 和 App Secret,这两个后面要用。

接下来配置权限。智能体需要接收消息和发送消息,所以至少要开通im:message、im:message:send_as_bot这两个权限。如果要用消息卡片,还需要im:message:card相关权限。权限开通后需要发布版本才能生效,企业内部应用一般审核很快。

然后配置事件订阅。在“事件订阅”页面,选择“使用长连接接收事件”,这样本地开发时不需要公网地址。添加im.message.receive_v1事件,当用户给机器人发消息时,飞书会通过长连接推送过来。

最后在“机器人”页面启用机器人功能,设置机器人名称和头像。到这里,飞书侧的配置就完成了。

3.2 长连接模式下的消息收发

长连接模式我用飞书官方提供的 SDK 来实现。核心逻辑是:SDK 建立连接后,注册一个消息处理函数,每当收到消息事件,就调用智能体的处理入口,拿到回复后再通过 SDK 发送回去。

这里有个细节需要注意:飞书的消息事件里,消息内容可能是纯文本、富文本或者图片。我的处理方式是只取纯文本部分,其他类型统一回复“暂不支持该消息类型”。这样做虽然功能有限,但保证了核心链路的稳定性。后续要扩展富文本处理,只需要在适配层加解析逻辑,不影响智能体内核。

发送消息时,我用了回复消息的接口,而不是主动发消息。回复消息会自动带上引用关系,用户在飞书里能看到上下文,体验更好。消息格式我优先用交互卡片,把智能体的回复包装成卡片内容,支持 Markdown 渲染。如果卡片发送失败,降级为纯文本发送。

3.3 消息去重与并发处理

飞书的事件推送可能会重复,比如网络抖动导致飞书没收到确认,就会重推。如果不做去重,用户会收到重复回复。我的做法是用消息 ID 做去重,维护一个最近处理过的消息 ID 集合,处理前先检查是否已处理过。

并发处理方面,长连接模式下 SDK 是单线程回调,如果智能体处理时间较长,会阻塞后续消息。我的解决方案是把消息处理放到线程池里,回调函数只负责把消息丢进队列就返回。线程池大小设为 4,实测足够应对个人使用场景。如果多人同时使用,可以适当调大,但要注意大模型 API 的速率限制。

注意:线程池里的任务如果抛异常,一定要捕获并记录日志,否则异常会静默丢失,排查起来很痛苦。我在每个任务外层都包了 try-except,把异常信息写到日志文件里。

4. 智能体内核的逐行拆解

4.1 提示词构造的关键细节

提示词是智能体的“大脑说明书”,构造得好不好直接决定效果。我的提示词分三部分:系统角色描述、工具列表、对话历史。

系统角色描述我写得很具体,不是简单的“你是一个助手”,而是“你是一个任务型智能体,可以调用工具来帮助用户完成任务。当用户的问题需要实时信息或外部操作时,优先调用工具;当用户只是闲聊或问常识时,直接回答”。这样模型能清楚知道自己的定位。

工具列表的格式我反复调整过。最终版本是每个工具一段,包含工具名、功能描述、参数说明。参数说明里,我明确写了每个参数的类型和是否必填。比如“参数:city(字符串,必填),表示城市名称”。模型对“必填”这个词很敏感,写了之后漏参数的情况明显减少。

对话历史我只保留最近 10 轮,太早的历史对当前决策帮助不大,反而增加 token 消耗。历史格式是“用户:xxx”和“助手:xxx”交替,工具调用结果也作为助手消息的一部分记录进去。

4.2 工具调用的解析与执行

模型返回的工具调用请求,格式我约定为一段特定标记包裹的 JSON。比如模型输出[TOOL_CALL]{"name": "get_weather", "params": {"city": "北京"}}[/TOOL_CALL],我用正则提取出 JSON 部分,解析后执行对应工具。

这里有个坑:模型有时会在 JSON 前后加额外的解释文字,比如“好的,我来调用工具:[TOOL_CALL]...”。我的正则做了兼容,只要标记存在就能提取出来。如果解析失败,就把模型的原始输出当作最终回复返回,避免卡死。

执行工具时,我加了 10 秒超时。超时后返回“工具执行超时”的错误信息给模型,让模型决定是重试还是换一种方式回答。异常捕获也是必须的,任何工具抛异常,都捕获后把异常信息返回给模型,模型通常会根据错误信息调整策略。

4.3 多轮循环的终止条件

循环终止有两个条件:一是模型不再请求调用工具,直接返回了文本回复;二是达到最大迭代次数(我设为 8)。达到最大次数时,我会把当前所有信息汇总,让模型生成一个总结性回复,而不是直接报错。

实测中,大部分任务 2-3 轮就结束了。需要多轮的一般是复杂查询,比如“帮我查一下明天北京的天气,如果下雨就提醒我带伞,顺便看看有没有合适的室内活动”。这个任务需要先查天气,再根据天气结果决定是否查活动,至少 3 轮。

最大迭代次数设为 8 是经验值。设太小,复杂任务完不成;设太大,万一模型陷入循环会浪费大量 token。8 轮足够覆盖绝大多数场景,同时 token 消耗可控。

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

5.1 模型不调用工具怎么办

这是最常见的问题。模型直接用自己的知识回答,而不是调用工具。原因通常有三个:工具描述不够清晰、系统提示词没有强调工具优先级、模型本身能力不足。

排查步骤:先看工具描述,是不是太简短或者有歧义。我遇到过“查询天气”这个描述,模型以为是自己知道天气,改成“调用外部天气 API 获取指定城市的实时天气”后就正常了。然后看系统提示词,有没有明确说“需要实时信息时优先调用工具”。最后换一个能力更强的模型试试,有些小模型对工具调用的支持确实弱。

5.2 工具参数解析失败

模型返回的 JSON 格式不对,比如用了单引号、少了括号、参数名拼错。我的处理方式是:先尝试标准 JSON 解析,失败后用正则做宽松提取,再失败就把原始输出返回给模型,让模型重新生成。

预防措施是在提示词里明确 JSON 格式要求,并给一个示例。示例的作用很大,模型会模仿示例的格式。我给的示例是{"name": "工具名", "params": {"参数名": "参数值"}},简单清晰。

5.3 飞书消息发送失败

常见原因有:token 过期、权限不足、消息格式错误。token 过期最常见,飞书的 tenant_access_token 有效期 2 小时,需要定时刷新。我的做法是每次发送前检查 token 是否过期,过期就重新获取。

权限不足一般是权限没开通或者版本没发布。消息格式错误通常是卡片 JSON 写错了,飞书对卡片格式要求较严,建议先用飞书的卡片搭建工具验证格式。

问题现象可能原因排查方法解决方案
模型不调用工具描述不清/提示词弱检查工具描述和系统提示细化描述,强调工具优先级
参数解析失败JSON 格式错误打印模型原始输出加示例,宽松解析,失败重试
消息重复回复事件重推检查消息 ID 去重逻辑维护已处理消息 ID 集合
token 过期未定时刷新检查 token 获取时间每次发送前检查并刷新
工具执行超时外部 API 慢加日志记录耗时设超时,返回错误让模型决策

5.4 性能优化的几个实用技巧

第一,工具描述和系统提示词可以缓存,不需要每次请求都重新构造。我用了一个简单的字典缓存,key 是工具集合的哈希,value 是拼好的提示词。

第二,对话历史不要无限增长。我设了 10 轮上限,超出后丢弃最早的。如果发现模型经常需要更早的上下文,可以考虑做摘要压缩,但那是另一个话题了。

第三,大模型 API 调用是瓶颈,能并行就并行。比如多个独立工具调用,可以同时发起。但要注意速率限制,我一般控制在并发 3 以内。

第四,日志要详细但不要冗余。我记录每次请求的输入、输出、耗时、token 消耗,但不记录完整的提示词(太长)。排查问题时,这些日志足够定位大部分问题。

6. 扩展方向与个人体会

6.1 从单智能体到多智能体协作

当前是单智能体架构,所有工具注册在一个列表里。如果工具数量超过 20 个,模型选择工具的准确率会下降。这时候可以考虑拆分:按领域分成多个子智能体,每个子智能体只负责一类工具,主智能体负责路由。

比如一个“办公助手”主智能体,下面分“日程管理”“文档处理”“消息通知”三个子智能体。用户请求先到主智能体,主智能体判断属于哪个领域,转发给对应子智能体。子智能体处理完把结果返回主智能体,主智能体再回复用户。

这种架构的复杂度会上升,但工具选择的准确率会明显提升。我的建议是:工具少于 15 个时用单智能体,超过 20 个再考虑拆分。

6.2 记忆系统的轻量级实现

当前对话历史只存在内存里,重启就丢失。要持久化的话,最简单的方案是用 SQLite 存对话记录,按用户 ID 和会话 ID 索引。每次处理前从数据库加载最近 10 轮,处理后写入新记录。

更进一步的记忆系统可以加入“长期记忆”:把重要的用户偏好、常用信息提取出来单独存储,每次对话时作为背景信息注入提示词。比如用户说过“我住在北京”,这个信息就存进长期记忆,以后查天气时默认城市就是北京。

长期记忆的提取可以用大模型来做,让模型判断哪些信息值得记住。但要注意隐私,敏感信息不要存。

6.3 我在这个项目里踩过的坑

最大的坑是低估了飞书事件推送的重复率。上线第一天就发现用户收到重复回复,排查后发现是飞书在没收到确认时会重推。加了消息 ID 去重后解决。

第二个坑是工具执行没有超时控制。有个工具调用的外部 API 挂了,请求一直不返回,整个智能体卡死。后来加了 10 秒超时,超时后返回错误信息,模型会尝试其他方式回答。

第三个坑是提示词太长导致 token 消耗过大。最初我把所有工具描述和历史都拼进去,一次请求 4000 多 token。后来精简了工具描述,历史限制 10 轮,降到 1500 token 左右,成本降了一半多。

这个项目后续还可以这样扩展:接入更多协作平台,把适配层做成插件式;加入定时任务能力,让智能体可以主动推送消息;支持图片和文件处理,扩展多模态能力。但核心的 100 行循环我打算保持稳定,所有扩展都通过外围模块实现,确保内核始终清晰可控。

相关新闻

技能管理系统Skills:用数据化技术画像精准定位自身能力短板

技能管理系统Skills:用数据化技术画像精准定位自身能力短板

从面试被问到“你还会什么”那一刻,我才意识到自己的技能清单有多失真。简历上写着“熟悉微服务”,可真聊起服务拆分和容错设计,脑子里能拿出来的只有几个名词;写了三年业务代码,却说不清自己到底擅长什么、短板在哪里…

2026/10/20 6:48:42 阅读更多 →
国产大模型崛起:榜单前20占6席,技术底座与落地实践全解析

国产大模型崛起:榜单前20占6席,技术底座与落地实践全解析

1. 榜单背后的真实格局:国产大模型凭什么挤进前二十全球AI模型榜单前20名里国产模型占了6个,这个数字放在三年前几乎没人敢想。我翻了一圈榜单的评测维度,发现这次上榜的国产模型覆盖了文本理解、代码生成、多模态推理、长上下文处理几个主要…

2026/10/20 6:47:42 阅读更多 →
PPO与GRPO深度对比:从Critic机制到组内相对优势的工程实践

PPO与GRPO深度对比:从Critic机制到组内相对优势的工程实践

1. 从一道面试题说起:PPO和GRPO到底差在哪面试里聊强化学习,PPO几乎是绕不开的。但这两年如果你在简历上写了“做过RLHF”“训过大模型对齐”,面试官很可能追问一句:GRPO了解吗?它把Critic砍了,那优势函数从…

2026/10/20 6:47:42 阅读更多 →

最新新闻

Abaqus隧道空洞模拟:地表沉降有限元分析建模方法与工程应用

Abaqus隧道空洞模拟:地表沉降有限元分析建模方法与工程应用

从实际项目出发,我接到过一个很有意思的模拟任务:用Abaqus分析隧道施工中形成的空洞对地表沉降的影响规律。这类问题在岩土工程里特别典型——城市地下管线破损、盾构注浆不密实、矿山采空区塌落,都会在地层内部留下空洞,而地表沉…

2026/10/20 7:38:07 阅读更多 →
轻量级Coding Agent设计:如何通过架构减法节省58.7% Token

轻量级Coding Agent设计:如何通过架构减法节省58.7% Token

1. 项目概述:一个轻量级 Coding Agent 的诞生逻辑“我做了一个 Coding Agent,结果比 Claude Code 少用了 58.7% Token”——这句话刚在技术社区刷屏时,我第一反应不是惊讶,而是立刻点开原文代码仓库,翻看它的 prompt 结…

2026/10/20 7:38:07 阅读更多 →
用APIKit与Param Miner挖掘隐藏API端点:Burp Suite主动探测实战指南

用APIKit与Param Miner挖掘隐藏API端点:Burp Suite主动探测实战指南

先问个扎心的问题:做接口测试或者安全评估的时候,你是不是还停留在“开Burp、挂代理、点开HTTP History看请求”这个阶段?如果是,那我要说一句可能不太中听的话——你手里的Burp Suite,至少有一半的潜力被你浪费了。经…

2026/10/20 7:38:07 阅读更多 →
MIL-C-5015军规连接器振动加速度传感器:选型安装与排查

MIL-C-5015军规连接器振动加速度传感器:选型安装与排查

干设备状态监测好些年,有一类传感器是小众但特别关键的——带军规连接器的振动加速度传感器。光看名字“MIL-C-5015振动加速度传感器5/8-24UNF军标连接器”就够劝退了,但如果你在航空发动机试车台、燃气轮机厂房、重型机械现场转过一圈,一定会…

2026/10/20 7:38:07 阅读更多 →
杰理平台time PWM实战:定时器比较输出的配置与踩坑指南

杰理平台time PWM实战:定时器比较输出的配置与踩坑指南

前阵子帮朋友调一块杰理平台的小板子,现象很蹊跷:按键控制LED亮度时,IO口确实有电平变化,但用示波器一看,波形完全不是PWM该有的样子,反而是一串不规则的脉冲,频率还飘。翻遍SDK里的例程&#x…

2026/10/20 7:38:07 阅读更多 →
纯.NET自绘打印模板设计器:拖拽式可视化与动态渲染

纯.NET自绘打印模板设计器:拖拽式可视化与动态渲染

简介:这是一套面向.NET开发者的C#可视化打印模板设计源码,纯.NET实现、不依赖任何第三方控件,适合需要为发票、报告、证书、标签及单据类业务快速搭建打印能力的初中级工程师。资源提供模板编辑器、图形设计工具与布局管理器等组件&#xff0…

2026/10/20 7:37:06 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

/* 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 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

/* 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 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

/* 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 1:01:30 阅读更多 →

周新闻

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

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

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

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

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

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

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

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

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

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 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/19 2:54:50 阅读更多 →
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/19 9:10:22 阅读更多 →
黑夜航拍船只数据集训练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/18 12:16:16 阅读更多 →