鸿蒙化适配:脚手架工具初始化资产的分层重构实践
每年都有团队在“新项目初始化”上浪费大量时间手拉一个基础工程、改包名、配路由、接网络层然后还要统一代码规范。我之前维护过一款 Dart 编写的 CLI 脚手架工具代号 easy_init_cli核心就一句话用一条命令按标准模板生成完整工程骨架把初始化资产统一管起来。开发同事拿到手只需要告诉它项目叫什么、要哪些模块剩下的重复劳动全部自动化。最近我们在做跨端工程向鸿蒙化场景迁移时遇到一个绕不开的问题easy_init_cli 生成的模板还是“原来那个世界”的工程形态——原平台宿主壳、插件目录、依赖声明、构建产物全都不适用于新的目标运行环境。如果直接把老模板生搬硬套产出的工程在鸿蒙侧根本没法编译通过如果简单复制一套模板出来单独维护后面每改一处都要同步两份迟早漂移得不可收拾。所以我把 easy_init_cli 做了一次完整的鸿蒙化适配重点不是改业务代码而是重构“初始化资产”的分层方式。这篇文章不聊虚的直接讲我实际动手的路径资产怎么拆、模板怎么抽象、生成器怎么改、参数怎么设计以及真实踩过的坑。适合正在维护基础工程的开发者、在做脚手架迁移的工具链维护者以及所有想把“工程初始化”从手工复制变成自动化资产治理的团队。1. 内容整体设计与思路拆解1.1 重新认识初始化资产不止是“模板文件夹”很多人把脚手架工具理解成“模板目录 字符串替换”这远远不够。我在实际维护 easy_init_cli 的过程中发现初始化资产应该拆成四类模板文件目录结构、源码片段、配置文件外壳带可变占位符变量定义项目名、包名、版本号、作者标识等依赖清单不同功能组合下的依赖树、版本约束、插件集生成逻辑CLI 内部如何装配以上内容以及可选功能开关的处理方式这四类东西共同构成了初始化资产。它的价值不在于“有一堆文件”而在于“同一套资产可以稳定复现出标准工程”。过去没有 CLI 的时候团队初始化项目靠的是互相传压缩包每个人拿到之后手工改一改改到后面五个项目的结构各长各的。而 easy_init_cli 这类工具本质上是把“初始化”变成一个可审计、可回滚、参数化的工程行为。1.2 问题本质不是代码不能跑而是“工程形态”变了鸿蒙化适配刚开始时我下意识地以为难点在于代码能不能在新运行环境上跑起来。但作为脚手架工具真正的矛盾出现在“初始化那一刻”——模板生成出的工程结构跟鸿蒙侧的工程要求对不上。原平台工程默认包含这样的形态pubspec.yaml 管理核心依赖android/ios/web 三套宿主壳目录插件通过 Dart 侧统一接口暴露原生能力构建行为由 Gradle 或 Xcode 工程定义而鸿蒙化之后目标工程会有完全不同的要求AppScope、entry 模块结构以及模块描述文件声明ArkTS 侧的代码组织方式与资源配置方式原生能力需要走端侧桥接机制对外暴露权限声明、后台任务、隐私声明全部在资源文件里以特定格式描述CLI 如果感知不到这些差异它生成的工程就是“半个工程”——文件在但形态不对。想清楚这件事之后我把适配重点放在了“工程形态描述层”而不是业务代码层。工具要做的是在初始化阶段就生成一个鸿蒙侧认可的形状后续编译和运行的问题自然少一大半。1.3 三条适配路线和最终的选择面对适配我评估过三种路线第一原模板分叉维护。直接把现有模板复制一份改造成鸿蒙版。这个方案最快但后患很大。模板不是一次性产物它随技术栈迭代持续演进两个版本长期存在修 bug 要同步两份改结构要核对两份迟早出现发布版本不一致。第二在生成器里加目标系统判断。这种做法把适配压力全部放进 CLI 代码里if/else 之间堆满平台判断。短期可用长期会让模板资产和生成逻辑耦合加深模板的独立演进能力被破坏。第三资产配置化多平台共享底层描述。把模板拆成“公共段”和“平台段”用配置声明来定义每个目标系统需要哪些分段、哪些文件、哪些变量的默认值。CLI 本身不知道鸿蒙还是原平台它只是按照配置去装配资产。我最终选了第三条。理由很实际配置化之后新增平台不需要改 CLI 主程序只需要新增一份目标系统配置模板的公共部分保持单点维护生成器逻辑可以保持纯粹只干“读配置、渲染文件、执行钩子”三件事。2. 拆解 easy_init_cli 的鸿蒙化关键修改点2.1 模板目录从“一体式”改成“分段式”原来的模板结构是整体一份目录我把它拆成了三个层次。每个层次解决不同问题。分段内容作用公共段 common项目级目录、README、通用脚本所有目标系统都能复用Flutter 核心段 flutter_corepubspec 模板、Dart 源码目录、插件装配原平台侧工程主体鸿蒙核心段 harmony_core模块描述、ArkTS 目录、资源配置骨架鸿蒙侧工程主体生命周期钩子 hooks初始化前后需要执行的脚本处理平台差异的补充动作这样做的好处是“影响范围收敛”。公共段里的变量比如项目显示名几乎对所有目标系统都有意义但权限声明这种变量只有鸿蒙核心段需要认识。分段之后变量声明可以跟着段走CLI 在处理模板时能明确知道哪些变量属于哪个桶不会所有变量混在一个大池子里互相打架。2.2 变量命名空间避免模板渲染“天下大乱”模板变量看起来简单实际坑很多。最典型的场景模板里用了一个{{name}}往某个配置里一套结果发现配置里面也有一个“name”字段两个完全不是同一个意思渲染结果全线雪崩。我这里的做法是把变量划分成三个命名空间内置变量统一加ti_前缀比如ti_app_name、ti_bundle_id、ti_target_platform用户输入变量统一加user_前缀来自命令行参数或交互式输入输出变量统一加out_前缀用于记录渲染结果、文件清单等这套前缀规则不是拍脑袋定出来的。真实项目里模板文件数量多、嵌套深变量来源杂只有靠前缀才能在一堆文件里快速追踪某个变量是谁注入的。鸿蒙化之后我还维护了一张变量影响表记录每个变量作用到哪些平台、哪些文件避免跨平台污染。渲染时有一个强制性校验先扫描模板目录收集所有占位符再和已声明变量比对只要发现未声明的占位符就直接报错而不是静默替换为空字符串。这个设计帮我挡掉很多隐蔽问题。很多脚手架生成出来表面上文件齐全实际缺字段就是因为没有做占位符声明校验变量拼写错误被静默吞掉。2.3 命令行参数设计不是越多越好easy_init_cli 原先的参数只有项目名和模板名鸿蒙化之后需要扩展但我控制得很克制easy_init_cli init DemoApp \ --platform harmony \ --profile mobile \ --features router,login,network \ --no-examples参数语义要清晰--platform告诉 CLI 取哪个平台分段可选flutter或harmony--profile按 profile 选择模板组合比如mobile、lite、full--features打开或关闭可选模块生成器根据这个值决定要不要把对应功能模块写进工程--no-examples移除示例代码适合想拿空工程开始写业务的团队我有意把可选参数控制在七个以内。东西越多用户理解成本越高测试组合数也会爆炸。真正复杂的配置我引导用户放到模板配置文件里去做而不是全堆在命令行。命令行负责高频操作配置文件负责低频但复杂的定制这样边界最清晰。3. 实操过程与核心环节实现3.1 资产仓库的落地布局这次适配我先把旧的单一模板目录迁移成新的资产布局。以模拟工具仓库的目录结构为例assets/ manifests/ common.yaml harmony_mobile.yaml stages/ common/ flutter_core/ harmony_core/ snippets/ router_config.tpl network_config.tpl hooks/ pre_init.sh post_init.shmanifests/common.yaml是模板元数据里面声明三件事变量、组装顺序、路径变换规则。manifests/harmony_mobile.yaml是鸿蒙侧 profile它继承公共配置再做平台相关的覆盖。extends: common platform: harmony module_type: shared variable_defaults: ti_target_platform: harmony ti_oh_version: 5.0 stages_order: [common, flutter_core, harmony_core] filters: - **/*.tpl - !**/ios/** - !**/android/**filters 里排除 ios 和 android 目录是鸿蒙化很关键的一步。原模板里有大量原平台宿主壳相关文件在鸿蒙目标下这些完全没用。用 filter 来排除比在模板文件里写if(platformharmony)干净得多。后者会让模板文件内部充满条件片段过三个月再看根本分不清哪些分支还活着。3.2 渲染引擎的三个适配点第一文件级变量注入。按文件读取模板内容用统一的模板语法替换占位符。这一层逻辑不复杂但性能要注意大模板库扫描时如果每次都起一个新的正则引擎会很慢复用编译后的匹配器能快不少。第二路径变换。模板文件名本身可能带占位符比如src/{{ti_package_name}}/main.dart。渲染内容之后还要处理路径把目录名里的占位符也替换掉。最容易漏的是 Windows 下的分隔符统一用 posix 风格处理路径到落地时再按系统转换可以避免一部分莫名其妙的问题。第三钩子执行。初始化完成后跑 hooks/post_init.sh 做收尾工作比如创建私有化 Git 仓库、初始化本地依赖缓存、清理临时文件。钩子脚本按平台走不同分支鸿蒙场景下会额外做一些资源配置校验。核心流程我用 TypeScript 风格的伪代码描述一下实际 CLI 主程序的骨架就是这个逻辑const config loadConfig(assets/manifests/common.yaml); const profile loadProfile(assets/manifests/harmony_mobile.yaml); const variables collectVariables(profile.stagesOrder, config); validateVariables(variables, config); const fileList scanStageFiles(assets/stages, profile.stagesOrder, profile.filters); for (const file of fileList) { const content renderTemplate(file, variables); writeFile(transformPath(file, variables), content); } runHook(post_init, variables);这个流程最核心的点是validateVariables。它保证“模板里出现的一切占位符”都有定义才不会出现生成完才发现若干字段缺失的问题。3.3 初始化基座代码的拆分脚手架不应该生成业务代码它应该生成“基座代码”。基座指的是工程能跑起来但还没有具体业务逻辑的那层骨架。鸿蒙化之后基座代码的分工变得更清晰。资产类型原平台形态鸿蒙目标形态工程描述文件pubspec.yaml / settings.gradleoh-package.json5 / module.json5应用入口main.dartentry/src/main/ets/entryability路由注册router_builder.dart路由表配置文件网络层基座dio 封装 / api_client.dart端侧网络能力封装存储基座shared_preferences 封装首选项封装这张表是模板拆分的核心依据。以前模板只有一个视角现在是两个视角共享一套公共结构再各管各的差异部分。拿“存储基座”来说公共段只放抽象接口定义flutter_core 段放原平台缓存实现harmony_core 段放鸿蒙侧首选项封装实现。业务代码永远只依赖抽象接口这样团队在哪个平台开发都不影响业务层。3.4 生成后的验收步骤生成完工程不能光看文件存在。我建立了一套验收清单检查 module.json5 是否存在且模块名称与项目名一致检查权限声明文件是否存在且声明的权限范围与 profile 匹配检查路由表文件是否存在对应页面的注册项检查依赖清单里是否包含指定版本的第三方基础库执行一次静态编译检查确认工程能通过编译期校验清单是自动化验证的基础。如果生成器改崩了模板一开始就能检测到而不是等开发同事反馈“工程跑不起来”。4. 常见问题排查与避坑实录4.1 高频问题速查表问题现象可能原因处理方式生成后缺少模块描述文件profile 的 stages_order 漏了 harmony_core检查 profile 配置确认分段完整渲染结果出现空字段占位符拼写错误或变量前缀冲突开启未声明占位符校验定位到具体模板文件Windows 上路径多层反斜杠模板路径和落地路径的/处理不一致统一用 posix 路径处理落地时再转换依赖版本从旧工程带过来了版本约束在模板文件里写死把版本号抽到变量声明里按 profile 覆盖权限声明在鸿蒙侧不生效权限逻辑只考虑了原平台配置新增权限映射表按目标系统输出对应格式4.2 下沉到目录和编码的坑除表格里的常规问题还有几个不踩不知道的坑第一模板文件编码问题。跨平台模板最容易出现 Linux 下 UTF-8 正常、Windows 记事本动过之后带 BOM 头的问题。BOM 头对于某些解析器来说没事但对 Yaml 解析和脚本执行都可能引起怪异行为。我在渲染引擎里统一做编码归一化读取时检测 BOM 并剥离写入时统一 UTF-8 无 BOM。第二模板里的空目录。Git 不追踪空目录但模板资产仓库往往需要保留一些空目录作为标准工程结构。我维护了一份.gitkeep文件列表CLI 在生成阶段会自动为需要的空目录补上占位文件生成完成后统一删除。第三过滤规则的 glob 写法。每个模板文件都要被 filter 匹配**和!组合的优先级写得不对容易误伤公共段文件。建议单独写一个过滤器测试用例列表每个 pattern 对应几个明确的命中/不命中样本防止后续改配置时无意扩大排除范围。4.3 日志体系决定了排查效率CLI 工具的日志设计直接决定了出了问题之后要花十分钟还是两小时解决。我分了三级info正常流程输出不啰嗦debug打印渲染文件清单、变量环境、profile 最终生效值trace输出模板扫描结果、过滤规则匹配过程、钩子执行输出很多人排查问题是靠“猜”加一堆打印再看。但 CLI 工具跑了那么长流程最需要的是能把整个过程还原出来的信息。trace 级别的日志记录每一个模板文件被谁匹配、为什么被排除、变量从哪里来基本上下一次通过日志就能定位问题。5. 工程资产治理与回归测试5.1 资产版本分仓避免“发布混乱”适配过程中我放弃了一个旧习惯——把模板资产和 CLI 主程序放同一个仓库。原因是模板迭代频率远高于 CLI 主程序。鸿蒙化之后公共段模板可能按周更新而 CLI 的命令行逻辑可能几个月不动一版。两者放一起意味着每次模板小改都要发一版 CLI很重。新方案是模板资产单独一个仓库打独立版本号比如1.2.0-harmony.1。CLI 在安装时记录当前使用的资产版本生成工程时把这版本信息写进工程元数据。将来审计时就知道某个项目是基于哪个资产版本生成的排查问题时有据可依。5.2 清单测试与冒烟回归工具测试我主要做两类。第一类是清单测试用测试用例覆盖不同的 profile 与 feature 组合断言生成后关键文件存在、关键内容匹配。第二类是冒烟回归每次改动后自动初始化一个临时鸿蒙工程跑一次静态编译检查。具体操作脚本大致是这样# 冒烟脚本示例 easy_init_cli init SmokeApp --platform harmony --features network cd SmokeApp # 检查关键文件 test -f AppScope/app.json5 echo app.json5 exists test -f entry/src/main/module.json5 echo module exists # 拉取依赖并执行必要检查 make check这套冒烟流程不复杂但能兜住大部分回归。最怕的是模板这么折腾下来某天发现某些文件没被渲染出来而团队已经用了新模板初始化了好几个项目。6. 从实践里沉淀下来的几点经验适配做完之后我对“脚手架工具”的定位有了新的认识。工程初始化工具最核心的资产不是代码而是对工程结构的“权威定义”。鸿蒙化适配真正难的地方不是某个 API 怎么调而是你要有勇气打破“工程本来就应该长这样”的惯性认知。如果你也在维护类似的 CLI 工具我个人建议把目标系统差异的抽象放在“资产分层”这个层级而不是散落在生成逻辑的每个 if 分支里。CLI 的价值不在于模板生成完就结束而在于生成之后依然能通过资产版本、配置声明、回归测试去管理工程资产的整个生命周期。这次适配给我的最大收获其实是把“生成工程”这个动词变成了一套可维护、可演进、可追溯的资产治理体系。

相关新闻

Codeforces Round 1077

Codeforces Round 1077

憧憬成为 Master 第 46 集 —— 投降喵 (Codeforces Round 1077 (Div. 2)) https://www.bilibili.com/video/BV1Yu6PBPEjy/ Codeforces Round 1077 (Div. 1) https://www.bilibili.com/video/BV1Qa6ABbEhM/ [赛时4/7]Codeforces Round 1077 (div.2) https://www.bilibili.com/v…

2026/10/11 6:26:16 阅读更多 →
AnyPS5项目解析:跨平台游戏兼容性技术探析

AnyPS5项目解析:跨平台游戏兼容性技术探析

我无法基于当前输入生成符合要求的博文。原因如下:输入中仅提供了项目标题"AnyPS5",但未提供任何实质性的【项目正文】、【关键词】或【摘要描述】;所谓“相关热搜词”和“最新网络热词”字段为空,无实际内容可供分析&a…

2026/10/11 6:26:16 阅读更多 →
电脑触摸板反复失灵?I2C HID设备代码10的修复经验

电脑触摸板反复失灵?I2C HID设备代码10的修复经验

笔记本用着用着,触摸板突然没反应了,手指怎么滑动都无法移动光标,但插上USB鼠标又一切正常。更奇怪的是,有时候重启电脑就好了,过几天又会出现同样的问题。 这种情况不一定是触摸板坏了。尤其是Win10、Win11系统更新或…

2026/10/11 6:25:16 阅读更多 →

最新新闻

【单片机毕业设计】基于单片机的自行车骑行定位与运动数据监测装置设计 基于物联网的骑行速度里程与定位追踪监测系统设计(030205)

【单片机毕业设计】基于单片机的自行车骑行定位与运动数据监测装置设计 基于物联网的骑行速度里程与定位追踪监测系统设计(030205)

博主介绍:✌️码农一枚 ,专注于大学生项目实战开发、讲解和毕业🚢文撰写修改等。全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机,Java、小程序技术领域和毕业项目实战 ✌️…

2026/10/11 7:53:04 阅读更多 →
厦门Java后端面试复盘:分布式事务、库存扣减与订单状态机

厦门Java后端面试复盘:分布式事务、库存扣减与订单状态机

上周刚从厦门回来,一口气面了两家公司的Java后端岗位。趁记忆还热乎,把这次的面试总结整理出来。如果你也在看厦门的后端机会,或者正准备约面试,这篇复盘应该能给你一些参考。两家公司一家是跨境ERP自研,另一家做本地生…

2026/10/11 7:53:04 阅读更多 →
Flask实现企业员工人事工资管理系统前台:从登录到工资查询的完整实践

Flask实现企业员工人事工资管理系统前台:从登录到工资查询的完整实践

前阵子帮一家做内部管理系统的小公司搭了一个员工自助门户,跑起来之后身边好几个同事都问我说,你这套“前台”到底是怎么拆出来的,跟后台管理端混在一起会不会乱。借着这个机会,我把PythonFlask实现企业员工人事工资管理系统前台的…

2026/10/11 7:53:04 阅读更多 →
WOFOST与AquaCrop作物模型对比:机理、参数与应用选择

WOFOST与AquaCrop作物模型对比:机理、参数与应用选择

做作物模型的人,迟早会在某个项目里同时撞见WOFOST和AquaCrop这两个名字。一个来自欧洲,一个出自联合国粮农组织,都是全球应用最广的作物生长模型,但如果你只把它们当作“模拟产量的工具”来用,那从一开始就搞错了方向…

2026/10/11 7:53:04 阅读更多 →
VLX-Seek实时视觉理解模型:架构设计与工程优化实战

VLX-Seek实时视觉理解模型:架构设计与工程优化实战

1. 从标题拆解 VLX-Seek 的真实定位1.1 这个标题到底在说什么第一次看到“VLX-Seek 实时视觉理解模型”这个标题,我脑子里跳出来的第一个判断是:这不是一个单纯的图像分类模型,也不是一个只做目标检测的模型,而是一个把“看”和“…

2026/10/11 7:53:04 阅读更多 →
告别语义漂移:用 BM25 与向量检索打造个人日记的混合召回(Hybrid Search)

告别语义漂移:用 BM25 与向量检索打造个人日记的混合召回(Hybrid Search)

秋雨淅淅沥沥下了一整天,空气里泛着潮湿的泥土香气。我坐在书房里,试图从这几年积攒的上千篇日记与手账随笔中,找出一年前在某家咖啡馆尝过的“桂花乌龙生椰拿铁”。 我在私有 RAG 助手里键入了这几个字,屏幕很快吐出了一堆相关记…

2026/10/11 7:52:03 阅读更多 →

日新闻

流感时间序列预测实战: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 阅读更多 →