This commit is contained in:
Your Name
2026-08-11 09:12:51 +08:00
parent c3ceb0dd0f
commit cfe4c82c90
111 changed files with 26110 additions and 826 deletions
+329
View File
@@ -0,0 +1,329 @@
# 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 均在最终归档前完成。