Compare commits
140 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e3682fd06f | |||
| 33f5ad68bf | |||
| 5bf036bb81 | |||
| 5383af4cc5 | |||
| 47f7cb2bf1 | |||
| 603650fbf4 | |||
| 5d20f5bfeb | |||
| af38de9fd1 | |||
| 18252e24b8 | |||
| 1ce2c10107 | |||
| 48caa23997 | |||
| ea141d9f17 | |||
| 769740a46d | |||
| b472b9eec9 | |||
| 0d5940433d | |||
| 9a21690fe9 | |||
| deff6319e2 | |||
| 2257a78d6b | |||
| e5fd454bb3 | |||
| a0f6b7c67d | |||
| ee33af7b75 | |||
| be370cc615 | |||
| 21b059bd41 | |||
| 2d90b73b94 | |||
| cd8f480719 | |||
| b137f17599 | |||
| 2ffccb844c | |||
| 16c905b51b | |||
| 6f012d419a | |||
| 03d7bca9a5 | |||
| 2abad3b537 | |||
| 1bd153d7f2 | |||
| a2e6f205d6 | |||
| 30c377d0f0 | |||
| fc5cea43ac | |||
| c2ab9674a0 | |||
| 1c6e4c1e74 | |||
| f5a333b839 | |||
| 2e15a70b14 | |||
| 72cd0747a4 | |||
| 3d4c7929c3 | |||
| 20b1fd63de | |||
| 105591bac5 | |||
| 195b7dd5dd | |||
| da44d66dc8 | |||
| 0a85c4f87e | |||
| efce7b102f | |||
| db5edc948c | |||
| 45c3780bf5 | |||
| 6fcf42e4ac | |||
| e0f9f811b7 | |||
| 03b5c1c9f4 | |||
| 8dce016552 | |||
| 58992330aa | |||
| 2c687b01a4 | |||
| c0e377f479 | |||
| 6001b87215 | |||
| b08ab6ee66 | |||
| 7f80012d7f | |||
| fbe95591d5 | |||
| 64879cfcdd | |||
| d222aad2ec | |||
| 13b28ebbc7 | |||
| 30648e6b93 | |||
| 9dd1052374 | |||
| aec9568c39 | |||
| 0a1f0e0b26 | |||
| 5c31ae91ca | |||
| 3ba6168c6a | |||
| 2628b83d4b | |||
| a7b2b0e23b | |||
| ef982fdb78 | |||
| 6853ec5388 | |||
| 796ad7bb51 | |||
| 20b34918d3 | |||
| 04e5512777 | |||
| 0510cd8d9c | |||
| 91b65f94ca | |||
| 70513cf049 | |||
| 87aed0b765 | |||
| 733a7015af | |||
| 7a237431e0 | |||
| 6ed3772ff5 | |||
| a86ead93e4 | |||
| b0ce384b8d | |||
| b56566f40e | |||
| 6782d0ea23 | |||
| e6092dda6f | |||
| 5f13698129 | |||
| 29ba9a358e | |||
| 3ab56e6cfa | |||
| 56c4faa8ee | |||
| d23c808376 | |||
| 41930b858e | |||
| 9dccf5318e | |||
| f216381619 | |||
| d454d3fa4b | |||
| a5ee45aa4e | |||
| d333f783fe | |||
| faab318127 | |||
| a4296a4bdd | |||
| 7eee89ded3 | |||
| f9bc2d917c | |||
| b96cd4b7c2 | |||
| f185d21f55 | |||
| a61b0cce81 | |||
| 2a75a2d2c8 | |||
| 512f669f57 | |||
| 21dbbb4c3c | |||
| 55d0c99e2b | |||
| 3907a8bf74 | |||
| 46dfa2cffb | |||
| 97e2b26900 | |||
| 39e33f7a5b | |||
| 040f646480 | |||
| 8b007c637d | |||
| f0a62c6306 | |||
| 30edfee00e | |||
| c2da8e184e | |||
| a19319d815 | |||
| d07ea186fa | |||
| 80caec373a | |||
| 3caefd6716 | |||
| 4d0dd2d721 | |||
| 2befaaef9d | |||
| 20b0bf4dd7 | |||
| b4fd8d9b8b | |||
| fe85fa2a62 | |||
| b14a533b17 | |||
| 42969b2218 | |||
| c4ee75a1af | |||
| 6f2137cbb0 | |||
| 70a2246e8b | |||
| b7e3dac661 | |||
| 2e4a1996c8 | |||
| fe4e952853 | |||
| 18654b6806 | |||
| ee7d00c0b1 | |||
| 9cd1a94369 | |||
| 470e85c0ff |
@@ -23,3 +23,6 @@
|
|||||||
*.meta text
|
*.meta text
|
||||||
*.anim text
|
*.anim text
|
||||||
*.controller text
|
*.controller text
|
||||||
|
|
||||||
|
# Rust ts-rs 生成的共享契约:保留在仓库中供 TS 消费,但不作为手写源文件统计。
|
||||||
|
packages/shared/src/contracts/generated/** linguist-generated=true
|
||||||
|
|||||||
@@ -221,8 +221,7 @@ jobs:
|
|||||||
- name: Run AI game creator shell agent-run smoke
|
- name: Run AI game creator shell agent-run smoke
|
||||||
run: npm run check:native-shells:agc-rust-smoke
|
run: npm run check:native-shells:agc-rust-smoke
|
||||||
|
|
||||||
# AGC 壳依赖的共享 / 平台 crate 测试用的是 server-rs workspace 与两个无锁独立 crate
|
# AGC 壳依赖的共享 / 平台和编辑器插件 crate 各自预热独立 manifest,再运行对应测试。
|
||||||
# 的 manifest,属另一套依赖图,因此单独一个 job 预热、单独跑。
|
|
||||||
ai-game-creator-shell-rust-crates:
|
ai-game-creator-shell-rust-crates:
|
||||||
name: AI game creator shell Rust crates
|
name: AI game creator shell Rust crates
|
||||||
runs-on: genarrative-ci
|
runs-on: genarrative-ci
|
||||||
@@ -263,13 +262,15 @@ jobs:
|
|||||||
# `cargo test --manifest-path` 单独跑这两个 crate。不在这里预热的话,这两条测试
|
# `cargo test --manifest-path` 单独跑这两个 crate。不在这里预热的话,这两条测试
|
||||||
# 会在测试阶段自己 `Updating crates.io index`,crates.io 一抖动整条 job 就红
|
# 会在测试阶段自己 `Updating crates.io index`,crates.io 一抖动整条 job 就红
|
||||||
# (见 #327 / PR #316 run 1950)。
|
# (见 #327 / PR #316 run 1950)。
|
||||||
# 两个 crate 都没有提交 Cargo.lock,所以这里只能做不带锁标志的 fetch:
|
# Cocos 插件也使用独立且未提交的锁文件,一并预热。
|
||||||
|
# 这些 crate 都没有提交 Cargo.lock,所以这里只能做不带锁标志的 fetch:
|
||||||
# 加锁标志会因为缺少锁文件直接失败。生成的 Cargo.lock 落在两个 crate 目录内,
|
# 加锁标志会因为缺少锁文件直接失败。生成的 Cargo.lock 落在两个 crate 目录内,
|
||||||
# 已被各自的 .gitignore 忽略,只留在容器里;随后的测试阶段因此能用锁定版本
|
# 已被各自的 .gitignore 忽略,只留在容器里;随后的测试阶段因此能用锁定版本
|
||||||
# 解析,不再触碰 registry index。
|
# 解析,不再触碰 registry index。
|
||||||
for manifest_path in \
|
for manifest_path in \
|
||||||
server-rs/crates/agent-runtime-core/Cargo.toml \
|
server-rs/crates/agent-runtime-core/Cargo.toml \
|
||||||
server-rs/crates/agent-runtime-orchestration/Cargo.toml; do
|
server-rs/crates/agent-runtime-orchestration/Cargo.toml \
|
||||||
|
plugins/agc-cocos-editor/native/cocos-editor-bridge/Cargo.toml; do
|
||||||
for attempt in $(seq 1 5); do
|
for attempt in $(seq 1 5); do
|
||||||
if cargo fetch \
|
if cargo fetch \
|
||||||
--target x86_64-unknown-linux-gnu \
|
--target x86_64-unknown-linux-gnu \
|
||||||
@@ -284,6 +285,23 @@ jobs:
|
|||||||
done
|
done
|
||||||
done
|
done
|
||||||
|
|
||||||
|
- name: Prepare Unity plugin Rust dependencies
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
for attempt in $(seq 1 5); do
|
||||||
|
if cargo fetch --locked \
|
||||||
|
--target x86_64-unknown-linux-gnu \
|
||||||
|
--manifest-path plugins/agc-unity-editor/native/unity-editor-bridge/Cargo.toml; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if [[ "${attempt}" -eq 5 ]]; then
|
||||||
|
echo 'Unity plugin Cargo dependency fetch failed after 5 attempts.' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep $((attempt * 2))
|
||||||
|
done
|
||||||
|
|
||||||
- name: Run AI game creator shell shared crate gates
|
- name: Run AI game creator shell shared crate gates
|
||||||
run: npm run check:native-shells:agc-rust-crates
|
run: npm run check:native-shells:agc-rust-crates
|
||||||
|
|
||||||
|
|||||||
+10
@@ -41,7 +41,17 @@ temp*build*/
|
|||||||
/apps/ai-game-creator-shell/src-tauri/resources/codex/win-x64/codex-resources/
|
/apps/ai-game-creator-shell/src-tauri/resources/codex/win-x64/codex-resources/
|
||||||
/apps/ai-game-creator-shell/src-tauri/resources/codex/win-x64/codex-package.json
|
/apps/ai-game-creator-shell/src-tauri/resources/codex/win-x64/codex-package.json
|
||||||
/apps/ai-game-creator-shell/src-tauri/resources/plugins/
|
/apps/ai-game-creator-shell/src-tauri/resources/plugins/
|
||||||
|
/apps/ai-game-creator-shell/src-tauri/resources/codex/mac-native/bin/
|
||||||
|
/apps/ai-game-creator-shell/src-tauri/resources/codex/mac-native/codex-path/
|
||||||
|
/apps/ai-game-creator-shell/src-tauri/resources/codex/mac-native/codex-resources/
|
||||||
|
/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
|
||||||
/plugins/agc-cocos-editor/native/payload/
|
/plugins/agc-cocos-editor/native/payload/
|
||||||
|
/plugins/agc-unity-editor/dotnet/**/bin/
|
||||||
|
/plugins/agc-unity-editor/dotnet/**/obj/
|
||||||
|
/plugins/agc-unity-editor/dotnet/publish/
|
||||||
|
/plugins/agc-unity-editor/dotnet/native-build/
|
||||||
/apps/ai-game-creator-shell/logs/
|
/apps/ai-game-creator-shell/logs/
|
||||||
/apps/ai-game-creator-shell/.llm-drafts/
|
/apps/ai-game-creator-shell/.llm-drafts/
|
||||||
/apps/ai-game-creator-shell/game-creator.config.local.json
|
/apps/ai-game-creator-shell/game-creator.config.local.json
|
||||||
|
|||||||
+10
-1
@@ -1,5 +1,14 @@
|
|||||||
{
|
{
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"semi": true,
|
"semi": true,
|
||||||
"trailingComma": "all"
|
"trailingComma": "all",
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "packages/shared/src/contracts/generated/**/*.ts",
|
||||||
|
"options": {
|
||||||
|
"printWidth": 1000,
|
||||||
|
"singleQuote": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+16
@@ -172,6 +172,20 @@ _Avoid_: 多步骤向导、完整规则编辑器、拖拽编辑器
|
|||||||
Bark Battle 平台作品闭环按契约与领域规则、后端存储/API、最小前端纵切、投影体验、收口验证的顺序推进。
|
Bark Battle 平台作品闭环按契约与领域规则、后端存储/API、最小前端纵切、投影体验、收口验证的顺序推进。
|
||||||
_Avoid_: mock 先行堆积、前后端各自发散、先做排行榜 UI
|
_Avoid_: mock 先行堆积、前后端各自发散、先做排行榜 UI
|
||||||
|
|
||||||
|
## 项目开发对话(DirectProject)
|
||||||
|
|
||||||
|
**项目对话历史**:
|
||||||
|
AGC 本地项目内 Codex 原始对话条目的持久集合,是聊天展示、工具卡片和线程恢复注入的唯一持久事实源。
|
||||||
|
_Avoid_: 会话缓存、展示态历史、按 UI 需要另存的对话副本
|
||||||
|
|
||||||
|
**运行态事件**:
|
||||||
|
Thread Manager 向订阅者推送的当前回合原始事件流,只服务运行期间与短期断线恢复,不替代项目对话历史。
|
||||||
|
_Avoid_: 进度通知、快照轮询、第二套历史
|
||||||
|
|
||||||
|
**聊天投影**:
|
||||||
|
把项目对话历史条目与运行态事件转换成消息气泡和工具卡片的读取期转换;不持久化,也不构成事实源。
|
||||||
|
_Avoid_: 投影缓存文件、已脱敏卡片库、第二套 reducer
|
||||||
|
|
||||||
## Relationships
|
## Relationships
|
||||||
|
|
||||||
- 一个 **汪汪声浪大作战** 单局包含多个 **有效声浪触发**。
|
- 一个 **汪汪声浪大作战** 单局包含多个 **有效声浪触发**。
|
||||||
@@ -206,3 +220,5 @@ _Avoid_: mock 先行堆积、前后端各自发散、先做排行榜 UI
|
|||||||
- “入口闭环”曾可能只指内部 demo 或单个详情 CTA;已解析为 **正式作品入口闭环**,不新增独立专区或活动页。
|
- “入口闭环”曾可能只指内部 demo 或单个详情 CTA;已解析为 **正式作品入口闭环**,不新增独立专区或活动页。
|
||||||
- “创作编辑”曾可能指多步骤向导或完整编辑器;已解析为 **轻配置编辑流程**,使用单页表单 + 预览卡片完成保存草稿、发布和发布后跳转作品详情。
|
- “创作编辑”曾可能指多步骤向导或完整编辑器;已解析为 **轻配置编辑流程**,使用单页表单 + 预览卡片完成保存草稿、发布和发布后跳转作品详情。
|
||||||
- “实施顺序”曾可能按 UI 或功能并行发散;已解析为契约/领域规则先行,再做后端存储/API,随后打通最小前端纵切,最后补投影体验与收口验证。
|
- “实施顺序”曾可能按 UI 或功能并行发散;已解析为契约/领域规则先行,再做后端存储/API,随后打通最小前端纵切,最后补投影体验与收口验证。
|
||||||
|
- “回合进度事件”曾同时指 Direct turn update 与 Thread Manager 运行态事件;已解析为 AGC 项目开发对话只保留 **运行态事件**。
|
||||||
|
- “哪些消息可显示”曾可能由后端历史分页判断;已解析为可见性判断属于 **聊天投影**,后端只按原始条目分页,前端负责跳过不可显示条目并推进分页锚点。
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@genarrative/ai-game-creator-shell",
|
"name": "@genarrative/ai-game-creator-shell",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.47",
|
"version": "0.1.67",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node scripts/start-tauri-dev.mjs",
|
"dev": "node scripts/start-tauri-dev.mjs",
|
||||||
|
|||||||
@@ -676,11 +676,11 @@ async function runSelfTest() {
|
|||||||
designFoundationAssetCall?.arguments?.input?.outputPath ===
|
designFoundationAssetCall?.arguments?.input?.outputPath ===
|
||||||
'assets/ui-prototype.png' &&
|
'assets/ui-prototype.png' &&
|
||||||
designFoundationAssetCall.arguments.input.aspectRatio === '16:9' &&
|
designFoundationAssetCall.arguments.input.aspectRatio === '16:9' &&
|
||||||
designFoundationAssetCall.arguments.input.assetKind === 'ui-prototype' &&
|
designFoundationAssetCall.arguments.input.assetKind === 'ui-design' &&
|
||||||
artAssetPlanAssetCall?.arguments?.input?.outputPath ===
|
artAssetPlanAssetCall?.arguments?.input?.outputPath ===
|
||||||
'assets/art-spritesheet.png' &&
|
'assets/art-spritesheet.png' &&
|
||||||
artAssetPlanAssetCall.arguments.input.aspectRatio === '1:1' &&
|
artAssetPlanAssetCall.arguments.input.aspectRatio === '1:1' &&
|
||||||
artAssetPlanAssetCall.arguments.input.assetKind === 'art-spritesheet',
|
artAssetPlanAssetCall.arguments.input.assetKind === 'icon-spritesheet',
|
||||||
'self-test-visual-assets-invalid',
|
'self-test-visual-assets-invalid',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ const requiredFormalArtifactSpecs = [
|
|||||||
{ path: 'game/balance.json', kind: 'json' },
|
{ path: 'game/balance.json', kind: 'json' },
|
||||||
{ path: 'assets/manifest.art.json', kind: 'json' },
|
{ path: 'assets/manifest.art.json', kind: 'json' },
|
||||||
{ path: 'assets/manifest.audio.json', kind: 'json' },
|
{ path: 'assets/manifest.audio.json', kind: 'json' },
|
||||||
{ path: 'game/index.html', kind: 'game-entry' },
|
{ path: 'game/index.html', kind: 'file' },
|
||||||
{ path: 'exports/README.md', kind: 'file' },
|
{ path: 'exports/README.md', kind: 'file' },
|
||||||
];
|
];
|
||||||
const editorImageArtifactSpecs = [
|
const editorImageArtifactSpecs = [
|
||||||
|
|||||||
@@ -14,16 +14,71 @@ const appRoot = fileURLToPath(new URL('..', import.meta.url));
|
|||||||
// 提交摘要里的 pathspec 与 `git log` 都以仓库根为基准,不能在应用目录里执行。
|
// 提交摘要里的 pathspec 与 `git log` 都以仓库根为基准,不能在应用目录里执行。
|
||||||
const repoRoot = path.resolve(appRoot, '..', '..');
|
const repoRoot = path.resolve(appRoot, '..', '..');
|
||||||
const defaultReleaseTarget = 'x86_64-pc-windows-msvc';
|
const defaultReleaseTarget = 'x86_64-pc-windows-msvc';
|
||||||
const releaseTarget =
|
function defaultTarget() {
|
||||||
process.env.AGC_BUILD_TARGET?.trim() || defaultReleaseTarget;
|
return process.env.AGC_BUILD_TARGET?.trim() || defaultReleaseTarget;
|
||||||
const bundleRoot = path.join(
|
}
|
||||||
appRoot,
|
|
||||||
'src-tauri',
|
function explicitBuildTarget(args) {
|
||||||
'target',
|
let target;
|
||||||
releaseTarget,
|
const separator = args.indexOf('--');
|
||||||
'release',
|
const options = separator < 0 ? args : args.slice(0, separator);
|
||||||
'bundle',
|
for (let index = 0; index < options.length; index += 1) {
|
||||||
);
|
const argument = options[index];
|
||||||
|
let value;
|
||||||
|
if (argument === '--target' || argument === '-t') {
|
||||||
|
value = options[++index];
|
||||||
|
} else if (argument.startsWith('--target=')) {
|
||||||
|
value = argument.slice('--target='.length);
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!value?.trim() || value.startsWith('-')) {
|
||||||
|
throw new Error('--target 缺少有效目标');
|
||||||
|
}
|
||||||
|
if (target !== undefined) throw new Error('不能重复指定 --target');
|
||||||
|
target = value.trim();
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
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',
|
||||||
|
].includes(target)
|
||||||
|
) {
|
||||||
|
throw new Error(`不支持的发布目标:${target}`);
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 在入口冻结目标;所有发布步骤共享同一上下文,不再各自读取默认目标。 */
|
||||||
|
export function resolveReleaseContext(args = [], env = process.env) {
|
||||||
|
const target = validateReleaseTarget(
|
||||||
|
explicitBuildTarget(args) ||
|
||||||
|
env.AGC_BUILD_TARGET?.trim() ||
|
||||||
|
defaultReleaseTarget,
|
||||||
|
);
|
||||||
|
return Object.freeze({
|
||||||
|
target,
|
||||||
|
channel: resolveReleaseChannel(env, target),
|
||||||
|
bundleRoot: path.join(
|
||||||
|
appRoot,
|
||||||
|
'src-tauri',
|
||||||
|
'target',
|
||||||
|
target,
|
||||||
|
'release',
|
||||||
|
'bundle',
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
const packageJsonPath = path.join(appRoot, 'package.json');
|
const packageJsonPath = path.join(appRoot, 'package.json');
|
||||||
const rootPackageLockPath = path.resolve(appRoot, '../..', 'package-lock.json');
|
const rootPackageLockPath = path.resolve(appRoot, '../..', 'package-lock.json');
|
||||||
const tauriConfigPath = path.join(appRoot, 'src-tauri', 'tauri.conf.json');
|
const tauriConfigPath = path.join(appRoot, 'src-tauri', 'tauri.conf.json');
|
||||||
@@ -50,6 +105,7 @@ export const agcReleasePathPatterns = [
|
|||||||
'packages/',
|
'packages/',
|
||||||
'server-rs/crates/',
|
'server-rs/crates/',
|
||||||
'plugins/agc-cocos-editor/',
|
'plugins/agc-cocos-editor/',
|
||||||
|
'plugins/agc-unity-editor/',
|
||||||
'apps/desktop-shell/src-tauri/icons/',
|
'apps/desktop-shell/src-tauri/icons/',
|
||||||
'package.json',
|
'package.json',
|
||||||
'package-lock.json',
|
'package-lock.json',
|
||||||
@@ -99,7 +155,7 @@ export function nextPatchVersion(localVersion, remoteVersion) {
|
|||||||
return `${major}.${minor}.${patch + 1}`;
|
return `${major}.${minor}.${patch + 1}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveReleasePlatform(target = releaseTarget) {
|
export function resolveReleasePlatform(target = defaultTarget()) {
|
||||||
if (target.includes('windows')) return 'windows';
|
if (target.includes('windows')) return 'windows';
|
||||||
if (target.includes('apple-darwin')) return 'darwin';
|
if (target.includes('apple-darwin')) return 'darwin';
|
||||||
if (target.includes('linux')) return 'linux';
|
if (target.includes('linux')) return 'linux';
|
||||||
@@ -108,7 +164,7 @@ export function resolveReleasePlatform(target = releaseTarget) {
|
|||||||
|
|
||||||
export function resolveReleaseChannel(
|
export function resolveReleaseChannel(
|
||||||
env = process.env,
|
env = process.env,
|
||||||
target = releaseTarget,
|
target = defaultTarget(),
|
||||||
) {
|
) {
|
||||||
const platform = resolveReleasePlatform(target);
|
const platform = resolveReleasePlatform(target);
|
||||||
const requested = env.AGC_UPDATE_CHANNEL?.trim();
|
const requested = env.AGC_UPDATE_CHANNEL?.trim();
|
||||||
@@ -142,13 +198,10 @@ export function updateManifestUrl(channel = resolveReleaseChannel()) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新插件按运行时平台键查找清单条目:universal macOS 包同时挂
|
* 单架构产物只登记实际目标,不能把同一原生资源映射为另一架构。
|
||||||
* `darwin-aarch64` 与 `darwin-x86_64`,单架构目标只挂对应键。
|
|
||||||
*/
|
*/
|
||||||
export function resolveManifestPlatformKeys(target = releaseTarget) {
|
export function resolveManifestPlatformKeys(target = defaultTarget()) {
|
||||||
if (target === 'universal-apple-darwin') {
|
validateReleaseTarget(target);
|
||||||
return ['darwin-aarch64', 'darwin-x86_64'];
|
|
||||||
}
|
|
||||||
if (target === 'aarch64-apple-darwin') return ['darwin-aarch64'];
|
if (target === 'aarch64-apple-darwin') return ['darwin-aarch64'];
|
||||||
if (target === 'x86_64-apple-darwin') return ['darwin-x86_64'];
|
if (target === 'x86_64-apple-darwin') return ['darwin-x86_64'];
|
||||||
if (target.includes('windows')) {
|
if (target.includes('windows')) {
|
||||||
@@ -256,8 +309,8 @@ function replaceVersionLine(source, version, pattern, label) {
|
|||||||
return source.replace(pattern, `$1${version}$3`);
|
return source.replace(pattern, `$1${version}$3`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function prepareReleaseVersion() {
|
export async function prepareReleaseVersion(context = resolveReleaseContext()) {
|
||||||
const channel = resolveReleaseChannel();
|
const { channel } = context;
|
||||||
const localVersion = parseVersion(readPackageJson().version, '本地版本');
|
const localVersion = parseVersion(readPackageJson().version, '本地版本');
|
||||||
const remoteVersion = await resolveRemoteHighWaterVersion(channel);
|
const remoteVersion = await resolveRemoteHighWaterVersion(channel);
|
||||||
const requestedVersion = process.env.AGC_RELEASE_VERSION?.trim();
|
const requestedVersion = process.env.AGC_RELEASE_VERSION?.trim();
|
||||||
@@ -330,18 +383,14 @@ export async function prepareReleaseVersion() {
|
|||||||
|
|
||||||
export function buildTauriBuildArguments(
|
export function buildTauriBuildArguments(
|
||||||
args = [],
|
args = [],
|
||||||
target = releaseTarget,
|
target = defaultTarget(),
|
||||||
platform = process.platform,
|
platform = process.platform,
|
||||||
) {
|
) {
|
||||||
const noBundle = args.includes('--no-bundle');
|
const noBundle = args.includes('--no-bundle');
|
||||||
const targetIndex = args.indexOf('--target');
|
const explicitTarget = explicitBuildTarget(args);
|
||||||
const explicitTarget =
|
|
||||||
targetIndex >= 0
|
|
||||||
? args[targetIndex + 1]
|
|
||||||
: args
|
|
||||||
.find((value) => value.startsWith('--target='))
|
|
||||||
?.slice('--target='.length);
|
|
||||||
const targetArgs = noBundle || explicitTarget ? [] : ['--target', target];
|
const targetArgs = noBundle || explicitTarget ? [] : ['--target', target];
|
||||||
|
if (!noBundle || explicitTarget)
|
||||||
|
validateReleaseTarget(explicitTarget || target);
|
||||||
const features = defaultEditorFeatures(
|
const features = defaultEditorFeatures(
|
||||||
explicitTarget || (noBundle ? platform : target),
|
explicitTarget || (noBundle ? platform : target),
|
||||||
);
|
);
|
||||||
@@ -374,18 +423,33 @@ function writeChannelConfigFile(channel) {
|
|||||||
return configPath;
|
return configPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function runTauriBuild(args = []) {
|
export function runTauriBuild(
|
||||||
const tauriArguments = buildTauriBuildArguments(args);
|
args = [],
|
||||||
if (!tauriArguments.includes('--config') && !tauriArguments.includes('-c')) {
|
context = resolveReleaseContext(args),
|
||||||
const channel = resolveReleaseChannel();
|
{ spawn = spawnSync } = {},
|
||||||
const configPath = writeChannelConfigFile(channel);
|
) {
|
||||||
console.log(
|
if (
|
||||||
`[ai-game-creator-shell] 渠道 ${channel} 端点配置:${configPath}`,
|
explicitBuildTarget(args) &&
|
||||||
);
|
explicitBuildTarget(args) !== context.target
|
||||||
tauriArguments.push('--config', configPath);
|
) {
|
||||||
|
throw new Error('构建参数与发布上下文目标不一致');
|
||||||
}
|
}
|
||||||
|
const tauriArguments = buildTauriBuildArguments(args, context.target);
|
||||||
|
const { channel } = context;
|
||||||
|
const configPath = writeChannelConfigFile(channel);
|
||||||
|
console.log(
|
||||||
|
`[ai-game-creator-shell] 渠道 ${channel} 端点配置:${configPath}`,
|
||||||
|
);
|
||||||
|
// 最后合并渠道配置,防止用户配置中的端点与实际发布目标分叉。
|
||||||
|
const separator = tauriArguments.indexOf('--');
|
||||||
|
tauriArguments.splice(
|
||||||
|
separator < 0 ? tauriArguments.length : separator,
|
||||||
|
0,
|
||||||
|
'--config',
|
||||||
|
configPath,
|
||||||
|
);
|
||||||
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
||||||
const result = spawnSync(
|
const result = spawn(
|
||||||
npmCommand,
|
npmCommand,
|
||||||
['--prefix', '../..', 'exec', 'tauri', '--', ...tauriArguments],
|
['--prefix', '../..', 'exec', 'tauri', '--', ...tauriArguments],
|
||||||
{ cwd: appRoot, stdio: 'inherit', shell: process.platform === 'win32' },
|
{ cwd: appRoot, stdio: 'inherit', shell: process.platform === 'win32' },
|
||||||
@@ -402,11 +466,11 @@ function listFiles(root) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function artifactPriority(filePath) {
|
function artifactPriority(filePath, target) {
|
||||||
const name = path.basename(filePath).toLowerCase();
|
const name = path.basename(filePath).toLowerCase();
|
||||||
if (releaseTarget.includes('windows')) return name.endsWith('.exe') ? 0 : 99;
|
if (target.includes('windows')) return name.endsWith('.exe') ? 0 : 99;
|
||||||
// 更新链路要的是 updater 产物(macOS 为 .app.tar.gz),dmg 只作人工分发。
|
// 更新链路要的是 updater 产物(macOS 为 .app.tar.gz),dmg 只作人工分发。
|
||||||
if (releaseTarget.includes('apple-darwin')) {
|
if (target.includes('apple-darwin')) {
|
||||||
return name.endsWith('.app.tar.gz') ? 0 : 99;
|
return name.endsWith('.app.tar.gz') ? 0 : 99;
|
||||||
}
|
}
|
||||||
if (name.endsWith('.appimage.tar.gz')) return 0;
|
if (name.endsWith('.appimage.tar.gz')) return 0;
|
||||||
@@ -416,7 +480,8 @@ function artifactPriority(filePath) {
|
|||||||
return 99;
|
return 99;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectReleaseArtifact(files) {
|
export function selectReleaseArtifact(files, target = defaultTarget()) {
|
||||||
|
validateReleaseTarget(target);
|
||||||
const explicit = process.env.AGC_UPDATE_ARTIFACT?.trim();
|
const explicit = process.env.AGC_UPDATE_ARTIFACT?.trim();
|
||||||
if (explicit) {
|
if (explicit) {
|
||||||
const resolved = path.resolve(explicit);
|
const resolved = path.resolve(explicit);
|
||||||
@@ -427,14 +492,50 @@ export function selectReleaseArtifact(files) {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
[...files]
|
[...files]
|
||||||
.filter((filePath) => artifactPriority(filePath) < 99)
|
.filter((filePath) => artifactPriority(filePath, target) < 99)
|
||||||
.sort((left, right) => {
|
.sort((left, right) => {
|
||||||
const priority = artifactPriority(left) - artifactPriority(right);
|
const priority =
|
||||||
|
artifactPriority(left, target) - artifactPriority(right, target);
|
||||||
return priority || left.localeCompare(right);
|
return priority || left.localeCompare(right);
|
||||||
})[0] ?? null
|
})[0] ?? null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function selectFirstInstallArtifact(
|
||||||
|
files,
|
||||||
|
{ target, version, artifact },
|
||||||
|
) {
|
||||||
|
validateReleaseTarget(target);
|
||||||
|
let selected;
|
||||||
|
if (target.includes('windows')) {
|
||||||
|
selected = artifact;
|
||||||
|
if (!selected?.endsWith('.exe')) {
|
||||||
|
throw new Error('Windows 首装包必须复用本次 NSIS .exe 更新包');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Tauri DMG 文件名使用 aarch64 / x64,而 updater 的 Intel 平台键是 x86_64。
|
||||||
|
const architecture = target.startsWith('aarch64') ? 'aarch64' : 'x64';
|
||||||
|
const suffix = `_${version}_${architecture}.dmg`;
|
||||||
|
const candidates = files.filter((file) =>
|
||||||
|
path.basename(file).endsWith(suffix),
|
||||||
|
);
|
||||||
|
if (candidates.length !== 1) {
|
||||||
|
throw new Error(
|
||||||
|
`首装 DMG 必须唯一匹配本次版本 ${version} 和架构 ${architecture},找到 ${candidates.length} 个`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
selected = candidates[0];
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!fs.existsSync(selected) ||
|
||||||
|
!fs.statSync(selected).isFile() ||
|
||||||
|
fs.statSync(selected).size === 0
|
||||||
|
) {
|
||||||
|
throw new Error(`首装包不存在或为空:${selected}`);
|
||||||
|
}
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
|
||||||
function readUpdaterSignature(artifactPath) {
|
function readUpdaterSignature(artifactPath) {
|
||||||
const signaturePath = `${artifactPath}.sig`;
|
const signaturePath = `${artifactPath}.sig`;
|
||||||
if (!fs.existsSync(signaturePath)) {
|
if (!fs.existsSync(signaturePath)) {
|
||||||
@@ -450,26 +551,37 @@ function readUpdaterSignature(artifactPath) {
|
|||||||
export function createUpdateManifest(
|
export function createUpdateManifest(
|
||||||
artifactPath,
|
artifactPath,
|
||||||
{
|
{
|
||||||
channel = resolveReleaseChannel(),
|
target = defaultTarget(),
|
||||||
target = releaseTarget,
|
channel = resolveReleaseChannel(process.env, target),
|
||||||
publishedAt = new Date().toISOString(),
|
publishedAt = new Date().toISOString(),
|
||||||
notes = readReleaseNotes(),
|
notes = readReleaseNotes(),
|
||||||
commit = readHeadCommit(),
|
commit = readHeadCommit(),
|
||||||
|
downloadArtifact,
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
|
validateReleaseTarget(target);
|
||||||
|
resolveReleaseChannel({ AGC_UPDATE_CHANNEL: channel }, target);
|
||||||
const signature = readUpdaterSignature(artifactPath);
|
const signature = readUpdaterSignature(artifactPath);
|
||||||
const version = readPackageJson().version;
|
const version = readPackageJson().version;
|
||||||
|
const firstInstallArtifact = selectFirstInstallArtifact(
|
||||||
|
downloadArtifact ? [downloadArtifact] : [],
|
||||||
|
{ target, version, artifact: artifactPath },
|
||||||
|
);
|
||||||
const fileName = path.basename(artifactPath);
|
const fileName = path.basename(artifactPath);
|
||||||
const url = `${ossBaseUrl()}/${channel}/${encodeURIComponent(version)}/${encodeURIComponent(fileName)}`;
|
const url = `${ossBaseUrl()}/${channel}/${encodeURIComponent(version)}/${encodeURIComponent(fileName)}`;
|
||||||
|
const downloadUrl = `${ossBaseUrl()}/${channel}/${encodeURIComponent(version)}/${encodeURIComponent(path.basename(firstInstallArtifact))}`;
|
||||||
const platforms = {};
|
const platforms = {};
|
||||||
|
const downloads = {};
|
||||||
for (const key of resolveManifestPlatformKeys(target)) {
|
for (const key of resolveManifestPlatformKeys(target)) {
|
||||||
platforms[key] = { signature, url };
|
platforms[key] = { signature, url };
|
||||||
|
downloads[key] = { url: downloadUrl };
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
version,
|
version,
|
||||||
...(notes ? { notes } : {}),
|
...(notes ? { notes } : {}),
|
||||||
pub_date: publishedAt,
|
pub_date: publishedAt,
|
||||||
platforms,
|
platforms,
|
||||||
|
downloads,
|
||||||
// 非标准字段:更新插件会忽略,发布脚本用它定位下一次自动更新摘要的起点。
|
// 非标准字段:更新插件会忽略,发布脚本用它定位下一次自动更新摘要的起点。
|
||||||
...(commit ? { commit } : {}),
|
...(commit ? { commit } : {}),
|
||||||
};
|
};
|
||||||
@@ -604,12 +716,20 @@ export function createLegacyUpdateManifest(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateUpdateManifest() {
|
export async function generateUpdateManifest(
|
||||||
const channel = resolveReleaseChannel();
|
context = resolveReleaseContext(),
|
||||||
const artifact = selectReleaseArtifact(listFiles(bundleRoot));
|
) {
|
||||||
|
const { channel, target, bundleRoot } = context;
|
||||||
|
const files = listFiles(bundleRoot);
|
||||||
|
const artifact = selectReleaseArtifact(files, target);
|
||||||
if (!artifact) {
|
if (!artifact) {
|
||||||
throw new Error(`未找到可发布的 AGC 安装包:${bundleRoot}`);
|
throw new Error(`未找到可发布的 AGC 安装包:${bundleRoot}`);
|
||||||
}
|
}
|
||||||
|
const downloadArtifact = selectFirstInstallArtifact(files, {
|
||||||
|
target,
|
||||||
|
version: readPackageJson().version,
|
||||||
|
artifact,
|
||||||
|
});
|
||||||
const manualNotes = readReleaseNotes();
|
const manualNotes = readReleaseNotes();
|
||||||
const previousCommit = await resolvePreviousReleaseCommit(channel);
|
const previousCommit = await resolvePreviousReleaseCommit(channel);
|
||||||
const commits = collectReleaseCommits(previousCommit);
|
const commits = collectReleaseCommits(previousCommit);
|
||||||
@@ -623,7 +743,12 @@ export async function generateUpdateManifest() {
|
|||||||
`[ai-game-creator-shell] 未生成自动更新摘要(上一发布 commit=${previousCommit ?? '未知'},客户端相关提交=${commits ? commits.length : '不可判定'},最近提交=${recentCommits ? recentCommits.length : '不可判定'})`,
|
`[ai-game-creator-shell] 未生成自动更新摘要(上一发布 commit=${previousCommit ?? '未知'},客户端相关提交=${commits ? commits.length : '不可判定'},最近提交=${recentCommits ? recentCommits.length : '不可判定'})`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const manifest = createUpdateManifest(artifact, { channel, notes });
|
const manifest = createUpdateManifest(artifact, {
|
||||||
|
channel,
|
||||||
|
target,
|
||||||
|
notes,
|
||||||
|
downloadArtifact,
|
||||||
|
});
|
||||||
const manifestPath = path.join(bundleRoot, 'latest.json');
|
const manifestPath = path.join(bundleRoot, 'latest.json');
|
||||||
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
||||||
const notesPath = path.join(bundleRoot, 'release-notes.txt');
|
const notesPath = path.join(bundleRoot, 'release-notes.txt');
|
||||||
@@ -648,6 +773,7 @@ export async function generateUpdateManifest() {
|
|||||||
`[ai-game-creator-shell] 渠道 ${channel}:已生成 ${manifestPath}`,
|
`[ai-game-creator-shell] 渠道 ${channel}:已生成 ${manifestPath}`,
|
||||||
);
|
);
|
||||||
console.log(`[ai-game-creator-shell] 安装包:${artifact}`);
|
console.log(`[ai-game-creator-shell] 安装包:${artifact}`);
|
||||||
|
console.log(`[ai-game-creator-shell] 首装包:${downloadArtifact}`);
|
||||||
console.log(
|
console.log(
|
||||||
manualNotes
|
manualNotes
|
||||||
? '[ai-game-creator-shell] 更新摘要:使用 AGC_UPDATE_RELEASE_NOTES 手动文案'
|
? '[ai-game-creator-shell] 更新摘要:使用 AGC_UPDATE_RELEASE_NOTES 手动文案'
|
||||||
@@ -664,6 +790,7 @@ export async function generateUpdateManifest() {
|
|||||||
return {
|
return {
|
||||||
channel,
|
channel,
|
||||||
artifact,
|
artifact,
|
||||||
|
downloadArtifact,
|
||||||
manifest,
|
manifest,
|
||||||
manifestPath,
|
manifestPath,
|
||||||
notes,
|
notes,
|
||||||
@@ -675,12 +802,24 @@ export async function generateUpdateManifest() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function buildRelease(
|
||||||
|
args = [],
|
||||||
|
{
|
||||||
|
prepareVersion = prepareReleaseVersion,
|
||||||
|
build = runTauriBuild,
|
||||||
|
generateManifest = generateUpdateManifest,
|
||||||
|
} = {},
|
||||||
|
) {
|
||||||
|
const context = resolveReleaseContext(args);
|
||||||
|
if (!args.includes('--no-bundle')) await prepareVersion(context);
|
||||||
|
build(args, context);
|
||||||
|
if (!args.includes('--no-bundle')) return generateManifest(context);
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
process.argv[1] &&
|
process.argv[1] &&
|
||||||
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
||||||
) {
|
) {
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
if (!args.includes('--no-bundle')) await prepareReleaseVersion();
|
await buildRelease(args);
|
||||||
runTauriBuild(args);
|
|
||||||
if (!args.includes('--no-bundle')) await generateUpdateManifest();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import { fileURLToPath } from 'node:url';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
agcReleasePathPatterns,
|
agcReleasePathPatterns,
|
||||||
|
buildRelease,
|
||||||
|
buildTauriBuildArguments,
|
||||||
collectRecentReleaseCommits,
|
collectRecentReleaseCommits,
|
||||||
collectReleaseCommits,
|
collectReleaseCommits,
|
||||||
compareVersions,
|
compareVersions,
|
||||||
@@ -22,17 +24,46 @@ import {
|
|||||||
createUpdateManifest,
|
createUpdateManifest,
|
||||||
formatRecentReleaseNotes,
|
formatRecentReleaseNotes,
|
||||||
formatReleaseNotes,
|
formatReleaseNotes,
|
||||||
|
generateUpdateManifest,
|
||||||
nextPatchVersion,
|
nextPatchVersion,
|
||||||
resolveManifestPlatformKeys,
|
resolveManifestPlatformKeys,
|
||||||
resolvePreviousReleaseCommit,
|
resolvePreviousReleaseCommit,
|
||||||
resolveReleaseChannel,
|
resolveReleaseChannel,
|
||||||
|
resolveReleaseContext,
|
||||||
resolveRemoteHighWaterVersion,
|
resolveRemoteHighWaterVersion,
|
||||||
|
runTauriBuild,
|
||||||
|
selectFirstInstallArtifact,
|
||||||
selectReleaseArtifact,
|
selectReleaseArtifact,
|
||||||
updateManifestUrl,
|
updateManifestUrl,
|
||||||
} from './build-release.mjs';
|
} from './build-release.mjs';
|
||||||
|
|
||||||
const windowsTarget = 'x86_64-pc-windows-msvc';
|
const windowsTarget = 'x86_64-pc-windows-msvc';
|
||||||
const universalTarget = 'universal-apple-darwin';
|
const universalTarget = 'universal-apple-darwin';
|
||||||
|
const packageVersion = JSON.parse(
|
||||||
|
readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
|
||||||
|
).version;
|
||||||
|
|
||||||
|
function createDmgFixture(root, target, version = packageVersion) {
|
||||||
|
const architecture = target.startsWith('aarch64') ? 'aarch64' : 'x64';
|
||||||
|
const dmg = path.join(root, `陶泥儿_${version}_${architecture}.dmg`);
|
||||||
|
writeFileSync(dmg, 'first installation disk image');
|
||||||
|
return dmg;
|
||||||
|
}
|
||||||
|
|
||||||
|
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']) {
|
||||||
|
assert.deepEqual(buildTauriBuildArguments([], target), [
|
||||||
|
'build',
|
||||||
|
'--target',
|
||||||
|
target,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function withEnv(overrides, run) {
|
function withEnv(overrides, run) {
|
||||||
const previous = new Map();
|
const previous = new Map();
|
||||||
@@ -132,9 +163,12 @@ test('channel manifest URL and build-time endpoint follow the channel', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('universal macOS builds publish one artifact under both platform keys', () => {
|
test('macOS manifests only advertise the architecture actually built', () => {
|
||||||
assert.deepEqual(resolveManifestPlatformKeys(universalTarget), [
|
assert.throws(() => resolveManifestPlatformKeys(universalTarget), /单架构/);
|
||||||
|
assert.deepEqual(resolveManifestPlatformKeys('aarch64-apple-darwin'), [
|
||||||
'darwin-aarch64',
|
'darwin-aarch64',
|
||||||
|
]);
|
||||||
|
assert.deepEqual(resolveManifestPlatformKeys('x86_64-apple-darwin'), [
|
||||||
'darwin-x86_64',
|
'darwin-x86_64',
|
||||||
]);
|
]);
|
||||||
assert.deepEqual(resolveManifestPlatformKeys(windowsTarget), [
|
assert.deepEqual(resolveManifestPlatformKeys(windowsTarget), [
|
||||||
@@ -142,6 +176,317 @@ test('universal macOS builds publish one artifact under both platform keys', ()
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('release context resolves explicit targets before environment/default and fails closed', () => {
|
||||||
|
for (const args of [
|
||||||
|
['--target', 'aarch64-apple-darwin'],
|
||||||
|
['--target=aarch64-apple-darwin'],
|
||||||
|
['-t', 'aarch64-apple-darwin'],
|
||||||
|
]) {
|
||||||
|
for (const env of [{}, { AGC_BUILD_TARGET: windowsTarget }]) {
|
||||||
|
const context = resolveReleaseContext(args, env);
|
||||||
|
assert.equal(context.target, 'aarch64-apple-darwin');
|
||||||
|
assert.equal(context.channel, 'dev-mac');
|
||||||
|
assert.match(
|
||||||
|
context.bundleRoot.replaceAll('\\', '/'),
|
||||||
|
/target\/aarch64-apple-darwin\/release\/bundle$/,
|
||||||
|
);
|
||||||
|
assert.ok(Object.isFrozen(context));
|
||||||
|
}
|
||||||
|
assert.throws(
|
||||||
|
() => resolveReleaseContext(args, { AGC_UPDATE_CHANNEL: 'dev-win' }),
|
||||||
|
/只能用于 windows/,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert.equal(resolveReleaseContext([], {}).target, windowsTarget);
|
||||||
|
assert.equal(
|
||||||
|
resolveReleaseContext([], { AGC_BUILD_TARGET: 'x86_64-apple-darwin' })
|
||||||
|
.channel,
|
||||||
|
'dev-mac',
|
||||||
|
);
|
||||||
|
for (const args of [
|
||||||
|
['--target'],
|
||||||
|
['--target='],
|
||||||
|
['--target', '--no-bundle'],
|
||||||
|
['--target', windowsTarget, '--target=aarch64-apple-darwin'],
|
||||||
|
['--target', universalTarget],
|
||||||
|
['--target', 'unknown'],
|
||||||
|
])
|
||||||
|
assert.throws(() => resolveReleaseContext(args, {}));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('explicit macOS target drives version lookup, Tauri endpoint, artifact and manifest together', async () => {
|
||||||
|
const calls = [];
|
||||||
|
const seenContexts = [];
|
||||||
|
await withStubbedFetch(
|
||||||
|
(url) => {
|
||||||
|
calls.push(url);
|
||||||
|
assert.match(url, /\/dev-mac\/latest\.json$/);
|
||||||
|
return jsonResponse({ version: '0.1.67' });
|
||||||
|
},
|
||||||
|
() =>
|
||||||
|
withEnv(
|
||||||
|
{ AGC_BUILD_TARGET: undefined, AGC_UPDATE_CHANNEL: undefined },
|
||||||
|
() =>
|
||||||
|
buildRelease(['--target', 'aarch64-apple-darwin'], {
|
||||||
|
prepareVersion: async (context) => {
|
||||||
|
seenContexts.push(context);
|
||||||
|
assert.equal(
|
||||||
|
await resolveRemoteHighWaterVersion(context.channel),
|
||||||
|
'0.1.67',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
build: (args, context) => {
|
||||||
|
seenContexts.push(context);
|
||||||
|
runTauriBuild(args, context, {
|
||||||
|
spawn: (_binary, command) => {
|
||||||
|
const configIndex = command.lastIndexOf('--config');
|
||||||
|
const config = JSON.parse(
|
||||||
|
readFileSync(command[configIndex + 1], 'utf8'),
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
config.plugins.updater.endpoints[0],
|
||||||
|
/\/dev-mac\/latest\.json$/,
|
||||||
|
);
|
||||||
|
assert.ok(command.includes('aarch64-apple-darwin'));
|
||||||
|
assert.ok(
|
||||||
|
!command.includes('--features=cocos-editor-execute'),
|
||||||
|
);
|
||||||
|
return { status: 0 };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
generateManifest: (context) => {
|
||||||
|
seenContexts.push(context);
|
||||||
|
withSignedArtifact('陶泥儿.app.tar.gz', (artifact) => {
|
||||||
|
assert.equal(
|
||||||
|
selectReleaseArtifact(
|
||||||
|
['/tmp/win.exe', artifact, '/tmp/mac.dmg'],
|
||||||
|
context.target,
|
||||||
|
),
|
||||||
|
artifact,
|
||||||
|
);
|
||||||
|
const manifest = createUpdateManifest(artifact, {
|
||||||
|
...context,
|
||||||
|
downloadArtifact: createDmgFixture(
|
||||||
|
path.dirname(artifact),
|
||||||
|
context.target,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
assert.deepEqual(Object.keys(manifest.platforms), [
|
||||||
|
'darwin-aarch64',
|
||||||
|
]);
|
||||||
|
assert.match(
|
||||||
|
manifest.platforms['darwin-aarch64'].url,
|
||||||
|
/\/dev-mac\//,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert.equal(calls.length, 1, 'Mac 不应读取 Windows 迁移指针');
|
||||||
|
assert.equal(seenContexts.length, 3);
|
||||||
|
assert.ok(seenContexts.every((context) => context === seenContexts[0]));
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const target of ['aarch64-apple-darwin', 'x86_64-apple-darwin']) {
|
||||||
|
test(`real manifest writer publishes the ${target} updater and first installer separately`, async () => {
|
||||||
|
const root = mkdtempSync(path.join(os.tmpdir(), 'agc-mac-manifest-'));
|
||||||
|
try {
|
||||||
|
const artifact = path.join(root, '陶泥儿.app.tar.gz');
|
||||||
|
writeFileSync(artifact, 'mac package');
|
||||||
|
writeFileSync(`${artifact}.sig`, 'mac signature');
|
||||||
|
writeFileSync(path.join(root, 'windows.exe'), 'wrong platform');
|
||||||
|
const downloadArtifact = createDmgFixture(root, target);
|
||||||
|
const context = {
|
||||||
|
...resolveReleaseContext([`--target=${target}`], {}),
|
||||||
|
bundleRoot: root,
|
||||||
|
};
|
||||||
|
const result = await withStubbedFetch(
|
||||||
|
(url) => {
|
||||||
|
assert.match(url, /\/dev-mac\/latest\.json$/);
|
||||||
|
return jsonResponse({}, 404);
|
||||||
|
},
|
||||||
|
() => generateUpdateManifest(context),
|
||||||
|
);
|
||||||
|
assert.equal(result.artifact, artifact);
|
||||||
|
assert.equal(result.downloadArtifact, downloadArtifact);
|
||||||
|
assert.equal(result.manifestPath, path.join(root, 'latest.json'));
|
||||||
|
assert.equal(result.legacyManifestPath, null);
|
||||||
|
const key = target.startsWith('aarch64')
|
||||||
|
? 'darwin-aarch64'
|
||||||
|
: 'darwin-x86_64';
|
||||||
|
assert.deepEqual(Object.keys(result.manifest.platforms), [key]);
|
||||||
|
assert.deepEqual(Object.keys(result.manifest.downloads), [key]);
|
||||||
|
assert.match(
|
||||||
|
result.manifest.platforms[key].url,
|
||||||
|
/\/dev-mac\/.*\.app\.tar\.gz$/,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
decodeURIComponent(
|
||||||
|
new URL(result.manifest.downloads[key].url).pathname,
|
||||||
|
),
|
||||||
|
`/agc/dev-mac/${packageVersion}/${path.basename(downloadArtifact)}`,
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
JSON.parse(readFileSync(result.manifestPath, 'utf8')),
|
||||||
|
result.manifest,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('DMG selection ignores other versions and architectures but rejects missing, empty and ambiguous current packages', () => {
|
||||||
|
const root = mkdtempSync(path.join(os.tmpdir(), 'agc-dmg-selection-'));
|
||||||
|
try {
|
||||||
|
const target = 'aarch64-apple-darwin';
|
||||||
|
const options = {
|
||||||
|
target,
|
||||||
|
version: '2.3.4',
|
||||||
|
artifact: path.join(root, '陶泥儿.app.tar.gz'),
|
||||||
|
};
|
||||||
|
const oldVersion = createDmgFixture(root, target, '2.3.3');
|
||||||
|
const wrongArchitecture = createDmgFixture(
|
||||||
|
root,
|
||||||
|
'x86_64-apple-darwin',
|
||||||
|
'2.3.4',
|
||||||
|
);
|
||||||
|
assert.throws(() => selectFirstInstallArtifact([], options), /找到 0 个/u);
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
selectFirstInstallArtifact([oldVersion, wrongArchitecture], options),
|
||||||
|
/找到 0 个/u,
|
||||||
|
);
|
||||||
|
const current = createDmgFixture(root, target, '2.3.4');
|
||||||
|
assert.equal(
|
||||||
|
selectFirstInstallArtifact(
|
||||||
|
[oldVersion, wrongArchitecture, current],
|
||||||
|
options,
|
||||||
|
),
|
||||||
|
current,
|
||||||
|
);
|
||||||
|
writeFileSync(current, '');
|
||||||
|
assert.throws(
|
||||||
|
() => selectFirstInstallArtifact([current], options),
|
||||||
|
/不存在或为空/u,
|
||||||
|
);
|
||||||
|
writeFileSync(current, 'valid dmg');
|
||||||
|
const second = path.join(root, '另一包_2.3.4_aarch64.dmg');
|
||||||
|
writeFileSync(second, 'ambiguous dmg');
|
||||||
|
assert.throws(
|
||||||
|
() => selectFirstInstallArtifact([current, second], options),
|
||||||
|
/找到 2 个/u,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('manifest writer refuses to create latest when the current Mac DMG is missing', async () => {
|
||||||
|
const root = mkdtempSync(path.join(os.tmpdir(), 'agc-missing-dmg-'));
|
||||||
|
try {
|
||||||
|
const artifact = path.join(root, '陶泥儿.app.tar.gz');
|
||||||
|
writeFileSync(artifact, 'updater archive');
|
||||||
|
writeFileSync(`${artifact}.sig`, 'signature');
|
||||||
|
const context = {
|
||||||
|
...resolveReleaseContext(['--target=aarch64-apple-darwin'], {}),
|
||||||
|
bundleRoot: root,
|
||||||
|
};
|
||||||
|
await assert.rejects(
|
||||||
|
() => generateUpdateManifest(context),
|
||||||
|
/首装 DMG 必须唯一匹配/u,
|
||||||
|
);
|
||||||
|
assert.throws(() => readFileSync(path.join(root, 'latest.json')), {
|
||||||
|
code: 'ENOENT',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('invalid target or mismatched channel fails before any release side effect', async () => {
|
||||||
|
let touched = false;
|
||||||
|
const sideEffects = {
|
||||||
|
prepareVersion: () => {
|
||||||
|
touched = true;
|
||||||
|
},
|
||||||
|
build: () => {
|
||||||
|
touched = true;
|
||||||
|
},
|
||||||
|
generateManifest: () => {
|
||||||
|
touched = true;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await assert.rejects(
|
||||||
|
() => buildRelease(['--target', universalTarget], sideEffects),
|
||||||
|
/单架构/,
|
||||||
|
);
|
||||||
|
await withEnv({ AGC_UPDATE_CHANNEL: 'dev-win' }, () =>
|
||||||
|
assert.rejects(
|
||||||
|
() => buildRelease(['--target=aarch64-apple-darwin'], sideEffects),
|
||||||
|
/只能用于 windows/,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert.equal(touched, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
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 [
|
||||||
|
resolveReleaseContext([], {}),
|
||||||
|
resolveReleaseContext(['--target', windowsTarget], {
|
||||||
|
AGC_BUILD_TARGET: 'aarch64-apple-darwin',
|
||||||
|
}),
|
||||||
|
]) {
|
||||||
|
assert.equal(context.channel, 'dev-win');
|
||||||
|
assert.equal(
|
||||||
|
selectReleaseArtifact(files, context.target),
|
||||||
|
'/tmp/windows.exe',
|
||||||
|
);
|
||||||
|
runTauriBuild(
|
||||||
|
['--target', windowsTarget, '--config', 'user-config.json'],
|
||||||
|
context,
|
||||||
|
{
|
||||||
|
spawn: (_binary, command) => {
|
||||||
|
assert.ok(
|
||||||
|
command.includes(
|
||||||
|
'--features=cocos-editor-execute,unity-editor-execute',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert.ok(command.includes('user-config.json'));
|
||||||
|
const configIndex = command.lastIndexOf('--config');
|
||||||
|
const config = JSON.parse(
|
||||||
|
readFileSync(command[configIndex + 1], 'utf8'),
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
config.plugins.updater.endpoints[0],
|
||||||
|
/\/dev-win\/latest\.json$/,
|
||||||
|
);
|
||||||
|
return { status: 0 };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('no-bundle smoke skips version writes and manifest generation', async () => {
|
||||||
|
const steps = [];
|
||||||
|
await buildRelease(['--no-bundle', '--target=aarch64-apple-darwin'], {
|
||||||
|
prepareVersion: () => {
|
||||||
|
steps.push('version');
|
||||||
|
},
|
||||||
|
build: (_args, context) => {
|
||||||
|
steps.push(context.channel);
|
||||||
|
},
|
||||||
|
generateManifest: () => {
|
||||||
|
steps.push('manifest');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert.deepEqual(steps, ['dev-mac']);
|
||||||
|
});
|
||||||
|
|
||||||
test('channel manifest carries version, platform keys and signature', () => {
|
test('channel manifest carries version, platform keys and signature', () => {
|
||||||
withSignedArtifact('陶泥儿_0.1.48_x64-setup.exe', (artifact) => {
|
withSignedArtifact('陶泥儿_0.1.48_x64-setup.exe', (artifact) => {
|
||||||
withEnv({ AGC_UPDATE_RELEASE_NOTES: '修复与改进' }, () => {
|
withEnv({ AGC_UPDATE_RELEASE_NOTES: '修复与改进' }, () => {
|
||||||
@@ -154,6 +499,9 @@ test('channel manifest carries version, platform keys and signature', () => {
|
|||||||
assert.equal(manifest.notes, '修复与改进');
|
assert.equal(manifest.notes, '修复与改进');
|
||||||
assert.equal(manifest.pub_date, '2026-09-17T00:00:00.000Z');
|
assert.equal(manifest.pub_date, '2026-09-17T00:00:00.000Z');
|
||||||
assert.deepEqual(Object.keys(manifest.platforms), ['windows-x86_64']);
|
assert.deepEqual(Object.keys(manifest.platforms), ['windows-x86_64']);
|
||||||
|
assert.deepEqual(manifest.downloads, {
|
||||||
|
'windows-x86_64': { url: manifest.platforms['windows-x86_64'].url },
|
||||||
|
});
|
||||||
assert.equal(
|
assert.equal(
|
||||||
manifest.platforms['windows-x86_64'].signature,
|
manifest.platforms['windows-x86_64'].signature,
|
||||||
'signature-content',
|
'signature-content',
|
||||||
@@ -334,17 +682,18 @@ test('recent commit fallback marks that entries may repeat the previous release'
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('release upload forces overwrite for artifact, signature and channel pointers', () => {
|
test('release entry forwards the built artifacts and dry-run mode to the uploader', () => {
|
||||||
const source = readFileSync(
|
const source = readFileSync(
|
||||||
new URL('./release-upload.mjs', import.meta.url),
|
new URL('./release-upload.mjs', import.meta.url),
|
||||||
'utf8',
|
'utf8',
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.match(
|
||||||
(source.match(/runOssutil\(\[\s*'cp',\s*'--force'/gu) ?? []).length,
|
source,
|
||||||
4,
|
/const release = await buildRelease\(process\.argv\.slice\(2\)\)/u,
|
||||||
);
|
);
|
||||||
assert.match(source, /agc\/\$\{channel\}\/latest\.json/u);
|
assert.match(source, /uploadReleaseArtifacts\(release, \{/u);
|
||||||
assert.match(source, /agc\/latest\.json/u);
|
assert.match(source, /const dryRun = readReleaseDryRun\(\);/u);
|
||||||
|
assert.ok(source.includes('\n dryRun,\n'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('release notes list client commits with short sha and bound their size', () => {
|
test('release notes list client commits with short sha and bound their size', () => {
|
||||||
|
|||||||
@@ -19,6 +19,6 @@ export function withDefaultCargoFeatures(argv, features) {
|
|||||||
|
|
||||||
export function defaultEditorFeatures(target) {
|
export function defaultEditorFeatures(target) {
|
||||||
return target === 'win32' || target.includes('windows')
|
return target === 'win32' || target.includes('windows')
|
||||||
? ['cocos-editor-execute']
|
? ['cocos-editor-execute', 'unity-editor-execute']
|
||||||
: [];
|
: [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ test('Windows release includes the same editor feature as development', () => {
|
|||||||
buildTauriBuildArguments([], 'x86_64-pc-windows-msvc', 'win32'),
|
buildTauriBuildArguments([], 'x86_64-pc-windows-msvc', 'win32'),
|
||||||
[
|
[
|
||||||
'build',
|
'build',
|
||||||
'--features=cocos-editor-execute',
|
'--features=cocos-editor-execute,unity-editor-execute',
|
||||||
'--target',
|
'--target',
|
||||||
'x86_64-pc-windows-msvc',
|
'x86_64-pc-windows-msvc',
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -35,6 +35,12 @@ const windowsTauriConfig = JSON.parse(
|
|||||||
'utf8',
|
'utf8',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
const macosTauriConfig = JSON.parse(
|
||||||
|
fs.readFileSync(
|
||||||
|
new URL('../src-tauri/tauri.macos.conf.json', import.meta.url),
|
||||||
|
'utf8',
|
||||||
|
),
|
||||||
|
);
|
||||||
const cargoManifestSource = fs.readFileSync(
|
const cargoManifestSource = fs.readFileSync(
|
||||||
new URL('../src-tauri/Cargo.toml', import.meta.url),
|
new URL('../src-tauri/Cargo.toml', import.meta.url),
|
||||||
'utf8',
|
'utf8',
|
||||||
@@ -137,7 +143,6 @@ const allowedUncalledTauriCommands = [
|
|||||||
'reload_agc_plugin',
|
'reload_agc_plugin',
|
||||||
'call_agc_plugin',
|
'call_agc_plugin',
|
||||||
'read_agc_plugin_panel',
|
'read_agc_plugin_panel',
|
||||||
'set_agc_plugin_project_path',
|
|
||||||
'set_agc_plugin_enabled',
|
'set_agc_plugin_enabled',
|
||||||
];
|
];
|
||||||
const sourceExtensions = new Set([
|
const sourceExtensions = new Set([
|
||||||
@@ -1358,6 +1363,37 @@ if (windowsTauriConfig.bundle?.useLocalToolsDir !== true) {
|
|||||||
'AI game creator shell Windows Tauri config must cache bundling tools in the project target directory',
|
'AI game creator shell Windows Tauri config must cache bundling tools in the project target directory',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
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}`,
|
||||||
|
]),
|
||||||
|
['resources/plugins', 'plugins'],
|
||||||
|
]),
|
||||||
|
'macOS must bundle the complete native Codex layout and plugin workspace',
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
macosTauriConfig.plugins?.updater?.endpoints,
|
||||||
|
[
|
||||||
|
'https://agc-dev.oss-rg-china-mainland.aliyuncs.com/agc/dev-mac/latest.json',
|
||||||
|
],
|
||||||
|
'macOS local builds must not use the Windows update channel',
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
macosTauriConfig.bundle?.macOS?.minimumSystemVersion,
|
||||||
|
'15.0',
|
||||||
|
'macOS deployment baseline must cover the bundled native zsh requirement',
|
||||||
|
);
|
||||||
|
|
||||||
if (tauriConfig.app?.withGlobalTauri !== true) {
|
if (tauriConfig.app?.withGlobalTauri !== true) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -1722,7 +1758,7 @@ for (const snippet of [
|
|||||||
'fn append_local_permission_log_at(',
|
'fn append_local_permission_log_at(',
|
||||||
'"command.auto"',
|
'"command.auto"',
|
||||||
'GameCreationAppPermission::Auto',
|
'GameCreationAppPermission::Auto',
|
||||||
'GAME_CREATOR_BUNDLED_CODEX_CLI_RELATIVE_PATH',
|
'fn game_creator_bundled_codex_cli_path',
|
||||||
'validate_game_creator_bundled_codex_cli',
|
'validate_game_creator_bundled_codex_cli',
|
||||||
'内置 Codex CLI 完整性校验失败',
|
'内置 Codex CLI 完整性校验失败',
|
||||||
]) {
|
]) {
|
||||||
|
|||||||
@@ -0,0 +1,222 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { spawn, spawnSync } from 'node:child_process';
|
||||||
|
import { createHash } from 'node:crypto';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import os from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
// 只操作临时复制品;不启动 GUI、不读取开发机凭据、不访问 Provider。
|
||||||
|
assert.equal(process.platform, 'darwin', '此验证必须在 macOS 执行');
|
||||||
|
const source = path.resolve(process.argv[2] || '');
|
||||||
|
assert.ok(
|
||||||
|
source.endsWith('.app') && fs.statSync(source).isDirectory(),
|
||||||
|
'请传入 .app 绝对路径',
|
||||||
|
);
|
||||||
|
const root = fs.realpathSync(
|
||||||
|
fs.mkdtempSync(path.join(os.tmpdir(), 'agc-macos-bundle-')),
|
||||||
|
);
|
||||||
|
const app = path.join(root, '陶泥儿 隔离测试.app');
|
||||||
|
const home = path.join(root, 'home');
|
||||||
|
const config = path.join(root, 'config');
|
||||||
|
const tmp = path.join(root, 'tmp');
|
||||||
|
const codexHome = path.join(root, 'codex-home');
|
||||||
|
for (const directory of [home, config, tmp, codexHome]) {
|
||||||
|
fs.mkdirSync(directory, { mode: 0o700 });
|
||||||
|
}
|
||||||
|
const env = {
|
||||||
|
HOME: home,
|
||||||
|
PATH: '/usr/bin:/bin',
|
||||||
|
TMPDIR: tmp,
|
||||||
|
CODEX_HOME: codexHome,
|
||||||
|
};
|
||||||
|
|
||||||
|
function run(command, args) {
|
||||||
|
const result = spawnSync(command, args, {
|
||||||
|
cwd: root,
|
||||||
|
env,
|
||||||
|
encoding: 'utf8',
|
||||||
|
timeout: 30_000,
|
||||||
|
maxBuffer: 1024 * 1024,
|
||||||
|
});
|
||||||
|
assert.ifError(result.error);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hashFile(file) {
|
||||||
|
const hash = createHash('sha256');
|
||||||
|
for await (const chunk of fs.createReadStream(file)) hash.update(chunk);
|
||||||
|
return hash.digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handshake(executable) {
|
||||||
|
const child = spawn(executable, ['app-server'], {
|
||||||
|
cwd: root,
|
||||||
|
env,
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
|
});
|
||||||
|
let buffered = '';
|
||||||
|
let stderrBytes = 0;
|
||||||
|
try {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const timer = setTimeout(
|
||||||
|
() => reject(new Error('app-server 初始化超时')),
|
||||||
|
15_000,
|
||||||
|
);
|
||||||
|
const finish = (error) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
if (error) reject(error);
|
||||||
|
else resolve();
|
||||||
|
};
|
||||||
|
child.on('error', finish);
|
||||||
|
child.on('exit', (code) =>
|
||||||
|
finish(new Error(`app-server 提前退出 ${code}`)),
|
||||||
|
);
|
||||||
|
child.stderr.on('data', (chunk) => {
|
||||||
|
stderrBytes += chunk.length;
|
||||||
|
if (stderrBytes > 1024 * 1024)
|
||||||
|
finish(new Error('app-server stderr 超限'));
|
||||||
|
});
|
||||||
|
child.stdout.on('data', (chunk) => {
|
||||||
|
buffered += chunk.toString('utf8');
|
||||||
|
if (buffered.length > 1024 * 1024)
|
||||||
|
return finish(new Error('app-server stdout 超限'));
|
||||||
|
let end;
|
||||||
|
while ((end = buffered.indexOf('\n')) >= 0) {
|
||||||
|
const line = buffered.slice(0, end);
|
||||||
|
buffered = buffered.slice(end + 1);
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(line);
|
||||||
|
if (message.id !== 1) continue;
|
||||||
|
assert.ok(message.result?.userAgent, '初始化必须返回真实服务身份');
|
||||||
|
assert.equal(message.error, undefined);
|
||||||
|
child.stdin.write(`${JSON.stringify({ method: 'initialized' })}\n`);
|
||||||
|
finish();
|
||||||
|
} catch (error) {
|
||||||
|
finish(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
child.stdin.on('error', finish);
|
||||||
|
child.stdin.write(
|
||||||
|
`${JSON.stringify({
|
||||||
|
id: 1,
|
||||||
|
method: 'initialize',
|
||||||
|
params: {
|
||||||
|
clientInfo: {
|
||||||
|
name: 'agc_bundle_smoke',
|
||||||
|
title: 'AGC bundle smoke',
|
||||||
|
version: '1',
|
||||||
|
},
|
||||||
|
capabilities: { experimentalApi: true },
|
||||||
|
},
|
||||||
|
})}\n`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
if (child.exitCode === null && child.signalCode === null) {
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
const timer = setTimeout(() => child.kill('SIGKILL'), 3000);
|
||||||
|
child.once('exit', () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
child.kill('SIGTERM');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.cpSync(source, app, { recursive: true });
|
||||||
|
const resources = path.join(app, 'Contents/Resources');
|
||||||
|
const bundle = path.join(resources, 'coding-agent/mac-native');
|
||||||
|
const executable = path.join(bundle, 'bin/codex');
|
||||||
|
const main = path.join(
|
||||||
|
app,
|
||||||
|
'Contents/MacOS/genarrative-ai-game-creator-shell',
|
||||||
|
);
|
||||||
|
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.version, 'codex-cli 0.147.0');
|
||||||
|
const components = [
|
||||||
|
'bin/codex',
|
||||||
|
'bin/codex-code-mode-host',
|
||||||
|
'codex-path/rg',
|
||||||
|
'codex-resources/zsh/bin/zsh',
|
||||||
|
'codex-package.json',
|
||||||
|
];
|
||||||
|
assert.deepEqual(Object.keys(manifest.files).sort(), [...components].sort());
|
||||||
|
for (const component of components) {
|
||||||
|
const file = path.join(bundle, component);
|
||||||
|
assert.equal(await hashFile(file), manifest.files[component], component);
|
||||||
|
if (component !== 'codex-package.json') {
|
||||||
|
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.ok(fs.existsSync(path.join(bundle, 'NOTICE.md')));
|
||||||
|
const plugin = path.join(resources, 'plugins/agc-cocos-editor');
|
||||||
|
for (const file of [
|
||||||
|
'plugin.json',
|
||||||
|
'src/entry.mjs',
|
||||||
|
'panels/cocos-editor.html',
|
||||||
|
]) {
|
||||||
|
assert.ok(fs.existsSync(path.join(plugin, file)), file);
|
||||||
|
}
|
||||||
|
const packageFiles = fs.readdirSync(resources, { recursive: true });
|
||||||
|
assert.ok(
|
||||||
|
!packageFiles.some((file) =>
|
||||||
|
/(^|\/)(\.env[^/]*|auth\.json|node_modules|target|\.git)(\/|$)|\.(exe|dll)$/.test(
|
||||||
|
file,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert.equal(run(executable, ['--version']).stdout.trim(), manifest.version);
|
||||||
|
assert.equal(
|
||||||
|
run(path.join(bundle, 'codex-path/rg'), ['--version']).status,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
run(path.join(bundle, 'codex-resources/zsh/bin/zsh'), ['--version']).status,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 使用正式 AGC 查找/校验入口,而非只证明 sidecar 可以独立执行。
|
||||||
|
const status = run(main, ['--config-dir', config, '--llm-status']);
|
||||||
|
const statusText = `${status.stdout}\n${status.stderr}`;
|
||||||
|
assert.ok(!statusText.includes('Codex CLI 未安装'), statusText);
|
||||||
|
assert.ok(
|
||||||
|
statusText.includes('authentication-required'),
|
||||||
|
'隔离账号应仅被登录门禁拒绝',
|
||||||
|
);
|
||||||
|
await handshake(executable);
|
||||||
|
|
||||||
|
// 临时复制品缺少辅助程序时,正式入口必须拒绝内置程序;PATH 无全局 Codex 可兜底。
|
||||||
|
fs.renameSync(
|
||||||
|
path.join(bundle, 'bin/codex-code-mode-host'),
|
||||||
|
path.join(root, 'saved-code-mode-host'),
|
||||||
|
);
|
||||||
|
const broken = run(main, ['--config-dir', config, '--llm-status']);
|
||||||
|
assert.notEqual(broken.status, 0);
|
||||||
|
assert.match(`${broken.stdout}\n${broken.stderr}`, /Codex CLI 未安装/);
|
||||||
|
console.log(
|
||||||
|
'PASS: 隔离安装包资源、架构、摘要、权限、正式 Codex 查找、app-server 握手及缺组件拒绝',
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
'未验证:GUI、真实登录/Provider 对话、Cocos macOS 原生桥接;插件 Node 仍为外部前提',
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
@@ -290,7 +290,7 @@ function deterministicArtManifest(
|
|||||||
{
|
{
|
||||||
id: 'garden-guardians-spritesheet',
|
id: 'garden-guardians-spritesheet',
|
||||||
path: 'assets/art-spritesheet.png',
|
path: 'assets/art-spritesheet.png',
|
||||||
kind: 'art-spritesheet',
|
kind: 'icon-spritesheet',
|
||||||
usage: ['defenders', 'enemies', 'battlefield-ui'],
|
usage: ['defenders', 'enemies', 'battlefield-ui'],
|
||||||
source: 'canvas',
|
source: 'canvas',
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
@@ -710,7 +710,7 @@ function canvasAssetCall(agentId) {
|
|||||||
outputPath: 'assets/ui-prototype.png',
|
outputPath: 'assets/ui-prototype.png',
|
||||||
aspectRatio: '16:9',
|
aspectRatio: '16:9',
|
||||||
imageSize: '2K',
|
imageSize: '2K',
|
||||||
assetKind: 'ui-prototype',
|
assetKind: 'ui-design',
|
||||||
assetLabel: '游戏横屏界面原型图',
|
assetLabel: '游戏横屏界面原型图',
|
||||||
replaceExisting: false,
|
replaceExisting: false,
|
||||||
});
|
});
|
||||||
@@ -721,7 +721,7 @@ function canvasAssetCall(agentId) {
|
|||||||
outputPath: 'assets/art-spritesheet.png',
|
outputPath: 'assets/art-spritesheet.png',
|
||||||
aspectRatio: '1:1',
|
aspectRatio: '1:1',
|
||||||
imageSize: '1K',
|
imageSize: '1K',
|
||||||
assetKind: 'art-spritesheet',
|
assetKind: 'icon-spritesheet',
|
||||||
assetLabel: '游戏首版核心美术素材',
|
assetLabel: '游戏首版核心美术素材',
|
||||||
replaceExisting: false,
|
replaceExisting: false,
|
||||||
sliceMode: 'connected-components',
|
sliceMode: 'connected-components',
|
||||||
@@ -830,12 +830,12 @@ function missingGeneratedVisualAssetObservation(context, agentId) {
|
|||||||
},
|
},
|
||||||
'design-foundation': {
|
'design-foundation': {
|
||||||
path: 'assets/ui-prototype.png',
|
path: 'assets/ui-prototype.png',
|
||||||
kind: 'ui-prototype',
|
kind: 'ui-design',
|
||||||
summary: '策划界面原型图尚未按正式视觉流程生成并登记,不能完成任务',
|
summary: '策划界面原型图尚未按正式视觉流程生成并登记,不能完成任务',
|
||||||
},
|
},
|
||||||
'art-asset-plan': {
|
'art-asset-plan': {
|
||||||
path: 'assets/art-spritesheet.png',
|
path: 'assets/art-spritesheet.png',
|
||||||
kind: 'art-spritesheet',
|
kind: 'icon-spritesheet',
|
||||||
summary: '首版美术素材图尚未按正式视觉流程生成并登记,不能完成任务',
|
summary: '首版美术素材图尚未按正式视觉流程生成并登记,不能完成任务',
|
||||||
},
|
},
|
||||||
}[agentId];
|
}[agentId];
|
||||||
@@ -2667,7 +2667,7 @@ function createDeterministicCanvasFixture(apiKey) {
|
|||||||
asset: {
|
asset: {
|
||||||
assetId: `asset-${sliceImageId}`,
|
assetId: `asset-${sliceImageId}`,
|
||||||
assetObjectId: sliceAssetObjectId,
|
assetObjectId: sliceAssetObjectId,
|
||||||
assetKind: 'art-spritesheet-slice',
|
assetKind: 'icon',
|
||||||
projectId,
|
projectId,
|
||||||
taskId,
|
taskId,
|
||||||
},
|
},
|
||||||
@@ -2707,7 +2707,7 @@ function createDeterministicCanvasFixture(apiKey) {
|
|||||||
spritesheetAsset: {
|
spritesheetAsset: {
|
||||||
assetId: `asset-${imageId}`,
|
assetId: `asset-${imageId}`,
|
||||||
assetObjectId,
|
assetObjectId,
|
||||||
assetKind: 'art-spritesheet',
|
assetKind: 'icon-spritesheet',
|
||||||
projectId,
|
projectId,
|
||||||
taskId,
|
taskId,
|
||||||
},
|
},
|
||||||
@@ -2749,7 +2749,7 @@ function createDeterministicCanvasFixture(apiKey) {
|
|||||||
const assetObjectId = `asset-object-${imageId}`;
|
const assetObjectId = `asset-object-${imageId}`;
|
||||||
const resourceId = `resource-${imageId}`;
|
const resourceId = `resource-${imageId}`;
|
||||||
const assetKind =
|
const assetKind =
|
||||||
typeof body?.assetKind === 'string' ? body.assetKind : 'game-art';
|
typeof body?.assetKind === 'string' ? body.assetKind : 'image';
|
||||||
images.set(imageId, {
|
images.set(imageId, {
|
||||||
...image,
|
...image,
|
||||||
objectKey,
|
objectKey,
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发布上传的 OSS 命令行整理:把 ossutil 参数与凭据整理成可执行或可打印的形式,
|
* 发布上传的 OSS 命令行整理:把 ossutil 参数与凭据整理成可执行或可打印的形式,
|
||||||
* 便于在 dry-run 下核对将要执行的上传,同时保证任何输出都不回显凭据明文。
|
* 便于在 dry-run 下核对将要执行的上传,同时保证任何输出都不回显凭据明文。
|
||||||
@@ -25,3 +28,89 @@ export function formatOssutilCommand({ binary, args, endpoint, credentials }) {
|
|||||||
}
|
}
|
||||||
return parts.map(quoteArgument).join(' ');
|
return parts.map(quoteArgument).join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createReleaseUploadPlan(
|
||||||
|
{
|
||||||
|
artifact,
|
||||||
|
downloadArtifact,
|
||||||
|
channel,
|
||||||
|
manifest,
|
||||||
|
manifestPath,
|
||||||
|
legacyManifestPath,
|
||||||
|
},
|
||||||
|
bucket,
|
||||||
|
) {
|
||||||
|
if (!artifact || !downloadArtifact || !manifestPath || !manifest?.version) {
|
||||||
|
throw new Error('发布结果缺少更新包、首装包或清单');
|
||||||
|
}
|
||||||
|
const prefix = `oss://${bucket}/agc/${channel}`;
|
||||||
|
const artifacts = [
|
||||||
|
...new Set(
|
||||||
|
[artifact, `${artifact}.sig`, downloadArtifact].map((file) =>
|
||||||
|
path.resolve(file),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
const plan = artifacts.map((source) => ({
|
||||||
|
source,
|
||||||
|
destination: `${prefix}/${manifest.version}/${path.basename(source)}`,
|
||||||
|
}));
|
||||||
|
plan.push({ source: manifestPath, destination: `${prefix}/latest.json` });
|
||||||
|
if (legacyManifestPath) {
|
||||||
|
plan.push({
|
||||||
|
source: legacyManifestPath,
|
||||||
|
destination: `oss://${bucket}/agc/latest.json`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uploadReleaseArtifacts(
|
||||||
|
release,
|
||||||
|
{
|
||||||
|
bucket,
|
||||||
|
endpoint,
|
||||||
|
binary = 'ossutil',
|
||||||
|
accessKeyId,
|
||||||
|
accessKeySecret,
|
||||||
|
dryRun = false,
|
||||||
|
spawn = spawnSync,
|
||||||
|
log = console.log,
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
if (Boolean(accessKeyId) !== Boolean(accessKeySecret)) {
|
||||||
|
throw new Error('OSS AccessKey ID 和 Secret 必须同时提供');
|
||||||
|
}
|
||||||
|
const plan = createReleaseUploadPlan(release, bucket);
|
||||||
|
for (const { source, destination } of plan) {
|
||||||
|
// 全部安装对象成功后才执行 latest 指针;失败立即终止,不发布悬空链接。
|
||||||
|
const args = ['cp', '--force', source, destination];
|
||||||
|
if (dryRun) {
|
||||||
|
log(
|
||||||
|
`[dry-run] ${formatOssutilCommand({ binary, args, endpoint, credentials: Boolean(accessKeyId) })}`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const credentials = accessKeyId
|
||||||
|
? ['--access-key-id', accessKeyId, '--access-key-secret', accessKeySecret]
|
||||||
|
: [];
|
||||||
|
const result = spawn(
|
||||||
|
binary,
|
||||||
|
[...args, '--endpoint', endpoint, ...credentials],
|
||||||
|
{
|
||||||
|
stdio: 'inherit',
|
||||||
|
shell: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (result.error)
|
||||||
|
throw new Error(`无法执行 ${binary},请先安装并配置 ossutil`);
|
||||||
|
if (result.status !== 0) {
|
||||||
|
throw new Error(
|
||||||
|
`OSS 上传失败(退出码 ${result.status ?? 1}):${destination}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
log(`[ai-game-creator-shell] 已上传 ${destination}`);
|
||||||
|
}
|
||||||
|
if (dryRun) log('[ai-game-creator-shell] dry-run:未写入任何 OSS 对象');
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { readFileSync } from 'node:fs';
|
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||||||
|
import os from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
|
|
||||||
import { formatOssutilCommand, readReleaseDryRun } from './release-oss.mjs';
|
import {
|
||||||
|
createReleaseUploadPlan,
|
||||||
|
formatOssutilCommand,
|
||||||
|
readReleaseDryRun,
|
||||||
|
uploadReleaseArtifacts,
|
||||||
|
} from './release-oss.mjs';
|
||||||
|
|
||||||
test('dry run only accepts explicit truthy values', () => {
|
test('dry run only accepts explicit truthy values', () => {
|
||||||
assert.equal(readReleaseDryRun({}), false);
|
assert.equal(readReleaseDryRun({}), false);
|
||||||
@@ -33,12 +40,158 @@ test('printed upload command keeps arguments and hides credentials', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('uploader gates every ossutil call behind the dry run switch', () => {
|
function withReleaseFixture(channel, architecture, run) {
|
||||||
const source = readFileSync(
|
const root = mkdtempSync(path.join(os.tmpdir(), 'agc-upload-plan-'));
|
||||||
new URL('./release-upload.mjs', import.meta.url),
|
try {
|
||||||
'utf8',
|
const artifact = path.join(
|
||||||
);
|
root,
|
||||||
assert.match(source, /const dryRun = readReleaseDryRun\(\);/u);
|
channel === 'dev-win'
|
||||||
assert.match(source, /if \(dryRun\) \{/u);
|
? '陶泥儿_1.2.3_x64-setup.exe'
|
||||||
assert.match(source, /dry-run:未写入任何 OSS 对象/u);
|
: '陶泥儿.app.tar.gz',
|
||||||
|
);
|
||||||
|
const downloadArtifact =
|
||||||
|
channel === 'dev-win'
|
||||||
|
? artifact
|
||||||
|
: path.join(root, `陶泥儿_1.2.3_${architecture}.dmg`);
|
||||||
|
const manifestPath = path.join(root, 'latest.json');
|
||||||
|
const legacyManifestPath =
|
||||||
|
channel === 'dev-win' ? path.join(root, 'legacy-latest.json') : null;
|
||||||
|
for (const file of [
|
||||||
|
artifact,
|
||||||
|
`${artifact}.sig`,
|
||||||
|
downloadArtifact,
|
||||||
|
manifestPath,
|
||||||
|
legacyManifestPath,
|
||||||
|
].filter(Boolean)) {
|
||||||
|
writeFileSync(file, 'fixture');
|
||||||
|
}
|
||||||
|
return run({
|
||||||
|
artifact,
|
||||||
|
downloadArtifact,
|
||||||
|
channel,
|
||||||
|
manifest: { version: '1.2.3' },
|
||||||
|
manifestPath,
|
||||||
|
legacyManifestPath,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadOptions = {
|
||||||
|
bucket: 'agc-dev',
|
||||||
|
endpoint: 'oss-rg-china-mainland.aliyuncs.com',
|
||||||
|
log: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const architecture of ['aarch64', 'x64']) {
|
||||||
|
test(`uploads every ${architecture} Mac object before the channel pointer`, () => {
|
||||||
|
withReleaseFixture('dev-mac', architecture, (release) => {
|
||||||
|
const calls = [];
|
||||||
|
uploadReleaseArtifacts(release, {
|
||||||
|
...uploadOptions,
|
||||||
|
spawn: (binary, args, options) => {
|
||||||
|
assert.equal(binary, 'ossutil');
|
||||||
|
assert.equal(options.shell, false);
|
||||||
|
assert.deepEqual(args.slice(0, 2), ['cp', '--force']);
|
||||||
|
calls.push({ source: args[2], destination: args[3] });
|
||||||
|
return { status: 0 };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert.deepEqual(
|
||||||
|
calls.map(({ source }) => source),
|
||||||
|
[
|
||||||
|
release.artifact,
|
||||||
|
`${release.artifact}.sig`,
|
||||||
|
release.downloadArtifact,
|
||||||
|
release.manifestPath,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
calls[2].destination,
|
||||||
|
`oss://agc-dev/agc/dev-mac/1.2.3/陶泥儿_1.2.3_${architecture}.dmg`,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
calls[3].destination,
|
||||||
|
'oss://agc-dev/agc/dev-mac/latest.json',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('Windows uploads the shared installer once and publishes migration metadata last', () => {
|
||||||
|
withReleaseFixture('dev-win', 'x64', (release) => {
|
||||||
|
const plan = createReleaseUploadPlan(release, 'agc-dev');
|
||||||
|
assert.deepEqual(
|
||||||
|
plan.map(({ source }) => source),
|
||||||
|
[
|
||||||
|
release.artifact,
|
||||||
|
`${release.artifact}.sig`,
|
||||||
|
release.manifestPath,
|
||||||
|
release.legacyManifestPath,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
assert.equal(plan.at(-1).destination, 'oss://agc-dev/agc/latest.json');
|
||||||
|
const calls = [];
|
||||||
|
uploadReleaseArtifacts(release, {
|
||||||
|
...uploadOptions,
|
||||||
|
spawn: (_binary, args) => {
|
||||||
|
assert.deepEqual(args.slice(0, 2), ['cp', '--force']);
|
||||||
|
calls.push(args[3]);
|
||||||
|
return { status: 0 };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert.deepEqual(
|
||||||
|
calls,
|
||||||
|
plan.map(({ destination }) => destination),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const failedArtifactIndex of [0, 1, 2]) {
|
||||||
|
test(`failed Mac object ${failedArtifactIndex} prevents both later objects and latest publication`, () => {
|
||||||
|
withReleaseFixture('dev-mac', 'aarch64', (release) => {
|
||||||
|
const destinations = [];
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
uploadReleaseArtifacts(release, {
|
||||||
|
...uploadOptions,
|
||||||
|
spawn: (_binary, args) => {
|
||||||
|
destinations.push(args[3]);
|
||||||
|
return {
|
||||||
|
status: destinations.length - 1 === failedArtifactIndex ? 1 : 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
/OSS 上传失败/u,
|
||||||
|
);
|
||||||
|
assert.equal(destinations.length, failedArtifactIndex + 1);
|
||||||
|
assert.ok(
|
||||||
|
destinations.every(
|
||||||
|
(destination) => !destination.endsWith('/latest.json'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('dry run prints the complete plan without spawning uploads or exposing credentials', () => {
|
||||||
|
withReleaseFixture('dev-mac', 'aarch64', (release) => {
|
||||||
|
const output = [];
|
||||||
|
uploadReleaseArtifacts(release, {
|
||||||
|
...uploadOptions,
|
||||||
|
dryRun: true,
|
||||||
|
accessKeyId: 'fixture-id',
|
||||||
|
accessKeySecret: 'fixture-secret',
|
||||||
|
spawn: () => assert.fail('dry run must never execute ossutil'),
|
||||||
|
log: (line) => output.push(line),
|
||||||
|
});
|
||||||
|
assert.equal(
|
||||||
|
output.filter((line) => line.startsWith('[dry-run]')).length,
|
||||||
|
4,
|
||||||
|
);
|
||||||
|
assert.match(output.join('\n'), /\.dmg/u);
|
||||||
|
assert.match(output.at(-1), /未写入任何 OSS 对象/u);
|
||||||
|
assert.doesNotMatch(output.join('\n'), /fixture-id|fixture-secret|已上传/u);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import { spawnSync } from 'node:child_process';
|
import { readReleaseDryRun, uploadReleaseArtifacts } from './release-oss.mjs';
|
||||||
import path from 'node:path';
|
|
||||||
|
|
||||||
import { formatOssutilCommand, readReleaseDryRun } from './release-oss.mjs';
|
|
||||||
|
|
||||||
const bucket = process.env.AGC_OSS_BUCKET?.trim() || 'agc-dev';
|
const bucket = process.env.AGC_OSS_BUCKET?.trim() || 'agc-dev';
|
||||||
const endpoint =
|
const endpoint =
|
||||||
@@ -12,81 +9,14 @@ if (!/^[a-z0-9][a-z0-9.-]{1,62}$/u.test(bucket) || /[\r\n\0]/u.test(endpoint)) {
|
|||||||
process.env.AGC_UPDATE_OSS_BASE_URL ||= `https://${bucket}.${endpoint}/agc`;
|
process.env.AGC_UPDATE_OSS_BASE_URL ||= `https://${bucket}.${endpoint}/agc`;
|
||||||
const dryRun = readReleaseDryRun();
|
const dryRun = readReleaseDryRun();
|
||||||
|
|
||||||
const { generateUpdateManifest, prepareReleaseVersion, runTauriBuild } =
|
const { buildRelease } = await import('./build-release.mjs');
|
||||||
await import('./build-release.mjs');
|
|
||||||
|
|
||||||
function runOssutil(args) {
|
const release = await buildRelease(process.argv.slice(2));
|
||||||
const binary = process.env.OSSUTIL_BIN?.trim() || 'ossutil';
|
uploadReleaseArtifacts(release, {
|
||||||
const accessKeyId = process.env.AGC_OSS_ACCESS_KEY_ID?.trim();
|
bucket,
|
||||||
const accessKeySecret = process.env.AGC_OSS_ACCESS_KEY_SECRET;
|
endpoint,
|
||||||
if (Boolean(accessKeyId) !== Boolean(accessKeySecret)) {
|
binary: process.env.OSSUTIL_BIN?.trim() || 'ossutil',
|
||||||
throw new Error('OSS AccessKey ID 和 Secret 必须同时提供');
|
accessKeyId: process.env.AGC_OSS_ACCESS_KEY_ID?.trim(),
|
||||||
}
|
accessKeySecret: process.env.AGC_OSS_ACCESS_KEY_SECRET,
|
||||||
if (dryRun) {
|
dryRun,
|
||||||
// 演练:只打印将要执行的上传,凭据以占位符呈现,不写入 OSS。
|
});
|
||||||
console.log(
|
|
||||||
`[dry-run] ${formatOssutilCommand({
|
|
||||||
binary,
|
|
||||||
args,
|
|
||||||
endpoint,
|
|
||||||
credentials: Boolean(accessKeyId),
|
|
||||||
})}`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const credentialArgs = accessKeyId
|
|
||||||
? ['--access-key-id', accessKeyId, '--access-key-secret', accessKeySecret]
|
|
||||||
: [];
|
|
||||||
const result = spawnSync(
|
|
||||||
binary,
|
|
||||||
[...args, '--endpoint', endpoint, ...credentialArgs],
|
|
||||||
{
|
|
||||||
stdio: 'inherit',
|
|
||||||
shell: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (result.error) {
|
|
||||||
throw new Error(`无法执行 ${binary},请先安装并配置 ossutil`);
|
|
||||||
}
|
|
||||||
if (result.status !== 0) process.exit(result.status ?? 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
await prepareReleaseVersion();
|
|
||||||
runTauriBuild([]);
|
|
||||||
const { artifact, channel, legacyManifestPath, manifest, manifestPath } =
|
|
||||||
await generateUpdateManifest();
|
|
||||||
const artifactKey = `agc/${channel}/${manifest.version}/${path.basename(artifact)}`;
|
|
||||||
// Jenkins/ossutil 默认会在目标对象已存在时交互询问并按默认值跳过;
|
|
||||||
// 发布清单是固定的 latest 指针,必须显式覆盖,否则流水线会误报成功但远端仍保留旧版本。
|
|
||||||
runOssutil(['cp', '--force', artifact, `oss://${bucket}/${artifactKey}`]);
|
|
||||||
runOssutil([
|
|
||||||
'cp',
|
|
||||||
'--force',
|
|
||||||
`${artifact}.sig`,
|
|
||||||
`oss://${bucket}/${artifactKey}.sig`,
|
|
||||||
]);
|
|
||||||
runOssutil([
|
|
||||||
'cp',
|
|
||||||
'--force',
|
|
||||||
manifestPath,
|
|
||||||
`oss://${bucket}/agc/${channel}/latest.json`,
|
|
||||||
]);
|
|
||||||
console.log(`[ai-game-creator-shell] 已上传 oss://${bucket}/${artifactKey}`);
|
|
||||||
console.log(
|
|
||||||
`[ai-game-creator-shell] 已上传 oss://${bucket}/agc/${channel}/latest.json`,
|
|
||||||
);
|
|
||||||
if (legacyManifestPath) {
|
|
||||||
// 迁移桥:让仍走旧 sha256 清单的已发布客户端升级到新协议,一个版本周期后删除。
|
|
||||||
runOssutil([
|
|
||||||
'cp',
|
|
||||||
'--force',
|
|
||||||
legacyManifestPath,
|
|
||||||
`oss://${bucket}/agc/latest.json`,
|
|
||||||
]);
|
|
||||||
console.log(
|
|
||||||
`[ai-game-creator-shell] 已上传迁移指针 oss://${bucket}/agc/latest.json`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (dryRun) {
|
|
||||||
console.log('[ai-game-creator-shell] dry-run:未写入任何 OSS 对象');
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
import { withDefaultCargoFeatures } from './cargo-features.mjs';
|
import {
|
||||||
|
defaultEditorFeatures,
|
||||||
|
withDefaultCargoFeatures,
|
||||||
|
} from './cargo-features.mjs';
|
||||||
import {
|
import {
|
||||||
readAgcDevEndpoint,
|
readAgcDevEndpoint,
|
||||||
resolveAgcDevEndpoint,
|
resolveAgcDevEndpoint,
|
||||||
@@ -47,9 +50,8 @@ function buildTauriArguments(argv, devUrl = readAgcDevEndpoint().url) {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// `agc_cocos_execute` 与 Cocos 编辑器适配器只在 `cocos-editor-execute` feature 下
|
// 开发和发行构建使用同一平台编辑器 feature 集合。
|
||||||
// 注册。开发构建默认在 Windows 打开它,否则 Agent 的工具清单里根本没有该工具,
|
// 可用 AGC_DEV_CARGO_FEATURES(逗号分隔)覆盖,传空串即关闭。
|
||||||
// 只能退化成改写脚本。可用 AGC_DEV_CARGO_FEATURES(逗号分隔)覆盖,传空串即关闭。
|
|
||||||
function readDevCargoFeatures(env = process.env) {
|
function readDevCargoFeatures(env = process.env) {
|
||||||
const override = env.AGC_DEV_CARGO_FEATURES;
|
const override = env.AGC_DEV_CARGO_FEATURES;
|
||||||
if (override !== undefined) {
|
if (override !== undefined) {
|
||||||
@@ -58,7 +60,7 @@ function readDevCargoFeatures(env = process.env) {
|
|||||||
.map((value) => value.trim())
|
.map((value) => value.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
return process.platform === 'win32' ? ['cocos-editor-execute'] : [];
|
return defaultEditorFeatures(process.platform);
|
||||||
}
|
}
|
||||||
|
|
||||||
function withDevCargoFeatures(argv, features = readDevCargoFeatures()) {
|
function withDevCargoFeatures(argv, features = readDevCargoFeatures()) {
|
||||||
|
|||||||
+14
-1
@@ -1745,7 +1745,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "genarrative-ai-game-creator-shell"
|
name = "genarrative-ai-game-creator-shell"
|
||||||
version = "0.1.47"
|
version = "0.1.67"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"agent-runtime-core",
|
"agent-runtime-core",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -1793,6 +1793,7 @@ dependencies = [
|
|||||||
"ttf-parser",
|
"ttf-parser",
|
||||||
"typed_floats",
|
"typed_floats",
|
||||||
"unicode-normalization",
|
"unicode-normalization",
|
||||||
|
"unity-editor-bridge",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
@@ -3931,6 +3932,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5016,6 +5018,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
|
"ts-rs",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6407,6 +6410,16 @@ version = "0.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unity-editor-bridge"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"editor-adapter-api",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unsafe-libyaml"
|
name = "unsafe-libyaml"
|
||||||
version = "0.2.11"
|
version = "0.2.11"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "genarrative-ai-game-creator-shell"
|
name = "genarrative-ai-game-creator-shell"
|
||||||
version = "0.1.47"
|
version = "0.1.67"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
@@ -12,6 +12,7 @@ template-library-fixtures = []
|
|||||||
cocos-editor = ["cocos-editor-bridge/process-discovery"]
|
cocos-editor = ["cocos-editor-bridge/process-discovery"]
|
||||||
cocos-editor-execute = ["cocos-editor", "cocos-editor-bridge/windows-bootstrap"]
|
cocos-editor-execute = ["cocos-editor", "cocos-editor-bridge/windows-bootstrap"]
|
||||||
cocos-editor-injection = ["cocos-editor-execute", "cocos-editor-bridge/windows-injection"]
|
cocos-editor-injection = ["cocos-editor-execute", "cocos-editor-bridge/windows-injection"]
|
||||||
|
unity-editor-execute = []
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
@@ -27,6 +28,7 @@ nalgebra = { version = "0.35.0", features = ["serde-serialize"] }
|
|||||||
agent-runtime-core = { path = "../../../server-rs/crates/agent-runtime-core" }
|
agent-runtime-core = { path = "../../../server-rs/crates/agent-runtime-core" }
|
||||||
cocos-editor-bridge = { path = "../../../plugins/agc-cocos-editor/native/cocos-editor-bridge", default-features = false }
|
cocos-editor-bridge = { path = "../../../plugins/agc-cocos-editor/native/cocos-editor-bridge", default-features = false }
|
||||||
editor-adapter-api = { path = "../../../server-rs/crates/editor-adapter-api" }
|
editor-adapter-api = { path = "../../../server-rs/crates/editor-adapter-api" }
|
||||||
|
unity-editor-bridge = { path = "../../../plugins/agc-unity-editor/native/unity-editor-bridge" }
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
axum = "0.8"
|
axum = "0.8"
|
||||||
chromiumoxide = "0.9.1"
|
chromiumoxide = "0.9.1"
|
||||||
@@ -56,7 +58,7 @@ platform-agent = { path = "../../../server-rs/crates/platform-agent" }
|
|||||||
portable-pty = "0.9"
|
portable-pty = "0.9"
|
||||||
reqwest = { version = "0.12", default-features = false, features = ["json", "multipart", "native-tls", "stream"] }
|
reqwest = { version = "0.12", default-features = false, features = ["json", "multipart", "native-tls", "stream"] }
|
||||||
regex = "1"
|
regex = "1"
|
||||||
shared-contracts = { path = "../../../server-rs/crates/shared-contracts", default-features = false }
|
shared-contracts = { path = "../../../server-rs/crates/shared-contracts", default-features = false, features = ["ts-bindings"] }
|
||||||
tauri = { version = "2.11.2", features = [] }
|
tauri = { version = "2.11.2", features = [] }
|
||||||
tauri-plugin-dialog = "2.7.1"
|
tauri-plugin-dialog = "2.7.1"
|
||||||
tauri-plugin-http = { version = "2.5.9", default-features = false, features = ["charset", "cookies", "http2", "rustls-tls"] }
|
tauri-plugin-http = { version = "2.5.9", default-features = false, features = ["charset", "cookies", "http2", "rustls-tls"] }
|
||||||
|
|||||||
@@ -1,31 +1,18 @@
|
|||||||
|
#[path = "build_support/codex_bundle.rs"]
|
||||||
|
mod codex_bundle;
|
||||||
#[path = "build_support/frontend_dist_guard.rs"]
|
#[path = "build_support/frontend_dist_guard.rs"]
|
||||||
mod frontend_dist_guard;
|
mod frontend_dist_guard;
|
||||||
#[path = "build_support/runtime_prompt_bundle.rs"]
|
#[path = "build_support/runtime_prompt_bundle.rs"]
|
||||||
mod runtime_prompt_bundle;
|
mod runtime_prompt_bundle;
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
use std::io::{BufReader, Read};
|
use std::io::{BufReader, Read};
|
||||||
|
|
||||||
const BUNDLED_CODEX_CLI_VERSION: &str = "codex-cli 0.147.0";
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
const BUNDLED_CODEX_FILES: [&str; 6] = [
|
|
||||||
"bin/codex.exe",
|
|
||||||
"bin/codex-code-mode-host.exe",
|
|
||||||
"codex-path/rg.exe",
|
|
||||||
"codex-resources/codex-command-runner.exe",
|
|
||||||
"codex-resources/codex-windows-sandbox-setup.exe",
|
|
||||||
"codex-package.json",
|
|
||||||
];
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
fn sha256_file(path: &std::path::Path) -> Result<String, std::io::Error> {
|
fn sha256_file(path: &std::path::Path) -> Result<String, std::io::Error> {
|
||||||
let file = fs::File::open(path)?;
|
let file = fs::File::open(path)?;
|
||||||
let mut reader = BufReader::new(file);
|
let mut reader = BufReader::new(file);
|
||||||
@@ -42,7 +29,15 @@ fn sha256_file(path: &std::path::Path) -> Result<String, std::io::Error> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn stage_bundled_codex_cli(manifest_dir: &std::path::Path) {
|
fn stage_bundled_codex_cli(manifest_dir: &std::path::Path) {
|
||||||
#[cfg(windows)]
|
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 {
|
||||||
|
assert!(
|
||||||
|
!target.contains("windows") && !target.contains("apple-darwin"),
|
||||||
|
"不支持的 Codex 随包目标:{target}"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
};
|
||||||
{
|
{
|
||||||
let app_root = manifest_dir
|
let app_root = manifest_dir
|
||||||
.parent()
|
.parent()
|
||||||
@@ -51,24 +46,23 @@ fn stage_bundled_codex_cli(manifest_dir: &std::path::Path) {
|
|||||||
.parent()
|
.parent()
|
||||||
.and_then(|apps_dir| apps_dir.parent())
|
.and_then(|apps_dir| apps_dir.parent())
|
||||||
.expect("AI 游戏创作应用必须位于仓库 apps 目录下");
|
.expect("AI 游戏创作应用必须位于仓库 apps 目录下");
|
||||||
let source_candidates = [
|
let package = layout.npm_package;
|
||||||
app_root.join(
|
let source_candidates = [app_root, repo_root]
|
||||||
"node_modules/@openai/codex-win32-x64/vendor/x86_64-pc-windows-msvc",
|
.into_iter()
|
||||||
),
|
.flat_map(|root| {
|
||||||
app_root.join(
|
[
|
||||||
"node_modules/@openai/codex/node_modules/@openai/codex-win32-x64/vendor/x86_64-pc-windows-msvc",
|
root.join(format!("node_modules/@openai/{package}/vendor/{target}")),
|
||||||
),
|
root.join(format!(
|
||||||
repo_root.join(
|
"node_modules/@openai/codex/node_modules/@openai/{package}/vendor/{target}"
|
||||||
"node_modules/@openai/codex-win32-x64/vendor/x86_64-pc-windows-msvc",
|
)),
|
||||||
),
|
]
|
||||||
repo_root.join(
|
})
|
||||||
"node_modules/@openai/codex/node_modules/@openai/codex-win32-x64/vendor/x86_64-pc-windows-msvc",
|
.collect::<Vec<_>>();
|
||||||
),
|
|
||||||
];
|
|
||||||
let source = source_candidates
|
let source = source_candidates
|
||||||
.iter()
|
.iter()
|
||||||
.find(|path| {
|
.find(|path| {
|
||||||
BUNDLED_CODEX_FILES
|
layout
|
||||||
|
.files
|
||||||
.iter()
|
.iter()
|
||||||
.all(|relative| path.join(relative).is_file())
|
.all(|relative| path.join(relative).is_file())
|
||||||
})
|
})
|
||||||
@@ -83,14 +77,26 @@ fn stage_bundled_codex_cli(manifest_dir: &std::path::Path) {
|
|||||||
.join(";")
|
.join(";")
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
let target_dir = manifest_dir.join("resources/codex/win-x64");
|
let metadata: serde_json::Value = serde_json::from_slice(
|
||||||
|
&fs::read(source.join("codex-package.json")).expect("读取 Codex 原生包元数据失败"),
|
||||||
|
)
|
||||||
|
.expect("Codex 原生包元数据无效");
|
||||||
|
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");
|
let notice = target_dir.join("NOTICE.md");
|
||||||
|
if target.contains("apple-darwin") {
|
||||||
|
let source_notice =
|
||||||
|
manifest_dir.join("resources/codex/【声明】Mac内置Codex组件-2026-09-18.md");
|
||||||
|
stage_plugin_file(&source_notice, ¬ice);
|
||||||
|
println!("cargo:rerun-if-changed={}", source_notice.display());
|
||||||
|
}
|
||||||
if !notice.is_file() {
|
if !notice.is_file() {
|
||||||
panic!("内置 Codex CLI 第三方声明缺失:{}", notice.display());
|
panic!("内置 Codex CLI 第三方声明缺失:{}", notice.display());
|
||||||
}
|
}
|
||||||
fs::create_dir_all(&target_dir).expect("创建内置 Codex CLI 资源目录失败");
|
fs::create_dir_all(&target_dir).expect("创建内置 Codex CLI 资源目录失败");
|
||||||
let mut file_hashes = serde_json::Map::new();
|
let mut file_hashes = serde_json::Map::new();
|
||||||
for relative in BUNDLED_CODEX_FILES {
|
for relative in layout.files {
|
||||||
let source_path = source.join(relative);
|
let source_path = source.join(relative);
|
||||||
let target_path = target_dir.join(relative);
|
let target_path = target_dir.join(relative);
|
||||||
if let Some(parent) = target_path.parent() {
|
if let Some(parent) = target_path.parent() {
|
||||||
@@ -104,15 +110,23 @@ fn stage_bundled_codex_cli(manifest_dir: &std::path::Path) {
|
|||||||
if !target_matches_source {
|
if !target_matches_source {
|
||||||
fs::copy(&source_path, &target_path).expect("复制内置 Codex CLI 资源失败");
|
fs::copy(&source_path, &target_path).expect("复制内置 Codex CLI 资源失败");
|
||||||
}
|
}
|
||||||
|
// 内容相同但曾被错误 chmod 的 staging 文件也必须恢复执行权限。
|
||||||
|
fs::set_permissions(
|
||||||
|
&target_path,
|
||||||
|
fs::metadata(&source_path)
|
||||||
|
.expect("读取组件权限失败")
|
||||||
|
.permissions(),
|
||||||
|
)
|
||||||
|
.expect("保留内置 Codex CLI 组件权限失败");
|
||||||
file_hashes.insert(
|
file_hashes.insert(
|
||||||
relative.to_string(),
|
relative.to_string(),
|
||||||
serde_json::Value::String(source_sha256),
|
serde_json::Value::String(source_sha256),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let manifest = serde_json::json!({
|
let manifest = serde_json::json!({
|
||||||
"schemaVersion": "genarrative-codex-sidecar.v2",
|
"schemaVersion": codex_bundle::SCHEMA,
|
||||||
"platform": "win32-x64",
|
"platform": layout.platform,
|
||||||
"version": BUNDLED_CODEX_CLI_VERSION,
|
"version": codex_bundle::CLI_VERSION,
|
||||||
"files": file_hashes,
|
"files": file_hashes,
|
||||||
});
|
});
|
||||||
let manifest_path = target_dir.join("manifest.json");
|
let manifest_path = target_dir.join("manifest.json");
|
||||||
@@ -126,7 +140,7 @@ fn stage_bundled_codex_cli(manifest_dir: &std::path::Path) {
|
|||||||
{
|
{
|
||||||
fs::write(&manifest_path, manifest_payload).expect("写入内置 Codex CLI 清单失败");
|
fs::write(&manifest_path, manifest_payload).expect("写入内置 Codex CLI 清单失败");
|
||||||
}
|
}
|
||||||
for relative in BUNDLED_CODEX_FILES {
|
for relative in layout.files {
|
||||||
println!("cargo:rerun-if-changed={}", source.join(relative).display());
|
println!("cargo:rerun-if-changed={}", source.join(relative).display());
|
||||||
}
|
}
|
||||||
println!("cargo:rerun-if-changed={}", notice.display());
|
println!("cargo:rerun-if-changed={}", notice.display());
|
||||||
@@ -182,6 +196,7 @@ fn main() {
|
|||||||
);
|
);
|
||||||
let manifest_path = manifest_dir.join("prompts/runtime/manifest.json");
|
let manifest_path = manifest_dir.join("prompts/runtime/manifest.json");
|
||||||
stage_bundled_codex_cli(&manifest_dir);
|
stage_bundled_codex_cli(&manifest_dir);
|
||||||
|
prepare_unity_editor_helper(&manifest_dir);
|
||||||
stage_plugin_workspace(&manifest_dir);
|
stage_plugin_workspace(&manifest_dir);
|
||||||
stage_cocos_editor_payload(&manifest_dir);
|
stage_cocos_editor_payload(&manifest_dir);
|
||||||
let compiled = runtime_prompt_bundle::compile_manifest(&manifest_path)
|
let compiled = runtime_prompt_bundle::compile_manifest(&manifest_path)
|
||||||
@@ -252,12 +267,116 @@ fn stage_cocos_editor_payload(manifest_dir: &std::path::Path) {
|
|||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
fn stage_cocos_editor_payload(_manifest_dir: &std::path::Path) {}
|
fn stage_cocos_editor_payload(_manifest_dir: &std::path::Path) {}
|
||||||
|
|
||||||
|
/// Unity helper 是插件的随包运行文件。内容指纹避免每次 Cargo 检查都重新发布 .NET。
|
||||||
|
fn prepare_unity_editor_helper(manifest_dir: &std::path::Path) {
|
||||||
|
println!("cargo:rerun-if-env-changed=CARGO_FEATURE_UNITY_EDITOR_EXECUTE");
|
||||||
|
let target = env::var("TARGET").expect("Cargo TARGET");
|
||||||
|
if env::var_os("CARGO_FEATURE_UNITY_EDITOR_EXECUTE").is_none()
|
||||||
|
|| target != "x86_64-pc-windows-msvc"
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let root = manifest_dir.join("../../../plugins/agc-unity-editor/dotnet");
|
||||||
|
let mut sources = Vec::new();
|
||||||
|
collect_unity_helper_sources(&root, &mut sources);
|
||||||
|
sources.sort();
|
||||||
|
let mut fingerprint = Sha256::new();
|
||||||
|
for source in &sources {
|
||||||
|
println!("cargo:rerun-if-changed={}", source.display());
|
||||||
|
fingerprint.update(
|
||||||
|
source
|
||||||
|
.strip_prefix(&root)
|
||||||
|
.expect("helper source")
|
||||||
|
.to_string_lossy()
|
||||||
|
.as_bytes(),
|
||||||
|
);
|
||||||
|
fingerprint.update([0]);
|
||||||
|
fingerprint.update(fs::read(source).expect("读取 Unity helper 源文件失败"));
|
||||||
|
}
|
||||||
|
let fingerprint = format!("{:x}", fingerprint.finalize());
|
||||||
|
let publish = root.join("publish/win-x64");
|
||||||
|
let executable = publish.join("Agc.Unity.Attach.exe");
|
||||||
|
let stamp = publish.join(".agc-source.sha256");
|
||||||
|
println!("cargo:rerun-if-changed={}", executable.display());
|
||||||
|
if unity_helper_publish_complete(&publish)
|
||||||
|
&& fs::read_to_string(&stamp).ok().as_deref() == Some(&fingerprint)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
cfg!(windows),
|
||||||
|
"构建 Unity 插件 helper 需要 Windows .NET 10 与 x64 C++ 工具链"
|
||||||
|
);
|
||||||
|
let status = std::process::Command::new("powershell.exe")
|
||||||
|
.args([
|
||||||
|
"-NoProfile",
|
||||||
|
"-NonInteractive",
|
||||||
|
"-ExecutionPolicy",
|
||||||
|
"Bypass",
|
||||||
|
"-File",
|
||||||
|
])
|
||||||
|
.arg(root.join("build.ps1"))
|
||||||
|
.current_dir(&root)
|
||||||
|
.status()
|
||||||
|
.expect("无法启动 Unity helper 构建脚本");
|
||||||
|
assert!(
|
||||||
|
status.success() && unity_helper_publish_complete(&publish),
|
||||||
|
"Unity helper 构建失败或缺少运行文件/许可"
|
||||||
|
);
|
||||||
|
fs::write(stamp, fingerprint).expect("写入 Unity helper 构建指纹失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unity_helper_publish_complete(publish: &std::path::Path) -> bool {
|
||||||
|
[
|
||||||
|
"Agc.Unity.Attach.exe",
|
||||||
|
"NOTICE",
|
||||||
|
"THIRD-PARTY-NOTICES.txt",
|
||||||
|
"licenses/DotCraft-Apache-2.0.txt",
|
||||||
|
"licenses/Roslyn-MIT.txt",
|
||||||
|
"licenses/upstream.json",
|
||||||
|
"licenses/dotnet-LICENSE.TXT",
|
||||||
|
"licenses/dotnet-THIRD-PARTY-NOTICES.TXT",
|
||||||
|
"licenses/microsoft.codeanalysis.common-ThirdPartyNotices.rtf",
|
||||||
|
"licenses/microsoft.codeanalysis.csharp-ThirdPartyNotices.rtf",
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.all(|name| {
|
||||||
|
fs::symlink_metadata(publish.join(name)).is_ok_and(|metadata| {
|
||||||
|
metadata.is_file() && !metadata.file_type().is_symlink() && metadata.len() > 0
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_unity_helper_sources(root: &std::path::Path, sources: &mut Vec<PathBuf>) {
|
||||||
|
for entry in fs::read_dir(root)
|
||||||
|
.expect("Unity helper 源码目录缺失")
|
||||||
|
.flatten()
|
||||||
|
{
|
||||||
|
let kind = entry.file_type().expect("读取 Unity helper 源文件类型失败");
|
||||||
|
assert!(!kind.is_symlink(), "Unity helper 源码不允许符号链接");
|
||||||
|
let name = entry.file_name();
|
||||||
|
if kind.is_dir() {
|
||||||
|
if !matches!(
|
||||||
|
name.to_str(),
|
||||||
|
Some("bin" | "obj" | "publish" | "native-build")
|
||||||
|
) {
|
||||||
|
collect_unity_helper_sources(&entry.path(), sources);
|
||||||
|
}
|
||||||
|
} else if kind.is_file() {
|
||||||
|
sources.push(entry.path());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 把 `plugins/` 工作区里的插件包随包映射到应用资源目录。
|
/// 把 `plugins/` 工作区里的插件包随包映射到应用资源目录。
|
||||||
///
|
///
|
||||||
/// 只复制插件运行需要的清单、入口、面板和 native payload,不复制 native 源码、
|
/// 只复制插件运行需要的清单、入口、面板和 native payload,不复制 native 源码、
|
||||||
/// Cargo target 目录或 node_modules。
|
/// Cargo target 目录或 node_modules。
|
||||||
#[cfg(windows)]
|
|
||||||
fn stage_plugin_workspace(manifest_dir: &std::path::Path) {
|
fn stage_plugin_workspace(manifest_dir: &std::path::Path) {
|
||||||
|
let target = env::var("TARGET").expect("Cargo TARGET");
|
||||||
|
if !target.contains("windows") && !target.contains("apple-darwin") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let repo_root = manifest_dir
|
let repo_root = manifest_dir
|
||||||
.parent()
|
.parent()
|
||||||
.and_then(|app_root| app_root.parent())
|
.and_then(|app_root| app_root.parent())
|
||||||
@@ -266,6 +385,10 @@ fn stage_plugin_workspace(manifest_dir: &std::path::Path) {
|
|||||||
.to_path_buf();
|
.to_path_buf();
|
||||||
let workspace = repo_root.join("plugins");
|
let workspace = repo_root.join("plugins");
|
||||||
let destination_root = manifest_dir.join("resources/plugins");
|
let destination_root = manifest_dir.join("resources/plugins");
|
||||||
|
// staging 是专用生成目录;重建清除跨目标 payload 与已删除插件的残留。
|
||||||
|
if destination_root.exists() {
|
||||||
|
std::fs::remove_dir_all(&destination_root).expect("清理插件 staging 失败");
|
||||||
|
}
|
||||||
std::fs::create_dir_all(&destination_root).expect("创建插件资源目录失败");
|
std::fs::create_dir_all(&destination_root).expect("创建插件资源目录失败");
|
||||||
let entries = match std::fs::read_dir(&workspace) {
|
let entries = match std::fs::read_dir(&workspace) {
|
||||||
Ok(entries) => entries,
|
Ok(entries) => entries,
|
||||||
@@ -273,6 +396,13 @@ fn stage_plugin_workspace(manifest_dir: &std::path::Path) {
|
|||||||
};
|
};
|
||||||
for entry in entries.flatten() {
|
for entry in entries.flatten() {
|
||||||
let plugin_root = entry.path();
|
let plugin_root = entry.path();
|
||||||
|
assert!(
|
||||||
|
!entry
|
||||||
|
.file_type()
|
||||||
|
.expect("读取插件目录类型失败")
|
||||||
|
.is_symlink(),
|
||||||
|
"插件工作区不允许符号链接"
|
||||||
|
);
|
||||||
if !plugin_root.is_dir() || !plugin_root.join("plugin.json").is_file() {
|
if !plugin_root.is_dir() || !plugin_root.join("plugin.json").is_file() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -285,19 +415,26 @@ fn stage_plugin_workspace(manifest_dir: &std::path::Path) {
|
|||||||
for relative in [
|
for relative in [
|
||||||
std::path::PathBuf::from("src"),
|
std::path::PathBuf::from("src"),
|
||||||
std::path::PathBuf::from("panels"),
|
std::path::PathBuf::from("panels"),
|
||||||
|
std::path::PathBuf::from("skills"),
|
||||||
std::path::PathBuf::from("native/payload"),
|
std::path::PathBuf::from("native/payload"),
|
||||||
|
std::path::PathBuf::from("dotnet/publish/win-x64"),
|
||||||
] {
|
] {
|
||||||
|
if (relative == std::path::Path::new("native/payload") && !target.contains("windows"))
|
||||||
|
|| (relative == std::path::Path::new("dotnet/publish/win-x64")
|
||||||
|
&& (target != "x86_64-pc-windows-msvc"
|
||||||
|
|| env::var_os("CARGO_FEATURE_UNITY_EDITOR_EXECUTE").is_none()))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
copy_plugin_tree(&plugin_root.join(&relative), &destination.join(&relative));
|
copy_plugin_tree(&plugin_root.join(&relative), &destination.join(&relative));
|
||||||
}
|
}
|
||||||
println!("cargo:rerun-if-changed={}", plugin_root.display());
|
println!("cargo:rerun-if-changed={}", plugin_root.display());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
fn stage_plugin_file(source: &std::path::Path, destination: &std::path::Path) {
|
fn stage_plugin_file(source: &std::path::Path, destination: &std::path::Path) {
|
||||||
let Ok(bytes) = std::fs::read(source) else {
|
let bytes = std::fs::read(source)
|
||||||
return;
|
.unwrap_or_else(|error| panic!("读取随包资源失败 {}:{error}", source.display()));
|
||||||
};
|
|
||||||
if std::fs::read(destination).is_ok_and(|existing| existing == bytes) {
|
if std::fs::read(destination).is_ok_and(|existing| existing == bytes) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -307,7 +444,6 @@ fn stage_plugin_file(source: &std::path::Path, destination: &std::path::Path) {
|
|||||||
std::fs::write(destination, bytes).expect("复制插件资源失败");
|
std::fs::write(destination, bytes).expect("复制插件资源失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
fn copy_plugin_tree(source: &std::path::Path, destination: &std::path::Path) {
|
fn copy_plugin_tree(source: &std::path::Path, destination: &std::path::Path) {
|
||||||
let entries = match std::fs::read_dir(source) {
|
let entries = match std::fs::read_dir(source) {
|
||||||
Ok(entries) => entries,
|
Ok(entries) => entries,
|
||||||
@@ -316,10 +452,17 @@ fn copy_plugin_tree(source: &std::path::Path, destination: &std::path::Path) {
|
|||||||
for entry in entries.flatten() {
|
for entry in entries.flatten() {
|
||||||
let target = destination.join(entry.file_name());
|
let target = destination.join(entry.file_name());
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
|
assert!(
|
||||||
|
!entry
|
||||||
|
.file_type()
|
||||||
|
.expect("读取插件文件类型失败")
|
||||||
|
.is_symlink(),
|
||||||
|
"插件资源不允许符号链接"
|
||||||
|
);
|
||||||
if path.is_dir() {
|
if path.is_dir() {
|
||||||
let name = entry.file_name();
|
let name = entry.file_name();
|
||||||
let name = name.to_string_lossy();
|
let name = name.to_string_lossy();
|
||||||
if matches!(name.as_ref(), "target" | "node_modules" | ".git") {
|
if name.starts_with('.') || matches!(name.as_ref(), "target" | "node_modules") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
std::fs::create_dir_all(&target).expect("创建插件资源目录失败");
|
std::fs::create_dir_all(&target).expect("创建插件资源目录失败");
|
||||||
@@ -331,12 +474,14 @@ fn copy_plugin_tree(source: &std::path::Path, destination: &std::path::Path) {
|
|||||||
if name.contains(".test.") {
|
if name.contains(".test.") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if name.starts_with('.') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
stage_plugin_file(&path, &target);
|
stage_plugin_file(&path, &target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
fn copy_plugin_file(source: &std::path::Path, destination: &std::path::Path) {
|
fn copy_plugin_file(source: &std::path::Path, destination: &std::path::Path) {
|
||||||
if !source.is_file() {
|
if !source.is_file() {
|
||||||
return;
|
return;
|
||||||
@@ -345,6 +490,3 @@ fn copy_plugin_file(source: &std::path::Path, destination: &std::path::Path) {
|
|||||||
.expect("创建插件资源目录失败");
|
.expect("创建插件资源目录失败");
|
||||||
std::fs::copy(source, destination).expect("复制插件资源失败");
|
std::fs::copy(source, destination).expect("复制插件资源失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(windows))]
|
|
||||||
fn stage_plugin_workspace(_manifest_dir: &std::path::Path) {}
|
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
//! 构建与运行共用的平台布局;只允许分发锁定原生包里的明确组件。
|
||||||
|
|
||||||
|
pub const VERSION: &str = "0.147.0";
|
||||||
|
pub const CLI_VERSION: &str = "codex-cli 0.147.0";
|
||||||
|
pub const SCHEMA: &str = "genarrative-codex-sidecar.v2";
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct Layout {
|
||||||
|
pub platform: &'static str,
|
||||||
|
pub npm_package: &'static str,
|
||||||
|
pub directory: &'static str,
|
||||||
|
pub executable: &'static str,
|
||||||
|
pub files: &'static [&'static str],
|
||||||
|
}
|
||||||
|
|
||||||
|
const WINDOWS_FILES: &[&str] = &[
|
||||||
|
"bin/codex.exe",
|
||||||
|
"bin/codex-code-mode-host.exe",
|
||||||
|
"codex-path/rg.exe",
|
||||||
|
"codex-resources/codex-command-runner.exe",
|
||||||
|
"codex-resources/codex-windows-sandbox-setup.exe",
|
||||||
|
"codex-package.json",
|
||||||
|
];
|
||||||
|
const MAC_FILES: &[&str] = &[
|
||||||
|
"bin/codex",
|
||||||
|
"bin/codex-code-mode-host",
|
||||||
|
"codex-path/rg",
|
||||||
|
"codex-resources/zsh/bin/zsh",
|
||||||
|
"codex-package.json",
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn for_target(target: &str) -> Option<Layout> {
|
||||||
|
match target {
|
||||||
|
"x86_64-pc-windows-msvc" => Some(Layout {
|
||||||
|
platform: "win32-x64",
|
||||||
|
npm_package: "codex-win32-x64",
|
||||||
|
directory: "win-x64",
|
||||||
|
executable: "bin/codex.exe",
|
||||||
|
files: WINDOWS_FILES,
|
||||||
|
}),
|
||||||
|
"aarch64-apple-darwin" | "x86_64-apple-darwin" => Some(Layout {
|
||||||
|
platform: if target.starts_with("aarch64") {
|
||||||
|
"darwin-arm64"
|
||||||
|
} else {
|
||||||
|
"darwin-x64"
|
||||||
|
},
|
||||||
|
npm_package: if target.starts_with("aarch64") {
|
||||||
|
"codex-darwin-arm64"
|
||||||
|
} else {
|
||||||
|
"codex-darwin-x64"
|
||||||
|
},
|
||||||
|
directory: "mac-native",
|
||||||
|
executable: "bin/codex",
|
||||||
|
files: MAC_FILES,
|
||||||
|
}),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_package_metadata(
|
||||||
|
metadata: &serde_json::Value,
|
||||||
|
target: &str,
|
||||||
|
layout: Layout,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if metadata["layoutVersion"] == 1
|
||||||
|
&& metadata["version"] == VERSION
|
||||||
|
&& metadata["target"] == target
|
||||||
|
&& metadata["entrypoint"] == layout.executable
|
||||||
|
&& metadata["resourcesDir"] == "codex-resources"
|
||||||
|
&& metadata["pathDir"] == "codex-path"
|
||||||
|
{
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(format!("Codex 原生包版本、布局或架构不匹配目标 {target}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn platform_layouts_are_explicit_and_preserve_upstream_components() {
|
||||||
|
let mac = for_target("aarch64-apple-darwin").unwrap();
|
||||||
|
assert_eq!(mac.platform, "darwin-arm64");
|
||||||
|
assert_eq!(mac.npm_package, "codex-darwin-arm64");
|
||||||
|
assert!(mac.files.contains(&"codex-resources/zsh/bin/zsh"));
|
||||||
|
assert!(mac.files.contains(&"bin/codex-code-mode-host"));
|
||||||
|
assert!(!mac.files.iter().any(|file| file.ends_with(".exe")));
|
||||||
|
let intel = for_target("x86_64-apple-darwin").unwrap();
|
||||||
|
assert_eq!(intel.platform, "darwin-x64");
|
||||||
|
assert_eq!(intel.npm_package, "codex-darwin-x64");
|
||||||
|
let windows = for_target("x86_64-pc-windows-msvc").unwrap();
|
||||||
|
assert_eq!(windows.directory, "win-x64");
|
||||||
|
assert_eq!(windows.files.len(), 6);
|
||||||
|
assert!(windows
|
||||||
|
.files
|
||||||
|
.contains(&"codex-resources/codex-windows-sandbox-setup.exe"));
|
||||||
|
assert!(for_target("universal-apple-darwin").is_none());
|
||||||
|
assert!(for_target("aarch64-pc-windows-msvc").is_none());
|
||||||
|
assert!(for_target("x86_64-unknown-linux-gnu").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn metadata_rejects_version_architecture_and_layout_drift() {
|
||||||
|
let target = "aarch64-apple-darwin";
|
||||||
|
let layout = for_target(target).unwrap();
|
||||||
|
let valid = serde_json::json!({
|
||||||
|
"layoutVersion": 1,
|
||||||
|
"version": VERSION,
|
||||||
|
"target": target,
|
||||||
|
"entrypoint": "bin/codex",
|
||||||
|
"resourcesDir": "codex-resources",
|
||||||
|
"pathDir": "codex-path",
|
||||||
|
});
|
||||||
|
assert!(validate_package_metadata(&valid, target, layout).is_ok());
|
||||||
|
for (key, value) in [
|
||||||
|
("layoutVersion", serde_json::json!(2)),
|
||||||
|
("version", serde_json::json!("0.0.0")),
|
||||||
|
("target", serde_json::json!("x86_64-apple-darwin")),
|
||||||
|
("entrypoint", serde_json::json!("bin/codex.exe")),
|
||||||
|
("resourcesDir", serde_json::json!("../private")),
|
||||||
|
("pathDir", serde_json::json!(null)),
|
||||||
|
] {
|
||||||
|
let mut invalid = valid.clone();
|
||||||
|
invalid[key] = value;
|
||||||
|
assert!(
|
||||||
|
validate_package_metadata(&invalid, target, layout).is_err(),
|
||||||
|
"{key}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -14,7 +14,7 @@ Let the client derive projections from real disk changes and trusted tool result
|
|||||||
3. Keep read scopes separate: `asset.list` is the current project manifest, `asset.library.list` is the signed-in account library, and the web project's canvas resource read model is the authoritative canvas list. The account library is not the complete canvas list.
|
3. Keep read scopes separate: `asset.list` is the current project manifest, `asset.library.list` is the signed-in account library, and the web project's canvas resource read model is the authoritative canvas list. The account library is not the complete canvas list.
|
||||||
4. Use `canvas.asset_import` for safe account/canvas asset IDs or project-relative local paths. The client rechecks ownership and validates bytes; host absolute paths require native UI file-picker authorization.
|
4. Use `canvas.asset_import` for safe account/canvas asset IDs or project-relative local paths. The client rechecks ownership and validates bytes; host absolute paths require native UI file-picker authorization.
|
||||||
5. When the user explicitly asks to create or derive video, character animation, sound effect, or background music, call `agc_create_or_derive_resource`. Use `create` only for video/audio without a source and `derive` with a registered `sourceLocalAssetId`; character animation is always derived from an image. Keep `prompt` inside the per-kind limit that the client really enforces: background music at most 140 characters, sound effect at most 1900, video and character animation at most 4000. A longer prompt is rejected before submission, so write the short version first instead of retrying the same text.
|
5. When the user explicitly asks to create or derive video, character animation, sound effect, or background music, call `agc_create_or_derive_resource`. Use `create` only for video/audio without a source and `derive` with a registered `sourceLocalAssetId`; character animation is always derived from an image. Keep `prompt` inside the per-kind limit that the client really enforces: background music at most 140 characters, sound effect at most 1900, video and character animation at most 4000. A longer prompt is rejected before submission, so write the short version first instead of retrying the same text.
|
||||||
6. When the user explicitly asks to remove an image background, call `agc_remove_background` with a registered image `sourceLocalAssetId` and `assetName`. Optional `backgroundMode` is `complex` (semantic foreground segmentation; default) or `flat` (solid-colour background removal). Prefer `flat` when the background is known to be solid. Only `flat` accepts optional `screenColor`: `auto`, `#RRGGBB`, or omitted for automatic detection by the service. Do not select a colour on behalf of `auto`. The client requires the signed-in account, owns canvas/folder context and task identity, and returns only bounded queue state.
|
6. When the user explicitly asks to remove an image background, call `agc_remove_background` with a registered image `sourceLocalAssetId` and `assetName`. Optional `backgroundMode` is `complex` (semantic foreground segmentation; default) or `flat` (solid-colour background removal). Prefer `flat` when the background is known to be solid. Only `flat` accepts optional `screenColor`: `auto`, `#RRGGBB`, or omitted for automatic detection by the service. Do not select a colour on behalf of `auto`. The client requires the signed-in account, owns canvas/folder context and task identity, waits for the accepted operation, downloads and registers the completed local asset, and preserves the operation for recovery when the remote result is not yet known.
|
||||||
7. Preserve existing relative paths when a small edit is sufficient so client resource identities remain stable.
|
7. Preserve existing relative paths when a small edit is sufficient so client resource identities remain stable.
|
||||||
8. Do not edit `.agent/manifest.json`, revision counters, version records, resource IDs, canvas identities, source provenance, generation ledgers, or browser receipts by hand.
|
8. Do not edit `.agent/manifest.json`, revision counters, version records, resource IDs, canvas identities, source provenance, generation ledgers, or browser receipts by hand.
|
||||||
9. Do not create a version when no game file changed. The client compares content fingerprints and advances revision only after an actual source change.
|
9. Do not create a version when no game file changed. The client compares content fingerprints and advances revision only after an actual source change.
|
||||||
|
|||||||
+3
-1
@@ -16,4 +16,6 @@ Read scopes remain separate: `asset.list` is the current project's local manifes
|
|||||||
|
|
||||||
`prompt` limits are per kind and are enforced before any paid submission: background music accepts 1-140 characters, sound effect 1-1900, video and character animation 1-4000, and image editing (`agc_edit_image`) 1-32000. The client composes the submitted request from a fixed prefix plus your prompt, so an over-limit prompt fails locally with the exact limit; shorten the text rather than resubmitting the same value. `agc_edit_image` remains the image path; this tool never generates or edits still images.
|
`prompt` limits are per kind and are enforced before any paid submission: background music accepts 1-140 characters, sound effect 1-1900, video and character animation 1-4000, and image editing (`agc_edit_image`) 1-32000. The client composes the submitted request from a fixed prefix plus your prompt, so an over-limit prompt fails locally with the exact limit; shorten the text rather than resubmitting the same value. `agc_edit_image` remains the image path; this tool never generates or edits still images.
|
||||||
|
|
||||||
`agc_remove_background` accepts a registered image `sourceLocalAssetId`, `assetName`, and optional `backgroundMode` and `screenColor`. `complex` uses semantic segmentation to identify the foreground; `flat` removes a solid-colour background. Prefer `flat` when the background is known to be solid; omitting the mode selects `complex`. Only `flat` accepts a colour: `auto`, `#RRGGBB`, or omitted for automatic service detection. Never infer a concrete colour for `auto`. Empty or invalid values and colour without `flat` are rejected. The client resolves the formal source resource, canvas/folder context, stable operation identity, idempotency key, and authenticated External v1 `/api/external/v1/editor/images/background-removals` call. Mode and colour are part of request identity. Its result is bounded queue state; Codex must not poll internal workers, construct source URLs, or retry with a new identity after an uncertain response.
|
`agc_remove_background` accepts a registered image `sourceLocalAssetId`, `assetName`, and optional `backgroundMode` and `screenColor`. `complex` uses semantic segmentation to identify the foreground; `flat` removes a solid-colour background. Prefer `flat` when the background is known to be solid; omitting the mode selects `complex`. Only `flat` accepts a colour: `auto`, `#RRGGBB`, or omitted for automatic service detection. Never infer a concrete colour for `auto`. Empty or invalid values and colour without `flat` are rejected. The client resolves the formal source resource, canvas/folder context, stable operation identity, idempotency key, and authenticated request. Ordinary account mode maps the External v1 shaped route to `/api/editor/images/background-removals`; ExternalDeveloper mode uses `/api/external/v1/editor/images/background-removals`. Mode and colour are part of request identity. After acceptance, the client polls the authenticated generation status route, downloads the completed media, and commits it to the local manifest. If completion is unknown, it retains the same local operation for recovery; it never retries with a new identity or exposes internal worker details.
|
||||||
|
|
||||||
|
After an interrupted call, inspect `agc_list_registered_assets.pendingOperations`. Calling `agc_remove_background` again with the same source, name, mode, and colour resumes the matching pending operation. A submission marked `reconciliation-required` needs client-side reconciliation and cannot be automatically resumed. Do not change parameters to bypass a pending task. A queued receipt, fixed progress value, or absent local file does not establish that the background-removal provider is waiting in a queue; report only the observed state.
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user