上周我把一个叫chess-analysis的 Claude Code 技能挂到了 Show HN 上。这个项目的起因很朴素我下了十几年棋最近一年养成一个习惯每盘棋结束都把 PGN 棋谱丢进本地文件夹再用 Stockfish 批量跑一遍引擎分析把漏算的战术、压不住的胜势、时间恐慌里的离谱昏招都标记出来。问题是这套流程的输出永远是干巴巴的bestmove和centipawn数字我看完还得自己对着棋谱翻译成“为什么这步不好”。折腾几次之后我决定把整个分析流水线封装成一个 Claude Code 的 skill——一个能被 Claude Code CLI 直接加载的本地技能包。它自动读取 PGN 文件、调用开源国际象棋引擎做局面评估再把原始数据变成有上下文、有注解、甚至能指出你棋风短板的复盘报告。这文章不是给你展示什么惊天动地的黑科技而是想借这个实际项目把 Agent Skill 从“听说过”讲到“能复现”。我会拆解技能包的目录结构、SKILL.md的编写规范、python-chess 与 Stockfish 的协作方式以及我在调试过程中踩过的五个低级坑。无论你是想给 Claude Code 写自定义技能还是单纯想优化自己的棋谱复盘流程这篇都值得读完再动手。1. 这个技能解决什么问题从“跑引擎”到“读复盘”先说痛点。传统上我们做棋局复盘要么靠人肉摆谱要么靠 Stockfish 跑引擎。引擎能告诉你“这步棋导致胜率从 85% 掉到 40%”但它不会告诉你“你在这个残局里犯了典型的中局思维错误”。专业棋手看得懂数值业余爱好者看了只会一脸懵。我想要的是一个能衔接两者的工具引擎负责计算大模型负责把计算结论翻译成人类能理解的复盘语言。chess-analysis这个技能在 Claude Code 里做三件事解析棋谱从 PGN 文件里读出处着记录包括开局名称、双方用时、ECO 编码、最终结果。调用引擎分析对选定的局面跑 Stockfish 深度分析输出最佳着法、评估分值、主要变例principal variation。生成复盘报告让 Claude 基于引擎数据写一段教练风格的点评指出关键转折点、漏算的战术、以及可能存在的思路问题。这个设计解决了我的核心烦扰以前跑完引擎还要自己对着 JSON 数据脑补现在一句话就能获得带上下文的复盘。而且因为技能是本地执行的PGN 文件不用上传到任何云端全程在自己电脑上跑隐私上也安心得多。适合谁来参考两类人。第一类是 Claude Code 的进阶用户想弄明白自定义 skill 到底怎么写、怎么调试、怎么跟外部程序协作。第二类是有一定棋龄的业余棋手平时用 Stockfish 分析但总觉得差点意思想借助 LLM 获得更贴近人类的复盘视角。当然如果你两个身份都占了那这个项目真的是为你准备的。2. 拆解 Agent SkillClaude Code 技能的真实加载方式在动手写代码之前得先厘清一个概念Claude Code 里的 skill 到底是什么我用一句大白话解释——它是一个“按需加载的说明书工具箱”组合本质是一个目录目录里有一个SKILL.md文件外加任意数量的辅助脚本、模板、参考文档。当用户提问触发技能描述时Claude Code 会把SKILL.md里的指令注入到上下文并允许技能调用指定的工具或脚本。它和常见的 CLI 脚本最大的区别在于CLI 脚本是“你主动跑它”skill 是“AI 判断该用它时自动调用”。这一点非常像函数按需加载只不过加载的是一整套工作流。比如你直接运行python analyze_pgn.py得到的是原始 JSON而让 Claude Code 加载chess-analysis技能它会把 JSON 翻译成一篇有教练口吻的分析报告还能回答你跟着追问的“那下一步呢”。这里必须提一下技能目录的加载规则。Claude Code 会扫描几个固定位置路径作用域说明~/.claude/skills/用户级所有项目都能用适合通用技能.claude/skills/项目级只对当前项目生效适合定制流程.claude/skills/的子目录项目级可以按团队或模块拆分把技能放在用户级目录是最省心的做法。我的chess-analysis就放在~/.claude/skills/chess-analysis/下这样在任何目录打开 Claude Code 都能直接调用。SKILL.md是整个技能包的入口。Claude Code 读取它来决定什么时候加载、加载后该干什么。文件头部有一段 YAML frontmatter包含name、description、allowed-tools这些关键字段。description尤其重要它是 AI 判断“这个问题是否该用这个技能”的匹配依据。写得太泛任何棋类问题都会触发它写得太窄真正需要时又不会被调用。这是个需要在实践中反复调节的平衡点。还有一点值得注意Anthropic 官方文档里提到过一个disable-model-invocation选项设为true后技能不会因为描述匹配自动触发只能由用户显式“召唤”。我一开始用默认的自动触发模式结果在分析非棋谱文件时 Claude 也频繁想加载技能浪费了不少 token。后来干脆把自动触发关掉需要用的时候直接在提示词里写“用 chess-analysis 技能分析这个棋谱”反而更清爽。如果你希望技能在特定场景下被动等待用户指令这个开关值得优先配置。3. 搭一个能跑的技能包目录、SKILL.md 与标注规范接下来进入实操。先看我最终版的技能目录结构然后逐个文件解释为什么这么放。~/.claude/skills/chess-analysis/ ├── SKILL.md ├── scripts/ │ ├── analyze_game.py │ ├── detect_tactics.py │ └── requirements.txt └── templates/ └── report_template.mdSKILL.md是入口文件Claude Code 首先读它。scripts/存放实际干活的 Python 脚本templates/放报告模板。目录结构的核心原则是把需要动脑的逻辑写进脚本把需要调校的措辞写进 SKILL.md 指令。这样你调整报告风格时不用碰代码改代码时也不用担心破坏提示词。下面是SKILL.md的简化版标注了关键字段--- name: chess-analysis description: 分析国际象棋棋谱.pgn 文件调用 Stockfish 进行局面评估输出结构化复盘报告。适用于用户提供 PGN 文件、棋谱路径或要求解释某步棋好坏、查找漏算战术等场景。 allowed-tools: bash, Read, Write, Edit disable-model-invocation: true metadata: version: 0.1.0 --- !-- 使用本技能时必须按以下步骤执行不要跳过 -- 1. 如果用户只给了棋谱文本先将其保存为临时 .pgn 文件。 2. 运行 python scripts/analyze_game.py pgn_path 获取 JSON 分析结果。 3. 如果 JSON 中包含 tactics 字段继续运行 python scripts/detect_tactics.py pgn_path --move move_number 深入检测。 4. 把 JSON 数据映射到 templates/report_template.md生成复盘报告。 5. 输出报告时保留引擎数值但需用自然语言解释每个数值的业务含义。 ## 输出格式要求 - 开局阶段简要说明开局名称与变例走向。 - 中局阶段列出关键转折点写明引擎评估变化。 - 残局阶段指出决定性失误或精确胜法。 - 战术漏算用“当时你走 X错过了 Y”的句式描述。 ## Suggestions - 当用户说“这步棋好吗”“为什么走这里”时优先分析当前局面。 - 当用户说“复盘整盘”时按顺序输出全盘分析。 - 如果 Stockfish 未安装提示用户安装并提供 brew 安装命令。看到## Suggestions这一段了吗这是 Claude Code 技能规范里很有特色的设计它直接在技能文档里给 AI 提供行为建议告诉模型用户用什么话术时会对应走哪条分支。以前写提示词是用自然语言埋在正文里现在Suggestions变成了一个显式区块让意图匹配更稳定。但光有SKILL.md还不够。真正干活的还是scripts/analyze_game.py。这个脚本读取 PGN、调用引擎、输出 JSON。它必须写得让 AI 能轻松解析——因为 Claude Code 是通过 bash 工具运行脚本的它只能通过标准输出拿到结果。如果脚本输出乱七八糟的调试信息AI 就会误读。我在脚本里特意定义一个 JSON 输出协议结构固定成下面这样{ pgn: 1. e4 e5 2. Nf3 Nc6 ..., opening: Italian Game, declined_advantage: [ { move_number: 17, side: white, played: Nxd4, best: Qb3, eval_before: 0.8, eval_after: -0.3, message: 白方主动交换后黑方获得反击机会 } ], final_eval: { white: 3.2, black: -5.1, result: 白胜 } }这个结构有四个要点opening方便报告首段直接引用declined_advantage是核心数据记录每一步“评估下滑”的着法final_eval用于结尾整个输出只有 JSON没有日志。Claude Code 拿到 JSON 后按SKILL.md的指示生成报告——它不需要理解 JSON 后面有没有隐藏信息因为它只需要按字段填模板。4. 核心分析脚本用 python-chess 串起 PGN 和 Stockfish脚本技术栈选得相当轻python-chess 负责解析棋谱Stockfish 负责引擎计算标准库subprocess负责中间通信。为什么不用 C-chess 引擎的 Python 封装因为 python-chess 本身就带有engine模块可以很方便地启动 UCI 引擎并控制搜索深度没必要多引入一层依赖。先看依赖安装requirements.txt里只有两行python-chess1.999Stockfish 不需要 pip 安装它是个独立可执行文件装好之后在SKILL.md里让用户通过包管理器装就行# macOS brew install stockfish # Ubuntu/Debian apt install stockfish这里我踩过一个坑早期版本的脚本里我把 Stockfish 路径硬编码成/usr/local/bin/stockfish结果别人的机器上引擎在别的位置直接崩。后来改成先读环境变量找不到就用shutil.which(stockfish)自动探测。analyze_game.py的核心逻辑可以拆成四步载入对局用chess.pgn.read_game()解析 PGN 文件。逐步评估遍历棋谱里的每一步每走一步就获取当前局面然后调用引擎计算。记录偏差对比刚走出的实际着法和引擎推荐的最佳着法计算评估分差。输出 JSON把关键数据整合成结构化文件。逐步评估的代码大致长这样import chess import chess.pgn import chess.engine def analyze_game(pgn_path, engine_path, depth18): game read_pgn(pgn_path) board game.board() engine chess.engine.SimpleEngine.popen_uci(engine_path) declined [] for move_number, node in enumerate(game.mainline(), start1): played_move node.move info engine.analyse(board, chess.engine.Limit(depthdepth)) best_move info.get(pv, [])[0] if pv in info else None best_score info[score].pov(chess.WHITE).score(mate_score10000) board.push(played_move) # 走完实际着法后再评估一遍看分数变化 info_after engine.analyse(board, chess.engine.Limit(depthdepth)) score_after info_after[score].pov(chess.WHITE).score(mate_score10000) if best_move and played_move ! best_move and score_after best_score - 50: declined.append({ move_number: move_number, side: white if board.turn chess.BLACK else black, played: board.san(played_move), best: board.san(best_move) if best_move else unknown, eval_before: best_score / 100, eval_after: score_after / 100, message: 实际着法比引擎最佳着法少收益约 {} 分.format((best_score - score_after) / 100) }) return {opening: game.headers.get(Opening, ), declined_advantage: declined}这里面最容易出错的是side字段。我在代码里用board.turn判断是谁走完之后轮到谁走——因为board.push(played_move)之后board.turn已经翻转了所以要反过来判断实际走子方。这个问题调了我将近一个小时排查的时候一直在想“为什么黑棋的失误全被标成白棋”。还有score.pov(chess.WHITE)这个 API。一个新手常掉进的坑是直接用info[score].white()之类的写法但 python-chess 的Score对象区分相对分和绝对分必须指定视角方向。这里的pov是 point of view 的缩写指定从白方视角返回数值。如果你不指定某些局面下黑方明明占优数值却显示为负数报告就全反了。detect_tactics.py是我后加的增强功能。它用浅层搜索depth12快速扫描整盘棋判断是否存在可以一步吃子的战术组合。原理很简单如果引擎在某个深度下给出的主变例里包含连续将军、强制吃后、双重攻击这类模式就把对应步数标记为“战术机会”。这个脚本不用跑全深度所以整体速度和analyze_game.py完全不在一个量级——全盘 40 步棋深度 18 大约要 20 秒而战术检测 3 秒内跑完。5. 让 Claude Code 真正用起来触发描述、调试命令与实战演示技能包建好之后最兴奋也最容易翻车的环节是“验证 AI 能不能按预期调用”。我最初把它放在~/.claude/skills/下激动地写了条提示语“分析这个棋谱”结果 Claude 完全没搭理技能反而自己生成了大段泛泛而谈的棋理分析一个引擎数值都没有。原因就是description写得太像散文AI 匹配度太低。解决方式很直接把description改成强触发型的话术让它覆盖各种用户可能输入的自然语言表达。description: 当用户提供国际象棋棋谱.pgn 文件路径或文本、要求分析对局、复盘失误、解释具体棋步优劣或询问引擎评估结果时必须调用本技能。改完之后在项目目录里重新加载技能再输入“帮我分析这盘棋” 帮我分析这盘棋 好的我先找到棋谱文件路径然后调用 chess-analysis 技能进行分析。 [读取 ~/games/2024-09-15.pgn] [运行 python scripts/analyze_game.py ...] [运行 python scripts/detect_tactics.py ...] 这是一盘意大利开局ECO C50的对局白方在第 14 步 Nxd4 出现重大评估下滑 引擎最佳着法为 Qb3保持约 0.8 分的优势。实际走 Nxd4 后黑方通过 Bb4 获得反击 局势逆转至 -0.3。从这一手开始白方整盘再也没有拿回主动权。看到没Claude 会先读取文件、再调用脚本最后把 JSON 数据组织成回话回复。整个流程有板有眼而不是停留在“我看看棋谱然后凭感觉说两句”的状态。调试阶段有几个命令/操作比较有用操作用途/skill斜杠命令查看当前项目已加载哪些技能claude --debug对话模式打印上下文中注入的技能指令确认 SKILL.md 是否真的被读取ls ~/.claude/skills/确认目录结构存在且路径拼写无误python scripts/analyze_game.py test.pgn脱离 Claude Code 直接测脚本排查代码问题--debug模式是我最推荐的排查手段。它会把“系统加载了哪些文件、注入了哪些指令”展示出来。如果技能没被加载你一眼就能看到是不是路径不对如果加载了但没触发那就是description写得太菜。还有一个非常关键的体验优化如果技能脚本需要接收的参数很多别指望用户每次手动输入完整路径。我在SKILL.md里明确写了“如果用户提供的 PGN 是文本而非路径先用 Write 工具保存为临时文件再传给脚本”。这样用户只需要粘贴 FEN 或 PGN 文本Claude 自己会处理文件生命周期。6. 踩坑实录我修过的五个低级错误这部分我觉得比前面的技术细节更值钱。每个坑都是真金白银换来的调试时间分享出来能让你少走不少弯路。坑一description 写得太像博客文章导致触发失败。脚本写得再好AI 不加载技能就等于白干。这不是脚本 bug而是description匹配逻辑的问题。我的经验是description里应该直接列出“用户说哪些话你可以用我”而不是描述“我是做什么的”。把“分析国际象棋棋谱并调用 Stockfish 生成复盘”改成“当用户提供.pgn文件或要求复盘棋局时使用”触发率肉眼可见地提升。坑二脚本用绝对路径/硬编码路径导致别人环境跑不了。我早期的SKILL.md里写的是python /Users/myname/.claude/skills/chess-analysis/scripts/analyze_game.py在共享给朋友时直接运行失败。后来全部改成相对路径写法python scripts/analyze_game.py并假设执行目录是技能目录本身。为了让这个假设成立我在SKILL.md里加了一句“第一步先 cd 到技能目录”这样无论用户在哪个项目路径下打开 Claude Code脚本都能稳定执行。坑三yaml frontmatter 里的冒号没加引号导致解析失败。SKILL.md的 frontmatter 是 YAML 格式如果你在 description 里写了英文冒号比如“分析棋谱: 基于...”YAML 解析器会把整行截断。不要问我是怎么发现的。在 openAI 的 Markdown 世界里这不是问题但在 YAML 里值里的冒号后面必须跟空格或加引号。我把所有 description 的值都套上了双引号一劳永逸。坑四误把引擎分析当“标准答案”。Stockfish 分析结果不是上帝。深度不同的分析对同一个局面可能给出完全不同的评级。我在SKILL.md里专门写了“分析输出必须标注搜索深度如果深度小于 16需要提示用户当前结果可信度有限”。这样报告末尾会自动加一句“该分析基于深度 18 的搜索结论仅供参考”。这份“免责声明”不光在棋理上更严谨也避免用户拿一个浅层搜索的结果去跟别人对喷。坑五没考虑 PGN 文件编码与乱码问题。很多棋谱是从网上复制的可能带 BOM 头、Windows 换行符甚至中文注释放的是 UTF-8 BOM。read_game()能解析大部分 PGN但对乱码棋谱会出现诡异的报错。解决方式是在脚本开头做一次编码检测与清理raw_text open(pgn_path, rb).read().decode(utf-8-sig) raw_text raw_text.replace(\r\n, \n)这步看起来不起眼但能减少三分之一的无故报错。7. 复盘效果对比与后续扩展方向把技能挂到 Show HN 之后不少网友按着 README 跑了一遍反馈集中在两点一是“AI 输出比裸引擎数据舒服多了”二是“居然能从棋谱里读出棋风特征”。这个反馈跟我的感受高度一致。我自己的使用体验是以前跑完 Stockfish 要花二十分钟整理复盘笔记现在一条命令生成初稿我只需要校对几个关键转折点。更重要的是这个技能把分析从“数值驱动”提升到了“概念驱动”层面——它不再只说“e4 比 c4 好 0.5 分”而是说“你在这个封闭局面里忽视了白格象的价值”。再往后扩展的方向我目前列了三件事接入 Lichess API自动拉取最近对局然后跑技能批量复盘省去手动保存 PGN 的步骤。开局树可视化在 JSON 里补上开局变例路径让 Claude 生成 ASCII 棋谱树或导出可分享链接。多模型支持把 Stockfish 换成lc0或其他神经网络引擎对比不同引擎对同一局面的判断分歧。这些扩展其实都在“Skill 外部程序”的框架里不需要推翻现有设计只要在SKILL.md里增加新的步骤指令在scripts/下新增对应脚本即可。我个人对第三项最感兴趣因为神经引擎和传统引擎的评估差异恰好能揭示一些“纯计算式盲区”。最后再分享一个经验这类个人复盘技能最容易翻车的不是技术而是你对“分析深度”的预期管理。Stockfish 在深度 18 以上才有较高准确度但那意味着更长的等待时间和更高的 CPU 占用。我建议技能默认跑深度 14~16够用且速度快只有用户明确要求“深度分析”时才上升到 20。这个平衡点在SKILL.md里写清楚比你事后反复解释要省心得多。
企业数字化 ERP 产品动态
相关推荐
Fetch流式加载大模型API的底层原理与实战 1. 为什么流式加载大模型API接口必须用Fetch——一个被多数人忽略的底层事实你有没有遇到过这样的场景:在前端调用大模型API时,明明后端已经用SSE(Server-Sent Events)或chunked transfer encoding分块返回了token流,但… · 2026/11/3 8:58:45
MySQL增强半同步复制实战与优化指南 1. 项目概述 MySQL主从复制是数据库高可用架构的基础,而增强半同步复制(Lossless Semi-Synchronous Replication)则是介于异步复制和全同步复制之间的折中方案。我在电商系统数据库运维中,发现当主库突然宕机时,异步复… · 2026/11/3 8:57:44
ax是什么?从电机绕组到部署脚本的歧义排查实战 前几天我连着处理了两个跟“ax”沾边的问题,一个是电机调试现场被问“直流无刷电机里ax by cz怎么划分的,是按照垂直轴线划分的么”,另一个是部署环境的同事报“sim_ekb_install这套脚本执行完以后,ax、nf、zz这几个文件夹内全是空… · 2026/11/3 8:57:44
WanVideo_comfy:ComfyUI视频生成模型一站式解决方案 WanVideo_comfy:ComfyUI视频生成模型一站式解决方案
WanVideo_comfy是一个专为ComfyUI设计的视频生成模型整合包,汇集了Wan-AI生态系统中的多种高质量视频生成模型。该项目通过统一的架构和优化的部署方案,为AI视频创作者提供了从文本到视频… · 2026/11/3 9:25:14
WorldStereo 2.0世界扩展详解:从全景图到可导航3D世界的转换秘籍 WorldStereo 2.0世界扩展详解:从全景图到可导航3D世界的转换秘籍
你是否曾梦想将一张普通的全景图变成一个可以自由探索的3D世界?🤔 腾讯混元团队的HY-World 2.0项目中的WorldStereo 2.0世界扩展技术,正是实现这一梦想的魔法钥匙… · 2026/11/3 9:25:14
【系统架构师设计(五)】需求工程之:需求开发与需求管理概述、结构化需求分析法 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 9:24:14
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46
STM32F411RE与MAX14431实现SPI隔离:工业现场抗共模干扰实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
ESP32+大模型AI硬件开发:从Demo到产品的8个工程问题 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
基于MAX14431BAEE+与PIC18F85J10的SPI隔离方案设计与实践 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46