# DoctorWorkstation 发布打包就绪审计 审计日期:2026-08-11(Asia/Shanghai) 审计主机:Windows 10/11 工作区 `D:\web\zyt\app` 审计边界:只检查入口、脚本、PyInstaller spec、冻结依赖、伴随页、文档和既有产物;未执行完整 PyInstaller,未改业务/打包源码。 ## 结论 **当前状态:不可直接发布既有 `dist`,也不应立即从当前 Git 状态制作正式发布包。** 构建流水线的主体是完整的:Windows/macOS 均有根目录中英文入口,能从锁文件准备 Python/Node 依赖,能构建 TRTC Web companion,PyInstaller 使用 onedir/.app,构建后会检查 QtWebEngine 与 companion,并在隔离目录中运行冻结入口 `--smoke-test`。Windows 既有 ZIP 的命名和 SHA-256 也正确。 但发布前有三个 P0:macOS 入口在 Git 中没有可执行位;新增 QtMultimedia 业务文件仍未纳入版本控制且工作树存在大量未提交发布输入;当前 Windows 成品早于新增媒体代码,缺少 QtMultimedia Python 模块、Widgets 库及媒体插件。另有 P1:当前冻结烟测会在媒体能力缺失时仍通过,生产级签名/公证也尚未进入一键流水线。 **macOS 特别说明:PyInstaller 不能从 Windows 交叉产出或验收 `.app`。本审计只能检查 macOS 脚本和合同;最终 `.app`、原生 Qt/媒体插件、签名、摄像头/麦克风权限及 TRTC 实通必须在原生 macOS(对应 arm64 或 x86_64)上构建和验证。** 严重度定义: - P0:在任何正式构建/分发前必须解决。 - P1:可做内部技术构建,但生产发布前必须解决或由发布负责人书面接受风险。 - P2:不阻断本次内部构建,建议纳入发布工程改进。 ## 构建前阻断 ### P0-1:macOS Finder/命令行入口在 Git 索引中全部为 `100644` `git ls-files --stage -- '*.command' 'scripts/*.sh'` 显示以下文件均为 `100644`,不是 `100755`: - `run_macos.command` - `package_macos.command` - `一键运行.command` - `一键打包.command` - `scripts/build_macos.sh` - `scripts/package_macos.sh` - `scripts/run_macos.sh` - `scripts/macos_helpers.sh` - `scripts/check_macos_entrypoints.sh` 影响:新的 macOS clone 中,Finder 双击 `.command` 和 README 中的 `./scripts/build_macos.sh` 不能满足“一键运行/一键打包”;`scripts/check_macos_entrypoints.sh` 自己也要求这些入口 `-x`。Windows/NTFS 下 Git Bash 把文件视为可执行,因此当前合同检查通过并不能覆盖 Git 模式错误。 发布前处理: ```powershell git update-index --chmod=+x run_macos.command package_macos.command ` "一键运行.command" "一键打包.command" ` scripts/build_macos.sh scripts/package_macos.sh scripts/run_macos.sh ` scripts/macos_helpers.sh scripts/check_macos_entrypoints.sh git ls-files --stage -- '*.command' 'scripts/*.sh' ``` 第二条命令的每一行都必须以 `100755` 开头,并将模式变化提交到发布 commit。 ### P0-2:当前发布输入不在可复现的 Git 状态 审计时工作树含多处已修改和大量未跟踪代码;与本次媒体发布直接相关的 `src/doctor_workstation/ui/diagnosis_media.py` 及对应测试仍是未跟踪文件。若从当前目录构建,PyInstaller 会把这些本地文件打入包;若从当前 HEAD/干净 CI clone 构建,则不会包含它们。两者结果不一致。 发布前必须由负责人审阅 `git status --short`,只纳入确认属于本次版本的源码、测试、脚本和文档,并从确定的 commit/tag 构建。不要用 `git add -A` 掩盖当前大量测试产物和临时目录。 ### P0-3:既有 Windows ZIP/onedir 早于新增 QtMultimedia 代码,禁止分发 证据: - `dist/DoctorWorkstation/DoctorWorkstation.exe`:2026-08-10 17:07。 - `dist/DoctorWorkstation-Windows-x64-0.1.0.zip`:2026-08-10 17:08。 - `src/doctor_workstation/ui/diagnosis_media.py`:2026-08-11 01:05。 - 既有成品只有 `Qt6Multimedia.dll`/QML 辅助文件,缺少: - `PySide6/QtMultimedia.pyd` - `PySide6/QtMultimediaWidgets.pyd` - `PySide6/Qt6MultimediaWidgets.dll` - `PySide6/plugins/multimedia/ffmpegmediaplugin.dll` - `PySide6/plugins/multimedia/windowsmediaplugin.dll` 既有 ZIP 也缺少同一组文件。它的 SHA-256 文件本身有效,但只证明旧 ZIP 未损坏,不证明它对应当前源码。 此外,根目录“一键运行”优先启动既有 `dist`,所以当前直接双击会运行旧代码,不会验证当前工作树。正式验收前必须完成干净重建,并以新 ZIP 的时间戳、文件清单和哈希替换旧证据。 ## P1:生产发布前必须闭环 ### P1-1:QtMultimedia 依赖可用,但冻结收集没有独立门禁 通过项: - `pyproject.toml` 依赖 `PySide6>=6.8.2,<7`;`uv.lock` 当前锁定 PySide6/PySide6 Addons/Essentials 6.11.1,Addons 提供 QtMultimedia。 - 当前 `.venv` 和 `.venv-build` 均有 `QtMultimedia.pyd`、`QtMultimediaWidgets.pyd`、`Qt6MultimediaWidgets.dll` 以及 Windows `plugins/multimedia` 下的 FFmpeg/Windows Media 插件。 - 当前 PyInstaller 6.22 的官方 `hook-PySide6.QtMultimedia.py` 会追加 `PySide6.QtMultimediaWidgets`,Qt 模块映射会收集 Qt 6 的 `multimedia` 插件。当前源码中的静态 import 理论上会触发该 hook。 缺口: - `packaging/doctor_workstation.spec` 的显式 hidden imports 只有 QtWebEngine 相关模块,没有显式列出 `PySide6.QtMultimedia`/`PySide6.QtMultimediaWidgets`。 - Windows/macOS 构建脚本只检查 QtWebEngine helper、Chromium `.pak` 和 `video_companion_dist/index.html`,不检查 QtMultimedia Python 扩展、Widgets 库或媒体后端插件。 - `diagnosis_media.py` 捕获 `ImportError` 并降级到外部打开,因此媒体模块缺失不会让应用启动失败。 - 实测旧冻结包缺少上述模块/插件,但隔离执行 `DoctorWorkstation.exe --smoke-test` 仍返回 0。这证明现有烟测不是 QtMultimedia 发布门禁。 发布前至少应做到以下二者之一,建议二者都做: 1. 在 spec 中显式纳入 `PySide6.QtMultimedia` 和 `PySide6.QtMultimediaWidgets`,继续使用 PyInstaller 官方 hooks 收集平台插件。 2. 在两个构建脚本中增加平台化的成品断言;Windows 检查两个 `.pyd`、`Qt6MultimediaWidgets.dll` 和 `plugins/multimedia`,macOS 检查两个 Python 扩展、Qt multimedia framework/dylib 和 `plugins/multimedia`。 最终验收还要用一个获准分发的 MP4(以及产品真实需要时的 HLS)实际播放,不能只看文件存在。 ### P1-2:`--smoke-test` 只证明冻结启动,不证明 WebRTC/媒体可用 当前烟测的隔离、30 秒超时、非零退出码和未捕获异常日志检查设计良好;Windows/macOS 都不会访问真实后端。它会启动 composition root,并在约 1.2 秒后退出。 它不执行以下能力: - 创建 `QMediaPlayer` 并确认媒体 backend 被发现; - 播放音视频并检查解码/渲染; - 打开 `QWebEngineView` 的 companion 主文档; - 执行 `TRTC.isSupported()`、请求摄像头/麦克风或加入真实测试房间; - 验证设备插拔、休眠恢复、弱网、屏幕共享或录播 codec。 因此冻结烟测应保留为启动门禁,同时新增“冻结媒体插件合同”和原生机器上的手工/自动媒体准入。README 已提示首发要做真实 TRTC/设备验收,这项不能被 `--smoke-test` 替代。 ### P1-3:生产分发签名链未纳入一键打包 Windows:当前流水线生成 ZIP 与 SHA-256,但没有 Authenticode 签名步骤。SHA-256 提供完整性,不提供发布者身份;对外分发前应在压缩前签名并验证最终可执行文件。 macOS:spec 接受可选的 `MACOS_CODESIGN_IDENTITY`,构建脚本只运行 `codesign --verify --deep --strict`。未提供 Developer ID 时,PyInstaller 的临时/ad-hoc 签名也可能通过这一完整性检查;脚本不执行 notarization/stapling,也没有验证嵌套 `QtWebEngineProcess.app` 的实际 entitlements/运行能力。当前 README 仅把签名、公证作为人工说明。 若本次只是内部 QA 包,应明确标注“未签名/未公证内部构建”。若是生产或院外分发,应先把 Windows 签名,以及 macOS Developer ID → nested code 检查 → notarization → stapling → 重新生成最终 ZIP/哈希,纳入发布流水线。注意:staple 会改变 `.app`,所以不能继续分发公证前生成的 ZIP。 ### P1-4:macOS 成品尚无原生验证证据 Windows 上的 `bash -n` 和合同脚本只能证明 shell 文本结构。以下证据必须在 macOS 构建机补齐: - `.app` 原生架构与最低系统版本; - `QtWebEngineProcess.app`、Qt framework、QtMultimedia 插件实际存在并签名有效; - `NSCameraUsageDescription`/`NSMicrophoneUsageDescription` 能触发正确权限流程; - companion 的本地 `file:`/qrc 资源、WebChannel 和 TRTC 能在打包态工作; - arm64/x64 输出名与实际二进制架构一致; - 最终 ZIP 的 Gatekeeper/notary 结果。 这不是 Windows 端可以规避的检查;必须安排一台原生 macOS runner/构建机。 ## P2:建议改进 1. 仓库没有 `.gitattributes`,而本机 `core.autocrlf=true`。当前 macOS shell 文件实际是无 BOM 的 LF,`bash -n` 通过,但建议用 `.gitattributes` 固定 `*.sh`/`*.command` 为 LF,并固定批处理/PowerShell 的预期行尾。 2. Windows 使用 `uv sync --frozen`,macOS 使用 `uv sync --locked`。当前 `uv.lock` 包含所需依赖,但 `--frozen` 不承担 lock 新鲜度检查;发布前可增加 `uv lock --check`,或统一为能拒绝 pyproject/lock 漂移的策略。 3. `packaging/windows/version_info.txt` 的 `0.1.0.0` 与 `pyproject.toml` 的 `0.1.0` 当前一致,但它是手工同步;脚本没有发布前一致性检查。升级版本时可能出现 ZIP 名与 EXE 版本资源不一致。 4. README 顶部的一键入口描述正确,但“测试与打包”段落调用 `build_*`,只产生 onedir/.app,不产生版本化 ZIP/哈希。建议明确区分“构建”与“发布打包”,并把 `package_*`/根入口列为正式发布命令。 5. `.env.example` 对 demo/生产、SSL、TRTC secret 禁止项说明清楚,未包含长期凭据;但发布 ZIP 不包含 README 或 `.env.example`,README 也没有明确冻结包从哪里查找 `.env`。若企业部署依赖环境文件,应说明冻结态放置/注入方式;若只允许登录页保存非敏感配置,也应明确说明无需随包放 `.env`。 6. Windows 用固定 `dist/SHA256SUMS.txt`,macOS 用相邻的 `.sha256`;两者都可校验,但发布自动化和用户说明可以统一。`dist` 还可能保留旧版本 ZIP,根打包入口成功后打开整个目录,建议在发布清单中明确唯一应交付的文件。 7. `Build_DoctorWorkstation.bat -ValidateOnly` 会打印 `Package ready in: ...`,即使它只做了入口预检;不影响退出码,但容易被误认作已生成新包。 8. spec 没有设置 Windows EXE/macOS bundle 的原生应用图标;窗口内 SVG 正常,但系统文件/Finder 图标仍是默认值。若品牌发布有要求,应在最终签名前补齐 `.ico`/`.icns`。 9. AGENTS.md 指向的 `.trellis/workflow.md` 与 `.trellis/spec/` 在本工作区不存在;本次无法执行项目内 Trellis 规范审查。这不影响二进制启动,但属于发布治理缺口。 ## 已通过的检查 | 检查项 | 结果 | 备注 | |---|---:|---| | Windows 根英文入口 `Build_DoctorWorkstation.bat -ValidateOnly` | PASS | Node/uv/必需文件预检通过 | | Windows 根英文入口 `Run_DoctorWorkstation.bat -ValidateOnly` | PASS | 当前命中既有冻结 EXE;不代表源码新鲜 | | Windows 两个中文别名 `-ValidateOnly` | PASS | 正确转发参数和退出码 | | PowerShell parser | PASS | `build_windows.ps1`、`package_windows.ps1`、`run_windows.ps1` 均无解析错误 | | macOS `bash -n` | PASS | helpers、run/package/build/check 与四个 `.command` 均通过 | | `scripts/check_macos_entrypoints.sh` | PASS(有限) | Git Bash 下合同通过;不能覆盖 Git `100644` 问题 | | 入口/冻结异常合同 pytest | PASS | `tests/test_one_click_entrypoints.py` + `tests/test_entrypoint.py`,4 passed | | 当前 source QtMultimedia import | PASS | `QMediaPlayer`、`QAudioOutput`、`QVideoWidget` 均可导入;两个 Windows multimedia plugin 可见 | | TRTC companion 工作树 | PASS | `video_companion` 无 Git 变更;dist 新于源码 | | companion 相对资源 | PASS | `base: './'`;index 引用的 JS/CSS 均存在 | | companion 打包副本 | PASS(旧包) | source dist 与旧 frozen 副本的 `index.html` SHA-256 相同 | | spec companion/resources 映射 | PASS | `video_companion/dist -> video_companion_dist`、`resources -> resources` 与运行时查找一致 | | QtWebEngine 收集策略 | PASS | 显式 imports 触发官方 hooks,构建脚本检查 helper 与 `.pak` | | 冻结启动烟测机制 | PASS(范围有限) | 旧 EXE 在隔离 demo/offscreen 环境中 `--smoke-test` 退出 0;同时暴露媒体缺口未被检测 | | Windows 输出名 | PASS(旧包) | `DoctorWorkstation-Windows-x64-0.1.0.zip` | | Windows SHA-256 | PASS(旧包) | `SHA256SUMS.txt` 与实际 ZIP hash 一致 | | 版本号一致性 | PASS | pyproject `0.1.0`;Windows resource `0.1.0.0` | ## 入口、spec 和输出合同摘要 ### Windows - 根入口:中文别名 → 英文 `.bat` → `scripts/package_windows.ps1`/`run_windows.ps1`。 - 运行:优先 `dist/DoctorWorkstation/DoctorWorkstation.exe`,否则使用 `.venv` 或 `uv sync --frozen` 后运行源码。 - 发布打包:Node 20+ → `uv sync --frozen --extra build` → `npm ci` → companion build → PyInstaller `--clean` → 静态成品检查 → 隔离冻结烟测 → ZIP → SHA-256。 - 产物: - onedir:`dist/DoctorWorkstation/` - 启动器:`dist/Start_DoctorWorkstation.bat` - ZIP:`dist/DoctorWorkstation-Windows-x64-.zip` - 校验:`dist/SHA256SUMS.txt` ### macOS - 根入口:四个 `.command` 解析自身目录后用 `/bin/bash` 调用 `scripts/run_macos.sh` 或 `scripts/package_macos.sh`。 - 运行:优先 `/usr/bin/open dist/DoctorWorkstation.app`,否则确保 uv、同步锁定依赖并运行源码。 - 发布打包:原生 Darwin 检查 → uv/Node 20+(必要时下载)→ `npm ci` → companion build → PyInstaller `.app` → QtWebEngine/companion 检查 → codesign verify → 隔离冻结烟测 → `ditto` ZIP → SHA-256。 - 产物: - bundle:`dist/DoctorWorkstation.app` - ZIP:`dist/DoctorWorkstation-macOS-{arm64|x64}-.zip` - 校验:同路径 `.sha256` ## 主代理最终构建与验证命令 以下命令应在 P0 修复、发布输入已审阅并固定到 commit/tag 后执行。不要把当前旧 `dist` 当成成功证据。 ### 1. 通用发布前门禁 ```powershell git status --short git diff --check git ls-files --stage -- '*.command' 'scripts/*.sh' ``` 要求:无意外修改/未跟踪发布输入;macOS 操作文件均为 `100755`。研究报告或明确允许的生成物可以存在,但不得混入发布源清单。 ### 2. Windows:在 Windows x64 原生主机执行 轻量预检: ```powershell .\Build_DoctorWorkstation.bat -ValidateOnly .\.venv\Scripts\python.exe -m pytest -p no:cacheprovider ` tests\test_one_click_entrypoints.py tests\test_entrypoint.py ``` 正式构建(该入口内部已经执行 locked dependency preparation、companion build、PyInstaller、冻结烟测、ZIP 和哈希): ```powershell .\Build_DoctorWorkstation.bat ``` 构建后强制文件门禁: ```powershell $artifact = Resolve-Path 'dist\DoctorWorkstation' $qt = Join-Path $artifact '_internal\PySide6' $required = @( (Join-Path $artifact 'DoctorWorkstation.exe'), (Join-Path $qt 'QtMultimedia.pyd'), (Join-Path $qt 'QtMultimediaWidgets.pyd'), (Join-Path $qt 'Qt6Multimedia.dll'), (Join-Path $qt 'Qt6MultimediaWidgets.dll'), (Join-Path $qt 'plugins\multimedia\ffmpegmediaplugin.dll'), (Join-Path $qt 'plugins\multimedia\windowsmediaplugin.dll'), (Join-Path $artifact '_internal\video_companion_dist\index.html') ) $missing = $required | Where-Object { -not (Test-Path -LiteralPath $_ -PathType Leaf) } if ($missing) { throw "Missing frozen release files:`n$($missing -join "`n")" } if (-not (Get-ChildItem -LiteralPath $artifact -Recurse -File ` -Filter 'QtWebEngineProcess.exe' | Select-Object -First 1)) { throw 'QtWebEngineProcess.exe missing' } if (-not (Get-ChildItem -LiteralPath $artifact -Recurse -File ` -Filter 'qtwebengine_resources*.pak' | Select-Object -First 1)) { throw 'QtWebEngine resources missing' } ``` 版本化 ZIP 与哈希复核: ```powershell $version = ([regex]::Match( [IO.File]::ReadAllText((Resolve-Path 'pyproject.toml')), '(?m)^version\s*=\s*"([^"]+)"' )).Groups[1].Value $zip = Resolve-Path "dist\DoctorWorkstation-Windows-x64-$version.zip" $actual = (Get-FileHash -LiteralPath $zip -Algorithm SHA256).Hash $expected = ((Get-Content -Raw -Encoding UTF8 'dist\SHA256SUMS.txt').Trim() -split '\s+')[0] if ($actual -ne $expected) { throw 'Windows release checksum mismatch' } Write-Host "Verified: $zip`nSHA-256: $actual" ``` 最后在一台无开发环境的 Windows 10/11 x64 机器解压 ZIP,双击 `Start_DoctorWorkstation.bat`,完成登录/demo、内嵌 companion、获准 MP4/HLS 回放、摄像头/麦克风与一次真实测试房间验收。若为生产外发,必须先完成 Authenticode,再重新生成 ZIP 和 SHA-256。 ### 3. macOS:只能在原生 macOS 主机执行 模式、语法和入口合同: ```bash git ls-files --stage -- '*.command' 'scripts/*.sh' test "$(git ls-files --stage -- '*.command' 'scripts/*.sh' | \ awk '$1 != "100755" { bad++ } END { print bad + 0 }')" = 0 bash -n scripts/macos_helpers.sh scripts/run_macos.sh \ scripts/package_macos.sh scripts/build_macos.sh \ scripts/check_macos_entrypoints.sh \ run_macos.command package_macos.command \ 一键运行.command 一键打包.command CI=1 DOCTOR_NONINTERACTIVE=1 bash scripts/check_macos_entrypoints.sh ``` 内部 QA 构建可直接运行;生产构建应先提供真实 Developer ID: ```bash export MACOS_CODESIGN_IDENTITY='Developer ID Application: <组织名称> ()' ./一键打包.command ``` 构建后检查: ```bash app='dist/DoctorWorkstation.app' test -x "$app/Contents/MacOS/DoctorWorkstation" test -n "$(find "$app" -type f \( -name 'QtMultimedia.so' -o -name 'QtMultimedia.*.so' \) -print -quit)" test -n "$(find "$app" -type f \( -name 'QtMultimediaWidgets.so' -o -name 'QtMultimediaWidgets.*.so' \) -print -quit)" test -n "$(find "$app" -type f -path '*/plugins/multimedia/*' -print -quit)" test -n "$(find "$app" -type f -name 'QtWebEngineProcess' -print -quit)" test -n "$(find "$app" -type f -name 'qtwebengine_resources*.pak' -print -quit)" test -n "$(find "$app" -type f -path '*video_companion_dist/index.html' -print -quit)" codesign --verify --deep --strict --verbose=2 "$app" codesign -d --entitlements :- "$app/Contents/MacOS/DoctorWorkstation" file "$app/Contents/MacOS/DoctorWorkstation" ``` 版本化 ZIP 与哈希复核: ```bash version="$(awk -F '"' '/^version = "/ { print $2; exit }' pyproject.toml)" case "$(uname -m)" in arm64) arch='arm64' ;; x86_64) arch='x64' ;; *) echo 'Unsupported macOS architecture' >&2; exit 1 ;; esac zip="DoctorWorkstation-macOS-$arch-$version.zip" (cd dist && shasum -a 256 -c "$zip.sha256") ``` 生产外发还需用 `xcrun notarytool` 提交、等待成功并 `xcrun stapler staple`。staple 后要用 `ditto --keepParent` 重新生成最终 ZIP 并重算 `.sha256`,再以 `spctl --assess --type execute --verbose=4` 和一台干净 macOS 13+ 机器验收。最终必须实测摄像头、麦克风、扬声器切换、QtMultimedia 回放、QtWebEngine companion 和 TRTC 测试房间。 ## 发布准入判定 满足以下条件才可把状态改为“可发布”: 1. macOS 操作文件的 Git 模式全部为 `100755`。 2. intended release source 已纳入确定 commit/tag,构建工作树无意外输入。 3. Windows 从该 commit 完整重建;新 ZIP 含 QtMultimedia Python/Widgets/插件、QtWebEngine 和当前 companion,哈希通过。 4. 原生 macOS 从同一 commit 完整重建;`.app` 含对应媒体/QtWebEngine 组件,架构、签名、权限、ZIP 哈希通过。 5. 两个平台冻结 `--smoke-test` 通过,且独立媒体/companion/TRTC 验收通过。 6. 若为生产外发,Windows Authenticode 与 macOS Developer ID/notarization/stapling 均在最终归档前完成。