Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .claude/agents/eng-infra.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 后若补丁脚本报「结构已变」**:先确认新版是否已自带该修复,再决定要不要删掉本补丁,不要盲目跳过。

## 测试命令总表
Expand Down
17 changes: 14 additions & 3 deletions .github/workflows/installer-ui-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 '<?xml version="1.0"?><OfficeApp/>'
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

Expand All @@ -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,欢迎页少一组控件、安装秒完——点自定义安装
Expand Down Expand Up @@ -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' }

Expand Down
23 changes: 23 additions & 0 deletions desktop/build/installer.nsh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
108 changes: 108 additions & 0 deletions desktop/build/win/awd-oneclick-ui.nsh
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<n>" ; 可选:安装所需磁盘空间(KB)。设了才有磁盘闸;
; ; 桌面端直接给 electron-builder 的 APP_64_UNPACKED_SIZE
; !define AWD_UI_REQUIRED_EXTRA_KB "<n>" ; 可选:所需空间之外的余量(KB),默认 0
; !define AWD_UI_TERMS_URL / AWD_UI_PRIVACY_URL
; !macro AwdUiOnLaunch ; 可选:完成卡「立即体验」的动作(缺省=仅关闭)
; 然后在页面序列里插:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -414,6 +422,96 @@ 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

; 就地拦下并说清楚差多少。
; 提示只走对话框,**没有**去染红展开行里那个「可用 X GB」标签:试过
; SetCtlColors + InvalidateRect、再试 SetCtlColors + RedrawWindow(同步重绘),
; 两轮 installer-ui-smoke 截图都证明运行期改这个控件的颜色不生效(文案对、标签还是灰的)。
; 没有 Windows 真机可调,而对话框已经把「需要多少 / 该盘有多少 / 怎么办」说全了,
; 与其留一段看着像生效其实没生效的代码,不如不留。要改文案也不行:标签宽
; ${AWDUI_SPACE_W} 是按「可用 XX GB」排的版,加字会溢出,且文案与美术位图、热区坐标
; 是同一套 96dpi 基准,改一处要改三处。
Function AwdSpaceRefused
${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
Expand All @@ -434,6 +532,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
Expand Down
8 changes: 7 additions & 1 deletion desktop/build/win/ui-harness.nsi
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
; Windows runner 上编译、驱动并逐阶段截图(dev-board#339)。
; 编译参数:
; makensis -INPUTCHARSET UTF8 -DHARNESS_LANG=SimpChinese|English
; -DGENART=<位图目录> -DPAYLOAD=<假 payload 目录> -DOUTFILE=<exe> ui-harness.nsi
; -DGENART=<位图目录> -DPAYLOAD=<假 payload 目录> -DOUTFILE=<exe>
; [-DREQUIRED_KB=<n>] ui-harness.nsi
; REQUIRED_KB 打开磁盘空间闸(dev-board#350):给个小值走「够用」分支,给个大到
; 不可能满足的值走「拦下」分支——installer-ui-smoke 两个分支都编译并实跑截图。
Unicode true
!include "MUI2.nsh"

Expand All @@ -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
Expand Down
Loading
Loading