From 30a190d183d6bc543ac7dd708236bfa04557e0b0 Mon Sep 17 00:00:00 2001 From: kdletters <61648117+kdletters@users.noreply.github.com> Date: Tue, 29 Sep 2026 17:59:36 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20AGC=20NSIS=20=E5=AE=89?= =?UTF-8?q?=E8=A3=85=E9=92=A9=E5=AD=90=E5=9C=A8=E6=A8=A1=E6=9D=BF=E5=B8=B8?= =?UTF-8?q?=E9=87=8F=E4=B8=8E=E6=8F=92=E4=BB=B6=E7=9B=AE=E5=BD=95=E4=B9=8B?= =?UTF-8?q?=E5=89=8D=E7=BC=96=E8=AF=91=E5=AF=BC=E8=87=B4=E7=9A=84=E5=87=BA?= =?UTF-8?q?=E5=8C=85=E5=A4=B1=E8=B4=A5=20-=20installer-hooks.nsh=20?= =?UTF-8?q?=E9=A1=B6=E5=B1=82=E6=94=B6=E6=95=9B=E4=B8=BA=20!define=20/=20V?= =?UTF-8?q?ar=20/=20!macro=EF=BC=9A=E8=BF=81=E7=A7=BB=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E6=94=B9=E6=88=90=20!macro=20AgcMigrateLegacyIdentity=EF=BC=8C?= =?UTF-8?q?=E7=94=B1=20!macro=20NSIS=5FHOOK=5FPOSTINSTALL=20=E5=9C=A8?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF=E6=8F=92=E5=85=A5=E7=82=B9=EF=BC=88Section?= =?UTF-8?q?=20Install=EF=BC=8Caddplugindir=20=E4=B9=8B=E5=90=8E=EF=BC=89?= =?UTF-8?q?=E5=B1=95=E5=BC=80=EF=BC=8C=E9=81=BF=E5=85=8D=20include=20?= =?UTF-8?q?=E6=97=B6=20INSTALLMODE=20=E6=9C=AA=E5=AE=9A=E4=B9=89=E8=B5=B0?= =?UTF-8?q?=E9=94=99=E5=88=86=E6=94=AF=E3=80=81nsis=5Ftauri=5Futils=20?= =?UTF-8?q?=E6=8F=92=E4=BB=B6=E7=9B=AE=E5=BD=95=E8=BF=98=E6=B2=A1=20add=20?= =?UTF-8?q?=E5=AF=BC=E8=87=B4=20makensis=20=E6=8A=A5=20Plugin=20not=20foun?= =?UTF-8?q?d=20=E4=B8=AD=E6=96=AD=20-=20=E6=97=A7=E5=B1=95=E7=A4=BA?= =?UTF-8?q?=E5=90=8D=E6=94=B9=E8=B5=B0=E8=BF=90=E8=A1=8C=E6=9C=9F=E5=8F=98?= =?UTF-8?q?=E9=87=8F=20$AgcLegacyIdentity=EF=BC=88=E6=9B=BF=E6=8D=A2=20Pus?= =?UTF-8?q?h/Pop=20+=20Function=EF=BC=89=EF=BC=8C=E5=8E=BB=E6=8E=89=20sect?= =?UTF-8?q?ion=20=E4=B8=8A=E4=B8=8B=E6=96=87=E9=87=8C=E4=B8=8D=E5=8F=AF?= =?UTF-8?q?=E7=94=A8=E7=9A=84=20Return=20=E6=8F=90=E5=89=8D=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=EF=BC=8C=E6=B8=85=E7=90=86=E4=B8=8E=E8=A1=A5=E5=BB=BA?= =?UTF-8?q?=E8=AF=AD=E4=B9=89=E4=BF=9D=E6=8C=81=E4=B8=8D=E5=8F=98=20-=20?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=A4=B4=E8=A1=A5=E7=BC=96=E8=AF=91=E6=9C=9F?= =?UTF-8?q?=E4=BD=8D=E7=BD=AE=E7=BA=AA=E5=BE=8B=EF=BC=9A=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=E5=9C=A8=20installer.nsi=20=E7=AC=AC=2028=20=E8=A1=8C=20includ?= =?UTF-8?q?e=20=E6=9C=AC=E6=96=87=E4=BB=B6=EF=BC=8C=E6=97=A9=E4=BA=8E=20!d?= =?UTF-8?q?efine=20INSTALLMODE=20/=20MAINBINARYNAME=20=E4=B8=8E=20!addplug?= =?UTF-8?q?indir=EF=BC=8C=E9=A1=B6=E5=B1=82=E5=8F=AA=E8=83=BD=E6=94=BE?= =?UTF-8?q?=E5=A3=B0=E6=98=8E=20-=20build-release.test.mjs=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E5=AE=88=E5=8D=AB=E7=94=A8=E4=BE=8B=EF=BC=9A=E9=A1=B6?= =?UTF-8?q?=E5=B1=82=E4=B8=8D=E5=BE=97=E5=87=BA=E7=8E=B0=20Function?= =?UTF-8?q?=E3=80=81=E6=8F=92=E4=BB=B6=E8=B0=83=E7=94=A8=E3=80=81=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=E5=B8=B8=E9=87=8F=E5=BC=95=E7=94=A8=E6=88=96=E8=A3=B8?= =?UTF-8?q?=E5=8F=AF=E6=89=A7=E8=A1=8C=E8=AF=AD=E5=8F=A5=EF=BC=88=E5=AF=B9?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=89=8D=E5=BD=A2=E6=80=81=E5=91=BD=E4=B8=AD?= =?UTF-8?q?=2036=20=E6=9D=A1=EF=BC=8C=E5=90=AB=E7=AC=AC=2068=20=E8=A1=8C?= =?UTF-8?q?=E9=A1=B6=E5=B1=82=E6=8F=92=E4=BB=B6=E8=B0=83=E7=94=A8=EF=BC=89?= =?UTF-8?q?=20-=20pitfalls=20=E4=B8=8E=20decision-log=20=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E8=AF=A5=E8=B8=A9=E5=9D=91=E3=80=81=E6=A0=B9=E5=9B=A0=E3=80=81?= =?UTF-8?q?=E6=9C=AC=E6=9C=BA=E9=80=90=E5=AD=97=E5=A4=8D=E7=8E=B0=E4=B8=8E?= =?UTF-8?q?=E5=A4=84=E7=90=86=E5=8F=A3=E5=BE=84=20-=20AGC=20=E6=B8=A0?= =?UTF-8?q?=E9=81=93=E5=AE=89=E8=A3=85=E8=BA=AB=E4=BB=BD=E9=9A=94=E7=A6=BB?= =?UTF-8?q?=E9=87=8C=E7=A8=8B=E7=A2=91=E8=A1=A5=202026-09-29=20=E9=92=A9?= =?UTF-8?q?=E5=AD=90=E7=BC=96=E8=AF=91=E4=BD=8D=E7=BD=AE=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E5=B0=8F=E8=8A=82=E4=B8=8E=E6=9C=AC=E6=9C=BA=E6=8E=A2=E9=92=88?= =?UTF-8?q?=E5=8F=96=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scripts/build-release.test.mjs | 73 +++++++++++++ .../src-tauri/windows/installer-hooks.nsh | 102 ++++++++++-------- ...€�里程碑】AGC渠道安装身份隔离-2026-09-21.md | 11 ++ .../shared-memory/decision-log.md | 10 ++ docs/project-memory/shared-memory/pitfalls.md | 10 ++ 5 files changed, 159 insertions(+), 47 deletions(-) diff --git a/apps/ai-game-creator-shell/scripts/build-release.test.mjs b/apps/ai-game-creator-shell/scripts/build-release.test.mjs index 6f4c3315d..eed74a9fe 100644 --- a/apps/ai-game-creator-shell/scripts/build-release.test.mjs +++ b/apps/ai-game-creator-shell/scripts/build-release.test.mjs @@ -310,6 +310,79 @@ test('dev 渠道的 Windows 包注入改名迁移钩子,其它渠道与目标 }); }); +/** + * Tauri 2.11 的 NSIS 模板在 `installer.nsi` 第 28 行 include 钩子文件,位置**早于** + * `!define INSTALLMODE` / `!define MAINBINARYNAME` 等模板常量,也早于 + * `!addplugindir "${ADDITIONALPLUGINSPATH}"`(nsis_tauri_utils 插件目录)。 + * 所以顶层只能放声明:顶层 Function 会在 include 处就编译(模板常量还是未定义的, + * makensis 静默替换成空文本),顶层调 `nsis_tauri_utils::*` 会直接 `Plugin not found` + * 并中断整个打包(2026-09-29 Jenkins 打包失败的形态)。可执行逻辑必须待在 !macro 里, + * 等模板的 `!insertmacro NSIS_HOOK_*`(Section Install,addplugindir 之后)展开。 + */ +test('安装钩子的可执行逻辑必须全在 !macro 内,顶层不得引用模板常量或插件', () => { + const templateConstants = [ + 'MAINBINARYNAME', + 'PRODUCTNAME', + 'MANUFACTURER', + 'INSTALLMODE', + 'BUNDLEID', + 'ADDITIONALPLUGINSPATH', + 'VERSION', + 'OUTFILE', + ]; + const hooksPath = createChannelConfig('dev', windowsTarget).bundle.windows + .nsis.installerHooks; + const offenders = []; + let macroDepth = 0; + for (const [index, rawLine] of readFileSync(hooksPath, 'utf8') + .split('\n') + .entries()) { + const line = rawLine.trim(); + if (line === '' || line.startsWith(';')) continue; + if (/^!macroend\b/u.test(line)) { + macroDepth = Math.max(0, macroDepth - 1); + continue; + } + if (/^!macro\b/u.test(line)) { + macroDepth += 1; + continue; + } + if (macroDepth > 0) continue; + + const where = `第 ${index + 1} 行`; + if (/^Function(?:End)?\b/u.test(line)) { + offenders.push(`${where} 顶层定义 Function:函数体在 include 处就编译`); + continue; + } + if (line.includes('::')) { + offenders.push(`${where} 顶层调用插件:此时 addplugindir 还没执行`); + continue; + } + const usedConstant = templateConstants.find((name) => + line.includes(`\${${name}}`), + ); + if (usedConstant) { + offenders.push(`${where} 顶层引用模板常量 \${${usedConstant}}`); + continue; + } + if ( + !/^(?:!define\b|!macro\b|!macroend\b|!ifn?def\b|!if\b|!else\b|!endif\b|!undef\b|Var\b)/u.test( + line, + ) + ) { + offenders.push(`${where} 顶层出现可执行语句:${line}`); + } + } + assert.deepEqual( + offenders, + [], + `installer-hooks.nsh 顶层只能放 !define / Var / !macro;` + + `模板在 installer.nsi 第 28 行 include 它,早于模板常量与 addplugindir,` + + `可执行逻辑一律写进 !macro,等 !insertmacro NSIS_HOOK_* 时再展开:\n` + + offenders.join('\n'), + ); +}); + /** * RFC 7386(tauri-utils 用 `json_patch::merge`)语义:对象递归合并,数组整体替换。 * 这里按同样语义复现 Tauri CLI 的 `--config` 合并,用来守住"渠道配置不得丢窗口契约"。 diff --git a/apps/ai-game-creator-shell/src-tauri/windows/installer-hooks.nsh b/apps/ai-game-creator-shell/src-tauri/windows/installer-hooks.nsh index dee01e1d4..a5dadc3bd 100644 --- a/apps/ai-game-creator-shell/src-tauri/windows/installer-hooks.nsh +++ b/apps/ai-game-creator-shell/src-tauri/windows/installer-hooks.nsh @@ -15,71 +15,79 @@ ; ; 该文件只注入 dev 渠道(改名发生在 dev 渠道)的 Windows 包;其它渠道的展示名从未变过, ; 不能拿这份旧身份表去删它们的安装。 +; +; ⚠️ 编译期位置(改本文件前必读):Tauri 2.11 的 NSIS 模板在 installer.nsi 第 28 行 +; `!include` 本文件,位置早于 `!define INSTALLMODE` / `!define MAINBINARYNAME` 等模板常量, +; 也早于 `!addplugindir "${ADDITIONALPLUGINSPATH}"`(nsis_tauri_utils 插件目录)。所以顶层只能放 +; !define / Var / !macro 这类声明: +; - 顶层引用 `${MAINBINARYNAME}`、`${PRODUCTNAME}` 会命中未定义常量(makensis warning 6000), +; 字符串被静默替换成空文本; +; - 顶层写 `!if "${INSTALLMODE}" == "currentUser"` 会走错分支(此时 INSTALLMODE 还是空值); +; - 顶层调用 nsis_tauri_utils::* 会直接 `Plugin not found`,打包在 makensis 这一步整条中断。 +; 可执行逻辑一律写进 !macro:宏体在模板的 `!insertmacro NSIS_HOOK_*` 处展开(Section Install, +; 位于 addplugindir 之后),那时模板常量与插件都已就绪。 ; 旧展示名表:以后再改展示名,必须在这里追加旧名,否则升级后旧快捷方式会继续指向旧安装。 !define AGC_LEGACY_IDENTITY_1 "陶泥儿" !define AGC_LEGACY_IDENTITY_2 "Genarrative AI Game Creator" +Var AgcLegacyIdentity Var AgcLegacyInstallDir Var AgcMigratedLegacyDesktopShortcut -; 清理一个旧身份。调用方把旧展示名压栈(Push),本函数不返回错误。 +; 清理一个旧身份。调用方先把旧展示名写进 $AgcLegacyIdentity,再 !insertmacro 本宏;宏不返回错误。 ; 只在「旧展示名目录下确实存在我们的主程序」时才动手,避免误删同名目录; ; 当前安装目录 $INSTDIR 永不进入删除路径。 -Function AgcMigrateLegacyIdentity - Pop $R0 +!macro AgcMigrateLegacyIdentity StrCpy $AgcLegacyInstallDir "" - ${If} ${FileExists} "$LOCALAPPDATA\$R0\${MAINBINARYNAME}.exe" - StrCpy $AgcLegacyInstallDir "$LOCALAPPDATA\$R0" - ${ElseIf} ${FileExists} "$PROGRAMFILES64\$R0\${MAINBINARYNAME}.exe" - StrCpy $AgcLegacyInstallDir "$PROGRAMFILES64\$R0" - ${Else} - Return + ${If} ${FileExists} "$LOCALAPPDATA\$AgcLegacyIdentity\${MAINBINARYNAME}.exe" + StrCpy $AgcLegacyInstallDir "$LOCALAPPDATA\$AgcLegacyIdentity" + ${ElseIf} ${FileExists} "$PROGRAMFILES64\$AgcLegacyIdentity\${MAINBINARYNAME}.exe" + StrCpy $AgcLegacyInstallDir "$PROGRAMFILES64\$AgcLegacyIdentity" ${EndIf} - ${If} $AgcLegacyInstallDir == $INSTDIR - Return - ${EndIf} - - ; 桌面图标:只有目标正好指向旧目录的主程序时才删,避免误删同名的用户快捷方式。 - !insertmacro IsShortcutTarget "$DESKTOP\$R0.lnk" "$AgcLegacyInstallDir\${MAINBINARYNAME}.exe" - Pop $0 - ${If} $0 = 1 - StrCpy $AgcMigratedLegacyDesktopShortcut 1 - !insertmacro UnpinShortcut "$DESKTOP\$R0.lnk" - Delete "$DESKTOP\$R0.lnk" - ${EndIf} - - ; 开始菜单图标:必须先删掉,否则它会在当前身份图标补建后继续指向旧 exe。 - !insertmacro IsShortcutTarget "$SMPROGRAMS\$R0.lnk" "$AgcLegacyInstallDir\${MAINBINARYNAME}.exe" - Pop $0 - ${If} $0 = 1 - !insertmacro UnpinShortcut "$SMPROGRAMS\$R0.lnk" - Delete "$SMPROGRAMS\$R0.lnk" - ${EndIf} - - ; 旧安装目录与卸载项整体清理:旧 exe 留着就会继续把新版本装进新目录。 - ; 先结束残留的旧实例(同名主程序):更新路径下调用方通常已退出,这里兜住手动启动的情况。 - !if "${INSTALLMODE}" == "currentUser" - nsis_tauri_utils::KillProcessCurrentUser "${MAINBINARYNAME}.exe" + ${If} $AgcLegacyInstallDir != "" + ${AndIf} $AgcLegacyInstallDir != $INSTDIR + ; 桌面图标:只有目标正好指向旧目录的主程序时才删,避免误删同名的用户快捷方式。 + !insertmacro IsShortcutTarget "$DESKTOP\$AgcLegacyIdentity.lnk" "$AgcLegacyInstallDir\${MAINBINARYNAME}.exe" Pop $0 - !else - nsis_tauri_utils::KillProcess "${MAINBINARYNAME}.exe" + ${If} $0 = 1 + StrCpy $AgcMigratedLegacyDesktopShortcut 1 + !insertmacro UnpinShortcut "$DESKTOP\$AgcLegacyIdentity.lnk" + Delete "$DESKTOP\$AgcLegacyIdentity.lnk" + ${EndIf} + + ; 开始菜单图标:必须先删掉,否则它会在当前身份图标补建后继续指向旧 exe。 + !insertmacro IsShortcutTarget "$SMPROGRAMS\$AgcLegacyIdentity.lnk" "$AgcLegacyInstallDir\${MAINBINARYNAME}.exe" Pop $0 - !endif - RMDir /r "$AgcLegacyInstallDir" - ; 卸载项按展示名登记,旧名与当前名不同,必须按旧名清掉,否则「应用和功能」里会留下孤儿条目。 - DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\$R0" - DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\$R0" - DeleteRegKey HKCU "Software\${MANUFACTURER}\$R0" -FunctionEnd + ${If} $0 = 1 + !insertmacro UnpinShortcut "$SMPROGRAMS\$AgcLegacyIdentity.lnk" + Delete "$SMPROGRAMS\$AgcLegacyIdentity.lnk" + ${EndIf} + + ; 旧安装目录与卸载项整体清理:旧 exe 留着就会继续把新版本装进新目录。 + ; 先结束残留的旧实例(同名主程序):更新路径下调用方通常已退出,这里兜住手动启动的情况。 + !if "${INSTALLMODE}" == "currentUser" + nsis_tauri_utils::KillProcessCurrentUser "${MAINBINARYNAME}.exe" + Pop $0 + !else + nsis_tauri_utils::KillProcess "${MAINBINARYNAME}.exe" + Pop $0 + !endif + RMDir /r "$AgcLegacyInstallDir" + ; 卸载项按展示名登记,旧名与当前名不同,必须按旧名清掉,否则「应用和功能」里会留下孤儿条目。 + DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\$AgcLegacyIdentity" + DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\$AgcLegacyIdentity" + DeleteRegKey HKCU "Software\${MANUFACTURER}\$AgcLegacyIdentity" + ${EndIf} +!macroend !macro NSIS_HOOK_POSTINSTALL - Push "${AGC_LEGACY_IDENTITY_1}" - Call AgcMigrateLegacyIdentity - Push "${AGC_LEGACY_IDENTITY_2}" - Call AgcMigrateLegacyIdentity + StrCpy $AgcLegacyIdentity "${AGC_LEGACY_IDENTITY_1}" + !insertmacro AgcMigrateLegacyIdentity + StrCpy $AgcLegacyIdentity "${AGC_LEGACY_IDENTITY_2}" + !insertmacro AgcMigrateLegacyIdentity ; 迁移过的机器补建当前身份的快捷方式(更新模式下模板不会创建)。 StrCpy $R0 "$INSTDIR\${MAINBINARYNAME}.exe" diff --git a/docs/project-memory/plans/【里程碑】AGC渠道安装身份隔离-2026-09-21.md b/docs/project-memory/plans/【里程碑】AGC渠道安装身份隔离-2026-09-21.md index 959a49308..bcb207f8d 100644 --- a/docs/project-memory/plans/【里程碑】AGC渠道安装身份隔离-2026-09-21.md +++ b/docs/project-memory/plans/【里程碑】AGC渠道安装身份隔离-2026-09-21.md @@ -83,3 +83,14 @@ - **验证**:`node --test apps/ai-game-creator-shell/scripts/build-release.test.mjs` → 41 passed(新增用例断言只有 dev+Windows 注入、钩子文件存在且旧身份表完整、渠道配置经 JSON Merge Patch 后钩子不被 node runtime 资源映射顶掉)。 - **仍未验证**:钩子只在真实 NSIS 打包 + 真机升级时执行。本条需要一次 dev 渠道发布 + 真机升级,才能勾掉下面这条验收项: - [ ] 真机:改名前的 dev 安装(`%LOCALAPPDATA%\陶泥儿`)升级到改名后的包后,旧安装目录、旧快捷方式与旧卸载项被清理,`陶泥儿开发版` 的桌面/开始菜单图标出现且指向新安装。 + +## 本轮修正(2026-09-29,钩子的编译期位置) + +上一小节把改名迁移放进 NSIS installer hooks 之后,Jenkins 的 dev Windows 打包在 makensis 处中断:`Plugin not found, cannot call nsis_tauri_utils::KillProcess` + `!include: error in script: "...\installer-hooks.nsh" on line 68` → `Error in script "...\installer.nsi" on line 28 -- aborting creation process`,退出码 1(Rust 编译已经跑完,白等一轮)。根因是 Tauri 2.11 的模板在**第 28 行**就 include 钩子,早于 `!define INSTALLMODE` / `!define MAINBINARYNAME`(第 37、44 行)与 `!addplugindir "${ADDITIONALPLUGINSPATH}"`(第 89 行):顶层 `Function` 在 include 处立刻编译,于是 `${INSTALLMODE}` 判空走错分支、`nsis_tauri_utils::*` 还找不到插件。 + +- **修正**:钩子顶层收敛为 `!define` / `Var` / `!macro`;迁移逻辑改成 `!macro AgcMigrateLegacyIdentity`(旧展示名改用运行期变量 `$AgcLegacyIdentity` 传入,替换原来的 `Push`/`Pop` + `Function`),由 `!macro NSIS_HOOK_POSTINSTALL` 在模板插入点展开,`${INSTALLMODE}` 分支那时才求值。 +- **本机取证(编译层)**:从本地 `@tauri-apps/cli` 2.11.2 内嵌模板渲染出与构建机同一份 `installer.nsi`(钩子 include 落在第 28 行,与 Jenkins 日志一致),配同一份 `src-tauri/target/.tauri/NSIS/makensis.exe` 复现出**逐字相同**的报错;修复后同一份脚本零 warning 通过并产出安装包。字符串表核对:`currentUser` 形态只命中 `KillProcessCurrentUser`、`perMachine` 形态只命中 `KillProcess`。 +- **本机取证(运行层,探针安装器)**:用探针专属旧身份名 + 探针注册表键(不碰真实安装与真实桌面/开始菜单)实跑四组——无旧目录 → 卸载项与厂商键一个不动;旧目录 + 指向旧 exe 的桌面/开始菜单图标 → 旧目录、旧图标、旧卸载项被清,开始菜单图标补建、桌面图标只在原来就有时补建;桌面同名图标指向 `notepad.exe` → 图标保留且不补建桌面图标。探针产物事后全部清空,真实安装(`%LOCALAPPDATA%\陶泥儿`、其卸载项与桌面/开始菜单 `陶泥儿.lnk`)保持不变。 +- **本机取证(守卫用例)**:`build-release.test.mjs` 新增「安装钩子的可执行逻辑必须全在 !macro 内,顶层不得引用模板常量或插件」;对修复前的钩子文件跑同一解析逻辑命中 36 条顶层违规(含第 68 行顶层插件调用),修复后 0 条。 +- **验证命令**:`node --test apps/ai-game-creator-shell/scripts/build-release.test.mjs` → 42 passed。 +- **仍未验证**:真实 Jenkins dev 渠道出包 + 真机升级。上一小节最后那条真机验收项保持打开(本机现在正好是「旧身份 `%LOCALAPPDATA%\陶泥儿` + 旧图标」的样本,可以直接用它验收)。 diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 3f48e8adf..2707d781e 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -1,5 +1,15 @@ # 决策记录 +## 2026-09-29 安装钩子的可执行逻辑只允许待在 !macro 里 + +- 背景:dev 渠道改名迁移钩子的第一版把迁移写成顶层 `Function`,并在函数体里调 `nsis_tauri_utils::KillProcess`;Jenkins 打 Windows 包时 makensis 在 `installer-hooks.nsh` 第 68 行报 `Plugin not found` 并中断(模板第 28 行 include 钩子,早于模板常量与 `!addplugindir`)。 +- 决策(形态):`installer-hooks.nsh` 顶层只允许 `!define` / `Var` / `!macro`;所有可执行逻辑写进宏体,靠模板的 `!insertmacro NSIS_HOOK_*`(Section Install,插件目录已 add、模板常量已 define)展开求值;`${INSTALLMODE}` 这类模板常量只在宏体内引用。 +- 决策(传参):旧展示名改用运行期变量 `$AgcLegacyIdentity` 传入,不再用 `Push`/`Pop` + `Function`——宏体在 section 上下文展开,既拿不到 include 期还不存在的模板常量,也不能用 `Return` 提前返回。 +- 决策(守卫):把「顶层形态」写成用例(`build-release.test.mjs`:顶层不得出现 Function、插件调用、模板常量引用或裸可执行语句),让同形态回归在单测阶段就红,而不是等到构建机上的 makensis 中断。 +- 影响范围:`apps/ai-game-creator-shell/src-tauri/windows/installer-hooks.nsh`、`apps/ai-game-creator-shell/scripts/build-release.test.mjs`、pitfalls。 +- 验证:`node --test apps/ai-game-creator-shell/scripts/build-release.test.mjs` → 42 passed(含新增守卫用例);本地按 Tauri 模板渲染 + 同一份 makensis 复现 Jenkins 原始报错并在修复后零 warning 通过;`currentUser` / `perMachine` 两种形态分别编出 `KillProcessCurrentUser` / `KillProcess`。 +- 边界(未验证):真实 Jenkins dev 渠道出包与真机升级仍未执行,里程碑里的升级验收项保持打开。 + ## 2026-09-29 策划提示词写明工作区是相对路径根目录 - 决策:常驻提示词用一句说明工作区是相对路径根目录 `.`。策划文件仍放在工作区内并使用相对路径。不命名 `design_artifacts`,不宣布可以访问绝对路径或工作区外路径。 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 1086146c7..8c9ed77f7 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -1,5 +1,15 @@ # 踩坑与排障记录 +## 2026-09-29 NSIS 安装钩子被 include 在模板常量与插件目录之前(Jenkins 打包在 makensis 处中断) + +- **现象**:`Genarrative-Agc-Windows-Build` 打 dev 渠道 Windows 包时,Rust 编译过了,makensis 却报 `Plugin not found, cannot call nsis_tauri_utils::KillProcess`、`!include: error in script: "...\installer-hooks.nsh" on line 68`、`Error in script "...\installer.nsi" on line 28 -- aborting creation process`,Tauri 最后只补一句 `failed to bundle project 'The system cannot find the file specified. (os error 2)'`,退出码 1。 +- **原因**:Tauri 2.11 的 NSIS 模板把钩子放在 `installer.nsi` **第 28 行**(`{{#if installer_hooks}} !include "..." {{/if}}`,紧跟 `StrFunc.nsh`),早于 `!define INSTALLMODE`(第 37 行)与 `!define MAINBINARYNAME`(第 44 行),更早于第 89 行的 `!addplugindir "${ADDITIONALPLUGINSPATH}"`。钩子文件的第一版是顶层 `Function`,于是三件事同时踩雷:① include 时 `${INSTALLMODE}` 还是空值,`!if "${INSTALLMODE}" == "currentUser"` 走 `!else` 分支——编出来的正是报错里的第 68 行 `nsis_tauri_utils::KillProcess`;② 插件目录此时还没 add,makensis 找不到 `nsis_tauri_utils.dll`,直接中断整个打包;③ 同一函数体里的 `${MAINBINARYNAME}` / `${PRODUCTNAME}` / `${MANUFACTURER}` 也未定义(makensis warning 6000,字符串被静默替换成空文本)。只有第 29–94 行的顶层 `Function` 会这样,因为它跟着 include 立刻编译;`!macro` 体是插入时才展开的,所以放进宏里就没问题。 +- **本机复现**:从本地 `@tauri-apps/cli` 2.11.2 的二进制里取出内嵌模板,渲染出与构建机同一份 `installer.nsi`(钩子 include 正好落在第 28 行,与 Jenkins 日志一致),再用同一份 `src-tauri/target/.tauri/NSIS/makensis.exe` 编译 → 得到与 Jenkins **逐字相同**的三行报错;修复后同一份脚本零 warning 通过并产出安装包。修好的包字符串表里,`currentUser` 形态只命中 `KillProcessCurrentUser`、`perMachine` 形态只命中 `KillProcess`,证明 `${INSTALLMODE}` 分支现在是在宏展开时求值的。 +- **处理(现行口径)**:`installer-hooks.nsh` 顶层只允许 `!define` / `Var` / `!macro`;可执行逻辑全部待在宏里——迁移逻辑是 `!macro AgcMigrateLegacyIdentity`,旧展示名改用运行期变量 `$AgcLegacyIdentity` 传入(原来的 `Push`/`Pop` + `Function` 形态在 section 上下文里既拿不到模板常量、也不能用 `Return` 提前返回),由 `!macro NSIS_HOOK_POSTINSTALL` 在模板插入点(Section Install,addplugindir 之后)展开。 +- **判据/取证**:① `apps/ai-game-creator-shell/scripts/build-release.test.mjs` 新增守卫用例「安装钩子的可执行逻辑必须全在 !macro 内,顶层不得引用模板常量或插件」——对修复前的钩子文件跑同一解析逻辑命中 36 条顶层违规(含「第 68 行 顶层调用插件」),修复后 0 条;② 上面那份模板复现 + 双 `INSTALLMODE` 形态编译;③ 探针安装器(探针专属旧身份名 + 探针注册表键,全程不碰真实安装)实跑四组:旧目录不存在 → 卸载项与厂商键一个不动;旧目录 + 指向旧 exe 的桌面/开始菜单图标 → 旧目录、旧图标、旧卸载项被清,开始菜单图标补建、桌面图标只在原来就有时补建;桌面同名图标指向 `notepad.exe` → 图标保留且不补建桌面图标。 +- **不要踩的坑**:① 钩子顶层任何"本地看着能编译"的可执行逻辑都可能在构建机上炸掉整条发布,改钩子后必须至少跑一次真实 NSIS 打包或上面那条守卫用例;② 顶层引用 `${INSTALLMODE}` 这类模板常量不会报错,只会静默走错分支;③ 模板的 `${MAINBINARYNAME}` 是**不带 `.exe`** 的名字(模板自己拼 `.exe`),钩子里不要再加一次扩展名;④ 钩子文件含中文身份字面量,必须存成 UTF-8 with BOM。 +- **关联**:`apps/ai-game-creator-shell/src-tauri/windows/installer-hooks.nsh`、`apps/ai-game-creator-shell/scripts/build-release.mjs`、`apps/ai-game-creator-shell/scripts/build-release.test.mjs`、`docs/project-memory/plans/【里程碑】AGC渠道安装身份隔离-2026-09-21.md`。 + ## 2026-09-29 Codex 隔离用户目录被系统临时目录的符号链接拦下 - **现象**:Game Agent 回合还没改项目文件就失败。卡片是 `direct-codex-failure:v2 stage=code-generation code=runtime-failure`,摘要为「创建 Codex app-server 隔离用户目录失败:Codex 隔离用户目录 路径不能包含符号链接:」。诊断文件同样把路径脱敏,看不出是哪一级目录。