# 发布打包最终只读复审 日期:2026-08-11(Asia/Shanghai) 目标:判断是否可以**从当前工作树**重建 DoctorWorkstation 发布包。 边界:未执行完整 PyInstaller,未修改应用/打包源码;仅新增本复审报告。 ## 最终结论 **结论:当前工作树已具备启动原生完整重建的条件,未发现构建前 packaging 代码级 P0。** - Windows 当前工作树可进入 `Build_DoctorWorkstation.bat` 完整重建;根发布入口的 `-ValidateOnly` 已通过。 - macOS 的 9 个操作入口在 Git index 中现已全部为 `100755`,入口合同会同时检查文件系统 `-x` 和 Git index mode,不再被 Windows/NTFS 的可执行位模拟掩盖。 - spec 已显式收集 `PySide6.QtMultimedia`/`QtMultimediaWidgets`,并安装独立 PyInstaller runtime hook。 - Windows/macOS build 均先检查冻结媒体文件,再依次执行 `--media-smoke-test` 与 `--smoke-test`;package 仅在 build 成功后归档和计算哈希。 - README、packaging README 与 `.env.example` 已说明 build/package 区别、双媒体门禁、macOS mode 以及 `.env` 不进入发布包的部署策略。 本结论只表示“可以开始原生重建”,不表示两个平台产物已经生成或可以立即对外发布。旧 `dist` 仍是修复前成品,必须由新构建替换;macOS `.app` 仍只能在原生 macOS 上产出和验收。 ## P0:无 按本轮定义——是否能从**当前本地工作树**发起完整重建——未发现 P0。 特别澄清:`src/doctor_workstation/ui/diagnosis_media.py`、`packaging/runtime_media_smoke.py`、`tests/test_packaging_media_gate.py` 等文件目前未跟踪,但它们真实存在于当前工作树,spec/测试/发布入口都能读取,因此**不是本地工作树构建阻断**。它们的未提交状态属于跨机和后续复现风险,列为 P1,而不是本轮 P0。 ## P1:完整发布前仍须完成 ### P1-1:必须运行新的原生完整构建,旧 `dist` 不得作为验收证据 当前 `dist/DoctorWorkstation` 和版本 ZIP 早于 QtMultimedia 修复,仍缺少新媒体模块/插件。根“一键运行”会优先启动这个旧 EXE,所以完整重建前不要用它判断当前代码。 Windows 必须重新运行: ```powershell .\Build_DoctorWorkstation.bat ``` 只有控制台依次报告以下门禁通过,且随后生成新 ZIP/哈希,才算 Windows 重建成功: 1. QtWebEngine helper/resources 与 TRTC companion 文件检查; 2. `Frozen Qt multimedia file gate passed`; 3. `Frozen Qt multimedia smoke gate passed (--media-smoke-test, ...)`; 4. `Frozen application entry smoke gate passed (--smoke-test, ...)`; 5. `Windows package complete`。 本复审按约束未运行完整 PyInstaller,因此不能提前宣称这些冻结态门禁已经在新产物上通过。 ### P1-2:macOS 必须在原生目标架构主机完成构建 Windows 只能验证 shell 语法和合同,不能交叉产生 `.app`、macOS framework、媒体插件或有效签名。必须在 arm64 或 x86_64 macOS 构建机执行: ```bash CI=1 DOCTOR_NONINTERACTIVE=1 bash scripts/check_macos_entrypoints.sh ./一键打包.command ``` 成功证据必须包括媒体文件 gate、两个冻结 smoke gate、`codesign --verify`、实际架构、版本 ZIP 与 `.sha256`。这是一项平台执行门禁,不是当前脚本缺陷。 ### P1-3:当前工作树尚未形成可跨机复现的发布 commit/tag 当前本机直接构建会包含未跟踪媒体源码和 runtime hook,因此可以构建;但新的 clone、CI runner 或另一台 macOS 机器不会从当前 HEAD 获得这些文件。当前还有多处修改、staged mode 变化和大量测试产物。 因此: - 内部本机技术构建可以现在开始; - 跨机 macOS 构建或正式发布前,发布负责人仍应审阅 intended source,提交必要文件及 9 个 mode 变化,并从确定的 commit/tag 构建; - 不应使用 `git add -A` 把 `.pytest-tmp-*`、`artifacts/` 等临时产物一并纳入。 这是正式发布的可复现性 P1,但按根任务要求不作为“当前工作树本地重建”P0。 ### P1-4:生产签名、公证链仍是发布策略门禁 - Windows package 生成 ZIP 与 SHA-256,但没有集成 Authenticode。 - macOS 接受可选 `MACOS_CODESIGN_IDENTITY` 并运行 `codesign --verify`,但不自动 notarize/staple;ad-hoc 签名也可能通过完整性验证。 内部 QA 包可以明确标记为内部构建。生产/院外分发则必须在归档前完成 Windows 签名;macOS 必须完成 Developer ID、嵌套 QtWebEngine helper 检查、notarization 和 stapling。staple 会改变 `.app`,之后要重新生成最终 ZIP 与 SHA-256。 ### P1-5:媒体 gate 不替代真实播放和 TRTC 准入 新 `--media-smoke-test` 已真实创建 Qt 媒体对象并检查 decoder backend,但没有加载具体 MP4/HLS,也不请求摄像头/麦克风或加入 TRTC 房间。正式验收仍需在最终冻结包上覆盖: - 获准分发的 MP4,产品要求时再覆盖 HLS; - 画面渲染与音频输出; - QtWebEngine companion/WebChannel; - 摄像头、麦克风、扬声器切换; - TRTC 测试房间、弱网、设备插拔和休眠恢复。 ## P2:建议改进 1. 仓库仍无 `.gitattributes`,本机 `core.autocrlf=true`;当前 shell 文件是 LF 且 `bash -n` 通过,但建议固定 `*.sh`/`*.command` 为 LF。 2. Windows 使用 `uv sync --frozen`,macOS 使用 `uv sync --locked`;建议补 `uv lock --check` 或统一 lock 新鲜度策略。 3. Windows `version_info.txt` 的 `0.1.0.0` 与 pyproject `0.1.0` 当前一致,但仍是人工同步,建议增加版本一致性门禁。 4. `Build_DoctorWorkstation.bat -ValidateOnly` 仍输出 `Package ready in: ...`,容易让人误以为已经生成新包;退出码和行为本身正确。 5. Windows/macOS 的哈希文件命名分别为 `SHA256SUMS.txt` 与 `.sha256`,可统一以简化发布自动化。 6. spec 尚未设置原生 `.ico`/`.icns`;不影响构建或媒体能力,但正式品牌发布可补齐。 7. `research/release_packaging_audit.md` 与 `release_packaging_gate_fixed.md` 是阶段性历史记录,其中关于旧 mode/旧产物的描述不能替代本最终复审。 8. AGENTS.md 引用的 `.trellis/workflow.md` 和 `.trellis/spec/` 在当前工作区仍不存在;属于治理缺口,不是本地构建阻断。 ## 独立复审结果 ### 1. PyInstaller spec:PASS `packaging/doctor_workstation.spec` 现在: - 显式 hidden import: - `PySide6.QtMultimedia` - `PySide6.QtMultimediaWidgets` - 继续显式收集 QtWebEngine 模块; - 将 `video_companion/dist` 放入 `video_companion_dist`; - 要求 `packaging/runtime_media_smoke.py` 必须存在; - 以 `runtime_hooks=[str(MEDIA_SMOKE_HOOK)]` 安装媒体 gate。 当前 PyInstaller 6.22 的官方 `hook-PySide6.QtMultimedia.py` 会追加 Widgets 模块,Qt6 module mapping 会收集 `plugins/multimedia`,与本 spec 的显式 imports 一致。 ### 2. `--media-smoke-test`:PASS `packaging/runtime_media_smoke.py` 作为 runtime hook,在普通启动时无副作用;仅当 argv 含 `--media-smoke-test` 时才在应用入口前执行: - 导入 `QMediaPlayer`、`QAudioOutput`、`QMediaFormat`、`QVideoWidget`; - 创建并连接 player/audio/video 对象; - `player.isAvailable()` 必须为真; - Decode 模式至少暴露一种支持格式; - 显示 16×16 offscreen video widget,并完成一次 Qt event loop; - 任一错误固定返回 70,成功返回 0。 独立测试验证了三种状态:普通 argv 惰性退出 0;隔离掉 site-packages 后媒体 argv 返回 70;当前 PySide6 环境 offscreen 实例化返回 0。 ### 3. Windows build/package:PASS(静态与轻量合同) `scripts/build_windows.ps1` 在 PyInstaller 后要求以下文件: - `QtMultimedia.pyd` - `QtMultimediaWidgets.pyd` - `Qt6Multimedia.dll` - `Qt6MultimediaWidgets.dll` - `plugins/multimedia/ffmpegmediaplugin.dll` - `plugins/multimedia/windowsmediaplugin.dll` 随后先运行 `--media-smoke-test`,再运行原 `--smoke-test`;两者均沿用隔离配置目录、loopback-only proxy、offscreen、30 秒超时、退出码和未捕获异常日志检查。 `scripts/package_windows.ps1` 把 runtime hook 加入 required files,调用 build 之后才复制 release launcher、压缩 ZIP 和生成 SHA-256。`Build_DoctorWorkstation.bat -ValidateOnly` 本轮通过。 ### 4. macOS build/package:PASS(静态与轻量合同) `scripts/build_macos.sh` 在成功前要求: - `PySide6.QtMultimedia` 与 `QtMultimediaWidgets` Python `.so`; - 对应 framework,或适配 dylib 布局的后备检查; - `plugins/multimedia` 目录; - 至少一个 `*mediaplugin*.dylib/.so` backend; - QtWebEngine helper/resources、companion 与主可执行文件。 之后执行 `codesign --verify`,再顺序运行媒体 smoke 和应用 smoke。`scripts/package_macos.sh` 在准备依赖前先运行入口合同,并只在 build 全部成功后用 `ditto` 归档和计算哈希。 这些路径模式与当前 PySide6/PyInstaller 的 macOS framework/plugin 布局相容;最终仍需原生构建确认。 ### 5. macOS Git executable mode:PASS 以下 9 个文件的 index mode 本轮均为 `100755`: 1. `package_macos.command` 2. `run_macos.command` 3. `一键打包.command` 4. `一键运行.command` 5. `scripts/build_macos.sh` 6. `scripts/check_macos_entrypoints.sh` 7. `scripts/macos_helpers.sh` 8. `scripts/package_macos.sh` 9. `scripts/run_macos.sh` `scripts/check_macos_entrypoints.sh` 已将自身纳入 operational files,逐个执行 `bash -n`、检查实际 `-x`,在 Git 工作树中还读取 index mode 并要求 `100755`。本轮 Git Bash 合同执行通过。 ### 6. README 与 `.env`:PASS - 根 README 正确区分 `build_*`(只生成/验证 onedir 或 `.app`)和根/package 入口(生成版本 ZIP 与哈希)。 - Windows/macOS 一键打包描述已包含 QtWebEngine/QtMultimedia 文件门禁与两个冻结 smoke。 - README 明确 macOS 操作文件必须以 mode `100755` 跟踪。 - `.env.example` 与 README 明确该文件不进入 ZIP/`.app`;生产配置应由受控 launcher/MDM/进程环境注入。 - 文档明确禁止把密码、token、UserSig、TRTC SecretKey 等凭据放进 `.env` 或发布包。 ### 7. 输出合同:PASS(待新构建兑现) 输出命名未被修复破坏: - Windows onedir:`dist/DoctorWorkstation/` - Windows ZIP:`dist/DoctorWorkstation-Windows-x64-.zip` - Windows checksum:`dist/SHA256SUMS.txt` - macOS bundle:`dist/DoctorWorkstation.app` - macOS ZIP:`dist/DoctorWorkstation-macOS-{arm64|x64}-.zip` - macOS checksum:`.sha256` 两个 package 流程都位于 build/gate 之后,失败不会进入新的归档步骤。 ## 本轮实际执行的轻量检查 | 检查 | 结果 | |---|---:| | Windows 三份 PowerShell parser | PASS | | `Build_DoctorWorkstation.bat -ValidateOnly` | PASS | | 全部 macOS shell/command `bash -n` | PASS | | `scripts/check_macos_entrypoints.sh`(含 index mode) | PASS | | packaging media gate + one-click + entrypoint pytest | **10 passed** | | runtime hook/packaging test Ruff | PASS | | relevant tracked diff whitespace check | PASS | pytest 首次执行曾因沙箱无权枚举系统 `pytest-of-pc` 临时目录而产生 1 个 setup error;改用工作区内独立临时目录重跑后 10 项全部通过,并已安全清理该目录。该环境错误不是代码失败。 ## 主代理下一步 ### 当前 Windows 工作树 ```powershell .\Build_DoctorWorkstation.bat -ValidateOnly .\Build_DoctorWorkstation.bat ``` 完成后不要只看退出码;确认新 onedir/ZIP 时间戳、媒体文件、两个 smoke gate 日志及 SHA-256,并在无开发环境机器上做真实媒体/TRTC 验收。 ### 原生 macOS 先确保目标机器拿到与当前工作树等价的全部 intended files(正式发布推荐先形成 commit/tag),然后: ```bash git ls-files --stage -- '*.command' 'scripts/*.sh' CI=1 DOCTOR_NONINTERACTIVE=1 bash scripts/check_macos_entrypoints.sh export MACOS_CODESIGN_IDENTITY='Developer ID Application: <组织名称> ()' ./一键打包.command ``` 随后验证 `.app` 架构、媒体 framework/plugin、两个 smoke gate、签名、公证/staple、最终 ZIP 哈希和真实设备/TRTC 流程。