20 KiB
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.commandpackage_macos.command一键运行.command一键打包.commandscripts/build_macos.shscripts/package_macos.shscripts/run_macos.shscripts/macos_helpers.shscripts/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.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.pydPySide6/QtMultimediaWidgets.pydPySide6/Qt6MultimediaWidgets.dllPySide6/plugins/multimedia/ffmpegmediaplugin.dllPySide6/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以及 Windowsplugins/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 发布门禁。
发布前至少应做到以下二者之一,建议二者都做:
- 在 spec 中显式纳入
PySide6.QtMultimedia和PySide6.QtMultimediaWidgets,继续使用 PyInstaller 官方 hooks 收集平台插件。 - 在两个构建脚本中增加平台化的成品断言;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:建议改进
- 仓库没有
.gitattributes,而本机core.autocrlf=true。当前 macOS shell 文件实际是无 BOM 的 LF,bash -n通过,但建议用.gitattributes固定*.sh/*.command为 LF,并固定批处理/PowerShell 的预期行尾。 - Windows 使用
uv sync --frozen,macOS 使用uv sync --locked。当前uv.lock包含所需依赖,但--frozen不承担 lock 新鲜度检查;发布前可增加uv lock --check,或统一为能拒绝 pyproject/lock 漂移的策略。 packaging/windows/version_info.txt的0.1.0.0与pyproject.toml的0.1.0当前一致,但它是手工同步;脚本没有发布前一致性检查。升级版本时可能出现 ZIP 名与 EXE 版本资源不一致。- README 顶部的一键入口描述正确,但“测试与打包”段落调用
build_*,只产生 onedir/.app,不产生版本化 ZIP/哈希。建议明确区分“构建”与“发布打包”,并把package_*/根入口列为正式发布命令。 .env.example对 demo/生产、SSL、TRTC secret 禁止项说明清楚,未包含长期凭据;但发布 ZIP 不包含 README 或.env.example,README 也没有明确冻结包从哪里查找.env。若企业部署依赖环境文件,应说明冻结态放置/注入方式;若只允许登录页保存非敏感配置,也应明确说明无需随包放.env。- Windows 用固定
dist/SHA256SUMS.txt,macOS 用相邻的<zip>.sha256;两者都可校验,但发布自动化和用户说明可以统一。dist还可能保留旧版本 ZIP,根打包入口成功后打开整个目录,建议在发布清单中明确唯一应交付的文件。 Build_DoctorWorkstation.bat -ValidateOnly会打印Package ready in: ...,即使它只做了入口预检;不影响退出码,但容易被误认作已生成新包。- spec 没有设置 Windows EXE/macOS bundle 的原生应用图标;窗口内 SVG 正常,但系统文件/Finder 图标仍是默认值。若品牌发布有要求,应在最终签名前补齐
.ico/.icns。 - 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-<version>.zip - 校验:
dist/SHA256SUMS.txt
- onedir:
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 → 隔离冻结烟测 →dittoZIP → SHA-256。 - 产物:
- bundle:
dist/DoctorWorkstation.app - ZIP:
dist/DoctorWorkstation-macOS-{arm64|x64}-<version>.zip - 校验:同路径
<zip>.sha256
- bundle:
主代理最终构建与验证命令
以下命令应在 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 测试房间。
发布准入判定
满足以下条件才可把状态改为“可发布”:
- macOS 操作文件的 Git 模式全部为
100755。 - intended release source 已纳入确定 commit/tag,构建工作树无意外输入。
- Windows 从该 commit 完整重建;新 ZIP 含 QtMultimedia Python/Widgets/插件、QtWebEngine 和当前 companion,哈希通过。
- 原生 macOS 从同一 commit 完整重建;
.app含对应媒体/QtWebEngine 组件,架构、签名、权限、ZIP 哈希通过。 - 两个平台冻结
--smoke-test通过,且独立媒体/companion/TRTC 验收通过。 - 若为生产外发,Windows Authenticode 与 macOS Developer ID/notarization/stapling 均在最终归档前完成。