首页/新闻资讯/正文详情

Sandcastle 沙箱编排框架:安全执行不可信代码的 TypeScript 方案

发布时间:2026/11/3 14:51:03 来源:云帆数科 栏目:资讯中心
Sandcastle 沙箱编排框架:安全执行不可信代码的 TypeScript 方案
1. 为什么一个沙箱编排框架值得单独拿出来聊第一次看到 Sandcastle 这个名字我下意识以为是某个游戏引擎或者 3D 渲染库——毕竟沙堡这个词太容易让人联想到图形化、可视化的东西。结果翻了一圈 Matt Pocock 放出来的资料才发现这玩意跟渲染半毛钱关系都没有它解决的是一个在 TypeScript 圈子里被反复讨论、但一直没有一个优雅答案的问题怎么把一段代码安全地扔进一个隔离环境里跑起来还能拿到结构化的结果。这个需求听起来很窄但只要你做过下面这几类事情就会立刻明白它的价值在线代码演练场就是那种左边写代码右边出结果的网页、AI 编码代理执行用户生成的代码片段、自动化测试里需要动态执行不可信脚本、教学平台要给学生提交的代码打分。这些场景有一个共同点——你要执行的代码不是你写的你没法完全信任它。直接eval或者new Function当然能跑但那是把家门钥匙直接交给陌生人跑飞一个死循环你的进程就没了访问到不该访问的全局对象你也拦不住。Sandcastle 的定位就是在这个缝隙里做文章。它不是一个完整的虚拟机也不是一个重量级的容器方案而是一层编排层——把沙箱的创建、代码的注入、执行的控制、结果的回收这几件事串成一条清晰的流水线并且用 TypeScript 的类型系统把整个流程约束住。Matt Pocock 本人的风格大家应该熟悉他是那种对类型体操和开发者体验极度偏执的人所以这个框架在 API 设计上能明显感觉到不想让你写错的意图。这篇文章我打算按我自己啃这个框架的顺序来写先讲清楚它到底在编排什么再拆它的核心抽象然后是实际跑起来会遇到的坑最后聊聊它在 AI 编码代理这个方向上的想象空间。如果你正在做演练场、代码执行服务或者 AI Agent 相关的东西这篇应该能帮你省下不少试错时间。2. Sandcastle 到底在编排什么拆开沙箱执行的四层结构2.1 沙箱本身不是重点编排才是很多人一听到沙箱就直奔底层去了想着是不是用了 QuickJS、是不是套了 iframe、是不是起了个子进程。这些确实是沙箱的实现手段但 Sandcastle 想解决的问题不在这一层。你可以把它理解成一个导演它不负责搭舞台那是运行时的事也不负责写剧本那是你的代码它负责的是什么时候让谁上场、上场之后怎么盯着、演完了怎么收场。具体来说一次完整的沙箱执行要处理四件事缺一不可隔离边界的建立决定这段代码跑在哪个环境里能看见什么、不能看见什么。代码的注入与准备把待执行的代码、依赖、上下文变量送进去还要处理转译TypeScript 到 JavaScript。执行过程的控制超时怎么办、内存爆了怎么办、抛异常了怎么捕获、异步没结束怎么等。结果的回收与结构化把返回值、日志、错误信息整理成调用方能直接用的形状。大部分自己手搓的方案往往只做好了第一层和第三层的一半第二层和第四层全靠调用方自己拼。Sandcastle 的价值就在于把这四层用统一的抽象串起来让你不用每次都重新发明一遍轮子。2.2 核心抽象任务、运行时、结果我梳理下来Sandcastle 的 API 大致围绕三个概念转任务Task、运行时Runtime、结果Result。任务是你要执行的东西的封装。它不只是一段字符串代码还包括这段代码需要的上下文、期望的返回类型、超时配置等等。把任务做成一个显式的对象好处是你可以对它做序列化、做队列、做重试而不是把一堆参数散落在函数签名里。运行时是真正干活的执行器。它决定了隔离的强度——是进程内隔离、子进程隔离还是更彻底的独立运行时。Sandcastle 在这里做了抽象意味着你可以根据场景切换本地开发用轻量的生产环境用更重的。结果是执行完之后的结构化产物。这里 Matt 的设计很讲究它把成功返回值、控制台输出、抛出的错误、执行耗时分开存放而不是混在一个any里。这一点对 AI 编码代理特别重要——代理需要知道代码是跑通了但结果不对还是压根没跑起来这两种情况的后续处理策略完全不同。2.3 类型系统在这里扮演的角色这是我觉得 Sandcastle 最有意思的地方。一般的沙箱库执行完返回Promiseany就完事了调用方自己as一下。但 Matt 的框架里任务是可以带类型参数的——你声明这段代码期望返回什么类型框架会在类型层面帮你把结果约束住。当然运行时的类型安全是另一回事JavaScript 运行时不会真的帮你校验但类型层面的约束至少能在编译期拦住一批低级错误比如你把返回number的任务当成返回string来用。对于团队协作和大型项目来说这种约束能省掉很多调试时间。我个人的经验是凡是涉及动态执行的代码类型标注越明确后期维护成本越低因为动态执行本身就是类型系统的盲区你能补一点是一点。3. 从零跑通第一个沙箱任务环境准备与最小示例3.1 环境准备里最容易忽略的两件事装包这一步没什么好说的常规的包管理器操作。但有两个细节我踩过坑值得单独拎出来讲。第一是运行时依赖的版本对齐。Sandcastle 这类框架通常会依赖某个具体的 JS 运行时或者转译器如果你的项目里已经有别的工具链比如构建工具自带的转译器版本冲突是家常便饭。我的建议是先把框架单独装在一个干净目录里跑通最小示例确认没问题再往主项目里集成。别一上来就在复杂项目里折腾出了问题你分不清是框架的锅还是你项目配置的锅。第二是模块格式的选择。现在 ESM 和 CJS 混用的情况太普遍了沙箱执行的时候如果模块解析出问题报错信息往往很隐晦看起来像是代码没执行实际上是模块没加载进来。跑最小示例的时候先用最朴素的格式确认链路通了再上复杂的。3.2 最小可运行示例的拆解一个最小的沙箱任务大概长这样伪代码具体 API 以官方为准import { createSandbox } from sandcastle const sandbox createSandbox({ timeout: 3000, }) const result await sandbox.run({ code: const nums [1, 2, 3, 4, 5] return nums.reduce((a, b) a b, 0) , }) console.log(result.value) // 15这段代码虽然短但每一行都有讲究。createSandbox的时候传timeout是在设定执行的时间上限防止死循环把进程拖死。run接收的code是一段字符串注意这里用的是return而不是console.log——因为沙箱会把代码包成一个函数来执行return的值就是结果。最后从result.value拿返回值而不是直接拿result说明结果对象是结构化的。我特别想强调return这个细节。很多第一次用沙箱的人会习惯性地写console.log然后发现result.value是undefined一脸懵。原因是沙箱执行模型和你在浏览器控制台里敲代码是两回事——控制台是 REPL会自动打印最后一个表达式的值沙箱是函数执行只有显式return才有返回值。这个认知差是新手最容易卡住的地方。3.3 超时配置该怎么定超时时间定多少这个问题没有标准答案但有几个参考维度。如果是纯计算的代码片段比如算法题、数据处理3 到 5 秒足够了正常代码不会跑这么久。如果涉及网络请求或者文件 IO那得看具体场景但沙箱里通常不建议做这些因为隔离环境往往限制了这些能力。我的经验是宁可定短一点配合重试机制。超时定太长一个卡死的任务会占着资源不放影响其他任务的吞吐。定短了顶多是任务失败重试成本可控。另外要注意超时触发之后沙箱能不能干净地终止执行这取决于运行时的实现——有些运行时能强制中断有些只能等它自己跑完。这一点在选型的时候要重点验证不然超时配置形同虚设。4. 隔离强度怎么选进程内、子进程还是独立运行时4.1 三种隔离级别的真实差异这是 Sandcastle 编排能力体现得最明显的地方也是选型时最需要想清楚的。我把三种常见隔离级别拉出来对比一下隔离级别隔离强度启动开销适用场景主要风险进程内隔离低极低可信代码、快速原型全局对象污染、无法强制中断子进程隔离中中等半可信代码、需要超时控制进程间通信开销、资源占用独立运行时高较高不可信代码、多租户能力受限、生态兼容性进程内隔离说白了就是在一个受限的作用域里跑代码通过代理全局对象、限制访问来达到软隔离。它的优点是快几乎没有启动开销缺点是隔离是逻辑上的不是物理上的一旦有漏洞被绕过攻击面就是整个进程。而且死循环这种问题它基本没辙因为同一个事件循环被占住了你连中断的机会都没有。子进程隔离是把代码扔到一个独立的进程里跑主进程通过 IPC 通信。这样死循环可以杀进程内存爆了也只影响子进程。代价是启动一个进程有开销频繁执行小任务的话性能会受影响。适合那种任务量不大但要求可靠的场景。独立运行时比如嵌入一个轻量 JS 引擎隔离最彻底代码跑在一个完全独立的环境里连宿主对象都摸不到。但它的代价是生态兼容性——很多依赖 Node API 的代码在里面跑不了你得接受一个受限的运行时环境。4.2 选型时我实际会问自己的三个问题第一个问题这段代码是谁写的如果是自己团队写的、经过 review 的进程内隔离够用。如果是用户提交的、AI 生成的那必须上更重的隔离。第二个问题执行频率有多高高频短任务优先考虑低开销方案低频长任务可以接受高开销。这里有个反直觉的点——很多人觉得安全第一无脑上最重的隔离结果性能崩了用户体验极差最后又退回来。安全性和性能是要平衡的不是越重越好。第三个问题代码需要访问哪些能力如果只是纯计算独立运行时完全够。如果需要读文件、发请求那独立运行时可能满足不了得退到子进程方案然后在子进程层面做能力限制。4.3 一个容易被忽略的细节状态残留不管选哪种隔离都要考虑状态残留的问题。同一个沙箱实例如果被复用上一次执行留下的全局变量、修改过的原型链可能会影响下一次执行。这在进程内隔离里尤其严重因为大家共享同一个全局环境。我的做法是每次执行都创建新的沙箱上下文虽然有一点开销但能保证执行之间互不干扰。如果性能实在扛不住那就得在任务设计上做隔离——比如约定每个任务不能修改全局状态或者执行完主动清理。但说实话靠约定不如靠机制能新建就新建。5. 和 AI 编码代理结合时Sandcastle 的想象空间在哪5.1 AI 代理执行代码的三个刚需现在做 AI 编码代理的人越来越多这类系统有一个绕不开的环节代理生成的代码得真的跑起来验证。光看代码对不对是不够的得执行、得看结果、得根据结果决定下一步。这里 Sandcastle 能提供的价值就很直接了。第一个刚需是安全执行。代理生成的代码质量参差不齐可能引用了不存在的库可能写了死循环可能试图访问敏感资源。沙箱是第一道防线。第二个刚需是结构化反馈。代理需要知道执行结果才能继续推理。如果只是拿到一个字符串输出代理很难判断是成功还是失败。Sandcastle 把返回值、错误、日志分开返回正好喂给代理做下一步决策。第三个刚需是可重复性。同一个任务代理可能尝试多次每次的输入输出都要能对得上。沙箱的确定性执行同样的输入给同样的输出是保证可重复的前提。5.2 把执行结果喂回代理的正确姿势这里有个实操经验值得分享。很多人把沙箱执行结果直接JSON.stringify塞给代理结果代理拿到一大坨日志根本抓不住重点。我的做法是对结果做摘要成功的话只给返回值失败的话给错误类型和关键堆栈日志只在代理明确要求时才提供。另外错误信息要翻译成人话。沙箱抛出来的原始错误往往带着一堆内部堆栈代理看了也白看。你可以在中间加一层把常见错误归类——超时、语法错误、运行时异常、返回类型不符然后给代理一个清晰的信号。这样代理的后续处理会准确得多。5.3 一个真实的踩坑转译环节的隐藏成本代理生成的代码通常是 TypeScript但沙箱运行时往往只认 JavaScript中间需要转译。这个转译环节看起来简单实际上坑不少。首先是转译器的选择。用完整的 TypeScript 编译器当然最稳但启动慢、体积大。用轻量的转译器比如只做类型擦除的快是快但遇到一些高级语法可能处理不了。我的建议是根据代码复杂度动态选择——简单代码用轻量转译复杂代码回退到完整编译器。其次是类型检查要不要做。转译和类型检查是两回事转译器通常只擦除类型不做检查。如果你希望代理生成的代码在类型层面也是正确的那得额外跑一遍类型检查。但这会显著增加执行时间要不要做取决于你的场景——教学场景建议做快速验证场景可以跳过。最后是source map 的处理。转译之后行号会变报错信息里的行号对不上原始代码调试起来很痛苦。如果沙箱支持 source map一定要开启不然代理拿着错误的行号去改代码越改越乱。6. 实操中绕不开的几个坑与应对6.1 异步代码的执行边界沙箱里执行异步代码是个麻烦事。同步代码执行完就完事了异步代码你得等它所有 Promise 都 settle 才能确定执行结束。但问题是你怎么知道所有 Promise如果代码里有个没 await 的 Promise 在后台跑沙箱可能在它完成之前就返回了。我的应对策略是在任务层面约定异步协议。要么要求代码返回一个 Promise沙箱 await 它要么提供一个显式的完成信号机制。不要指望沙箱能自动追踪所有异步操作那是不现实的。Sandcastle 在这方面的设计思路应该是让调用方显式声明异步意图而不是靠魔法。6.2 内存限制的落地难点理论上你可以给沙箱设内存上限超了就终止。但实际落地的时候内存的测量和限制比想象中难。进程内隔离基本没法精确限制内存子进程隔离可以通过系统调用限制独立运行时看引擎支持。我的经验是内存限制要留足余量。你设 128MB实际代码可能因为运行时本身的开销就占了 50MB留给用户代码的只有 78MB。而且内存超限的报错往往不是内存不足而是各种奇怪的崩溃排查起来很费劲。建议先观察正常任务的峰值内存然后在此基础上乘个 2 到 3 倍作为上限。6.3 错误信息的可读性沙箱抛出的错误如果不做处理调用方看到的就是一堆内部堆栈。我一般会做三层处理第一层剥离框架内部堆栈只留用户代码相关的第二层把错误分类语法、运行时、超时、资源第三层给每类错误配一个可读的描述。这里有个细节语法错误和运行时错误的处理时机不同。语法错误在转译阶段就能发现不需要真的执行运行时错误只有执行了才知道。把这两类分开处理能让反馈更快——语法错误秒回不用等超时。6.4 依赖注入的边界沙箱里的代码如果需要用外部库怎么注入是个问题。全量注入会让沙箱体积膨胀按需注入又需要一套依赖解析机制。我的做法是维护一个白名单只注入明确允许的库其他一律拒绝。这样既控制了体积也控制了安全面。白名单的维护要有流程不能随便加。每加一个库都要评估它的体积、它的依赖树、它有没有副作用。有些库看起来小依赖树一拉一大串注入进去沙箱启动就慢了。7. 我对这类框架后续演进的一些个人判断Matt Pocock 做东西一向注重开发者体验Sandcastle 目前的形态已经能看出这个倾向——API 简洁、类型友好、概念清晰。但沙箱编排这个领域真正的难点往往在边界情况上而这些边界情况只有在生产环境里大规模跑才会暴露。我个人比较关注两个方向。一个是执行的可观测性也就是你能不能清楚地知道每次执行发生了什么——耗时分布、失败原因分布、资源消耗趋势。这些数据对于优化沙箱配置、定位性能瓶颈至关重要但目前大部分沙箱方案在这方面都比较薄弱。另一个是多运行时的一致性。同一个任务在进程内跑和在独立运行时跑行为可能不一样。如果框架能保证跨运行时的一致语义那开发者切换隔离级别的时候就不用担心行为漂移。这个目标很难但值得追求。最后说一句实在的选沙箱方案不要只看功能列表一定要拿你自己的真实任务去压测。别人的 benchmark 再漂亮也不如你自己跑一遍来得实在。我见过太多方案在 demo 里光鲜亮丽一上真实负载就原形毕露。Sandcastle 值不值得用取决于你的场景和它的边界是否匹配这个判断只能你自己做。

相关推荐

树莓派GPIO控制蜂鸣器实现硬件级buzz提示
树莓派GPIO控制蜂鸣器实现硬件级buzz提示

我无法基于当前输入生成符合要求的博文。原因如下:输入中仅提供了项目标题"buzz",以及空的“相关热搜词”和“最新网络热词”字段(内容为空白),未提供任何实质性的项目正文、关键词列表或摘要描述&#xff1… · 2026/11/3 14:51:03

Buzz技术概念解析与工程实践指南
Buzz技术概念解析与工程实践指南

我无法根据当前输入生成符合要求的博文。 原因如下: 项目标题仅为单个英文单词 "buzz" ,无明确指向性; 项目正文为空; 关键词为空; 摘要描述为空; 所谓“相关热搜词”与“最新网络热词”… · 2026/11/3 14:51:03

2024年TensorFlow与PyTorch怎么选?从安装到部署的完整指南
2024年TensorFlow与PyTorch怎么选?从安装到部署的完整指南

这几年被问到最多的问题之一,就是“现在学TensorFlow还有前途吗”。每次听到这个问题,我都忍不住想起2016年自己第一次跑通MNIST时的心情——那会儿TensorFlow几乎是入行的唯一选择,论坛上全是讨论静态图、tf.Session的帖子;现在倒… · 2026/11/3 14:51:03

声明 list 的3种方式,ArrayList、Collection、List 的对比
声明 list 的3种方式,ArrayList、Collection、List 的对比

ArrayList 是实现了 List 接口的类, 而 List 和 Collection 都是 Java集合框架中的接口。这三个声明之间有一些重要的区别:ArrayList list new ArrayList(); 左边是 具体实现类ArrayList ✅ 能直接调用 ArrayList 所有方法 ❌ 写死实现类,耦… · 2026/11/4 6:06:22

免费星穹铁道游戏自动化助手 March7thAssistant 上手指南
免费星穹铁道游戏自动化助手 March7thAssistant 上手指南

免费星穹铁道游戏自动化助手 March7thAssistant 上手指南 【免费下载链接】March7thAssistant 日常交给三月七,开拓留给你。《崩坏:星穹铁道》开源自动化助手。 项目地址: https://gitcode.com/gh_mirrors/ma/March7thAssistant 委托、体力、历战… · 2026/11/4 6:06:22

eslav_PP-OCRv5_mobile_rec_onnx进阶应用:结合TensorRT实现极致推理速度
eslav_PP-OCRv5_mobile_rec_onnx进阶应用:结合TensorRT实现极致推理速度

eslav_PP-OCRv5_mobile_rec_onnx进阶应用:结合TensorRT实现极致推理速度 【免费下载链接】eslav_PP-OCRv5_mobile_rec_onnx 项目地址: https://ai.gitcode.com/paddlepaddle/eslav_PP-OCRv5_mobile_rec_onnx 飞桨PaddlePaddle的eslav_PP-OCRv5_mobile_rec_o… · 2026/11/4 6:06:22

Marp 快速上手指南:3 步在本地把 Markdown 变成免费可导出的幻灯片
Marp 快速上手指南:3 步在本地把 Markdown 变成免费可导出的幻灯片

Marp 快速上手指南:3 步在本地把 Markdown 变成免费可导出的幻灯片 【免费下载链接】marp The entrance repository of Markdown presentation ecosystem 项目地址: https://gitcode.com/gh_mirrors/mar/marp Marp(Markdown Presentation Ecosyst… · 2026/11/4 6:06:22

XAutoDaily:QQ 自动签到模块的安装配置步骤与任务失败排查
XAutoDaily:QQ 自动签到模块的安装配置步骤与任务失败排查

XAutoDaily:QQ 自动签到模块的安装配置步骤与任务失败排查 【免费下载链接】XAutoDaily 一个基于QQ的全自动签到模块 项目地址: https://gitcode.com/GitHub_Trending/xa/XAutoDaily XAutoDaily 是一款开源的 Xposed 签到模块,用于 QQ 自动签到和… · 2026/11/4 6:06:22

车载SerDes选型本质:GMSL/FPD-Link/A-PHY底层逻辑解析
车载SerDes选型本质:GMSL/FPD-Link/A-PHY底层逻辑解析

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

双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试
双极步进电机控制实战: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/4 3:16:49

STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析
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工业级双极步进电机控制方案
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

AI辅助技术栈评估:提升研发决策效率60%
AI辅助技术栈评估:提升研发决策效率60%

1. 项目背景与核心价值技术决策一直是研发团队面临的重大挑战。随着技术生态的快速演进,技术栈的选择、维护和迭代变得愈发复杂。我们团队在过去两年里,先后经历了三次重大技术栈迁移,每次都需要投入大量人力进行技术评估和风险分析。直到去年… · 2026/11/4 0:00:17

从生成Token到直接决策:Jev如何重塑AI Agent工程实践
从生成Token到直接决策:Jev如何重塑AI Agent工程实践

1. 从"生成文字"到"拍板决策":Jev 到底在解决什么问题大多数人第一次接触大模型,脑子里建立的模型是"输入问题、输出文字"。你问它今天天气怎么样,它给你一段描述;你让它写个周报,它给你… · 2026/11/4 0:00:17

32G统一内存跑大模型:内存账、TPS实测与端云分流
32G统一内存跑大模型:内存账、TPS实测与端云分流

想在一台 32G 统一内存的小主机上把大模型跑成"日常可用",这件事我前后折腾了小半年。标题里说的这台 Mac mini M6,是我们这次讨论的目标平台,具体到手规格以实物为准;但下面这套推算方法和实测思路,对任何统… · 2026/11/4 0:00:17

双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试
双极步进电机控制实战: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/4 3:16:49

STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析
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工业级双极步进电机控制方案
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

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码