Files
xuetang/app/research/release_packaging_audit.md
T
2026-09-08 11:40:15 +08:00

20 KiB
Raw Blame History

DoctorWorkstation 发布打包就绪审计

审计日期:2026-08-11Asia/Shanghai
审计主机:Windows 10/11 工作区 D:\web\zyt\app
审计边界:只检查入口、脚本、PyInstaller spec、冻结依赖、伴随页、文档和既有产物;未执行完整 PyInstaller,未改业务/打包源码。

结论

当前状态:不可直接发布既有 dist,也不应立即从当前 Git 状态制作正式发布包。

构建流水线的主体是完整的:Windows/macOS 均有根目录中英文入口,能从锁文件准备 Python/Node 依赖,能构建 TRTC Web companionPyInstaller 使用 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-1macOS 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 模式错误。

发布前处理:

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.exe2026-08-10 17:07。
  • dist/DoctorWorkstation-Windows-x64-0.1.0.zip2026-08-10 17:08。
  • src/doctor_workstation/ui/diagnosis_media.py2026-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-1QtMultimedia 依赖可用,但冻结收集没有独立门禁

通过项:

  • pyproject.toml 依赖 PySide6>=6.8.2,<7uv.lock 当前锁定 PySide6/PySide6 Addons/Essentials 6.11.1Addons 提供 QtMultimedia。
  • 当前 .venv.venv-build 均有 QtMultimedia.pydQtMultimediaWidgets.pydQt6MultimediaWidgets.dll 以及 Windows plugins/multimedia 下的 FFmpeg/Windows Media 插件。
  • 当前 PyInstaller 6.22 的官方 hook-PySide6.QtMultimedia.py 会追加 PySide6.QtMultimediaWidgetsQt 模块映射会收集 Qt 6 的 multimedia 插件。当前源码中的静态 import 理论上会触发该 hook。

缺口:

  • packaging/doctor_workstation.spec 的显式 hidden imports 只有 QtWebEngine 相关模块,没有显式列出 PySide6.QtMultimedia/PySide6.QtMultimediaWidgets
  • Windows/macOS 构建脚本只检查 QtWebEngine helper、Chromium .pakvideo_companion_dist/index.html,不检查 QtMultimedia Python 扩展、Widgets 库或媒体后端插件。
  • diagnosis_media.py 捕获 ImportError 并降级到外部打开,因此媒体模块缺失不会让应用启动失败。
  • 实测旧冻结包缺少上述模块/插件,但隔离执行 DoctorWorkstation.exe --smoke-test 仍返回 0。这证明现有烟测不是 QtMultimedia 发布门禁。

发布前至少应做到以下二者之一,建议二者都做:

  1. 在 spec 中显式纳入 PySide6.QtMultimediaPySide6.QtMultimediaWidgets,继续使用 PyInstaller 官方 hooks 收集平台插件。
  2. 在两个构建脚本中增加平台化的成品断言;Windows 检查两个 .pydQt6MultimediaWidgets.dllplugins/multimediamacOS 检查两个 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 提供完整性,不提供发布者身份;对外分发前应在压缩前签名并验证最终可执行文件。

macOSspec 接受可选的 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 的 LFbash -n 通过,但建议用 .gitattributes 固定 *.sh/*.command 为 LF,并固定批处理/PowerShell 的预期行尾。
  2. Windows 使用 uv sync --frozenmacOS 使用 uv sync --locked。当前 uv.lock 包含所需依赖,但 --frozen 不承担 lock 新鲜度检查;发布前可增加 uv lock --check,或统一为能拒绝 pyproject/lock 漂移的策略。
  3. packaging/windows/version_info.txt0.1.0.0pyproject.toml0.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.txtmacOS 用相邻的 <zip>.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.ps1package_windows.ps1run_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.py4 passed
当前 source QtMultimedia import PASS QMediaPlayerQAudioOutputQVideoWidget 均可导入;两个 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_distresources -> 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.0Windows resource 0.1.0.0

入口、spec 和输出合同摘要

Windows

  • 根入口:中文别名 → 英文 .batscripts/package_windows.ps1/run_windows.ps1
  • 运行:优先 dist/DoctorWorkstation/DoctorWorkstation.exe,否则使用 .venvuv sync --frozen 后运行源码。
  • 发布打包:Node 20+ → uv sync --frozen --extra buildnpm ci → companion build → PyInstaller --clean → 静态成品检查 → 隔离冻结烟测 → ZIP → SHA-256。
  • 产物:
    • onedirdist/DoctorWorkstation/
    • 启动器:dist/Start_DoctorWorkstation.bat
    • ZIPdist/DoctorWorkstation-Windows-x64-<version>.zip
    • 校验:dist/SHA256SUMS.txt

macOS

  • 根入口:四个 .command 解析自身目录后用 /bin/bash 调用 scripts/run_macos.shscripts/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。
  • 产物:
    • bundledist/DoctorWorkstation.app
    • ZIPdist/DoctorWorkstation-macOS-{arm64|x64}-<version>.zip
    • 校验:同路径 <zip>.sha256

主代理最终构建与验证命令

以下命令应在 P0 修复、发布输入已审阅并固定到 commit/tag 后执行。不要把当前旧 dist 当成成功证据。

1. 通用发布前门禁

git status --short
git diff --check
git ls-files --stage -- '*.command' 'scripts/*.sh'

要求:无意外修改/未跟踪发布输入;macOS 操作文件均为 100755。研究报告或明确允许的生成物可以存在,但不得混入发布源清单。

2. Windows:在 Windows x64 原生主机执行

轻量预检:

.\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 和哈希):

.\Build_DoctorWorkstation.bat

构建后强制文件门禁:

$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 与哈希复核:

$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 主机执行

模式、语法和入口合同:

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

export MACOS_CODESIGN_IDENTITY='Developer ID Application: <组织名称> (<TEAMID>)'
./一键打包.command

构建后检查:

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 与哈希复核:

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 均在最终归档前完成。