修复 AGC 渠道更新互相终止与快捷方式迁移
Project CI / AI game creator shell Rust crates (push) Successful in 3m5s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 4m24s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 5m20s
Project CI / Backend tests (push) Successful in 6m42s
Project CI / Frontend tests (push) Successful in 3m2s
Project CI / Native shell tests (push) Successful in 7m16s
Project CI / AI game creator shell web tests (push) Successful in 3m17s
Project CI / Repository checks (push) Successful in 6m25s
Project CI / AI game creator shell Rust crates (push) Successful in 3m5s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 4m24s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 5m20s
Project CI / Backend tests (push) Successful in 6m42s
Project CI / Frontend tests (push) Successful in 3m2s
Project CI / Native shell tests (push) Successful in 7m16s
Project CI / AI game creator shell web tests (push) Successful in 3m17s
Project CI / Repository checks (push) Successful in 6m25s
为 release 与自定义渠道注入独立主程序名,隔离 Windows 更新进程。 新增 release 旧安装目录与快捷方式迁移钩子,保护 dev 历史目录清理。 同步渠道规范、共享决策与排障记录,补充发布脚本回归测试。
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
import {
|
||||
AGC_DEFAULT_CHANNEL,
|
||||
resolveChannelInstallIdentity,
|
||||
resolveChannelMainBinaryName,
|
||||
resolveReleaseChannel,
|
||||
} from './channel-identity.mjs';
|
||||
import { prepareNsisToolsetForRelease } from './nsis-toolset.mjs';
|
||||
@@ -38,6 +39,9 @@ const appRoot = fileURLToPath(new URL('..', import.meta.url));
|
||||
const devChannelInstallerHooks = fileURLToPath(
|
||||
new URL('../src-tauri/windows/installer-hooks.nsh', import.meta.url),
|
||||
);
|
||||
const releaseChannelInstallerHooks = fileURLToPath(
|
||||
new URL('../src-tauri/windows/release-installer-hooks.nsh', import.meta.url),
|
||||
);
|
||||
// Git 命令用于读取 release revision,必须在仓库根执行,不能在应用目录里执行。
|
||||
const repoRoot = path.resolve(appRoot, '..', '..');
|
||||
const defaultReleaseTarget = 'x86_64-pc-windows-msvc';
|
||||
@@ -423,6 +427,7 @@ export function createChannelConfig(
|
||||
const config = {
|
||||
productName,
|
||||
identifier,
|
||||
mainBinaryName: resolveChannelMainBinaryName(channel),
|
||||
app: {
|
||||
windows: [{ ...baseClientWindow, title: productName }],
|
||||
},
|
||||
@@ -432,10 +437,20 @@ export function createChannelConfig(
|
||||
},
|
||||
},
|
||||
};
|
||||
// 只有 dev 渠道改过展示名:它的包需要自带旧身份迁移;其它渠道不能拿这份旧名表去删安装。
|
||||
if (channel === AGC_DEFAULT_CHANNEL && target.includes('windows'))
|
||||
// dev 渠道迁移改名前的旧身份;release 渠道迁移曾使用过「陶泥儿 Release」的安装与快捷方式。
|
||||
if (
|
||||
target.includes('windows') &&
|
||||
(channel === AGC_DEFAULT_CHANNEL || channel === 'release')
|
||||
)
|
||||
config.bundle = {
|
||||
windows: { nsis: { installerHooks: devChannelInstallerHooks } },
|
||||
windows: {
|
||||
nsis: {
|
||||
installerHooks:
|
||||
channel === AGC_DEFAULT_CHANNEL
|
||||
? devChannelInstallerHooks
|
||||
: releaseChannelInstallerHooks,
|
||||
},
|
||||
},
|
||||
};
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -35,8 +35,10 @@ import {
|
||||
} from './build-release.mjs';
|
||||
import {
|
||||
AGC_APP_IDENTIFIER,
|
||||
AGC_MAIN_BINARY_NAME,
|
||||
AGC_PRODUCT_NAME,
|
||||
resolveChannelInstallIdentity,
|
||||
resolveChannelMainBinaryName,
|
||||
} from './channel-identity.mjs';
|
||||
import { supportedHostTarget } from './prepare-bundled-resources.mjs';
|
||||
|
||||
@@ -188,6 +190,7 @@ test('channel manifest URL and build-time endpoint follow the channel', () => {
|
||||
assert.deepEqual(createChannelConfig('dev', 'aarch64-apple-darwin'), {
|
||||
productName: `${AGC_PRODUCT_NAME}开发版`,
|
||||
identifier: AGC_APP_IDENTIFIER,
|
||||
mainBinaryName: AGC_MAIN_BINARY_NAME,
|
||||
app: {
|
||||
windows: [
|
||||
{
|
||||
@@ -236,6 +239,15 @@ test('channel install identity isolates co-installed builds and keeps the defaul
|
||||
productName: '陶泥儿 Beta-2',
|
||||
identifier: `${AGC_APP_IDENTIFIER}.beta-2`,
|
||||
});
|
||||
assert.equal(resolveChannelMainBinaryName('dev'), AGC_MAIN_BINARY_NAME);
|
||||
assert.equal(
|
||||
resolveChannelMainBinaryName('release'),
|
||||
`${AGC_MAIN_BINARY_NAME}-release`,
|
||||
);
|
||||
assert.equal(
|
||||
resolveChannelMainBinaryName('beta-2'),
|
||||
`${AGC_MAIN_BINARY_NAME}-beta-2`,
|
||||
);
|
||||
|
||||
// release 与其它非默认渠道通过独立 identifier 隔离运行身份与数据目录;
|
||||
// 自定义渠道另外保留可区分的产品名。
|
||||
@@ -269,6 +281,7 @@ test('channel install identity is baked into the same build-time config as the e
|
||||
const config = createChannelConfig('release', windowsTarget);
|
||||
assert.equal(config.productName, AGC_PRODUCT_NAME);
|
||||
assert.equal(config.identifier, `${AGC_APP_IDENTIFIER}.release`);
|
||||
assert.equal(config.mainBinaryName, `${AGC_MAIN_BINARY_NAME}-release`);
|
||||
assert.match(
|
||||
config.plugins.updater.endpoints[0],
|
||||
/\/release-win\/latest\.json$/u,
|
||||
@@ -280,7 +293,7 @@ test('channel install identity is baked into the same build-time config as the e
|
||||
* dev 渠道展示名改过两次(Genarrative AI Game Creator → 陶泥儿 → 陶泥儿开发版),而更新路径
|
||||
* (/UPDATE)不会重建快捷方式:改名后的包必须自带旧身份迁移,否则桌面快捷方式会一直指向旧安装。
|
||||
*/
|
||||
test('dev 渠道的 Windows 包注入改名迁移钩子,其它渠道与目标不注入', () => {
|
||||
test('渠道 Windows 包注入各自迁移钩子,且主进程文件名按渠道隔离', () => {
|
||||
withEnv({ AGC_UPDATE_OSS_BASE_URL: undefined }, () => {
|
||||
const devWindows = createChannelConfig('dev', windowsTarget);
|
||||
const hooksPath = devWindows.bundle.windows.nsis.installerHooks;
|
||||
@@ -296,10 +309,27 @@ test('dev 渠道的 Windows 包注入改名迁移钩子,其它渠道与目标
|
||||
`迁移钩子缺少旧身份:${legacyIdentity}`,
|
||||
);
|
||||
}
|
||||
assert.match(
|
||||
hooksSource,
|
||||
/AGC_RELEASE_MAIN_BINARY\s+"genarrative-ai-game-creator-shell-release"/u,
|
||||
);
|
||||
|
||||
// 改名只发生在 dev:其它渠道注入会拿 dev 的旧名表去删别人的安装。
|
||||
const releaseWindows = createChannelConfig('release', windowsTarget);
|
||||
const releaseHooksPath = releaseWindows.bundle.windows.nsis.installerHooks;
|
||||
assert.ok(releaseHooksPath);
|
||||
assert.equal(existsSync(releaseHooksPath), true);
|
||||
assert.match(readFileSync(releaseHooksPath, 'utf8'), /陶泥儿 Release/u);
|
||||
assert.equal(
|
||||
createChannelConfig('release', windowsTarget).bundle,
|
||||
releaseWindows.mainBinaryName,
|
||||
`${AGC_MAIN_BINARY_NAME}-release`,
|
||||
);
|
||||
assert.notEqual(
|
||||
devWindows.mainBinaryName,
|
||||
releaseWindows.mainBinaryName,
|
||||
'dev 与 release 的 NSIS 进程匹配名必须不同',
|
||||
);
|
||||
assert.equal(
|
||||
createChannelConfig('beta-2', windowsTarget).bundle,
|
||||
undefined,
|
||||
);
|
||||
// macOS 包没有 NSIS 安装器,不能带上 Windows 钩子。
|
||||
@@ -395,6 +425,44 @@ test('安装钩子的可执行逻辑必须全在 !macro 内,顶层不得引用
|
||||
);
|
||||
});
|
||||
|
||||
test('release 安装钩子迁移旧 Release 身份且保持编译期位置纪律', () => {
|
||||
const hooksPath = createChannelConfig('release', windowsTarget).bundle.windows
|
||||
.nsis.installerHooks;
|
||||
const hooksSource = readFileSync(hooksPath, 'utf8');
|
||||
assert.deepEqual(
|
||||
readFileSync(hooksPath).subarray(0, 3),
|
||||
Buffer.from([0xef, 0xbb, 0xbf]),
|
||||
'NSIS 中文钩子必须保留 UTF-8 BOM',
|
||||
);
|
||||
assert.match(hooksSource, /陶泥儿 Release/u);
|
||||
assert.match(hooksSource, /AGC_RELEASE_LEGACY_BINARY/u);
|
||||
assert.match(hooksSource, /NSIS_HOOK_PREINSTALL/u);
|
||||
assert.match(hooksSource, /NSIS_HOOK_POSTINSTALL/u);
|
||||
let macroDepth = 0;
|
||||
const offenders = [];
|
||||
for (const [index, rawLine] of hooksSource.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;
|
||||
if (
|
||||
line.includes('::') ||
|
||||
/^(?:Function(?:End)?|Return|Delete|ReadRegStr|StrCpy|CreateShortCut)\b/u.test(
|
||||
line,
|
||||
)
|
||||
)
|
||||
offenders.push(`第 ${index + 1} 行出现顶层可执行语句:${line}`);
|
||||
}
|
||||
assert.deepEqual(offenders, []);
|
||||
});
|
||||
|
||||
/**
|
||||
* RFC 7386(tauri-utils 用 `json_patch::merge`)语义:对象递归合并,数组整体替换。
|
||||
* 这里按同样语义复现 Tauri CLI 的 `--config` 合并,用来守住"渠道配置不得丢窗口契约"。
|
||||
|
||||
@@ -16,6 +16,12 @@
|
||||
export const AGC_DEFAULT_CHANNEL = 'dev';
|
||||
export const AGC_PRODUCT_NAME = '陶泥儿';
|
||||
export const AGC_APP_IDENTIFIER = 'world.genarrative.ai-game-creator';
|
||||
/**
|
||||
* Tauri/NSIS 默认按主可执行文件名查找并结束进程。
|
||||
* dev 保留历史文件名以维持既有更新链;其它渠道使用独立文件名,
|
||||
* 这样更新一个渠道时不会把另一个渠道的进程一起结束。
|
||||
*/
|
||||
export const AGC_MAIN_BINARY_NAME = 'genarrative-ai-game-creator-shell';
|
||||
|
||||
const reservedChannelNames = new Set([
|
||||
'win',
|
||||
@@ -75,3 +81,11 @@ export function resolveChannelInstallIdentity(channel = AGC_DEFAULT_CHANNEL) {
|
||||
: `${AGC_APP_IDENTIFIER}.${channel}`,
|
||||
});
|
||||
}
|
||||
|
||||
/** 渠道对应的主可执行文件名(不含平台扩展名)。 */
|
||||
export function resolveChannelMainBinaryName(channel = AGC_DEFAULT_CHANNEL) {
|
||||
validateReleaseChannel(channel);
|
||||
return channel === AGC_DEFAULT_CHANNEL
|
||||
? AGC_MAIN_BINARY_NAME
|
||||
: `${AGC_MAIN_BINARY_NAME}-${channel}`;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,9 @@ execFileSync(
|
||||
|
||||
import {
|
||||
AGC_APP_IDENTIFIER,
|
||||
AGC_MAIN_BINARY_NAME,
|
||||
resolveChannelInstallIdentity,
|
||||
resolveChannelMainBinaryName,
|
||||
} from './channel-identity.mjs';
|
||||
import {
|
||||
askHidden,
|
||||
@@ -1299,9 +1301,14 @@ if (tauriConfig.identifier !== AGC_APP_IDENTIFIER) {
|
||||
throw new Error('AI game creator shell identifier drifted');
|
||||
}
|
||||
|
||||
if (tauriConfig.mainBinaryName !== AGC_MAIN_BINARY_NAME) {
|
||||
throw new Error('AI game creator shell mainBinaryName drifted');
|
||||
}
|
||||
|
||||
if (
|
||||
tauriConfig.productName !== defaultChannelIdentity.productName ||
|
||||
tauriConfig.identifier !== defaultChannelIdentity.identifier
|
||||
tauriConfig.identifier !== defaultChannelIdentity.identifier ||
|
||||
tauriConfig.mainBinaryName !== resolveChannelMainBinaryName('dev')
|
||||
) {
|
||||
throw new Error(
|
||||
'AI game creator shell baseline config must match the default channel identity',
|
||||
@@ -1314,6 +1321,8 @@ for (const channel of ['release', 'beta-2']) {
|
||||
if (
|
||||
identity.productName === defaultChannelIdentity.productName ||
|
||||
identity.identifier === defaultChannelIdentity.identifier ||
|
||||
resolveChannelMainBinaryName(channel) ===
|
||||
resolveChannelMainBinaryName('dev') ||
|
||||
!identity.identifier.startsWith(`${AGC_APP_IDENTIFIER}.`)
|
||||
) {
|
||||
throw new Error(`channel install identity not isolated: ${channel}`);
|
||||
@@ -1631,13 +1640,11 @@ if (!viteConfigSource.includes('allow: [repoRoot]')) {
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!(
|
||||
tauriConfig.build?.beforeDevCommand?.includes(
|
||||
'run ai-game-creator-shell:dev-server',
|
||||
) || tauriConfig.build?.beforeDevCommand?.includes('run agc:serve')
|
||||
)
|
||||
) {
|
||||
if (!(
|
||||
tauriConfig.build?.beforeDevCommand?.includes(
|
||||
'run ai-game-creator-shell:dev-server',
|
||||
) || tauriConfig.build?.beforeDevCommand?.includes('run agc:serve')
|
||||
)) {
|
||||
throw new Error(
|
||||
'AI game creator shell beforeDevCommand must start the selected Vite dev server',
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "陶泥儿开发版",
|
||||
"mainBinaryName": "genarrative-ai-game-creator-shell",
|
||||
"version": "0.1.67",
|
||||
"identifier": "world.genarrative.ai-game-creator",
|
||||
"build": {
|
||||
|
||||
@@ -30,16 +30,19 @@
|
||||
; 旧展示名表:以后再改展示名,必须在这里追加旧名,否则升级后旧快捷方式会继续指向旧安装。
|
||||
!define AGC_LEGACY_IDENTITY_1 "陶泥儿"
|
||||
!define AGC_LEGACY_IDENTITY_2 "Genarrative AI Game Creator"
|
||||
!define AGC_RELEASE_MAIN_BINARY "genarrative-ai-game-creator-shell-release"
|
||||
|
||||
Var AgcLegacyIdentity
|
||||
Var AgcLegacyInstallDir
|
||||
Var AgcMigratedLegacyDesktopShortcut
|
||||
Var AgcLegacyOwnedByRelease
|
||||
|
||||
; 清理一个旧身份。调用方先把旧展示名写进 $AgcLegacyIdentity,再 !insertmacro 本宏;宏不返回错误。
|
||||
; 只在「旧展示名目录下确实存在我们的主程序」时才动手,避免误删同名目录;
|
||||
; 当前安装目录 $INSTDIR 永不进入删除路径。
|
||||
!macro AgcMigrateLegacyIdentity
|
||||
StrCpy $AgcLegacyInstallDir ""
|
||||
StrCpy $AgcLegacyOwnedByRelease 0
|
||||
|
||||
${If} ${FileExists} "$LOCALAPPDATA\$AgcLegacyIdentity\${MAINBINARYNAME}.exe"
|
||||
StrCpy $AgcLegacyInstallDir "$LOCALAPPDATA\$AgcLegacyIdentity"
|
||||
@@ -49,6 +52,15 @@ Var AgcMigratedLegacyDesktopShortcut
|
||||
|
||||
${If} $AgcLegacyInstallDir != ""
|
||||
${AndIf} $AgcLegacyInstallDir != $INSTDIR
|
||||
; release 新包使用独立主程序名;同一旧展示名目录若已有该文件,不能当作旧 dev 清理。
|
||||
${If} ${FileExists} "$AgcLegacyInstallDir\${AGC_RELEASE_MAIN_BINARY}.exe"
|
||||
StrCpy $AgcLegacyOwnedByRelease 1
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
|
||||
${If} $AgcLegacyInstallDir != ""
|
||||
${AndIf} $AgcLegacyInstallDir != $INSTDIR
|
||||
${AndIf} $AgcLegacyOwnedByRelease = 0
|
||||
; 桌面图标:只有目标正好指向旧目录的主程序时才删,避免误删同名的用户快捷方式。
|
||||
!insertmacro IsShortcutTarget "$DESKTOP\$AgcLegacyIdentity.lnk" "$AgcLegacyInstallDir\${MAINBINARYNAME}.exe"
|
||||
Pop $0
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
; AGC release 渠道的 Windows 安装迁移钩子。
|
||||
;
|
||||
; release 曾使用「陶泥儿 Release」作为 productName,后来统一为「陶泥儿」。
|
||||
; Tauri 的 NSIS 更新模式不会为改名后的身份重建快捷方式,也不会自动读取旧身份的安装目录,
|
||||
; 所以这里把旧安装目录、旧快捷方式迁移到当前 release 身份。
|
||||
;
|
||||
; release 渠道同时使用独立的 MAINBINARYNAME(genarrative-ai-game-creator-shell-release),
|
||||
; NSIS 默认的按文件名查杀逻辑因此不会碰 dev 渠道的主进程。
|
||||
;
|
||||
!define AGC_RELEASE_LEGACY_IDENTITY "陶泥儿 Release"
|
||||
!define AGC_RELEASE_LEGACY_BINARY "genarrative-ai-game-creator-shell"
|
||||
|
||||
Var AgcReleaseLegacyInstallDir
|
||||
Var AgcReleaseMigratedDesktopShortcut
|
||||
|
||||
; Tauri 模板会在 Section Install 开头先执行 SetOutPath,再展开 PREINSTALL 宏。
|
||||
; 此处仍可在复制文件前改写 $INSTDIR,让旧 release 安装原地升级。
|
||||
!macro NSIS_HOOK_PREINSTALL
|
||||
StrCpy $AgcReleaseLegacyInstallDir ""
|
||||
ReadRegStr $R0 SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\${AGC_RELEASE_LEGACY_IDENTITY}" "InstallLocation"
|
||||
; Tauri 将 InstallLocation 写成带引号的 REG_SZ;去掉包裹引号后再交给 FileExists。
|
||||
StrCpy $R1 $R0 1
|
||||
${If} $R1 == '$\"'
|
||||
StrCpy $R0 $R0 "" 1
|
||||
StrLen $R1 $R0
|
||||
IntOp $R1 $R1 - 1
|
||||
StrCpy $R0 $R0 $R1
|
||||
${EndIf}
|
||||
${If} $R0 != ""
|
||||
${AndIf} ${FileExists} "$R0\${AGC_RELEASE_LEGACY_BINARY}.exe"
|
||||
StrCpy $AgcReleaseLegacyInstallDir $R0
|
||||
StrCpy $INSTDIR $R0
|
||||
${EndIf}
|
||||
!macroend
|
||||
|
||||
!macro NSIS_HOOK_POSTINSTALL
|
||||
; 更新模式下模板不会重建快捷方式。先把旧名称和旧主程序名指向当前 release。
|
||||
!insertmacro IsShortcutTarget "$DESKTOP\${AGC_RELEASE_LEGACY_IDENTITY}.lnk" "$INSTDIR\${AGC_RELEASE_LEGACY_BINARY}.exe"
|
||||
Pop $0
|
||||
${If} $0 = 1
|
||||
StrCpy $AgcReleaseMigratedDesktopShortcut 1
|
||||
!insertmacro UnpinShortcut "$DESKTOP\${AGC_RELEASE_LEGACY_IDENTITY}.lnk"
|
||||
Delete "$DESKTOP\${AGC_RELEASE_LEGACY_IDENTITY}.lnk"
|
||||
${EndIf}
|
||||
|
||||
!insertmacro IsShortcutTarget "$SMPROGRAMS\${AGC_RELEASE_LEGACY_IDENTITY}.lnk" "$INSTDIR\${AGC_RELEASE_LEGACY_BINARY}.exe"
|
||||
Pop $0
|
||||
${If} $0 = 1
|
||||
!insertmacro UnpinShortcut "$SMPROGRAMS\${AGC_RELEASE_LEGACY_IDENTITY}.lnk"
|
||||
Delete "$SMPROGRAMS\${AGC_RELEASE_LEGACY_IDENTITY}.lnk"
|
||||
${EndIf}
|
||||
|
||||
; 用户可能早已把快捷方式改成当前名称,但仍指向旧主程序名。
|
||||
!insertmacro IsShortcutTarget "$DESKTOP\${PRODUCTNAME}.lnk" "$INSTDIR\${AGC_RELEASE_LEGACY_BINARY}.exe"
|
||||
Pop $0
|
||||
${If} $0 = 1
|
||||
!insertmacro SetShortcutTarget "$DESKTOP\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe"
|
||||
${EndIf}
|
||||
!insertmacro IsShortcutTarget "$SMPROGRAMS\${PRODUCTNAME}.lnk" "$INSTDIR\${AGC_RELEASE_LEGACY_BINARY}.exe"
|
||||
Pop $0
|
||||
${If} $0 = 1
|
||||
!insertmacro SetShortcutTarget "$SMPROGRAMS\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe"
|
||||
${EndIf}
|
||||
|
||||
; 开始菜单图标始终存在;桌面图标只在迁移前存在时补建,保持用户选择。
|
||||
StrCpy $R0 "$INSTDIR\${MAINBINARYNAME}.exe"
|
||||
${IfNot} ${FileExists} "$SMPROGRAMS\${PRODUCTNAME}.lnk"
|
||||
CreateShortCut "$SMPROGRAMS\${PRODUCTNAME}.lnk" "$R0"
|
||||
!insertmacro SetLnkAppUserModelId "$SMPROGRAMS\${PRODUCTNAME}.lnk"
|
||||
${EndIf}
|
||||
${If} $AgcReleaseMigratedDesktopShortcut = 1
|
||||
CreateShortCut "$DESKTOP\${PRODUCTNAME}.lnk" "$R0"
|
||||
!insertmacro SetLnkAppUserModelId "$DESKTOP\${PRODUCTNAME}.lnk"
|
||||
${EndIf}
|
||||
|
||||
; 新 release 已接管旧安装目录后,删除旧主程序,避免 dev 的迁移钩子把该目录误判为旧 dev。
|
||||
${If} $AgcReleaseLegacyInstallDir != ""
|
||||
Delete "$INSTDIR\${AGC_RELEASE_LEGACY_BINARY}.exe"
|
||||
DeleteRegKey SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\${AGC_RELEASE_LEGACY_IDENTITY}"
|
||||
DeleteRegKey HKCU "Software\genarrative\${AGC_RELEASE_LEGACY_IDENTITY}"
|
||||
${EndIf}
|
||||
!macroend
|
||||
@@ -8,6 +8,14 @@
|
||||
- 影响范围:`apps/ai-game-creator-shell/src/features/project-workspace/activeChatComposer.ts`(新增)、`src/App.tsx`、`src/view/project-development/chat/DirectProjectChatView.tsx`、`.../chat/components/DirectProjectComposer/DirectProjectComposer.tsx`、`.../planning/PlanningChatView.tsx`、`tests/activeChatComposer.test.ts`(新增,钉注册表合同)、`tests/resourceCanvasChatReferenceDrop.test.tsx`、`tests/appSurface/{project-development,design-agent}.suite.ts`、`docs/【功能说明】AGC聊天素材引用-2026-09-08.md`、`pitfalls.md`、本文件。
|
||||
- 验证(合并 master 后的最终一轮):`npx vitest run apps/ai-game-creator-shell/tests`(197 passed / 1 skipped 文件,1918 passed / 17 skipped 用例)、`npx vitest run tests/activeChatComposer.test.ts tests/resourceCanvasChatReferenceDrop.test.tsx`(2 files / 12 passed,含注册表合同:空批次、无输入区、句柄报落空、注销身份校验、重复注册告警、乱序注销)、`npx vitest run tests/appSurface.test.ts -t 引用`(4 passed)、`npm run agc:typecheck`(含 `check:tests:types`,exit 0)、`npm run check:encoding`、`git diff --check`、eslint `--max-warnings 0`(改动文件)。反向证伪:去掉注册调用后端到端用例变红;去掉空批次短路 / 重复注册告警后对应新用例各红一处。
|
||||
|
||||
## 2026-10-04 AGC 渠道更新进程与快捷方式隔离
|
||||
|
||||
- 背景:Tauri 2.11 的 Windows NSIS 模板通过 `MAINBINARYNAME` 查找并结束进程。dev 与 release 过去共用 `genarrative-ai-game-creator-shell.exe`,更新任一渠道都会结束另一渠道;release 从「陶泥儿 Release」改为「陶泥儿」后,`/UPDATE` 又不会自动重建旧快捷方式。
|
||||
- 决策:dev 保留历史主程序文件名以维持升级链;release 使用 `genarrative-ai-game-creator-shell-release.exe`,其它非默认渠道使用带渠道后缀的主程序名。构建期把 `mainBinaryName` 与渠道端点、productName、identifier 同批注入,NSIS 因文件名隔离而只匹配自身渠道进程。
|
||||
- 迁移:release Windows 包通过独立安装钩子读取旧 `陶泥儿 Release` 卸载项的安装目录,在原目录安装新包,迁移旧桌面/开始菜单快捷方式并清理旧主程序与孤儿卸载项;dev 继续使用原有旧展示名迁移钩子。
|
||||
- 影响范围:AGC 渠道身份脚本、发布构建配置、Tauri 基线配置、Windows NSIS 钩子与渠道发布测试;不改变 OSS 分区、更新端点或客户端数据目录合同。
|
||||
- 验证方式:渠道发布脚本定向测试、`check-config.mjs`、真实 NSIS 编译与双渠道安装/更新 smoke;真机安装仍需发布环境执行。
|
||||
|
||||
## 2026-10-03 AGC 栏目画布上传素材按入口栏目登记(Issue 359)
|
||||
|
||||
- 背景:AGC 客户端在资源栏目子画布(「UI 交互 / 角色与对象 / 场景与环境 / 音频」)左下角工具栏点「上传」后,提示条给出「已上传 1 个素材」,但当前栏目计数不变(仍「0 项」)、素材出现在「待归类」,用户看到的是"上传成功了但它从这一页消失了"。原因是上传登记的 manifest `kind` 只由**内容证据**推导(`assets.rs::uploaded_asset_kind`:图片 / 视频 / 代码 → `unclassified`,音频 → `audio`,文档 / 字体 → `document`),kind 派生分类与栏目词汇(`ui-interaction` / `character` / `scene` / `audio`)不是同一套,而 `upload_local_asset` 原先不接受入口栏目。
|
||||
|
||||
@@ -10,6 +10,15 @@
|
||||
- **判据/取证**:`npm run test -- apps/ai-game-creator-shell/tests` 里的 `resourceCanvasChatReferenceDrop.test.tsx`、`appSurface/project-development.suite.ts`(工具条「引用」)、`appSurface/design-agent.suite.ts`(策划链路)都断言**真实输入盒草稿**里出现 `[data-resource-reference-id="<assetId>"]`,不再是「事件被派发」;临时去掉注册调用后这三条会红,证明用例钉的是真链路。详见 [`【功能说明】AGC聊天素材引用-2026-09-08`](../../【功能说明】AGC聊天素材引用-2026-09-08.md) 文首一节。
|
||||
- **边界**:只要还保留「window 事件 + 模块外 ref 约定」这种形态,新增聊天面就必须一起进注册表;更彻底的形态是画布与聊天的共同宿主(`ProjectDevelopmentView`)用 context 下发插入能力,注册表语义与它一致,将来换实现不必动画布。
|
||||
|
||||
## 2026-10-04 AGC 渠道更新按固定主程序名互相查杀
|
||||
|
||||
- **现象**:开发版和 release 同时运行时,更新其中一个渠道会把另一个进程一起结束;release 从旧产品名升级后,旧 `陶泥儿 Release.lnk` 仍指向旧安装目录,更新后的 release 没有可用快捷方式。
|
||||
- **原因**:Tauri 2.11 的 NSIS `CheckIfAppIsRunning` / `KillProcess` 只按 `MAINBINARYNAME` 匹配。所有渠道都使用 `genarrative-ai-game-creator-shell.exe`,所以插件无法按安装目录区分进程;更新模式还会跳过快捷方式创建,产品名变化后旧图标不会自动迁移。
|
||||
- **处理**:`channel-identity.mjs` 新增 `resolveChannelMainBinaryName`:dev 保留旧文件名,release 与其它渠道使用后缀文件名;`createChannelConfig` 同批注入 Tauri `mainBinaryName`。release Windows 包使用独立 `release-installer-hooks.nsh`,从旧 `陶泥儿 Release` 卸载项恢复安装目录,迁移旧快捷方式并删除旧主程序/卸载项;dev 的历史改名钩子保持不变。
|
||||
- **不要踩的坑**:只改 `productName` 或 `identifier` 不能阻止 NSIS 互相查杀;只改安装包文件名也不能让 updater 选中正确的进程,必须把 `mainBinaryName` 写入构建期 Tauri 配置,并保证 dev 的历史文件名不变。NSIS 钩子仍只能在 `!macro` 内引用模板常量/插件,且文件必须 UTF-8 with BOM。
|
||||
- **判据/验证**:`node --test apps/ai-game-creator-shell/scripts/build-release.test.mjs` 覆盖渠道主程序名、release 钩子与宏位置;`node apps/ai-game-creator-shell/scripts/check-config.mjs` 校验基线;真实 Windows NSIS 编译与 dev/release 同机更新 smoke 仍需发布环境执行。
|
||||
- **关联**:`apps/ai-game-creator-shell/scripts/channel-identity.mjs`、`apps/ai-game-creator-shell/scripts/build-release.mjs`、`apps/ai-game-creator-shell/src-tauri/windows/{installer-hooks.nsh,release-installer-hooks.nsh}`、`docs/technical/【技术方案】AGC客户端更新检查与下载-2026-08-31.md`。
|
||||
|
||||
## 2026-10-03 AGC 栏目画布上传素材落「待归类」:kind 派生分类不等于入口栏目
|
||||
|
||||
- **现象**(Issue 359):在 AGC 资源栏目子画布(如「UI 交互」「角色与对象」)左下角工具栏点「上传」选图片 / 视频 / 代码类文件,提示条给出「已上传 1 个素材」,但当前栏目计数纹丝不动(仍「0 项」),素材出现在「待归类」。用户看到的是"上传成功了,可它就消失在这个页面里"。
|
||||
@@ -6323,6 +6332,7 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
|
||||
- **验证**:`cargo test --locked -p api-server --bin api-server app::tests::http_tracing`(默认并发与 `--test-threads=1` 各连跑 20 次)、`cargo test -p platform-llm observability_tests`;更接近 CI 并发的是整段 `app::tests::`(91 用例同进程)与 `--skip bgfilter_worker --skip wallet_refund_outbox` 的全量 bin(1133 用例)连跑。
|
||||
- **关联**:`server-rs/crates/api-server/src/app.rs`、`server-rs/crates/platform-llm/src/observability_tests.rs`。
|
||||
|
||||
|
||||
## 2026-10-04 AGC 通知计数与 graceful terminate 的断言偶发都来自"跨线程 / 跨用例串台"
|
||||
|
||||
- **现象**:`agent::thread_manager::tests::active_turn_changes_publish_one_notification_per_real_change` 偶发 `left: 8 / right: 7`(进度内容变化必须通知一次);`process_session::tests::process_session_graceful_terminate_keeps_wrapper_alive_for_target_cleanup` 偶发 `left: "exited" / right: "terminated"`;两者都在 `AI game creator shell Rust lane 2/2` 分片里红。
|
||||
|
||||
@@ -58,7 +58,7 @@ SpacetimeDB crate、SDK、CLI / standalone 与生成 bindings 按 `2.8.3` 对齐
|
||||
- DirectProject 对话先在完整历史中按回合/原始 item 身份关联,再分页渲染;每个回合只有一个呈现入口。有流按 item `seq` 交替文本和工具,无流采用历史正文;禁止位置猜配或同时展示累计回复与 item 正文。流写入单调归并,收尾等待落盘任务,不按磁盘“最后一段”猜最终回复位置。详见 AGC 实施计划的“DirectProject 回合展示唯一归属”。
|
||||
- 回合生命周期只由活动 client 回合快照和 Direct 事件恢复;Provider 的历史终态通知不能创建活动 client 回合。消息发送时间保存在历史信封,原始 item 不混入宿主字段;完成后的中间文本和工具默认收进“执行过程”,最终回复及失败提示保持可见。
|
||||
|
||||
- AGC 安装产品名由渠道身份决定:`dev` 显示“陶泥儿开发版”,`release` 复用正式产品名“陶泥儿”,自定义渠道显示“陶泥儿 <渠道显示名>”;Tauri `productName` 控制安装项、快捷方式与 EXE 产品描述,identifier 继续按 `<基线>.<渠道>` 派生,保证渠道数据与运行身份隔离(详见《AGC客户端更新检查与下载》的渠道与安装身份合同)。Windows 内置 Codex 安装到顶层 `coding-agent/win-x64/`,打包资源映射与运行时查找路径必须一致。内部可执行文件名保持稳定。
|
||||
- AGC 安装产品名由渠道身份决定:`dev` 显示“陶泥儿开发版”,`release` 复用正式产品名“陶泥儿”,自定义渠道显示“陶泥儿 <渠道显示名>”;Tauri `productName` 控制安装项、快捷方式与 EXE 产品描述,identifier 继续按 `<基线>.<渠道>` 派生,主程序文件名也按渠道隔离(dev 保留历史名,release 使用 `-release` 后缀),保证渠道数据、运行身份与更新进程隔离(详见《AGC客户端更新检查与下载》的渠道与安装身份合同)。Windows 内置 Codex 安装到顶层 `coding-agent/win-x64/`,打包资源映射与运行时查找路径必须一致。
|
||||
|
||||
- 新 Web 游戏为 `game/` 下的 npm + Vite + Phaser 4.2.1 工程,使用包导入且允许其它依赖;npm 预览与导出只读取 dist,运行素材需纳入构建。单 HTML → Phaser 迁移固定走 DirectProject:文件落盘后先用受控 `project.bootstrap` 在 `game` 执行无参数 `npm install`,再用支持相对 cwd 的 `project.verify` 构建并确认 `game/dist/index.html`,已有单 HTML/Godot 不通过 JSON Generator 伪装成 npm 工程。
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
### 渠道与安装身份合同
|
||||
|
||||
- 渠道同时决定**更新端点**与**安装身份**,两者都由构建期写入产物。默认渠道 `dev` 保持基线身份 `productName = 陶泥儿开发版`、`identifier = world.genarrative.ai-game-creator`;`release` 渠道使用正式产品名 `陶泥儿` 并派生 `identifier = world.genarrative.ai-game-creator.release`;其它自定义渠道派生 `productName = 陶泥儿 <渠道显示名>`(如 `beta-2` → `陶泥儿 Beta-2`)与 `identifier = world.genarrative.ai-game-creator.<渠道>`。渠道显示名按连字符分段首字母大写,不改动渠道本身。
|
||||
- Windows 主程序文件名也是渠道安装身份的一部分:`dev` 保留 `genarrative-ai-game-creator-shell.exe` 以延续既有升级链,`release` 使用 `genarrative-ai-game-creator-shell-release.exe`,其它渠道使用 `genarrative-ai-game-creator-shell-<channel>.exe`。NSIS 更新/卸载按该文件名查找进程,保证更新一个渠道不会结束另一个渠道。
|
||||
- 默认渠道身份**不可变更**:既有安装目录、卸载项、快捷方式与已发布客户端的升级链都建立在基线身份上。渠道身份由 `apps/ai-game-creator-shell/scripts/channel-identity.mjs` 单点定义,构建入口、macOS 发布入口与配置门禁共同消费;基线 `tauri.conf.json` 必须逐字等于默认渠道身份。
|
||||
- 安装身份决定的持久与可见事实:Windows 安装目录 `%LOCALAPPDATA%\<产品名>`、卸载项与 `HKCU\Software\genarrative\<产品名>`、WebView2 数据目录 `%LOCALAPPDATA%\<identifier>`、客户端数据目录 `%APPDATA%\<identifier>`;macOS `.app` 名、bundle id、DMG 卷名与菜单栏应用名。
|
||||
- 同机并存:不同渠道的包体可以在同一台设备上同时安装并同时运行,互不覆盖、互不顶掉;同一渠道的新版本仍是原地升级,因为更新端点与安装身份同属一个渠道。
|
||||
@@ -124,19 +125,20 @@
|
||||
|
||||
- 渠道安装身份映射(`<channel>` 为 `dev`、`release` 或自定义名称;`<Channel>` 为渠道显示名):
|
||||
|
||||
| 渠道 | productName | identifier | Windows 安装目录 | 客户端数据目录 |
|
||||
| -------------------- | ------------------ | ---------------------------------------------- | ----------------------------------- | ----------------------------------------------- |
|
||||
| `dev`(默认) | `陶泥儿` | `world.genarrative.ai-game-creator` | `%LOCALAPPDATA%\陶泥儿` | `%APPDATA%\world.genarrative.ai-game-creator` |
|
||||
| `release` / 自定义 | `陶泥儿 <Channel>` | `world.genarrative.ai-game-creator.<channel>` | `%LOCALAPPDATA%\陶泥儿 <Channel>` | `%APPDATA%\world.genarrative.ai-game-creator.<channel>` |
|
||||
| 渠道 | productName | 主程序文件名 | identifier | Windows 安装目录 | 客户端数据目录 |
|
||||
| -------------------- | ------------------ | ------------------------------------------------- | ---------------------------------------------- | ----------------------------------- | ----------------------------------------------- |
|
||||
| `dev`(默认) | `陶泥儿开发版` | `genarrative-ai-game-creator-shell.exe` | `world.genarrative.ai-game-creator` | `%LOCALAPPDATA%\陶泥儿开发版` | `%APPDATA%\world.genarrative.ai-game-creator` |
|
||||
| `release` | `陶泥儿` | `genarrative-ai-game-creator-shell-release.exe` | `world.genarrative.ai-game-creator.release` | `%LOCALAPPDATA%\陶泥儿` | `%APPDATA%\world.genarrative.ai-game-creator.release` |
|
||||
| 自定义渠道 | `陶泥儿 <Channel>` | `genarrative-ai-game-creator-shell-<channel>.exe` | `world.genarrative.ai-game-creator.<channel>` | `%LOCALAPPDATA%\陶泥儿 <Channel>` | `%APPDATA%\world.genarrative.ai-game-creator.<channel>` |
|
||||
|
||||
- 安装身份迁移:`dev` 客户端保持原身份,升级链路连续;`release` 与自定义渠道首次以新身份安装,**不接管也不迁移**任何既有 `dev` 安装、本地项目或登录态,设备上因此可以同时存在两个渠道的客户端,由用户自行决定是否卸载其一。
|
||||
- 安装身份迁移:`dev` 客户端保持原 identifier 与主程序文件名,升级链路连续;dev Windows 包继续清理历史展示名 `陶泥儿` / `Genarrative AI Game Creator` 的旧快捷方式。release 从旧展示名 `陶泥儿 Release` 迁移安装目录与快捷方式到当前 `陶泥儿` 身份;release 与自定义渠道不接管也不迁移其它渠道的数据或登录态。
|
||||
|
||||
## 构建与发布
|
||||
|
||||
- 发布入口:`npm run ai-game-creator-shell:release:upload`(构建 + 按渠道上传);仅构建不发布的 smoke 使用 `--no-bundle` 分支,不读远端版本、不改版本、不生成清单。
|
||||
- 发布入口只解析一次目标,优先级为 CLI `--target value` / `--target=value` / `-t value`、`AGC_BUILD_TARGET`、Windows 默认值;重复/空目标与不支持目标失败关闭。版本高水位、构建 feature/渠道端点、bundle 路径、产物后缀、清单平台键及摘要必须消费同一个发布上下文,不能分别回读默认目标。
|
||||
- 渠道由 `AGC_UPDATE_CHANNEL` 显式指定,默认 dev;Windows 与 macOS 目标均支持 dev、release 和自定义渠道,目标校验独立进行。
|
||||
- 渠道 `--config` 在 Tauri 构建前最后合并,同时注入 `productName`、`identifier`、updater 端点与窗口标题:安装身份与更新端点必须来自同一个渠道,不能各自回读默认值。macOS 发布入口构建 `*.app`、updater 归档与 DMG 前先按发布渠道解析产品名,产物名一律派生而不写死。
|
||||
- 渠道 `--config` 在 Tauri 构建前最后合并,同时注入 `productName`、`identifier`、`mainBinaryName`、updater 端点与窗口标题:安装身份与更新端点必须来自同一个渠道,不能各自回读默认值。macOS 发布入口构建 `*.app`、updater 归档与 DMG 前先按发布渠道解析产品名,产物名一律派生而不写死。
|
||||
- 渠道配置走 Tauri 的 JSON Merge Patch 语义:对象递归合并,**数组整体替换**。因此 `app.windows` 必须按基线 `tauri.conf.json` 的完整 client 窗口对象下发、只覆盖 `title`(脚本从基线读取后展开);任何"只写 `{ title }`"的写法都会让 `label` / `decorations` / 尺寸回落成 Tauri 默认值(`label=main`、`decorations=true`、800x600),表现为打包产物重新出现系统标题栏,并按 label 连带失效承载平台 HTTP 权限等 capability。守卫用例:`build-release.test.mjs` 的渠道配置合并用例与 `check-config.mjs` 的 `decorations` 门禁。
|
||||
- 定时调度分别判断服务端与客户端 scope:dev 小时调度在提交含 AGC 相关路径时发布对应渠道,纯文档或流水线自身的提交仍只跑 Full Build;release 每日调度在服务端相关路径变化时发布正式 Full Build,在 AGC 相关路径变化时发布 release 客户端,并在同一调度内等待、汇总各 lane 结果,失败 lane 下一轮补发。判定失败或勾选强制触发时按"需要发布"处理。
|
||||
- 更新摘要不再自动生成:发布脚本不读取提交记录生成 `notes`;只有 `AGC_UPDATE_RELEASE_NOTES` 非空时,才把显式手动文案写入渠道清单和旧协议清单的 `releaseNotes`。未设置时清单不携带更新说明,归档文件 `release-notes.txt` 记录“本次没有可用的更新摘要”。
|
||||
@@ -184,10 +186,11 @@
|
||||
|
||||
| 条款 | 验收方式 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| 渠道身份派生与默认渠道不变 | `node --test apps/ai-game-creator-shell/scripts/build-release.test.mjs` | 通过;`dev` 逐字等于基线 `陶泥儿开发版` / `world.genarrative.ai-game-creator`,`release` 使用 `陶泥儿` 并保持独立 identifier,`beta-2` 派生带后缀的产品名与独立 identifier,非法渠道失败关闭 |
|
||||
| 渠道身份派生与默认渠道不变 | `node --test apps/ai-game-creator-shell/scripts/build-release.test.mjs` | 通过;`dev` 逐字等于基线 `陶泥儿开发版` / `world.genarrative.ai-game-creator` / `genarrative-ai-game-creator-shell`,`release` 使用 `陶泥儿` 并保持独立 identifier 与 `-release` 主程序名,`beta-2` 派生带后缀的产品名、identifier 与主程序名,非法渠道失败关闭 |
|
||||
| 身份与端点同批注入 | 同上的渠道 `--config` 用例 | 通过;`productName` / `identifier` 与 `/<channel>-win|mac/latest.json` 来自同一次解析 |
|
||||
| 渠道产物首装包选择 | 同上的渠道 DMG 夹具用例 | 通过;`陶泥儿_<版本>_aarch64.dmg` 仍按 `<版本>_<架构>.dmg` 唯一匹配 |
|
||||
| 基线配置等于默认渠道身份 | `node apps/ai-game-creator-shell/scripts/check-config.mjs` | 通过;基线漂移与非默认渠道身份不隔离都会失败关闭 |
|
||||
| Windows 更新进程与快捷方式隔离 | `build-release.test.mjs` 渠道配置/NSIS 钩子用例 | 通过;dev/release 主程序文件名不同,release 迁移旧 `陶泥儿 Release` 安装目录与快捷方式 |
|
||||
| 全量发布脚本回归 | `node --test build-release.test.mjs release-oss.test.mjs prepare-macos-codex.test.mjs cargo-features.test.mjs` | 通过(64/64,含 macOS 入口按渠道解析产品名的守卫) |
|
||||
| AGC 自有 AppData 提权 ACL 范围 | `cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --bin genarrative-ai-game-creator-shell -- config::private_path_elevation_policy_tests` | 通过(12/12;含基线、`<基线>.release`、`<基线>.beta-2` 与相似前缀 `-backup` 的反向断言) |
|
||||
| 渠道身份进入真实构建产物 | `AGC_UPDATE_CHANNEL=release npm --prefix apps/ai-game-creator-shell run build -- --no-bundle --debug` | 通过;Tauri 接受 `productName=陶泥儿` / `identifier=world.genarrative.ai-game-creator.release` 并完成构建;产物字符串实测 `陶泥儿` × 1、`agc/release-win/latest.json` × 1、`world.genarrative.ai-game-creator.release` × 1、`agc/dev-win/latest.json` × 0 |
|
||||
|
||||
Reference in New Issue
Block a user