From 9549a91139ca5a98084f339275e5893081941f8d Mon Sep 17 00:00:00 2001 From: zeweihan Date: Tue, 1 Sep 2026 11:38:06 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix(installer):=20Windows=20=E4=B8=80?= =?UTF-8?q?=E9=94=AE=E5=AE=89=E8=A3=85=E5=99=A8=E5=8A=A0=E7=A3=81=E7=9B=98?= =?UTF-8?q?=E7=A9=BA=E9=97=B4=E9=97=B8=EF=BC=8C=E4=B8=8D=E8=B6=B3=E6=97=B6?= =?UTF-8?q?=E5=B0=B1=E5=9C=B0=E6=8B=A6=E4=B8=8B=EF=BC=88dev-board#350?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 以前 AwdUpdateSpace 只把可用空间显示成「可用 X GB」,不足照装不误:NSIS 在解压 中途报「Error opening file for writing」,用户拿到一个残缺安装。 闸放在 AwdInstallClick 里、$INSTDIR 定下来之后(用户可能刚在「自定义安装」里换过盘): 不足则弹提示说明「需要约 X.X GB / 该盘可用 Y.Y GB」,并把展开行的可用空间标签染红, 不进安装页。 所需空间不手抄数字:桌面端在 installer.nsh 里接 electron-builder 的 APP_64_UNPACKED_SIZE——打包时对 win-unpacked 整个目录(含 backend/jre/python/ frontend dist/skills 等 extraResources)实测的解包体积,KB,由 makensis 命令行 -D 传入, 在生成脚本第一行就存在(与 $launchLink/StdUtils 那批晚到的东西不同)。拿不到就 !error 编译失败,不静默少一道闸。余量 512MB:ARM64 壳覆盖层约 260MB(不在上面那个数里) + 解压临时占用 + 不把盘塞到 0 字节。 三条刻意的保守选择: - **读不出可用空间一律放行**(空串/0/非数字都算读不出来)。宁可让 NSIS 自己在解压时 报错,也不能因为一次读盘失败把装得下的用户挡在门外——闸设得过严比不设闸更糟。 - 引擎侧整套代码在 !ifdef AWD_UI_REQUIRED_KB 内,插件端安装器(装一份清单)不设闸, 编译产物一字节不变。 - 静默安装(/S,自动更新)不进 GUI 代码,这道闸对它不生效,那条路的失败处理归更新器管。 NSIS 地雷已按 eng-infra 的记录处理:FileFunc 的 GetRoot/DriveSpace 会踩调用方 $0-$2 (历史上把窗宽写成剩余 GB),所以 AwdReadFreeSpace 进出各 Push/Pop 一次、结果只落 Var;GB 文案自己算一位小数,取整会把「差 1.9 GB」显示成「差 1 GB」。顺手修一个会 自己造出来的陈旧态:AwdUpdateSpace 开头复位标签颜色,否则用户换到大盘后标签还红着。 验证:本机 makensis 不可用(homebrew v3.12 与 electron-builder v3.04 两个 mac 构建 都是非 ASCII 报 Bad text encoding、纯 ASCII 写产物 std::bad_alloc,**未改动的 HEAD 文件 同样复现**,已记进 eng-infra),只能靠 installer-ui-smoke。因此把反向用例做进了 CI: ui-harness.nsi 收 -DREQUIRED_KB,工作流多编一个所需空间约 858GB 的 harness-nospace.exe, installer-smoke.ps1 加 -ExpectBlocked 断言「弹了提示框 + 进程没退出 + 窗口还是 760 宽的 大卡片(真开装会缩成 360x132 的角落进度卡)」;两个正常 harness 也打开了闸(200MB), 正向那条路本身就是「闸没挡住正常安装」的回归证据。 Co-Authored-By: Claude Opus 5 --- .claude/agents/eng-infra.md | 15 +++ .github/workflows/installer-ui-smoke.yml | 17 +++- desktop/build/installer.nsh | 23 +++++ desktop/build/win/awd-oneclick-ui.nsh | 112 +++++++++++++++++++++++ desktop/build/win/ui-harness.nsi | 8 +- desktop/scripts/installer-smoke.ps1 | 45 ++++++++- 6 files changed, 215 insertions(+), 5 deletions(-) diff --git a/.claude/agents/eng-infra.md b/.claude/agents/eng-infra.md index 6df417774..381724d87 100644 --- a/.claude/agents/eng-infra.md +++ b/.claude/agents/eng-infra.md @@ -54,6 +54,21 @@ description: 工程基建领域。任务涉及构建、发版、CI workflow、 6. DMG 安装窗口视觉(PR#204):`build.dmg` 里的 `contents` 坐标是**图标中心、原点在窗口内容区左上角(不含标题栏)**;窗口尺寸由背景图 1x 像素尺寸决定(660x420),所以没写 `window`。背景图 `desktop/build/background.png` + `background@2x.png` 由 electron-builder 自动合成 hidpi TIFF,源文件是 `desktop/build/dmg-background.html`(顶部注释有 headless Chrome 重新生成命令)。改图标落位必须同步改 HTML 里的光晕/箭头位置,否则错位。 6.5. **win 安装器美术管线**(`desktop/scripts/render-win-installer-art.mjs`,安装器 UI 重设计新增):`build/win/*.html`(美术源文件)→ headless Chrome 截图 → ImageMagick 转 24 位 BMP3 入库为 `installerSidebar.bmp`/`installerHeader.bmp`。**sips 只能出 32 位 BMP,NSIS/MUI2 只认无 alpha 的经典 BMP,必须用 `magick`**——这是个地雷,脚本会校验 BM 头与色深不合格宁可失败也不入库。只有维护者改美术时手动跑一次,产物入库后 CI 与用户构建都不需要 Chrome/ImageMagick。 6.5.1. **一键安装 UI 位图管线**(`desktop/scripts/render-oneclick-art.mjs`,dev-board#339):`build/win/oneclick-*.html` → Chrome(`--force-device-scale-factor`)→ 24 位 BMP3,zh/en × 100/125/150/200% 共 24 张/产品,**产物不入库**(gitignore `generated/`),构建现场渲染:桌面端由 desktop-build.yml 的「Render one-click installer art」步骤(缺了它 NSIS 编译 File 找不到位图直接失败)、插件端由 build-installers.mjs 调用。三个地雷:makensis 的相对 File 路径按**脚本所在目录**解析(所有 -D 路径给绝对路径);直接调 makensis 必须 `-INPUTCHARSET UTF8`(脚本内含中文字符串);BMP 尺寸校验必须是基准×倍率,错一像素热区全歪。视觉回归用 `.github/workflows/installer-ui-smoke.yml`(Windows runner 编译 `build/win/ui-harness.nsi` 测试壳真跑全流程,`desktop/scripts/installer-smoke.ps1` 按基准坐标点击热区逐阶段截图上传 artifact)——没有 Windows 真机时唯一的视觉验证手段。 + **本机 makensis 不能用来预检**(2026-09-01 实测,Darwin 27):homebrew 的 v3.12 与 + electron-builder 缓存的 v3.04 两个 mac 构建都一样——非 ASCII 源码报 `Bad text encoding`, + 纯 ASCII 脚本走到写产物时 `std::bad_alloc` 崩。**未改动的 HEAD 文件同样复现**,不是谁改坏的, + 别在这上面查半天。改 NSIS 只能靠 installer-ui-smoke。 +6.5.2. **磁盘空间闸**(dev-board#350):引擎新增两个可选契约 `AWD_UI_REQUIRED_KB` / + `AWD_UI_REQUIRED_EXTRA_KB`,只有设了前者才编译出闸(插件端装的是一份清单,不设)。 + 桌面端在 `build/installer.nsh` 里直接把 **electron-builder 的 `-D APP_64_UNPACKED_SIZE`** + 接过来——那是打包时对 win-unpacked 整个目录实测的解包体积(KB),**不许手抄常量**, + 包体每次发版都在长;这个 define 是命令行 `-D` 传的,在生成脚本第一行就存在,和 + `$launchLink`/`StdUtils` 那批「晚于 include 才出现」的东西不是一回事。余量 512MB + (ARM64 壳 260MB + 解压临时占用 + 不把盘塞到 0)。取不到可用空间时**一律放行**—— + 闸设得过严会把装得下的用户挡在门外,比不设闸更糟。反向用例在 CI 里实跑: + `ui-harness.nsi` 收 `-DREQUIRED_KB`,smoke 工作流多编一个所需空间约 858GB 的 + `harness-nospace.exe`,用 `installer-smoke.ps1 -ExpectBlocked` 断言「弹了提示框 + + 进程没退 + 窗口还是 760 宽的大卡片(开装了会缩成 360×132 的角落进度卡)」。 6.6. **`dmg-builder` 补丁(`desktop/scripts/patch-dmg-builder.js` + package.json `postinstall`,安装器 UI 重设计新增)**:macOS 26.2+ 起 Finder 拒读 dmgbuild 写入 `.DS_Store` 的 `pBBk` 背景书签,导致桌面端主 DMG 背景不显示(electron-builder#9072 / dmgbuild#273,同版 Obsidian/Podman Desktop 同期中招)。`npm ci`/`npm install` 后自动对 `node_modules/dmg-builder/vendor/dmgbuild/core.py` 做定点补丁(跳过 Bookmark 生成,`icvp` 里的 alias 通道保留,老系统照常工作)。**升级 electron-builder 后若补丁脚本报「结构已变」**:先确认新版是否已自带该修复,再决定要不要删掉本补丁,不要盲目跳过。 ## 测试命令总表 diff --git a/.github/workflows/installer-ui-smoke.yml b/.github/workflows/installer-ui-smoke.yml index 15b0bcfdb..86904a1ee 100644 --- a/.github/workflows/installer-ui-smoke.yml +++ b/.github/workflows/installer-ui-smoke.yml @@ -38,8 +38,13 @@ jobs: $ws = $env:GITHUB_WORKSPACE New-Item -ItemType Directory -Force payload | Out-Null 1..4 | ForEach-Object { fsutil file createnew ("payload\blob$_.bin") 41943040 } - makensis /INPUTCHARSET UTF8 /DHARNESS_LANG=SimpChinese "/DGENART=$ws\desktop\build\win\generated" "/DPAYLOAD=$ws\payload" "/DOUTFILE=$ws\harness-zh.exe" desktop\build\win\ui-harness.nsi - makensis /INPUTCHARSET UTF8 /DHARNESS_LANG=English "/DGENART=$ws\desktop\build\win\generated" "/DPAYLOAD=$ws\payload" "/DOUTFILE=$ws\harness-en.exe" desktop\build\win\ui-harness.nsi + # REQUIRED_KB 打开磁盘空间闸(dev-board#350)。200MB:payload 只有 160MB, + # runner 盘上剩几十 GB,走的是「够用、正常放行」那条分支——这一路本身就是 + # 「闸没把正常安装挡住」的回归证据。 + makensis /INPUTCHARSET UTF8 /DHARNESS_LANG=SimpChinese /DREQUIRED_KB=204800 "/DGENART=$ws\desktop\build\win\generated" "/DPAYLOAD=$ws\payload" "/DOUTFILE=$ws\harness-zh.exe" desktop\build\win\ui-harness.nsi + makensis /INPUTCHARSET UTF8 /DHARNESS_LANG=English /DREQUIRED_KB=204800 "/DGENART=$ws\desktop\build\win\generated" "/DPAYLOAD=$ws\payload" "/DOUTFILE=$ws\harness-en.exe" desktop\build\win\ui-harness.nsi + # 反向用例:所需空间约 858 GB,runner 上必然不足,走「拦下」分支 + makensis /INPUTCHARSET UTF8 /DHARNESS_LANG=SimpChinese /DREQUIRED_KB=900000000 "/DGENART=$ws\desktop\build\win\generated" "/DPAYLOAD=$ws\payload" "/DOUTFILE=$ws\harness-nospace.exe" desktop\build\win\ui-harness.nsi Set-Content -Path dummy-manifest.xml -Value '' makensis /INPUTCHARSET UTF8 /DVERSION=0.0.1 "/DMANIFEST=$ws\dummy-manifest.xml" "/DOUTFILE=$ws\addin-smoke.exe" "/DARTDIR=$ws\office-addin\installer\win" "/DAWD_UI_ENGINE=$ws\desktop\build\win\awd-oneclick-ui.nsh" "/DGENART=$ws\office-addin\installer\win\generated" /DLEGALBASE=https://www.aiworkdeck.com office-addin\installer\win\installer.nsi @@ -51,6 +56,12 @@ jobs: shell: pwsh run: pwsh -File desktop/scripts/installer-smoke.ps1 -Exe harness-en.exe -OutDir shots/en + - name: Drive nospace harness (磁盘空间闸必须拦下,dev-board#350) + shell: pwsh + # 断言三件事:弹了提示框、进程没退出、窗口还是 760 宽的大卡片(开装了会 + # 缩成 360x132 的角落小进度卡)。截图落 shots/nospace/03-blocked.png。 + run: pwsh -File desktop/scripts/installer-smoke.ps1 -Exe harness-nospace.exe -OutDir shots/nospace -ExpectBlocked + - name: Drive addin installer (无目录展开分支的实跑覆盖) shell: pwsh # 插件端不开 AWD_UI_DIR_CHOICE,欢迎页少一组控件、安装秒完——点自定义安装 @@ -115,7 +126,7 @@ jobs: $ws = $env:GITHUB_WORKSPACE New-Item -ItemType Directory -Force payload | Out-Null 1..4 | ForEach-Object { fsutil file createnew ("payload\blob$_.bin") 41943040 } - makensis /INPUTCHARSET UTF8 /DHARNESS_LANG=SimpChinese "/DGENART=$ws\desktop\build\win\generated" "/DPAYLOAD=$ws\payload" "/DOUTFILE=$ws\harness-zh.exe" desktop\build\win\ui-harness.nsi + makensis /INPUTCHARSET UTF8 /DHARNESS_LANG=SimpChinese /DREQUIRED_KB=204800 "/DGENART=$ws\desktop\build\win\generated" "/DPAYLOAD=$ws\payload" "/DOUTFILE=$ws\harness-zh.exe" desktop\build\win\ui-harness.nsi if ($LASTEXITCODE -ne 0) { throw "makensis failed" } if (-not (Test-Path "$ws\harness-zh.exe")) { throw 'harness exe missing' } diff --git a/desktop/build/installer.nsh b/desktop/build/installer.nsh index fef7fcd4a..daf45b182 100644 --- a/desktop/build/installer.nsh +++ b/desktop/build/installer.nsh @@ -28,6 +28,29 @@ ExecShell "open" "$INSTDIR\${PRODUCT_FILENAME}.exe" !macroend +; 磁盘空间闸(dev-board#350):所需空间不手抄数字,直接用 electron-builder 的 +; APP_64_UNPACKED_SIZE——它是打包时对 win-unpacked 整个目录(含 extraResources: +; backend/jre/python/frontend dist/skills…)实测的解包体积,单位 KB,由 electron-builder +; 以 makensis 命令行 -D 传入,在生成脚本的第一行就已经存在。这一点与 $launchLink / +; APP_EXECUTABLE_FILENAME / StdUtils 那些「晚于本 include 才出现」的东西不同(见下面 +; AwdUiOnLaunch 的注释),所以这里引用它是安全的。手抄常量才是错的:包体每次发版都在长。 +; +; 余量 512MB 的构成:① ARM64 机器上 customInstall 还要再铺一份纯 arm64 Electron 壳 +;(约 260MB 未压缩,不在 APP_64_UNPACKED_SIZE 里);② NSIS/7z 解压过程本身的临时占用; +; ③ 不能装完就把盘塞到 0 字节。刻意没给更大:闸设得过严会把「其实装得下」的用户挡在 +; 门外,那比不设闸更糟。 +!ifndef BUILD_UNINSTALLER + !ifdef APP_64_UNPACKED_SIZE + !define AWD_UI_REQUIRED_KB "${APP_64_UNPACKED_SIZE}" + !define AWD_UI_REQUIRED_EXTRA_KB 524288 + !else + ; 宁可编译失败也不静默少一道闸:electron-builder 只有在 USE_NSIS_BUILT_IN_COMPRESSOR + ; 打开(源码里硬编码 false)或改了 defines 契约时才会不给这个值。真撞上就来这里 + ; 换成新的体积来源,别直接删掉这段。 + !error "APP_64_UNPACKED_SIZE 未定义:磁盘空间闸拿不到所需体积(dev-board#350)。electron-builder 的 defines 契约变了,去 desktop/build/installer.nsh 换体积来源。" + !endif +!endif + !include "win\awd-oneclick-ui.nsh" !macro customWelcomePage diff --git a/desktop/build/win/awd-oneclick-ui.nsh b/desktop/build/win/awd-oneclick-ui.nsh index ef1d3bf3f..f0c3585de 100644 --- a/desktop/build/win/awd-oneclick-ui.nsh +++ b/desktop/build/win/awd-oneclick-ui.nsh @@ -11,6 +11,9 @@ ; !define AWD_UI_ART "<已渲染位图目录>" ; render-oneclick-art.mjs 的产物目录 ; !define AWD_UI_DIR_CHOICE ; 可选:启用「自定义安装」路径展开(桌面端) ; !define AWD_UI_DIR_LEAF "AI WorkDeck" ; DIR_CHOICE 时必填:自选目录强制追加的子目录名 +; !define AWD_UI_REQUIRED_KB "" ; 可选:安装所需磁盘空间(KB)。设了才有磁盘闸; +; ; 桌面端直接给 electron-builder 的 APP_64_UNPACKED_SIZE +; !define AWD_UI_REQUIRED_EXTRA_KB "" ; 可选:所需空间之外的余量(KB),默认 0 ; !define AWD_UI_TERMS_URL / AWD_UI_PRIVACY_URL ; !macro AwdUiOnLaunch ; 可选:完成卡「立即体验」的动作(缺省=仅关闭) ; 然后在页面序列里插: @@ -130,6 +133,11 @@ Var AwdSpaceLabel !endif Var AwdWinX ; 大卡片左上角(展开时保持不动) Var AwdWinY +!ifdef AWD_UI_REQUIRED_KB +Var AwdFreeMb ; 目标盘可用空间(MB);"" = 读不出来 +Var AwdNeedMb ; 本次安装所需空间(MB) +Var AwdDriveRoot ; 目标盘根(如 "C:"),只为提示文案 +!endif ; px = base * scale / 100 !macro _AwdPx var base @@ -398,6 +406,9 @@ Function AwdBrowseClick FunctionEnd Function AwdUpdateSpace + ; 先把颜色复位:磁盘闸拦下时 AwdSpaceRefused 会把这个标签染红(dev-board#350), + ; 用户改完路径再进来必须变回常态,否则换到大盘上了标签还红着,反而误导。 + SetCtlColors $AwdSpaceLabel 0x8A9590 0xFFFFFF ${NSD_GetText} $AwdDirEdit $0 ${GetRoot} $0 $1 ${If} $1 == "" @@ -414,6 +425,97 @@ Function AwdUpdateSpace FunctionEnd !endif +; ---------- 磁盘空间闸(dev-board#350)---------- +; 以前只把可用空间显示成「可用 X GB」,不足照装不误:NSIS 在解压中途报 +; 「Error opening file for writing」,留下一个残缺安装。这里在真正开装之前拦一道。 +; 只有调用方给了 AWD_UI_REQUIRED_KB 才编译进来(插件端装的是一份清单,不设闸)。 +; 静默安装(/S,自动更新路径)不进 GUI 代码,这道闸对它不生效——那条路的失败处理 +; 归更新器自己管,此处不越界。 +!ifdef AWD_UI_REQUIRED_KB +!ifndef AWD_UI_REQUIRED_EXTRA_KB + !define AWD_UI_REQUIRED_EXTRA_KB 0 +!endif + +; MB → "X.Y"(GB,保留一位小数)。整数取整会把「需要 4 / 可用 3」显示成只差 1 GB, +; 实际可能差 1.9 GB——差多少是用户要不要去清盘的唯一依据,不能糊。 +!macro _AwdGbText outvar mb + IntOp $R5 ${mb} / 1024 + IntOp $R6 ${mb} % 1024 + IntOp $R6 $R6 * 10 + IntOp $R6 $R6 / 1024 + StrCpy ${outvar} "$R5.$R6" +!macroend +!define AwdGbText "!insertmacro _AwdGbText" + +; 读 $INSTDIR 所在盘的可用空间 → $AwdFreeMb / $AwdDriveRoot,读不到留空串。 +; FileFunc 的 GetRoot/DriveSpace 会踩调用方的 $0-$2(引擎里已有实锤记录,见 +; AwdToggleCustom 的注释:剩余 32GB 把窗口宽度写成 32px),所以进出各存取一次, +; 结果一律落在 Var 上——调用方拿到的 $0-$2 与调用前逐字节一致。 +Function AwdReadFreeSpace + Push $0 + Push $1 + Push $2 + StrCpy $AwdFreeMb "" + StrCpy $AwdDriveRoot "" + StrCpy $0 "$INSTDIR" + ${GetRoot} $0 $1 + ${If} $1 != "" + StrCpy $AwdDriveRoot $1 + ${DriveSpace} "$1\" "/D=F /S=M" $2 + StrCpy $AwdFreeMb $2 + ${EndIf} + Pop $2 + Pop $1 + Pop $0 +FunctionEnd + +; 空间够不够。出参:$AwdNeedMb 恒为所需 MB;栈顶 1=不足 / 0=可以装。 +; **读不出可用空间时一律放行**:宁可让 NSIS 自己在解压时报错,也不能因为一次读盘 +; 失败(网络盘、UNC 路径、DriveSpace 在某些卷上返回空)把装得下的用户挡在门外。 +; 同理,读出 0 或非数字也按「读不出来」处理——IntCmp 会把非数字当 0,那会误判成不足。 +Function AwdSpaceShort + IntOp $AwdNeedMb ${AWD_UI_REQUIRED_KB} + ${AWD_UI_REQUIRED_EXTRA_KB} + IntOp $AwdNeedMb $AwdNeedMb / 1024 + Call AwdReadFreeSpace + ${If} $AwdFreeMb == "" + Push 0 + ${ElseIf} $AwdFreeMb <= 0 + Push 0 + ${ElseIf} $AwdFreeMb < $AwdNeedMb + Push 1 + ${Else} + Push 0 + ${EndIf} +FunctionEnd + +; 就地拦下并说清楚差多少。 +Function AwdSpaceRefused + !ifdef AWD_UI_DIR_CHOICE + ; 展开态下把「可用 X GB」染红(收起态这个标签本来就不可见,只靠下面的对话框)。 + ; 只改颜色不改文案:标签宽 ${AWDUI_SPACE_W} 是按「可用 XX GB」排的版,加字会溢出, + ; 而文案与美术位图、热区坐标是同一套基准,改一处要改三处。 + SetCtlColors $AwdSpaceLabel 0xC0392B 0xFFFFFF + System::Call 'user32::InvalidateRect(p $AwdSpaceLabel, p 0, i 1)' + !endif + + ${AwdGbText} $R3 $AwdNeedMb + ${AwdGbText} $R4 $AwdFreeMb + ${If} $AwdLang == "zh" + !ifdef AWD_UI_DIR_CHOICE + MessageBox MB_OK|MB_ICONEXCLAMATION "可用空间不足,暂时无法安装。$\n$\n安装需要约 $R3 GB,$AwdDriveRoot 盘当前可用 $R4 GB。$\n可以点「自定义安装」换一个磁盘,或清理后重试。" + !else + MessageBox MB_OK|MB_ICONEXCLAMATION "可用空间不足,暂时无法安装。$\n$\n安装需要约 $R3 GB,$AwdDriveRoot 盘当前可用 $R4 GB。$\n请清理磁盘后重试。" + !endif + ${Else} + !ifdef AWD_UI_DIR_CHOICE + MessageBox MB_OK|MB_ICONEXCLAMATION "Not enough free disk space.$\n$\nSetup needs about $R3 GB; drive $AwdDriveRoot has $R4 GB free.$\nUse Custom install to pick another drive, or free up space and try again." + !else + MessageBox MB_OK|MB_ICONEXCLAMATION "Not enough free disk space.$\n$\nSetup needs about $R3 GB; drive $AwdDriveRoot has $R4 GB free.$\nFree up space and try again." + !endif + ${EndIf} +FunctionEnd +!endif + Function AwdInstallClick Pop $0 !ifdef AWD_UI_DIR_CHOICE @@ -434,6 +536,16 @@ Function AwdInstallClick StrCpy $INSTDIR $0 ${EndIf} !endif + !ifdef AWD_UI_REQUIRED_KB + ; 必须在 $INSTDIR 定下来之后再算——用户可能刚在「自定义安装」里换过盘。 + ; AwdSpaceShort 内部完整保存/恢复 $0-$2,上面那段路径处理的结果不受影响。 + Call AwdSpaceShort + Pop $1 + ${If} $1 = 1 + Call AwdSpaceRefused + Return + ${EndIf} + !endif ; 等价于按下「下一步」,交回 NSIS 页面机 SendMessage $HWNDPARENT ${WM_COMMAND} 1 0 FunctionEnd diff --git a/desktop/build/win/ui-harness.nsi b/desktop/build/win/ui-harness.nsi index 3f408542f..11ddad2aa 100644 --- a/desktop/build/win/ui-harness.nsi +++ b/desktop/build/win/ui-harness.nsi @@ -3,7 +3,10 @@ ; Windows runner 上编译、驱动并逐阶段截图(dev-board#339)。 ; 编译参数: ; makensis -INPUTCHARSET UTF8 -DHARNESS_LANG=SimpChinese|English -; -DGENART=<位图目录> -DPAYLOAD=<假 payload 目录> -DOUTFILE= ui-harness.nsi +; -DGENART=<位图目录> -DPAYLOAD=<假 payload 目录> -DOUTFILE= +; [-DREQUIRED_KB=] ui-harness.nsi +; REQUIRED_KB 打开磁盘空间闸(dev-board#350):给个小值走「够用」分支,给个大到 +; 不可能满足的值走「拦下」分支——installer-ui-smoke 两个分支都编译并实跑截图。 Unicode true !include "MUI2.nsh" @@ -17,6 +20,9 @@ InstallDir "$LOCALAPPDATA\AWDUIHarness" !define AWD_UI_DIR_LEAF "AI WorkDeck" !define AWD_UI_TERMS_URL "https://www.aiworkdeck.com/zh/legal/terms" !define AWD_UI_PRIVACY_URL "https://www.aiworkdeck.com/zh/legal/privacy" +!ifdef REQUIRED_KB + !define AWD_UI_REQUIRED_KB "${REQUIRED_KB}" +!endif !macro AwdUiOnLaunch ; 测试壳没有可启动的应用,空动作 !macroend diff --git a/desktop/scripts/installer-smoke.ps1 b/desktop/scripts/installer-smoke.ps1 index e26317fa2..023981631 100644 --- a/desktop/scripts/installer-smoke.ps1 +++ b/desktop/scripts/installer-smoke.ps1 @@ -3,10 +3,14 @@ # 只在 100% DPI 的 runner 上跑,基准坐标即物理像素;窗口无边框,客户区 == 窗口矩形。 param( [Parameter(Mandatory = $true)][string]$Exe, - [Parameter(Mandatory = $true)][string]$OutDir + [Parameter(Mandatory = $true)][string]$OutDir, + # 磁盘空间闸(dev-board#350)的反向用例:用一个所需空间大到不可能满足的 + # harness 产物驱动,断言「点了立即安装,但没开装」。 + [switch]$ExpectBlocked ) $ErrorActionPreference = 'Stop' Add-Type -AssemblyName System.Drawing +Add-Type -AssemblyName System.Windows.Forms Add-Type @" using System; using System.Runtime.InteropServices; @@ -20,6 +24,7 @@ public class W { [DllImport("user32.dll")] public static extern IntPtr GetAncestor(IntPtr h, uint flags); [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid); [DllImport("user32.dll")] public static extern void keybd_event(byte vk, byte scan, uint flags, UIntPtr extra); + [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); public struct RECT { public int L, T, R, B; } public struct POINT { public int x, y; } } @@ -92,6 +97,18 @@ function Shot([string]$name) { Write-Host "shot $name ($w x $ht at $($r.L),$($r.T))" } +function ShotFull([string]$name) { + # 磁盘不足提示是独立的 MessageBox 顶层窗,位置不受卡片矩形约束,只能整屏截 + $b = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds + $bmp = New-Object System.Drawing.Bitmap($b.Width, $b.Height) + $g = [System.Drawing.Graphics]::FromImage($bmp) + $g.CopyFromScreen($b.X, $b.Y, 0, 0, $bmp.Size) + $g.Dispose() + $bmp.Save((Join-Path $OutDir "$name.png"), [System.Drawing.Imaging.ImageFormat]::Png) + $bmp.Dispose() + Write-Host "shot $name (full screen $($b.Width) x $($b.Height))" +} + function ClickAt([int]$bx, [int]$by) { $r = Get-Rect Clear-Overlay ($r.L + $bx) ($r.T + $by) @@ -111,6 +128,32 @@ Start-Sleep -Milliseconds 600 Shot '02-expanded' # 3. 点「立即安装」(AWDUI_CTA 460,414,260,72 → 中心 590,450) ClickAt 590 450 + +if ($ExpectBlocked) { + # 磁盘空间闸应当在这一步就拦下:弹提示框、留在大卡片上、一个字节都不该开始拷 + Start-Sleep -Milliseconds 1500 + $fg = [W]::GetForegroundWindow() + ShotFull '03-blocked' + if ($fg -eq $h) { throw "disk-space gate did not fire: no modal appeared after clicking install" } + # MB_OK 用回车关掉 + [W]::SetForegroundWindow($fg) | Out-Null + [W]::keybd_event(0x0D, 0, 0, [UIntPtr]::Zero) + [W]::keybd_event(0x0D, 0, 2, [UIntPtr]::Zero) + Start-Sleep -Milliseconds 1000 + if ($p.HasExited) { throw "installer exited instead of staying on the welcome card" } + Shot '04-still-welcome' + $rb = Get-Rect + $wb = $rb.R - $rb.L + # 开装了窗口会缩成 360x132 的角落小进度卡;还是 760 宽就说明确实没开装 + if ($wb -lt 700) { throw "installer proceeded to the progress card despite insufficient space (window width $wb)" } + # 从大卡片右上角 ✕ 退出(AWDUI_CLOSE 712,8,40,32 → 中心 732,24) + ClickAt 732 24 + Start-Sleep -Seconds 2 + if (-not $p.HasExited) { $p.Kill(); throw "installer did not exit after closing the welcome card" } + Write-Host 'blocked flow completed: gate fired, install never started, installer closed cleanly' + exit 0 +} + Start-Sleep -Milliseconds 1500 Shot '03-progress' Start-Sleep -Milliseconds 2500 From 43d3787f9ed7547f330470c89823de65cbc34159 Mon Sep 17 00:00:00 2001 From: zeweihan Date: Tue, 1 Sep 2026 11:49:34 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix(installer):=20=E7=A3=81=E7=9B=98?= =?UTF-8?q?=E9=97=B8=E9=A6=96=E8=BD=AE=20CI=20=E5=AE=9E=E8=B7=91=E7=9A=84?= =?UTF-8?q?=E4=B8=A4=E5=A4=84=E4=BF=AE=E6=AD=A3=EF=BC=88dev-board#350?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 首轮 installer-ui-smoke(run 33466932338)证明闸本身是对的:截图里提示框写着 「安装需要约 858.3 GB,C: 盘当前可用 31.7 GB」,卡片没进安装页、窗口仍是 760 宽。 两处要修: 1. 标签没变红。InvalidateRect 只打脏标记,而提示框在卡片中部、盖不住右下角那个 「可用 31 GB」标签,关框时不会顺带擦除重画它,颜色就一直是旧的灰。改用 RedrawWindow(RDW_INVALIDATE|RDW_ERASE|RDW_UPDATENOW) 同步重绘。 2. 冒烟收尾把用例判红了。拦下用例最后点大卡片右上角 ✕ 想干净退出,实跑里进程 2 秒内没退(AwdCloseClick 是 Quit,从 nsDialogs::Show 的回调里调用是否即时生效 没有验证过)——这条路径与本闸无关,不该拿它把用例判红。收尾改为直接杀进程; 关窗行为由 zh/en 两条正常流程在完成卡上覆盖。 顺带留个观察给维护者:大卡片 ✕ 这条路至今没有任何用例覆盖过,值得单独看一眼。 Co-Authored-By: Claude Opus 5 --- desktop/build/win/awd-oneclick-ui.nsh | 6 +++++- desktop/scripts/installer-smoke.ps1 | 11 ++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/desktop/build/win/awd-oneclick-ui.nsh b/desktop/build/win/awd-oneclick-ui.nsh index f0c3585de..7b7b65c10 100644 --- a/desktop/build/win/awd-oneclick-ui.nsh +++ b/desktop/build/win/awd-oneclick-ui.nsh @@ -495,7 +495,11 @@ Function AwdSpaceRefused ; 只改颜色不改文案:标签宽 ${AWDUI_SPACE_W} 是按「可用 XX GB」排的版,加字会溢出, ; 而文案与美术位图、热区坐标是同一套基准,改一处要改三处。 SetCtlColors $AwdSpaceLabel 0xC0392B 0xFFFFFF - System::Call 'user32::InvalidateRect(p $AwdSpaceLabel, p 0, i 1)' + ; 必须 RedrawWindow 同步重绘,不能只 InvalidateRect:提示框在卡片中部、盖不住 + ; 右下角这个标签,关框时不会顺带擦除重画它,只打脏标记的话颜色一直是旧的 + ;(首轮 CI 截图实锤:文案对了、标签还是灰的)。 + ; RDW_INVALIDATE|RDW_ERASE|RDW_UPDATENOW = 0x1|0x4|0x100 + System::Call 'user32::RedrawWindow(p $AwdSpaceLabel, p 0, p 0, i 0x105)' !endif ${AwdGbText} $R3 $AwdNeedMb diff --git a/desktop/scripts/installer-smoke.ps1 b/desktop/scripts/installer-smoke.ps1 index 023981631..62edf95aa 100644 --- a/desktop/scripts/installer-smoke.ps1 +++ b/desktop/scripts/installer-smoke.ps1 @@ -146,11 +146,12 @@ if ($ExpectBlocked) { $wb = $rb.R - $rb.L # 开装了窗口会缩成 360x132 的角落小进度卡;还是 760 宽就说明确实没开装 if ($wb -lt 700) { throw "installer proceeded to the progress card despite insufficient space (window width $wb)" } - # 从大卡片右上角 ✕ 退出(AWDUI_CLOSE 712,8,40,32 → 中心 732,24) - ClickAt 732 24 - Start-Sleep -Seconds 2 - if (-not $p.HasExited) { $p.Kill(); throw "installer did not exit after closing the welcome card" } - Write-Host 'blocked flow completed: gate fired, install never started, installer closed cleanly' + # 收尾直接杀进程,不点大卡片的 ✕。本用例要断言的是「闸拦住了、没开装」,上面 + # 三条断言已经全覆盖;关窗行为由 zh/en 两条正常流程在完成卡上覆盖。首轮 CI 实跑里 + # 这一步点 ✕ 后进程 2 秒内没退(AwdCloseClick 是 Quit,从 nsDialogs::Show 的回调里 + # 调用是否即时生效没有验证过),拿一条与本闸无关的路径把用例判红不值当。 + $p.Kill() + Write-Host 'blocked flow completed: gate fired, install never started' exit 0 } From 92d90e15b3dba188c318182952796f91b9111d23 Mon Sep 17 00:00:00 2001 From: zeweihan Date: Tue, 1 Sep 2026 11:57:20 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix(installer):=20=E7=A3=81=E7=9B=98?= =?UTF-8?q?=E9=97=B8=E4=B8=8D=E5=86=8D=E5=B0=9D=E8=AF=95=E6=9F=93=E7=BA=A2?= =?UTF-8?q?=E5=8F=AF=E7=94=A8=E7=A9=BA=E9=97=B4=E6=A0=87=E7=AD=BE=EF=BC=88?= =?UTF-8?q?dev-board#350=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 两轮 installer-ui-smoke 的截图都证明运行期改这个控件的颜色不生效:先试 SetCtlColors + InvalidateRect(第一轮,文案对、标签还是灰的),再试 SetCtlColors + RedrawWindow 同步重绘(第二轮,还是灰的)。本机没有 Windows 真机可调, 而提示框已经把「需要多少 / 该盘有多少 / 怎么办」说全了——与其留一段看着像生效 其实没生效的代码,不如不留,把「为什么不做」的结论写进注释。 顺带把 AwdUpdateSpace 还原成 master 原样(那句颜色复位只为撤销染红而存在)。 本文件相对 master 现在是纯新增,一行都没删。 Co-Authored-By: Claude Opus 5 --- desktop/build/win/awd-oneclick-ui.nsh | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/desktop/build/win/awd-oneclick-ui.nsh b/desktop/build/win/awd-oneclick-ui.nsh index 7b7b65c10..158654bd2 100644 --- a/desktop/build/win/awd-oneclick-ui.nsh +++ b/desktop/build/win/awd-oneclick-ui.nsh @@ -406,9 +406,6 @@ Function AwdBrowseClick FunctionEnd Function AwdUpdateSpace - ; 先把颜色复位:磁盘闸拦下时 AwdSpaceRefused 会把这个标签染红(dev-board#350), - ; 用户改完路径再进来必须变回常态,否则换到大盘上了标签还红着,反而误导。 - SetCtlColors $AwdSpaceLabel 0x8A9590 0xFFFFFF ${NSD_GetText} $AwdDirEdit $0 ${GetRoot} $0 $1 ${If} $1 == "" @@ -489,19 +486,14 @@ Function AwdSpaceShort FunctionEnd ; 就地拦下并说清楚差多少。 +; 提示只走对话框,**没有**去染红展开行里那个「可用 X GB」标签:试过 +; SetCtlColors + InvalidateRect、再试 SetCtlColors + RedrawWindow(同步重绘), +; 两轮 installer-ui-smoke 截图都证明运行期改这个控件的颜色不生效(文案对、标签还是灰的)。 +; 没有 Windows 真机可调,而对话框已经把「需要多少 / 该盘有多少 / 怎么办」说全了, +; 与其留一段看着像生效其实没生效的代码,不如不留。要改文案也不行:标签宽 +; ${AWDUI_SPACE_W} 是按「可用 XX GB」排的版,加字会溢出,且文案与美术位图、热区坐标 +; 是同一套 96dpi 基准,改一处要改三处。 Function AwdSpaceRefused - !ifdef AWD_UI_DIR_CHOICE - ; 展开态下把「可用 X GB」染红(收起态这个标签本来就不可见,只靠下面的对话框)。 - ; 只改颜色不改文案:标签宽 ${AWDUI_SPACE_W} 是按「可用 XX GB」排的版,加字会溢出, - ; 而文案与美术位图、热区坐标是同一套基准,改一处要改三处。 - SetCtlColors $AwdSpaceLabel 0xC0392B 0xFFFFFF - ; 必须 RedrawWindow 同步重绘,不能只 InvalidateRect:提示框在卡片中部、盖不住 - ; 右下角这个标签,关框时不会顺带擦除重画它,只打脏标记的话颜色一直是旧的 - ;(首轮 CI 截图实锤:文案对了、标签还是灰的)。 - ; RDW_INVALIDATE|RDW_ERASE|RDW_UPDATENOW = 0x1|0x4|0x100 - System::Call 'user32::RedrawWindow(p $AwdSpaceLabel, p 0, p 0, i 0x105)' - !endif - ${AwdGbText} $R3 $AwdNeedMb ${AwdGbText} $R4 $AwdFreeMb ${If} $AwdLang == "zh"