diff --git a/.gitignore b/.gitignore index 34e0fbde7..10390a82e 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,8 @@ temp*build*/ /apps/ai-game-creator-shell/src-tauri/resources/codex/mac-native/codex-package.json /apps/ai-game-creator-shell/src-tauri/resources/codex/mac-native/manifest.json /apps/ai-game-creator-shell/src-tauri/resources/codex/mac-native/NOTICE.md +/apps/ai-game-creator-shell/src-tauri/resources/codex/mac-native/darwin-arm64/ +/apps/ai-game-creator-shell/src-tauri/resources/codex/mac-native/darwin-x64/ /plugins/agc-cocos-editor/native/payload/ /apps/ai-game-creator-shell/logs/ /apps/ai-game-creator-shell/.llm-drafts/ diff --git a/apps/ai-game-creator-shell/scripts/build-macos-ci.mjs b/apps/ai-game-creator-shell/scripts/build-macos-ci.mjs new file mode 100644 index 000000000..acafcc0fd --- /dev/null +++ b/apps/ai-game-creator-shell/scripts/build-macos-ci.mjs @@ -0,0 +1,113 @@ +import assert from 'node:assert/strict'; +import { execFileSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { resolveReleaseContext, runTauriBuild } from './build-release.mjs'; + +const appRoot = fileURLToPath(new URL('..', import.meta.url)); +const repoRoot = path.resolve(appRoot, '../..'); +assert.equal(process.platform, 'darwin', '只能在 macOS Agent 执行'); +assert.equal( + process.env.JENKINS_URL?.length > 0, + true, + '此入口仅用于 Jenkins 独立工作区', +); +assert.equal( + fs.realpathSync(process.env.WORKSPACE || '.'), + fs.realpathSync(repoRoot), + '必须在 Jenkins workspace 根目录执行', +); +const space = fs.statfsSync(repoRoot); +assert.ok( + space.bavail * space.bsize >= 8 * 1024 ** 3, + '构建前至少需要 8 GiB 可用空间;禁止自动清理开发缓存', +); + +// 本入口永不发布,不使用 Agent 用户可能持有的发布或 Apple 认证环境。 +for (const key of Object.keys(process.env)) { + if (/^(TAURI_SIGNING_|APPLE_|AGC_OSS_)/u.test(key)) delete process.env[key]; +} +process.env.RUSTC_WRAPPER = ''; +process.env.CARGO_TARGET_DIR = path.join(appRoot, 'src-tauri/target'); +const context = resolveReleaseContext(['--target=universal-apple-darwin']); +const args = [ + '--target=universal-apple-darwin', + '--bundles', + 'app', + '--ci', + '--no-sign', + '--config', + '{"bundle":{"createUpdaterArtifacts":false}}', +]; +const command = (binary, argv, options = {}) => + execFileSync(binary, argv, { cwd: repoRoot, stdio: 'inherit', ...options }); +runTauriBuild(args, context); +const app = path.join(context.bundleRoot, 'macos/陶泥儿.app'); +for (const architecture of ['arm64', 'x86_64']) { + command(process.execPath, [ + path.join(appRoot, 'scripts/check-macos-bundle.mjs'), + app, + architecture, + '--universal', + ]); +} +const version = JSON.parse( + fs.readFileSync(path.join(appRoot, 'package.json'), 'utf8'), +).version; +assert.match(version, /^\d+\.\d+\.\d+$/u); +const artifacts = path.join(repoRoot, 'artifacts'); +// 只清理本 Job 的归档输出,不能把上次 DMG 当成本次成功产物。 +fs.rmSync(artifacts, { recursive: true, force: true }); +fs.mkdirSync(artifacts, { recursive: true }); +const dmg = path.join(artifacts, `陶泥儿_${version}_universal.dmg`); +const stage = fs.mkdtempSync(path.join(os.tmpdir(), 'agc-ci-dmg-')); +try { + command('ditto', [app, path.join(stage, '陶泥儿.app')]); + fs.symlinkSync('/Applications', path.join(stage, 'Applications')); + command('hdiutil', [ + 'create', + '-volname', + '陶泥儿', + '-srcfolder', + stage, + '-format', + 'UDZO', + dmg, + ]); + command('hdiutil', ['verify', dmg]); +} finally { + fs.rmSync(stage, { recursive: true, force: true }); +} +const hash = createHash('sha256'); +for await (const chunk of fs.createReadStream(dmg)) hash.update(chunk); +fs.writeFileSync( + `${dmg}.sha256`, + `${hash.digest('hex')} ${path.basename(dmg)}\n`, +); +const commit = execFileSync('git', ['rev-parse', 'HEAD'], { + cwd: repoRoot, + encoding: 'utf8', +}).trim(); +fs.writeFileSync( + path.join(artifacts, 'build-manifest.json'), + `${JSON.stringify( + { + version, + commit, + target: context.target, + channel: context.channel, + signed: false, + notarized: false, + uploaded: false, + smoke: ['arm64', 'x86_64'], + intelSmoke: process.arch === 'arm64' ? 'Rosetta' : 'native', + }, + null, + 2, + )}\n`, +); +console.log('[macOS CI] universal 包与校验文件已生成;未发布、未签名或公证'); diff --git a/apps/ai-game-creator-shell/scripts/build-release.mjs b/apps/ai-game-creator-shell/scripts/build-release.mjs index b1fc307f5..478c58719 100644 --- a/apps/ai-game-creator-shell/scripts/build-release.mjs +++ b/apps/ai-game-creator-shell/scripts/build-release.mjs @@ -42,16 +42,12 @@ function explicitBuildTarget(args) { } function validateReleaseTarget(target) { - if (target === 'universal-apple-darwin') { - throw new Error( - '内置 Codex 资源仅支持 macOS 单架构构建,请使用 aarch64-apple-darwin 或 x86_64-apple-darwin', - ); - } if ( ![ 'x86_64-pc-windows-msvc', 'aarch64-apple-darwin', 'x86_64-apple-darwin', + 'universal-apple-darwin', ].includes(target) ) { throw new Error(`不支持的发布目标:${target}`); @@ -197,10 +193,12 @@ export function updateManifestUrl(channel = resolveReleaseChannel()) { } /** - * 单架构产物只登记实际目标,不能把同一原生资源映射为另一架构。 + * universal 主程序与双目录原生资源共用一个更新包;单架构只登记实际目标。 */ export function resolveManifestPlatformKeys(target = defaultTarget()) { validateReleaseTarget(target); + if (target === 'universal-apple-darwin') + return ['darwin-aarch64', 'darwin-x86_64']; if (target === 'aarch64-apple-darwin') return ['darwin-aarch64']; if (target === 'x86_64-apple-darwin') return ['darwin-x86_64']; if (target.includes('windows')) { 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 8cb9c0214..0596cefe9 100644 --- a/apps/ai-game-creator-shell/scripts/build-release.test.mjs +++ b/apps/ai-game-creator-shell/scripts/build-release.test.mjs @@ -39,13 +39,12 @@ import { const windowsTarget = 'x86_64-pc-windows-msvc'; const universalTarget = 'universal-apple-darwin'; -test('native sidecar builds reject universal targets and accept each macOS architecture', () => { - assert.throws(() => buildTauriBuildArguments([], universalTarget), /单架构/); - assert.throws( - () => buildTauriBuildArguments(['--target=universal-apple-darwin']), - /单架构/, - ); - for (const target of ['aarch64-apple-darwin', 'x86_64-apple-darwin']) { +test('native sidecar builds accept universal and each macOS architecture', () => { + for (const target of [ + universalTarget, + 'aarch64-apple-darwin', + 'x86_64-apple-darwin', + ]) { assert.deepEqual(buildTauriBuildArguments([], target), [ 'build', '--target', @@ -152,8 +151,11 @@ test('channel manifest URL and build-time endpoint follow the channel', () => { }); }); -test('macOS manifests only advertise the architecture actually built', () => { - assert.throws(() => resolveManifestPlatformKeys(universalTarget), /单架构/); +test('macOS manifests advertise exactly the architectures actually built', () => { + assert.deepEqual(resolveManifestPlatformKeys(universalTarget), [ + 'darwin-aarch64', + 'darwin-x86_64', + ]); assert.deepEqual(resolveManifestPlatformKeys('aarch64-apple-darwin'), [ 'darwin-aarch64', ]); @@ -197,7 +199,6 @@ test('release context resolves explicit targets before environment/default and f ['--target='], ['--target', '--no-bundle'], ['--target', windowsTarget, '--target=aarch64-apple-darwin'], - ['--target', universalTarget], ['--target', 'unknown'], ]) assert.throws(() => resolveReleaseContext(args, {})); @@ -314,8 +315,8 @@ test('invalid target or mismatched channel fails before any release side effect' }, }; await assert.rejects( - () => buildRelease(['--target', universalTarget], sideEffects), - /单架构/, + () => buildRelease(['--target', 'unknown'], sideEffects), + /不支持的发布目标/, ); await withEnv({ AGC_UPDATE_CHANNEL: 'dev-win' }, () => assert.rejects( @@ -326,6 +327,26 @@ test('invalid target or mismatched channel fails before any release side effect' assert.equal(touched, false); }); +test('universal uses the Mac channel and the same signed artifact for both architectures', () => { + const context = resolveReleaseContext(['--target', universalTarget], { + AGC_BUILD_TARGET: windowsTarget, + }); + assert.equal(context.channel, 'dev-mac'); + assert.ok(context.bundleRoot.includes(universalTarget)); + withSignedArtifact('陶泥儿.app.tar.gz', (artifact) => { + const manifest = createUpdateManifest(artifact, context); + assert.deepEqual(Object.keys(manifest.platforms), [ + 'darwin-aarch64', + 'darwin-x86_64', + ]); + assert.deepEqual( + manifest.platforms['darwin-aarch64'], + manifest.platforms['darwin-x86_64'], + ); + assert.match(manifest.platforms['darwin-aarch64'].url, /\/dev-mac\//); + }); +}); + test('Windows remains the default and explicit Windows overrides macOS environment', () => { const files = ['/tmp/mac.app.tar.gz', '/tmp/windows.exe', '/tmp/mac.dmg']; for (const context of [ diff --git a/apps/ai-game-creator-shell/scripts/check-config.mjs b/apps/ai-game-creator-shell/scripts/check-config.mjs index fe8943608..4fc8910a9 100644 --- a/apps/ai-game-creator-shell/scripts/check-config.mjs +++ b/apps/ai-game-creator-shell/scripts/check-config.mjs @@ -1367,18 +1367,20 @@ if (windowsTauriConfig.bundle?.useLocalToolsDir !== true) { assert.deepEqual( macosTauriConfig.bundle?.resources, Object.fromEntries([ - ...[ - 'bin/codex', - 'bin/codex-code-mode-host', - 'codex-path/rg', - 'codex-resources/zsh/bin/zsh', - 'codex-package.json', - 'NOTICE.md', - 'manifest.json', - ].map((file) => [ - `resources/codex/mac-native/${file}`, - `coding-agent/mac-native/${file}`, - ]), + ...['darwin-arm64', 'darwin-x64'].flatMap((arch) => + [ + 'bin/codex', + 'bin/codex-code-mode-host', + 'codex-path/rg', + 'codex-resources/zsh/bin/zsh', + 'codex-package.json', + 'NOTICE.md', + 'manifest.json', + ].map((file) => [ + `resources/codex/mac-native/${arch}/${file}`, + `coding-agent/mac-native/${arch}/${file}`, + ]), + ), ['resources/plugins', 'plugins'], ]), 'macOS must bundle the complete native Codex layout and plugin workspace', diff --git a/apps/ai-game-creator-shell/scripts/check-macos-bundle.mjs b/apps/ai-game-creator-shell/scripts/check-macos-bundle.mjs index 78e38fdd9..1466a7661 100644 --- a/apps/ai-game-creator-shell/scripts/check-macos-bundle.mjs +++ b/apps/ai-game-creator-shell/scripts/check-macos-bundle.mjs @@ -8,6 +8,13 @@ import path from 'node:path'; // 只操作临时复制品;不启动 GUI、不读取开发机凭据、不访问 Provider。 assert.equal(process.platform, 'darwin', '此验证必须在 macOS 执行'); const source = path.resolve(process.argv[2] || ''); +const architecture = + process.argv[3] || (process.arch === 'arm64' ? 'arm64' : 'x86_64'); +assert.ok( + ['arm64', 'x86_64'].includes(architecture), + '架构只接受 arm64 / x86_64', +); +const requireUniversal = process.argv.includes('--universal'); assert.ok( source.endsWith('.app') && fs.statSync(source).isDirectory(), '请传入 .app 绝对路径', @@ -31,13 +38,19 @@ const env = { }; function run(command, args) { - const result = spawnSync(command, args, { - cwd: root, - env, - encoding: 'utf8', - timeout: 30_000, - maxBuffer: 1024 * 1024, - }); + // 只强制被测应用切片;本机 Xcode 检查工具可能仅提供宿主架构。 + const useSlice = command.startsWith(`${app}${path.sep}`); + const result = spawnSync( + useSlice ? '/usr/bin/arch' : command, + useSlice ? [`-${architecture}`, command, ...args] : args, + { + cwd: root, + env, + encoding: 'utf8', + timeout: 120_000, + maxBuffer: 1024 * 1024, + }, + ); assert.ifError(result.error); return result; } @@ -60,7 +73,7 @@ async function handshake(executable) { await new Promise((resolve, reject) => { const timer = setTimeout( () => reject(new Error('app-server 初始化超时')), - 15_000, + 120_000, ); const finish = (error) => { clearTimeout(timer); @@ -129,20 +142,39 @@ async function handshake(executable) { try { fs.cpSync(source, app, { recursive: true }); const resources = path.join(app, 'Contents/Resources'); - const bundle = path.join(resources, 'coding-agent/mac-native'); + const platform = architecture === 'arm64' ? 'darwin-arm64' : 'darwin-x64'; + const bundle = path.join(resources, 'coding-agent/mac-native', platform); const executable = path.join(bundle, 'bin/codex'); const main = path.join( app, 'Contents/MacOS/genarrative-ai-game-creator-shell', ); + const mainArchitectures = run('/usr/bin/lipo', ['-archs', main]); + assert.equal(mainArchitectures.status, 0); + assert.ok(mainArchitectures.stdout.split(/\s+/).includes(architecture)); + if (requireUniversal) { + assert.deepEqual(mainArchitectures.stdout.trim().split(/\s+/).sort(), [ + 'arm64', + 'x86_64', + ]); + for (const platform of ['darwin-arm64', 'darwin-x64']) { + assert.ok( + fs.existsSync( + path.join( + resources, + 'coding-agent/mac-native', + platform, + 'manifest.json', + ), + ), + ); + } + } const manifest = JSON.parse( fs.readFileSync(path.join(bundle, 'manifest.json'), 'utf8'), ); assert.equal(manifest.schemaVersion, 'genarrative-codex-sidecar.v2'); - assert.equal( - manifest.platform, - process.arch === 'arm64' ? 'darwin-arm64' : 'darwin-x64', - ); + assert.equal(manifest.platform, platform); assert.equal(manifest.version, 'codex-cli 0.147.0'); const components = [ 'bin/codex', @@ -159,11 +191,7 @@ try { fs.accessSync(file, fs.constants.X_OK); const arch = run('/usr/bin/lipo', ['-archs', file]); assert.equal(arch.status, 0, component); - assert.equal( - arch.stdout.trim(), - process.arch === 'arm64' ? 'arm64' : 'x86_64', - component, - ); + assert.equal(arch.stdout.trim(), architecture, component); } } assert.ok(fs.existsSync(path.join(bundle, 'NOTICE.md'))); @@ -212,7 +240,7 @@ try { assert.notEqual(broken.status, 0); assert.match(`${broken.stdout}\n${broken.stderr}`, /Codex CLI 未安装/); console.log( - 'PASS: 隔离安装包资源、架构、摘要、权限、正式 Codex 查找、app-server 握手及缺组件拒绝', + `PASS (${architecture}): 隔离安装包资源、架构、摘要、权限、正式 Codex 查找、app-server 握手及缺组件拒绝`, ); console.log( '未验证:GUI、真实登录/Provider 对话、Cocos macOS 原生桥接;插件 Node 仍为外部前提', diff --git a/apps/ai-game-creator-shell/scripts/prepare-macos-codex.mjs b/apps/ai-game-creator-shell/scripts/prepare-macos-codex.mjs new file mode 100644 index 000000000..6335fda74 --- /dev/null +++ b/apps/ai-game-creator-shell/scripts/prepare-macos-codex.mjs @@ -0,0 +1,134 @@ +import assert from 'node:assert/strict'; +import { execFileSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const appRoot = fileURLToPath(new URL('..', import.meta.url)); +const repoRoot = path.resolve(appRoot, '../..'); +const platforms = { + arm64: 'aarch64-apple-darwin', + x64: 'x86_64-apple-darwin', +}; + +export function lockedMacPackage(lock, arch, version) { + assert.ok(Object.hasOwn(platforms, arch), '未知 macOS 架构'); + const alias = `@openai/codex-darwin-${arch}`; + const entry = lock.packages?.[`node_modules/${alias}`]; + assert.equal( + entry?.version, + `${version}-darwin-${arch}`, + '原生依赖必须与应用锁定版本一致', + ); + assert.deepEqual(entry.os, ['darwin']); + assert.deepEqual(entry.cpu, [arch]); + const url = new URL(entry.resolved); + assert.equal(url.protocol, 'https:'); + assert.equal( + url.hostname, + 'registry.npmjs.org', + '只下载锁定的官方 npm 原生包', + ); + assert.equal(url.username + url.password + url.search + url.hash, ''); + assert.match(entry.integrity, /^sha512-[A-Za-z0-9+/]+={0,2}$/); + return { alias, target: platforms[arch], ...entry }; +} + +export function verifyPackageIntegrity(bytes, expected) { + const actual = `sha512-${createHash('sha512').update(bytes).digest('base64')}`; + assert.equal(actual, expected, 'Codex 下载包 lockfile integrity 不匹配'); +} + +export function validateArchiveListing(listing) { + const files = listing.trim().split(/\r?\n/u); + assert.ok(files.length > 0); + for (const file of files) { + assert.ok(file.startsWith('package/'), '原生包必须只有 package 根目录'); + assert.ok( + !file.split('/').includes('..') && !file.includes('\\'), + '压缩包路径不安全', + ); + } +} + +export async function prepareMacosCodex() { + assert.equal(process.platform, 'darwin', '该入口仅用于 macOS 构建机'); + const lock = JSON.parse( + fs.readFileSync(path.join(repoRoot, 'package-lock.json'), 'utf8'), + ); + const app = JSON.parse( + fs.readFileSync(path.join(appRoot, 'package.json'), 'utf8'), + ); + const version = app.devDependencies['@openai/codex']; + assert.match(version, /^\d+\.\d+\.\d+$/u, 'Codex 必须锁定精确版本'); + const cache = path.join(appRoot, 'src-tauri/target/.macos-native-cache'); + fs.mkdirSync(cache, { recursive: true }); + for (const arch of Object.keys(platforms)) { + const entry = lockedMacPackage(lock, arch, version); + const archive = path.join(cache, `codex-${entry.version}.tgz`); + if (!fs.existsSync(archive)) { + const response = await fetch(entry.resolved, { + signal: AbortSignal.timeout(300_000), + }); + assert.ok(response.ok, `原生包下载失败 HTTP ${response.status}`); + const bytes = Buffer.from(await response.arrayBuffer()); + verifyPackageIntegrity(bytes, entry.integrity); + const partial = `${archive}.${process.pid}.tmp`; + fs.writeFileSync(partial, bytes); + fs.renameSync(partial, archive); + } + verifyPackageIntegrity(fs.readFileSync(archive), entry.integrity); + validateArchiveListing( + execFileSync('tar', ['-tzf', archive], { encoding: 'utf8' }), + ); + // 拒绝链接、设备及其它特殊条目,不能让 tar 在包目录之外写入。 + const entries = execFileSync('tar', ['-tvzf', archive], { + encoding: 'utf8', + }); + assert.ok( + entries + .trim() + .split(/\r?\n/u) + .every((line) => /^[-d]/u.test(line)), + '原生包禁止链接或特殊文件', + ); + const parent = path.join(repoRoot, 'node_modules/@openai'); + fs.mkdirSync(parent, { recursive: true }); + const stage = fs.mkdtempSync(path.join(parent, '.mac-native-')); + try { + execFileSync( + 'tar', + ['-xzf', archive, '-C', stage, '--strip-components=1'], + { stdio: 'pipe' }, + ); + const metadata = JSON.parse( + fs.readFileSync( + path.join(stage, 'vendor', entry.target, 'codex-package.json'), + 'utf8', + ), + ); + assert.equal(metadata.version, version); + assert.equal(metadata.target, entry.target); + assert.equal(metadata.entrypoint, 'bin/codex'); + const destination = path.join(repoRoot, 'node_modules', entry.alias); + assert.ok( + !fs.existsSync(destination) || + !fs.lstatSync(destination).isSymbolicLink(), + '拒绝覆盖链接依赖', + ); + fs.rmSync(destination, { recursive: true, force: true }); + fs.renameSync(stage, destination); + } finally { + fs.rmSync(stage, { recursive: true, force: true }); + } + console.log(`[macOS Codex] ${entry.version}: lockfile integrity 已验证`); + } +} + +if ( + process.argv[1] && + path.resolve(process.argv[1]) === fileURLToPath(import.meta.url) +) { + await prepareMacosCodex(); +} 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 new file mode 100644 index 000000000..2fd93ddf1 --- /dev/null +++ b/apps/ai-game-creator-shell/scripts/prepare-macos-codex.test.mjs @@ -0,0 +1,85 @@ +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import fs from 'node:fs'; +import { test } from 'node:test'; + +import { + lockedMacPackage, + validateArchiveListing, + verifyPackageIntegrity, +} from './prepare-macos-codex.mjs'; + +const lock = JSON.parse( + fs.readFileSync(new URL('../../../package-lock.json', import.meta.url)), +); +const version = JSON.parse( + fs.readFileSync(new URL('../package.json', import.meta.url)), +).devDependencies['@openai/codex']; + +test('both macOS dependencies resolve from the lockfile without floating versions', () => { + assert.equal( + lockedMacPackage(lock, 'arm64', version).target, + 'aarch64-apple-darwin', + ); + assert.equal( + lockedMacPackage(lock, 'x64', version).target, + 'x86_64-apple-darwin', + ); + assert.throws(() => lockedMacPackage(lock, 'other', version)); + assert.throws(() => lockedMacPackage(lock, 'x64', '0.0.0')); +}); + +test('native package integrity rejects tampering', () => { + const bytes = Buffer.from('pinned package'); + const integrity = `sha512-${createHash('sha512').update(bytes).digest('base64')}`; + verifyPackageIntegrity(bytes, integrity); + assert.throws(() => + verifyPackageIntegrity(Buffer.from('modified'), integrity), + ); +}); + +test('archive traversal and non-package entries fail closed', () => { + validateArchiveListing( + 'package/package.json\npackage/vendor/target/bin/codex\n', + ); + for (const listing of [ + '', + '/tmp/payload', + 'package/../private', + 'other/file', + 'package/..\\file', + ]) { + assert.throws(() => validateArchiveListing(listing)); + } +}); + +test('CI pipeline is manual archive-only and does not reuse a developer workspace', () => { + const pipeline = fs.readFileSync( + new URL( + '../../../jenkins/Jenkinsfile.ai-game-creator-shell-macos-build', + import.meta.url, + ), + 'utf8', + ); + for (const required of [ + 'genarrative-agc-macos', + 'disableConcurrentBuilds()', + '$AGC_AGENT_ROOT', + 'StrictHostKeyChecking=yes', + 'git merge-base --is-ancestor', + 'allowEmptyArchive: false', + ]) { + assert.ok(pipeline.includes(required), required); + } + for (const forbidden of [ + 'triggers {', + 'cron(', + 'pollSCM(', + 'release:upload', + 'AgcUpdaterSigningKey', + 'AliyunAccessKeyId', + 'git clean -fdx', + ]) { + assert.ok(!pipeline.includes(forbidden), forbidden); + } +}); diff --git a/apps/ai-game-creator-shell/src-tauri/build.rs b/apps/ai-game-creator-shell/src-tauri/build.rs index 6bca6d67a..ecd11f895 100644 --- a/apps/ai-game-creator-shell/src-tauri/build.rs +++ b/apps/ai-game-creator-shell/src-tauri/build.rs @@ -31,7 +31,23 @@ fn sha256_file(path: &std::path::Path) -> Result { fn stage_bundled_codex_cli(manifest_dir: &std::path::Path) { let target = env::var("TARGET").expect("Cargo TARGET"); println!("cargo:rustc-env=AGC_BUILD_TARGET={target}"); - let Some(layout) = codex_bundle::for_target(&target) else { + if target.contains("apple-darwin") { + // Tauri 的 universal 两次 Cargo 编译共用 resource staging, + // 每次都生成完整双架构目录,最终 bundle 不取决于最后编译的切片。 + let staging = manifest_dir.join("resources/codex/mac-native"); + if staging.exists() { + fs::remove_dir_all(&staging).expect("清理 macOS Codex staging 失败"); + } + for target in ["aarch64-apple-darwin", "x86_64-apple-darwin"] { + stage_codex_target(manifest_dir, target); + } + } else { + stage_codex_target(manifest_dir, &target); + } +} + +fn stage_codex_target(manifest_dir: &std::path::Path, target: &str) { + let Some(layout) = codex_bundle::for_target(target) else { assert!( !target.contains("windows") && !target.contains("apple-darwin"), "不支持的 Codex 随包目标:{target}" @@ -81,7 +97,7 @@ fn stage_bundled_codex_cli(manifest_dir: &std::path::Path) { &fs::read(source.join("codex-package.json")).expect("读取 Codex 原生包元数据失败"), ) .expect("Codex 原生包元数据无效"); - codex_bundle::validate_package_metadata(&metadata, &target, layout) + codex_bundle::validate_package_metadata(&metadata, target, layout) .unwrap_or_else(|error| panic!("{error}")); let target_dir = manifest_dir.join("resources/codex").join(layout.directory); let notice = target_dir.join("NOTICE.md"); diff --git a/apps/ai-game-creator-shell/src-tauri/build_support/codex_bundle.rs b/apps/ai-game-creator-shell/src-tauri/build_support/codex_bundle.rs index 1811f6a25..8abb36294 100644 --- a/apps/ai-game-creator-shell/src-tauri/build_support/codex_bundle.rs +++ b/apps/ai-game-creator-shell/src-tauri/build_support/codex_bundle.rs @@ -49,7 +49,11 @@ pub fn for_target(target: &str) -> Option { } else { "codex-darwin-x64" }, - directory: "mac-native", + directory: if target.starts_with("aarch64") { + "mac-native/darwin-arm64" + } else { + "mac-native/darwin-x64" + }, executable: "bin/codex", files: MAC_FILES, }), @@ -90,6 +94,9 @@ mod tests { let intel = for_target("x86_64-apple-darwin").unwrap(); assert_eq!(intel.platform, "darwin-x64"); assert_eq!(intel.npm_package, "codex-darwin-x64"); + assert_eq!(mac.directory, "mac-native/darwin-arm64"); + assert_eq!(intel.directory, "mac-native/darwin-x64"); + assert_ne!(mac.directory, intel.directory); let windows = for_target("x86_64-pc-windows-msvc").unwrap(); assert_eq!(windows.directory, "win-x64"); assert_eq!(windows.files.len(), 6); diff --git a/apps/ai-game-creator-shell/src-tauri/tauri.macos.conf.json b/apps/ai-game-creator-shell/src-tauri/tauri.macos.conf.json index 67695ad22..019a0f248 100644 --- a/apps/ai-game-creator-shell/src-tauri/tauri.macos.conf.json +++ b/apps/ai-game-creator-shell/src-tauri/tauri.macos.conf.json @@ -5,13 +5,20 @@ "minimumSystemVersion": "15.0" }, "resources": { - "resources/codex/mac-native/bin/codex": "coding-agent/mac-native/bin/codex", - "resources/codex/mac-native/bin/codex-code-mode-host": "coding-agent/mac-native/bin/codex-code-mode-host", - "resources/codex/mac-native/codex-path/rg": "coding-agent/mac-native/codex-path/rg", - "resources/codex/mac-native/codex-resources/zsh/bin/zsh": "coding-agent/mac-native/codex-resources/zsh/bin/zsh", - "resources/codex/mac-native/codex-package.json": "coding-agent/mac-native/codex-package.json", - "resources/codex/mac-native/NOTICE.md": "coding-agent/mac-native/NOTICE.md", - "resources/codex/mac-native/manifest.json": "coding-agent/mac-native/manifest.json", + "resources/codex/mac-native/darwin-arm64/bin/codex": "coding-agent/mac-native/darwin-arm64/bin/codex", + "resources/codex/mac-native/darwin-arm64/bin/codex-code-mode-host": "coding-agent/mac-native/darwin-arm64/bin/codex-code-mode-host", + "resources/codex/mac-native/darwin-arm64/codex-path/rg": "coding-agent/mac-native/darwin-arm64/codex-path/rg", + "resources/codex/mac-native/darwin-arm64/codex-resources/zsh/bin/zsh": "coding-agent/mac-native/darwin-arm64/codex-resources/zsh/bin/zsh", + "resources/codex/mac-native/darwin-arm64/codex-package.json": "coding-agent/mac-native/darwin-arm64/codex-package.json", + "resources/codex/mac-native/darwin-arm64/NOTICE.md": "coding-agent/mac-native/darwin-arm64/NOTICE.md", + "resources/codex/mac-native/darwin-arm64/manifest.json": "coding-agent/mac-native/darwin-arm64/manifest.json", + "resources/codex/mac-native/darwin-x64/bin/codex": "coding-agent/mac-native/darwin-x64/bin/codex", + "resources/codex/mac-native/darwin-x64/bin/codex-code-mode-host": "coding-agent/mac-native/darwin-x64/bin/codex-code-mode-host", + "resources/codex/mac-native/darwin-x64/codex-path/rg": "coding-agent/mac-native/darwin-x64/codex-path/rg", + "resources/codex/mac-native/darwin-x64/codex-resources/zsh/bin/zsh": "coding-agent/mac-native/darwin-x64/codex-resources/zsh/bin/zsh", + "resources/codex/mac-native/darwin-x64/codex-package.json": "coding-agent/mac-native/darwin-x64/codex-package.json", + "resources/codex/mac-native/darwin-x64/NOTICE.md": "coding-agent/mac-native/darwin-x64/NOTICE.md", + "resources/codex/mac-native/darwin-x64/manifest.json": "coding-agent/mac-native/darwin-x64/manifest.json", "resources/plugins": "plugins" } }, diff --git a/docs/project-memory/plans/【实施计划】Mac本机构建节点接入-2026-09-18.md b/docs/project-memory/plans/【实施计划】Mac本机构建节点接入-2026-09-18.md new file mode 100644 index 000000000..769673d6b --- /dev/null +++ b/docs/project-memory/plans/【实施计划】Mac本机构建节点接入-2026-09-18.md @@ -0,0 +1,14 @@ +# Mac 本机构建节点接入实施计划 + +- Version: 1 +- Status: preparing-network-required +- Date: 2026-09-18 +- Parent Spec: `【里程碑】Mac本机构建节点接入-2026-09-18.md` + +1. 本地准备 universal 依赖预备、CI 打包脚本和 Jenkinsfile,运行离线单测、配置门禁及编码检查。 +2. 恢复内网后只读核对 Jenkins 版本、既有节点/Job、Git 凭据标识与插件;已存在对象优先核对,不重复创建。 +3. 从同一控制器取得 agent.jar,以当前已有 Java 21 运行专用 inbound Agent;secret 保存在用户私有目录,LaunchAgent 参数只引用 secret 文件,不保存控制器 API Token。 +4. 建立单 executor、EXCLUSIVE 的专用 Mac 节点与手动 archive-only Job。源码必须是可追溯 Git 提交,未推送改动需另行确认源码交付方式,不能默认推送。 +5. 初次构建核对独立工作目录、空间、目标与依赖;在 Jenkins 实际 SUCCESS 后确认 DMG、SHA-256 和两架构 smoke 证据。 + +失败边界:网络不可达不启动重试服务;节点/Job 修改前保留原配置;不更改其它节点、Job 或调度。禁止把 CLI 日志、认证文件和私有路径提交 Git。未获得真实 SUCCESS 前保留活动计划。 diff --git a/docs/project-memory/plans/【实施计划】Mac通用安装包-2026-09-18.md b/docs/project-memory/plans/【实施计划】Mac通用安装包-2026-09-18.md new file mode 100644 index 000000000..9babc8f89 --- /dev/null +++ b/docs/project-memory/plans/【实施计划】Mac通用安装包-2026-09-18.md @@ -0,0 +1,16 @@ +# Mac 通用安装包实施计划 + +- Version: 1 +- Status: awaiting-user-acceptance +- Date: 2026-09-18 +- Parent Spec: `【里程碑】Mac通用安装包与构建管线-2026-09-18.md` + +1. 等待当前 Intel 编译结束,避免共用 staging 并发写。 +2. macOS build.rs 按白名单分别 stage 两套原生资源;共享 Layout 使用架构子目录,运行时原有 hash/版本校验不变。 +3. Tauri macOS 映射双目录,发布入口接受 universal 并生成两个清单键;更新配置门禁与定向测试。 +4. 安装包 smoke 支持显式选择主程序切片,验证 universal 主程序与该切片对应原生依赖。 +5. 用 Tauri universal 构建 app,分别做 arm64/Rosetta smoke,hdiutil 生成新 universal DMG,校验并交付。 + +不发布、不使用私钥;依赖仅从锁定 npm tarball 下载并对照 lockfile integrity。磁盘不足停止,不擅自删除其它 target/cache。保留单架构 DMG。Jenkins 配置作为后续门禁,不混入本地包构建。 + +本地构建与双架构隔离验证完成,证据见对应里程碑。更新 master 时先停止旧构建,保护并恢复改动后重建;未进行 Jenkins 写操作,待确认 Mac Agent 再制定管线实施计划。 diff --git a/docs/project-memory/plans/【里程碑】Mac本机构建节点接入-2026-09-18.md b/docs/project-memory/plans/【里程碑】Mac本机构建节点接入-2026-09-18.md new file mode 100644 index 000000000..728a3a31d --- /dev/null +++ b/docs/project-memory/plans/【里程碑】Mac本机构建节点接入-2026-09-18.md @@ -0,0 +1,34 @@ +# Mac 本机构建节点接入 + +- Version: 1 +- Status: reviewed +- Date: 2026-09-18 +- Parent Spec: `docs/【开发运维】本地开发验证与生产运维-2026-05-15.md` + +## 交付与边界 + +用户已确认使用当前 Mac,允许配置本机后台 Agent、创建 Jenkins 节点与构建 Job,并触发一次验证构建。复用已有 universal 产物合同与 Windows 管线的仓库访问凭据,仅手动构建和归档,无 OSS 上传、更新签名、Apple 签名/公证或定时调度。远程 Git 推送不在本次确认内。 + +节点仅运行明确匹配 `genarrative-agc-macos` 的可信构建,单 executor;Job 禁止并发。Agent 根、workspace、原生 staging 与 target 均独立于开发 checkout,禁止在开发目录执行 npm ci、git clean/reset 或 Cargo 构建。登录用户 Agent 不等于安全沙箱,Jenkins 管理员与获准运行此 Job 的人必须受信任。登出、休眠或脱离内网将影响节点可用性。 + +## 验收 + +1. 从 Jenkins 当前控制器下载匹配的 agent.jar,Java 21 兼容检查通过,凭据留在仓库外、权限受限,不输出 secret。 +2. 节点实际 online,专用标签、EXCLUSIVE、单 executor;本地 LaunchAgent 可重启、可卸载。 +3. SCM 构建固定源码 commit,依赖从 lockfile 安装,两种原生 Codex 包 integrity 校验通过。 +4. universal Release、两架构 smoke、DMG verify 通过;归档只有安装包、摘要和非敏感来源信息。 +5. 真实 Jenkins build 为 SUCCESS 且归档存在。不以 XML 创建或本地单测冒充远端运行成功。 + +## 初始检查与风险 + +2026-09-18 当前机器具备 Java 21、两种 Rust Apple target 与 Rosetta;剩余磁盘约 11 GiB,独立 checkout 构建前须检查空间,不清理用户缓存。控制器地址连续连接超时,属于网络阶段,尚未使用认证凭据或创建后台服务。必须先恢复内网可达性;不修改本机网络路由或代理规避此限制。 + +## 编码前评审 + +只扩展现有 Jenkinsfile/应用打包脚本,不新增发布系统。用专用工作区且保持不发布,是当前明确授权内的最小闭环。代码未推送前不可把远端 master 当作已具备 universal 双资源实现;首跑源码来源必须明确记录,不能暗用开发工作树。 + +## 当前证据与阻塞 + +Jenkinsfile、锁定双架构依赖预备脚本与 archive-only CI 打包入口已在本地准备。定向 Node 测试 37 项通过,类型/配置、编码、文档索引、定向 ESLint 与 diff 检查通过;未执行完整 Jenkins 构建或在线 Groovy 校验。 + +控制器直连多次超时,经当前代理请求返回 502;Java 21 已存在,无需安装新 JDK。尚未注册节点、创建 Job、保存 Agent secret、安装 LaunchAgent 或触发远端构建。需先恢复内网连接,再按本规范验收真实节点及构建状态;不把本地代码准备描述为已经接入成功。 diff --git a/docs/project-memory/plans/【里程碑】Mac通用安装包与构建管线-2026-09-18.md b/docs/project-memory/plans/【里程碑】Mac通用安装包与构建管线-2026-09-18.md new file mode 100644 index 000000000..5319d072a --- /dev/null +++ b/docs/project-memory/plans/【里程碑】Mac通用安装包与构建管线-2026-09-18.md @@ -0,0 +1,36 @@ +# Mac 通用安装包与构建管线 + +- Version: 1 +- Status: awaiting-user-acceptance +- Date: 2026-09-18 +- Parent Spec: `docs/technical/【技术方案】AGC客户端更新检查与下载-2026-08-31.md` + +## 当前交付门禁 + +当前仅交付 AGC 0.1.67 universal 本地 DMG,版本、Codex 0.147.0 与 macOS 15.0 下限不变。不生成私钥、不签名公证、不发布、不推送。 + +主程序含 arm64/x86_64 两个切片;macOS 资源分 `darwin-arm64` 与 `darwin-x64`,两套原生依赖保持上游相对路径和独立清单。运行切片只读取对应目录。构建必须校验两套锁定包身份、目标、版本与完整性;Windows 路径和资源不变。不合并原生 Codex 二进制,不改写其上游元数据。 + +## 验收 + +- 发布上下文接受 universal,dev-mac 的两个平台键同 URL/签名。 +- 主程序 lipo 两切片,资源检查两套原生架构、摘要和可执行位。 +- Apple Silicon 与 Rosetta 各做隔离 HOME/PATH 的真实 app-server 握手、正式程序查找与缺组件拒绝;Rosetta 不代替 Intel 真机。 +- 生成独立 universal DMG,不覆盖已有单架构 DMG;镜像校验、类型/配置/定向测试与编码检查通过。 + +## 后续管线边界 + +用户要求安装包完成后接入 Jenkins。先只读检查节点与现有 Job;创建/修改 Job、凭据或触发构建前额外确认。只有当前安装包门禁完成后再形成管线实现计划;不把凭据保存进源码、日志、计划或打包资源。 + +## 编码前评审 + +采用分架构资源而非 lipo Codex,避免破坏原生包元数据及 code-mode host、zsh 的资源寻址。macOS 共用配置明确列举两套文件,必须失败关闭,单架构诊断包也携带完整资源。现有 single-arch 选择逻辑改为同架构子目录,不涉及持久化数据迁移。 + +## 验收证据 + +- 同步至 master `9a21690fe`,本地 universal 修改无冲突恢复;未推送。 +- Tauri universal Release 构建通过,主程序 lipo 显示 arm64/x86_64,Info.plist 版本 0.1.67、最低 macOS 15.0。 +- `check-macos-bundle.mjs arm64 --universal` 与 `x86_64 --universal` 均通过:每架构资源摘要、原生身份、执行权限、正式 Codex 选择、隔离 app-server 握手、缺 code-mode host 时拒绝。x86_64 在 Rosetta 执行,尚非 Intel 真机。 +- 发布/feature/上传定向 Node 测试 33 项通过,共享 Codex 布局模块在隔离 Cargo harness 的 2 项测试通过;类型/配置、定向 ESLint、编码/文档和 diff 检查通过。 +- universal `.app` 约 672 MiB,DMG 约 290 MiB;hdiutil 完整性校验通过。未做 GUI、账号/Provider、Intel 真机或签名公证验收。 +- Jenkins 只读检查确认现有节点为 Linux 与 Windows,尚无 macOS Agent;创建管线前需用户指定并授权接入 Mac 节点。本地凭据不进入源码或验证产物。 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index eca895f7d..8e90f0a37 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -1,5 +1,9 @@ # 踩坑与排障记录 +## universal 主程序必须配套双架构原生依赖 + +AGC macOS 主程序可合并为 universal,但 Codex 原生包的 `codex-package.json`、code-mode host 和 zsh 仍有架构身份。两套包应各自保留上游布局与摘要,放入 `coding-agent/mac-native/darwin-arm64/`、`darwin-x64/`,由正在运行的主程序切片选择;不能只把主程序用 lipo 合并后复用最后一次构建的单架构资源。Tauri universal 两次 Cargo 构建共用 staging,每次都必须 stage 完整的两套资源。发布清单两个平台键同 URL/签名,只在 universal 产物上成立;Rosetta 隔离 smoke 不代替 Intel 真机验收。 + ## 生成草稿与异步展示边界必须按身份隔离 非模态生成浮层切换占位时按 draftId 分实例,卸载保留未提交/失败草稿,成功提交不再复活草稿;旧项目占位不存在时丢弃其保存回调。失败重试保留原请求输入和引用身份,引用失效不能静默过滤;修改已绑定输入须明确另起请求,不伪装成原请求重试。 diff --git a/docs/technical/【技术方案】AGC客户端更新检查与下载-2026-08-31.md b/docs/technical/【技术方案】AGC客户端更新检查与下载-2026-08-31.md index 03bd90534..d9cda42ee 100644 --- a/docs/technical/【技术方案】AGC客户端更新检查与下载-2026-08-31.md +++ b/docs/technical/【技术方案】AGC客户端更新检查与下载-2026-08-31.md @@ -75,11 +75,11 @@ | 渠道 | 构建目标 | 清单平台键 | 更新包 | 清单地址 | | --------- | ------------------------ | ---------------------------------------------- | ------------------------ | ------------------------------------ | | `dev-win` | `x86_64-pc-windows-msvc` | `windows-x86_64` | NSIS `.exe` + `.exe.sig` | `/agc/dev-win/latest.json` | -| `dev-mac` | `aarch64-apple-darwin` 或 `x86_64-apple-darwin` | 对应 `darwin-aarch64` 或 `darwin-x86_64` | `*.app.tar.gz` + `.sig` | `/agc/dev-mac/latest.json` | +| `dev-mac` | `universal-apple-darwin` | `darwin-aarch64` + `darwin-x86_64`(同一对象) | `*.app.tar.gz` + `.sig` | `/agc/dev-mac/latest.json` | - 对象布局:清单固定写成 `agc//latest.json`;安装包与签名写成 `agc///` 与 `.sig`。 -- macOS 当前采用单架构包:Apple Silicon 使用 `aarch64-apple-darwin`,Intel 使用 `x86_64-apple-darwin`;每次生成的清单只登记本次实际构建的架构,不把单架构原生 Codex 资源挂到另一架构。`universal-apple-darwin` 在版本读取/写入、构建和清单生成之前拒绝。 -- 渠道清单以实际运行架构为键。两种单架构构建不可轮流覆盖同一个 `latest.json` 并宣称双架构均可更新;当前不实现跨构建合并,Intel 发布需先完成其构建验证与多架构清单发布方案。 +- macOS 正式交付使用 universal 主程序,同时携带分目录的 arm64/x64 原生 Codex 组件;每个组件保持上游单架构布局与独立 SHA-256 清单,运行中的主程序切片只选择同架构目录。不得把两套原生包的元数据或辅助程序混装。 +- universal 更新包的两个平台键指向同一个 `.app.tar.gz` 和签名。单架构构建仅登记本架构供诊断,不轮流覆盖 dev-mac 正式清单;正式双架构发布必须构建 universal。 - 构建期要求:打开 `bundle.createUpdaterArtifacts` 以生成 `.sig`;构建环境提供签名私钥与密码(私钥内容不得入库);公钥写入客户端配置。公钥在首个带更新能力的版本发布后不可更换,更换等于放弃自动更新(只能手动重装)。 - 版本递增按渠道独立进行:发布脚本读取该渠道远端 `latest.json` 的 `version`,与本地版本取较高者递增 patch;两个渠道的版本号互不影响。 - 版本高水位:发布脚本取「渠道清单版本」与「旧协议迁移指针版本」(迁移窗口内)中的较大值再递增。只看渠道清单会在渠道启用初期把版本链改小 —— 2026-09-17 首次渠道发布即把旧指针的 0.1.57 退回 0.1.48,随后以显式 0.1.60 纠偏;迁移窗口结束(旧指针 404)后自动只剩渠道清单,`dev-mac` 不参与旧指针比较。 @@ -106,7 +106,7 @@ | 条款 | 验收方式 | 证据 | | ---------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | 渠道与端点映射、渠道校验 | `node --test apps/ai-game-creator-shell/scripts/build-release.test.mjs` | 通过(默认渠道、错配失败关闭、未知渠道失败关闭) | -| macOS 单架构清单与 universal 拒绝 | 定向发布脚本测试 | 单架构各用对应平台键;拒绝未闭合的 universal 发布 | +| macOS universal 清单与双架构依赖 | 定向发布脚本与安装包验证 | 两个平台键同 URL/签名;两套资源独立校验,真机验收另记 | | 缺签名时失败关闭 | 同上 | 通过 | | 开发态不检查更新 | `vitest run apps/ai-game-creator-shell/tests/appUpdate.test.ts` | 通过(开关关闭时不请求清单) | | 旧自研链路整条删除 | 代码检索无残留命令、事件与白名单条目 | 通过(`download_agc_update` / 下载事件 / 清单常量均无残留) | @@ -128,7 +128,7 @@ 已决策: -- macOS 采用单架构包,只登记实际构建架构;Intel 真机构建与跨架构清单合并未验收,不公开宣称双架构分发就绪。 +- macOS 采用 universal 包,两个平台键对应同一更新产物;主程序用 lipo 检查两种架构,Codex 资源分别做原生身份/摘要与启动验证。Rosetta 结果不代替 Intel 真机验收。 - 旧客户端迁移桥:保留一个版本周期。渠道清单上线后,发布管线同时把旧的 `agc/latest.json`(sha256 格式)指向 `dev-win` 最新安装包,让已发布客户端自动升级到新协议;下个周期整条删除。 - 签名密钥:由本仓库维护者生成并保管,私钥保存在仓库外(`%USERPROFILE%\.tauri\genarrative-agc-updater.key`),只有公钥进入客户端配置;Jenkins 用受保护凭据 `AgcUpdaterSigningKey` 与 `AgcUpdaterSigningKeyPassword` 注入为 Tauri 打包器读取的 `TAURI_SIGNING_PRIVATE_KEY` 与 `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`,本机可用 `TAURI_SIGNING_PRIVATE_KEY_PATH` 指向同一私钥。当前密钥不带密码;首次发布前仍可重新生成,首次发布后不可更换。 - macOS 发布方式:`dev-mac` 产物在本机 mac 上执行发布入口上传,Jenkins 暂不新增 macOS 节点;macOS 代码签名与公证凭据未确认前,相关闭环记为未验证项,不静默通过。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index aa1af47d2..eeae67991 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -392,11 +392,11 @@ Supervisor 认领该回执后,由父 run 自己为每个原 delivery 逐一创 - 调度边界:正式 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.147.0` 的原生 `codex.exe`;Rust build script 从 AGC 子包锁定依赖 stage 到 resource,并写入版本与 SHA-256 清单。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/`,包内子目录、组件名、版本和完整性校验保持原合同。 -- 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/`。构建与运行共享平台文件白名单,运行时由当前 `.app/Contents/MacOS` 定位相邻 `Resources`,完整性与版本验证通过后优先使用内置组件;失败沿既有外部安装回退,不能运行未校验的内置文件。单架构资源不能冒充 universal 包。 +- 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`,完整性与版本验证通过后优先使用内置组件;失败沿既有外部安装回退,不能运行未校验的内置文件。universal 主程序同时携带两套独立原生资源,运行切片按 Cargo 目标只选择对应目录;macOS 构建必须预备两套锁定原生依赖,不读取全局 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` 成功替代。未配置正式签名、公证的本地测试包不得作为公开发行包。 - macOS 安装包的系统下限取主程序和全部原生组件中的最高要求;锁定 Codex 0.147.0 原生依赖所携带的 zsh 要求 macOS 15.0,因此 `bundle.macOS.minimumSystemVersion` 明确为 `15.0`。更新原生依赖时重新检查 Mach-O 的系统下限,不能只按 AGC 主程序宣称兼容版本。 -- 发布链路的目标解析和单架构清单以《AGC客户端更新检查与下载》为准:CLI 目标优先,版本、构建、端点、bundle 与更新清单共用单一发布上下文。插件能力以《AGC通用插件宿主与编辑器适配》为准:无已注册 Cocos 原生适配器时隐藏且拒绝启动,前端自动启动只消费后端可用性投影。 +- 发布链路的目标解析和 universal 清单以《AGC客户端更新检查与下载》为准:CLI 目标优先,版本、构建、端点、bundle 与更新清单共用单一发布上下文。插件能力以《AGC通用插件宿主与编辑器适配》为准:无已注册 Cocos 原生适配器时隐藏且拒绝启动,前端自动启动只消费后端可用性投影。 - CLI 安全边界:CLI 固定使用 argv 启动,禁止 shell 拼接;工作目录使用本次请求专用的空临时目录,不把游戏项目绝对路径写入 prompt、stdout、stderr 或持久记录。调用固定使用 ephemeral、忽略用户配置和 exec rules、read-only sandbox、never approval,并关闭 Codex shell tool;只继承 CLI 运行和认证所需的最小环境,显式移除宿主 `CODEX_API_KEY`。用户级 Codex 登录态继续由本机 Codex 自己读取,API Key、auth 文件、Cookie、Token、`CODEX_HOME` 私有内容不得复制到项目配置、Runtime sidecar、Agent DB、conversation 或日志;stdout / stderr 无换行时也受硬上限约束,stderr 诊断只记录固定分类、字节数和 SHA-256。 - 协议边界:Runtime 把既有 `LlmRunRequest` 的消息和当前函数目录编码为有界 prompt,并从同一函数 JSON Schema 生成 Codex structured-output schema。CLI 输出转换为现有 `LlmRunResponse / LlmToolCall` 后,继续经过 native tool / MCP 参数校验、动作上限、权限、pending、receipt、验证与格式修复链;最终回复仍走唯一提交路径,不新增平行响应协议。 - 取消与恢复:Codex 子进程绑定当前 Provider request lifecycle,取消、暂停、Runner draining 或 GUI owner 丢失时终止并回收当前进程;started 后没有可信终态仍沿现有 Provider reconciliation 处理。`agentMode`、CLI 可执行身份和影响输出的 Codex 参数进入 `providerConfigFingerprint`,模式切换不得消费另一模式遗留的 retry/handoff。 diff --git a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index 92e4d4fc6..f20a47532 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -716,6 +716,14 @@ Pingora current release 自审脚本 `scripts/ops/pingora-current-release-audit. `Genarrative-Web-Build` 打包 `web.tar.gz` 前、`Genarrative-Web-Deploy` 解包后都会把 Web 静态目录规范为目录 `755`、文件 `644`。如果前端页面能打开但 public 图片、字体或音频返回 `403 Forbidden`,优先检查当前 `/srv/genarrative/web` 指向的 release 中对应文件权限是否被异常归档为 `600`,临时恢复可对该 release 的 `web` 目录执行目录 `755`、文件 `644` 的权限修正。 +### AGC macOS 手动构建节点 + +Mac universal 构建脚本位于 `jenkins/Jenkinsfile.ai-game-creator-shell-macos-build`,只对专用 `genarrative-agc-macos` 标签运行。节点按 EXCLUSIVE、单 executor 配置,Job 禁止并发且不设置 trigger;不接入现有每小时版本调度,不改 Windows 发布职责。它使用独立 Jenkins workspace,禁止指向开发 checkout 或共享其可写 target/node_modules。 + +该 Job 的目标仅为构建并归档:保留源码版本、不读取远端版本、不注入发布私钥/OSS 凭据、不运行 release upload。执行 `npm ci` 后使用锁文件校验并补齐两种 macOS Codex 原生依赖,再调用 `scripts/build-macos-ci.mjs`(AGC 应用目录下)生成 universal app 与 DMG、分别运行 arm64/x86_64 隔离 smoke。归档限 `artifacts/` 下的 DMG、SHA-256、非敏感构建清单和源码 commit;不归档用户 HOME、Jenkins secret、原始工作目录或全量日志。 + +当前用户的 LaunchAgent 受登录、休眠与局域网连通性影响,不能视为长期无人值守构建机。该用户进程也不是权限沙箱,只能承接受信任仓库和 Job;不能把匹配标签当作隔离恶意构建的措施。节点注册、上线和首个 Job 的成功必须以控制器实时状态确认,脚本入库不代表管线已经接通。 + ### CI 宿主 CPU 上限(Jenkins 16 核 / Gitea Actions runner 12 核) `genarrative-station` 上 Jenkins Built-In Node 与 Gitea Actions runner 容器共用同一台 32 逻辑核宿主机,两路 CI 都必须有硬上限,避免构建期把机器顶满、让交互用户卡顿。Jenkins 固定 16 核(50%):在宿主执行 `systemctl set-property jenkins.service CPUQuota=1600%`,立即生效且不需要重启 Jenkins,drop-in 落在 `/etc/systemd/system.control/jenkins.service.d/50-CPUQuota.conf`;该配额覆盖 Built-In Node 上所有子构建(Web / Api / Stdb 的 `npm ci`、Vitest、`tsc`、`cargo` 都跑在这台机器上),Deploy 阶段在远端 `genarrative-dev-deploy` / `genarrative-release-deploy` agent 执行,不受该上限约束。Gitea Actions runner 固定 12 核(37.5%):`/opt/gitea-stack/compose.yml` 的 `runner.cpus` 为 `"12.0"`,调整运行中的容器用 `docker update --cpus=12 gitea-runner`(不重建容器、不中断在跑 job);需要让容器配置与 compose 完全一致时,先确认 Gitea 没有 `in_progress` run,再 `cd /opt/gitea-stack && docker compose up -d runner`。 diff --git a/jenkins/Jenkinsfile.ai-game-creator-shell-macos-build b/jenkins/Jenkinsfile.ai-game-creator-shell-macos-build new file mode 100644 index 000000000..672f2906e --- /dev/null +++ b/jenkins/Jenkinsfile.ai-game-creator-shell-macos-build @@ -0,0 +1,94 @@ +pipeline { + agent { label 'genarrative-agc-macos' } + options { + disableConcurrentBuilds() + skipDefaultCheckout(true) + timeout(time: 90, unit: 'MINUTES') + buildDiscarder(logRotator(numToKeepStr: '10', artifactNumToKeepStr: '3')) + timestamps() + } + parameters { + string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '必须含 universal 双架构依赖实现的受信任分支') + string(name: 'COMMIT_HASH', defaultValue: '', description: '可选,固定属于源码分支的提交;不递增应用版本') + } + environment { + GIT_REMOTE_URL = 'ssh://git@192.168.35.82:2222/GenarrativeAI/Genarrative.git' + GIT_REMOTE_CREDENTIAL_ID = 'genarrative-local-gitea-ssh' + AGC_UPDATE_CHANNEL = 'dev-mac' + RUSTC_WRAPPER = '' + CARGO_BUILD_JOBS = '4' + } + stages { + stage('Checkout') { + steps { + // 由节点配置 AGC_AGENT_ROOT;不允许把开发工作树当作 Jenkins workspace。 + sh ''' + set -eu + test -n "$AGC_AGENT_ROOT" + case "$WORKSPACE" in "$AGC_AGENT_ROOT"/workspace/*) ;; *) echo '拒绝非专用 Agent 工作区'; exit 1;; esac + test "$(uname -s)" = Darwin + git check-ref-format --branch "$SOURCE_BRANCH" >/dev/null + ''' + withCredentials([sshUserPrivateKey(credentialsId: env.GIT_REMOTE_CREDENTIAL_ID, keyFileVariable: 'GIT_SSH_KEY', usernameVariable: 'GIT_SSH_USER')]) { + sh ''' + set -eu + export GIT_SSH_COMMAND="ssh -i \\"$GIT_SSH_KEY\\" -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes" + if [ ! -d .git ]; then + git init + git remote add origin "$GIT_REMOTE_URL" + fi + test "$(git remote get-url origin)" = "$GIT_REMOTE_URL" + git fetch --no-tags origin "+refs/heads/$SOURCE_BRANCH:refs/remotes/origin/$SOURCE_BRANCH" + ref="refs/remotes/origin/$SOURCE_BRANCH" + if [ -n "$COMMIT_HASH" ]; then + case "$COMMIT_HASH" in *[!0-9a-fA-F]* ) echo 'COMMIT_HASH 必须为十六进制'; exit 1;; esac + test "${#COMMIT_HASH}" -ge 7 && test "${#COMMIT_HASH}" -le 40 + git cat-file -e "$COMMIT_HASH^{commit}" + git merge-base --is-ancestor "$COMMIT_HASH" "$ref" + ref="$COMMIT_HASH" + fi + git reset --hard "$ref" + # 不清除 node_modules/target 缓存;被跟踪内容始终来自上述 commit。 + git clean -fd + git rev-parse HEAD > .jenkins-source-commit + ''' + } + } + } + stage('Toolchain and dependencies') { + steps { + sh ''' + set -eu + test "$(uname -m)" = arm64 + node --version + npm --version + cargo --version + xcrun --find clang + xcrun --find lipo + arch -x86_64 /usr/bin/uname -m + rustup target add aarch64-apple-darwin x86_64-apple-darwin + npm ci --no-audit --no-fund + node apps/ai-game-creator-shell/scripts/prepare-macos-codex.mjs + ''' + } + } + stage('Validate and build universal') { + steps { + sh ''' + set -eu + node --test apps/ai-game-creator-shell/scripts/prepare-macos-codex.test.mjs + node --test apps/ai-game-creator-shell/scripts/build-release.test.mjs apps/ai-game-creator-shell/scripts/cargo-features.test.mjs + node apps/ai-game-creator-shell/scripts/build-macos-ci.mjs + ''' + } + } + stage('Archive only') { + steps { + archiveArtifacts artifacts: 'artifacts/*.dmg,artifacts/*.sha256,artifacts/build-manifest.json,.jenkins-source-commit', fingerprint: true, allowEmptyArchive: false + } + } + } + post { + success { echo 'macOS universal 本地测试包已构建并归档;未上传 OSS、未正式签名/公证。' } + } +}