Playwright codegen 录制 + AI 补全:高效构建稳定 UI 自动化脚本

1. UI 自动化脚本的困境与破局思路

做过 UI 自动化的朋友应该都有体会,写脚本这件事本身并不难,难的是“写得快、写得稳、写得省”。一个中等规模的 Web 项目,页面元素动辄几百个,每个元素都要定位、等待、断言,纯手写脚本的话,一个熟练工一天能覆盖的用例数量也就十来个。更麻烦的是,前端改个 class 名或者调个 DOM 结构,脚本就得跟着改,维护成本高得离谱。

传统的录制回放工具,比如早期的一些 IDE 插件,确实能帮你把操作录下来生成代码,但生成出来的脚本往往“能用但不好用”——定位器脆弱、没有合理的等待策略、断言缺失、结构混乱。而纯靠 AI 生成脚本呢,又面临另一个问题:AI 对页面实际结构不了解,生成的定位器经常是猜的,跑起来一堆报错,你还得反复把错误信息喂回去让它修,Token 消耗大不说,来回调试的时间也不比手写少多少。

我最近在几个项目里试了一套组合拳:用 Playwright 的 codegen 做人工录制,拿到可靠的定位器和操作序列,再让 AI 基于录制结果补全断言、异常处理和参数化逻辑。这套流程的核心逻辑是——让机器做它擅长的事(精确录制元素定位),让人和 AI 做各自擅长的事(人判断业务流程,AI 补全代码结构)。实测下来,单个用例的脚本产出时间从原来的 40 分钟压缩到 12 分钟左右,而且脚本的稳定性明显优于纯 AI 生成或纯手写的版本。

这套方法适合谁呢?如果你正在做 Web 项目的 UI 自动化,团队里有人懂基本的测试流程但写代码速度跟不上,或者你已经在用 Playwright 但觉得写脚本效率太低,那这套“录制 + AI 补全”的思路可以直接抄作业。下面我把整个流程拆开,从工具选型、录制技巧、AI 补全的提示词设计到最终的脚本调优,一步步说清楚。

2. 工具选型与核心原理拆解

2.1 为什么是 Playwright codegen 而不是其他录制工具

市面上的录制工具不少,浏览器自带的 DevTools 能录,Selenium IDE 也能录,还有一些商业化的低代码平台。我选 Playwright codegen 主要看中三点:

第一,定位器质量高。Playwright 的 codegen 默认会优先生成基于角色(role)、文本(text)、标签(label)的定位器,而不是脆弱的 CSS 类名或 XPath。比如一个登录按钮,它生成的是page.getByRole('button', { name: '登录' })而不是page.locator('#app > div > form > button.btn-primary')。前者在前端重构时几乎不会失效,后者改个样式就废了。

第二,自动等待机制完善。Playwright 内置了 actionability 检查,点击前会自动等待元素可见、可交互、稳定。codegen 录出来的脚本天然带这个能力,不需要你手动加一堆sleep或waitForSelector。

第三,代码结构干净。codegen 生成的代码是标准的 Playwright 测试结构,可以直接放进测试框架里跑,不需要额外转换。

相比之下,Selenium IDE 生成的脚本定位器质量参差不齐,而且等待策略需要手动补;商业低代码平台虽然界面友好,但生成的脚本往往绑定平台,迁移成本高。所以如果你打算长期维护一套 UI 自动化,Playwright codegen 是目前性价比最高的起点。

2.2 “人工录制省 Token”背后的成本账

这里说的“省 Token”不是指省那点 API 费用,而是指省掉 AI 反复试错的过程。我做过对比实验:同一个登录流程,纯让 AI 根据页面描述生成脚本,平均要来回 6 到 8 轮才能跑通,每轮都要把页面 HTML 片段和报错信息发给模型,Token 消耗在 8000 到 12000 左右。而先用 codegen 录一遍,把生成的代码发给 AI 让它补全断言和异常处理,通常 1 到 2 轮就能定稿,Token 消耗降到 2000 以内。

更重要的是时间成本。纯 AI 生成时,你需要不断复制页面结构、解释业务逻辑、修正定位器,这个过程非常碎片化。而录制之后,AI 拿到的是已经验证过能跑通的代码,它只需要在现有基础上做增强,不需要从零推理页面结构,准确率和速度都高一个档次。

2.3 “AI 补全脚本”具体补什么

录制出来的脚本是“裸”的——它只有操作步骤,没有断言、没有异常处理、没有参数化、没有测试数据管理。AI 补全的核心就是把这四块加上去:

  • 断言补全:录制时你点了登录按钮,但脚本不知道登录成功的标志是什么。AI 可以根据页面上下文推断出应该断言 URL 变化、某个欢迎文本出现、或者某个元素消失。
  • 异常处理:网络波动、元素加载慢、弹窗干扰,这些在录制时不会出现,但实际跑的时候经常遇到。AI 可以帮你加上重试逻辑、超时处理和弹窗关闭的兜底代码。
  • 参数化:把硬编码的用户名、密码、搜索关键词提取成变量或测试数据,方便多组数据驱动。
  • 结构优化:把重复的操作提取成辅助函数,加上合理的注释和日志,让脚本可读可维护。

这四块如果手写,一个用例至少多花 20 分钟。交给 AI 做,你只需要写一段清晰的提示词,几分钟就能拿到可用的增强版脚本。

3. 录制环节的实操细节与避坑指南

3.1 环境准备与 codegen 启动参数

Playwright 的安装很简单,一条命令搞定:

npm init playwright@latest

安装过程中会让你选语言(TypeScript 或 JavaScript)、测试目录、是否加 GitHub Actions。如果你只是先试试,选 JavaScript 就行,后面想换 TypeScript 也不难。

启动 codegen 的命令是:

npx playwright codegen https://你的目标站点.com

但这里有几个参数我强烈建议加上:

npx playwright codegen \ --target=javascript \ --output=recorded-login.spec.js \ --viewport-size=1440,900 \ --lang=zh-CN \ https://你的目标站点.com

逐个解释一下:

  • --target=javascript:指定生成 JavaScript 代码,如果你用 TypeScript 就改成--target=typescript。
  • --output:直接把录制的代码写到文件里,不用手动复制粘贴。
  • --viewport-size:固定视口大小,避免因为窗口尺寸不同导致元素位置变化,影响定位器生成。
  • --lang=zh-CN:设置浏览器语言,有些站点的文本会随语言变化,固定住能保证定位器里的文本一致。

注意:如果你录制的站点需要登录才能访问,建议先用--save-storage参数保存登录状态,下次录制时用--load-storage加载,省去每次录登录流程的麻烦。

3.2 录制时的操作节奏与定位器选择

codegen 窗口打开后,左边是浏览器,右边是代码生成区。你在浏览器里的每一步操作都会实时生成代码。这里有几个实操心得:

第一,操作前先想清楚步骤。不要一边录一边想“下一步点哪”,那样录出来的脚本会有很多多余的点击和等待。我习惯先在纸上或脑子里过一遍完整流程,然后一气呵成录完。

第二,优先用键盘操作代替鼠标。比如输入框,用page.getByLabel('用户名').fill('testuser')比点击输入框再输入更稳定。codegen 会自动识别 fill 操作,生成的代码更干净。

第三,遇到动态元素时暂停一下。如果某个元素是异步加载的,codegen 可能会在你点击时还没生成定位器,导致录出来的代码定位不准。这时候可以在浏览器里等元素出现后再操作,或者手动在代码区修正定位器。

第四,善用断言录制。codegen 右上角有个“Assert”按钮,点一下可以录制断言。比如你想断言登录后出现了“欢迎”文本,就点 Assert,然后点那个文本元素,codegen 会生成expect(page.getByText('欢迎')).toBeVisible()。这个功能很多人不知道,但非常实用。

3.3 录制产物的初步检查清单

录完之后别急着丢给 AI,先自己过一遍,检查这几个点:

检查项合格标准常见问题
定位器类型优先 role/text/label出现大量 CSS 类名或 XPath
等待策略无硬编码 sleep出现waitForTimeout(5000)
操作完整性覆盖所有关键步骤漏掉弹窗关闭、页面跳转等待
断言存在至少有一个关键断言完全没有断言,只有操作
代码结构线性流程,无重复块同一操作重复出现多次

如果定位器里出现了类似#app > div:nth-child(3) > button这种,说明 codegen 没找到更好的定位方式,你需要手动改成基于文本或角色的定位。比如改成page.getByRole('button', { name: '提交' })。这一步花两分钟,能省掉后面无数次的调试。

4. AI 补全脚本的提示词设计与实操

4.1 提示词的核心结构

让 AI 补全脚本,提示词的质量直接决定输出质量。我试过很多版本,最后固定下来一个结构:角色设定 + 输入代码 + 补全要求 + 输出格式。

具体来说,我会这样写:

你是一名资深 Playwright 测试工程师。下面是一段通过 codegen 录制的 UI 自动化脚本,请帮我补全以下内容: 1. 为每个关键操作添加合理的断言(优先使用 expect 的 toBeVisible、toHaveURL、toHaveText) 2. 添加异常处理:对可能失败的操作加上重试逻辑,对可能出现的弹窗加上关闭兜底 3. 将硬编码的测试数据提取为变量,放在脚本顶部 4. 添加必要的注释和日志,方便后续维护 5. 保持原有的定位器不变,不要修改已经能跑通的定位方式 输出要求:直接输出完整的 JavaScript 代码,不要解释,不要用 markdown 代码块包裹。 录制的脚本如下: [粘贴 codegen 生成的代码]

这个提示词的关键点在于:明确告诉 AI 不要改定位器。因为 codegen 录出来的定位器是经过验证的,AI 如果自作主张改成它“觉得更好”的定位器,反而可能跑不通。另外,要求“直接输出代码”能避免 AI 加一堆解释性文字,省得你还要手动删。

4.2 补全后的脚本长什么样

举个例子,录制一个登录流程,codegen 原始输出大概是这样的:

await page.goto('https://example.com/login'); await page.getByLabel('用户名').fill('testuser'); await page.getByLabel('密码').fill('password123'); await page.getByRole('button', { name: '登录' }).click();

AI 补全后变成:

const BASE_URL = 'https://example.com/login'; const USERNAME = 'testuser'; const PASSWORD = 'password123'; async function login(page) { await page.goto(BASE_URL); await page.getByLabel('用户名').fill(USERNAME); await page.getByLabel('密码').fill(PASSWORD); await page.getByRole('button', { name: '登录' }).click(); // 断言登录成功:URL 跳转到首页,且出现欢迎文本 await expect(page).toHaveURL(/.*dashboard/); await expect(page.getByText('欢迎回来')).toBeVisible(); // 兜底:如果出现引导弹窗,自动关闭 const guidePopup = page.getByRole('button', { name: '跳过引导' }); if (await guidePopup.isVisible().catch(() => false)) { await guidePopup.click(); } }

对比一下就能看出差别:原始脚本只有操作,补全后有了断言、有了兜底、有了参数化。而且 AI 加的断言是基于页面上下文的合理推断,比如它知道登录后通常会跳转 URL 并显示欢迎信息。

4.3 多轮补全的策略

有时候一轮补全不够,比如 AI 加的断言跑起来发现页面实际没有那个文本。这时候不要重新生成,而是把报错信息发给 AI,让它针对性修正:

上面脚本跑的时候,第 15 行断言失败,报错是:expect.toBeVisible: Error: strict mode violation: getByText('欢迎回来') resolved to 2 elements。请修正这个断言,只匹配可见的那个元素。

这种针对性修正通常一轮就能解决,比重新生成整个脚本效率高得多。我一般最多做两轮修正,如果两轮还搞不定,说明录制环节可能有问题,需要回去重新录。

5. 常见问题排查与避坑经验

5.1 定位器失效的三种典型场景

场景一:文本随语言或环境变化。比如按钮文本是“提交”,但英文环境下变成“Submit”。解决办法是用正则匹配:page.getByRole('button', { name: /提交|Submit/ })。

场景二:元素在 iframe 里。codegen 录制时如果操作了 iframe 内的元素,生成的代码可能缺少 frame 切换。你需要手动加上page.frameLocator('#iframe-id').getByLabel('用户名')。

场景三:动态生成的 ID。有些前端框架会生成类似id="input-1234"的动态 ID,每次刷新都变。这种定位器录下来也是废的,必须改成基于 label 或 role 的定位。

5.2 AI 补全时的常见翻车点

翻车点一:AI 擅自修改定位器。前面提示词里强调了不要改,但有时候 AI 还是会“好心”帮你优化。解决办法是在提示词里加一句“如果必须修改定位器,请在注释里说明原因”。

翻车点二:AI 加的断言过于具体。比如它断言expect(page.getByText('欢迎回来,testuser')).toBeVisible(),但实际页面显示的是“欢迎回来,张三”。这种断言太脆弱,应该改成expect(page.getByText(/欢迎回来/)).toBeVisible()。

翻车点三:异常处理过度。AI 有时候会加一堆 try-catch,把真正的错误也吞掉了。我的原则是:只对“预期内可能失败”的操作加兜底,比如弹窗关闭、可选引导页跳过。核心业务操作不要加 try-catch,让它失败,这样你才能发现问题。

5.3 脚本稳定性调优的实操技巧

录完、补全完,脚本能跑通只是第一步。要让它稳定跑在 CI 环境里,还需要做几件事:

第一,设置合理的超时。Playwright 默认超时是 30 秒,对于加载慢的页面可能不够。可以在playwright.config.js里全局设置timeout: 60000,也可以对单个操作设置{ timeout: 10000 }。

第二,启用重试机制。在配置文件里加上retries: 2,这样偶发的网络波动不会导致整个测试失败。

第三,固定测试数据。不要用随机生成的数据,除非你确定每次跑完都能清理。我习惯在测试前用 API 创建固定数据,测试后删除,保证每次跑的环境一致。

第四,截图和录屏。在配置文件里开启screenshot: 'only-on-failure'和video: 'retain-on-failure',失败时能直接看到现场,排查效率翻倍。

5.4 常见问题速查表

问题现象可能原因解决方法
元素找不到定位器失效或元素未加载检查定位器,加 waitFor 或改用 role 定位
点击没反应元素被遮挡或不可交互加{ force: true }或先关闭遮挡弹窗
断言失败但页面正常断言条件太严格改用正则或部分匹配
脚本跑一半卡住页面跳转未等待加waitForURL或waitForLoadState
CI 上失败本地成功环境差异或超时太短增加超时,检查 CI 网络和分辨率

6. 从单脚本到测试套件的扩展思路

单个脚本跑通之后,下一步就是把它扩展成一套可维护的测试套件。这里分享几个我在实际项目中验证过的做法。

第一,提取公共操作成辅助函数。登录、登出、导航到某个模块,这些操作在多个用例里都会用到,提取成helpers.js里的函数,用例里直接调用。这样前端改登录流程时,你只需要改一个地方。

第二,用 fixtures 管理测试上下文。Playwright 的 fixtures 机制非常适合管理登录状态、测试数据、页面对象。你可以定义一个authenticatedPagefixture,它自动完成登录并返回已登录的 page 对象,用例里直接用就行。

第三,按业务模块组织测试文件。不要把所有用例塞在一个文件里,按模块分目录,比如tests/login/、tests/order/、tests/profile/。每个目录下放对应的 spec 文件和辅助函数。

第四,定期回归录制。前端大改版后,原来的定位器可能失效。这时候不要一个个手动修,重新用 codegen 录一遍关键流程,把新录的代码和旧脚本对比,快速定位变化点。

这套“录制 + AI 补全”的流程,我用了大概三个月,覆盖了五个项目的 UI 自动化建设。最大的感受是:不要把 AI 当成万能生成器,而是当成一个高效的代码助手。录制负责提供准确的“事实”,AI 负责在这些事实上做增强和补全,人负责判断业务逻辑和把控质量。三者各司其职,效率和质量才能同时上去。

最后分享一个小技巧:如果你录制的流程比较长,可以分段录制,每段录完先让 AI 补全并跑通,再录下一段。这样问题能及早发现,不会等到最后才发现某个环节的定位器有问题,导致整个脚本要重录。

相关新闻

OpenClaw嵌入式可信加固:启动度量+模型签名+MPU内存隔离

OpenClaw嵌入式可信加固:启动度量+模型签名+MPU内存隔离

简介:本资源是面向AI工程实践者与自托管开发者的安全加固实战指南,聚焦OpenClaw这一开源自托管个人AI代理的全链路安全防护。针对其易暴露、权限宽松、插件生态风险高等典型问题,提供从安装后立即执行的基础检查、核心配置文件加密与权限收紧…

2026/10/20 5:25:07 阅读更多 →
多模态模型意图歧义破解:VisInteract交互搜索机制与工程实践

多模态模型意图歧义破解:VisInteract交互搜索机制与工程实践

1. 从“自作主张”说起:多模态模型到底缺了什么做过多模态大模型应用的人大概都有过这种体验:你给模型一张图,问它“这个东西怎么用”,它洋洋洒洒给你写了一大段,看起来头头是道,但仔细一读,它回…

2026/10/20 5:24:06 阅读更多 →
提示词工程实战:学会和AI把话说清楚的技术

提示词工程实战:学会和AI把话说清楚的技术

先聊个现象。最近一年,身边越来越多非技术背景的朋友开始频繁使用ChatGPT,但同一款工具,有人拿它几分钟产出一份结构完整的活动方案,有人却只能在“帮我写个文案”这种模糊指令里反复打转,最后抱怨AI“不够聪明”。差距…

2026/10/20 5:24:06 阅读更多 →

最新新闻

从单体到弹性扩展:高并发架构改造的完整实战复盘

从单体到弹性扩展:高并发架构改造的完整实战复盘

1. 项目背景与架构需求拆解1.1 最初的系统,和它一步步暴露出的问题先说下背景。这个代号为"arch-system"的架构改造项目,源于团队负责的一条核心业务链路。最初的系统是典型的三层单体:一个应用服务承载所有接口,MySQL …

2026/10/20 6:03:23 阅读更多 →
PPT-Master:用PowerPoint构建业务操作系统的方法论

PPT-Master:用PowerPoint构建业务操作系统的方法论

1. “PPT-Master”不是软件名,而是一类人的生存状态最近在几个技术协作群和设计交流社区里,“PPT-Master”这个词高频出现,但没人把它当产品名说——它更像一句带点自嘲又透着狠劲的行业暗号。我第一次听到是在某次跨部门复盘会上&#xff0c…

2026/10/20 6:03:23 阅读更多 →
图灵奖60年:人机环境系统智能的演进与落地

图灵奖60年:人机环境系统智能的演进与落地

六十年前,那个以一位计算机先驱命名的奖项第一次颁出时,大概没有人想到,它会成为一部无声的“智能观变迁史”。这六十年间,获奖成果背后的工作其实都在回答同一个问题:机器到底怎样才算有智能?答案从“会算…

2026/10/20 6:03:23 阅读更多 →
浙江高考VB排序系列:冒泡与选择排序代码实现及应试技巧

浙江高考VB排序系列:冒泡与选择排序代码实现及应试技巧

1. 从一道排序题说起:为什么VB排序值得单独拎出来讲每年到了技术科目备考的冲刺阶段,总有一批同学在排序算法这块反复栽跟头。选择题里问“冒泡排序第三轮结束后序列长什么样”,填空题里给一段残缺代码让你补全循环边界,上机操作题…

2026/10/20 6:03:23 阅读更多 →
图灵奖六十年:从机器能否思考到人机环境系统智能的跃迁

图灵奖六十年:从机器能否思考到人机环境系统智能的跃迁

图灵奖整整六十年了。作为计算机科学领域最负盛名的荣誉,它这些年一直被当作衡量学术巅峰的标尺。可如果你把六十年来历届获奖者所代表的方向串成一条时间线,会发现一个远比"学术成就"更有意思的东西正在浮现:整个领域对"智能…

2026/10/20 6:03:23 阅读更多 →
主动学习新思路:损失预测模块如何精准筛选高价值样本

主动学习新思路:损失预测模块如何精准筛选高价值样本

1. 为什么主动学习要换一种采样思路做深度学习项目,尤其在数据标注预算有限的时候,大家最先想到的办法多半是“先随便标一部分,训练一版模型,再用模型挑一些‘难样本’送给标注员”。这套逻辑本身没问题,难就难在“难样…

2026/10/20 6:02:23 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

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

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

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

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

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

2026/10/20 1:01:30 阅读更多 →

周新闻

基于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/20 2:50:50 阅读更多 →

月新闻

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