Compare commits

..

26 Commits

Author SHA1 Message Date
k88936 0c4b1c34d9 标注模型下载的重复 task 查询
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 7m17s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 7m19s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 7m31s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 7m39s
Project CI / Backend tests (pull_request) Failing after 10s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m3s
Project CI / Repository checks (pull_request) Failing after 10s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m36s
Project CI / Native shell tests (pull_request) Failing after 11m43s
Project CI / AI game creator shell web tests (pull_request) Failing after 10m21s
Project CI / Frontend tests (pull_request) Successful in 12m23s
在 download_model 增加 TODO,说明 SDK 只提供 download_model(&Task) 因而必须再取一次 task

若将来 SDK 支持按 URL 下载,可直接使用 snapshot 中的模型 URL 省掉这次请求
2026-09-19 14:17:31 +08:00
k88936 3c1059aa39 移除 TripoUrl 的 Display 实现
删除 TripoUrl 的 Display,避免 `{}` 这类通用格式化输出带签名的完整 URL

完整 URL 仍可通过 as_str() 显式读取,日志与调试统一走 redacted() 或脱敏后的 Debug

技术方案文档补充该收敛说明
2026-09-19 14:17:00 +08:00
k88936 1392303f1c 请求契约把 model 改为必填字段
三个生成请求的 model 由 Option<Model3dModelVersion> 改为必填 Model3dModelVersion,并同步重新生成 TypeScript 绑定

validate_generation_options 不再重复校验 model 缺失,直接由必填的 model 推导 family

三个 client 的 to_sdk_params 改为直接提交模型版本字符串

冒烟示例与技术方案文档同步为必填写法
2026-09-19 14:16:39 +08:00
k88936 12512f26b2 multiview 输入拒绝未知字段
Model3dMultiviewInputs 增加 deny_unknown_fields,与 Model3dMultiviewToModelRequest 的严格反序列化保持一致

未知视图键(例如拼写错误的 righ)不再被静默丢弃,避免用少于调用方预期的视图提交任务

ts-rs 12 不支持 enum 级该属性,构建时会打印一条可忽略的告警,已在代码注释说明
2026-09-19 14:14:55 +08:00
k88936 800da43a8d 说明生成结果 DTO 的 number 精度边界
为 poll_after_ms、updated_at_micros、size_bytes 补充注释,说明其量级远低于 2^53,按 TS number 导出即可精确表示

重新生成 TypeScript 绑定,把上述说明同步为 JSDoc

技术方案补充同一说明,明确不做 bigint/string 特殊处理
2026-09-19 14:14:14 +08:00
k88936 b703f0c374 同步Tripo集成文档的契约导出与脱敏说明
补充 TripoUrl 脱敏输出与 as_str 读取完整 URL 的说明

补充请求可选字段在 TypeScript 绑定中为 field?: T | null 及 model3d barrel 导出入口

补充 ts-rs 生成后需 prettier 格式化再提交的流程说明
2026-09-19 13:17:58 +08:00
k88936 b87d9358e5 对齐 Api 与 Request 的可重试状态码
TripoError::is_retryable 的 Api 分支补充 408 与 425,与 Request 分支的可重试集合保持一致
2026-09-19 13:15:31 +08:00
k88936 f4996d7ac2 auto_size 仅在显式开启时按模型能力拦截
validate_generation_options 中 auto_size 的 H25 校验由 is_some() 改为 == Some(true)

auto_size: false 不再被当作启用,与 quad、smart_low_poly、generate_parts 的取值语义保持一致
2026-09-19 13:15:00 +08:00
k88936 09ef92420f multiview 复用统一的 task_id 校验
删除 submit_multiview_to_model 中重复的空白判断,改为调用 common::validate_task_id

task_id 非法时的错误字段由 Inputs 改为更精确的 TaskId,与 get_task、download_model 保持一致
2026-09-19 13:14:32 +08:00
k88936 091938ed59 multiview 提交前裁剪 task_id
to_sdk_params 改为提交 task_id.trim(),与 task_id.trim().is_empty() 的校验使用同一份标准化输入

带前后空白的 task_id 不再原样转发给 Provider
2026-09-19 13:13:54 +08:00
k88936 ddb8b9e8ba multiview 空白视图按未提供处理
to_sdk_params 新增 optional_view,将仅含空白的 left、back、right 归一化为未提供

空白视图不再以 Some(FileInput) 形式提交,与提交前“至少两个视图”的校验保持一致
2026-09-19 13:13:35 +08:00
k88936 230e4d49f2 image_to_model 提交前统一使用裁剪后的输入
to_sdk_params 改为传入 request.input.trim(),与提交前的空值校验使用同一份标准化输入

带前后空白的输入不再原样转发给 Provider
2026-09-19 13:12:50 +08:00
k88936 6c209b115a 冒烟示例在任务未完成时跳过下载并继续后续入口
轮询改用 throw_on_failure=false,失败终态交回示例按 status 判定而不是直接返回错误

非 Completed 的任务打印 status 与 failure 后 continue,避免一个入口失败中断其余入口的冒烟
2026-09-19 13:12:23 +08:00
k88936 b631125a16 冒烟示例不再输出完整下载地址
产物行改用 TripoUrl::redacted(),避免把带签名的下载 URL 写入标准输出
2026-09-19 13:07:41 +08:00
k88936 771ff88440 脱敏 TripoUrl 的 Debug 输出
为 TripoUrl 手写 Debug 实现,改用隐藏 query 与 fragment 的脱敏形式

新增 TripoUrl::redacted 提供日志用脱敏 URL,完整 URL 仍由 as_str 显式读取

TripoTaskSnapshot、TripoTaskOutput、TripoDownloadedModel 的调试输出随之不再暴露带签名的下载地址
2026-09-19 13:07:41 +08:00
k88936 950209f038 为 model3d 请求契约的可选字段补充 ts(optional)
为 image_to_model、multiview_to_model、text_to_model 三个请求的可选字段添加 ts(optional = nullable)

multiview 视图分支的 left、back、right 同步标记为可选

重新生成 packages/shared/src/contracts/model3d 下的 TypeScript 绑定,使 TS 侧可按 serde 语义省略字段
2026-09-19 13:07:41 +08:00
k88936 42a5929a73 清理 model3d 请求契约中冗余的 serde(default)
移除 image_to_model、multiview_to_model、text_to_model 三个请求结构体 Option 字段上的 serde(default)

同步移除 multiview 视图分支中三个可选视图的 serde(default)

serde 对 Option 字段本身即按 None 处理缺失字段,生成的 TypeScript 绑定保持不变
2026-09-19 13:07:41 +08:00
k88936 eda9da4d8d 单点维护 model3d 的 ts-rs 导出根目录
在 model3d/mod.rs 新增 model3d_ts_export_dir 宏,集中定义仓库相对导出根路径

common、image_to_model、multiview_to_model、text_to_model 改为调用宏并只声明各自子目录名
2026-09-19 13:07:41 +08:00
k88936 35be25dbd6 补齐 model3d 契约在 packages/shared 的导出入口
新增 packages/shared/src/contracts/model3d/index.ts 聚合 ts-rs 生成的 3D 生成契约类型

contracts/index.ts 增加 export type * from './model3d'

src/index.ts 增加 export type * from './contracts/model3d'
2026-09-19 13:02:01 +08:00
k88936 da9ee65119 记录Tripo真实冒烟结果结构
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 7m41s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 7m48s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 7m50s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 8m0s
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m17s
Project CI / Repository checks (pull_request) Failing after 10s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m29s
Project CI / Frontend tests (pull_request) Successful in 14m49s
Project CI / AI game creator shell web tests (pull_request) Failing after 18m48s
Project CI / Native shell tests (pull_request) Failing after 27m31s
保留三个生成入口的脱敏真实结果形状与下载产物信息

使用实际可访问的 Rustacean 图片完成 image 与 multiview 示例

同步严格 endpoint 结果契约和真实 Provider 验收文档
2026-09-18 21:42:17 +08:00
k88936 527706b84e 按接口收紧Tripo任务结果类型
为 text、image、multiview 三类生成定义独立必填结果结构

以枚举关联任务类型与具体结果并对缺失字段失败关闭

将通用 URL 类型由 TripoModelUrl 更名为 TripoUrl
2026-09-18 21:37:38 +08:00
k88936 a4beb53dc6 完善Tripo冒烟CLI产物流程
等待三类生成任务完成并打印映射后的输出

完成后下载模型产物到当前目录并输出文件信息

仅在示例内部使用SDK轮询器,不扩展平台公共接口
2026-09-18 18:51:49 +08:00
k88936 25314d9a4a 新增Tripo三类生成冒烟CLI
增加仅通过环境变量配置的 Tripo generation example

顺序提交 text、image、multiview 三类任务并执行单次状态查询

保持无命令行参数且不调用 SDK 轮询接口
2026-09-18 18:42:58 +08:00
k88936 5be4a51ac1 完善Tripo枚举与参数组合校验
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 7m19s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 7m22s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 7m26s
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 7m38s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m1s
Project CI / Repository checks (pull_request) Failing after 15s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m54s
Project CI / Native shell tests (pull_request) Failing after 3m5s
Project CI / AI game creator shell web tests (pull_request) Failing after 2m13s
Project CI / Frontend tests (pull_request) Successful in 5m44s
按提供的 Tripo 文档补齐 compression、texture quality 与 texture version 枚举

移除未在三个生成接口出现的 style 占位字段并同步 Rust/TypeScript contracts

增加模型版本、纹理版本、生成部件、quad、smart-low-poly 与 face limit 的副作用前校验

同步 Provider 技术方案和 ADR
2026-09-18 17:23:55 +08:00
k88936 37b355a814 接入Tripo三类3D生成Provider
加入固定 revision 的 tripo-rust-sdk 与 platform-tripo workspace crate

实现 text-to-model、image-to-model、multiview-to-model 提交、通用任务查询和显式模型下载

抽取通用任务类型、状态映射、输出 URL 校验与 SDK 错误归一,保持 wait_for_task 不对外暴露

同步 Provider 边界文档并暂不接入 api-server
2026-09-18 16:02:13 +08:00
k88936 1c4009ea3b 建立3D生成目录化契约
新增 model3d common、text-to-model、image-to-model、multiview-to-model Rust contracts

通过 ts-rs 按目录生成 TypeScript bindings,移除依赖 barrel 的组织方式

增加 model3d contracts 定向生成脚本
2026-09-18 15:59:19 +08:00
218 changed files with 7545 additions and 11138 deletions
-6
View File
@@ -41,12 +41,6 @@ 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/
/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/
+18 -16
View File
@@ -50,6 +50,24 @@ _Avoid_: 为每个玩法单独发明素材流水线、把系列素材建模成
## Language ## Language
### 3D Model Generation
**3D 模型生成操作**:
由文本提示驱动、可异步查询并最终产生模型文件的 provider-neutral 操作;操作状态和产品资源结果分开建模。
_Avoid_: 直接把第三方 provider task 当作产品资源、把模型 URL 当作永久资源地址
**Provider task**:
第三方 3D 生成平台返回的任务引用与任务快照,只存在于 provider adapter 的 Rust 边界内;它不是 Genarrative 的内部操作 ID。
_Avoid_: 把 Tripo task ID 当作稳定产品 operationId、让 SDK 类型穿透 api-server
**临时 provider URL**:
第三方任务完成后返回的短时模型下载地址,必须通过显式下载流程转换为本地或持久化资源,不能直接当作长期资产地址。
_Avoid_: 永久 OSS URL、资源 ID
**模型 artifact**:
由 text-to-model、image-to-model 或 multiview-to-model 任务产生、待应用层登记和持久化的模型文件结果;provider adapter 只提供可下载结果,不负责生成 resourceId 或 assetId。
_Avoid_: 仅 UI 中展示的模型链接、没有来源任务的静态文件
### Puzzle Clear ### Puzzle Clear
**拼消消**: **拼消消**:
@@ -172,20 +190,6 @@ _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
- 一个 **汪汪声浪大作战** 单局包含多个 **有效声浪触发** - 一个 **汪汪声浪大作战** 单局包含多个 **有效声浪触发**
@@ -220,5 +224,3 @@ _Avoid_: 投影缓存文件、已脱敏卡片库、第二套 reducer
- “入口闭环”曾可能只指内部 demo 或单个详情 CTA;已解析为 **正式作品入口闭环**,不新增独立专区或活动页。 - “入口闭环”曾可能只指内部 demo 或单个详情 CTA;已解析为 **正式作品入口闭环**,不新增独立专区或活动页。
- “创作编辑”曾可能指多步骤向导或完整编辑器;已解析为 **轻配置编辑流程**,使用单页表单 + 预览卡片完成保存草稿、发布和发布后跳转作品详情。 - “创作编辑”曾可能指多步骤向导或完整编辑器;已解析为 **轻配置编辑流程**,使用单页表单 + 预览卡片完成保存草稿、发布和发布后跳转作品详情。
- “实施顺序”曾可能按 UI 或功能并行发散;已解析为契约/领域规则先行,再做后端存储/API,随后打通最小前端纵切,最后补投影体验与收口验证。 - “实施顺序”曾可能按 UI 或功能并行发散;已解析为契约/领域规则先行,再做后端存储/API,随后打通最小前端纵切,最后补投影体验与收口验证。
- “回合进度事件”曾同时指 Direct turn update 与 Thread Manager 运行态事件;已解析为 AGC 项目开发对话只保留 **运行态事件**
- “哪些消息可显示”曾可能由后端历史分页判断;已解析为可见性判断属于 **聊天投影**,后端只按原始条目分页,前端负责跳过不可显示条目并推进分页锚点。
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@genarrative/ai-game-creator-shell", "name": "@genarrative/ai-game-creator-shell",
"private": true, "private": true,
"version": "0.1.67", "version": "0.1.47",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "node scripts/start-tauri-dev.mjs", "dev": "node scripts/start-tauri-dev.mjs",
@@ -14,71 +14,16 @@ 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';
function defaultTarget() { const releaseTarget =
return process.env.AGC_BUILD_TARGET?.trim() || defaultReleaseTarget; process.env.AGC_BUILD_TARGET?.trim() || defaultReleaseTarget;
} const bundleRoot = path.join(
function explicitBuildTarget(args) {
let target;
const separator = args.indexOf('--');
const options = separator < 0 ? args : args.slice(0, separator);
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, appRoot,
'src-tauri', 'src-tauri',
'target', 'target',
target, releaseTarget,
'release', 'release',
'bundle', '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');
@@ -154,7 +99,7 @@ export function nextPatchVersion(localVersion, remoteVersion) {
return `${major}.${minor}.${patch + 1}`; return `${major}.${minor}.${patch + 1}`;
} }
export function resolveReleasePlatform(target = defaultTarget()) { export function resolveReleasePlatform(target = releaseTarget) {
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';
@@ -163,7 +108,7 @@ export function resolveReleasePlatform(target = defaultTarget()) {
export function resolveReleaseChannel( export function resolveReleaseChannel(
env = process.env, env = process.env,
target = defaultTarget(), target = releaseTarget,
) { ) {
const platform = resolveReleasePlatform(target); const platform = resolveReleasePlatform(target);
const requested = env.AGC_UPDATE_CHANNEL?.trim(); const requested = env.AGC_UPDATE_CHANNEL?.trim();
@@ -197,10 +142,13 @@ export function updateManifestUrl(channel = resolveReleaseChannel()) {
} }
/** /**
* 单架构产物只登记实际目标,不能把同一原生资源映射为另一架构。 * 更新插件按运行时平台键查找清单条目:universal macOS 包同时挂
* `darwin-aarch64` 与 `darwin-x86_64`,单架构目标只挂对应键。
*/ */
export function resolveManifestPlatformKeys(target = defaultTarget()) { export function resolveManifestPlatformKeys(target = releaseTarget) {
validateReleaseTarget(target); if (target === 'universal-apple-darwin') {
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')) {
@@ -308,8 +256,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(context = resolveReleaseContext()) { export async function prepareReleaseVersion() {
const { channel } = context; const channel = resolveReleaseChannel();
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();
@@ -382,14 +330,18 @@ export async function prepareReleaseVersion(context = resolveReleaseContext()) {
export function buildTauriBuildArguments( export function buildTauriBuildArguments(
args = [], args = [],
target = defaultTarget(), target = releaseTarget,
platform = process.platform, platform = process.platform,
) { ) {
const noBundle = args.includes('--no-bundle'); const noBundle = args.includes('--no-bundle');
const explicitTarget = explicitBuildTarget(args); const targetIndex = args.indexOf('--target');
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),
); );
@@ -422,33 +374,18 @@ function writeChannelConfigFile(channel) {
return configPath; return configPath;
} }
export function runTauriBuild( export function runTauriBuild(args = []) {
args = [], const tauriArguments = buildTauriBuildArguments(args);
context = resolveReleaseContext(args), if (!tauriArguments.includes('--config') && !tauriArguments.includes('-c')) {
{ spawn = spawnSync } = {}, const channel = resolveReleaseChannel();
) {
if (
explicitBuildTarget(args) &&
explicitBuildTarget(args) !== context.target
) {
throw new Error('构建参数与发布上下文目标不一致');
}
const tauriArguments = buildTauriBuildArguments(args, context.target);
const { channel } = context;
const configPath = writeChannelConfigFile(channel); const configPath = writeChannelConfigFile(channel);
console.log( console.log(
`[ai-game-creator-shell] 渠道 ${channel} 端点配置:${configPath}`, `[ai-game-creator-shell] 渠道 ${channel} 端点配置:${configPath}`,
); );
// 最后合并渠道配置,防止用户配置中的端点与实际发布目标分叉。 tauriArguments.push('--config', 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 = spawn( const result = spawnSync(
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' },
@@ -465,11 +402,11 @@ function listFiles(root) {
}); });
} }
function artifactPriority(filePath, target) { function artifactPriority(filePath) {
const name = path.basename(filePath).toLowerCase(); const name = path.basename(filePath).toLowerCase();
if (target.includes('windows')) return name.endsWith('.exe') ? 0 : 99; if (releaseTarget.includes('windows')) return name.endsWith('.exe') ? 0 : 99;
// 更新链路要的是 updater 产物(macOS 为 .app.tar.gz),dmg 只作人工分发。 // 更新链路要的是 updater 产物(macOS 为 .app.tar.gz),dmg 只作人工分发。
if (target.includes('apple-darwin')) { if (releaseTarget.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;
@@ -479,8 +416,7 @@ function artifactPriority(filePath, target) {
return 99; return 99;
} }
export function selectReleaseArtifact(files, target = defaultTarget()) { export function selectReleaseArtifact(files) {
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);
@@ -491,10 +427,9 @@ export function selectReleaseArtifact(files, target = defaultTarget()) {
} }
return ( return (
[...files] [...files]
.filter((filePath) => artifactPriority(filePath, target) < 99) .filter((filePath) => artifactPriority(filePath) < 99)
.sort((left, right) => { .sort((left, right) => {
const priority = const priority = artifactPriority(left) - artifactPriority(right);
artifactPriority(left, target) - artifactPriority(right, target);
return priority || left.localeCompare(right); return priority || left.localeCompare(right);
})[0] ?? null })[0] ?? null
); );
@@ -515,15 +450,13 @@ function readUpdaterSignature(artifactPath) {
export function createUpdateManifest( export function createUpdateManifest(
artifactPath, artifactPath,
{ {
target = defaultTarget(), channel = resolveReleaseChannel(),
channel = resolveReleaseChannel(process.env, target), target = releaseTarget,
publishedAt = new Date().toISOString(), publishedAt = new Date().toISOString(),
notes = readReleaseNotes(), notes = readReleaseNotes(),
commit = readHeadCommit(), commit = readHeadCommit(),
} = {}, } = {},
) { ) {
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 fileName = path.basename(artifactPath); const fileName = path.basename(artifactPath);
@@ -671,11 +604,9 @@ export function createLegacyUpdateManifest(
}; };
} }
export async function generateUpdateManifest( export async function generateUpdateManifest() {
context = resolveReleaseContext(), const channel = resolveReleaseChannel();
) { const artifact = selectReleaseArtifact(listFiles(bundleRoot));
const { channel, target, bundleRoot } = context;
const artifact = selectReleaseArtifact(listFiles(bundleRoot), target);
if (!artifact) { if (!artifact) {
throw new Error(`未找到可发布的 AGC 安装包:${bundleRoot}`); throw new Error(`未找到可发布的 AGC 安装包:${bundleRoot}`);
} }
@@ -692,7 +623,7 @@ 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, target, notes }); const manifest = createUpdateManifest(artifact, { channel, notes });
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');
@@ -744,24 +675,12 @@ 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);
await buildRelease(args); if (!args.includes('--no-bundle')) await prepareReleaseVersion();
runTauriBuild(args);
if (!args.includes('--no-bundle')) await generateUpdateManifest();
} }
@@ -14,8 +14,6 @@ import { fileURLToPath } from 'node:url';
import { import {
agcReleasePathPatterns, agcReleasePathPatterns,
buildRelease,
buildTauriBuildArguments,
collectRecentReleaseCommits, collectRecentReleaseCommits,
collectReleaseCommits, collectReleaseCommits,
compareVersions, compareVersions,
@@ -24,14 +22,11 @@ import {
createUpdateManifest, createUpdateManifest,
formatRecentReleaseNotes, formatRecentReleaseNotes,
formatReleaseNotes, formatReleaseNotes,
generateUpdateManifest,
nextPatchVersion, nextPatchVersion,
resolveManifestPlatformKeys, resolveManifestPlatformKeys,
resolvePreviousReleaseCommit, resolvePreviousReleaseCommit,
resolveReleaseChannel, resolveReleaseChannel,
resolveReleaseContext,
resolveRemoteHighWaterVersion, resolveRemoteHighWaterVersion,
runTauriBuild,
selectReleaseArtifact, selectReleaseArtifact,
updateManifestUrl, updateManifestUrl,
} from './build-release.mjs'; } from './build-release.mjs';
@@ -39,21 +34,6 @@ import {
const windowsTarget = 'x86_64-pc-windows-msvc'; const windowsTarget = 'x86_64-pc-windows-msvc';
const universalTarget = 'universal-apple-darwin'; const universalTarget = 'universal-apple-darwin';
test('native sidecar builds reject universal targets and accept each macOS architecture', () => {
assert.throws(() => buildTauriBuildArguments([], universalTarget), /单架构/);
assert.throws(
() => buildTauriBuildArguments(['--target=universal-apple-darwin']),
/单架构/,
);
for (const target of ['aarch64-apple-darwin', 'x86_64-apple-darwin']) {
assert.deepEqual(buildTauriBuildArguments([], target), [
'build',
'--target',
target,
]);
}
});
function withEnv(overrides, run) { function withEnv(overrides, run) {
const previous = new Map(); const previous = new Map();
for (const [key, value] of Object.entries(overrides)) { for (const [key, value] of Object.entries(overrides)) {
@@ -152,12 +132,9 @@ test('channel manifest URL and build-time endpoint follow the channel', () => {
}); });
}); });
test('macOS manifests only advertise the architecture actually built', () => { test('universal macOS builds publish one artifact under both platform keys', () => {
assert.throws(() => resolveManifestPlatformKeys(universalTarget), /单架构/); assert.deepEqual(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), [
@@ -165,218 +142,6 @@ test('macOS manifests only advertise the architecture actually built', () => {
]); ]);
}); });
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);
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]));
});
test('real manifest writer uses the resolved bundle root and does not emit Windows artifacts', 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 context = {
...resolveReleaseContext(['--target=x86_64-apple-darwin'], {}),
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.manifestPath, path.join(root, 'latest.json'));
assert.equal(result.legacyManifestPath, null);
assert.deepEqual(Object.keys(result.manifest.platforms), ['darwin-x86_64']);
assert.match(result.manifest.platforms['darwin-x86_64'].url, /\/dev-mac\//);
} 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'));
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: '修复与改进' }, () => {
@@ -580,7 +345,6 @@ test('release upload forces overwrite for artifact, signature and channel pointe
); );
assert.match(source, /agc\/\$\{channel\}\/latest\.json/u); assert.match(source, /agc\/\$\{channel\}\/latest\.json/u);
assert.match(source, /agc\/latest\.json/u); assert.match(source, /agc\/latest\.json/u);
assert.match(source, /await buildRelease\(process\.argv\.slice\(2\)\)/u);
}); });
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', () => {
@@ -35,12 +35,6 @@ 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',
@@ -1364,37 +1358,6 @@ 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(
@@ -1759,7 +1722,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',
'fn game_creator_bundled_codex_cli_path', 'GAME_CREATOR_BUNDLED_CODEX_CLI_RELATIVE_PATH',
'validate_game_creator_bundled_codex_cli', 'validate_game_creator_bundled_codex_cli',
'内置 Codex CLI 完整性校验失败', '内置 Codex CLI 完整性校验失败',
]) { ]) {
@@ -1,222 +0,0 @@
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 });
}
@@ -12,7 +12,8 @@ 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 { buildRelease } = await import('./build-release.mjs'); const { generateUpdateManifest, prepareReleaseVersion, runTauriBuild } =
await import('./build-release.mjs');
function runOssutil(args) { function runOssutil(args) {
const binary = process.env.OSSUTIL_BIN?.trim() || 'ossutil'; const binary = process.env.OSSUTIL_BIN?.trim() || 'ossutil';
@@ -50,8 +51,10 @@ function runOssutil(args) {
if (result.status !== 0) process.exit(result.status ?? 1); if (result.status !== 0) process.exit(result.status ?? 1);
} }
await prepareReleaseVersion();
runTauriBuild([]);
const { artifact, channel, legacyManifestPath, manifest, manifestPath } = const { artifact, channel, legacyManifestPath, manifest, manifestPath } =
await buildRelease(process.argv.slice(2)); await generateUpdateManifest();
const artifactKey = `agc/${channel}/${manifest.version}/${path.basename(artifact)}`; const artifactKey = `agc/${channel}/${manifest.version}/${path.basename(artifact)}`;
// Jenkins/ossutil 默认会在目标对象已存在时交互询问并按默认值跳过; // Jenkins/ossutil 默认会在目标对象已存在时交互询问并按默认值跳过;
// 发布清单是固定的 latest 指针,必须显式覆盖,否则流水线会误报成功但远端仍保留旧版本。 // 发布清单是固定的 latest 指针,必须显式覆盖,否则流水线会误报成功但远端仍保留旧版本。
+2 -1
View File
@@ -1745,7 +1745,7 @@ dependencies = [
[[package]] [[package]]
name = "genarrative-ai-game-creator-shell" name = "genarrative-ai-game-creator-shell"
version = "0.1.67" version = "0.1.47"
dependencies = [ dependencies = [
"agent-runtime-core", "agent-runtime-core",
"axum", "axum",
@@ -5016,6 +5016,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2",
"ts-rs",
] ]
[[package]] [[package]]
@@ -1,6 +1,6 @@
[package] [package]
name = "genarrative-ai-game-creator-shell" name = "genarrative-ai-game-creator-shell"
version = "0.1.67" version = "0.1.47"
edition = "2021" edition = "2021"
publish = false publish = false
+48 -82
View File
@@ -1,18 +1,31 @@
#[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);
@@ -29,15 +42,7 @@ 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) {
let target = env::var("TARGET").expect("Cargo TARGET"); #[cfg(windows)]
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()
@@ -46,23 +51,24 @@ 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 package = layout.npm_package; let source_candidates = [
let source_candidates = [app_root, repo_root] app_root.join(
.into_iter() "node_modules/@openai/codex-win32-x64/vendor/x86_64-pc-windows-msvc",
.flat_map(|root| { ),
[ app_root.join(
root.join(format!("node_modules/@openai/{package}/vendor/{target}")), "node_modules/@openai/codex/node_modules/@openai/codex-win32-x64/vendor/x86_64-pc-windows-msvc",
root.join(format!( ),
"node_modules/@openai/codex/node_modules/@openai/{package}/vendor/{target}" repo_root.join(
)), "node_modules/@openai/codex-win32-x64/vendor/x86_64-pc-windows-msvc",
] ),
}) repo_root.join(
.collect::<Vec<_>>(); "node_modules/@openai/codex/node_modules/@openai/codex-win32-x64/vendor/x86_64-pc-windows-msvc",
),
];
let source = source_candidates let source = source_candidates
.iter() .iter()
.find(|path| { .find(|path| {
layout BUNDLED_CODEX_FILES
.files
.iter() .iter()
.all(|relative| path.join(relative).is_file()) .all(|relative| path.join(relative).is_file())
}) })
@@ -77,26 +83,14 @@ fn stage_bundled_codex_cli(manifest_dir: &std::path::Path) {
.join("") .join("")
) )
}); });
let metadata: serde_json::Value = serde_json::from_slice( let target_dir = manifest_dir.join("resources/codex/win-x64");
&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, &notice);
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 layout.files { for relative in BUNDLED_CODEX_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() {
@@ -110,23 +104,15 @@ 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": codex_bundle::SCHEMA, "schemaVersion": "genarrative-codex-sidecar.v2",
"platform": layout.platform, "platform": "win32-x64",
"version": codex_bundle::CLI_VERSION, "version": BUNDLED_CODEX_CLI_VERSION,
"files": file_hashes, "files": file_hashes,
}); });
let manifest_path = target_dir.join("manifest.json"); let manifest_path = target_dir.join("manifest.json");
@@ -140,7 +126,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 layout.files { for relative in BUNDLED_CODEX_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());
@@ -270,11 +256,8 @@ fn stage_cocos_editor_payload(_manifest_dir: &std::path::Path) {}
/// ///
/// 只复制插件运行需要的清单、入口、面板和 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())
@@ -283,10 +266,6 @@ 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,
@@ -294,13 +273,6 @@ 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;
} }
@@ -315,18 +287,17 @@ fn stage_plugin_workspace(manifest_dir: &std::path::Path) {
std::path::PathBuf::from("panels"), std::path::PathBuf::from("panels"),
std::path::PathBuf::from("native/payload"), std::path::PathBuf::from("native/payload"),
] { ] {
if relative == std::path::Path::new("native/payload") && !target.contains("windows") {
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 bytes = std::fs::read(source) let Ok(bytes) = std::fs::read(source) else {
.unwrap_or_else(|error| panic!("读取随包资源失败 {}{error}", source.display())); return;
};
if std::fs::read(destination).is_ok_and(|existing| existing == bytes) { if std::fs::read(destination).is_ok_and(|existing| existing == bytes) {
return; return;
} }
@@ -336,6 +307,7 @@ 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,
@@ -344,17 +316,10 @@ 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 name.starts_with('.') || matches!(name.as_ref(), "target" | "node_modules") { if matches!(name.as_ref(), "target" | "node_modules" | ".git") {
continue; continue;
} }
std::fs::create_dir_all(&target).expect("创建插件资源目录失败"); std::fs::create_dir_all(&target).expect("创建插件资源目录失败");
@@ -366,14 +331,12 @@ 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;
@@ -382,3 +345,6 @@ 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) {}
@@ -1,133 +0,0 @@
//! 构建与运行共用的平台布局;只允许分发锁定原生包里的明确组件。
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}"
);
}
}
}
@@ -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, 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. 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.
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.
@@ -16,6 +16,4 @@ 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 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. `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.
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.
@@ -1,6 +1,6 @@
{ {
"schemaVersion": "agc-skill-pack.v1", "schemaVersion": "agc-skill-pack.v1",
"version": "2026-08-26.24", "version": "2026-08-26.20",
"skills": [ "skills": [
{ {
"name": "agc-game-production-workflow", "name": "agc-game-production-workflow",
@@ -63,7 +63,7 @@
"agents/openai.yaml", "agents/openai.yaml",
"references/platform-art-contract.md" "references/platform-art-contract.md"
], ],
"sha256": "47ac742d9b88e5d6cd9833484ab212152578e58ae27f7add312fd1d78183385c" "sha256": "c6329c6a3cbd17a237d042349d7fd8adcf240287ef56d23b49329923e976d534"
}, },
{ {
"name": "agc-web-game-development", "name": "agc-web-game-development",
@@ -123,7 +123,7 @@
"agents/openai.yaml", "agents/openai.yaml",
"references/projection-contract.md" "references/projection-contract.md"
], ],
"sha256": "247787975944ce8b21d7c879c39c60ec13608056cff9426ac374c9299937d475" "sha256": "93210c0eeb73b279d35aa85c201c226139b0bdf041f3300ac2c6e2c1bdd63afe"
} }
] ]
} }
@@ -19,12 +19,6 @@ image, UI design image, or publication material; use `agc_edit_image` for an
edit of an existing registered image; use `taonier_prepare_game_art` only for edit of an existing registered image; use `taonier_prepare_game_art` only for
the complete game-art package and its canonical slices. the complete game-art package and its canonical slices.
With `agc_generate_image`, `kind="character"` and `kind="art-spritesheet"`
generate the subject on a solid-colour background and automatically matte it
away afterwards, producing transparent-background results; write the prompt
for the subject only, never for a scene. `kind="image"` keeps the rendered
frame without extra processing.
When `agc_generate_image` is used with `kind="art-spritesheet"`, `sliceMode` is When `agc_generate_image` is used with `kind="art-spritesheet"`, `sliceMode` is
required and has no default, so decide it explicitly: required and has no default, so decide it explicitly:
@@ -1,14 +0,0 @@
# 内置 Codex CLI
本安装包包含锁定版本 Codex CLI 0.147.0 的 macOS 原生组件。
Codex CLI 按 Apache License 2.0 分发,源码与许可证见
https://github.com/openai/codex。
组件来自项目锁定的 `@openai/codex` 原生 npm 依赖,保留上游的
`bin/codex``bin/codex-code-mode-host``codex-path/rg`
`codex-resources/zsh/bin/zsh``codex-package.json` 相对布局。
原生依赖中的 ripgrep 与 zsh 按各自上游许可证分发:
https://github.com/BurntSushi/ripgrep 和 https://www.zsh.org/。
安装包不包含 API Key、登录状态、用户配置或项目数据。
@@ -21,7 +21,6 @@ mod direct_project_history;
mod direct_project_turn_history; mod direct_project_turn_history;
mod direct_runtime; mod direct_runtime;
mod direct_thread_manager; mod direct_thread_manager;
mod direct_thread_wire;
mod direct_tool_bridge; mod direct_tool_bridge;
mod direct_tool_calls; mod direct_tool_calls;
mod direct_tools_mcp; mod direct_tools_mcp;
@@ -41,8 +40,7 @@ use codex_app_server::*;
pub(crate) use codex_app_server::{ pub(crate) use codex_app_server::{
cancel_direct_codex_turn_at, cancel_direct_codex_turn_at,
direct_codex_canonical_project_identity_for_commands as direct_codex_canonical_project_identity, direct_codex_canonical_project_identity_for_commands as direct_codex_canonical_project_identity,
direct_game_creator_codex_chat_at, direct_game_creator_home_codex_chat, direct_game_creator_codex_chat_at, direct_game_creator_home_codex_chat, DirectTurnCancelView,
direct_thread_id_for_project, DirectTurnCancelView,
}; };
use codex_cli::*; use codex_cli::*;
pub(crate) use codex_cli::{ pub(crate) use codex_cli::{
@@ -57,7 +55,6 @@ pub(crate) use direct_project_history::*;
pub(crate) use direct_project_turn_history::*; pub(crate) use direct_project_turn_history::*;
pub(crate) use direct_runtime::*; pub(crate) use direct_runtime::*;
pub(crate) use direct_thread_manager::*; pub(crate) use direct_thread_manager::*;
pub(crate) use direct_thread_wire::*;
pub(crate) use direct_tool_bridge::*; pub(crate) use direct_tool_bridge::*;
pub(crate) use direct_tool_calls::*; pub(crate) use direct_tool_calls::*;
pub(crate) use direct_tools_mcp::*; pub(crate) use direct_tools_mcp::*;
@@ -20,29 +20,6 @@ pub(crate) fn direct_codex_canonical_project_identity(
)) ))
} }
/// 项目根目录在 Thread Manager 里的线程身份。
///
/// 订阅入口、回合事件写入和"回合被兜底释放"三处必须算出同一个字符串,否则前端会订阅到
/// 一个永不产生事件的空线程。这个字符串**只取决于路径**:能归一就用 canonical 路径,只有
/// 归一本身失败(路径不存在 / 不是目录 / 无法安全解析)才退回调用方给的字符串。
///
/// 这里刻意不读 `.agent/manifest.json`:那次读取是"项目权威身份"(连接池摘要,见
/// `direct_codex_canonical_project_identity`)的要求,而线程 id 只是一个路径 key。把
/// manifest 的瞬时抖动混进线程 id,会让同一项目在"订阅那一刻"与"跑回合那一刻"算出两个
/// 字符串(例如调用方给的是符号链接路径),订阅就绑到一条永远不会有事件的空线程上。
pub(crate) fn direct_thread_id_for_project(root: &std::path::Path) -> String {
let Ok((canonical_root, _)) = resolve_direct_codex_project_authority(root) else {
return root.to_string_lossy().into_owned();
};
canonical_root
.to_str()
.and_then(|value| value.strip_prefix(r"\\?\"))
.map(std::path::Path::new)
.unwrap_or(canonical_root.as_path())
.to_string_lossy()
.into_owned()
}
pub(super) fn direct_codex_os_path_identity_bytes(path: &std::path::Path) -> Vec<u8> { pub(super) fn direct_codex_os_path_identity_bytes(path: &std::path::Path) -> Vec<u8> {
#[cfg(unix)] #[cfg(unix)]
{ {
File diff suppressed because it is too large Load Diff
@@ -5,10 +5,18 @@ use std::process::Stdio;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt};
#[path = "../../build_support/codex_bundle.rs"]
mod codex_bundle;
const GAME_CREATOR_CODEX_CLI_EXECUTABLE: &str = "codex"; const GAME_CREATOR_CODEX_CLI_EXECUTABLE: &str = "codex";
const GAME_CREATOR_BUNDLED_CODEX_CLI_RELATIVE_PATH: &str = "coding-agent/win-x64/bin/codex.exe";
const GAME_CREATOR_BUNDLED_CODEX_CLI_MANIFEST_RELATIVE_PATH: &str =
"coding-agent/win-x64/manifest.json";
const GAME_CREATOR_BUNDLED_CODEX_CLI_REQUIRED_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",
];
const GAME_CREATOR_CODEX_CLI_PROMPT_MAX_BYTES: usize = 4 * 1024 * 1024; const GAME_CREATOR_CODEX_CLI_PROMPT_MAX_BYTES: usize = 4 * 1024 * 1024;
const GAME_CREATOR_CODEX_CLI_STDOUT_MAX_BYTES: usize = 4 * 1024 * 1024; const GAME_CREATOR_CODEX_CLI_STDOUT_MAX_BYTES: usize = 4 * 1024 * 1024;
const GAME_CREATOR_CODEX_CLI_STDERR_MAX_BYTES: usize = 256 * 1024; const GAME_CREATOR_CODEX_CLI_STDERR_MAX_BYTES: usize = 256 * 1024;
@@ -30,11 +38,11 @@ fn game_creator_codex_cli_executable_candidates_for(
path: Option<&std::ffi::OsStr>, path: Option<&std::ffi::OsStr>,
) -> Vec<PathBuf> { ) -> Vec<PathBuf> {
let mut candidates = Vec::new(); let mut candidates = Vec::new();
if let Some(bundled) = game_creator_bundled_codex_cli_path(resource_dir) {
candidates.push(bundled);
}
#[cfg(windows)] #[cfg(windows)]
{ {
if let Some(resource_dir) = resource_dir {
candidates.push(resource_dir.join(GAME_CREATOR_BUNDLED_CODEX_CLI_RELATIVE_PATH));
}
fn append_native_npm_candidates(candidates: &mut Vec<PathBuf>, npm_root: &Path) { fn append_native_npm_candidates(candidates: &mut Vec<PathBuf>, npm_root: &Path) {
let vendor_root = npm_root let vendor_root = npm_root
.join("node_modules") .join("node_modules")
@@ -101,63 +109,44 @@ fn game_creator_codex_cli_executable_candidates() -> Vec<PathBuf> {
} }
fn game_creator_bundled_resource_dir() -> Option<PathBuf> { fn game_creator_bundled_resource_dir() -> Option<PathBuf> {
let executable = std::env::current_exe().ok()?;
game_creator_bundled_resource_dir_for(&executable)
}
fn game_creator_bundled_resource_dir_for(executable: &Path) -> Option<PathBuf> {
#[cfg(windows)] #[cfg(windows)]
{ {
executable.parent().map(Path::to_path_buf) std::env::current_exe()
.ok()
.and_then(|path| path.parent().map(Path::to_path_buf))
} }
#[cfg(target_os = "macos")] #[cfg(not(windows))]
{ {
let macos = executable.parent()?;
let contents = macos.parent()?;
// 只接受真正的 app bundle 结构,开发态不从任意相邻目录加载程序。
if macos.file_name()? != "MacOS"
|| contents.file_name()? != "Contents"
|| contents.parent()?.extension()? != "app"
{
return None;
}
Some(contents.join("Resources"))
}
#[cfg(not(any(windows, target_os = "macos")))]
{
let _ = executable;
None None
} }
} }
fn game_creator_bundled_codex_cli_path(resource_dir: Option<&Path>) -> Option<PathBuf> { fn game_creator_bundled_codex_cli_path(resource_dir: Option<&Path>) -> Option<PathBuf> {
let layout = codex_bundle::for_target(env!("AGC_BUILD_TARGET"))?; resource_dir.map(|resource_dir| resource_dir.join(GAME_CREATOR_BUNDLED_CODEX_CLI_RELATIVE_PATH))
Some(
resource_dir?
.join("coding-agent")
.join(layout.directory)
.join(layout.executable),
)
} }
fn validate_game_creator_bundled_codex_cli(executable: &Path) -> Result<String, String> { fn validate_game_creator_bundled_codex_cli(executable: &Path) -> Result<String, String> {
let layout = codex_bundle::for_target(env!("AGC_BUILD_TARGET"))
.ok_or_else(|| "当前平台不支持内置 Codex CLI".to_string())?;
let bundle_root = executable let bundle_root = executable
.parent() .parent()
.and_then(Path::parent) .and_then(Path::parent)
.ok_or_else(|| "内置 Codex CLI 路径无效".to_string())?; .ok_or_else(|| "内置 Codex CLI 路径无效".to_string())?;
let manifest_path = bundle_root.join("manifest.json"); let manifest_path = bundle_root.join(
Path::new(GAME_CREATOR_BUNDLED_CODEX_CLI_MANIFEST_RELATIVE_PATH)
.file_name()
.expect("bundled Codex manifest file name"),
);
let manifest = std::fs::read_to_string(&manifest_path) let manifest = std::fs::read_to_string(&manifest_path)
.map_err(|_| "内置 Codex CLI 缺少完整性清单".to_string()) .map_err(|_| "内置 Codex CLI 缺少完整性清单".to_string())
.and_then(|value| { .and_then(|value| {
serde_json::from_str::<GameCreatorBundledCodexCliManifest>(&value) serde_json::from_str::<GameCreatorBundledCodexCliManifest>(&value)
.map_err(|_| "内置 Codex CLI 完整性清单无效".to_string()) .map_err(|_| "内置 Codex CLI 完整性清单无效".to_string())
})?; })?;
if manifest.schema_version != codex_bundle::SCHEMA if manifest.schema_version != "genarrative-codex-sidecar.v2"
|| manifest.platform != layout.platform || manifest.platform != "win32-x64"
|| manifest.version.trim().is_empty() || manifest.version.trim().is_empty()
|| layout.files.iter().any(|relative| { || GAME_CREATOR_BUNDLED_CODEX_CLI_REQUIRED_FILES
.iter()
.any(|relative| {
manifest.files.get(*relative).map_or(true, |hash| { manifest.files.get(*relative).map_or(true, |hash| {
hash.len() != 64 || !hash.bytes().all(|byte| byte.is_ascii_hexdigit()) hash.len() != 64 || !hash.bytes().all(|byte| byte.is_ascii_hexdigit())
}) })
@@ -165,7 +154,7 @@ fn validate_game_creator_bundled_codex_cli(executable: &Path) -> Result<String,
{ {
return Err("内置 Codex CLI 完整性清单不受支持".to_string()); return Err("内置 Codex CLI 完整性清单不受支持".to_string());
} }
for relative in layout.files { for relative in GAME_CREATOR_BUNDLED_CODEX_CLI_REQUIRED_FILES {
let path = bundle_root.join(relative); let path = bundle_root.join(relative);
let bytes = std::fs::read(&path).map_err(|_| { let bytes = std::fs::read(&path).map_err(|_| {
format!( format!(
@@ -174,7 +163,7 @@ fn validate_game_creator_bundled_codex_cli(executable: &Path) -> Result<String,
) )
})?; })?;
let actual = format!("{:x}", Sha256::digest(bytes)); let actual = format!("{:x}", Sha256::digest(bytes));
if !actual.eq_ignore_ascii_case(manifest.files.get(*relative).expect("validated hash")) { if !actual.eq_ignore_ascii_case(manifest.files.get(relative).expect("validated hash")) {
return Err(format!("内置 Codex CLI 完整性校验失败:组件 {relative}")); return Err(format!("内置 Codex CLI 完整性校验失败:组件 {relative}"));
} }
} }
@@ -910,7 +899,7 @@ mod tests {
fn codex_cli_candidates_prefer_bundled_sidecar_before_npm_and_path() { fn codex_cli_candidates_prefer_bundled_sidecar_before_npm_and_path() {
let temp = tempfile::tempdir().expect("temp dir"); let temp = tempfile::tempdir().expect("temp dir");
let resources = temp.path().join("resources"); let resources = temp.path().join("resources");
let bundled = game_creator_bundled_codex_cli_path(Some(&resources)).unwrap(); let bundled = resources.join(GAME_CREATOR_BUNDLED_CODEX_CLI_RELATIVE_PATH);
std::fs::create_dir_all(bundled.parent().expect("bundled parent")).expect("bundled dir"); std::fs::create_dir_all(bundled.parent().expect("bundled parent")).expect("bundled dir");
let app_data = temp.path().join("app-data"); let app_data = temp.path().join("app-data");
let native = app_data let native = app_data
@@ -929,19 +918,20 @@ mod tests {
assert_eq!(candidates[1], native); assert_eq!(candidates[1], native);
} }
#[cfg(any(windows, target_os = "macos"))] #[cfg(windows)]
#[test] #[test]
fn bundled_codex_cli_requires_matching_manifest_hash() { fn bundled_codex_cli_requires_matching_manifest_hash() {
let temp = tempfile::tempdir().expect("temp dir"); let temp = tempfile::tempdir().expect("temp dir");
let executable = game_creator_bundled_codex_cli_path(Some(temp.path())).unwrap(); let executable = temp
let layout = codex_bundle::for_target(env!("AGC_BUILD_TARGET")).unwrap(); .path()
.join(GAME_CREATOR_BUNDLED_CODEX_CLI_RELATIVE_PATH);
std::fs::create_dir_all(executable.parent().expect("sidecar parent")).expect("sidecar dir"); std::fs::create_dir_all(executable.parent().expect("sidecar parent")).expect("sidecar dir");
let bundle_root = executable let bundle_root = executable
.parent() .parent()
.and_then(Path::parent) .and_then(Path::parent)
.expect("bundle root"); .expect("bundle root");
let mut hashes = std::collections::BTreeMap::new(); let mut hashes = std::collections::BTreeMap::new();
for relative in layout.files { for relative in GAME_CREATOR_BUNDLED_CODEX_CLI_REQUIRED_FILES {
let path = bundle_root.join(relative); let path = bundle_root.join(relative);
std::fs::create_dir_all(path.parent().expect("component parent")) std::fs::create_dir_all(path.parent().expect("component parent"))
.expect("component dir"); .expect("component dir");
@@ -949,14 +939,14 @@ mod tests {
std::fs::write(&path, bytes.as_bytes()).expect("component bytes"); std::fs::write(&path, bytes.as_bytes()).expect("component bytes");
hashes.insert(relative, format!("{:x}", Sha256::digest(bytes.as_bytes()))); hashes.insert(relative, format!("{:x}", Sha256::digest(bytes.as_bytes())));
} }
let manifest = serde_json::json!({ let files = serde_json::to_string(&hashes).expect("component hashes");
"schemaVersion": codex_bundle::SCHEMA, std::fs::write(
"platform": layout.platform, bundle_root.join("manifest.json"),
"version": "codex-cli test", format!(
"files": hashes, r#"{{"schemaVersion":"genarrative-codex-sidecar.v2","platform":"win32-x64","version":"codex-cli test","files":{files}}}"#
}); ),
let manifest_path = bundle_root.join("manifest.json"); )
std::fs::write(&manifest_path, manifest.to_string()).expect("sidecar manifest"); .expect("sidecar manifest");
assert_eq!( assert_eq!(
validate_game_creator_bundled_codex_cli(&executable).expect("trusted sidecar"), validate_game_creator_bundled_codex_cli(&executable).expect("trusted sidecar"),
@@ -966,31 +956,15 @@ mod tests {
assert!(validate_game_creator_bundled_codex_cli(&executable) assert!(validate_game_creator_bundled_codex_cli(&executable)
.expect_err("tampered sidecar must be rejected") .expect_err("tampered sidecar must be rejected")
.contains("完整性校验失败")); .contains("完整性校验失败"));
std::fs::write(&executable, format!("trusted {}", layout.executable)).unwrap();
let mut wrong_platform = manifest.clone();
wrong_platform["platform"] = serde_json::json!("wrong-platform");
std::fs::write(&manifest_path, wrong_platform.to_string()).unwrap();
assert!(validate_game_creator_bundled_codex_cli(&executable)
.expect_err("wrong platform must be rejected")
.contains("完整性清单不受支持"));
std::fs::write(&manifest_path, manifest.to_string()).unwrap();
let helper = bundle_root.join(layout.files[1]);
std::fs::write(&helper, b"tampered helper").unwrap();
assert!(validate_game_creator_bundled_codex_cli(&executable)
.expect_err("tampered helper must be rejected")
.contains("完整性校验失败"));
std::fs::remove_file(&helper).unwrap();
assert!(validate_game_creator_bundled_codex_cli(&executable)
.expect_err("missing helper must be rejected")
.contains("缺少必需组件"));
} }
#[cfg(any(windows, target_os = "macos"))] #[cfg(windows)]
#[test] #[test]
fn bundled_codex_cli_rejects_a_manifest_without_code_mode_host() { fn bundled_codex_cli_rejects_a_manifest_without_code_mode_host() {
let temp = tempfile::tempdir().expect("temp dir"); let temp = tempfile::tempdir().expect("temp dir");
let executable = game_creator_bundled_codex_cli_path(Some(temp.path())).unwrap(); let executable = temp
let layout = codex_bundle::for_target(env!("AGC_BUILD_TARGET")).unwrap(); .path()
.join(GAME_CREATOR_BUNDLED_CODEX_CLI_RELATIVE_PATH);
std::fs::create_dir_all(executable.parent().expect("sidecar parent")).expect("sidecar dir"); std::fs::create_dir_all(executable.parent().expect("sidecar parent")).expect("sidecar dir");
let bundle_root = executable let bundle_root = executable
.parent() .parent()
@@ -1001,13 +975,9 @@ mod tests {
let hash = format!("{:x}", Sha256::digest(bytes)); let hash = format!("{:x}", Sha256::digest(bytes));
std::fs::write( std::fs::write(
bundle_root.join("manifest.json"), bundle_root.join("manifest.json"),
serde_json::json!({ format!(
"schemaVersion": codex_bundle::SCHEMA, r#"{{"schemaVersion":"genarrative-codex-sidecar.v2","platform":"win32-x64","version":"codex-cli test","files":{{"bin/codex.exe":"{hash}"}}}}"#
"platform": layout.platform, ),
"version": "codex-cli test",
"files": { layout.executable: hash },
})
.to_string(),
) )
.expect("sidecar manifest"); .expect("sidecar manifest");
@@ -1016,38 +986,6 @@ mod tests {
.contains("完整性清单不受支持")); .contains("完整性清单不受支持"));
} }
#[cfg(target_os = "macos")]
#[test]
fn macos_bundle_resources_and_candidates_do_not_require_a_development_path() {
let executable = Path::new("/Applications/陶泥儿 测试.app/Contents/MacOS/taonier");
let resources = game_creator_bundled_resource_dir_for(executable).unwrap();
assert_eq!(
resources,
Path::new("/Applications/陶泥儿 测试.app/Contents/Resources")
);
let bundled = game_creator_bundled_codex_cli_path(Some(&resources)).unwrap();
let candidates = game_creator_codex_cli_executable_candidates_for(
Some(&resources),
None,
None,
None,
None,
);
assert_eq!(candidates, vec![bundled, PathBuf::from("codex")]);
assert!(
game_creator_bundled_resource_dir_for(Path::new("/tmp/target/release/taonier"))
.is_none()
);
assert!(
game_creator_bundled_resource_dir_for(Path::new("/tmp/Contents/MacOS/taonier"))
.is_none()
);
assert_eq!(
game_creator_codex_cli_executable_candidates_for(None, None, None, None, None),
vec![PathBuf::from("codex")],
);
}
#[cfg(windows)] #[cfg(windows)]
#[test] #[test]
fn codex_cli_resolver_finds_current_native_install() { fn codex_cli_resolver_finds_current_native_install() {
@@ -2,9 +2,9 @@
//! 有项目路径或导入状态时输出路径映射;否则保持首页元数据文案。不灌正文。 //! 有项目路径或导入状态时输出路径映射;否则保持首页元数据文案。不灌正文。
pub(crate) const MAX_DIRECT_CODEX_ATTACHMENTS: usize = 8; pub(crate) const MAX_DIRECT_CODEX_ATTACHMENTS: usize = 8;
pub(crate) const MAX_DIRECT_CODEX_ATTACHMENT_NAME_CHARS: usize = 160; const MAX_DIRECT_CODEX_ATTACHMENT_NAME_CHARS: usize = 160;
pub(crate) const MAX_DIRECT_CODEX_ATTACHMENT_MEDIA_TYPE_CHARS: usize = 96; const MAX_DIRECT_CODEX_ATTACHMENT_MEDIA_TYPE_CHARS: usize = 96;
pub(crate) const MAX_DIRECT_CODEX_ATTACHMENT_LOCAL_PATH_CHARS: usize = 512; const MAX_DIRECT_CODEX_ATTACHMENT_LOCAL_PATH_CHARS: usize = 512;
const HOME_ATTACHMENT_HEADER: &str = const HOME_ATTACHMENT_HEADER: &str =
"[首页附件说明:当前尚未打开项目,以下仅为附件元数据,附件内容尚不可读取]"; "[首页附件说明:当前尚未打开项目,以下仅为附件元数据,附件内容尚不可读取]";
@@ -550,8 +550,6 @@ fn extract_mcp_arguments(root: &Path, tool: &str, arguments: &Value) -> Value {
"agc_generate_image" => { "agc_generate_image" => {
copy_string(object, "kind", &mut out); copy_string(object, "kind", &mut out);
copy_string(object, "sliceMode", &mut out); copy_string(object, "sliceMode", &mut out);
copy_number(object, "sliceCount", &mut out);
copy_string(object, "screenColor", &mut out);
copy_string(object, "aspectRatio", &mut out); copy_string(object, "aspectRatio", &mut out);
copy_string(object, "imageSize", &mut out); copy_string(object, "imageSize", &mut out);
copy_string(object, "assetName", &mut out); copy_string(object, "assetName", &mut out);
@@ -1196,13 +1194,7 @@ mod tests {
"item": { "item": {
"type": "mcpToolCall", "type": "mcpToolCall",
"tool": "agc_generate_image", "tool": "agc_generate_image",
"arguments": { "arguments": { "prompt": prompt, "kind": "icon-spec" }
"prompt": prompt,
"kind": "art-spritesheet",
"sliceMode": "connected-components",
"sliceCount": 8,
"screenColor": "#CFEFFF"
}
} }
})); }));
audit.finish(true); audit.finish(true);
@@ -1213,8 +1205,6 @@ mod tests {
let stored = item["arguments"]["prompt"].as_str().expect("prompt"); let stored = item["arguments"]["prompt"].as_str().expect("prompt");
assert_eq!(stored.chars().count(), DIRECT_CODEX_AUDIT_BRIEF_CHARS); assert_eq!(stored.chars().count(), DIRECT_CODEX_AUDIT_BRIEF_CHARS);
assert_eq!(item["arguments"]["promptChars"], json!(5000)); assert_eq!(item["arguments"]["promptChars"], json!(5000));
assert_eq!(item["arguments"]["sliceCount"], json!(8));
assert_eq!(item["arguments"]["screenColor"], json!("#CFEFFF"));
assert_eq!( assert_eq!(
item["arguments"]["promptSha256"], item["arguments"]["promptSha256"],
json!(sha256_hex("".repeat(5000).as_bytes())) json!(sha256_hex("".repeat(5000).as_bytes()))
@@ -5,7 +5,7 @@ mod validation;
mod wire; mod wire;
pub(crate) use model::{ pub(crate) use model::{
DirectCodexUserAttachmentReferencePart, DirectCodexUserContentPart, DirectCodexUserItem, DirectCodexUserContentPart, DirectCodexUserItem, DirectCodexUserMessageEnvelope,
DirectCodexUserMessageItem, DirectCodexUserRole, DirectCodexUserRuntimeRegionPart, DirectCodexUserMessageItem, DirectCodexUserRole, DirectCodexUserRuntimeRegionPart,
}; };
pub(crate) use validation::validate_direct_codex_user_item; pub(crate) use validation::validate_direct_codex_user_item;
@@ -36,21 +36,6 @@ pub(crate) enum DirectCodexUserContentPart {
AgcResourceReference { resource_id: String }, AgcResourceReference { resource_id: String },
#[serde(rename = "agc_runtime_region_reference")] #[serde(rename = "agc_runtime_region_reference")]
AgcRuntimeRegionReference(DirectCodexUserRuntimeRegionPart), AgcRuntimeRegionReference(DirectCodexUserRuntimeRegionPart),
/// Uploaded project attachment kept inline in canonical content.
#[serde(rename = "agc_attachment_reference")]
AgcAttachmentReference(DirectCodexUserAttachmentReferencePart),
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/project-workspace/generated/"))]
pub(crate) struct DirectCodexUserAttachmentReferencePart {
pub(crate) name: String,
pub(crate) media_type: String,
#[ts(type = "number")]
pub(crate) size: u64,
pub(crate) local_path: String,
pub(crate) status: String,
} }
#[derive(Clone, Debug, Deserialize, Serialize, TS)] #[derive(Clone, Debug, Deserialize, Serialize, TS)]
@@ -76,6 +61,13 @@ pub(crate) struct DirectCodexUserRuntimeRegionPart {
pub(crate) resource_ids: Vec<String>, pub(crate) resource_ids: Vec<String>,
} }
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/project-workspace/generated/"))]
pub(crate) struct DirectCodexUserMessageEnvelope {
pub(crate) item: DirectCodexUserItem,
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

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