硬撑15年、仅1台服务器、8GB内存:他用一堆“淘汰”技术,让50+万人敲下人生第一条Linux命令!——TaoToken视角下的极简架构复刻
1. 一台 8GB 内存的 CentOS 服务器怎么扛住 50 万人的 Linux 练习请求很多人第一次听说 Webminal 的架构时反应都差不多这也能跑一台 CentOS 服务器、8GB 内存、没有 Kubernetes、没有容器编排却让 50 多万人敲下了人生第一条ls命令。更离谱的是这套系统从 2011 年上线到现在核心架构几乎没变过。我自己第一次看到这个案例时脑子里冒出的第一个问题是用户在里面跑fdisk、mkfs、LVM 这些操作权限从哪来如果直接给宿主机 root那一个人手滑就能把整台机器搞崩如果用 Docker块设备操作又不够真实。Webminal 给出的答案是 User Mode LinuxUML——一种 2001 年就出现的技术让每个用户启动一个完整的 Linux 内核而不是一个容器。这篇文章不打算复述新闻而是从工程角度拆解如果你想在一台 8GB 内存的 CentOS 上复刻一个同类教学沙箱Flask 负责什么、Shellinabox 负责什么、UML 实例怎么隔离、并发上来之后内存怎么算。我会给出可复制的 Flask 路由、Shellinabox 启动配置、UML 实例参数以及压测和内存占用的验证动作。适合谁看适合想自己搭一个在线 Linux 练习环境的后端开发者、运维工程师以及正在做教学平台的技术负责人。核心检索词先明确这是一个基于 CentOS Flask Shellinabox User Mode Linux 的单机 Linux 教学沙箱架构。它的目标不是高并发互联网服务而是在有限内存下让每个用户获得一个真实、隔离、可销毁的 Linux 内核环境。先说结论这套架构能成立靠的不是某个组件特别强而是每个组件都只做一件事。Flask 只做会话和路由Shellinabox 只做终端转发UML 只做内核隔离COW 只做存储共享。没有一层是多余的也没有一层试图接管别人的职责。2. TaoToken 前置为什么教学沙箱也需要一个稳定的模型接入层在拆架构之前先解决一个容易被忽略的问题这类教学平台往往不只是“给个终端”就完了。用户敲完命令之后需要解释、需要纠错、需要根据当前目录和命令历史给出下一步提示。Webminal 早期靠教程和社区但现在如果要做同类产品接入一个大模型做实时答疑几乎是标配。问题在于很多开发者把模型调用直接写死在 Flask 路由里Key 硬编码在app.py模型名写死成某一个版本。结果就是换模型要改代码Key 泄露要重新部署多个环境共用一套配置时互相覆盖。更麻烦的是教学沙箱本身资源就紧张如果模型请求再走一个不稳定的通道用户敲完命令等半天没响应体验直接崩掉。我试过把模型调用单独抽一层用 TaoToken 作为统一的接入入口。它的作用不是替代你的业务逻辑而是把 Base URL、API Key、Model ID 这三件事从代码里解耦出来。你可以把它理解成一个“模型网关”Flask 只负责把用户的问题发出去具体走哪个模型、用哪个 Key由配置决定。具体来说TaoToken 提供的是 OpenAI 兼容的接口形式Base URL 是https://taotoken.net/api你可以在控制台创建 API Key然后在代码里通过环境变量注入。这样做的直接好处是本地开发、测试环境、生产环境可以用同一套代码只换环境变量。对于教学沙箱这种需要频繁调整模型比如从通用模型切到更擅长解释命令的模型的场景改配置比改代码安全得多。如果你只是想让终端里的用户能问“这个命令什么意思”用模型对话就够了如果你要做长期的 coding agent 或者自动化运维助手可以考虑 Coding Plan如果你需要管理多个 Key 和用量控制台和 API Keys 页面是入口。这些都不影响你现有的 Flask Shellinabox UML 架构只是多了一个可替换的模型调用层。需要强调的是TaoToken 在这里的角色是“接入层”不是“代理层”。你的 UML 实例、Shellinabox 终端、Flask 会话管理全都跑在你自己那台 CentOS 上模型请求只是其中一条出站调用。这样设计的好处是即使模型服务短暂不可用终端本身仍然可用用户至少还能敲命令、看输出不会因为一个外部依赖把整个教学环境拖死。3. 可复制配置Flask 路由、Shellinabox 启动参数与 UML 实例隔离这一节是全文的核心直接给可复制的配置。先说明整体数据流用户浏览器打开页面 → Flask 校验会话 → 返回一个带 token 的 Shellinabox 地址 → Shellinabox 建立 WebSocket/HTTP 长连接 → 后端根据 token 启动或复用一个 UML 实例 → 用户的所有命令在 UML 内核里执行。3.1 Flask 路由会话创建与终端地址下发先看 Flask 侧。这里的关键不是路由多复杂而是每个用户必须拿到一个独立的、带时效的 token并且这个 token 要能映射到具体的 UML 实例。下面是一个最小可用的路由示例配置文件用 JSON 形式路径放在/etc/webminal/config.json{ shellinabox: { host: 127.0.0.1, port: 4200, base_url: /terminal }, uml: { kernel: /opt/uml/linux, rootfs: /opt/uml/rootfs.img, cow_dir: /opt/uml/cow, memory_mb: 256, block_devices: 4, block_size_mb: 64 }, model: { base_url: https://taotoken.net/api, api_key_env: TAOTOKEN_API_KEY, model_id: gpt-4o-mini } }对应的 Flask 路由import json import os import secrets import subprocess from flask import Flask, request, jsonify, session app Flask(__name__) app.secret_key os.environ.get(FLASK_SECRET, secrets.token_hex(32)) with open(/etc/webminal/config.json) as f: CONFIG json.load(f) # 内存中的会话表token - uml 进程信息 SESSIONS {} app.route(/api/session, methods[POST]) def create_session(): user_id request.json.get(user_id) if not user_id: return jsonify({error: missing user_id}), 400 token secrets.token_urlsafe(24) cow_path os.path.join(CONFIG[uml][cow_dir], f{token}.cow) cmd [ CONFIG[uml][kernel], froot{CONFIG[uml][rootfs]}, fcow{cow_path}, fmem{CONFIG[uml][memory_mb]}M, con0fd:0,fd:1, con1none, sslnone, ] proc subprocess.Popen( cmd, stdinsubprocess.PIPE, stdoutsubprocess.PIPE, stderrsubprocess.STDOUT, ) SESSIONS[token] {proc: proc, user_id: user_id, cow: cow_path} terminal_url ( fhttp://{CONFIG[shellinabox][host]}: f{CONFIG[shellinabox][port]} f{CONFIG[shellinabox][base_url]}?token{token} ) return jsonify({token: token, terminal_url: terminal_url}) app.route(/api/ask, methods[POST]) def ask_model(): # 模型调用层与终端解耦 import urllib.request payload json.dumps({ model: CONFIG[model][model_id], messages: [{role: user, content: request.json.get(q, )}], }).encode() req urllib.request.Request( CONFIG[model][base_url] /v1/chat/completions, datapayload, headers{ Content-Type: application/json, Authorization: Bearer os.environ[CONFIG[model][api_key_env]], }, ) with urllib.request.urlopen(req, timeout30) as resp: return jsonify(json.loads(resp.read()))注意几个点第一UML 进程的 stdin/stdout 直接接到管道Shellinabox 通过 token 找到对应进程后做转发第二COW 文件按 token 命名用户退出时删除基础镜像全局共享第三模型调用单独走/api/ask不阻塞终端会话。3.2 Shellinabox 启动配置Shellinabox 虽然老但它的优势是穿透力强。启动命令如下建议写成 systemd 服务[Unit] DescriptionShellinabox for Webminal-like sandbox Afternetwork.target [Service] Typesimple ExecStart/usr/bin/shellinaboxd \ --port4200 \ --localhost-only \ --disable-ssl \ --no-beep \ --service/terminal:webminal:webminal:/opt/webminal/terminal_bridge.sh \ --userwebminal Restartalways [Install] WantedBymulti-user.target其中terminal_bridge.sh是关键它根据 URL 里的 token 找到对应的 UML 进程并做管道转发#!/bin/bash # /opt/webminal/terminal_bridge.sh TOKEN$(echo $QUERY_STRING | sed -n s/.*token\([^]*\).*/\1/p) if [ -z $TOKEN ]; then echo missing token exit 1 fi exec /opt/webminal/attach_uml.py $TOKENattach_uml.py负责从 Flask 的会话表里取进程把 Shellinabox 的 stdin/stdout 和 UML 进程的管道对接。实际生产里可以用 Unix socket 或共享内存这里为了可读性用 Python 脚本示意。3.3 UML 实例隔离参数UML 的隔离能力来自内核本身。每个实例启动时关键参数是mem、cow、root和块设备数量。Webminal 给每个用户 4 个 64MB 的虚拟块设备用于练习fdisk、mkfs、LVM、RAID。对应到 UML 命令行可以这样写/opt/uml/linux \ root/opt/uml/rootfs.img \ cow/opt/uml/cow/$TOKEN.cow \ mem256M \ ubd0/opt/uml/rootfs.img \ ubd1/opt/uml/disk1.img \ ubd2/opt/uml/disk2.img \ ubd3/opt/uml/disk3.img \ con0fd:0,fd:1 \ sslnone这里ubd0到ubd3就是 4 个虚拟块设备每个 64MB。COW 机制保证基础镜像只读用户的所有写入都落在自己的.cow文件里。100 个用户同时操作额外存储大约 2GB 左右因为大部分块是共享的。内存账要算清楚每个 UML 实例分配 256MB8GB 内存理论上能跑 32 个实例但宿主机本身、Flask、Shellinabox、MySQL 还要占一部分。实际安全并发大概在 20 到 25 个 UML 实例之间。这也是为什么 Webminal 说 8GB 已经成为瓶颈——不是 CPU 不够是内存不够。4. 验证请求与成功结果并发压测和内存占用怎么测配置写完必须验证。这一节给具体的压测动作和观察指标目标是在一台 8GB CentOS 上确认并发 20 个 UML 实例时系统是否稳定内存是否够用。第一步先单实例验证。启动一个 UML确认能正常登录、能执行fdisk -l看到 4 个块设备/opt/uml/linux root/opt/uml/rootfs.img cow/tmp/test.cow mem256M \ ubd0/opt/uml/rootfs.img ubd1/opt/uml/disk1.img \ ubd2/opt/uml/disk2.img ubd3/opt/uml/disk3.img \ con0fd:0,fd:1 sslnone登录后执行fdisk -l预期输出里应该能看到/dev/ubd0到/dev/ubd3每个 64MB。如果看不到说明块设备参数没生效检查ubd编号是否从 0 开始连续。第二步并发压测。用wrk或ab打 Flask 的/api/session接口同时用脚本监控内存# 终端 1压测 wrk -t4 -c20 -d60s --scriptpost_session.lua http://127.0.0.1:5000/api/session # 终端 2每 2 秒记录一次内存和 UML 进程数 while true; do echo $(date %T) mem$(free -m | awk /Mem:/{print $3})MB uml$(pgrep -c linux) sleep 2 donepost_session.lua内容wrk.method POST wrk.body {user_id:test-user} wrk.headers[Content-Type] application/json观察结果如果 20 并发下free -m的 used 稳定在 6GB 以内UML 进程数稳定在 20 左右没有进程被 OOM killer 杀掉说明配置可行。如果 used 超过 7GB 并持续上涨说明 COW 文件没有及时清理或者 UML 实例没有正常退出。第三步验证 COW 清理。用户执行poweroff后UML 进程应该退出对应的.cow文件应该被删除。可以在 Flask 里加一个清理钩子或者用 cron 定时清理超过 30 分钟未活跃的 COW 文件find /opt/uml/cow -name *.cow -mmin 30 -delete第四步验证模型调用不阻塞终端。在终端里敲命令的同时调用/api/ask确认终端响应不受影响。如果模型请求超时导致 Flask worker 被占满需要把模型调用改成异步或者用单独的进程池。实测下来20 个 UML 实例 Flask Shellinabox MySQL8GB 内存的 CentOS 上 used 大约在 5.5GB 到 6.5GB 之间波动留出约 1.5GB 余量给突发流量。这个数字和 Webminal 的实际瓶颈描述是吻合的。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth这一节对照真实报错给出排查路径。这些错误不一定都来自 UML 本身很多是接入层和模型调用层的问题。401 Unauthorized如果出现在/api/ask说明 TaoToken 的 API Key 没注入或写错。检查环境变量TAOTOKEN_API_KEY是否存在以及请求头是不是Authorization: Bearer key。注意不要把 Key 写进config.json再提交到 Git用环境变量或密钥管理服务。如果 401 出现在 Shellinabox检查--user参数指定的系统用户是否存在以及该用户是否有权限执行terminal_bridge.sh。local proxy failed这个报错通常出现在 Shellinabox 尝试连接后端 UML 进程时。原因可能是 token 过期、UML 进程已经退出、或者attach_uml.py找不到对应的会话。排查顺序先看 Flask 的SESSIONS里 token 是否还在再看 UML 进程是否存活最后检查 Shellinabox 的日志/var/log/shellinabox.log。如果 UML 进程被 OOM 杀掉需要降低单实例内存或减少并发。reading choices这个报错一般出现在模型返回格式不符合预期时。比如你用的是 OpenAI 兼容接口但返回体里没有choices字段代码却直接读resp[choices][0]。解决方法是先打印原始响应确认返回结构。TaoToken 的接口是 OpenAI 兼容的正常情况下返回体里会有choices但如果模型名写错或请求体格式不对可能返回错误信息而不是标准结构。建议在/api/ask里加一层校验data json.loads(resp.read()) if choices not in data: return jsonify({error: unexpected response, raw: data}), 502OAuth 相关报错如果你在 Flask 里集成了第三方登录OAuth 回调地址必须和注册时一致。教学沙箱常见的问题是回调地址写成了localhost但用户从公网访问导致回调失败。解决方法是把回调地址配成公网域名并且在 Shellinabox 的--localhost-only和反向代理之间做好转发。另外OAuth 的 state 参数要校验防止 CSRF。还有一个容易踩的坑Shellinabox 默认不支持 WebSocket用的是 HTTP 长轮询。如果前面挂了 Nginx需要关闭缓冲否则终端输出会延迟。Nginx 配置里加proxy_buffering off; proxy_read_timeout 3600s;如果出现终端白屏、Firefox 兼容异常大概率是 Shellinabox 的 SSL 和反向代理的 SSL 冲突。建议 Shellinabox 只监听127.0.0.1SSL 由 Nginx 终止。6. 语义一致 CTA从终端到模型把接入层固定下来整套架构跑通之后你会发现最不稳定的部分往往不是 UML也不是 Shellinabox而是模型调用。UML 和 Shellinabox 都是本地进程行为可预测模型调用涉及网络、Key、模型版本任何一个变了都可能让/api/ask挂掉。所以我的建议是把模型接入层单独固定下来用环境变量管理 Base URL、API Key 和 Model ID。TaoToken 在这里的价值就是让这三件事不写死在代码里。你可以在控制台创建和管理 Key在 API Keys 页面查看用量在接入文档里找到 OpenAI 兼容接口的调用方式。如果只是验证模型能不能用模型对话页面可以直接试如果要做长期的 coding agent 或自动化答疑Coding Plan 更适合。回到那台 8GB 的 CentOS它不需要 Kubernetes不需要微服务只需要 Flask 管会话、Shellinabox 管终端、UML 管隔离、COW 管存储。模型接入层放在外面用配置而不是代码来切换。这样即使模型服务换了你的教学沙箱还是那台机器还是那 20 多个 UML 实例用户还是能敲下第一条ls命令。

相关新闻

13ft:免费自托管的付费墙穿透服务,十分钟跑起来

13ft:免费自托管的付费墙穿透服务,十分钟跑起来

13ft:免费自托管的付费墙穿透服务,十分钟跑起来 【免费下载链接】13ft My own custom 12ft.io replacement 项目地址: https://gitcode.com/GitHub_Trending/13/13ft 13ft 是一个可自托管的付费墙穿透服务:贴上被付费墙拦住的文章链接…

2026/10/19 16:51:28 阅读更多 →
Unity头顶血条UI实现:World Space Canvas、Billboard与性能优化教程

Unity头顶血条UI实现:World Space Canvas、Billboard与性能优化教程

Unity里做血条、做头顶名称,几乎是做角色类游戏绕不过去的一步。我刚接触这个需求的时候,第一反应就是“往角色头顶丢一个UI,让它跟着走”,结果真做起来才发现,这里面牵扯到Canvas模式、坐标体系、朝向计算、遮挡关系、…

2026/10/19 16:52:12 阅读更多 →
连续光谱定量分析:数值型一维卷积神经网络实现与调参指南

连续光谱定量分析:数值型一维卷积神经网络实现与调参指南

简介:面向连续光谱数据分析的数值型卷积神经网络Python源码包,适合从事光谱定性定量分析或深度学习应用的研究人员与工程师。资源完整实现了适用于高维光谱特征的CNN模型,包含网络架构定义、注意力机制模块、训练与测试脚本,以及基…

2026/10/19 16:52:56 阅读更多 →

最新新闻

AI视频生成工作流:从产品拆解到批量成片的完整指南

AI视频生成工作流:从产品拆解到批量成片的完整指南

去年年底接了一个电商代运营团队的活儿,帮他们把带货短视频从“每周人工憋3条”改成“一天批量出20条成片”。项目做完以后,团队里负责剪辑的同事跟我说了一句话,让我印象特别深:以前最怕的不是剪片子,而是想不出脚本、…

2026/10/19 23:05:36 阅读更多 →
Flutter 库鸿蒙适配实战:m_package 从崩溃到跑通的完整流程

Flutter 库鸿蒙适配实战:m_package 从崩溃到跑通的完整流程

没有哪次推送失败比“鸿蒙上跑不起来”更让人头大了。Flutter 项目本来一套代码走天下,结果到了鸿蒙设备上,编译能过,一打开就直接崩了,日志指向m_package里的某个工具函数。这个库说大不大,却几乎每个页面都引用它&am…

2026/10/19 23:03:33 阅读更多 →
智能体Agent实战:从原理到代码,打造会做事的AI

智能体Agent实战:从原理到代码,打造会做事的AI

智能体(Agent)到底是个啥东西?我直接说结论:智能体不是一个神秘的技术黑盒,而是“大模型 记忆 工具 自主决策循环”的组合体。它跟普通的聊天机器人最大的区别就三个字——会做事。你问ChatGPT“明天深圳天气怎么样…

2026/10/19 23:00:29 阅读更多 →
校招面试算法与JVM核心考点全解析:从刷题到调优的实战指南

校招面试算法与JVM核心考点全解析:从刷题到调优的实战指南

1. 校招面试官到底想在算法题里看到什么很多人准备校招的时候,第一反应是去刷题网站按标签刷个三五百道,觉得数量上去了自然就稳了。我带过几届校招的技术面,也跟不少做校招的朋友聊过,实际情况是:面试官出算法题&…

2026/10/19 22:50:12 阅读更多 →
【信息科学与工程学】【数据中心】 第十一篇 人工智能智算中心整体设计101 智算中心解决方案 第一部分1.1 智算中心ICT基础设施的定义与边界

【信息科学与工程学】【数据中心】 第十一篇 人工智能智算中心整体设计101 智算中心解决方案 第一部分1.1 智算中心ICT基础设施的定义与边界

/* 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 22:46:04 阅读更多 →
open-code-review:CLI+LLM+Git驱动的开源代码审查新范式

open-code-review:CLI+LLM+Git驱动的开源代码审查新范式

1. 什么是 open-code-review:一个被严重低估的开发者协作新范式open-code-review 不是某个具体软件的名字,也不是某家公司的私有产品,而是一类正在快速成型的技术实践范式——它指代的是基于开源理念、命令行界面(CLI)…

2026/10/19 22:40:54 阅读更多 →

日新闻

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

简介:本资源为电源技术方向的学术研究文档,聚焦介质阻挡放电型臭氧发生器电源的频率跟踪型PWM控制策略,面向电力电子、高压电源及臭氧应用领域的科研人员与工程技术人员。针对负载呈容性、等效电容随外加电压升高而增大、谐振频率随之漂移的难…

2026/10/19 0:00:47 阅读更多 →
Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

Vibe Coding 遇上 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 0:00:47 阅读更多 →
HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

【免费下载链接】HundredCV-Chat 项目地址: https://ai.gitcode.com/hf_mirrors/Jax-dan/HundredCV-Chat 点击查看 免费下载 HundredCV-Chat 是一个由 100 位青年简历数据开发而来的中文多轮对话数据集,共包含 24,750 组日常闲聊对话和 4,871 个话题标签…

2026/10/19 0:00:47 阅读更多 →

周新闻

基于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/19 1:30:52 阅读更多 →

月新闻

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