diff --git a/apps/ai-game-creator-shell/scripts/agent-swarm-test-chat.mjs b/apps/ai-game-creator-shell/scripts/agent-swarm-test-chat.mjs index b1f317674..45656fc67 100644 --- a/apps/ai-game-creator-shell/scripts/agent-swarm-test-chat.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-swarm-test-chat.mjs @@ -20,7 +20,10 @@ import { createInterface } from 'node:readline/promises'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { inflateSync } from 'node:zlib'; -export const appIdentifier = 'world.genarrative.ai-game-creator'; +import { AGC_APP_IDENTIFIER } from './channel-identity.mjs'; + +// 联调工具驱动的始终是默认渠道客户端:安装身份取渠道基线,不跟随发布渠道。 +export const appIdentifier = AGC_APP_IDENTIFIER; export const configFileName = 'game-creator.config.json'; export const localConfigFileName = 'game-creator.config.local.json'; export const runnerEndpointFileName = 'agent-runner.endpoint.json'; diff --git a/apps/ai-game-creator-shell/scripts/build-macos-ci.mjs b/apps/ai-game-creator-shell/scripts/build-macos-ci.mjs index 49bf93abf..67c4ad0b8 100644 --- a/apps/ai-game-creator-shell/scripts/build-macos-ci.mjs +++ b/apps/ai-game-creator-shell/scripts/build-macos-ci.mjs @@ -14,6 +14,7 @@ import { resolveReleasePartition, runTauriBuild, } from './build-release.mjs'; +import { resolveChannelInstallIdentity } from './channel-identity.mjs'; import { readReleaseDryRun, uploadReleaseArtifacts } from './release-oss.mjs'; import { readUpdaterPubkey, @@ -39,27 +40,19 @@ const appRoot = fileURLToPath(new URL('..', import.meta.url)); const repoRoot = path.resolve(appRoot, '../..'); /** - * 产品名只从 Tauri 配置读取:它同时决定 `*.app` 目录名、updater 归档名与 DMG 卷名。 - * 写死会在改名后让入口静默找错对象(清理、打包、归档三处一起失效)。 + * 产品名只从渠道安装身份派生(渠道身份由构建期 `--config` 注入 Tauri 配置): + * 它同时决定 `*.app` 目录名、updater 归档名与 DMG 卷名。写死会在改名或换渠道后 + * 让入口静默找错对象(清理、打包、归档三处一起失效)。 */ -function readProductName() { - const read = (file) => - JSON.parse(fs.readFileSync(path.join(appRoot, 'src-tauri', file), 'utf8')); - const base = read('tauri.conf.json'); - const macosPath = path.join(appRoot, 'src-tauri', 'tauri.macos.conf.json'); - const productName = fs.existsSync(macosPath) - ? (read('tauri.macos.conf.json').productName ?? base.productName) - : base.productName; +function resolveProductName(channel) { + const { productName } = resolveChannelInstallIdentity(channel); assert.ok( typeof productName === 'string' && productName.trim().length > 0, - 'Tauri 配置缺少 productName', + '渠道安装身份缺少 productName', ); return productName; } -const productName = readProductName(); -const appBundleName = `${productName}.app`; -const updaterArtifactName = `${productName}.app.tar.gz`; assert.equal(process.platform, 'darwin', '只能在 macOS Agent 执行'); assert.equal( process.env.JENKINS_URL?.length > 0, @@ -102,6 +95,9 @@ process.env.CARGO_TARGET_DIR = path.join(appRoot, 'src-tauri/target'); const macTarget = 'aarch64-apple-darwin'; const context = resolveReleaseContext([`--target=${macTarget}`]); const partition = resolveReleasePartition(context.channel, context.target); +const productName = resolveProductName(context.channel); +const appBundleName = `${productName}.app`; +const updaterArtifactName = `${productName}.app.tar.gz`; const version = await prepareReleaseVersion(context); // 首装包名必须让清单侧的单架构分支唯一匹配:`<产品名>_<版本>_<架构>.dmg`, // 架构段用 Tauri 的 aarch64 口径(不是 updater 平台键的 arm64 / x86_64)。 diff --git a/apps/ai-game-creator-shell/scripts/build-release.mjs b/apps/ai-game-creator-shell/scripts/build-release.mjs index 70cfbdd87..bd1d5907b 100644 --- a/apps/ai-game-creator-shell/scripts/build-release.mjs +++ b/apps/ai-game-creator-shell/scripts/build-release.mjs @@ -13,6 +13,10 @@ import { defaultEditorFeatures, withDefaultCargoFeatures, } from './cargo-features.mjs'; +import { + resolveChannelInstallIdentity, + resolveReleaseChannel, +} from './channel-identity.mjs'; import { prepareNsisToolsetForRelease } from './nsis-toolset.mjs'; import { stageNodeRuntime } from './stage-node-runtime.mjs'; @@ -89,14 +93,7 @@ const cargoLockPath = path.join(appRoot, 'src-tauri', 'Cargo.lock'); const defaultOssBaseUrl = 'https://agc-dev.oss-rg-china-mainland.aliyuncs.com/agc'; -const reservedChannelNames = new Set([ - 'win', - 'mac', - 'windows', - 'macos', - 'darwin', - 'linux', -]); +export { resolveReleaseChannel } from './channel-identity.mjs'; /** * 影响 Windows 客户端产物的路径。调度管线的发布范围判定与这里的提交摘要必须 @@ -165,21 +162,6 @@ export function resolveReleasePlatform(target = defaultTarget()) { throw new Error(`不支持的发布目标:${target}`); } -export function resolveReleaseChannel(env = process.env) { - const channel = env.AGC_UPDATE_CHANNEL?.trim() ?? 'dev'; - if ( - !/^[a-z][a-z0-9-]{0,31}$/u.test(channel) || - channel.endsWith('-') || - reservedChannelNames.has(channel) || - /-(win|mac)$/u.test(channel) - ) { - throw new Error( - '发布渠道无效:请使用 dev、release 或最多 32 位的小写字母、数字和连字符名称,系统名称不属于渠道', - ); - } - return channel; -} - /** 系统分区延续已发布客户端端点,渠道本身不包含系统。 */ export function resolveReleasePartition( channel = resolveReleaseChannel(), @@ -428,12 +410,19 @@ export function buildTauriBuildArguments( ]; } -/** 渠道端点必须由构建期注入:官方更新插件的端点配置不支持运行期改渠道。 */ +/** + * 渠道端点与安装身份必须由构建期注入:官方更新插件的端点配置不支持运行期改渠道, + * 而 `productName` / `identifier` 决定安装目录、卸载项与客户端数据目录, + * 不同渠道必须在同一台设备上并存而不是互相顶掉。 + */ export function createChannelConfig( channel = resolveReleaseChannel(), target = defaultTarget(), ) { + const { productName, identifier } = resolveChannelInstallIdentity(channel); return { + productName, + identifier, plugins: { updater: { endpoints: [updateManifestUrl(channel, target)], 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 15bcc11aa..848b74692 100644 --- a/apps/ai-game-creator-shell/scripts/build-release.test.mjs +++ b/apps/ai-game-creator-shell/scripts/build-release.test.mjs @@ -37,6 +37,11 @@ import { selectReleaseArtifact, updateManifestUrl, } from './build-release.mjs'; +import { + AGC_APP_IDENTIFIER, + AGC_PRODUCT_NAME, + resolveChannelInstallIdentity, +} from './channel-identity.mjs'; const windowsTarget = 'x86_64-pc-windows-msvc'; const universalTarget = 'universal-apple-darwin'; @@ -184,6 +189,8 @@ test('channel manifest URL and build-time endpoint follow the channel', () => { 'https://agc-dev.oss-rg-china-mainland.aliyuncs.com/agc/dev-win/latest.json', ); assert.deepEqual(createChannelConfig('dev', 'aarch64-apple-darwin'), { + productName: AGC_PRODUCT_NAME, + identifier: AGC_APP_IDENTIFIER, plugins: { updater: { endpoints: [ @@ -204,6 +211,68 @@ test('channel manifest URL and build-time endpoint follow the channel', () => { }); }); +test('channel install identity isolates co-installed builds and keeps the default channel stable', () => { + // 默认渠道必须保持已发布客户端身份:改身份等于换一个 App,升级链会断。 + assert.deepEqual(resolveChannelInstallIdentity('dev'), { + productName: AGC_PRODUCT_NAME, + identifier: AGC_APP_IDENTIFIER, + }); + assert.deepEqual(resolveChannelInstallIdentity('release'), { + productName: '陶泥儿 Release', + identifier: `${AGC_APP_IDENTIFIER}.release`, + }); + assert.deepEqual(resolveChannelInstallIdentity('beta-2'), { + productName: '陶泥儿 Beta-2', + identifier: `${AGC_APP_IDENTIFIER}.beta-2`, + }); + + // 同一台设备上不同渠道的安装目录、卸载项与数据目录必须互不相同。 + for (const channel of ['release', 'beta-2', 'a'.repeat(32)]) { + const identity = resolveChannelInstallIdentity(channel); + assert.notEqual(identity.productName, AGC_PRODUCT_NAME); + assert.notEqual(identity.identifier, AGC_APP_IDENTIFIER); + assert.ok(identity.identifier.startsWith(`${AGC_APP_IDENTIFIER}.`)); + } + + for (const channel of ['dev-win', 'Release', 'win', 'beta-']) { + assert.throws( + () => resolveChannelInstallIdentity(channel), + /发布渠道无效/u, + ); + } +}); + +test('channel install identity is baked into the same build-time config as the endpoint', () => { + withEnv({ AGC_UPDATE_OSS_BASE_URL: undefined }, () => { + const config = createChannelConfig('release', windowsTarget); + assert.equal(config.productName, '陶泥儿 Release'); + assert.equal(config.identifier, `${AGC_APP_IDENTIFIER}.release`); + assert.match( + config.plugins.updater.endpoints[0], + /\/release-win\/latest\.json$/u, + ); + }); +}); + +test('channel products keep first-install selection working under the channel product name', () => { + const root = mkdtempSync(path.join(os.tmpdir(), 'agc-channel-dmg-')); + try { + const { productName } = resolveChannelInstallIdentity('release'); + const dmg = path.join(root, `${productName}_${packageVersion}_aarch64.dmg`); + writeFileSync(dmg, 'channel first installation disk image'); + writeFileSync(path.join(root, 'windows.exe'), 'wrong platform'); + assert.equal( + selectFirstInstallArtifact([dmg, path.join(root, 'windows.exe')], { + target: 'aarch64-apple-darwin', + version: packageVersion, + }), + dmg, + ); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + test('packaged renderer receives the same channel as the updater manifest', () => { const context = resolveReleaseContext([], { AGC_BUILD_TARGET: windowsTarget, diff --git a/apps/ai-game-creator-shell/scripts/channel-identity.mjs b/apps/ai-game-creator-shell/scripts/channel-identity.mjs new file mode 100644 index 000000000..5de68db22 --- /dev/null +++ b/apps/ai-game-creator-shell/scripts/channel-identity.mjs @@ -0,0 +1,73 @@ +/** + * AGC 渠道 → 安装身份。 + * + * 渠道同时决定两件事: + * - 更新端点:OSS 分区 `-win` / `-mac` 的清单地址; + * - 安装身份:`productName` 与 `identifier`。 + * + * 安装身份决定 Windows 安装目录与卸载项、macOS `.app` 名字与 bundle id、 + * Windows WebView2 数据目录以及 `%APPDATA%\` 客户端数据目录。 + * 因此不同渠道的包体在同一台设备上并存时互不顶掉,也不会共享登录态、 + * 本地项目与运行锁。 + * + * 默认渠道 `dev` 保持已发布客户端身份不变:升级链路与既有安装不能断。 + */ + +export const AGC_DEFAULT_CHANNEL = 'dev'; +export const AGC_PRODUCT_NAME = '陶泥儿'; +export const AGC_APP_IDENTIFIER = 'world.genarrative.ai-game-creator'; + +const reservedChannelNames = new Set([ + 'win', + 'mac', + 'windows', + 'macos', + 'darwin', + 'linux', +]); + +/** 校验渠道名:小写字母开头,允许数字与连字符,系统名不属于渠道。 */ +export function validateReleaseChannel(channel) { + if ( + typeof channel !== 'string' || + !/^[a-z][a-z0-9-]{0,31}$/u.test(channel) || + channel.endsWith('-') || + reservedChannelNames.has(channel) || + /-(win|mac)$/u.test(channel) + ) { + throw new Error( + '发布渠道无效:请使用 dev、release 或最多 32 位的小写字母、数字和连字符名称,系统名称不属于渠道', + ); + } + return channel; +} + +export function resolveReleaseChannel(env = process.env) { + return validateReleaseChannel(env.AGC_UPDATE_CHANNEL?.trim() ?? 'dev'); +} + +/** 安装身份里的展示后缀:`release` → `Release`,`beta-2` → `Beta-2`。 */ +export function channelDisplaySuffix(channel) { + return validateReleaseChannel(channel) + .split('-') + .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)) + .join('-'); +} + +/** + * 渠道对应的安装身份。默认渠道返回基线身份,其它渠道派生渠道后缀, + * 保证同一台设备上不同渠道互不覆盖。 + */ +export function resolveChannelInstallIdentity(channel = AGC_DEFAULT_CHANNEL) { + validateReleaseChannel(channel); + if (channel === AGC_DEFAULT_CHANNEL) { + return Object.freeze({ + productName: AGC_PRODUCT_NAME, + identifier: AGC_APP_IDENTIFIER, + }); + } + return Object.freeze({ + productName: `${AGC_PRODUCT_NAME} ${channelDisplaySuffix(channel)}`, + identifier: `${AGC_APP_IDENTIFIER}.${channel}`, + }); +} diff --git a/apps/ai-game-creator-shell/scripts/check-config.mjs b/apps/ai-game-creator-shell/scripts/check-config.mjs index df9518a8d..57c325aef 100644 --- a/apps/ai-game-creator-shell/scripts/check-config.mjs +++ b/apps/ai-game-creator-shell/scripts/check-config.mjs @@ -27,6 +27,11 @@ import { appIdentifier, defaultRealSwarmTestTask, } from './agent-swarm-test-chat.mjs'; +import { + AGC_APP_IDENTIFIER, + AGC_PRODUCT_NAME, + resolveChannelInstallIdentity, +} from './channel-identity.mjs'; import { askHidden, assertSafeGameCreatorConfigDestination, @@ -1308,7 +1313,8 @@ if ( } for (const requiredSource of [ - "export const appIdentifier = 'world.genarrative.ai-game-creator'", + "import { AGC_APP_IDENTIFIER } from './channel-identity.mjs'", + 'export const appIdentifier = AGC_APP_IDENTIFIER', "'--swarm-chat'", "'--autonomous-game-build'", "'--preview-serve'", @@ -1319,14 +1325,38 @@ for (const requiredSource of [ } } -if (tauriConfig.productName !== '陶泥儿') { +// 基线配置必须等于默认渠道的安装身份:默认渠道不能改身份,否则已发布客户端 +// 的升级链路与既有安装目录都会断开。 +const defaultChannelIdentity = resolveChannelInstallIdentity('dev'); +if (tauriConfig.productName !== AGC_PRODUCT_NAME) { throw new Error('AI game creator shell productName drifted'); } -if (tauriConfig.identifier !== 'world.genarrative.ai-game-creator') { +if (tauriConfig.identifier !== AGC_APP_IDENTIFIER) { throw new Error('AI game creator shell identifier drifted'); } +if ( + tauriConfig.productName !== defaultChannelIdentity.productName || + tauriConfig.identifier !== defaultChannelIdentity.identifier +) { + throw new Error( + 'AI game creator shell baseline config must match the default channel identity', + ); +} + +// 非默认渠道必须派生出独立安装身份,否则同机安装会互相顶掉。 +for (const channel of ['release', 'beta-2']) { + const identity = resolveChannelInstallIdentity(channel); + if ( + identity.productName === defaultChannelIdentity.productName || + identity.identifier === defaultChannelIdentity.identifier || + !identity.identifier.startsWith(`${AGC_APP_IDENTIFIER}.`) + ) { + throw new Error(`channel install identity not isolated: ${channel}`); + } +} + const expectedBundledDesignAgentResources = { 'design-agent': 'design-agent', ...Object.fromEntries( diff --git a/apps/ai-game-creator-shell/scripts/prepare-macos-codex.test.mjs b/apps/ai-game-creator-shell/scripts/prepare-macos-codex.test.mjs index b62fe76a1..fc3546be7 100644 --- a/apps/ai-game-creator-shell/scripts/prepare-macos-codex.test.mjs +++ b/apps/ai-game-creator-shell/scripts/prepare-macos-codex.test.mjs @@ -174,8 +174,16 @@ test('macOS release entry and smoke script derive product names from config and new URL('./build-macos-ci.mjs', import.meta.url), 'utf8', ); - // 产品名决定 *.app、updater 归档与 DMG 卷名:写死会在改名后静默找错对象。 - assert.ok(entry.includes('readProductName'), '入口必须从 Tauri 配置读产品名'); + // 产品名决定 *.app、updater 归档与 DMG 卷名:它必须从渠道安装身份派生, + // 写死会在换渠道或改名后静默找错对象。 + assert.ok( + entry.includes('resolveChannelInstallIdentity'), + '入口必须从渠道安装身份派生产品名', + ); + assert.ok( + entry.includes('resolveProductName(context.channel)'), + '产品名必须按当前发布渠道解析', + ); assert.ok(!entry.includes('陶泥儿'), 'macOS 发布入口不得写死产品名'); assert.ok( entry.includes("const macTarget = 'aarch64-apple-darwin'"), diff --git a/apps/ai-game-creator-shell/src-tauri/src/config.rs b/apps/ai-game-creator-shell/src-tauri/src/config.rs index d3f31417c..5587e7a06 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/config.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/config.rs @@ -862,6 +862,48 @@ pub(crate) fn validate_game_creator_private_path_ancestors( Ok(()) } +/// 客户端安装身份基线:`productName` / `identifier` 由构建期按渠道注入。 +/// +/// 默认渠道保持基线身份,其它渠道派生 `<基线>.<渠道>`,因此同一台设备上 +/// 不同渠道各自拥有独立的安装目录与 AppData 数据目录。 +#[cfg(windows)] +const GAME_CREATOR_APP_IDENTIFIER: &str = "world.genarrative.ai-game-creator"; + +#[cfg(windows)] +fn is_game_creator_packaged_app_data_leaf(name: &std::ffi::OsStr) -> bool { + let Some(name) = name.to_str() else { + return false; + }; + let Some(remainder) = name.strip_prefix(GAME_CREATOR_APP_IDENTIFIER) else { + return false; + }; + if remainder.is_empty() { + return true; + } + // 渠道名是小写字母开头的 32 位以内小写字母、数字与连字符。 + remainder + .strip_prefix('.') + .is_some_and(|channel| !channel.is_empty() && channel.len() <= 32) +} + +/// 路径是否位于 `<平台配置根>/<安装身份目录>` 之内。提权助手是独立进程, +/// 看不到父进程的配置目录覆盖,因此这里必须按目录名识别全部渠道身份。 +#[cfg(windows)] +fn path_is_inside_game_creator_packaged_app_data(root: &Path, path: &Path) -> bool { + let root = normalize_windows_policy_path(root); + let path = normalize_windows_policy_path(path); + let Ok(relative) = path.strip_prefix(&root) else { + return false; + }; + relative + .components() + .next() + .is_some_and(|component| match component { + std::path::Component::Normal(name) => is_game_creator_packaged_app_data_leaf(name), + _ => false, + }) +} + /// Automatic ACL repair for managed paths is limited to objects AGC owns. A /// separate, explicit user-selected scope below covers native picker/project /// root results, including projects stored outside the current profile. @@ -900,11 +942,8 @@ fn game_creator_private_path_allows_auto_elevation(path: &Path) -> bool { // elevated helper runs in a fresh process, so the in-memory runtime // config-dir override is unavailable there; recognize the packaged // path from the user's profile as well. - let packaged_app_data = home - .join("AppData") - .join("Local") - .join("world.genarrative.ai-game-creator"); - if starts_with_path(&packaged_app_data) { + if path_is_inside_game_creator_packaged_app_data(&home.join("AppData").join("Local"), &path) + { return true; } } @@ -914,7 +953,7 @@ fn game_creator_private_path_allows_auto_elevation(path: &Path) -> bool { .map(PathBuf::from) .filter(|candidate| candidate.is_absolute()) { - if starts_with_path(&root.join("world.genarrative.ai-game-creator")) { + if path_is_inside_game_creator_packaged_app_data(&root, &path) { return true; } } @@ -1096,10 +1135,9 @@ fn game_creator_runtime_config_repair_scope(path: &Path) -> WindowsAclRepairScop .filter(|candidate| candidate.is_absolute()) { if is_builtin_root(home.join(".config").join("genarrative")) - || is_builtin_root( - home.join("AppData") - .join("Local") - .join("world.genarrative.ai-game-creator"), + || path_is_inside_game_creator_packaged_app_data( + &home.join("AppData").join("Local"), + &path, ) { return WindowsAclRepairScope::Managed; @@ -1110,7 +1148,7 @@ fn game_creator_runtime_config_repair_scope(path: &Path) -> WindowsAclRepairScop .map(PathBuf::from) .filter(|candidate| candidate.is_absolute()) { - if is_builtin_root(root.join("world.genarrative.ai-game-creator")) { + if path_is_inside_game_creator_packaged_app_data(&root, &path) { return WindowsAclRepairScope::Managed; } } @@ -5044,18 +5082,38 @@ mod private_path_elevation_policy_tests { #[cfg(windows)] #[test] - fn verbatim_packaged_appdata_path_keeps_managed_repair_scope() { + fn packaged_appdata_paths_keep_managed_repair_scope_for_every_channel() { let root = std::env::var_os("LOCALAPPDATA") .or_else(|| std::env::var_os("APPDATA")) .map(PathBuf::from) .expect("local appdata"); - let packaged = root.join("world.genarrative.ai-game-creator"); - let verbatim = PathBuf::from(format!(r"\\?\{}", packaged.display())); - assert!(game_creator_private_path_allows_auto_elevation(&verbatim)); + // 默认渠道是基线目录,其它渠道派生 `<基线>.<渠道>`;提权助手按目录名识别, + // 两种身份都必须落在 managed 赋权范围内。 + for leaf in [ + "world.genarrative.ai-game-creator", + "world.genarrative.ai-game-creator.release", + "world.genarrative.ai-game-creator.beta-2", + ] { + let packaged = root.join(leaf).join("diagnostics"); + let verbatim = PathBuf::from(format!(r"\\?\{}", packaged.display())); + assert!( + game_creator_private_path_allows_auto_elevation(&verbatim), + "{leaf}" + ); + assert_eq!( + game_creator_runtime_config_repair_scope(&verbatim), + WindowsAclRepairScope::Managed, + "{leaf}" + ); + } + + // 相似前缀不是安装身份目录,不能落进 managed 赋权范围。 + let foreign = root.join("world.genarrative.ai-game-creator-backup"); + assert!(!game_creator_private_path_allows_auto_elevation(&foreign)); assert_eq!( - game_creator_runtime_config_repair_scope(&verbatim), - WindowsAclRepairScope::Managed + game_creator_runtime_config_repair_scope(&foreign), + WindowsAclRepairScope::UserSelected ); } diff --git a/apps/ai-game-creator-shell/src-tauri/src/main.rs b/apps/ai-game-creator-shell/src-tauri/src/main.rs index cb3f75eb7..d123ec186 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/main.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs @@ -2005,6 +2005,13 @@ fn show_startup_error_dialog(log_path: Option<&Path>) { } } +/// 客户端产品名跟随构建期渠道身份:默认渠道是「陶泥儿」,其它渠道带渠道后缀 +/// (例如「陶泥儿 Release」)。同机并存的渠道客户端因此在窗口标题、任务栏与 +/// Alt-Tab 里可区分;默认渠道结果不变。 +pub(crate) fn game_creator_product_name(app: &tauri::AppHandle) -> String { + app.package_info().name.clone() +} + /// 配置目录就绪前的启动日志路径:优先用已经生效的配置目录(例如 `--config-dir` /// 已经设置好的目录),否则退到平台配置根。两者都不可用时返回 `None`,此时 /// `StartupLogSlot::fail` 仍然必须给出用户可见提示。 @@ -2468,6 +2475,16 @@ fn main() { setup_log.fail("startup.appdata.resolve.failed details=config-dir-uninitialized"); error })?; + // 主窗口标题与产品名保持一致:配置里的标题来自基线配置,渠道后缀只 + // 由构建期身份决定,因此必须在这里按产品名覆盖。 + match app.get_webview_window("client") { + Some(window) => { + if let Err(error) = window.set_title(&game_creator_product_name(app.handle())) { + app_log!("startup.window-title.failed: {error}"); + } + } + None => app_log!("startup.window-title.failed: 缺少 client 主窗口"), + } spawn_project_snapshot_scheduler(app.handle().clone()); if let Err(error) = builtin_plugins::initialize(&config_dir) { app_log!("startup.builtin-plugins.initialize.failed: {error}"); diff --git a/apps/ai-game-creator-shell/src-tauri/src/windows.rs b/apps/ai-game-creator-shell/src-tauri/src/windows.rs index fc146f387..854970f6f 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/windows.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/windows.rs @@ -164,7 +164,7 @@ pub(crate) fn open_game_creator_workspace_window( existing.close().map_err(|error| error.to_string())?; } tauri::WebviewWindowBuilder::new(&app, "main", workspace_window_url(project_path)) - .title("陶泥儿") + .title(crate::game_creator_product_name(&app)) .decorations(false) .inner_size(1180.0, 820.0) .min_inner_size(760.0, 560.0) @@ -183,7 +183,7 @@ pub(crate) fn open_game_creator_launcher_window( existing.set_focus().map_err(|error| error.to_string())?; } else { tauri::WebviewWindowBuilder::new(&app, "launcher", launcher_window_url()) - .title("陶泥儿") + .title(crate::game_creator_product_name(&app)) .decorations(false) .inner_size(820.0, 640.0) .min_inner_size(720.0, 520.0) diff --git a/docs/project-memory/plans/【实施计划】AGC渠道安装身份隔离-2026-09-21.md b/docs/project-memory/plans/【实施计划】AGC渠道安装身份隔离-2026-09-21.md new file mode 100644 index 000000000..0b57d90e7 --- /dev/null +++ b/docs/project-memory/plans/【实施计划】AGC渠道安装身份隔离-2026-09-21.md @@ -0,0 +1,49 @@ +# 【实施计划】AGC 渠道安装身份隔离 + +| 字段 | 值 | +| --- | --- | +| Milestone | `docs/project-memory/plans/【里程碑】AGC渠道安装身份隔离-2026-09-21.md` | +| Status | ready | +| Owner | 当前 Agent | + +## 修改边界 + +- 允许修改: + - `apps/ai-game-creator-shell/scripts/channel-identity.mjs`(新增,渠道身份单点定义) + - `apps/ai-game-creator-shell/scripts/build-release.mjs`、`build-macos-ci.mjs`、`check-config.mjs`、`agent-swarm-test-chat.mjs` + - `apps/ai-game-creator-shell/src-tauri/src/main.rs`、`src-tauri/src/windows.rs`、`src-tauri/src/config.rs` + - 对应测试:`build-release.test.mjs`、`prepare-macos-codex.test.mjs` + - 文档:AGC 更新主规范、共享记忆与本计划对 +- 明确不修改:OSS 分区布局、官网下载接口与页面、Jenkins Job 参数、渠道版本发行逻辑、Apple 签名/公证、移动壳。 + +## 实现顺序 + +1. 抽出 `channel-identity.mjs`:渠道校验、渠道显示名、`resolveChannelInstallIdentity()`;`build-release.mjs` 复用并再导出渠道校验。 +2. 渠道 `--config` 同时注入 `productName` 与 `identifier`;macOS 发布入口按发布渠道解析产品名(`.app`、updater 归档、DMG 卷名与文件名)。 +3. Rust:主窗口/工作区/启动器窗口标题取构建期产品名;AGC 自有 AppData 目录的 ACL managed 识别覆盖 `<基线>` 与 `<基线>.<渠道>`。 +4. 门禁与测试:`check-config.mjs` 断言基线等于默认渠道身份、非默认渠道身份隔离;`build-release.test.mjs` 增补身份与首装包用例;`prepare-macos-codex.test.mjs` 改断言为消费渠道身份。 +5. 文档:更新 AGC 更新主规范与共享记忆,登记决策与踩坑。 + +## 验证命令 + +1. `node --check apps/ai-game-creator-shell/scripts/channel-identity.mjs apps/ai-game-creator-shell/scripts/build-release.mjs apps/ai-game-creator-shell/scripts/build-macos-ci.mjs apps/ai-game-creator-shell/scripts/check-config.mjs` +2. `node --test apps/ai-game-creator-shell/scripts/build-release.test.mjs apps/ai-game-creator-shell/scripts/release-oss.test.mjs apps/ai-game-creator-shell/scripts/prepare-macos-codex.test.mjs apps/ai-game-creator-shell/scripts/cargo-features.test.mjs` +3. `node apps/ai-game-creator-shell/scripts/check-config.mjs` +4. `cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --bin genarrative-ai-game-creator-shell -- config::tests:: --test-threads=1` +5. `npm run check:encoding`、`npm run check:doc-index`、`git diff --check` + +## 验证结果 + +- `node --test build-release.test.mjs release-oss.test.mjs prepare-macos-codex.test.mjs cargo-features.test.mjs`:64/64 通过(新增渠道身份、身份注入与渠道 DMG 首装选择三条用例)。 +- `node apps/ai-game-creator-shell/scripts/check-config.mjs`、`npm --prefix apps/ai-game-creator-shell run typecheck`:通过。 +- `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 通过。 +- `AGC_UPDATE_CHANNEL=release npm --prefix apps/ai-game-creator-shell run build -- --no-bundle --debug`:Tauri 接受派生的 `productName` / `identifier` 并完成构建;产物字符串实测 `陶泥儿 Release` × 1、`agc/release-win/latest.json` × 1、`world.genarrative.ai-game-creator.release` × 1、`agc/dev-win/latest.json` × 0。 +- `cargo fmt --check`(AGC 壳)、`npm run check:encoding`、`npm run check:doc-index`、`git diff --check`:通过。 +- 未执行:真实渠道打包(需要签名私钥与发号/上传授权)、双渠道真机安装与并存、macOS 节点实跑。 + +## 风险与回滚点 + +- 风险:默认渠道身份若被改动,既有安装目录、卸载项与升级链会断。回滚点:基线 `tauri.conf.json` 与默认渠道映射不变,门禁用例钉住。 +- 风险:非默认渠道首次以新身份安装,老 `dev` 用户不会自动迁移本地数据。回滚点:渠道身份只影响非默认渠道构建,撤销该渠道的构建产物即可,仓库侧无数据迁移。 +- 风险:窗口标题改为构建期产品名后,标题不再等于配置里的字面量。回滚点:去掉 `main.rs` 的标题覆盖调用,行为回到配置标题。 +- 风险:ACL managed 识别放宽到前缀族。回滚点:`is_game_creator_packaged_app_data_leaf` 收紧回单一直线值,但非默认渠道的提权修复会重新失败关闭。 diff --git a/docs/project-memory/plans/【里程碑】AGC渠道安装身份隔离-2026-09-21.md b/docs/project-memory/plans/【里程碑】AGC渠道安装身份隔离-2026-09-21.md new file mode 100644 index 000000000..dea7272cd --- /dev/null +++ b/docs/project-memory/plans/【里程碑】AGC渠道安装身份隔离-2026-09-21.md @@ -0,0 +1,44 @@ +# 里程碑:AGC 渠道安装身份隔离 + +| 字段 | 值 | +| --- | --- | +| Version | 1.0 | +| Status | 代码与源码级验收已落地,等待真机双渠道安装验收 | +| Date | 2026-09-21 | +| Parent Spec | `docs/technical/【技术方案】AGC客户端更新检查与下载-2026-08-31.md` | + +## 目标 + +不同渠道的 AGC 包体在同一台设备上并存:安装、运行、客户端数据与更新互不覆盖、互不顶掉。 + +## 范围 + +- 构建期按渠道产出安装身份(`productName` / `identifier`),默认渠道 `dev` 身份保持不变。 +- 渠道产物命名、首装包选择、macOS 发布入口与配置门禁都消费同一份渠道身份。 +- 同机并存的可见区分(窗口标题)与 AGC 自有 AppData 目录的提权 ACL 识别范围。 + +## 不在范围内 + +- 不迁移、不共享既有本地项目、工程快照、模板、登录态与诊断数据。 +- 不新增渠道,不改 OSS 分区布局、官网下载页与后端接口。 +- 不做 Apple 代码签名/公证,不恢复 Intel 架构。 + +## 依赖与前置条件 + +- 渠道与更新端点合同已落地(`-win` / `-mac` 分区与 Tauri updater 端点)。 +- 构建入口统一在 Tauri 构建前注入渠道 `--config`。 + +## 验收标准 + +- [x] 默认渠道的 `productName` / `identifier` 与基线配置逐字一致,既有安装与升级链不断(`check-config.mjs` + 渠道身份用例)。 +- [x] `release` 与自定义渠道派生独立 `productName` 与 `identifier`,与 `dev` 可在同一台设备并存(源码级;真机安装见下方未完成项)。 +- [x] 渠道产物(NSIS `.exe`、`.app.tar.gz`、DMG)与首装包选择跟随渠道身份且唯一匹配(发布脚本用例)。 +- [x] 非默认渠道的客户端数据目录、WebView2 目录与窗口标题跟随渠道身份(构建产物字符串实测 + 运行期标题取产品名)。 +- [x] AGC 自有 AppData 目录的 ACL managed 范围覆盖全部渠道身份,且不扩大到相似前缀目录(Rust 定向 12/12)。 +- [ ] 真机:同一台设备同时安装 `dev` 与 `release`,二者可并存、可各自原地更新。 + +## 证据要求 + +- 自动化:`build-release.test.mjs`、`release-oss.test.mjs`、`prepare-macos-codex.test.mjs`、`check-config.mjs` 与 Rust `config::tests::` 定向测试。 +- 运行时:单渠道 `--no-bundle` 构建烟测;真机双渠道安装、并存与各自更新。 +- 边界:非法渠道失败关闭;默认渠道身份不变;相似前缀目录不进入 managed 赋权范围。 diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 77d706d4b..490aca0bc 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -1,5 +1,13 @@ # 决策记录 +## 2026-09-21 渠道进安装身份:不同渠道的 AGC 包体在同一台设备并存 + +- 背景:渠道此前只决定更新端点(`plugins.updater.endpoints`)与渲染层平台 origin,`productName` / `identifier` 与渠道无关,于是所有渠道共用 `%LOCALAPPDATA%\陶泥儿` 安装目录、同一个卸载项(`HKCU\Software\Microsoft\Windows\CurrentVersion\Uninstall\陶泥儿`,另有 `HKCU\Software\genarrative\陶泥儿`)以及同一份 `%APPDATA%\world.genarrative.ai-game-creator` 数据目录。本机 0.1.48 安装实测:主程序二进制里只有 1 处 `agc/dev-win/latest.json`、0 处 release 端点,说明渠道在产物里只体现为端点。后果是后装的渠道静默顶掉先装的渠道,并接管更新端点、平台服务器与本地登录态/项目数据。 +- 决策:渠道同时决定**安装身份**。默认渠道 `dev` 保持基线 `productName = 陶泥儿`、`identifier = world.genarrative.ai-game-creator`(既有安装目录、卸载项与升级链不断);其它渠道派生 `陶泥儿 <渠道显示名>`(`release` → `陶泥儿 Release`)与 `world.genarrative.ai-game-creator.<渠道>`。身份与更新端点必须在同一个构建期 `--config` 里注入,禁止分别回读默认值。窗口标题、macOS 产物名(`.app` / updater 归档 / DMG 卷名)、首装包选择与 Windows 提权 ACL 的 managed 识别范围同批跟随该身份。 +- 边界:不做本地数据迁移或共享——切渠道等于换一个客户端;`release` 与自定义渠道首次以新身份安装,不接管、不迁移既有 `dev` 安装与本地项目,由用户自行决定是否卸载其一。 +- 影响范围:新增 `apps/ai-game-creator-shell/scripts/channel-identity.mjs`;`build-release.mjs`、`build-macos-ci.mjs`、`check-config.mjs`、`agent-swarm-test-chat.mjs`、`src-tauri/src/{main.rs,windows.rs,config.rs}` 与对应测试;主规范 `docs/technical/【技术方案】AGC客户端更新检查与下载-2026-08-31.md`。 +- 验证方式:`node --test apps/ai-game-creator-shell/scripts/build-release.test.mjs release-oss.test.mjs prepare-macos-codex.test.mjs cargo-features.test.mjs`(64/64,新增渠道身份与渠道 DMG 首装选择用例)、`node apps/ai-game-creator-shell/scripts/check-config.mjs`(基线等于默认渠道身份、非默认渠道身份隔离)、`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` 与相似前缀反向断言)、`AGC_UPDATE_CHANNEL=release npm --prefix apps/ai-game-creator-shell run build -- --no-bundle --debug`(产物字符串实测 `陶泥儿 Release` × 1、`agc/release-win/latest.json` × 1、`world.genarrative.ai-game-creator.release` × 1、`agc/dev-win/latest.json` × 0)。真机双渠道安装、并存与各自更新尚未执行,按未验证项记录。 + ## 2026-09-21 项目快照按部署渠道分区,后台按渠道查看并按素材查询口径展示用户 - 背景:AGC 项目快照此前统一写在 `agc/project-snapshots/v1/{user}/{project}/`,而开发与正式两套部署共用同一个 bucket(都默认 `agc-dev`)。结果是渠道混在一层前缀里:正式后台会列出开发渠道上传的项目,列表上也看不出项目属于哪个渠道;同时“项目工程”列表只有裸用户 ID,且用“加载更多”逐段追加,翻页与定位都困难。 @@ -11,6 +19,7 @@ - 未迁移的历史对象:`agc/project-snapshots/v1/` 下现存对象(只读核对过的两份历史清单)保留在 OSS,但不再写入、不再进入后台列表;需要取回时按旧前缀在 OSS 侧直接读取,确实要在后台看到时再单独开一个只读兼容视图。 - 验证方式:`cargo test -p platform-oss snapshot` 7 passed(渠道校验、键布局、v2 根渠道枚举、v1 历史键仍必须私有);`cargo test -p api-server project_snapshot` 18 passed/1 ignored(渠道失败关闭、游标跨渠道拒绝、用户昵称/陶泥号解析、归档与配额回归);`cargo test -p api-server protected_route_matrix`、`route_contract` 通过(新路由纳入后台鉴权矩阵);`npm run admin-web:typecheck` 与 `npx vitest run apps/admin-web/src` 202 passed。 + ## 2026-09-21 模板正文目录门禁:CLI 打包与后台上传同一份段名单 - 背景:模板包组织指南把 `.agent/`、`.git/`、`node_modules/`、根目录 `dist/` 等列为「不要放进 ZIP」,但两条发布路径此前只校验路径安全与 `entry` 是否存在,放进去的东西会跟着建到用户项目里(模板自带 `.agent/` 会让新项目继承一个陌生身份)。这条约定只靠作者自觉。 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 0205590b1..16e38d539 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -1,5 +1,14 @@ # 踩坑与排障记录 +## 2026-09-21 不同渠道的包体在同一台设备安装会互相顶掉 + +- **现象**:在一台已经装了某个渠道 AGC 客户端的设备上安装另一个渠道的安装包,装完后旧客户端直接消失(安装目录被覆盖、卸载项被接管),更新端点、平台服务器与本地登录态一起换成新渠道的;两个渠道的客户端无法共存。 +- **原因**:渠道此前只烘焙了 `plugins.updater.endpoints` 与 `VITE_AGC_PLATFORM_CHANNEL`(`apps/ai-game-creator-shell/scripts/build-release.mjs` 的 `createChannelConfig`),`productName` / `identifier` 用的是渠道无关的基线值。Tauri 的 Windows 安装目录与卸载项由 `productName` 决定,WebView2 数据目录与客户端数据目录由 `identifier` 决定,于是所有渠道落到 `%LOCALAPPDATA%\陶泥儿`、`HKCU\...\Uninstall\陶泥儿` 与 `%APPDATA%\world.genarrative.ai-game-creator`。 +- **处理(现行口径)**:渠道进入安装身份,默认渠道保持基线身份不变,其它渠道派生 `<产品名> <渠道显示名>` 与 `<基线>.<渠道>`;身份与端点在同一次构建期 `--config` 注入。见决策记录 2026-09-21 条目。 +- **核对方式**:装完任渠道的包后看 `HKCU\Software\Microsoft\Windows\CurrentVersion\Uninstall\<产品名>` 的 `InstallLocation`、`%APPDATA%\` 与主程序窗口标题是否按渠道分开;同名安装目录或同名数据目录说明身份没有生效。 +- **易错点**:只改安装包文件名或快捷方式名而不改 `identifier`,两个渠道仍会抢同一份 Agent Runner / 项目锁与登录态;反过来把渠道后缀加在默认渠道上,既有安装的升级链会断(客户端认不出旧安装)。相似前缀目录(如 `world.genarrative.ai-game-creator-backup`)不得进入提权 ACL 的 managed 范围。 +- **关联**:`apps/ai-game-creator-shell/scripts/channel-identity.mjs`、`build-release.mjs`、`build-macos-ci.mjs`、`src-tauri/src/config.rs`。 + ## 发布器守卫拒绝时不要把 process.exit 用在 fetch 句柄未关闭处 - 现象:`agc-template-library-publish.mjs --dry-run` 撞上「同一 `templateVersion` 的 ZIP 不得变」门禁时,终端只剩一句 `Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\win\async.c`,看不到任何拒绝原因,看起来像脚本崩溃而不是被拒绝。 diff --git a/docs/project-memory/shared-memory/project-overview.md b/docs/project-memory/shared-memory/project-overview.md index 3cd48c7d9..2729c69de 100644 --- a/docs/project-memory/shared-memory/project-overview.md +++ b/docs/project-memory/shared-memory/project-overview.md @@ -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 安装产品名统一为“陶泥儿”,由 Tauri `productName` 控制安装项、快捷方式与 EXE 产品描述;Windows 内置 Codex 安装到顶层 `coding-agent/win-x64/`,打包资源映射与运行时查找路径必须一致。内部可执行文件名与应用 identifier 保持稳定。 +- AGC 安装产品名基线为“陶泥儿”,由 Tauri `productName` 控制安装项、快捷方式与 EXE 产品描述;默认渠道 `dev` 保持基线产品名与 identifier `world.genarrative.ai-game-creator` 不变,其它渠道派生 `陶泥儿 <渠道显示名>` 与 `<基线>.<渠道>`,让不同渠道的包体在同一台设备上并存而不互相顶掉(详见《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 工程。 diff --git a/docs/project-memory/shared-memory/team-conventions.md b/docs/project-memory/shared-memory/team-conventions.md index aed755b62..41778637a 100644 --- a/docs/project-memory/shared-memory/team-conventions.md +++ b/docs/project-memory/shared-memory/team-conventions.md @@ -35,7 +35,7 @@ - AGC 批量追加素材标签由原生在一次项目写锁与 revision CAS 下合并各项原标签,先校验全批再写 manifest;前端不能循环单素材分类命令,不回传展示层推导的分类或旧标签全集,以免部分写入或覆盖未编辑字段。 -- AGC 正式包的平台服务跟随构建渠道:`release` 连接 `https://www.genarrative.world`,`dev` 连接 `https://dev.genarrative.world`;本地 debug 态保留 release/dev/custom 服务器选择,会话凭据始终按 origin 隔离。发布渠道为 `dev/release/自定义名称`,Windows/Mac 是系统,OSS 的 `-win/mac` 仅是延续既有地址的分区。官网通过服务端 `GENARRATIVE_CLIENT_DOWNLOAD_CHANNEL`(默认 dev)选择渠道,公开同源 `/api/client-downloads` 汇总其各系统首装包与真实版本;未发布隐藏,单系统失败不影响其它下载,不跨渠道补齐。发布先上传 EXE/DMG 再写对应分区清单,不维护会互相覆盖的共享 OSS 索引。主站 Vite 代理复用实际 `runtimeServerTarget`。完整约定见 AGC 客户端更新检查与下载专题。 +- AGC 正式包的平台服务跟随构建渠道:`release` 连接 `https://www.genarrative.world`,`dev` 连接 `https://dev.genarrative.world`;本地 debug 态保留 release/dev/custom 服务器选择,会话凭据始终按 origin 隔离。发布渠道为 `dev/release/自定义名称`,Windows/Mac 是系统,OSS 的 `-win/mac` 仅是延续既有地址的分区。官网通过服务端 `GENARRATIVE_CLIENT_DOWNLOAD_CHANNEL`(默认 dev)选择渠道,公开同源 `/api/client-downloads` 汇总其各系统首装包与真实版本;未发布隐藏,单系统失败不影响其它下载,不跨渠道补齐。发布先上传 EXE/DMG 再写对应分区清单,不维护会互相覆盖的共享 OSS 索引。主站 Vite 代理复用实际 `runtimeServerTarget`。渠道同时决定安装身份:默认渠道 `dev` 必须保持基线 `productName` / `identifier` 不变,其它渠道派生独立身份,使不同渠道的包体可在同一台设备并存且本地数据不共享;身份与更新端点必须在同一次构建期注入里确定,禁止分别回读默认值。完整约定见 AGC 客户端更新检查与下载专题。 - AGC 模板库灰度复用 `agc:template-library`:未配置关闭,已配置时遵循现有灰度启停、用户 ID/标签和比例规则;服务端返回权威结论,客户端入口和原生清单/下载/建项均执行门禁,主体切换丢弃旧异步结果。公开 OSS 不是保密边界,已创建项目不受影响。 - 画布卡片类型与信息角标共用 `CanvasCardCornerActions`;菜单收纳共用 `OverflowActions`,宿主决定展示数量和资源命令。AGC 选中菜单前 5 项直显,Web 默认不折叠;浮层 portal 继续接入现有画布关闭与滚轮归属判据。 diff --git a/docs/technical/【技术方案】AGC客户端更新检查与下载-2026-08-31.md b/docs/technical/【技术方案】AGC客户端更新检查与下载-2026-08-31.md index 69559ba2a..68388ccc4 100644 --- a/docs/technical/【技术方案】AGC客户端更新检查与下载-2026-08-31.md +++ b/docs/technical/【技术方案】AGC客户端更新检查与下载-2026-08-31.md @@ -1,6 +1,6 @@ # AGC 客户端更新检查与下载 -更新时间:`2026-09-20` +更新时间:`2026-09-21` 本文件是 AGC 客户端自动更新的主规范:更新能力由 Tauri 官方插件 `tauri-plugin-updater` 承担,并按下文渠道分发。 @@ -18,6 +18,17 @@ - 验收必须覆盖 dev/release/自定义渠道各自端点和对象地址、独立版本、非法名称、旧 dev 地址延续、release 不写旧迁移桥、网站配置贯通、跨渠道链接拒绝和部分失败。 - 更新链路的信任来源从「清单里的 sha256 + 受信域名」升级为「发布签名 + 受信域名」:清单里的 `signature` 由构建期私钥生成,客户端用内置公钥校验,校验不过就拒绝安装。 +### 渠道与安装身份合同 + +- 渠道同时决定**更新端点**与**安装身份**,两者都由构建期写入产物。默认渠道 `dev` 保持基线身份 `productName = 陶泥儿`、`identifier = world.genarrative.ai-game-creator`;其它渠道(`release` 与自定义渠道)派生 `productName = 陶泥儿 <渠道显示名>`(`release` → `陶泥儿 Release`、`beta-2` → `陶泥儿 Beta-2`)与 `identifier = world.genarrative.ai-game-creator.<渠道>`。渠道显示名按连字符分段首字母大写,不改动渠道本身。 +- 默认渠道身份**不可变更**:既有安装目录、卸载项、快捷方式与已发布客户端的升级链都建立在基线身份上。渠道身份由 `apps/ai-game-creator-shell/scripts/channel-identity.mjs` 单点定义,构建入口、macOS 发布入口与配置门禁共同消费;基线 `tauri.conf.json` 必须逐字等于默认渠道身份。 +- 安装身份决定的持久与可见事实:Windows 安装目录 `%LOCALAPPDATA%\<产品名>`、卸载项与 `HKCU\Software\genarrative\<产品名>`、WebView2 数据目录 `%LOCALAPPDATA%\`、客户端数据目录 `%APPDATA%\`;macOS `.app` 名、bundle id、DMG 卷名与菜单栏应用名。 +- 同机并存:不同渠道的包体可以在同一台设备上同时安装并同时运行,互不覆盖、互不顶掉;同一渠道的新版本仍是原地升级,因为更新端点与安装身份同属一个渠道。 +- 数据不跨渠道共享:本地项目、工程快照、模板、登录态、诊断日志与 Runner/项目锁按渠道身份分目录。切渠道等于换一个客户端,不迁移、不合并本地数据;渠道内的 origin 隔离规则不变。 +- 主窗口与工作区/启动器窗口标题取构建期产品名,让同机并存的渠道客户端在任务栏与 Alt-Tab 中可区分;默认渠道标题仍是「陶泥儿」。 +- 首装包与更新包的对象名包含产品名(如 `陶泥儿 Release_0.1.96_x64-setup.exe`、`陶泥儿 Release_0.1.96_aarch64.dmg`)。清单 `downloads` 地址由发布脚本按本次真实产物派生,禁止写死产品名;首装包选择按 `<版本>_<架构>.dmg` 唯一匹配,不依赖产品名字面量。 +- Windows 提权 ACL 修复助手按目录名识别安装身份:`<基线>` 与 `<基线>.<渠道>` 都在 AGC 自有的 managed 范围内;相似前缀(例如 `world.genarrative.ai-game-creator-backup`)不在范围内,落回 user-selected 范围或直接拒绝。 + ## 非目标 - 不做灰度放量、分批更新、强制更新和自动回滚;渠道只决定「取哪份清单」。 @@ -110,11 +121,21 @@ - 迁移起点:已发布客户端(含当前线上版本)内置自研清单地址 `agc/latest.json`(sha256 格式),下载与安装由自研 Rust 命令完成。 - 迁移策略见「未决问题与决策」。迁移完成后,自研清单解析、下载命令、下载进度事件以及为此放行的 CSP / HTTP 白名单条目按「四不写」整条删除,不留兼容分支与墓碑说明。 +- 渠道安装身份映射(`` 为 `dev`、`release` 或自定义名称;`` 为渠道显示名): + +| 渠道 | productName | identifier | Windows 安装目录 | 客户端数据目录 | +| -------------------- | ------------------ | ---------------------------------------------- | ----------------------------------- | ----------------------------------------------- | +| `dev`(默认) | `陶泥儿` | `world.genarrative.ai-game-creator` | `%LOCALAPPDATA%\陶泥儿` | `%APPDATA%\world.genarrative.ai-game-creator` | +| `release` / 自定义 | `陶泥儿 ` | `world.genarrative.ai-game-creator.` | `%LOCALAPPDATA%\陶泥儿 ` | `%APPDATA%\world.genarrative.ai-game-creator.` | + +- 安装身份迁移:`dev` 客户端保持原身份,升级链路连续;`release` 与自定义渠道首次以新身份安装,**不接管也不迁移**任何既有 `dev` 安装、本地项目或登录态,设备上因此可以同时存在两个渠道的客户端,由用户自行决定是否卸载其一。 + ## 构建与发布 - 发布入口:`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 前先按发布渠道解析产品名,产物名一律派生而不写死。 - 定时调度只在本轮到达的提交包含 AGC 相关路径(客户端、共享包、`server-rs/crates`、AGC 插件、桌面壳图标、根依赖清单)时才触发渠道发布;纯文档或流水线自身的提交只跑 Full Build,不推高客户端版本号。判定失败或勾选强制触发时按"需要发布"处理。 - 更新摘要自动生成:发布脚本用渠道清单里的 `commit` 字段(上一次发布的提交)到本次提交之间、且只覆盖客户端相关路径的提交列表生成 `notes`(每条 `- 提交标题(短 SHA)`,最多 12 条、主题 80 字、整体 900 字,超出折叠或截断),同时写入旧协议清单的 `releaseNotes` 和归档文件 `release-notes.txt`。`AGC_UPDATE_RELEASE_NOTES` 非空时以手动文案为准;无法判定起点(缺少上次 `commit` 或本地没有该提交)时不写摘要。清单缺少 `commit` 时回退用上一次成功构建的 `COMMIT_HASH`(CI 通过 `AGC_UPDATE_PREVIOUS_COMMIT` 传入)作为锚点,因此首次启用摘要或更换渠道后也能立即产出摘要。锚点仍不可得(清单读取失败或没有 CI 锚点)时降级为「最近客户端改动」列表并注明可能与上一版重复 —— 摘要属于附注,任何情况下都不允许因为它让发布失败。 - 清单里的 `commit` 是非标准字段:更新插件忽略未知字段,发布脚本用它定位下一次摘要的起点。 @@ -157,6 +178,25 @@ | 真实更新闭环(含升级后重启) | 0.1.47 客户端按提示下载安装并重启 | 通过(2026-09-17 用户实测:提示 → 下载 → 安装 → 关于页显示新版本,再次检查为已是最新) | | 更新摘要端到端展示 | 公网读取渠道清单 `notes` 与客户端更新提示 | 通过(2026-09-17 用户实测:0.1.62 清单带 8 条自动摘要,客户端提示正常显示多行内容) | +渠道安装身份隔离已于 `2026-09-21` 完成源码验收: + +| 条款 | 验收方式 | 结果 | +| --- | --- | --- | +| 渠道身份派生与默认渠道不变 | `node --test apps/ai-game-creator-shell/scripts/build-release.test.mjs` | 通过;`dev` 逐字等于基线 `陶泥儿` / `world.genarrative.ai-game-creator`,`release` / `beta-2` 派生独立产品名与 identifier,非法渠道失败关闭 | +| 身份与端点同批注入 | 同上的渠道 `--config` 用例 | 通过;`productName` / `identifier` 与 `/-win|mac/latest.json` 来自同一次解析 | +| 渠道产物首装包选择 | 同上的渠道 DMG 夹具用例 | 通过;`陶泥儿 Release_<版本>_aarch64.dmg` 仍按 `<版本>_<架构>.dmg` 唯一匹配 | +| 基线配置等于默认渠道身份 | `node apps/ai-game-creator-shell/scripts/check-config.mjs` | 通过;基线漂移与非默认渠道身份不隔离都会失败关闭 | +| 全量发布脚本回归 | `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` 并完成构建;产物字符串实测 `陶泥儿 Release` × 1、`agc/release-win/latest.json` × 1、`world.genarrative.ai-game-creator.release` × 1、`agc/dev-win/latest.json` × 0 | + +未验证项(不得按已通过处理): + +- 真实 Windows 双渠道安装与并存:尚未在同一台设备安装 `dev` 与 `release` 两个渠道的安装包,安装目录/卸载项/数据目录的分离与两个客户端同时运行属于发布验收,本次只到源码与脚本层级。 +- 未生成安装包:本轮构建烟测止于 `--no-bundle`,NSIS 安装目录 / 卸载项 / 快捷方式按渠道分开、以及「装完 release 后 dev 仍在」的现场证据需要一次真实渠道打包与安装。 +- macOS 侧只验证到入口派生逻辑:`.app` 名、bundle id 与 DMG 卷名的渠道派生没有在 macOS 节点实跑。 +- 客户端数据隔离的运行期事实(`%APPDATA%\` 分目录、登录态不跨渠道)未做真机对照。 + 待执行证据(首次渠道发布后回填): | 条款 | 验收方式 | 证据 | @@ -174,6 +214,7 @@ - macOS 发布方式:已接入专用 macOS Jenkins 节点(label `genarrative-agc-macos`,EXCLUSIVE 单 executor),由 `Jenkinsfile.ai-game-creator-shell-macos-build` 执行 `scripts/build-macos-ci.mjs` 完成 arm64 单架构构建、arm64 隔离 smoke、arm64 DMG(`<产品名>_<版本>_aarch64.dmg`)、分区清单生成、更新包验签与 OSS 上传。`AGC_RELEASE_DRY_RUN` 默认为关(与 Windows 渠道对称,即直接发布),只有勾选后才退化为「只打印上传计划、不写 OSS」的演练。 - macOS 代码签名与公证暂缺:产物为未签名 + 未公证,构建入口剥离 `APPLE_*` 凭据跳过 Apple 签名,不传 `--no-sign`(它还会跳过 updater 的 minisign 签名,产物将没有 `.sig`);构建清单实测记录 `appleSigned` 与签名类型,`latest.json` 侧固定记录 `notarized=false`,首装需用户在 Gatekeeper 中手动放行。该限制作为已知未验证项记录,不静默通过;「安装 → 重启接管新版本」的自动更新闭环仍需实机验收。 - 更新包验签门禁:构建完成、上传 OSS 之前,用产物内烘焙的 `plugins.updater.pubkey` 复核 `<更新包>.sig`(Tauri 使用 minisign 的 `ED` 预哈希模式)。keyId 不一致或校验失败立即失败关闭,禁止上传——客户端校验失败会直接拒绝安装,且公钥发布后不可更换。 +- 渠道安装身份(2026-09-21):渠道此前只决定更新端点,`productName` / `identifier` 与渠道无关,导致不同渠道的包体共用 `%LOCALAPPDATA%\陶泥儿`、同一个卸载项与同一份 `%APPDATA%\world.genarrative.ai-game-creator` 数据目录,后装的渠道静默顶掉先装的渠道并接管更新端点与本地登录态。现决策为「渠道进安装身份」:默认渠道保持基线身份不动,其它渠道派生 `<产品名> <渠道显示名>` 与 `<基线>.<渠道>`,渠道内仍原地升级,同机并存与数据隔离成立。窗口标题、macOS 产物名与 Windows 提权 ACL 的 managed 识别范围同批跟随该身份。 待办: diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 7020c59e2..671f17ad6 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -578,7 +578,7 @@ Supervisor 认领该回执后,由父 run 自己为每个原 delivery 逐一创 - 模式合同:客户端 AppData 配置新增全局 `agentMode`,只接受 `codex_cli / provider`。缺省和新安装默认使用 `codex_cli`,原有 HTTP LLM Provider 路径完整保留并可显式切回 `provider`;切换只影响下一次节点请求,不新增 Runner、任务图、会话库、配置库或业务事实源。 - 调度边界:正式 DAG、manifest、Agent task/session/run 身份、队列、锁、委派、all-join、完成门、Provider lifecycle、持久 retry/handoff 与 `needs-reconciliation` 继续由现有 AGC Runtime 掌控。每个被调度节点在 `codex_cli` 模式下直接启动一次非交互 `codex exec` 充当该节点的推理 Agent;Codex 返回当前 Runtime 广告函数的结构化调用,Runtime 仍是唯一 ToolHost,不允许 CLI 自己写项目、执行命令、调用 MCP 或形成第二套 revision / verification 真相。 - 安装包侧车:Windows x64 release 固定随 Tauri resource 打包 `@openai/codex@0.155.1` 的原生 `codex.exe`;Rust build script 从 AGC 子包锁定依赖 stage 到 resource,并写入版本与 SHA-256 清单。固定版本只在 `build_support/codex_bundle.rs` 声明一次,构建脚本、宿主补丁执行器身份、逐次审批协议允许列表和模型目录捕获共同引用它,避免多处字面量漂移。Windows 侧车映射只写入 `tauri.windows.conf.json`,通用 `tauri.conf.json` 不得让 Linux / macOS 构建依赖未生成的 Windows 二进制。运行时只在文件摘要和 `codex-cli` 版本同时匹配清单时优先选内置侧车;缺失、损坏或版本漂移时跳过它,按既有 npm 安装、PATH 顺序回退。安装包同时携带 Apache-2.0 第三方声明;API Key、`auth.json`、Cookie、Token、用户 `CODEX_HOME`、用户配置和项目数据绝不打包。 -- Windows x64 release 安装包只生成 NSIS,不生成 MSI:`tauri.windows.conf.json` 的 `bundle.targets` 固定为 `["nsis"]`,通用配置继续保留其它平台的默认打包目标。安装后的产品名、开始菜单 / 桌面快捷方式和 EXE 产品描述统一由 `tauri.conf.json` 的 `productName: "陶泥儿"` 生成;应用 identifier 与内部可执行文件名保持稳定。内置 Codex 资源安装到顶层 `coding-agent/win-x64/`,运行时从同一路径查找 `bin/codex.exe` 与 `manifest.json`;仓库 staging 仍使用 `resources/codex/win-x64/`,包内子目录、组件名、版本和完整性校验保持原合同。 +- Windows x64 release 安装包只生成 NSIS,不生成 MSI:`tauri.windows.conf.json` 的 `bundle.targets` 固定为 `["nsis"]`,通用配置继续保留其它平台的默认打包目标。安装后的产品名、开始菜单 / 桌面快捷方式和 EXE 产品描述由 `tauri.conf.json` 的 `productName` 生成:基线是 `陶泥儿`,发布构建按渠道由 `--config` 覆盖为 `陶泥儿 <渠道显示名>`,identifier 同批派生 `<基线>.<渠道>`(默认渠道保持基线值),因此不同渠道的包体可在同一台设备并存;内部可执行文件名保持稳定。内置 Codex 资源安装到顶层 `coding-agent/win-x64/`,运行时从同一路径查找 `bin/codex.exe` 与 `manifest.json`;仓库 staging 仍使用 `resources/codex/win-x64/`,包内子目录、组件名、版本和完整性校验保持原合同。 - macOS 安装包必须携带锁定版本的原生 Codex、`codex-code-mode-host`、`rg`、上游 zsh、`codex-package.json` 和第三方声明,保留上游相对布局;构建时按 Cargo 目标选择 npm 原生依赖,缺文件、版本或目标不匹配立即失败,不借用开发机 PATH 里的 Codex。资源只在 `tauri.macos.conf.json` 映射到 `Contents/Resources/coding-agent/mac-native/darwin-arm64/` 与 `darwin-x64/`。构建与运行共享平台文件白名单,运行时由当前 `.app/Contents/MacOS` 定位相邻 `Resources`,完整性与版本验证通过后优先使用内置组件;失败沿既有外部安装回退,不能运行未校验的内置文件。macOS 当前只构建 arm64 单架构,但资源映射仍并列携带两套锁定原生 Codex 依赖(运行切片按 Cargo 目标只选择对应目录),恢复 Intel 时无需改动资源布局;随包 Node 只有宿主架构那一份,所以不得构建 universal 包。不读取全局 Codex。 - 内置插件的清单、运行入口与面板同时在 Windows/macOS 随包分发,继续由既有 PluginHost 的应用资源目录扫描入口发现;不携带开发依赖、缓存、测试或私有配置。插件文件随包不等于原生适配器跨平台:Cocos 进程桥接仍受现有 Windows 实现和 feature 门禁约束,macOS 原生桥接另行设计与验收,不复制 Windows DLL 冒充支持。系统 Node、用户 Cocos Creator、账号登录、网络和生成工程的 npm 工具链仍是现有外部前提,不在此次 Codex 侧车补齐中隐式变更。 - macOS 安装包验收必须包括:脱离仓库位置的 `.app` 资源与架构检查、受限 PATH/隔离 HOME 下内置 Codex 启动和 app-server 握手、必需文件缺失/篡改/平台错误的拒绝测试,以及 DMG 完整性检查。真实登录、Provider 对话、GUI 和 Cocos 操作必须独立列出证据,不能用压缩包生成或 `--version` 成功替代。未配置正式签名、公证的本地测试包不得作为公开发行包。 diff --git a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index ccb2bbd32..23d4a3cd5 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -147,6 +147,8 @@ revision 变化时,调度管线把同一个完整 commit 通过 `COMMIT_HASH` 手工发布入口 `Genarrative-Manual-Build-And-Deploy` 的 `DEPLOY_TARGET` 与 AGC 更新渠道是两个独立维度。手工入口必须把 `release` 映射为 `AGC_UPDATE_CHANNEL=release`、把 `development` 映射为 `dev`,并把该参数同时透传给 `Genarrative-Agc-Windows-Build` 与 `Genarrative-Agc-MacOS-Build`;只传 `AGC_RELEASE_VERSION` 时下游会落回各自默认 `dev`。统一号 `0.1.95` 的 release 包应分别位于 `agc/release-win/0.1.95/` 与 `agc/release-mac/0.1.95/`,渠道清单为对应目录下的 `latest.json`,不存在 `agc/release/` 这一层。补发本轮已烧号的版本时,直接以相同 `AGC_RELEASE_VERSION` 重跑两条 AGC Job,不重新发号。 +渠道参数同时决定安装身份:默认渠道 `dev` 产出 `productName=陶泥儿` + `identifier=world.genarrative.ai-game-creator`(既有安装与升级链),其它渠道产出 `陶泥儿 <渠道显示名>` + `<基线>.<渠道>`,因此同一台设备可以同时装 `dev` 与 `release` 两个客户端,互不覆盖、数据不共享(渠道与安装身份合同见《AGC客户端更新检查与下载》)。发布后核对发布对象名(如 `agc/release-win/<版本>/陶泥儿 Release_<版本>_x64-setup.exe`)与装机后的卸载项、`%APPDATA%\` 是否按渠道分开。 + `Genarrative-Agc-Windows-Build` 的 `Tauri NSIS toolchain` 阶段必须在 Rust 编译前预置 NSIS 工具链并失败关闭:tauri-bundler 打包时现场从 GitHub 下载 `nsis-3.11.zip` 与 `nsis_tauri_utils.dll` 且不重试,构建机每次检出都会重下,响应一旦被截断就只能抛 `io: unexpected end of file`,让发布在编译数分钟后才失败。该阶段先跑 `node apps/ai-game-creator-shell/scripts/ensure-nsis-toolset.mjs`(固定 SHA1 校验、4 次重试、解压到 `target/.tauri/NSIS`),再执行 `makensis.exe -VERSION` 验证可执行性;Checkout 的 `git clean -fdx` 必须带 `-e apps/ai-game-creator-shell/src-tauri/target/.tauri`,只保留这份工具缓存、其余 `target/` 内容照常清空,否则工作区内缓存会被每个构建删掉,退回到「每次从 GitHub 重下」(实测裸 `git clean -fdx` 会输出 `Would remove apps/ai-game-creator-shell/src-tauri/target/`);原始归档缓存在工作区外的 `%ProgramData%\genarrative\tauri-nsis-cache`(可用 `AGC_TAURI_NSIS_CACHE_DIR` 覆盖),因此同一节点只有冷缓存才需要联网,离线补缓存时把这两个文件放进缓存目录即可;构建机确实无法访问 GitHub 时使用 bundler 自带的 `TAURI_BUNDLER_TOOLS_GITHUB_MIRROR_TEMPLATE` / `TAURI_BUNDLER_TOOLS_GITHUB_MIRROR` 指向可达镜像。升级 `@tauri-apps/cli` 时必须同步核对 `nsis-toolset.mjs` 里的归档地址、SHA1 与必需文件清单(与 tauri-bundler 的 `NSIS_REQUIRED_FILES` 逐条对齐),否则预置会被 bundler 判为不完整。 调度状态是调度 Job 工作区里的 `.jenkins-last-triggered-revision`,构建描述同时回显本次 revision 与结果。工作区被清理(例如 `Wipe Out Workspace`)或状态文件缺失时,下一次运行按“版本变化”处理并触发一次,之后恢复稳定;需要重建同一版本时勾选 `FORCE_TRIGGER`。Job 按仓库内 `jenkins/scheduled-revision-trigger-job-config.xml` 创建:`scriptPath=jenkins/Jenkinsfile.scheduled-revision-trigger`、Git 入口 `ssh://git@127.0.0.1:2222/GenarrativeAI/Genarrative.git`、凭据 `genarrative-local-gitea-ssh`、`` 留空(定时器写在 Jenkinsfile 里)。推送后必须让三个 live Job 各自加载一次新 Jenkinsfile,并只读核对 `config.xml`:Full 与 AGC 不再有 cron,定时只来自新调度 Job;只改 Jenkinsfile 而不确认 live 配置时,旧 cron 仍会继续触发。 @@ -744,7 +746,7 @@ Mac 单架构(arm64)构建脚本位于 `jenkins/Jenkinsfile.ai-game-creator- Job 名为 `Genarrative-Agc-MacOS-Build`,SCM 直接读取仓库内上述 Jenkinsfile,参数为 `SOURCE_BRANCH`、`COMMIT_HASH`、`AGC_UPDATE_CHANNEL`、`AGC_RELEASE_VERSION`、`AGC_RELEASE_DRY_RUN`、`AGC_UPDATE_RELEASE_NOTES`、`OSSUTIL_BIN`、`CARGO_BUILD_JOBS`、`NOTIFICATION_EMAILS`。渠道参数是基础名(不含系统,默认 `dev`),脚本不接受 `dev-mac` 这类系统后缀,写入分区固定推导为 `-mac`:这与 Windows Job 的 `-win` 对称,也延续已发布客户端的端点。 -该 Job 的职责是构建并发布 `-mac` 分区更新:执行 `npm ci` 后使用锁文件校验并补齐两种 macOS Codex 原生依赖,再调用 `scripts/build-macos-ci.mjs`(AGC 应用目录下)生成 arm64 单架构 app、arm64 隔离 smoke、arm64 DMG(`<产品名>_<版本>_aarch64.dmg`)与分区清单 `latest.json`,用产物内烘焙的公钥复核更新包签名(`verify-updater-signature.mjs`),最后按 `AGC_RELEASE_DRY_RUN` 决定是否上传 OSS。签到会同时取 `master`,让渠道清单里上一次发布的 commit 可解析——缺了它更新摘要会退化成「最近客户端改动」(该步失败只降级摘要,不阻断发布)。产物名(`*.app`、updater 归档、DMG、卷名)一律从 Tauri `productName` 推导,校验脚本从包内 `Info.plist` 读取可执行名,改产品名不会让入口静默找错对象;隔离 smoke 用 `ditto --clone` 复制副本(实测整轮 6.8 秒,此前整包复制约 1 分钟),并在构建前删除本次将写出的 DMG/更新包/签名,保证归档产物一定来自本次构建。归档限 `artifacts/` 下的 DMG、SHA-256、`latest.json`、更新包签名、更新摘要、非敏感构建清单和源码 commit;不归档用户 HOME、Jenkins secret、原始工作目录或全量日志。 +该 Job 的职责是构建并发布 `-mac` 分区更新:执行 `npm ci` 后使用锁文件校验并补齐两种 macOS Codex 原生依赖,再调用 `scripts/build-macos-ci.mjs`(AGC 应用目录下)生成 arm64 单架构 app、arm64 隔离 smoke、arm64 DMG(`<产品名>_<版本>_aarch64.dmg`)与分区清单 `latest.json`,用产物内烘焙的公钥复核更新包签名(`verify-updater-signature.mjs`),最后按 `AGC_RELEASE_DRY_RUN` 决定是否上传 OSS。签到会同时取 `master`,让渠道清单里上一次发布的 commit 可解析——缺了它更新摘要会退化成「最近客户端改动」(该步失败只降级摘要,不阻断发布)。产物名(`*.app`、updater 归档、DMG、卷名)一律从渠道安装身份派生的产品名推导(默认渠道即基线 `productName`,`release` 等渠道带渠道后缀),校验脚本从包内 `Info.plist` 读取可执行名,改产品名或换渠道都不会让入口静默找错对象;隔离 smoke 用 `ditto --clone` 复制副本(实测整轮 6.8 秒,此前整包复制约 1 分钟),并在构建前删除本次将写出的 DMG/更新包/签名,保证归档产物一定来自本次构建。归档限 `artifacts/` 下的 DMG、SHA-256、`latest.json`、更新包签名、更新摘要、非敏感构建清单和源码 commit;不归档用户 HOME、Jenkins secret、原始工作目录或全量日志。 发布凭据全部走 Jenkins 全局凭据,并在 `withCredentials` 内注入当前进程:`AgcUpdaterSigningKey`(与 `AgcUpdaterSigningKeyPassword`)映射为 `TAURI_SIGNING_PRIVATE_KEY` / `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`,`AliyunAccessKeyId` / `AliyunaccessKeySecret` 映射为 `AGC_OSS_ACCESS_KEY_ID` / `AGC_OSS_ACCESS_KEY_SECRET`;私钥与凭据不写入 workspace、日志或归档产物。上传顺序为更新包、签名、首装包,三者全部成功后才覆盖 `agc/-mac/latest.json` 指针;`AGC_RELEASE_DRY_RUN` 与 Windows 渠道对称:默认关闭即真发布,勾选后才退化为演练(只打印将上传的对象、不写任何 OSS 对象)。本 Job 是正式发布入口,调度器在发号后与 Windows 一起触发它,并额外传 `SKIP_IF_SUPERSEDED=true`——Mac 节点是日常办公机,离线期间排队的旧构建在节点回来后若已被源码分支推进,直接跳过而不发布过期版本。Mac 节点需要 `ossutil`(实测 1.7.19 原生 arm64 可用,装在 `~/.local/bin`,已在 Job 的 PATH 内),可用 `OSSUTIL_BIN` 指定命令名或绝对路径。首次发布建议显式指定 `AGC_RELEASE_VERSION`,避免按渠道高水位递增时出现版本链回退。