Files
zyt/app/research/release_packaging_audit.md
T
2026-08-11 09:12:51 +08:00

330 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 模式错误。
发布前处理:
```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-1QtMultimedia 依赖可用,但冻结收集没有独立门禁
通过项:
- `pyproject.toml` 依赖 `PySide6>=6.8.2,<7``uv.lock` 当前锁定 PySide6/PySide6 Addons/Essentials 6.11.1Addons 提供 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 提供完整性,不提供发布者身份;对外分发前应在压缩前签名并验证最终可执行文件。
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-4macOS 成品尚无原生验证证据
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 用相邻的 `<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.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`
### 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}-<version>.zip`
- 校验:同路径 `<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: <组织名称> (<TEAMID>)'
./一键打包.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 均在最终归档前完成。