Files
STM32F4-Base/docs/环境与部署/本地工具路径环境变量方案.md
T
wandering1 f5d524fdde feat(门禁): 把「钩子跑在谁身上」机械化——ci_local 新增 L0a 解释器预检 + 环境动手术的顺序与回滚
task-33 诊断结论的落地(道G 执行,环境**一个字节没动**:不装包、不重建、不改 requirements 约束)。

**要防的是什么**:`.git/hooks/pre-commit` 把 `INSTALL_PYTHON` 钉在 `.venv`,而 43 条
`language: system` 用
裸 `python` ⇒ pre-commit 本体走 `.venv`(3.12.4)、钩子 entry 走 PATH 首位(3.14.7,零第
三方包)。今天
不炸是**巧合**(那 43 条模块级 import 全是标准库,唯一要 fontTools 的那条自己解析解释器)。真正的危
险形态是:某条钩子在**跑到它那一刻**才报 `No module named ...`——一次提交里前 14 条已经改写完工作区、
第 15 条才崩,现场最难看,也最容易被误当成代码问题。L0a 把这件事提前到门禁第一段。

**FAIL 判据刻意收窄成三事齐备**:模块级第三方 import + 该脚本不自解析解释器 + PATH 首位那个解释器
真缺包。惰性/条件导入(`make_font` 的 fontTools 在函数体内)只列为**候选**、不判决——否则就是假阳性
(见下)。退出码 2 = 环境错误,已写进文件头退出码段与用法。

**道G 自己推翻过自己两处,都留在文档里没删**:
① 上一版诊断说「42 条里唯一需要第三方包的那条自带解析」是**宽松正则**审出来的(把注释与字符串里的包
   名也算命中),机械口径下按模块级 import 判是 **0 条**,条目数也因 nested-comment 落地从 42 变 43;
② 第一版 L0a 把 font-charset 判成 FAIL,他拿真解释器实跑反证(`C:\Python314\python.exe
   scripts/check_font_charset.py` = exit 0,因为 fontTools 的导入在函数体内、加载期不执行)
⇒ 判据改窄。
**红-绿自证**:临时假仓库三条钩子(要 fontTools 且不自解析 / 同样要但自解析 / 纯标准库)→ 坏的那条判
FAIL、好的判 OK、整体 rc=2;真实仓 → 43 条、forced 缺包 0、候选 fontTools、exit 0;同一预检在 .venv 与
PATH 首位两个解释器下都 exit 0。

文档 §5 重排成**依赖表(§5.2) → 动之前死顺序(§5.3) → 遗留账(§5.4)**(原文按追加顺序把失效顺序插成了
§5.4,读起来先结论后依据;重排时同步修掉块内那条会指错的「见下面 §5.3 第三条」,以及 ci_local 里指向
死顺序却写着 §5.2 的指针——这两处 doc-links 都**静默放过**,因为本文恰好有同号小节,见 §3.5 盲区)。
四种现场(删/换 `.venv`、只换包、换 PATH 首位、改 requirements 约束)逐行钉死「第一现场 / 症状 / 还能
动什么」,回滚按现场分条不混用:freeze 备份放仓外、只 `--force-reinstall --no-deps` 单包不 `-U`、
`pre-commit clean` 会重建别人的钩子环境所以必须先要窗口、最后手段才删 `.venv`(删前记住基底是 Anaconda,
换机复制不出来)。

⚠ 一条与本笔无关的既有红灯(道G 报出,我核过成因):`check_size_gate.py --local` 现在 exit 1 —— 它按
**git 索引 blob** 生成代码指纹,而工作区正有 comm 道在飞的 `App/protocol/iec104/iec104_comm
.{c,h}` 未入
索引,「同码」必然不成立;**该脚本不是 pre-commit 钩子**(只挂在 ci_local),所以不拦提交,但别人跑
ci_local 会看到它,别误判成 L0a 新加的东西。台账回填等这批在飞代码提交后统一做一次。

自查:py_compile 0、`--preflight-only` exit 0(两个解释器下都是)、`check_doc_links --a
ll` exit 0(198 篇)、
本篇 `--sections-strict` 通过、`id-check --strict` exit 0 且不可判定绝对数 **978 未变**
(写文档没吃余量)、
mojibake / bom(81) / nested-comment(409) 全 0、尾随空白 0。
2026-10-27 09:42:20 +08:00

22 KiB
Raw Blame History

本地工具路径环境变量方案

本文档描述项目的本机工具路径环境变量机制:变量清单、解析与失败语义、各级配置方法、验收判据 (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 会话(立即生效,适合试配置)

# 仅当前会话有效;换终端/重启即失效
$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 用户级持久配置(日常开发机,推荐)

# 当前用户、全部新进程可见;无需管理员
[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 服务)

# 必须管理员;写入机器级(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 <包>==<requirements.txt 里的版本>, 不要顺手 -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-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 阶段迁移及离线/实机验收范围。未改工具调用代码