Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ca80cb28d2 |
@@ -102,7 +102,7 @@ Use OpenAPI as the final authority; these common values are a routing aid:
|
||||
|
||||
- Image `kind`: `spec`, `character`, `quick-edit`, `ui-design`, `publication-material`; ordinary image generation may omit it.
|
||||
- External v1 currently has no structured game-scene generation operation. Do not send `kind: "scene"` or `assetKind: "scene"` through generic image generation; the server rejects both before queueing.
|
||||
- Image `model`: `gpt-image-2.5`, `gemini-3.1-flash-image-preview`, `nanobanana2`, `nano-banana`. Persisted `gpt-image-2` / `gpt-image-2-c` are legacy values resolved only when submitting a new task.
|
||||
- Image `model`: `gpt-image-2`, `gemini-3.1-flash-image-preview`, `nanobanana2`, `nano-banana`.
|
||||
- Image `aspectRatio`: `1:1`, `2:3`, `3:2`, `9:16`, `16:9`.
|
||||
- Image `imageSize`: `0.5K`, `1K`, `2K`.
|
||||
- Video `model`: `seedance2.0`, `seedance2.0-fast`, `kling3.0`, `kling3.0-omni`, `veo3.1`, `veo3.1-fast`.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
name: gpt-image-2-apimart
|
||||
description: Generate or inspect project image assets through this repository's image workflow using the GPT Image 2.5 business model. Use when Codex needs to create puzzle template sample images, reproduce the server-rs image request body, dry-run image prompts, batch-generate local project thumbnails, or debug VECTOR_ENGINE_BASE_URL / VECTOR_ENGINE_API_KEY image-generation configuration without exposing secrets. The directory name is historical.
|
||||
description: Generate or inspect project image assets through this repository's VectorEngine gpt-image-2 workflow with gpt-image-2-c fallback. Use when Codex needs to create puzzle template sample images, reproduce the server-rs image request body, dry-run image prompts, batch-generate local project thumbnails, or debug VECTOR_ENGINE_BASE_URL / VECTOR_ENGINE_API_KEY image-generation configuration without exposing secrets. The directory name is historical.
|
||||
---
|
||||
|
||||
# GPT Image 2.5 project image workflow
|
||||
# gpt-image-2 VectorEngine
|
||||
|
||||
Use this skill for project-local image asset generation that must match the repository's image request contract. Use the business model identifier `gpt-image-2.5`; provider concrete model routing is owned by `server-rs`, and this client must not perform a cross-model fallback. The folder still contains `apimart` in its name for compatibility with existing local plugin references.
|
||||
Use this skill for project-local image asset generation that must match the repository's `server-rs` VectorEngine image path. Keep the product/price model identifier and primary provider request as `gpt-image-2`, then fall back once to `gpt-image-2-c` for eligible provider failures. The folder still contains `apimart` in its name for compatibility with existing local plugin references.
|
||||
|
||||
## Workflow
|
||||
|
||||
@@ -40,7 +40,7 @@ Default body:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gpt-image-2.5",
|
||||
"model": "gpt-image-2",
|
||||
"prompt": "<prompt>",
|
||||
"n": 1,
|
||||
"size": "1024x1024"
|
||||
@@ -58,14 +58,14 @@ Content-Type: multipart/form-data
|
||||
Multipart fields:
|
||||
|
||||
```text
|
||||
model=gpt-image-2.5
|
||||
model=gpt-image-2
|
||||
prompt=<prompt>
|
||||
n=1
|
||||
size=1024x1024
|
||||
image=@reference.png
|
||||
```
|
||||
|
||||
In this repository, calls with no reference images use `POST /v1/images/generations`; calls with any reference image use `POST /v1/images/edits` and pass references as one or more `image` form parts. Both paths send the business model `gpt-image-2.5`; provider routing and retry policy remain server-owned. Match3D container UI generation embeds `public/match3d-background-references/pot-fused-reference.png` into the edit request as an `image` part.
|
||||
In this repository, calls with no reference images use `POST /v1/images/generations`; calls with any reference image use `POST /v1/images/edits` and pass references as one or more `image` form parts. Both paths prefer `gpt-image-2`; on an eligible upstream/model failure they retry with `gpt-image-2-c`. Do not fall back for authentication, local validation, request-budget exhaustion, uncertain send/connection failure, content-safety rejection, or a generated image URL download failure. Match3D container UI generation embeds `public/match3d-background-references/pot-fused-reference.png` into the edit request as an `image` part.
|
||||
|
||||
Accept image output from `data[].url`, `data[].b64_json`, or direct nested `url` fields. VectorEngine image generation currently returns synchronously; do not poll APIMart task endpoints.
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@ const skillRoot = path.resolve(__dirname, '..');
|
||||
const repoRoot = path.resolve(skillRoot, '..', '..', '..');
|
||||
const defaultOutDir = path.join(repoRoot, 'public', 'anthro-cat-illustrations');
|
||||
const defaultTimeoutMs = 1000000;
|
||||
const preferredImageModel = 'gpt-image-2.5';
|
||||
const preferredImageModel = 'gpt-image-2';
|
||||
const fallbackImageModel = 'gpt-image-2-c';
|
||||
|
||||
const prompts = [
|
||||
{
|
||||
@@ -255,8 +256,41 @@ async function fetchJson(url, options, timeoutMs) {
|
||||
}
|
||||
}
|
||||
|
||||
function shouldFallbackImageModel(error) {
|
||||
const raw =
|
||||
`${error?.message || ''}\n${error?.vectorEngineBody || ''}`.toLowerCase();
|
||||
if (error?.vectorEngineResponseParse) {
|
||||
return !containsContentRejection(raw);
|
||||
}
|
||||
const status = Number(error?.vectorEngineStatus || 0);
|
||||
if (status === 408 || status >= 500) {
|
||||
return true;
|
||||
}
|
||||
if (status === 429) {
|
||||
return !containsContentRejection(raw);
|
||||
}
|
||||
const mentionsImageModel =
|
||||
raw.includes('model') ||
|
||||
raw.includes('模型') ||
|
||||
raw.includes(preferredImageModel) ||
|
||||
raw.includes(fallbackImageModel);
|
||||
return (
|
||||
[400, 404, 422].includes(status) &&
|
||||
mentionsImageModel &&
|
||||
/(not found|not supported|unsupported|unavailable|does not exist|invalid model|unknown model|不存在|不支持|不可用|未开通)/u.test(
|
||||
raw,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function containsContentRejection(raw) {
|
||||
return /(invalid_prompt|safety|content[_ ]policy|moderation|prompt rejected|content rejected|prompt refusal|content refusal|rejected by safety|rejected by moderation|敏感|违规|安全策略|内容审核|提示词拒绝|内容拒绝)/u.test(
|
||||
raw,
|
||||
);
|
||||
}
|
||||
|
||||
async function requestImagePayload(env, entry) {
|
||||
for (const model of [preferredImageModel]) {
|
||||
for (const model of [preferredImageModel, fallbackImageModel]) {
|
||||
const requestBody = {
|
||||
model,
|
||||
prompt: buildPrompt(entry),
|
||||
@@ -288,7 +322,12 @@ async function requestImagePayload(env, entry) {
|
||||
error.vectorEngineBody = JSON.stringify(payload).slice(0, 600);
|
||||
throw error;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
if (model !== preferredImageModel || !shouldFallbackImageModel(error)) {
|
||||
throw error;
|
||||
}
|
||||
console.warn(
|
||||
`VectorEngine ${preferredImageModel} failed, retrying with ${fallbackImageModel}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
throw new Error(`VectorEngine returned no image for ${entry.id}`);
|
||||
@@ -369,7 +408,7 @@ if (dryRun) {
|
||||
requests: selectedPrompts.map((entry) => ({
|
||||
id: entry.id,
|
||||
title: entry.title,
|
||||
fallbackModel: null,
|
||||
fallbackModel: fallbackImageModel,
|
||||
body: {
|
||||
model: preferredImageModel,
|
||||
prompt: buildPrompt(entry),
|
||||
|
||||
@@ -18,7 +18,8 @@ const defaultOutDir = path.join(
|
||||
'puzzle-creation-templates',
|
||||
);
|
||||
const defaultTimeoutMs = 1000000;
|
||||
const preferredImageModel = 'gpt-image-2.5';
|
||||
const preferredImageModel = 'gpt-image-2';
|
||||
const fallbackImageModel = 'gpt-image-2-c';
|
||||
|
||||
const args = new Map();
|
||||
for (let index = 2; index < process.argv.length; index += 1) {
|
||||
@@ -225,8 +226,41 @@ async function fetchJson(url, options, timeoutMs) {
|
||||
}
|
||||
}
|
||||
|
||||
function shouldFallbackImageModel(error) {
|
||||
const raw =
|
||||
`${error?.message || ''}\n${error?.vectorEngineBody || ''}`.toLowerCase();
|
||||
if (error?.vectorEngineResponseParse) {
|
||||
return !containsContentRejection(raw);
|
||||
}
|
||||
const status = Number(error?.vectorEngineStatus || 0);
|
||||
if (status === 408 || status >= 500) {
|
||||
return true;
|
||||
}
|
||||
if (status === 429) {
|
||||
return !containsContentRejection(raw);
|
||||
}
|
||||
const mentionsImageModel =
|
||||
raw.includes('model') ||
|
||||
raw.includes('模型') ||
|
||||
raw.includes(preferredImageModel) ||
|
||||
raw.includes(fallbackImageModel);
|
||||
return (
|
||||
[400, 404, 422].includes(status) &&
|
||||
mentionsImageModel &&
|
||||
/(not found|not supported|unsupported|unavailable|does not exist|invalid model|unknown model|不存在|不支持|不可用|未开通)/u.test(
|
||||
raw,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function containsContentRejection(raw) {
|
||||
return /(invalid_prompt|safety|content[_ ]policy|moderation|prompt rejected|content rejected|prompt refusal|content refusal|rejected by safety|rejected by moderation|敏感|违规|安全策略|内容审核|提示词拒绝|内容拒绝)/u.test(
|
||||
raw,
|
||||
);
|
||||
}
|
||||
|
||||
async function requestImagePayload(env, template) {
|
||||
for (const model of [preferredImageModel]) {
|
||||
for (const model of [preferredImageModel, fallbackImageModel]) {
|
||||
const requestBody = {
|
||||
model,
|
||||
prompt: buildPrompt(template),
|
||||
@@ -260,7 +294,12 @@ async function requestImagePayload(env, template) {
|
||||
error.vectorEngineBody = JSON.stringify(payload).slice(0, 600);
|
||||
throw error;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
if (model !== preferredImageModel || !shouldFallbackImageModel(error)) {
|
||||
throw error;
|
||||
}
|
||||
console.warn(
|
||||
`VectorEngine ${preferredImageModel} failed, retrying with ${fallbackImageModel}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
throw new Error(`VectorEngine returned no image for ${template.id}`);
|
||||
@@ -345,7 +384,7 @@ if (dryRun) {
|
||||
requests: selectedTemplates.map((template) => ({
|
||||
id: template.id,
|
||||
title: template.title,
|
||||
fallbackModel: null,
|
||||
fallbackModel: fallbackImageModel,
|
||||
body: {
|
||||
model: preferredImageModel,
|
||||
prompt: buildPrompt(template),
|
||||
|
||||
@@ -23,6 +23,3 @@
|
||||
*.meta text
|
||||
*.anim text
|
||||
*.controller text
|
||||
|
||||
# Rust ts-rs 生成的共享契约:保留在仓库中供 TS 消费,但不作为手写源文件统计。
|
||||
packages/shared/src/contracts/generated/** linguist-generated=true
|
||||
|
||||
@@ -221,7 +221,8 @@ jobs:
|
||||
- name: Run AI game creator shell agent-run smoke
|
||||
run: npm run check:native-shells:agc-rust-smoke
|
||||
|
||||
# AGC 壳依赖的共享 / 平台和编辑器插件 crate 各自预热独立 manifest,再运行对应测试。
|
||||
# AGC 壳依赖的共享 / 平台 crate 测试用的是 server-rs workspace 与两个无锁独立 crate
|
||||
# 的 manifest,属另一套依赖图,因此单独一个 job 预热、单独跑。
|
||||
ai-game-creator-shell-rust-crates:
|
||||
name: AI game creator shell Rust crates
|
||||
runs-on: genarrative-ci
|
||||
@@ -262,15 +263,13 @@ jobs:
|
||||
# `cargo test --manifest-path` 单独跑这两个 crate。不在这里预热的话,这两条测试
|
||||
# 会在测试阶段自己 `Updating crates.io index`,crates.io 一抖动整条 job 就红
|
||||
# (见 #327 / PR #316 run 1950)。
|
||||
# Cocos 插件也使用独立且未提交的锁文件,一并预热。
|
||||
# 这些 crate 都没有提交 Cargo.lock,所以这里只能做不带锁标志的 fetch:
|
||||
# 两个 crate 都没有提交 Cargo.lock,所以这里只能做不带锁标志的 fetch:
|
||||
# 加锁标志会因为缺少锁文件直接失败。生成的 Cargo.lock 落在两个 crate 目录内,
|
||||
# 已被各自的 .gitignore 忽略,只留在容器里;随后的测试阶段因此能用锁定版本
|
||||
# 解析,不再触碰 registry index。
|
||||
for manifest_path in \
|
||||
server-rs/crates/agent-runtime-core/Cargo.toml \
|
||||
server-rs/crates/agent-runtime-orchestration/Cargo.toml \
|
||||
plugins/agc-cocos-editor/native/cocos-editor-bridge/Cargo.toml; do
|
||||
server-rs/crates/agent-runtime-orchestration/Cargo.toml; do
|
||||
for attempt in $(seq 1 5); do
|
||||
if cargo fetch \
|
||||
--target x86_64-unknown-linux-gnu \
|
||||
@@ -285,23 +284,6 @@ jobs:
|
||||
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
|
||||
run: npm run check:native-shells:agc-rust-crates
|
||||
|
||||
|
||||
@@ -48,10 +48,6 @@ temp*build*/
|
||||
/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-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/.llm-drafts/
|
||||
/apps/ai-game-creator-shell/game-creator.config.local.json
|
||||
|
||||
+1
-10
@@ -1,14 +1,5 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"semi": true,
|
||||
"trailingComma": "all",
|
||||
"overrides": [
|
||||
{
|
||||
"files": "packages/shared/src/contracts/generated/**/*.ts",
|
||||
"options": {
|
||||
"printWidth": 1000,
|
||||
"singleQuote": false
|
||||
}
|
||||
}
|
||||
]
|
||||
"trailingComma": "all"
|
||||
}
|
||||
|
||||
-29
@@ -44,35 +44,6 @@ _Avoid_: 把同一资源的全局元数据和某一次摆放坐标混在同一
|
||||
由图片生成或图片修改流程产生的画布资源,必须记录来源资源、提示词、实际提示词、模型、provider、任务 ID 和生成时间;本期 `/editor` 的生成修改先允许 mock 生成资源,但仍按生成资源元数据形状保存。
|
||||
_Avoid_: 无来源的静态素材、只显示在 UI 但不落工程资源记录的生成结果
|
||||
|
||||
**图片模型历史值与使用端解析**:
|
||||
图片资源中已持久化的 `gpt-image-2` 是历史业务事实,读回时保持原值;新任务使用业务模型值 `gpt-image-2.5`。当用户基于历史资源再次发起生成或编辑任务时,服务端只在新任务的使用端把历史值解析为当前业务模型,不改写历史资源。provider route 属于服务端执行与审计边界,前端不接收、不持久化、不展示,也不据此分支。
|
||||
_Avoid_: 读取数据库时改写历史模型值、把 provider route 暴露为前端模型选项或公开 DTO
|
||||
|
||||
**图片 provider 显式路由**:
|
||||
api-server 在任务入口按业务语义显式选择具体 provider model name(生成或编辑),并把同一具体名传给图片平台适配器和后台定价解析;图片平台适配器不从参考图数量或前端字段猜测任务。具体 provider model name 只存在于服务端调用、定价配置和审计边界。
|
||||
后台管理 Web/API 是明确例外,可以查看和编辑两个具体定价 key;主站普通前端与公开定价 API 不接收这些 key。
|
||||
_Avoid_: 让图片适配器隐式猜路由、让主站前端携带 provider model name
|
||||
|
||||
**业务模型**:
|
||||
面向任务与产品契约的稳定模型值;当前 GPT 图片新任务的业务模型是 `gpt-image-2.5`。业务模型不等同于 provider 的具体计费/请求 model,也不暴露 provider 凭证或 endpoint。
|
||||
_Avoid_: 把 provider concrete model 当作前端业务选项、用业务模型值直接推断 provider 凭证
|
||||
|
||||
**具体模型**:
|
||||
服务端发送请求和定价使用的 concrete model name。GPT Image 2.5 生成与编辑分别是 `gpt-image-2.5-flare-c` 和 `gpt-image-2.5-sunburst-c`;nanobanana 仍使用 `gemini-3.1-flash-image-preview`。具体模型只在服务端执行、定价和审计边界出现。
|
||||
_Avoid_: 把具体模型写入普通前端 DTO、让未知字符串自动选择 provider
|
||||
|
||||
**provider client**:
|
||||
按具体模型选出的外部图片 provider 连接配置,包含 provider identity、base URL 和 API key;VectorEngine 与 Tiantoken client 共享图片协议执行器,不复制请求/响应业务逻辑。两套 required client 在 api-server 启动时构造。
|
||||
_Avoid_: 在首次请求时才创建 client、在 provider client 中复制尺寸/重试/审计逻辑、跨 provider credential fallback
|
||||
|
||||
**历史模型值**:
|
||||
已持久化的 `gpt-image-2` 或 `gpt-image-2-c` 字符串,只作为历史事实原样读取和审计;基于历史资源提交新任务时,在使用端解析为当前 GPT Image 2.5 业务任务,不回写历史记录,也不把旧值作为现役 provider route。
|
||||
_Avoid_: 数据库批量改写历史值、把历史值重新路由到 VectorEngine、把兼容解析扩散到普通前端
|
||||
|
||||
**GPT Image 2.5 新生成展示名**:
|
||||
`GPT Image 2.5` 是新生成任务的产品展示名;历史资源与既有编辑上下文不因新模型上线而改写展示语义。
|
||||
_Avoid_: 把新生成展示名扩散到历史记录、历史生成器或旧编辑上下文
|
||||
|
||||
**系列素材图集生成**:
|
||||
一组同类素材的统一批量生成方式,采用批量规划、sheet 生图、后端切图、透明化、OSS 持久化和局部重生成的通用流水线。
|
||||
_Avoid_: 为每个玩法单独发明素材流水线、把系列素材建模成任一玩法专属 DTO
|
||||
|
||||
@@ -676,11 +676,11 @@ async function runSelfTest() {
|
||||
designFoundationAssetCall?.arguments?.input?.outputPath ===
|
||||
'assets/ui-prototype.png' &&
|
||||
designFoundationAssetCall.arguments.input.aspectRatio === '16:9' &&
|
||||
designFoundationAssetCall.arguments.input.assetKind === 'ui-design' &&
|
||||
designFoundationAssetCall.arguments.input.assetKind === 'ui-prototype' &&
|
||||
artAssetPlanAssetCall?.arguments?.input?.outputPath ===
|
||||
'assets/art-spritesheet.png' &&
|
||||
artAssetPlanAssetCall.arguments.input.aspectRatio === '1:1' &&
|
||||
artAssetPlanAssetCall.arguments.input.assetKind === 'icon-spritesheet',
|
||||
artAssetPlanAssetCall.arguments.input.assetKind === 'art-spritesheet',
|
||||
'self-test-visual-assets-invalid',
|
||||
);
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ const requiredFormalArtifactSpecs = [
|
||||
{ path: 'game/balance.json', kind: 'json' },
|
||||
{ path: 'assets/manifest.art.json', kind: 'json' },
|
||||
{ path: 'assets/manifest.audio.json', kind: 'json' },
|
||||
{ path: 'game/index.html', kind: 'file' },
|
||||
{ path: 'game/index.html', kind: 'game-entry' },
|
||||
{ path: 'exports/README.md', kind: 'file' },
|
||||
];
|
||||
const editorImageArtifactSpecs = [
|
||||
|
||||
@@ -105,7 +105,6 @@ export const agcReleasePathPatterns = [
|
||||
'packages/',
|
||||
'server-rs/crates/',
|
||||
'plugins/agc-cocos-editor/',
|
||||
'plugins/agc-unity-editor/',
|
||||
'apps/desktop-shell/src-tauri/icons/',
|
||||
'package.json',
|
||||
'package-lock.json',
|
||||
|
||||
@@ -344,11 +344,7 @@ test('Windows remains the default and explicit Windows overrides macOS environme
|
||||
context,
|
||||
{
|
||||
spawn: (_binary, command) => {
|
||||
assert.ok(
|
||||
command.includes(
|
||||
'--features=cocos-editor-execute,unity-editor-execute',
|
||||
),
|
||||
);
|
||||
assert.ok(command.includes('--features=cocos-editor-execute'));
|
||||
assert.ok(command.includes('user-config.json'));
|
||||
const configIndex = command.lastIndexOf('--config');
|
||||
const config = JSON.parse(
|
||||
|
||||
@@ -19,6 +19,6 @@ export function withDefaultCargoFeatures(argv, features) {
|
||||
|
||||
export function defaultEditorFeatures(target) {
|
||||
return target === 'win32' || target.includes('windows')
|
||||
? ['cocos-editor-execute', 'unity-editor-execute']
|
||||
? ['cocos-editor-execute']
|
||||
: [];
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ test('Windows release includes the same editor feature as development', () => {
|
||||
buildTauriBuildArguments([], 'x86_64-pc-windows-msvc', 'win32'),
|
||||
[
|
||||
'build',
|
||||
'--features=cocos-editor-execute,unity-editor-execute',
|
||||
'--features=cocos-editor-execute',
|
||||
'--target',
|
||||
'x86_64-pc-windows-msvc',
|
||||
],
|
||||
|
||||
@@ -143,6 +143,7 @@ const allowedUncalledTauriCommands = [
|
||||
'reload_agc_plugin',
|
||||
'call_agc_plugin',
|
||||
'read_agc_plugin_panel',
|
||||
'set_agc_plugin_project_path',
|
||||
'set_agc_plugin_enabled',
|
||||
];
|
||||
const sourceExtensions = new Set([
|
||||
|
||||
@@ -290,7 +290,7 @@ function deterministicArtManifest(
|
||||
{
|
||||
id: 'garden-guardians-spritesheet',
|
||||
path: 'assets/art-spritesheet.png',
|
||||
kind: 'icon-spritesheet',
|
||||
kind: 'art-spritesheet',
|
||||
usage: ['defenders', 'enemies', 'battlefield-ui'],
|
||||
source: 'canvas',
|
||||
status: 'ready',
|
||||
@@ -710,7 +710,7 @@ function canvasAssetCall(agentId) {
|
||||
outputPath: 'assets/ui-prototype.png',
|
||||
aspectRatio: '16:9',
|
||||
imageSize: '2K',
|
||||
assetKind: 'ui-design',
|
||||
assetKind: 'ui-prototype',
|
||||
assetLabel: '游戏横屏界面原型图',
|
||||
replaceExisting: false,
|
||||
});
|
||||
@@ -721,7 +721,7 @@ function canvasAssetCall(agentId) {
|
||||
outputPath: 'assets/art-spritesheet.png',
|
||||
aspectRatio: '1:1',
|
||||
imageSize: '1K',
|
||||
assetKind: 'icon-spritesheet',
|
||||
assetKind: 'art-spritesheet',
|
||||
assetLabel: '游戏首版核心美术素材',
|
||||
replaceExisting: false,
|
||||
sliceMode: 'connected-components',
|
||||
@@ -830,12 +830,12 @@ function missingGeneratedVisualAssetObservation(context, agentId) {
|
||||
},
|
||||
'design-foundation': {
|
||||
path: 'assets/ui-prototype.png',
|
||||
kind: 'ui-design',
|
||||
kind: 'ui-prototype',
|
||||
summary: '策划界面原型图尚未按正式视觉流程生成并登记,不能完成任务',
|
||||
},
|
||||
'art-asset-plan': {
|
||||
path: 'assets/art-spritesheet.png',
|
||||
kind: 'icon-spritesheet',
|
||||
kind: 'art-spritesheet',
|
||||
summary: '首版美术素材图尚未按正式视觉流程生成并登记,不能完成任务',
|
||||
},
|
||||
}[agentId];
|
||||
@@ -2667,7 +2667,7 @@ function createDeterministicCanvasFixture(apiKey) {
|
||||
asset: {
|
||||
assetId: `asset-${sliceImageId}`,
|
||||
assetObjectId: sliceAssetObjectId,
|
||||
assetKind: 'icon',
|
||||
assetKind: 'art-spritesheet-slice',
|
||||
projectId,
|
||||
taskId,
|
||||
},
|
||||
@@ -2707,7 +2707,7 @@ function createDeterministicCanvasFixture(apiKey) {
|
||||
spritesheetAsset: {
|
||||
assetId: `asset-${imageId}`,
|
||||
assetObjectId,
|
||||
assetKind: 'icon-spritesheet',
|
||||
assetKind: 'art-spritesheet',
|
||||
projectId,
|
||||
taskId,
|
||||
},
|
||||
@@ -2749,7 +2749,7 @@ function createDeterministicCanvasFixture(apiKey) {
|
||||
const assetObjectId = `asset-object-${imageId}`;
|
||||
const resourceId = `resource-${imageId}`;
|
||||
const assetKind =
|
||||
typeof body?.assetKind === 'string' ? body.assetKind : 'image';
|
||||
typeof body?.assetKind === 'string' ? body.assetKind : 'game-art';
|
||||
images.set(imageId, {
|
||||
...image,
|
||||
objectKey,
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import {
|
||||
defaultEditorFeatures,
|
||||
withDefaultCargoFeatures,
|
||||
} from './cargo-features.mjs';
|
||||
import { withDefaultCargoFeatures } from './cargo-features.mjs';
|
||||
import {
|
||||
readAgcDevEndpoint,
|
||||
resolveAgcDevEndpoint,
|
||||
@@ -50,8 +47,9 @@ function buildTauriArguments(argv, devUrl = readAgcDevEndpoint().url) {
|
||||
];
|
||||
}
|
||||
|
||||
// 开发和发行构建使用同一平台编辑器 feature 集合。
|
||||
// 可用 AGC_DEV_CARGO_FEATURES(逗号分隔)覆盖,传空串即关闭。
|
||||
// `agc_cocos_execute` 与 Cocos 编辑器适配器只在 `cocos-editor-execute` feature 下
|
||||
// 注册。开发构建默认在 Windows 打开它,否则 Agent 的工具清单里根本没有该工具,
|
||||
// 只能退化成改写脚本。可用 AGC_DEV_CARGO_FEATURES(逗号分隔)覆盖,传空串即关闭。
|
||||
function readDevCargoFeatures(env = process.env) {
|
||||
const override = env.AGC_DEV_CARGO_FEATURES;
|
||||
if (override !== undefined) {
|
||||
@@ -60,7 +58,7 @@ function readDevCargoFeatures(env = process.env) {
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
return defaultEditorFeatures(process.platform);
|
||||
return process.platform === 'win32' ? ['cocos-editor-execute'] : [];
|
||||
}
|
||||
|
||||
function withDevCargoFeatures(argv, features = readDevCargoFeatures()) {
|
||||
|
||||
-13
@@ -1793,7 +1793,6 @@ dependencies = [
|
||||
"ttf-parser",
|
||||
"typed_floats",
|
||||
"unicode-normalization",
|
||||
"unity-editor-bridge",
|
||||
"url",
|
||||
"uuid",
|
||||
"windows-sys 0.61.2",
|
||||
@@ -3932,7 +3931,6 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5018,7 +5016,6 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"ts-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6410,16 +6407,6 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||
|
||||
[[package]]
|
||||
name = "unity-editor-bridge"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"editor-adapter-api",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unsafe-libyaml"
|
||||
version = "0.2.11"
|
||||
|
||||
@@ -12,7 +12,6 @@ template-library-fixtures = []
|
||||
cocos-editor = ["cocos-editor-bridge/process-discovery"]
|
||||
cocos-editor-execute = ["cocos-editor", "cocos-editor-bridge/windows-bootstrap"]
|
||||
cocos-editor-injection = ["cocos-editor-execute", "cocos-editor-bridge/windows-injection"]
|
||||
unity-editor-execute = []
|
||||
|
||||
[build-dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
@@ -28,7 +27,6 @@ nalgebra = { version = "0.35.0", features = ["serde-serialize"] }
|
||||
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 }
|
||||
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"
|
||||
axum = "0.8"
|
||||
chromiumoxide = "0.9.1"
|
||||
@@ -58,7 +56,7 @@ platform-agent = { path = "../../../server-rs/crates/platform-agent" }
|
||||
portable-pty = "0.9"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "multipart", "native-tls", "stream"] }
|
||||
regex = "1"
|
||||
shared-contracts = { path = "../../../server-rs/crates/shared-contracts", default-features = false, features = ["ts-bindings"] }
|
||||
shared-contracts = { path = "../../../server-rs/crates/shared-contracts", default-features = false }
|
||||
tauri = { version = "2.11.2", features = [] }
|
||||
tauri-plugin-dialog = "2.7.1"
|
||||
tauri-plugin-http = { version = "2.5.9", default-features = false, features = ["charset", "cookies", "http2", "rustls-tls"] }
|
||||
|
||||
@@ -196,7 +196,6 @@ fn main() {
|
||||
);
|
||||
let manifest_path = manifest_dir.join("prompts/runtime/manifest.json");
|
||||
stage_bundled_codex_cli(&manifest_dir);
|
||||
prepare_unity_editor_helper(&manifest_dir);
|
||||
stage_plugin_workspace(&manifest_dir);
|
||||
stage_cocos_editor_payload(&manifest_dir);
|
||||
let compiled = runtime_prompt_bundle::compile_manifest(&manifest_path)
|
||||
@@ -267,107 +266,6 @@ fn stage_cocos_editor_payload(manifest_dir: &std::path::Path) {
|
||||
#[cfg(not(windows))]
|
||||
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/` 工作区里的插件包随包映射到应用资源目录。
|
||||
///
|
||||
/// 只复制插件运行需要的清单、入口、面板和 native payload,不复制 native 源码、
|
||||
@@ -415,15 +313,9 @@ fn stage_plugin_workspace(manifest_dir: &std::path::Path) {
|
||||
for relative in [
|
||||
std::path::PathBuf::from("src"),
|
||||
std::path::PathBuf::from("panels"),
|
||||
std::path::PathBuf::from("skills"),
|
||||
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()))
|
||||
{
|
||||
if relative == std::path::Path::new("native/payload") && !target.contains("windows") {
|
||||
continue;
|
||||
}
|
||||
copy_plugin_tree(&plugin_root.join(&relative), &destination.join(&relative));
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
"agents/openai.yaml",
|
||||
"references/platform-art-contract.md"
|
||||
],
|
||||
"sha256": "6668bf1aa69601bcc65c97fdcd879c699c3139befcd70805d95614997f6e44e9"
|
||||
"sha256": "47ac742d9b88e5d6cd9833484ab212152578e58ae27f7add312fd1d78183385c"
|
||||
},
|
||||
{
|
||||
"name": "agc-web-game-development",
|
||||
|
||||
+2
-2
@@ -19,13 +19,13 @@ 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
|
||||
the complete game-art package and its canonical slices.
|
||||
|
||||
With `agc_generate_image`, `kind="character"` and `kind="icon-spritesheet"`
|
||||
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="icon-spritesheet"`, `sliceMode` is
|
||||
When `agc_generate_image` is used with `kind="art-spritesheet"`, `sliceMode` is
|
||||
required and has no default, so decide it explicitly:
|
||||
|
||||
- Use `sliceMode="grid"` with `gridX` and `gridY` (1-32 each) only when the user
|
||||
|
||||
@@ -1198,7 +1198,7 @@ mod tests {
|
||||
"tool": "agc_generate_image",
|
||||
"arguments": {
|
||||
"prompt": prompt,
|
||||
"kind": "icon-spritesheet",
|
||||
"kind": "art-spritesheet",
|
||||
"sliceMode": "connected-components",
|
||||
"sliceCount": 8,
|
||||
"screenColor": "#CFEFFF"
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
use super::model::{DirectCodexUserContentPart, DirectCodexUserItem, DirectCodexUserMessageItem};
|
||||
use super::model::{DirectCodexUserContentPart, DirectCodexUserItem};
|
||||
use super::validation::validate_direct_codex_user_item;
|
||||
use crate::agent::{read_manifest_for_project, sanitize_attachment_local_path};
|
||||
use crate::ui_editor::persistence::{
|
||||
generate_ui_design_code_at, GenerateUiDesignCodeInput, UI_DESIGN_DOC_ASSET_KIND,
|
||||
UI_DESIGN_DOC_MEDIA_TYPE,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use std::path::Path;
|
||||
|
||||
@@ -115,165 +111,29 @@ pub(crate) fn direct_codex_user_item_to_prompt(
|
||||
item: &DirectCodexUserItem,
|
||||
) -> Result<String, String> {
|
||||
let wire = direct_codex_user_item_to_wire_input(root, item)?;
|
||||
let DirectCodexUserItem::Message(message) = item;
|
||||
let mut prompt = wire
|
||||
.as_array()
|
||||
wire.as_array()
|
||||
.ok_or_else(|| "DirectProject user item wire input 不是数组".to_string())
|
||||
.and_then(|parts| {
|
||||
.map(|parts| {
|
||||
parts
|
||||
.iter()
|
||||
.map(|part| {
|
||||
part.get("text")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| "DirectProject user item wire part 缺少 text".to_string())
|
||||
})
|
||||
.collect::<Result<String, String>>()
|
||||
})?;
|
||||
if let Some(code_context) = render_ui_design_code_context(root, message)? {
|
||||
prompt.push('\n');
|
||||
prompt.push_str(&code_context);
|
||||
}
|
||||
if prompt.trim().is_empty() {
|
||||
return Err("DirectProject user item 不能转换为空 prompt".to_string());
|
||||
}
|
||||
Ok(prompt)
|
||||
}
|
||||
|
||||
/// 本轮 prompt 的 UI 设计文档引用上下文:复用 UI Editor 代码导出,把带文档注释的
|
||||
/// JS 片段路径交给模型;生成失败只追加原始错误,不阻断本轮引用,其它引用继续处理。
|
||||
///
|
||||
/// 只在生成本轮 prompt 时展开:历史 item 回读走 `direct_codex_user_item_to_response_item`
|
||||
/// 的纯投影,不得在这里产生项目写副作用。
|
||||
fn render_ui_design_code_context(
|
||||
root: &Path,
|
||||
message: &DirectCodexUserMessageItem,
|
||||
) -> Result<Option<String>, String> {
|
||||
let referenced_ids = message
|
||||
.content
|
||||
.iter()
|
||||
.filter_map(|part| match part {
|
||||
DirectCodexUserContentPart::AgcResourceReference { resource_id } => {
|
||||
Some(resource_id.trim())
|
||||
}
|
||||
_ => None,
|
||||
.filter_map(|part| part.get("text").and_then(Value::as_str))
|
||||
.collect::<String>()
|
||||
})
|
||||
.and_then(|prompt| {
|
||||
if prompt.trim().is_empty() {
|
||||
Err("DirectProject user item 不能转换为空 prompt".to_string())
|
||||
} else {
|
||||
Ok(prompt)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if referenced_ids.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let manifest = read_manifest_for_project(root)?;
|
||||
let mut lines = Vec::new();
|
||||
for resource_id in referenced_ids {
|
||||
let is_ui_design_doc = manifest.assets.iter().any(|asset| {
|
||||
asset.id == resource_id
|
||||
&& asset.kind == UI_DESIGN_DOC_ASSET_KIND
|
||||
&& asset.media_type == UI_DESIGN_DOC_MEDIA_TYPE
|
||||
});
|
||||
if !is_ui_design_doc {
|
||||
continue;
|
||||
}
|
||||
lines.push(
|
||||
match generate_ui_design_code_at(GenerateUiDesignCodeInput {
|
||||
project_path: root.to_string_lossy().into_owned(),
|
||||
expected_project_id: manifest.project_id.clone(),
|
||||
asset_id: resource_id.to_string(),
|
||||
}) {
|
||||
Ok(result) => format!("请先阅读生成的带有文档的代码片段: {}", result.relative_path),
|
||||
Err(error) => format!("生成代码遇到错误{error}"),
|
||||
},
|
||||
);
|
||||
}
|
||||
if lines.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(lines.join("\n")))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{direct_codex_user_item_to_prompt, direct_codex_user_item_to_response_item};
|
||||
use crate::ui_editor::persistence::UI_DESIGN_DOC_MEDIA_TYPE;
|
||||
use super::direct_codex_user_item_to_response_item;
|
||||
use serde_json::json;
|
||||
use shared_contracts::game_creation_app::{
|
||||
GameCreationAppAssetKind, GameCreationAppAssetSource, GameCreationAppAssetSourceKind,
|
||||
};
|
||||
use std::path::Path;
|
||||
|
||||
const PROMPT_CONTEXT_PROJECT_ID: &str = "direct-prompt-context-project";
|
||||
|
||||
fn prompt_context_project() -> tempfile::TempDir {
|
||||
let directory = tempfile::tempdir().expect("temp project");
|
||||
crate::init_local_game_project_at(
|
||||
directory.path(),
|
||||
PROMPT_CONTEXT_PROJECT_ID,
|
||||
"Direct prompt 上下文测试",
|
||||
)
|
||||
.expect("init project");
|
||||
directory
|
||||
}
|
||||
|
||||
fn register_fixture_asset(
|
||||
root: &Path,
|
||||
relative_path: &str,
|
||||
kind: GameCreationAppAssetKind,
|
||||
media_type: &str,
|
||||
) -> String {
|
||||
let absolute_path = crate::resolve_local_project_path(root, relative_path)
|
||||
.expect("resolve fixture asset path");
|
||||
std::fs::create_dir_all(absolute_path.parent().expect("fixture asset parent"))
|
||||
.expect("create fixture asset parent");
|
||||
std::fs::write(&absolute_path, b"{}").expect("write fixture asset file");
|
||||
crate::assets::register_local_asset_at(
|
||||
root,
|
||||
relative_path,
|
||||
kind,
|
||||
media_type,
|
||||
"prompt-context-test",
|
||||
GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Generated,
|
||||
canvas_project_id: None,
|
||||
resource_id: Some("prompt-context".to_string()),
|
||||
asset_object_id: None,
|
||||
task_id: None,
|
||||
prompt: None,
|
||||
model: None,
|
||||
generation_route: None,
|
||||
generation_kind: None,
|
||||
reference_resource_ids: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("register fixture asset")
|
||||
.id
|
||||
}
|
||||
|
||||
fn ui_design_doc_fixture(initialize_state: bool) -> (tempfile::TempDir, String) {
|
||||
let directory = prompt_context_project();
|
||||
let asset_id = register_fixture_asset(
|
||||
directory.path(),
|
||||
"ui/design.json",
|
||||
GameCreationAppAssetKind::UiDesignDoc,
|
||||
UI_DESIGN_DOC_MEDIA_TYPE,
|
||||
);
|
||||
if initialize_state {
|
||||
crate::ui_editor::persistence::initialize_ui_design_state_at(
|
||||
directory.path(),
|
||||
PROMPT_CONTEXT_PROJECT_ID,
|
||||
&asset_id,
|
||||
)
|
||||
.expect("initialize UI design state");
|
||||
}
|
||||
(directory, asset_id)
|
||||
}
|
||||
|
||||
fn user_item_with_resource_reference(asset_id: &str) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"id": "turn-1:user",
|
||||
"content": [{"type": "agc_resource_reference", "resourceId": asset_id}],
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standard_response_item_passes_through_without_agc_private_parts() {
|
||||
let item = json!({
|
||||
@@ -314,81 +174,4 @@ mod tests {
|
||||
.expect_err("history item without type must fail");
|
||||
assert!(error.contains("缺少 type"), "{error}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui_design_doc_reference_appends_generated_code_context() {
|
||||
let (project, asset_id) = ui_design_doc_fixture(true);
|
||||
let item: super::DirectCodexUserItem =
|
||||
serde_json::from_value(user_item_with_resource_reference(&asset_id))
|
||||
.expect("canonical user item");
|
||||
let prompt =
|
||||
direct_codex_user_item_to_prompt(project.path(), &item).expect("prompt projection");
|
||||
assert!(
|
||||
prompt.contains(&format!(
|
||||
"[素材引用 resourceId={asset_id};项目路径=ui/design.json]"
|
||||
)),
|
||||
"{prompt}"
|
||||
);
|
||||
assert!(
|
||||
prompt.contains("请先阅读生成的带有文档的代码片段: ui/generated-"),
|
||||
"{prompt}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_projection_never_writes_generated_ui_design_code() {
|
||||
let (project, asset_id) = ui_design_doc_fixture(true);
|
||||
let item = user_item_with_resource_reference(&asset_id);
|
||||
direct_codex_user_item_to_response_item(project.path(), &item).expect("history projection");
|
||||
let generated_root =
|
||||
crate::resolve_local_project_path(project.path(), "ui").expect("resolve ui directory");
|
||||
let generated_files = std::fs::read_dir(generated_root)
|
||||
.expect("read ui directory")
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter(|entry| {
|
||||
entry
|
||||
.file_name()
|
||||
.to_string_lossy()
|
||||
.starts_with("generated-")
|
||||
})
|
||||
.count();
|
||||
assert_eq!(
|
||||
generated_files, 0,
|
||||
"历史回读只做纯投影,不得生成 UI 设计代码"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui_design_generation_failure_keeps_reference_and_reports_error() {
|
||||
let (project, asset_id) = ui_design_doc_fixture(false);
|
||||
let item: super::DirectCodexUserItem =
|
||||
serde_json::from_value(user_item_with_resource_reference(&asset_id))
|
||||
.expect("canonical user item");
|
||||
let prompt =
|
||||
direct_codex_user_item_to_prompt(project.path(), &item).expect("prompt projection");
|
||||
assert!(prompt.contains("素材引用 resourceId="), "{prompt}");
|
||||
assert!(prompt.contains("生成代码遇到错误"), "{prompt}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn other_asset_kind_reference_does_not_generate_ui_design_code() {
|
||||
let project = prompt_context_project();
|
||||
let asset_id = register_fixture_asset(
|
||||
project.path(),
|
||||
"assets/hero.png",
|
||||
GameCreationAppAssetKind::Character,
|
||||
"image/png",
|
||||
);
|
||||
let item: super::DirectCodexUserItem =
|
||||
serde_json::from_value(user_item_with_resource_reference(&asset_id))
|
||||
.expect("canonical user item");
|
||||
let prompt =
|
||||
direct_codex_user_item_to_prompt(project.path(), &item).expect("prompt projection");
|
||||
assert!(prompt.contains("素材引用 resourceId="), "{prompt}");
|
||||
assert!(
|
||||
!prompt.contains("请先阅读生成的带有文档的代码片段"),
|
||||
"{prompt}"
|
||||
);
|
||||
assert!(!prompt.contains("生成代码遇到错误"), "{prompt}");
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user