OpenSearch Docker部署密码修改指南:从安全初始化到运维实践
1. 项目概述与核心需求解析最近在帮一个朋友部署一套内部知识库系统后端选型用到了Opensearch。部署过程很顺利Docker Compose一把梭服务很快就跑起来了。但紧接着就遇到了一个几乎所有新手都会踩的坑默认的管理员密码是什么怎么改这看似简单的问题背后其实涉及到Opensearch的安全初始化机制、配置文件管理以及后续的运维安全基线。如果你也在用Docker部署Opensearch或者从官网下载的tar包安装然后对着登录界面一脸茫然那么这篇从实战中总结出来的密码修改指南就是为你准备的。它不仅会告诉你怎么改密码更会解释清楚Opensearch的安全模型以及如何避免把自己“锁在门外”。Opensearch作为Elasticsearch的一个开源分支继承了其强大的搜索和分析能力同时也特别强调了安全性。从1.0.0版本开始它就默认启用了安全插件Security Plugin这意味着首次启动时系统会为内置用户如admin生成一个随机的初始密码。这个设计初衷是好的强制用户从第一步就关注安全。但如果你没有留意启动日志或者用Docker部署时日志被淹没这个随机密码就像一把不知道藏在哪里的钥匙让你无法进入自己搭建的系统。我们的目标就是找到这把钥匙并换成自己熟悉的锁。2. Opensearch安全模型与密码初始化机制要修改密码首先得理解Opensearch如何处理初始安全凭据。这与它的启动方式紧密相关。2.1 首次启动的“安全引导”过程当你第一次启动一个全新的Opensearch节点或集群且未提供任何预先配置的安全凭据时它会自动进入“安全引导”Security Bootstrap流程。这个流程的核心是生成并存储一系列内置用户的初始密码。这些内置用户包括admin: 拥有所有权限的超级管理员。kibanaserver: 供Kibana或OpenSearch Dashboards连接Opensearch使用的专用账户。logstash: 用于Logstash写入数据。等等。这些密码是高强度随机生成的并且会以散列值的形式保存在Opensearch索引通常是.opendistro_security和本地配置文件目录中。对于Docker部署一个关键行为是如果用于存储安全配置的卷volume或绑定挂载bind mount目录是空的那么容器首次启动时就会触发这个引导过程并生成密码。2.2 密码的存储位置与获取方式随机密码生成后会输出到两个地方标准输出/日志文件这是最直接的获取方式。在终端或Docker日志中你会看到类似下面的信息[INFO ] Generated a random password for user admin. [INFO ] Generated a random password for user kibanaserver. [INFO ] Password for user admin is: XXXXXXXXXXXXXXX [INFO ] Password for user kibanaserver is: XXXXXXXXXXXXXXX注意在Docker Compose或Daemon模式下这些日志可能不会停留在屏幕上需要你用docker logs container_name命令去查看。容器内部文件密码也会被写入容器内的一个特定文件。对于官方的Opensearch Docker镜像这个文件路径通常是/usr/share/opensearch/config/opensearch-security/initial_admin_password。你可以通过进入容器来获取它# 进入正在运行的Opensearch容器 docker exec -it your_opensearch_container_name bash # 查看初始密码 cat /usr/share/opensearch/config/opensearch-security/initial_admin_password这个文件通常只包含admin用户的密码。重要提示这个initial_admin_password文件在密码被成功修改后可能会被自动删除取决于版本和配置。因此它只是一个临时的、用于首次登录的凭据。2.3 为什么需要修改默认密码原因不言而喻安全合规使用随机、复杂且只有你知道的密码是安全运维的基本要求。初始随机密码虽然复杂但一旦泄露比如日志被不当公开风险极高。便于管理一个你自己设定、易于记忆但依然强壮或妥善保管的密码远比一个需要从日志里翻找的随机字符串更利于日常运维。自动化脚本集成在CI/CD或自动化部署脚本中使用一个已知的、可控的密码远比解析动态生成的日志要可靠得多。3. 修改管理员密码的三种核心方法根据你的部署环境和运维习惯可以选择以下最适合你的方法。我将以修改admin用户密码为例其他内置用户如kibanaserver的修改方法完全一致。3.1 方法一通过Opensearch API最推荐、最通用这是最标准、适用于所有部署方式Docker、裸机、K8s的方法。前提是你能以某种方式使用初始密码登录。步骤详解获取初始密码通过上文提到的docker logs或进入容器查看文件的方式找到admin的初始密码。假设我们获取到的密码是MyInitialRandomPass123!。构造API请求Opensearch提供了专用的_plugins/_security/api/account端点来修改当前登录用户的密码。我们需要使用HTTP PUT请求。端点https://opensearch-host:port/_plugins/_security/api/account认证使用初始密码进行Basic Auth认证。请求体包含当前密码和新密码。使用cURL命令执行打开终端执行以下命令。请替换{...}中的内容为你自己的信息。curl -X PUT https://localhost:9200/_plugins/_security/api/account \ -H Content-Type: application/json \ -u admin:MyInitialRandomPass123! \ -k \ --data { current_password: MyInitialRandomPass123!, password: YourNewStrongPassword2024 }参数拆解与注意事项-u admin:MyInitialRandomPass123!: 这是Basic认证参数格式为用户名:密码。这是你使用初始密码进行登录的方式。-k: 这个参数非常重要。它告诉cURL跳过SSL证书验证。因为Opensearch默认使用自签名证书如果不加-kcURL会因证书不受信任而拒绝连接。在生产环境中你应该配置并使用受信任的证书并移除-k参数。--data: 里面是JSON格式的请求体。current_password必须与你通过-u参数提供的密码一致password是你的新密码。如果你的Opensearch监听的不是9200端口或者需要通过IP访问请相应修改URL。解读响应如果成功你会收到一个JSON响应{message:Password updated}这意味着密码已经修改成功。现在你可以立即使用新密码YourNewStrongPassword2024通过Kibana/OpenSearch Dashboards或API登录了。实操心得在写自动化脚本时最好在修改密码后立即用新密码尝试一个简单的API调用如GET /来验证修改是否生效避免后续步骤因认证失败而中断。3.2 方法二通过OpenSearch Dashboards界面最直观如果你已经部署了OpenSearch Dashboards原Kibana并且能通过初始密码登录进去那么图形化界面是最简单的修改方式。登录Dashboards使用https://dashboards-host:port访问用户名为admin密码为初始随机密码。进入安全设置在左侧导航栏找到并点击Security安全模块。选择内部用户在Security页面进入Internal Users内部用户选项卡。修改用户密码在用户列表中找到admin用户点击其右侧的Edit编辑或Actions菜单下的对应选项。在编辑界面你会看到修改密码的字段。注意这里通常需要你输入当前密码Current Password和新密码New Password/Confirm New Password。填写后保存即可。重新登录修改成功后当前会话可能会失效需要你使用新密码重新登录Dashboards。图形化界面的优势与局限优势无需记忆命令操作直观尤其适合不常与命令行打交道的运维人员。局限你必须先能访问Dashboards。有时因为网络策略或反向代理配置问题可能无法直接访问。3.3 方法三通过初始化配置文件适用于自动化部署如果你希望在部署之初就设定好密码而不是启动后再修改可以通过预置配置文件来实现。这常用于Docker Compose或K8s的自动化部署场景。其核心原理是在Opensearch容器首次启动前就将包含已哈希密码的安全配置文件internal_users.yml注入到容器内的配置目录中从而跳过随机密码生成阶段。步骤详解生成密码哈希首先你需要为新密码生成Opensearch安全插件认可的BCrypt哈希值。官方提供了hash.sh脚本。# 进入Opensearch容器的工具目录假设你已经将本地目录挂载或能访问容器 # 或者如果你有本地安装的Opensearch可以使用其脚本 cd /path/to/opensearch/plugins/opensearch-security/tools ./hash.sh -p YourNewStrongPassword2024执行后脚本会输出一串以$2y$...开头的BCrypt哈希值。复制这个哈希值。准备internal_users.yml文件创建一个名为internal_users.yml的文件内容如下_meta: type: internalusers config_version: 2 admin: hash: $2y$12$YourGeneratedHashValueHere1234567890ABCDEFGHIJKLMNOPQRSTUV reserved: true backend_roles: - admin description: Admin user with all permissions将hash:后面的值替换为你上一步生成的哈希值。在Docker Compose中挂载配置文件修改你的docker-compose.yml将准备好的internal_users.yml文件挂载到容器内的安全配置目录。version: 3 services: opensearch: image: opensearchproject/opensearch:latest container_name: opensearch environment: - discovery.typesingle-node - bootstrap.memory_locktrue volumes: - opensearch-data:/usr/share/opensearch/data - ./your_custom_config/opensearch-security/internal_users.yml:/usr/share/opensearch/config/opensearch-security/internal_users.yml ports: - 9200:9200 - 9600:9600 networks: - opensearch-net volumes: opensearch-data: networks: opensearch-net:关键点我们通过volumes将本地的internal_users.yml覆盖了容器内默认的或空的配置文件。启动服务运行docker-compose up -d。这次启动Opensearch会检测到internal_users.yml已存在且有效从而直接使用其中定义的哈希密码而不会生成随机密码。你可以直接用admin和YourNewStrongPassword2024登录。注意事项这种方法要求你对Opensearch的安全配置文件结构有一定了解。务必确保internal_users.yml的语法和缩进正确否则可能导致安全插件加载失败进而使整个Opensearch节点无法启动。建议先在测试环境验证。4. 基于Docker Compose部署的完整实操流程让我们以一个最常见的场景——使用Docker Compose单节点部署Opensearch和OpenSearch Dashboards为例从头走一遍密码修改的完整流程。4.1 编写docker-compose.yml创建一个项目目录并在其中创建docker-compose.yml文件。version: 3 services: opensearch: image: opensearchproject/opensearch:2.11.0 container_name: my-opensearch environment: - discovery.typesingle-node - bootstrap.memory_locktrue - OPENSEARCH_JAVA_OPTS-Xms512m -Xmx512m - DISABLE_SECURITY_PLUGINfalse ulimits: memlock: soft: -1 hard: -1 volumes: - opensearch-data:/usr/share/opensearch/data ports: - 9200:9200 - 9600:9600 networks: - opensearch-net dashboards: image: opensearchproject/opensearch-dashboards:2.11.0 container_name: my-dashboards ports: - 5601:5601 environment: - OPENSEARCH_HOSTS[https://opensearch:9200] - DISABLE_SECURITY_DASHBOARDS_PLUGINfalse networks: - opensearch-net volumes: opensearch-data: networks: opensearch-net: driver: bridge4.2 启动服务并捕获初始密码在终端中进入该目录启动服务docker-compose up -d查看Opensearch容器的日志寻找初始密码docker logs my-opensearch | grep -A2 -B2 password for user admin或者直接查看容器内的密码文件docker exec my-opensearch cat /usr/share/opensearch/config/opensearch-security/initial_admin_password假设我们得到密码J8KqL!2s9mZ#pW。4.3 使用API修改密码现在我们使用方法一的cURL命令来修改密码。新密码我们设定为MySecureOpensearchPass123!。curl -X PUT https://localhost:9200/_plugins/_security/api/account \ -H Content-Type: application/json \ -u admin:J8K\qL\!2s\9mZ\#pW \ -k \ --data { current_password: J8KqL!2s9mZ#pW, password: MySecureOpensearchPass123! }注意由于初始密码中包含Shell特殊字符,!,,#在-u参数中必须使用反斜杠\进行转义否则命令会解析错误。在JSON的current_password字段中这些字符在字符串内是安全的无需转义。4.4 验证新密码并登录Dashboards验证API访问使用新密码调用一个简单API验证修改成功。curl -k -u admin:MySecureOpensearchPass123! https://localhost:9200/你应该能看到包含version、name等信息的Opensearch欢迎JSON。登录OpenSearch Dashboards打开浏览器访问http://localhost:5601。用户名输入admin密码输入MySecureOpensearchPass123!。成功登录后你就进入了Dashboards的管理界面。至此你已经完成了从部署到安全登录的全过程。记得将新密码妥善保存到密码管理器中。5. 常见问题排查与进阶技巧在实际操作中你可能会遇到一些意外情况。下面是我总结的几个典型问题及其解决方案。5.1 问题一忘记了初始密码也找不到日志和文件怎么办这是最令人头疼的情况。别慌还有“终极手段”——重新初始化安全配置。但这会重置所有安全设置用户、角色、权限等请谨慎操作并确保你有备份或这是在测试环境。解决方案停止Opensearch容器。删除安全配置索引和本地文件。对于Docker这意味着删除对应的数据卷或挂载目录中的安全配置相关文件。最彻底的方法是删除整个数据卷警告这将丢失所有数据。# 找到你的数据卷名 docker volume ls # 删除卷 (例如卷名为 projectname_opensearch-data) docker volume rm projectname_opensearch-data对于Docker Compose你也可以直接删除整个项目目录下的volumes对应的本地目录如果你用的是本地路径挂载或者运行docker-compose down -v-v参数会删除Compose文件中声明的匿名卷。重新启动容器。由于安全配置存储被清空Opensearch会再次执行安全引导生成新的随机密码。这次请务必记下日志中的密码。避坑技巧养成好习惯在第一次启动容器后立即执行docker logs container_name | grep -i password admin_password.txt将密码保存到本地文件。5.2 问题二修改密码时遇到“invalid_password_exception”可能的原因和解决步骤当前密码错误仔细核对-u参数和JSON体中的current_password是否与初始密码完全一致注意大小写和特殊字符转义。新密码不符合策略Opensearch默认有密码强度策略。确保新密码足够复杂通常要求长度、大小写字母、数字、特殊字符的组合。如果确实需要弱密码仅限测试环境需要在opensearch.yml中修改策略但这极不推荐。用户不存在或已被禁用确认你修改的用户如admin确实存在。虽然admin是内置用户但在极端配置下可能被改动。5.3 问题三如何修改其他内置用户如kibanaserver的密码kibanaserver用户是Dashboards连接Opensearch的凭证。修改它的密码需要两步在Opensearch中修改kibanaserver用户的密码。你需要使用admin权限。可以通过Dashboards的Security界面修改或者使用API需要admin认证curl -X PUT https://localhost:9200/_plugins/_security/api/internalusers/kibanaserver \ -H Content-Type: application/json \ -u admin:MySecureOpensearchPass123! \ -k \ --data { password: NewKibanaServerPass456! }注意端点变成了/internalusers/kibanaserver且无需提供当前密码。在OpenSearch Dashboards的配置中更新密码。修改Dashboards的配置文件opensearch_dashboards.yml对于Docker通常通过环境变量或挂载自定义配置opensearch.username: kibanaserver opensearch.password: NewKibanaServerPass456!对于Docker Compose可以在dashboards服务的environment部分添加environment: - OPENSEARCH_USERNAMEkibanaserver - OPENSEARCH_PASSWORDNewKibanaServerPass456!然后重启Dashboards容器。5.4 进阶技巧在CI/CD流水线中自动化处理密码对于需要频繁部署测试环境的团队手动处理密码是不可接受的。可以结合方法三配置文件预置和Secrets管理工具如HashiCorp Vault, AWS Secrets Manager来实现自动化。在构建阶段生成哈希在你的CI脚本如GitLab CI、Jenkins Pipeline中从一个安全的源获取或生成管理员密码然后调用hash.sh脚本可以事先将脚本放入构建环境生成哈希。动态生成internal_users.yml使用模板引擎如Jinja2, envsubst将哈希值注入到internal_users.yml模板中生成最终的配置文件。在部署时挂载将动态生成的配置文件作为ConfigMapK8s或直接写入镜像的特定层在容器启动时使用。密码本身的管理绝对不要将明文密码或哈希值硬编码在代码或镜像中。使用CI/CD系统的Secret变量或专业的Secrets管理服务来传递密码。这个过程确保了从密码生成、配置注入到服务启动的全流程自动化与安全化是云原生时代运维Opensearch这类中间件的标准做法。

相关新闻

从零搭建Minecraft BedWars服务器:核心机制、插件配置与性能调优全指南

从零搭建Minecraft BedWars服务器:核心机制、插件配置与性能调优全指南

在 Minecraft 服务器生态中,BedWars(起床战争)是经久不衰的经典玩法之一,其核心在于团队合作、资源争夺与策略攻防。对于服务器运营者而言,一个稳定、功能丰富且易于管理的 BedWars 插件是吸引和留住玩家的关键。The H…

2026/10/11 1:27:04 阅读更多 →
09.本地提交如何安全地同步到远程仓库

09.本地提交如何安全地同步到远程仓库

本地提交如何安全地同步到远程仓库 Git 是分布式版本控制系统,本地提交不依赖服务器。团队协作时,大家通常约定一个远程仓库交换提交,但远程仓库不会自动与本地保持同步。 一套可控的同步流程应该回答四个问题: 当前仓库连接了…

2026/9/26 21:12:58 阅读更多 →
数学奇点全解析:从函数失效到系统平衡,理解技术中的临界点

数学奇点全解析:从函数失效到系统平衡,理解技术中的临界点

1. 从“奇点”的日常误解谈起最近在几个技术社区和跨领域交流群里,经常看到“奇点”这个词被频繁提及。有人用它来形容某个技术突破的临界点,比如“AI奇点即将到来”;有人用它来指代一个复杂系统中无法处理的特殊状态;甚至在一些出…

2026/10/10 13:49:47 阅读更多 →

最新新闻

面向对象程序设计-第一阶段补充-师生友谊

面向对象程序设计-第一阶段补充-师生友谊

作者 GONG单位 哈尔滨华德学院以下程序分别定义了学生类和教师类,学生有N个课程成绩,可以打印成绩单,任课教师负责为学生填报相应课程的成绩。类的声明已经给出,请按各类成员函数的功能说明完成相应的完整定义。并满足主函数的测试…

2026/10/11 1:27:28 阅读更多 →
环形补给站算法:从前缀和到单调栈的线性优化实战

环形补给站算法:从前缀和到单调栈的线性优化实战

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

2026/10/11 1:27:28 阅读更多 →
AI翻译神器高效助力多语言沟通 精准便捷满足各类翻译需求

AI翻译神器高效助力多语言沟通 精准便捷满足各类翻译需求

构建一个高质量的国外参考文献库,听起来很宏大,但其实就是把“找、管、用”这三件事做对。整个过程最关键的一步,是选对一个能陪你走完全程的“智能伙伴”。我强烈推荐 切问学术,它能让这件事从杂乱无序变得井井有条。 第一步&am…

2026/10/11 1:27:28 阅读更多 →
AI导论教案:从感知机到LLM的可验证教学实践

AI导论教案:从感知机到LLM的可验证教学实践

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

2026/10/11 1:27:28 阅读更多 →
链表找环:Floyd 判圈算法(LeetCode 141  142 )

链表找环:Floyd 判圈算法(LeetCode 141 142 )

在链表相关的算法题中,「判断链表是否有环」以及「寻找入环点」是两道极其经典的题目。它们不仅考察了对指针的操作,更蕴含了一个巧妙的数学原理——Floyd 判圈算法(龟兔赛跑算法)。 一、 LeetCode 141:判断链表中是否…

2026/10/11 1:27:28 阅读更多 →
工业AI质检大模型技术方案:小样本快速换线与边缘部署实战

工业AI质检大模型技术方案:小样本快速换线与边缘部署实战

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

2026/10/11 1:26:28 阅读更多 →

日新闻

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

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

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

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

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

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

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

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

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

2026/10/11 0:00:27 阅读更多 →

周新闻

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

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

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

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

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

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

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

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

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

2026/10/11 0:00:27 阅读更多 →

月新闻

我发现了一个新思路:用 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/10 5:23: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/9 21:32:20 阅读更多 →
黑夜航拍船只数据集训练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/10 10:38:42 阅读更多 →