TaoToken 模块组织与依赖关系:TypeScript CLI 循环依赖排查实战
1. TypeScript CLI 循环依赖排查从构建报错到模块拆分TypeScript CLI 项目写到一定规模最容易撞上的不是类型体操而是模块组织与依赖关系失控。你可能会遇到这样的场景本地tsc突然报Cannot access X before initialization或者bun build打包后运行时报undefined is not a function再或者madge一跑满屏红色箭头——循环依赖。这类问题在 CLI 项目里尤其隐蔽因为 CLI 通常有一个聚合入口比如tools.ts、commands.ts所有子模块都往这里注册子模块又反过来引用入口里的类型或工具函数环就形成了。我试过在一个 200 文件的 TypeScript CLI 里排查循环依赖最初靠肉眼翻 import效率极低。后来固定了一套流程先用依赖图工具把环可视化再用类型集中化 延迟 require 拆环最后用构建产物和运行时双重验证。这套流程对 Claude Code 这类大型 CLI 的模块组织思路是通用的——它的Tool.ts里明确注释了「从集中位置导入权限类型以打破 import cycles」tools.ts里用require()包裹TeamCreateTool来延迟解析都是同一套打法。这篇文章面向正在维护 TypeScript CLI 的开发者尤其是遇到循环依赖导致构建失败、启动报错、打包产物异常的人。你会拿到可复制的依赖图生成配置、循环检测命令、拆环前后的对照代码以及一套从定位到验证的完整动作。核心检索词就是「TypeScript CLI 循环依赖排查」和「模块组织与依赖关系治理」下面所有步骤都围绕这两个点展开。先说清楚循环依赖为什么在 CLI 里特别容易发生。CLI 的典型结构是入口cli.ts导入main.tsmain.ts导入聚合层tools.ts/commands.ts聚合层导入各个具体实现tools/BashTool/BashTool.ts而具体实现又需要引用核心抽象Tool.ts里的类型甚至需要调用tools.ts里的注册函数。只要有一条反向边环就闭合了。更麻烦的是 TypeScript 的import type在编译后会被擦除但如果你写成了普通import运行时就真的会去加载那个模块环在运行时才暴露。所以排查循环依赖要分两层看编译期类型层和运行期值层。import type造成的环通常不影响运行但会让tsc的类型推断变慢甚至报错普通import造成的环才是运行时undefined的元凶。下面的步骤会同时覆盖这两层。2. TaoToken 前置拿到 Base URL、API Key 与 Model ID在开始改代码之前先把模型调用这条链路打通因为后面验证拆环效果时你需要一个能实际跑起来的 CLI 命令来确认运行时没有回归。TaoToken 在这里的角色是提供兼容 OpenAI 风格的接口让你在 CLI 里用统一的 Base URL API Key Model ID 三件套调用模型不用为每个供应商写一套适配。你需要准备三样东西第一API Key。到控制台的 API Keys 页面创建一个复制出来。地址是 https://taotoken.net/api-keys 创建后只显示一次记得存到环境变量里别硬编码进代码。第二Base URL。TaoToken 的 API 入口是 https://taotoken.net/api 注意这个地址不带任何查询参数直接作为 OpenAI SDK 的baseURL使用。如果你用的是 Anthropic 风格的 SDK走的是另一套路径但本文的 CLI 示例统一用 OpenAI 兼容风格方便你直接套。第三Model ID。在模型对话页面可以看到当前可用的模型列表选一个你常用的比如claude-sonnet-4-5这类标识。Model ID 要和你 CLI 里配置的字段完全一致大小写敏感。把这三个值写进.env文件或者直接 export 到 shellexport TAOTOKEN_API_KEYsk-你的key export TAOTOKEN_BASE_URLhttps://taotoken.net/api export TAOTOKEN_MODELclaude-sonnet-4-5这里有个坑要提前说很多 CLI 项目会在启动时读取配置如果你的配置模块本身就在循环依赖环里那么process.env可能还没被加载就被引用了导致读到undefined。所以配置读取要放在依赖图的最底层基础设施层不要让它依赖任何上层模块。这也是后面拆环时的一个原则。如果你还没决定用哪个模型可以先到模型对话页面发一条测试消息确认 Key 和 Base URL 能通再回到 CLI 里配置。这样能把「模型调用失败」和「循环依赖导致运行异常」两类问题分开避免排查时互相干扰。对于长期做 CLI 编码和 Agent 开发的场景可以考虑 Coding Plan它更适合高频调用和长会话不用每次手动管额度。但本文的重点是依赖治理模型调用只是验证手段你按自己的用量选就行。3. 可复制配置依赖图生成与循环检测这一节给你可以直接落地的配置。目标是把 TypeScript CLI 的模块依赖关系画出来并自动检测循环。我用的是madgedependency-cruiser组合前者出图快后者规则细。先装依赖npm i -D madge dependency-cruiser3.1 madge 配置与循环检测命令madge可以直接对src目录跑循环检测输出环的路径。最常用的命令npx madge --circular --extensions ts,tsx src/如果只想看依赖图不检测环生成 SVGnpx madge --image deps.svg --extensions ts,tsx src/但 CLI 项目往往有路径别名/之类madge默认不认tsconfig的paths需要显式指定npx madge --circular --extensions ts,tsx --ts-config tsconfig.json src/实测下来madge对import type也会算进依赖所以它报的环里有一部分是类型层的不影响运行。这时候要配合dependency-cruiser做更细的判定。3.2 dependency-cruiser 配置片段在项目根目录建.dependency-cruiser.js下面这份配置可以直接用重点是no-circular规则和tsConfig路径解析/** type {import(dependency-cruiser).IConfiguration} */ module.exports { forbidden: [ { name: no-circular, severity: error, comment: 禁止循环依赖CLI 项目里环会导致运行时 undefined, from: {}, to: { circular: true, }, }, { name: no-orphans, severity: warn, comment: 孤立模块可能是没被引用的死代码, from: { orphan: true, pathNot: [\\.d\\.ts$, (^|/)tsconfig\\.json$], }, to: {}, }, ], options: { doNotFollow: { path: node_modules, }, tsConfig: { fileName: tsconfig.json, }, enhancedResolveOptions: { exportsFields: [exports], conditionNames: [import, require, node, default], extensions: [.js, .jsx, .ts, .tsx], }, reporterOptions: { dot: { collapsePattern: node_modules/[^/], }, archi: { collapsePattern: ^(packages|src|lib|app|bin|test)/[^/]/[^/], }, }, }, };跑检测npx depcruise --config .dependency-cruiser.js src/输出会直接告诉你哪条边构成了环比如tools.ts - TeamCreateTool.ts - tools.ts。这就是你要拆的目标。3.3 tsconfig 路径别名配置如果你的 CLI 用了路径别名tsconfig.json里要有对应的paths否则依赖工具解析不到真实文件环会漏报{ compilerOptions: { target: ES2022, module: ESNext, moduleResolution: Bundler, strict: true, baseUrl: ., paths: { /*: [src/*], tools/*: [src/tools/*], types/*: [src/types/*] } }, include: [src/**/*.ts, src/**/*.tsx] }注意moduleResolution用Bundler还是NodeNext会影响解析行为。CLI 项目如果最终用bun build或esbuild打包用Bundler更贴近实际如果用tsc直接产出用NodeNext。这个字段配错依赖图会失真。3.4 把检测接进 CI在package.json里加脚本提交前自动跑{ scripts: { dep:check: depcruise --config .dependency-cruiser.js src/, dep:graph: madge --image deps.svg --extensions ts,tsx --ts-config tsconfig.json src/ } }这样每次 PR 都会拦下新增的环避免依赖关系继续恶化。4. 验证请求从定位到拆分的完整动作配置就绪后走一遍完整的定位到拆分流程。假设depcruise报了一个环src/tools.ts - src/tools/TeamCreateTool/TeamCreateTool.ts - src/tools.ts。4.1 定位环的具体边先看tools.ts里怎么引用TeamCreateTool// src/tools.ts import { TeamCreateTool } from ./tools/TeamCreateTool/TeamCreateTool.js; export function getTools() { return [TeamCreateTool, /* ...其他工具 */]; }再看TeamCreateTool.ts里怎么反向引用tools.ts// src/tools/TeamCreateTool/TeamCreateTool.ts import { getTools } from ../../tools.js; export const TeamCreateTool { name: TeamCreate, async run() { const all getTools(); // ... }, };环就在这里tools.ts导入TeamCreateToolTeamCreateTool又导入tools.ts的getTools。运行时tools.ts开始执行遇到import TeamCreateTool去加载TeamCreateTool.ts后者又去加载tools.ts此时tools.ts还没执行完getTools是undefined于是报getTools is not a function。4.2 拆环方案一延迟 require最直接的改法是把TeamCreateTool.ts里的静态导入改成函数内延迟 require// src/tools/TeamCreateTool/TeamCreateTool.ts export const TeamCreateTool { name: TeamCreate, async run() { // 延迟到运行时才解析打破静态环 const { getTools } require(../../tools.js) as typeof import(../../tools.js); const all getTools(); // ... }, };这样静态依赖图里TeamCreateTool.ts不再指向tools.ts环断开。代价是失去了静态类型检查的即时性但as typeof import(...)把类型补回来了。4.3 拆环方案二类型集中化如果环是因为共享类型造成的把类型抽到src/types/下。比如Tool.ts里原本从tools.ts导入PermissionResult改成从types/permissions.ts导入// src/Tool.ts // 从集中位置导入权限类型打破 import cycle import type { AdditionalWorkingDirectory, PermissionMode, PermissionResult, } from ./types/permissions.js;types/permissions.ts只依赖其他类型文件不依赖任何实现所以它是依赖图的叶子不会成环。这是 Claude Code 里明确采用的做法注释里写得很清楚。4.4 拆环方案三接口隔离如果TeamCreateTool只是需要「获取所有工具」这个能力可以把它抽成一个接口由上层注入而不是直接导入tools.ts// src/types/toolRegistry.ts export interface ToolRegistry { getAll(): unknown[]; }TeamCreateTool依赖ToolRegistry接口tools.ts实现它并在注册时注入。这样依赖方向变成TeamCreateTool - types/toolRegistry不再指向tools.ts。4.5 验证拆环效果改完后重新跑检测npx depcruise --config .dependency-cruiser.js src/应该看到no-circular规则通过没有 error。再跑madge确认npx madge --circular --extensions ts,tsx --ts-config tsconfig.json src/输出No circular dependency found!就对了。然后做运行时验证。写一个最小 CLI 命令实际调用TeamCreateTool// src/entrypoints/cli.ts import { getTools } from ../tools.js; async function main() { const tools getTools(); const teamCreate tools.find((t: any) t.name TeamCreate); if (!teamCreate) { throw new Error(TeamCreate tool not registered); } const result await teamCreate.run(); console.log(TeamCreate result:, result); } main().catch((err) { console.error(CLI failed:, err); process.exit(1); });跑起来npx tsx src/entrypoints/cli.ts如果之前是getTools is not a function现在应该能正常输出结果。这一步很关键因为depcruise通过不代表运行时一定没问题——有些环是动态require造成的静态工具看不到。4.6 用模型调用做端到端验证如果你的 CLI 里有调用模型的功能用 TaoToken 的三件套跑一次真实请求确认拆环没有破坏配置加载链路// src/services/api/client.ts import OpenAI from openai; const client new OpenAI({ apiKey: process.env.TAOTOKEN_API_KEY, baseURL: process.env.TAOTOKEN_BASE_URL, }); export async function chat(prompt: string) { const res await client.chat.completions.create({ model: process.env.TAOTOKEN_MODEL!, messages: [{ role: user, content: prompt }], }); return res.choices[0]?.message?.content; }跑npx tsx -e import(./src/services/api/client.js).then(m m.chat(ping).then(console.log))能打印出模型回复说明配置模块、服务模块、入口模块的依赖链都是通的拆环没有引入新的加载顺序问题。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth拆环过程中和模型调用链路上有几类报错特别常见逐个对照。5.1 401 Unauthorized报错长这样Error: 401 Unauthorized { error: { message: Invalid API key, type: invalid_request_error } }原因通常是 API Key 没读到或读错了。检查顺序第一process.env.TAOTOKEN_API_KEY是否真的被加载CLI 启动时有没有加载.env第二Key 有没有多余空格或换行第三Base URL 是不是写成了https://taotoken.net/api/末尾多斜杠有时会导致路径拼接错误。如果配置模块本身在环里process.env可能还没初始化就被引用这时候要先把配置读取移到依赖图最底层。5.2 local proxy failed报错Error: local proxy failed: connect ECONNREFUSED 127.0.0.1:7890这是本地网络配置残留导致的。检查你的 shell 里有没有HTTP_PROXY/HTTPS_PROXY指向一个已经不存在的本地端口。CLI 项目里如果用了一些网络库它们会自动读这些环境变量。清掉unset HTTP_PROXY HTTPS_PROXY ALL_PROXY然后重跑。注意不要在任何配置里写死代理地址这类问题在 CI 环境里尤其容易复现。5.3 reading choices of undefined报错TypeError: Cannot read properties of undefined (reading choices)这是模型调用返回结构不符合预期。常见原因第一baseURL配错请求打到了非兼容端点返回的不是 OpenAI 格式第二model字段填了一个不存在的 Model ID服务端返回错误对象而不是正常响应第三SDK 版本和接口不匹配。排查时先把原始响应打出来const res await client.chat.completions.create({ /* ... */ }); console.log(JSON.stringify(res, null, 2));确认res.choices存在再往下走。如果res本身是undefined说明 SDK 调用抛异常被吞了检查有没有 try/catch 把错误吃掉了。5.4 OAuth 相关报错报错Error: OAuth token expired Error: invalid_grant如果你的 CLI 集成了 OAuth 登录拆环时如果把 token 刷新逻辑和入口模块耦合在一起可能出现刷新失败。检查 token 存储模块是否依赖了上层模块导致刷新时读不到配置。OAuth 的 token 管理应该放在服务层只依赖基础设施层的存储和 HTTP 客户端不要反向依赖入口。5.5 拆环后仍然报 undefined有时候depcruise显示无环但运行时还是undefined。这通常是动态require的时机问题。比如你把require放在了模块顶层而不是函数内// 错误还是在模块加载时执行 const { getTools } require(../../tools.js);这样环只是从静态图里消失了运行时依然在加载阶段触发。正确做法是放进函数体确保调用时才解析// 正确调用时才解析 function run() { const { getTools } require(../../tools.js); }5.6 三件套配置对照表如果你在 CLI 里同时用了多个模型供应商用下面这张表核对字段避免混用配置项环境变量示例值常见错误Base URLTAOTOKEN_BASE_URLhttps://taotoken.net/api末尾多斜杠、写成网页地址API KeyTAOTOKEN_API_KEYsk-xxxx含空格、未加载 .envModel IDTAOTOKEN_MODELclaude-sonnet-4-5大小写错误、用了不存在的模型这三项在 CLI 里出现时必须成对出现Base URL Key Model ID。少任何一个都会导致 401 或reading choices。如果你用的是 Claude Code 风格的配置检查settings.json里的字段名是否和代码里读取的一致。6. 把依赖治理变成日常动作拆完一个环不代表结束。CLI 项目会持续加功能新的环随时可能长出来。我的做法是把depcruise接进 pre-commit 和 CI任何新增的no-circular违规直接拦下。同时在types/目录里维护一份共享类型清单新类型优先往这里放而不是从实现模块里导出。另外延迟require虽然好用但不要滥用。它牺牲了静态分析能力用多了会让依赖图变得不可信。优先用类型集中化和接口隔离实在拆不开再用延迟require并且在代码里写清楚注释说明为什么这里必须延迟。最后每次重构模块组织后跑一遍端到端的模型调用验证。用 TaoToken 的三件套发一条真实请求确认配置加载、服务调用、入口执行这条链路没有因为拆环而断裂。这一步花不了几分钟但能挡住大部分「静态检查通过、运行时崩溃」的回归。如果你在拆环过程中遇到depcruise报的环和实际运行时报错对不上多半是动态require或import type造成的差异。把madge和depcruise两个工具的输出对照着看再结合运行时的堆栈基本能定位到具体那条边。依赖治理没有一劳永逸但有了这套检测和拆分流程至少不会让环悄悄堆积到无法收拾。

相关新闻

长安市井禅

长安市井禅

《长安市井禅》作者:子乐城墙根下人潮过城砖磨软半生刚曲江池边云起处不携烟火也带香终南的烟霞折一角揣进衣兜拐进西羊市的巷口要碗胡辣汤就着晨光一口清宁一口油钟楼的钟声撞过几轮卖甑糕的老汉还在那棵槐树下把甜糯码齐像和尚叠袈裟一层不多一层不少大雁塔的影子…

2026/10/13 10:37:06 阅读更多 →
Redis键明明过期了,业务还在读到旧数据

Redis键明明过期了,业务还在读到旧数据

线上经常遇到一种很无解的缓存问题。 给Redis的Key设置了过期时间,比如10分钟自动失效。时间到了之后,我们预想的是缓存清空、自动走数据库刷新数据。 但实际线上表现很诡异:过期之后一段时间内,接口依然能读到旧缓存数据&#xf…

2026/10/13 10:37:10 阅读更多 →
中小工厂设备远程运维系统搭建指南:从选型到落地避坑

中小工厂设备远程运维系统搭建指南:从选型到落地避坑

先把一个真实到扎心的场景放在这儿:凌晨两点,注塑机上模温机突然报警,温度往下掉,值班工人不敢动设备,电话直接打到你手机上。你硬着头皮从被窝爬起来,开车到厂里一看,就是加热管烧了&#xff0…

2026/10/10 18:43:24 阅读更多 →

最新新闻

麦语言函数手册实战:从零构建可复现的量化指标脚本

麦语言函数手册实战:从零构建可复现的量化指标脚本

简介:麦语言函数手册是文华财经推出的程序化函数库参考文档,面向使用文华平台进行量化策略开发的交易者与编程初学者,帮助其快速查阅函数用法、理解模型编写逻辑。手册系统梳理了自编策略模型支持的各类函数,涵盖历史数据引用&…

2026/10/14 6:57:33 阅读更多 →
高专院校机器视觉课程实践方案:从OpenCV到工业检测项目的落地指南

高专院校机器视觉课程实践方案:从OpenCV到工业检测项目的落地指南

第一次把这门课带上实训台的时候,说实话我心里也没底。机器视觉课程在本科院校都算硬骨头,在高专院校更是容易走样:讲理论学生听不懂,讲算法代码跟不上,买工业相机又怕吃灰。带着这些问题,我前后在几个年级…

2026/10/14 6:57:32 阅读更多 →
三菱PLC通讯协议那么多,到底该用哪个?

三菱PLC通讯协议那么多,到底该用哪个?

三菱 PLC 覆盖串口、以太网、现场总线、工业以太网四大类,特点是自有协议强(CCLink/MC/SLMP)、Modbus 全系标配、高端加 EtherCAT/OPC UA;和汇川相比,CCLink 生态更成熟,EtherCAT 起步稍晚。一、串口协议&a…

2026/10/14 6:57:32 阅读更多 →
纯视觉方案能否取代激光雷达?从BEV到占用网络的技术拆解

纯视觉方案能否取代激光雷达?从BEV到占用网络的技术拆解

最近圈内聊“纯视觉”聊得很凶,正好赶上某新势力品牌旗舰车型X-01交付,全车没有一颗激光雷达,只靠摄像头加神经网络就把城市领航辅助跑了起来,官方甚至直接把“纯视觉”三个字写进了宣传语。我自己的感知算法相关工作做了六七年&a…

2026/10/14 6:57:32 阅读更多 →
AnyPS5实战指南:从局域网串流到远程游玩的全场景方案

AnyPS5实战指南:从局域网串流到远程游玩的全场景方案

1. 项目概述:AnyPS5到底在解决什么问题先聊聊这个项目名。AnyPS5,字面拆开就是"Any"加"PS5"——任何一台PS5、任何一块屏幕、任何一张沙发、任何一段碎片时间,都能把它变成你的游戏现场。我最初接触到这个项目概念时&…

2026/10/14 6:57:32 阅读更多 →
Spring AI Alibaba停更后Java AI工程化重构指南

Spring AI Alibaba停更后Java AI工程化重构指南

1. 项目概述:当一个热门生态组件突然“静音”,Java开发者该盯住什么?“Spring AI Alibaba已停更了,Java还有希望吗?”——这句话最近在几个主流技术社区的讨论区反复刷屏,不是因为某个重大漏洞,…

2026/10/14 6:56:32 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

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

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

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

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

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

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

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

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

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

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 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/13 22:01:27 阅读更多 →
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 阅读更多 →