# 本地工具路径环境变量方案 > 本文档描述项目的本机工具路径环境变量机制:变量清单、解析与失败语义、各级配置方法、验收判据 > (2026-10-02 实施并生效;实施过程与后续变更见文末变更记录)。 > **约束(现行)**:只用进程/用户/CI 服务环境变量配置本机差异;不提交本机路径,不引入 `.env`/`python-dotenv`, > 不把个人工具路径写入共享 workflow;仓库内部路径继续相对脚本/仓库根解析。 ## 1. 变量清单 | 变量 | 必需性 | 值的形态 | 使用者 | 默认/回退 | |---|---|---|---|---| | `DTU_KEIL_UV4` | MCU 构建机必需 | UV4.exe 完整路径,例如 `D:\Tools\Keil\UV4\UV4.exe` | `keil_build.py`(`@build`) | 不设即报错(exit 2);不从 C 盘旧路径猜测 | | `DTU_STLINK_CLI` | 复位/烧录/回读机必需 | `ST-LINK_CLI.exe` 完整路径,例如 `C:\Program Files (x86)\STMicroelectronics\STM32 ST-LINK Utility\ST-LINK Utility\ST-LINK_CLI.exe` | `stlink_flash.py` | 未设时回退 PATH(`where ST-LINK_CLI.exe` 首个命中);已设但无效 = 硬报错 | | `DTU_PRECOMMIT_PYTHON` | 可选(覆盖用) | 含 `pre_commit` 模块的 `python.exe` 完整路径 | `ci_local.py` L0、CI `l0-gates` | 未设时按 §1.1 探测(当前解释器 → `.venv` → PATH);已设但无效 = 硬报错 | | `DTU_GRAPHVIZ_DOT` | 可选(覆盖用) | `dot.exe` 完整路径 | `gen_refgraph.py` | 未设时只查 PATH;无 Graphviz 时降级为"生成 .dot/.json、跳过渲染" | | `DTU_MSVC_CL` | 可选(覆盖用) | x64 `cl.exe` 完整路径(`bin\Hostx64\x64` 下) | `unit_test_harness.py` L0.5、CI `l05` | 未设时按 VS 安装根自动探测(2019/2022/2026 各 edition);已设但不符 `bin\Hostx64\x64\cl.exe` = 硬报错 | | `DTU_WINDOWS_KITS_ROOT` | 可选(覆盖用) | `Windows Kits\10` 根目录(须含 `Include`/`Lib`) | `unit_test_harness.py` L0.5、CI `l05` | 未设时从 `%ProgramFiles*%` 推导根自动探测;已设但无 `Include`/`Lib` = 硬报错 | | `DTU_VSWHERE` | 可选(覆盖用) | `vswhere.exe` 完整路径 | `build_sim.ps1` | 未设时用 `%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe` 推导;已设但无效 = 硬报错 | | `DTU_CPPCHECK` | 可选(覆盖用) | `cppcheck.exe` 完整路径 | CI `l0-gates` cppcheck 步骤 | 未设时用 PATH(`Get-Command`);已设但无效 = 硬报错。本地 `ci_local.py` L0 不读该变量:cppcheck 钩子按 pre-commit `entry: cppcheck` 从 PATH 解析 | > 变量值只存路径本身(不加引号);可执行文件变量存 `.exe` 完整路径,目录变量(Kits 根)存目录。 > 不设任何变量时全部走自动发现(PATH / 系统目录推导 / `.venv` / `vswhere`);变量只用于覆盖非标准安装位置。 > 字体系统目录(`simsun.ttc`)不设 DTU 变量:由 `%WINDIR%\Fonts\` 推导,`make_font.py` 的 `--font` 参数仍最高优先。 ### 1.1 Python 优先级特别说明(`ci_local.py` / CI L0) `pre-commit` 是门禁的**执行环境**而非"工具地址",优先级与其余工具不同: 1. 显式 `DTU_PRECOMMIT_PYTHON`(若设置,必须可执行且能 `import pre_commit`,否则报错); 2. 当前解释器(`sys.executable`,即实际运行 `ci_local.py` 的 Python); 3. 仓库 `.venv\Scripts\python.exe`(若存在且装了 pre-commit); 4. PATH 中的 `python`。 CI 侧同理:`DTU_PRECOMMIT_PYTHON` → runner PATH → workspace `.venv`。 ## 2. 解析与失败语义(所有调用端统一) 1. **显式值校验**:变量已设置时,先校验其指向的文件/目录真实存在且符合形态(如 `cl.exe` 必须位于 `bin\Hostx64\x64\` 下);无效则**立即报错**,错误信息必须包含变量名与所给值,**不得静默回退**到自动发现。 2. **未设置才自动发现**:PATH / 系统目录推导 / `.venv` / `vswhere` 等;自动发现失败按各工具既有降级或报错语义处理。 3. **BAT 实现约束**:`.bat` 中读取环境变量用 `%DTU_...%`;判空用 `if defined`;错误分支 `exit /b` 非零。BAT 内解析一律平铺 `goto` 标签风格,不用括号块(`echo )` 会提前闭合块)。 4. **PowerShell 实现约束**:`$env:DTU_...`;`build_sim.ps1` 的 CMake 配置不直接读环境变量(保持生成器无关)。 5. **workflow run 块**:`ci.yml` 的 run 块内用 `${env:DTU_...}`(未设置时为空串)与 PowerShell `$env:DTU_...` 判空,**不得**在 workflow 文件里写死任何本机绝对路径。 6. **Windows 命令行引号规则**:值含空格(如 `C:\Program Files (x86)\...`)时,调用端必须整体加引号(`"%DTU_KEIL_UV4%"` / `"& $env:DTU_..."`);变量值本身**不要**带引号。 7. **系统变量推导**:`%WINDIR%`、`%ProgramFiles%`、`%ProgramFiles(x86)%` 仅用于推导(如 `make_font.py` 的 `simsun.ttc`、`build_sim.ps1` 的 vswhere 路径),不得作为"工具安装目录"的硬编码兜底。 ## 3. 本机配置方法 ### 3.1 当前 PowerShell 会话(立即生效,适合试配置) ```powershell # 仅当前会话有效;换终端/重启即失效 $env:DTU_KEIL_UV4 = 'D:\Tools\Keil\UV4\UV4.exe' $env:DTU_STLINK_CLI = 'D:\Tools\ST-LINK Utility\ST-LINK_CLI.exe' # 验证:Test-Path $env:DTU_KEIL_UV4 → True ``` ### 3.2 用户级持久配置(日常开发机,推荐) ```powershell # 当前用户、全部新进程可见;无需管理员 [Environment]::SetEnvironmentVariable('DTU_KEIL_UV4', 'D:\Tools\Keil\UV4\UV4.exe', 'User') [Environment]::SetEnvironmentVariable('DTU_STLINK_CLI', 'D:\Tools\ST-LINK Utility\ST-LINK_CLI.exe', 'User') # 已存在同名值时直接覆盖;删除用 [Environment]::SetEnvironmentVariable('DTU_KEIL_UV4', $null, 'User') # 设置后需重开终端/IDE 才对新进程生效 ``` ### 3.3 Gitea Windows runner(SYSTEM 服务) ```powershell # 必须管理员;写入机器级(Machine)环境,供 Gitea runner 服务(SYSTEM 账户)读取 [Environment]::SetEnvironmentVariable('DTU_PRECOMMIT_PYTHON', 'C:\ProgramData\anaconda3\python.exe', 'Machine') [Environment]::SetEnvironmentVariable('DTU_CPPCHECK', 'C:\Program Files\Cppcheck\cppcheck.exe', 'Machine') # 写入后需重启 Gitea runner 服务/任务才能带上新变量(服务启动时读取环境变量快照) ``` > 注意:runner 以 SYSTEM 运行,**用户级变量对 runner 不可见**;反之机器级变量对交互用户可见但不应依赖。开发机日常配置用 User 级,runner 专用覆盖用 Machine 级。 > 值示例中的 `C:\ProgramData\anaconda3\...` 仅演示"显式覆盖"形态;若 runner 的 PATH/`.venv` 可自动发现,则不必设置该变量。 ## 4. 验收判据 - 新电脑只配置本机 `DTU_*` 环境变量(§3),clone 仓库后可直接运行各开发命令,**不修改任何脚本/工作流中的本机路径**。 - 显式变量指向无效路径时,命令以非零退出码失败且错误信息指明变量名,不静默使用其他工具。 - 仓库内不出现提交的本机绝对工具路径(文档中的示例路径除外)。 - 跨机验证操作清单见 `项目部署环境要求.md` §2.12;runner 侧机器级变量设置见 `docs/CI/远程CI完整指南.md` §4.1.3。 --- ## 5. 三个解释器各能跑什么(2026-10-27 实测,道G task-33) > 起因:有队友在 `.venv` 里 `import identify.tags` 失败,又用 `pip` 查不到 `pre-commit`,据此判定 > 「`.venv` 里的 pre-commit 安装是残缺的」。**复测结论:环境不残缺,两条观测都成立但推错了因**—— > 前者是版本本身没有那个模块(下表第三行),后者是查错了对象(下表第二行)。 > 取证方法固定:同一个探针脚本(`sys.executable` + 逐个 `import` + `importlib.metadata.version`) > 分别喂给三个解释器;脚本放仓外临时目录,不入库、不改任何环境。 | 解释器 | 身份 | 门禁相关模块 | 第三方包 | 结论 | |---|---|---|---|---| | `.venv\Scripts\python.exe` | 3.12.4(**基底是 Anaconda**,非干净 CPython) | `identify` 可导入、`pre_commit` 可导入;`identify.tags` **无此模块** | fontTools 4.51.0 / pillow 10.3.0 / pyserial 3.5 / pyyaml 6.0.3 / cfgv 3.5.0 全在,`pre-commit` 与 `identify` 的 dist-info 齐全(`pip show` 可查) | **就是钩子的执行环境**:`.git/hooks/pre-commit` 里 `INSTALL_PYTHON` 写死指向它 | | `C:\Python314\python.exe` | 3.14.7,PATH 首位 | 三个全缺 | **一个都没装**(`pip` 元数据全 `PackageNotFoundError`) | 「查不到 pre-commit」的现场就是它;`language: system` 的 42 条 entry 的裸 `python` 落在这里 | | `C:\Users\...\Python311\python.exe` | 3.11.9,PATH 次位 | 同上全缺 | 同上全缺 | 与门禁无关,列出来只为排除「装了但找不到」的猜测 | **为什么"缺 `identify.tags`"不是故障**:`identify-2.6.19.dist-info/RECORD` 一共 24 行,列出的模块只有 `__init__.py` / `cli.py` / `extensions.py` / `identify.py` / `interpreters.py` / `vendor/licenses.py`—— **根本没有 `identify/tags.py` 这一条**,而磁盘上的文件与 RECORD 逐项一致。即"元数据完好、文件齐全、 只是这个版本没有那个模块"。标签常量在这个版本里位于 `identify/identify.py`(`TEXT` / `BINARY` / `EXECUTABLE` / `ALL_TAGS`),pre-commit 4.6.2 自己也是从那里取的(`clientlib.py` 里 `from identify.identify import ALL_TAGS`),**全仓没有任何脚本 import `identify.tags`**(实测 grep 零命中)。 ⇒ 需要复算文件类型标签时,正确入口是 `identify.identify`;旧写法 `identify.tags` 在这个版本对上必然失败。 ### 5.1 新增钩子 entry 的硬规则(今天没炸的真正原因) `language: system` 的 entry 不继承 `.venv`:pre-commit 不改 `PATH`,所以裸 `python` 命中 PATH 首位 (本机 = 3.14.7,**零第三方包**)。实测本仓 45 条 system 钩子里 **43 条 entry 以裸 `python` 开头**, 今天能全绿的原因是(**2026-10-27 二次修正,改以机械口径为准**): 1. 这些条目**没有任何一条在模块级 import 第三方包**(抽测 bom-drift / layer-deps / doc-links 与一套 带 harness 的主机单测,在 3.14.7 下 exit 0); 2. 真正会碰第三方的是**下游脚本里的惰性导入**——典型是字体链 `tools/font_gen/make_font.py` 在函数体内 `from fontTools.ttLib import TTCollection`。它会不会被走到静态判不了,所以 `scripts/ci_local.py` 的 L0a 预检把这类只列成**候选**(实测列出 `font-charset` / `generated-fresh` 两条经下游引入 fontTools),摊开给人看、不作判决;判决只留给「模块级 import + 裸 python + 该解释器确实没装 + 脚本不自带解释器解析」四事齐备那种必然崩。 > ⚠ 本节 2026-10-27 的早先版本写作「42 条里唯一需要第三方包的那条自带解释器解析」。那是我拿一条宽松 > 正则(连注释与字符串里的包名都算命中)审计出的**保守过头的结论**,条目数也因 `nested-comment` 钩子 > 落地从 42 变 43。现在这条口径由 L0a 预检每次跑门禁前重算,人不再需要记这个数。 ⇒ 写新钩子时二选一,别开第三条路:要么脚本只用标准库,要么在脚本内部按 「显式变量 > 当前解释器 > 仓库 `.venv` > PATH」自行解析并**探针校验**(先例与理由见 `docs/门禁说明/generated-fresh.md` §4)。**不要**把 entry 写死成某台机器的绝对路径。 ### 5.2 动环境的失效顺序与回滚(含本笔机械化) **先把"谁依赖谁"钉住,才知道动一下会红在哪一步**(下表每行都是本机实测或代码事实,不是推测): | 被动什么 | 第一现场 | 症状 | 还能动什么 | |---|---|---|---| | 删 / 重建 `.venv` | `.git/hooks/pre-commit` 里的 `INSTALL_PYTHON` 指它 | `git commit` **当场起不来**:钩子脚本先判 `if [ -x "$INSTALL_PYTHON" ]`,不成立才回落到 PATH 上的 `pre-commit`——本机 `where.exe pre-commit` 报找不到,于是回落分支也是死路 | 重建 `.venv` 后必须 `pip install -r requirements.txt`(含 pre-commit),否则回落分支永远还是死路 | | 只换 `.venv` 里的包(不删目录) | pre-commit 本体与 `language: python` 钩子各自独立的缓存环境(`~/.cache/pre-commit/repo*/py_env*/`)互不相干 | 只影响 pre-commit 本体;改写型钩子跑在缓存环境里,**看不见** `.venv` 的变动(实测那份缓存环境里连 `identify` 都没装) | 缓存环境坏了要 `pre-commit clean`——它会连别人正在跑的钩子环境一起重建,必须先要到静默窗口 | | 换 PATH 首位解释器 | 那 43 条裸 python 条目 | 模块级第三方 import 一条都没有 ⇒ 普通情况下无感;但 3.12 → 3.14 这种版本跨度会改变标准库行为,是"靠巧合不炸"的敞口 | 设 `DTU_PRECOMMIT_PYTHON` 只钉得住 pre-commit 本体,**钉不住**裸 python 条目的落点——那只能靠脚本自解析 | | 改 `requirements.txt` 的版本约束 | 别人机器上的一次 `pip install -r` | 本机无感,别人下次重建环境时才炸 | 这不属于"修本机环境",要改先报属主,别夹在诊断笔里 | **回滚按现场分条,别混用**: - `.venv` 被删 / 建坏:`python -m venv .venv` → `pip install -r requirements.txt` → `& '.venv\Scripts\python.exe' -m pre_commit --version` 通 → `ci_local.py --preflight-only` 通 → 再跑完整门禁。 - 单包装坏:按修复前那次 `pip freeze`(放仓外,不入库)反向 `pip install <包>==<原版本>`; **不要** `-U` 顺手把别人的依赖一起动。 - 缓存环境坏:`pre-commit clean` 后跑一次 `ci_local.py` 让它重建;重建期间别提交(几十秒内会拿到半装好的环境)。 - 只有全坏透才删 `.venv` 重来;删前记下 `pip freeze`,并记住本仓 `.venv` 基底是 Anaconda (换机复制不出来,见上面 §5.2 依赖表与 §5.3 第 5 条)。 **本笔把上面这些机械化了一半**:`scripts/ci_local.py` 新增 **L0a 钩子解释器预检**(`--preflight-only` 可单跑,也是 `ci_local.py` 每次的第一段),报清"钩子跑在谁身上 / 裸 python 落到谁 / 那个解释器缺什么 / 哪些只是候选",四事齐备即 exit 2 并指名到钩子。红-绿在临时假仓库上验过(同一份配置里 bad-hook 判 FAIL、 good-hook 判 OK,整体 `rc = 2`)。**它不改环境、不装包、不写仓库**,也不替代上面的修复死顺序—— 它只保证一件事:这条链下次断掉时报错发生在第一秒,并且带着钩子的名字。 ### 5.3 什么情况下才需要动环境,以及动之前要走死顺序 默认动作是**不动**(本节存在即结论:没有需要重装的东西)。只有出现下列**具体现象**才执行修复, 且必须先要到静默窗口——`pre-commit` 与 `.venv` 是全队共享运行面,中途换包会把别人的一次钩子跑打断: 1. 停表:确认没有人在跑提交/门禁/台架(build 与 bench 两类锁都空闲,见 `task-worktree`); 2. 备份指针:记下 `& '.venv\Scripts\python.exe' -m pip freeze > 仓外文件`(不入库); 3. 只补不重建:优先 `pip install --force-reinstall --no-deps <包>==`, **不要**顺手 `-U`(那会连带动别人的依赖);确实要整体重建才 `pip install -r requirements.txt`, 且该文件必须保持纯 ASCII(Windows 下 pip 按代码页解码); 4. 自证(缺一不算修完):`& '.venv\Scripts\python.exe' -m pip show pre-commit` 有版本 → `-m pre_commit --version` 通 → `scripts/check_generated_fresh.py` 与字体字符集钩子在 `.venv` 与 PATH 两种解释器下都 exit 0 → 最后跑一次 `scripts/ci_local.py`; 5. 回滚:新装/升级出问题时按第 2 步的 freeze 反向 `pip install <包>==<原版本>`;**别删 `.venv` 重来**, 删了就把「基底是 Anaconda」这条不可复制性也一起删掉(换机复现不了,见本文 §3 与 `docs/环境与部署/项目部署环境要求.md`)。 ### 5.4 仍开着的账(不在本次范围,登记给对应属主) - `DTU_PRECOMMIT_PYTHON` 本机当前**未设**,靠 §1.1 的优先级自动落到 `.venv`;如果哪天 PATH 首位换了 解释器,静态门禁档就会跟着换——把它显式设成 `.venv` 是一条零成本的加固(改的是本机环境,不动仓库)。 - 42 条裸 `python` entry 与两个解释器的版本差(3.14 vs 3.12):今天没有可观测的行为差,但这是 「靠巧合不炸」。要不要把 entry 统一改成显式解释器解析,属门禁属主的决定(同一族的先例见 §5.1)。 - 本机的 `.venv` 基底是 Anaconda 发行版,`python -m venv` 在别的机器上给出的不是同一个东西; 这条差异目前无人记录在 `docs/环境与部署/项目部署环境要求.md` 里。 ## 变更记录 | 日期 | 变更 | |---|---| | 2026-10-27 | **新增 §5.2 动环境的失效顺序与回滚(原插成 §5.4,本笔按依赖表→死顺序→遗留账重排)+ §5.1 二次修正(道G task-33 修复分支)**:四种现场(删/换 `.venv`、只换包、换 PATH 首位、改 requirements 约束)逐行钉死"第一现场 / 症状 / 还能动什么",回滚按现场分条不混用;`scripts/ci_local.py` 同批加 **L0a 钩子解释器预检**(`--preflight-only` 单跑,也是每次门禁的第一段:报"钩子跑在谁身上 / 裸 python 落到谁 / 缺哪些包 / 哪些是惰性候选",四事齐备 exit 2 指名到钩子,红-绿在临时假仓库验过)。§5.1 那条「42 条里唯一需要第三方包的那条自带解释器解析」被更严的口径推翻:按**模块级 import** 判是 0 条,条目数也因 `nested-comment` 落地变 43;fontTools 属下游**惰性**导入,预检只列候选不判决。 | 2026-10-27 | **新增 §5 解释器实测矩阵(道G task-33,只读诊断,未动任何环境)**:三个解释器(仓库 `.venv` / PATH 首位 / PATH 次位)逐个探针实测,钉住三条事实——① `.venv` **不是残缺**:`pre-commit` 与 `identify` 的 dist-info 齐全(`pip show` 可查),`identify-2.6.19` 的 RECORD 里本就没有 `tags.py`,标签常量在该版本的 `identify.identify` 里,pre-commit 自己也是从那里取(旧写法 `identify.tags` 在这个版本对上必失败,全仓零引用);② 钩子执行环境被 `.git/hooks/pre-commit` 的 `INSTALL_PYTHON` 钉死为 `.venv`,而 `language: system` 的 42 条 entry 用裸 `python` → 落到 PATH 首位那个**零第三方包**的解释器,今天没炸是「42 条全只用标准库 + 唯一需要第三方包的那条自带解释器解析」两件事同时成立,据此写下新增 entry 的硬规则(§5.1);③ 「查不到 pre-commit」的现场是查了 PATH 上的解释器。另附 §5.2「默认不动环境;确需修复时的死顺序与回滚(只补不重建、freeze 备份放仓外、按 freeze 反向回滚、禁止删 `.venv` 重来)」与 §5.3 三条待属主的账。| 2026-10-02 | **文档改造**:删除规划阶段内容(设计结论、路径耦合盘点、代码改造映射、分阶段推广与验收、风险与边界、后续任务拆分),保留并重组为现行机制说明——§1 变量清单(含 Python 优先级特别说明)、§2 解析与失败语义、§3 本机配置方法(会话/用户/runner 三级)、§4 验收判据、变更记录;章节重编号,外部引用同步(`gen_refgraph.py` §3→§1、`ci_local.py` §3.1→§1.1、`Gitea_Runner部署指南.md` §5.3→§3.3) | | 2026-10-02 | **实施完成**:阶段 A~D 落地。① 10 处调用点全部改为"`DTU_*` 变量优先 + 自动发现回退"(显式值无效 = 硬报错、不静默回退):`build/download/reset.bat`(goto 标签式解析,纯 ASCII)、`ci_local.py`(`DTU_PRECOMMIT_PYTHON` 严格校验)、`ci.yml`(`DTU_PRECOMMIT_PYTHON`/`DTU_CPPCHECK`/`DTU_MSVC_CL`/`DTU_WINDOWS_KITS_ROOT`,run 块纯 ASCII)、`gen_refgraph.py`(`DTU_GRAPHVIZ_DOT`→PATH,保留无渲染降级)、`unit_test_harness.py`(`DTU_MSVC_CL`/`DTU_WINDOWS_KITS_ROOT` + `%ProgramFiles*%` 推导取代静态盘符列表)、`build_sim.ps1`(`DTU_VSWHERE`→`%ProgramFiles(x86)%` 推导,去字面 C 盘回退)、`make_font.py`(`%WINDIR%` 推导);`_build.cmd` 确认零调用后删除。② 本机按 §3.2 配置用户级 `DTU_KEIL_UV4`/`DTU_STLINK_CLI`(其余工具本机均可自动发现,未设变量),验证:`@build` BUILD_OK 0/0、`ci_local.py` L0 15 钩子 + L0.5 278/278 断言全绿、refgraph 重生成(含修复 `引用关系图.json` 末尾换行缺失导致的 end-of-file-fixer 抖动,生成器补 `f.write("\n")`)、`build_sim.ps1` vswhere 推导命中 VS2026(当时模拟器 CMake 配置期暴露**既有**问题:`App/cfg_file.c` 已移入 `App/cfg/` 但 `simulator/CMakeLists.txt` 未同步——与环境变量改造无关;2026-10-02 已修复:CMakeLists 同步 `App/cfg/` + `test/board/` 引用,模拟器全量构建通过)、`reset.bat list` 识别探针。③ 文档收口:`AGENTS.md`/`MDK-ARM/README.md`/`项目部署环境要求.md`(新增 §2.1.1)/`Gitea_Runner部署指南.md`(新增 §5.1.3 机器级变量)/`CI配置指南.md`/`pre-commit使用说明.md`/字体/测试流程/引用图说明/ST-LINK 说明/实机规划/`scripts/README.md` 共 12 篇同步为变量口径。④ `requirements.txt` 增 `pre-commit==4.6.2`(重建 `.venv` 即可跑 L0)。⑤ 全量 `pre-commit --all-files` + `ci_local.py` 通过。待办:新 workflow 推送远程后跑一次 CI 两 job 闭环验证;第二台不同安装路径机器的交叉验证(操作清单已固化入 `项目部署环境要求.md` §2.12) | | 2026-10-02 | 初版规划:盘点 BAT/Python/PowerShell/Gitea workflow 中机器相关工具路径;定义 `DTU_*` 变量清单、优先级、用户级与 SYSTEM runner 配置边界;规划 4 阶段迁移及离线/实机验收范围。未改工具调用代码 |