c2671e5c2f
- What: 新增 `apps/studio/src/orisys_studio/session_metadata.py`(273 行,纯函数 + 一个 `SessionFacts` 数据类,无 Qt、无状态),把 `StudioMainWindow` 里"记录必须自述"的那部分逻辑搬过去: `config_sha256` / `sdk_version` / `device_serial` / `capture_metadata` / `filter_metadata` / `filter_settings_from_prefs` / `video_fps` / `device_sample_rate` / `build_session_metadata`。 `app_window` 侧只留一行转发(保留 `_config_hash`/`_sdk_version`/`_device_serial`/`_capture_metadata`/ `_filter_metadata`/`_filter_settings`/`_video_fps`/`_device_sample_rate` 这些名字,因为测试与 `ui_check` 会直接调它们),`_build_session_metadata` 缩减为"把只有窗口能看见的事实装进 `SessionFacts`,然后交给模块"。`app_window.py` 3045 → 2901 行(-196/+51)。 - Why: 用户已同意分拆 `app_window.py`(当前 155 个方法、fan-out 36、唯一 fan-in=1 的中心),并选定四个 新模块的命名。A 步(会话/采集/记录)本身约 1100 行、40 处调用点,一次搬完风险高,所以按"两种变化原因 分开命名"的原则切成两步:**A1(本提交)= 无状态的事实与规则**,A2 = 带状态的 `SessionController` (worker 生命周期、phase、定时器、packet 扇出),后者将调用本模块组装记录块。 顺带把这几条规则从"只在记录流程里被间接覆盖"变成可单测:录制视频的 fps 对相机取**配置值**、对文件取 **实测值**、且永不写 0 fps;滤波器从**真正运行它们的 worker** 读回(旧 SDK 可能少应用),并把 `unsupported_by_sdk` 明确记下;任何元数据读取失败都只是空块,绝不让一次记录失败。 - Verification: 新增 `apps/studio/tests/test_session_metadata.py` **15 项**:配置内容哈希(含缺失/空路径)、 SDK 版本恒为字符串、设备序列号三种字段与嵌套 `info.SN`、capture 状态缺失/抛异常都返回空块、 滤波器回读与 `unsupported_by_sdk`、空/损坏 preferences 等于"什么都不滤"、 相机 vs 文件的 fps 规则与 0 fps 兜底、Nyquist 用的采样率来源与垃圾输入、完整 session 块(设备/版本/采集/ 标定/深度/约定/滤波/录像模式/video_config/3D 块逐项断言)、worker 消失且从未收到包时仍产出完整块、 3D 描述原样透传、三种录像模式如实记录。全量:shared+apps 同一次运行 **603 项 0 失败 15 跳过** (较上一提交 +15)、`ui_check` 587 项 0 失败、`locale_check` 0 缺失。 - Not in this change: 行为零改动(同一批函数、同一批默认值),也没有搬任何**状态**——`_latest_fps`/ `_latest_process_ms`/`_latest_capture_*`/`_record_error_count`/`_capture_checked`/`_dropped_reported` 仍在窗口里,A2 连同 worker 生命周期、phase、定时器与 packet 扇出一起搬; 回放(B)与视图/显示(C)两个控制器未开始。四个模块的名字已定: `SessionController` / `ReplayController`(引擎已改名 `ReplayEngine`)/ `ViewLayoutController` / `DisplayPrefsController`。