Compare commits

..

3 Commits

Author SHA1 Message Date
suzmii fc46cabb75 补充Mac Jenkins节点工具链路径
为LaunchAgent构建环境注入Node、npm、Cargo和Homebrew路径
2026-09-18 22:34:49 +08:00
suzmii 762f037150 修复Mac Jenkins节点工作区根目录回退
移除不兼容的节点环境变量配置依赖
在Jenkinsfile中使用专用Agent根目录默认值
避免节点分配阶段环境变量属性解析失败
2026-09-18 21:57:52 +08:00
suzmii 48985d3447 接入Mac通用构建与Jenkins归档管线
补齐macOS universal双架构Codex资源与构建校验
统一发布清单和检查脚本支持universal目标
新增锁定原生依赖完整性校验与隔离构建smoke
新增Mac Jenkins Agent归档构建Job与本机构建接入规范
2026-09-18 19:34:23 +08:00
74 changed files with 1329 additions and 2097 deletions
+2
View File
@@ -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/
@@ -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 包与校验文件已生成;未发布、未签名或公证');
@@ -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')) {
@@ -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 [
@@ -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',
@@ -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 仍为外部前提',
@@ -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();
}
@@ -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);
}
});
+18 -2
View File
@@ -31,7 +31,23 @@ fn sha256_file(path: &std::path::Path) -> Result<String, std::io::Error> {
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");
@@ -49,7 +49,11 @@ pub fn for_target(target: &str) -> Option<Layout> {
} 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);
@@ -1,12 +1,11 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main",
"description": "AI 游戏创作主窗口允许读系统剪贴板,用于粘贴素材附件和复制生成文件路径;允许弹出原生打开/保存对话框用于素材上传与导出。",
"description": "AI 游戏创作主窗口允许读系统剪贴板图片,用于粘贴素材附件;允许弹出原生打开/保存对话框用于素材上传与导出。",
"windows": ["client"],
"permissions": [
"clipboard-manager:allow-read-image",
"clipboard-manager:allow-read-text",
"clipboard-manager:allow-write-text",
"core:image:allow-rgba",
"core:image:allow-size",
"core:resources:allow-close",
@@ -2,7 +2,7 @@ use crate::config::build_game_creator_llm_client_from_llm_config;
use crate::config::load_game_creator_app_config;
use crate::ui_editor::commands::utils::{
parse_limited_llm_tool_arguments, read_ui_reference_image_data_url, request_ui_editor_llm,
required_tool_call_arguments, strict_json_schema,
strict_json_schema,
};
use crate::ui_editor::component::text::FontSource;
use crate::ui_editor::component::{Component, NodeComponent};
@@ -406,8 +406,12 @@ pub(crate) async fn bind_components_impl_with_provider(
.await
}
.map_err(|error| format!("组件绑定失败:{error}"))?;
let arguments = required_tool_call_arguments(&response, "bind_ui_components")?;
let parsed = parse_binding_response(arguments, editable_nodes.len())?;
let call = response
.tool_calls
.iter()
.find(|call| call.name == "bind_ui_components")
.ok_or_else(|| "LLM 未返回 bind_ui_components 工具调用".to_string())?;
let parsed = parse_binding_response(&call.arguments, editable_nodes.len())?;
let known_sprite_ids = state.sprite_assets.keys().cloned().collect::<HashSet<_>>();
let known_font_ids = state.font_assets.keys().cloned().collect::<HashSet<_>>();
let result = validate_and_materialize(
@@ -1,8 +1,7 @@
use crate::config::build_game_creator_llm_client_from_llm_config;
use crate::config::load_game_creator_app_config;
use crate::ui_editor::commands::utils::{
parse_limited_llm_tool_arguments, request_ui_editor_llm, required_tool_call_arguments,
strict_json_schema,
parse_limited_llm_tool_arguments, request_ui_editor_llm, strict_json_schema,
};
use crate::ui_editor::state::{State, UITree};
use platform_llm::{LlmFunctionTool, LlmMessage, LlmRunRequest, LlmToolChoice};
@@ -173,7 +172,6 @@ mod materialize {
use crate::ui_editor::layout::node::{
Node as LayoutNode, NodeMetadata, NodeSource, StageStatus,
};
use crate::ui_editor::layout::offset::NodeOffset;
use crate::ui_editor::layout::transform::Transform;
use crate::ui_editor::state::{State, UITree};
use crate::ui_editor::utils::{random_node_id, NodeId, UIDesignImageId};
@@ -304,7 +302,6 @@ mod materialize {
component: None,
children_display_mode: ChildrenDisplayMode::Exclusive,
children: members.into_iter().map(|member| member.node).collect(),
offset: NodeOffset::default(),
},
priority,
src_ui_design,
@@ -488,11 +485,15 @@ pub(crate) async fn merge_ui_impl_with_provider(
app_log!("ui_merge.error stage=llm_request error={error}");
format!("UI 树合并失败:{error}")
})?;
let arguments = required_tool_call_arguments(&response, MERGE_TOOL_NAME).map_err(|error| {
app_log!("ui_merge.error stage=parse_tool_call reason=missing_tool_call error={error}");
format!("LLM 未返回 {MERGE_TOOL_NAME} 工具调用")
})?;
let arguments = parse_limited_llm_tool_arguments(arguments).map_err(|error| {
let call = response
.tool_calls
.iter()
.find(|call| call.name == MERGE_TOOL_NAME)
.ok_or_else(|| {
app_log!("ui_merge.error stage=parse_tool_call reason=missing_tool_call");
format!("LLM 未返回 {MERGE_TOOL_NAME} 工具调用")
})?;
let arguments = parse_limited_llm_tool_arguments(&call.arguments).map_err(|error| {
app_log!("ui_merge.error stage=parse_arguments error={error}");
format!("UI 合并工具参数无效:{error}")
})?;
@@ -526,7 +527,6 @@ mod tests {
use crate::ui_editor::layout::children_display_mode::ChildrenDisplayMode;
use crate::ui_editor::layout::control_layout::ControlLayout;
use crate::ui_editor::layout::node::{Node, NodeMetadata, NodeSource, StageStatus};
use crate::ui_editor::layout::offset::NodeOffset;
use crate::ui_editor::layout::transform::Transform;
use crate::ui_editor::state::{State, UITree};
use crate::ui_editor::utils::{NodeId, UIDesignImageId};
@@ -548,7 +548,6 @@ mod tests {
component: None,
children_display_mode: ChildrenDisplayMode::Stack,
children,
offset: NodeOffset::default(),
}
}
@@ -2,18 +2,17 @@ use crate::config::build_game_creator_llm_client_from_llm_config;
use crate::config::load_game_creator_app_config;
use crate::ui_editor::commands::utils::{
parse_limited_llm_tool_arguments, read_ui_reference_image_data_url, request_ui_editor_llm,
required_tool_call_arguments, strict_json_schema,
strict_json_schema,
};
use crate::ui_editor::component::{Component, NodeComponent};
use crate::ui_editor::layout::children_display_mode::ChildrenDisplayMode;
use crate::ui_editor::layout::control_layout::ControlLayout;
use crate::ui_editor::layout::dimension::UIRect;
use crate::ui_editor::layout::node::{Node as LayoutNode, NodeMetadata, NodeSource, StageStatus};
use crate::ui_editor::layout::offset::NodeOffset;
use crate::ui_editor::layout::transform::Transform;
use crate::ui_editor::resource::ui_design_image::UIDesignImage;
use crate::ui_editor::state::{State, UITree};
use crate::ui_editor::utils::{random_node_id, UIDesignImageId};
use crate::ui_editor::utils::{random_node_id, NodeId, UIDesignImageId};
use nalgebra::{Point2, Vector2};
use platform_llm::{
LlmFunctionTool, LlmMessage, LlmMessageContentPart, LlmRunRequest, LlmToolChoice,
@@ -323,7 +322,6 @@ fn convert_node(
component: source.component.clone().into_option(),
children_display_mode: ChildrenDisplayMode::Stack,
children,
offset: NodeOffset::default(),
})
}
@@ -770,18 +768,21 @@ pub(crate) async fn recognize_ui_impl_with_provider(
!response.text.trim().is_empty(),
response.tool_calls.len()
);
let arguments = required_tool_call_arguments(&response, "recognize_ui_structure")
.map_err(|error| {
let call = response
.tool_calls
.iter()
.find(|call| call.name == "recognize_ui_structure")
.ok_or_else(|| {
app_log!(
"ui_recognition.error stage=parse_tool_call reason=missing_tool_call root={} error={error}",
"ui_recognition.error stage=parse_tool_call root={} reason=missing_tool_call",
root_id.as_str()
);
format!(
"LLM 未返回 recognize_ui_structure 工具调用(根界面图 {}",
"根界面图 {}LLM 未返回 recognize_ui_structure 工具调用",
root_id.as_str()
)
})?;
let arguments = parse_limited_llm_tool_arguments(arguments).map_err(|error| {
let arguments = parse_limited_llm_tool_arguments(&call.arguments).map_err(|error| {
app_log!(
"ui_recognition.error stage=parse_arguments root={} error={error}",
root_id.as_str()
@@ -857,7 +858,6 @@ pub(crate) async fn recognize_ui_impl_with_provider(
component: recognition_root.component.into_option(),
children_display_mode: ChildrenDisplayMode::Stack,
children,
offset: NodeOffset::default(),
};
// Tree identity and root identity are assigned by Rust, never chosen by the model.
ui_trees.push(UITree {
@@ -22,7 +22,6 @@ mod tests {
use crate::ui_editor::layout::control_layout::ControlLayout;
use crate::ui_editor::layout::node::Node;
use crate::ui_editor::layout::node::{NodeMetadata, NodeSource, StageStatus};
use crate::ui_editor::layout::offset::NodeOffset;
use crate::ui_editor::resource::ui_design_image::UIDesignImage;
use crate::ui_editor::state::{State, UITree};
use crate::ui_editor::utils::{NodeId, UIDesignImageId};
@@ -47,7 +46,6 @@ mod tests {
component,
children_display_mode: ChildrenDisplayMode::Stack,
children,
offset: NodeOffset::default(),
}
}
fn state(root: Node) -> State {
@@ -2,7 +2,7 @@ use crate::config::build_game_creator_llm_client_from_llm_config;
use crate::config::load_game_creator_app_config;
use crate::ui_editor::commands::utils::{
parse_limited_llm_tool_arguments, read_ui_reference_image_data_url, request_ui_editor_llm,
required_tool_call_arguments, strict_json_schema,
strict_json_schema,
};
use crate::ui_editor::resource::ui_design_image::UIDesignImageRole;
use crate::ui_editor::state::State;
@@ -214,14 +214,15 @@ pub(crate) async fn suggest_ui_design_semantic_impl(
!response.text.trim().is_empty(),
response.tool_calls.len()
);
let arguments = required_tool_call_arguments(&response, "suggest_ui_design_semantics")
.map_err(|error| {
app_log!(
"ui_design_suggestion.error stage=parse_tool_call reason=missing_tool_call error={error}"
);
let call = response
.tool_calls
.iter()
.find(|call| call.name == "suggest_ui_design_semantics")
.ok_or_else(|| {
app_log!("ui_design_suggestion.error stage=parse_tool_call reason=missing_tool_call");
"LLM 响应无效(详情:未返回 suggest_ui_design_semantics 工具调用)".to_string()
})?;
let arguments = parse_limited_llm_tool_arguments(arguments).map_err(|error| {
let arguments = parse_limited_llm_tool_arguments(&call.arguments).map_err(|error| {
app_log!("ui_design_suggestion.error stage=parse_arguments error={error}");
format!("LLM 响应无效(详情:UI 语义建议工具参数无效:{error}")
})?;
@@ -95,29 +95,6 @@ pub(crate) fn parse_limited_llm_tool_arguments(
serde_json::from_str(arguments).map_err(|error| format!("LLM 工具参数不是有效 JSON{error}"))
}
/// Stable structured-action adapter: locate the required tool call and parse its
/// bounded JSON arguments. Prompt, schema and materialization stay in each
/// operation-specific command.
pub(crate) fn required_tool_arguments(
response: &LlmRunResponse,
tool_name: &str,
) -> Result<serde_json::Value, String> {
let arguments = required_tool_call_arguments(response, tool_name)?;
parse_limited_llm_tool_arguments(arguments)
}
pub(crate) fn required_tool_call_arguments<'a>(
response: &'a LlmRunResponse,
tool_name: &str,
) -> Result<&'a str, String> {
response
.tool_calls
.iter()
.find(|call| call.name == tool_name)
.map(|call| call.arguments.as_str())
.ok_or_else(|| format!("LLM 未返回 {tool_name} 工具调用"))
}
pub(crate) async fn read_ui_reference_image_data_url(path: PathBuf) -> Result<String, String> {
tokio::task::spawn_blocking(move || read_ui_reference_image_data_url_blocking(&path))
.await
@@ -2,5 +2,4 @@ pub mod children_display_mode;
pub mod control_layout;
pub mod dimension;
pub mod node;
pub mod offset;
pub mod transform;
@@ -1,7 +1,6 @@
use crate::ui_editor::component::Component;
use crate::ui_editor::layout::children_display_mode::ChildrenDisplayMode;
use crate::ui_editor::layout::control_layout::ControlLayout;
use crate::ui_editor::layout::offset::NodeOffset;
use crate::ui_editor::utils::NodeId;
use serde::{Deserialize, Serialize};
use ts_rs::TS;
@@ -15,7 +14,6 @@ pub struct Node {
pub component: Option<Component>,
pub children_display_mode: ChildrenDisplayMode,
pub children: Vec<Node>,
pub offset: NodeOffset,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, TS)]
@@ -1,20 +0,0 @@
use serde::{Deserialize, Serialize};
use ts_rs::TS;
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, TS)]
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/ui-editor/types/"))]
pub struct NodeOffset {
#[ts(as = "[f32; 2]")]
pub min: [f32; 2],
#[ts(as = "[f32; 2]")]
pub max: [f32; 2],
}
impl Default for NodeOffset {
fn default() -> Self {
Self {
min: [0.0, 0.0],
max: [0.0, 0.0],
}
}
}
@@ -1126,10 +1126,8 @@ mod tests {
}
},
"children_display_mode": "Stack",
"children": [],
"offset": { "min": [0.0, 0.0], "max": [0.0, 0.0] }
}],
"offset": { "min": [0.0, 0.0], "max": [0.0, 0.0] }
"children": []
}]
}
}],
"ui_design_images": {
@@ -1311,8 +1309,7 @@ mod tests {
},
"component": null,
"children_display_mode": "Stack",
"children": [],
"offset": { "min": [0.0, 0.0], "max": [0.0, 0.0] }
"children": []
}
}],
"ui_design_images": {
@@ -1546,8 +1543,7 @@ mod tests {
}
},
"children_display_mode": "Stack",
"children": [],
"offset": { "min": [0.0, 0.0], "max": [0.0, 0.0] }
"children": []
}
}],
"ui_design_images": {
@@ -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"
}
},
@@ -78,11 +78,7 @@ export function ResourceCanvasGenerationPlaceholderCardView({
<Sparkles size={18} aria-hidden="true" />
<strong>{placeholder.assetName}</strong>
<small>
{
RESOURCE_CANVAS_GENERATION_PLACEHOLDER_STATUS_LABELS[
placeholder.status
]
}
{RESOURCE_CANVAS_GENERATION_PLACEHOLDER_STATUS_LABELS[placeholder.status]}
</small>
<button
type="button"
@@ -155,7 +155,9 @@ export function createResourceCanvasAssetGenerationQueue(
referenceAssetIds: task.referenceAssetIds,
outputPath: task.outputPath,
// 入口栏目:原生支持时按它登记归类;不支持时后端忽略,落点仍按正式归类走。
...(task.targetCategory ? { targetCategory: task.targetCategory } : {}),
...(task.targetCategory
? { targetCategory: task.targetCategory }
: {}),
})) as LocalProjectAssetGenerationTaskRecord;
let started: LocalProjectAssetGenerationTaskRecord;
try {
@@ -175,9 +175,7 @@ export function resourceCanvasGenerationPlaceholdersForProject(
placeholders: readonly ResourceCanvasGenerationPlaceholder[],
projectId: string,
): ResourceCanvasGenerationPlaceholder[] {
return placeholders.filter(
(placeholder) => placeholder.projectId === projectId,
);
return placeholders.filter((placeholder) => placeholder.projectId === projectId);
}
/** 按任务找回占位:成功落点与失败收口都以 taskId 为准,不按素材名猜。 */
@@ -185,18 +183,14 @@ export function resourceCanvasGenerationPlaceholderByTaskId(
placeholders: readonly ResourceCanvasGenerationPlaceholder[],
taskId: string,
): ResourceCanvasGenerationPlaceholder | null {
return (
placeholders.find((placeholder) => placeholder.taskId === taskId) ?? null
);
return placeholders.find((placeholder) => placeholder.taskId === taskId) ?? null;
}
export function resourceCanvasGenerationPlaceholderByDraftId(
placeholders: readonly ResourceCanvasGenerationPlaceholder[],
draftId: string,
): ResourceCanvasGenerationPlaceholder | null {
return (
placeholders.find((placeholder) => placeholder.draftId === draftId) ?? null
);
return placeholders.find((placeholder) => placeholder.draftId === draftId) ?? null;
}
export const RESOURCE_CANVAS_GENERATION_PLACEHOLDER_STATUS_LABELS: Record<

Some files were not shown because too many files have changed in this diff Show More