实现AGC直连运行时与陶泥儿美术闭环

新增直连 Codex 项目聊天、会话恢复与可读错误反馈

接入陶泥儿规范图、背景、固定四切片并校验实际渲染

同步游戏代码资源分类、项目版本 revision 与外部编辑器契约
This commit is contained in:
kdletters
2026-08-17 09:57:31 +08:00
parent f69d9a2e11
commit 647cf82cec
50 changed files with 3419 additions and 731 deletions
@@ -51,7 +51,7 @@ Every generation row requires a stable `Idempotency-Key` header and returns HTTP
| --- | --- | --- | --- |
| Image generation | `/api/external/v1/editor/images/generations` | `prompt` | `kind`, `style`, `model`, `aspectRatio`, `imageSize`, `size`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
| Image edit/redraw | `/api/external/v1/editor/images/edits` | `prompt`, `sourceReferenceId` | `referenceImageSrcs`, `model`, `size`, `projectId`, `assetFolderId`, `assetLabel`, `targetLayerId`, `canvasCompletion` |
| Icon spritesheet | `/api/external/v1/editor/icon-spritesheets/generations` | `referenceId`, `iconDescriptions` | `style`, `referenceImageSrcs`, `screenColor`, `model`, `aspectRatio`, `imageSize`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
| Icon spritesheet | `/api/external/v1/editor/icon-spritesheets/generations` | `referenceId`, `iconDescriptions` | `sliceLayout`, `style`, `referenceImageSrcs`, `screenColor`, `model`, `aspectRatio`, `imageSize`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
| UI asset extraction | `/api/external/v1/editor/ui-designs/assets/extractions` | `sourceImageSrc`, `aspectRatio`, `imageSize` | `screenColor`, `model`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `spritesheetLabel`, `canvasCompletion` |
| Character animation | `/api/external/v1/editor/character-animations/generations` | `sourceLayerId`, `sourceImageSrc`, `sourceWidth`, `sourceHeight`, `promptText`, `resolution`, `ratio`, `frameCount`, `durationSeconds`, `model` | `projectId`, `sourceResourceId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
| Video generation | `/api/external/v1/editor/videos/generations` | `prompt`, `model`, `aspectRatio`, `durationSeconds`, `resolution`, `mode`, `sound` | `referenceImageSrcs`, `referenceVideoSrcs`, `referenceAudioSrcs`, `webSearchEnabled`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
@@ -92,6 +92,8 @@ For image edit/redraw, confirming an upload is not sufficient: create a project
The icon-spritesheet primary `referenceId` is intentionally stricter than ordinary image references: it accepts only a current-owner project resource ID or asset ID whose authoritative `assetKind` is `icon-spec`. It does not accept an `objectKey`, URL, Data URL, or Blob URL.
`sliceLayout: "grid-2x2"` is an opt-in contract for four fixed game-runtime assets. The provider prompt and server persistence both preserve the ordered slots left-top, right-top, left-bottom, right-bottom. Omit it to retain the default connected-component slicing behaviour for ordinary free-form icon sheets.
## Common Values
Use OpenAPI as the final authority; these common values are a routing aid:
@@ -78,9 +78,9 @@ Keep the existing autonomous-build task graph. Do not add a parallel task system
1. `art-director` generates `assets/art-spec.png` with image generation, `kind: "spec"`, then registers it as `assetKind: "icon-spec"`. This image is the authoritative visual spec; `generationInputs.artSpec` is supporting structured context.
2. `design-foundation` generates `assets/ui-prototype.png` with `kind: "ui-design"`, using the registered art-spec resource ID in `referenceImageSrcs`.
3. `art-asset-plan` generates transparent `assets/art-spritesheet.png` through icon spritesheet generation, using the same registered art-spec resource ID as `referenceId` plus concrete `iconDescriptions`.
3. `art-asset-plan` generates transparent `assets/art-spritesheet.png` through icon spritesheet generation, using the same registered art-spec resource ID as `referenceId` plus concrete `iconDescriptions`. For the four-category game contract it must also send `sliceLayout: "grid-2x2"`; this is an explicit fixed-slot contract, not a client-side guessed crop.
For a playable Canvas game, do not stop at generation. Make `code-prototype` depend on `art-asset-plan` and consume the persisted `iconImageSrcs` slices for core players, blocks or targets, scene obstacles, and feedback. For the four-category game-chat contract, require exactly four slices before registering the local runtime sheet; both fewer and extra components fail closed. Treat `art-spec.png` as reference-only. A full-sheet `<img>`, CSS background, path-only mention, guessed equal-grid crop, or code-drawn replacement for core entities is not runtime asset use. If slicing produces `sliceWarning`, keep the complete transparent sheet as a valid editor artifact, but fail the playable game asset gate until real slice files or verified atlas coordinates exist; never invent coordinates or replace the icon-spritesheet route with ordinary image generation.
For a playable Canvas game, do not stop at generation. Make `code-prototype` depend on `art-asset-plan` and consume the persisted `iconImageSrcs` slices for core players, blocks or targets, scene obstacles, and feedback. For the four-category game-chat contract, require response `sliceLayout: "grid-2x2"` and exactly four slices before registering the local runtime sheet; both fewer and extra components fail closed. Treat `art-spec.png` as reference-only. A full-sheet `<img>`, CSS background, path-only mention, guessed equal-grid crop, or code-drawn replacement for core entities is not runtime asset use. If slicing produces `sliceWarning`, keep the complete transparent sheet as a valid editor artifact, but fail the playable game asset gate until real slice files or verified atlas coordinates exist; never invent coordinates or replace the icon-spritesheet route with ordinary image generation.
Never use `assets/ui-prototype.png` as the spritesheet visual-spec reference. UI extraction is outside this canonical DAG.
@@ -2,10 +2,10 @@
"agentMode": "codex_app_server",
"llm": {
"apiKey": "",
"baseUrl": "https://api.openai.com/v1",
"model": "gpt-4.1",
"baseUrl": "https://dev.genarrative.world/gpt/v1",
"model": "gpt-5.6-sol",
"apiKind": "openai_responses",
"reasoningEffort": "high",
"reasoningEffort": "max",
"stream": false,
"webSearchEnabled": false,
"contextWindowTokens": 128000,
@@ -312,7 +312,7 @@ function assertContractRecordsMatch(label, leftRecords, rightRecords) {
function parseAppInvokeCommandNames(source) {
return Array.from(
source.matchAll(/invoke(?:<[^>]*>)?\(\s*['"]([a-z0-9_]+)['"]/g),
source.matchAll(/(?:invoke|directInvoke)(?:<[^>]*>)?\(\s*['"]([a-z0-9_]+)['"]/g),
([, command]) => command,
);
}
@@ -1189,9 +1189,9 @@ const allowedLlmReasoningEfforts = new Set([
'max',
]);
if (defaultAppConfig.llm?.reasoningEffort !== 'high') {
if (defaultAppConfig.llm?.reasoningEffort !== 'max') {
throw new Error(
'AI game creator shell default llm.reasoningEffort must stay high',
'AI game creator shell default llm.reasoningEffort must stay max',
);
}
@@ -0,0 +1,25 @@
import fs from 'node:fs';
import { spawn } from 'node:child_process';
import readline from 'node:readline';
const configPath = process.env.AGC_CONFIG_PATH ?? 'C:/Users/kdletters/AppData/Roaming/world.genarrative.ai-game-creator/game-creator.config.json';
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
const args = ['app-server', '--stdio', '-c', 'mcp_servers={}', '-c', 'web_search="disabled"', '-c', 'agents.enabled=false'];
for (const feature of ['apps','browser_use','browser_use_external','browser_use_full_cdp_access','computer_use','goals','image_generation','in_app_browser','plugins','remote_plugin','shell_tool','tool_suggest','unified_exec','workspace_dependencies']) args.push('--disable', feature);
args.push('-c', 'model_provider="genarrative_agc"', '-c', 'model_providers.genarrative_agc.name="Genarrative AGC"', '-c', `model_providers.genarrative_agc.base_url="${config.llm.baseUrl.replace(/\/$/, '')}"`, '-c', 'model_providers.genarrative_agc.env_key="GENARRATIVE_AGC_API_KEY"', '-c', 'model_providers.genarrative_agc.wire_api="responses"');
const child = spawn('C:/Program Files/nodejs/node.exe', ['C:/Users/kdletters/AppData/Roaming/npm/node_modules/@openai/codex/bin/codex.js', ...args], { cwd: process.cwd(), env: { ...process.env, GENARRATIVE_AGC_API_KEY: config.llm.apiKey }, stdio: ['pipe', 'pipe', 'pipe'] });
const send = (value) => child.stdin.write(`${JSON.stringify(value)}\n`);
const timer = setTimeout(() => { console.log('smoke-timeout'); child.kill(); process.exit(1); }, Number(process.env.DIRECT_SMOKE_TIMEOUT_MS ?? 180000));
readline.createInterface({ input: child.stdout }).on('line', (line) => {
let message; try { message = JSON.parse(line); } catch { return; }
if (message.id === 2) {
console.log('thread-start-ok');
const write = process.env.DIRECT_SMOKE_WRITE === '1';
send({ id: 3, method: 'turn/start', params: { threadId: message.result.thread.id, input: [{ type: 'text', text: process.env.DIRECT_SMOKE_PROMPT ?? (write ? '在当前工作区创建 smoke-marker.txt,写入 DIRECT_CODEX_WRITE_OK,然后回复已完成。' : '只回复 DIRECT_CODEX_SMOKE_OK,不要修改文件。') }], model: config.llm.model, approvalPolicy: 'never', sandboxPolicy: write ? { type: 'workspaceWrite', writableRoots: [process.cwd()], networkAccess: true } : { type: 'readOnly', networkAccess: false } } });
}
if (message.id === 3 && message.error) { console.log(`turn-start-error:${message.error.message}`); clearTimeout(timer); child.kill(); process.exit(1); }
if (message.method === 'turn/completed') { console.log(`turn-completed:${message.params?.turn?.status}`); clearTimeout(timer); child.kill(); process.exit(message.params?.turn?.status === 'completed' ? 0 : 1); }
});
send({ id: 1, method: 'initialize', params: { clientInfo: { name: 'agc-direct-smoke', version: '0.1' }, capabilities: { experimentalApi: true } } });
send({ method: 'initialized', params: {} });
send({ id: 2, method: 'thread/start', params: { cwd: process.cwd(), model: config.llm.model, approvalPolicy: 'never', sandbox: process.env.DIRECT_SMOKE_WRITE === '1' ? 'workspace-write' : 'read-only', ephemeral: true } });
@@ -11,6 +11,7 @@ use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
mod codex_app_server;
mod codex_cli;
mod direct_runtime;
mod generation;
mod interaction;
mod prompt;
@@ -20,11 +21,13 @@ mod runtime_driver;
mod runtime_protocol;
mod runtime_state;
mod runtime_tools;
pub(crate) use codex_app_server::direct_game_creator_codex_chat_at;
use codex_app_server::*;
use codex_cli::*;
pub(crate) use codex_cli::{
game_creator_codex_cli_executable_path, game_creator_codex_cli_version_identity,
};
pub(crate) use direct_runtime::*;
pub(crate) use generation::*;
pub(crate) use interaction::*;
pub(crate) use prompt::*;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -58,10 +58,12 @@ pub(crate) use canvas_generation::request_platform_art_asset_with_options_for_te
#[allow(unused_imports)]
pub(crate) use canvas_generation::{
build_platform_art_asset_prompt, editor_api_key_is_configured, generate_platform_art_asset_at,
maybe_generate_platform_art_asset_step, needs_platform_art_asset_generation,
platform_art_asset_art_spec, platform_art_asset_output_extension_matches,
prepare_platform_art_asset_output_path, project_canvas_asset_media_types,
role_has_canvas_assets, suggested_canvas_tool_call, PlatformArtAssetGenerationOptions,
generate_platform_art_asset_with_options_at,
generate_platform_art_asset_with_required_slices_at, maybe_generate_platform_art_asset_step,
needs_platform_art_asset_generation, platform_art_asset_art_spec,
platform_art_asset_output_extension_matches, prepare_platform_art_asset_output_path,
project_canvas_asset_media_types, role_has_canvas_assets, suggested_canvas_tool_call,
PlatformArtAssetGenerationOptions,
};
#[allow(unused_imports)]
pub(crate) use draft_validation::{
@@ -430,7 +430,8 @@ pub(crate) async fn external_editor_json_request(
.map_err(|error| format!("{action}失败:{error}"))?;
let status = response.status();
if !status.is_success() {
return Err(format!("{action}失败:HTTP {}", status.as_u16()));
let body = response.text().await.unwrap_or_default();
return Err(format_external_http_error(action, status, &body));
}
response
.json::<serde_json::Value>()
@@ -438,6 +439,92 @@ pub(crate) async fn external_editor_json_request(
.map_err(|error| format!("解析{action}响应失败:{error}"))
}
/// Keep provider validation details useful to the operator without copying an
/// entire response body (which may contain URLs, ids, paths or credentials).
fn format_external_http_error(action: &str, status: reqwest::StatusCode, body: &str) -> String {
let detail = summarize_external_http_error_body(body);
match detail {
Some(detail) => format!("{action}失败:HTTP {}{detail}", status.as_u16()),
None => format!("{action}失败:HTTP {}", status.as_u16()),
}
}
fn summarize_external_http_error_body(body: &str) -> Option<String> {
let payload = serde_json::from_str::<serde_json::Value>(body).ok()?;
let mut fields = Vec::new();
collect_external_http_error_fields(&payload, &mut fields);
if fields.is_empty() {
return None;
}
let mut detail = fields.join("");
detail = redact_secret_tokens(&detail);
detail = redact_absolute_path_tokens(&detail);
for field in ["operationId", "operation_id", "api_key", "access_token"] {
detail = redact_external_http_error_assignment(&detail, field);
}
detail.chars().take(600).collect::<String>().into()
}
fn redact_external_http_error_assignment(value: &str, field: &str) -> String {
let marker = format!("{field}=");
let mut output = String::with_capacity(value.len());
let mut cursor = 0usize;
while let Some(offset) = value[cursor..].find(&marker) {
let start = cursor + offset;
output.push_str(&value[cursor..start]);
output.push_str(&marker);
output.push_str("[redacted]");
let value_start = start + marker.len();
let value_end = value[value_start..]
.char_indices()
.find_map(|(index, character)| {
matches!(
character,
';' | '' | ',' | '' | '&' | '\n' | '\r' | '\t' | ' '
)
.then_some(value_start + index)
})
.unwrap_or(value.len());
cursor = value_end;
}
output.push_str(&value[cursor..]);
output
}
fn collect_external_http_error_fields(value: &serde_json::Value, output: &mut Vec<String>) {
const ALLOWED_KEYS: [&str; 7] = [
"code", "field", "message", "reason", "detail", "error", "status",
];
match value {
serde_json::Value::Object(map) => {
for key in ALLOWED_KEYS {
if let Some(value) = map.get(key) {
match value {
serde_json::Value::String(text) if !text.trim().is_empty() => {
output.push(format!("{key}={}", text.trim()));
}
serde_json::Value::Number(number) => {
output.push(format!("{key}={number}"));
}
_ => collect_external_http_error_fields(value, output),
}
}
}
for (key, child) in map {
if !ALLOWED_KEYS.contains(&key.as_str()) {
collect_external_http_error_fields(child, output);
}
}
}
serde_json::Value::Array(items) => {
for item in items {
collect_external_http_error_fields(item, output);
}
}
_ => {}
}
}
pub(crate) fn external_generation_poll_after_ms(payload: &serde_json::Value) -> u64 {
external_editor_response_data(payload)
.get("pollAfterMs")
@@ -791,6 +878,7 @@ pub(in crate::agent) struct PreparedPlatformArtAssetGeneration {
warning: Option<String>,
slice_warning: Option<String>,
slices: Vec<PreparedPlatformArtAssetSlice>,
spritesheet_slice_layout: Option<String>,
generation_route: String,
generation_kind: String,
reference_resource_ids: Vec<String>,
@@ -850,7 +938,11 @@ fn canonical_art_spec_reference_at(
}
fn canonical_art_spritesheet_icon_descriptions(prompt: &str) -> Vec<String> {
let project_context = truncate_inline(prompt.trim(), 320);
// External Editor validates each description independently (currently at
// 200 Unicode characters). Keep the gameplay context short enough that a
// long creation request cannot reject the atlas before it is queued.
const MAX_DESCRIPTION_CHARS: usize = 200;
const CONTEXT_PREFIX: &str = ";遵循同一项目视觉规范:";
[
"第 1 类(左上):当前玩法的玩家主体或主要操作对象;只生成一个轮廓连贯、可独立使用的完整素材",
"第 2 类(右上):当前玩法的方块、目标物、收集物、敌对实体或危险物;只生成一个完整素材",
@@ -858,10 +950,36 @@ fn canonical_art_spritesheet_icon_descriptions(prompt: &str) -> Vec<String> {
"第 4 类(右下):得分、受击、成长、失败、胜利或操作反馈特效;只生成一个完整素材",
]
.into_iter()
.map(|category| format!("{category};遵循同一项目视觉规范:{project_context}"))
.map(|category| {
let context_budget = MAX_DESCRIPTION_CHARS.saturating_sub(
category
.chars()
.count()
.saturating_add(CONTEXT_PREFIX.chars().count()),
);
let project_context = truncate_inline_bounded(prompt.trim(), context_budget);
format!("{category}{CONTEXT_PREFIX}{project_context}")
})
.collect()
}
fn truncate_inline_bounded(value: &str, max_chars: usize) -> String {
let normalized = value.split_whitespace().collect::<Vec<_>>().join(" ");
let actual_chars = normalized.chars().count();
if actual_chars <= max_chars {
return normalized;
}
if max_chars <= 3 {
return ".".repeat(max_chars);
}
let mut output = normalized
.chars()
.take(max_chars.saturating_sub(3))
.collect::<String>();
output.push_str("...");
output
}
fn decode_platform_art_image_with_limits(
download: &CanvasResourceDownload,
label: &str,
@@ -1099,7 +1217,7 @@ async fn prepare_platform_art_spritesheet_slices(
Ok(prepared)
}
pub(in crate::agent) async fn generate_platform_art_asset_with_options_at(
pub(crate) async fn generate_platform_art_asset_with_options_at(
root: &Path,
prompt: &str,
briefs: &[AgentGroupBrief],
@@ -1117,6 +1235,32 @@ pub(in crate::agent) async fn generate_platform_art_asset_with_options_at(
commit_prepared_platform_art_asset_at(root, prepared, options, |_| Ok(()))
}
/// Generates the canonical game spritesheet together with the four durable
/// core slices. Callers that promise a playable game must use this instead
/// of the permissive asset path: a bare spritesheet is not enough evidence
/// that player, target, obstacle, and feedback visuals are available.
pub(crate) async fn generate_platform_art_asset_with_required_slices_at(
root: &Path,
prompt: &str,
briefs: &[AgentGroupBrief],
options: &PlatformArtAssetGenerationOptions,
) -> Result<GeneratedPlatformArtAsset, String> {
if options.asset_kind != "art-spritesheet" {
return Err("严格游戏切片生成只允许 art-spritesheet 资产类型".to_string());
}
{
let recovery_lock =
acquire_project_write_lock(root, "canvas.asset_generate.strict.recover")?;
recover_interrupted_strict_platform_art_transaction_locked_at(root, &recovery_lock)?;
}
let prepared =
request_platform_art_asset_with_options_at(root, prompt, briefs, options).await?;
let lock = acquire_project_write_lock(root, "canvas.asset_generate.strict")?;
recover_interrupted_strict_platform_art_transaction_locked_at(root, &lock)?;
advance_agent_runtime_project_revision_locked(root)?;
commit_prepared_platform_art_asset_strict_slices_at(root, prepared, options, |_| Ok(()))
}
pub(in crate::agent) async fn request_platform_art_asset_with_options_at(
root: &Path,
prompt: &str,
@@ -1249,7 +1393,7 @@ pub(in crate::agent) async fn request_platform_art_asset_with_runtime_options_at
let is_canonical_art_spritesheet = options.asset_kind == "art-spritesheet";
let canonical_reference = matches!(
options.asset_kind.as_str(),
"ui-prototype" | "art-spritesheet"
"ui-prototype" | "game-background" | "art-spritesheet"
)
.then(|| canonical_art_spec_reference_at(root, &canvas_context.project_id))
.transpose()?;
@@ -1262,6 +1406,7 @@ pub(in crate::agent) async fn request_platform_art_asset_with_runtime_options_at
serde_json::json!({
"referenceId": reference_id,
"iconDescriptions": canonical_art_spritesheet_icon_descriptions(&generation_prompt),
"sliceLayout": "grid-2x2",
"screenColor": "auto",
"aspectRatio": options.aspect_ratio,
"imageSize": options.image_size,
@@ -1346,6 +1491,7 @@ pub(in crate::agent) async fn request_platform_art_asset_with_runtime_options_at
.await?;
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_default();
if external_generation_submit_rejection_is_definitive(status) {
if let Some(context) = runtime_context {
remove_platform_art_generation_runtime_state_at(
@@ -1354,15 +1500,23 @@ pub(in crate::agent) async fn request_platform_art_asset_with_runtime_options_at
&context.run_id,
)?;
}
return Err(format!("请求平台图片生成失败:HTTP {}", status.as_u16()));
return Err(format_external_http_error(
"请求平台图片生成",
status,
&body,
));
}
if runtime_context.is_some() {
return Err(format!(
"{EXTERNAL_GENERATION_RESULT_UNKNOWN_PREFIX} 平台图片生成提交返回 HTTP {},服务端是否已产生副作用未知",
status.as_u16()
"{EXTERNAL_GENERATION_RESULT_UNKNOWN_PREFIX} {},服务端是否已产生副作用未知",
format_external_http_error("平台图片生成提交", status, &body)
));
}
return Err(format!("请求平台图片生成失败:HTTP {}", status.as_u16()));
return Err(format_external_http_error(
"请求平台图片生成",
status,
&body,
));
}
let submission_payload = response
.json::<serde_json::Value>()
@@ -1498,6 +1652,11 @@ pub(in crate::agent) async fn request_platform_art_asset_with_runtime_options_at
Vec::new()
};
let warning = platform_art_generation_warning(generated);
let spritesheet_slice_layout = if is_canonical_art_spritesheet {
json_string_field(generated, "sliceLayout")
} else {
None
};
let resource_id = json_string_field(resource, "resourceId");
let task_id = if is_canonical_art_spritesheet {
consistent_canvas_task_id("External Editor 图集主图", &[generated, resource, asset])?
@@ -1553,6 +1712,7 @@ pub(in crate::agent) async fn request_platform_art_asset_with_runtime_options_at
warning,
slice_warning,
slices,
spritesheet_slice_layout,
generation_route,
generation_kind,
reference_resource_ids,
@@ -4869,6 +5029,7 @@ fn validate_strict_platform_art_spritesheet_contract(
task_id: Option<&str>,
generation_route: &str,
generation_kind: &str,
spritesheet_slice_layout: Option<&str>,
reference_resource_ids: &[String],
has_transparent_pixels: bool,
has_visible_pixels: bool,
@@ -4905,6 +5066,11 @@ fn validate_strict_platform_art_spritesheet_contract(
{
return Err("game-chat 图集生成 route/kind 与严格图集合同不一致".to_string());
}
if spritesheet_slice_layout.map(str::trim) != Some("grid-2x2") {
return Err(
"game-chat 图集必须由 External Editor 以 grid-2x2 固定切片合同生成".to_string(),
);
}
if reference_resource_ids.len() != 1
|| reference_resource_ids[0].trim().is_empty()
|| reference_resource_ids[0].trim() == resource_id
@@ -5248,6 +5414,7 @@ fn commit_prepared_platform_art_asset_with_before_replace_hook(
warning,
mut slice_warning,
slices,
spritesheet_slice_layout,
generation_route,
generation_kind,
reference_resource_ids,
@@ -5266,6 +5433,7 @@ fn commit_prepared_platform_art_asset_with_before_replace_hook(
task_id.as_deref(),
&generation_route,
&generation_kind,
spritesheet_slice_layout.as_deref(),
&reference_resource_ids,
spritesheet_has_transparent_pixels,
spritesheet_has_visible_pixels,
@@ -5685,6 +5853,17 @@ pub(crate) fn platform_art_asset_art_spec(
"references": [],
});
}
if options.asset_kind == "game-background" {
return serde_json::json!({
"assetType": "background",
"subject": format!("{};使用项目原创命名和原创场景设计", options.asset_label),
"style": "原创横屏 Web 游戏场景背景;层次清晰,保留可玩区域的视觉留白,不包含 UI 或文字",
"composition": "严格 16:9 单场景背景,前景、中景、远景有明确层次;不出现角色、可收集物、操作按钮、HUD、Logo 或文字",
"format": format!("{} {}", options.aspect_ratio, options.image_size),
"constraints": "必须是可见的真实图片产物,适合作为 Canvas 或 HTML 游戏场景底图;不得做成素材图集、完整游戏截图、海报或概念板;必须原创,不得复刻现有游戏场景、Logo、贴图、标志性布局或受保护视觉语言",
"references": [],
});
}
serde_json::json!({
"assetType": "art",
"subject": format!("{};使用项目原创命名和原创阵营设计", options.asset_label),
@@ -5713,6 +5892,12 @@ pub(crate) fn build_platform_art_asset_prompt(
truncate_prompt_context(prompt.trim())
);
}
if options.asset_kind == "game-background" {
return format!(
"为 Web 小游戏生成一张可直接作为运行画面底图的原创 16:9 场景背景。严格从下方用户需求提炼自己的游戏主题、地点、季节、材质和氛围;画面要为真实可玩区域留出足够清楚的中部空间,并有前景、中景、远景层次。不得画玩家角色、道具、棋子、障碍、HUD、操作按钮、文字、Logo、完整游戏截图、海报或素材图集;这些元素会从独立透明核心图集中绘制。不得自行假设为塔防或加入玩法合同中不存在的实体;必须原创,不得复刻现有游戏场景、贴图、标志性布局或受保护视觉语言。\n\n用户需求:{}",
truncate_prompt_context(prompt.trim())
);
}
let art_asset_brief = briefs
.iter()
.flat_map(|brief| brief.role_briefs.iter())
@@ -5737,6 +5922,28 @@ mod canvas_generation_tests {
ColorType, ImageEncoder,
};
#[test]
fn external_http_error_summary_keeps_validation_details_without_sensitive_context() {
let body = serde_json::json!({
"error": {
"code": "invalid-request",
"field": "sliceLayout",
"message": "只支持 grid-2x2operationId=private-operation-idapi_key=private-key",
},
"details": {
"path": "C:\\Users\\private\\secret.json",
}
})
.to_string();
let summary = summarize_external_http_error_body(&body).expect("summary");
assert!(summary.contains("code=invalid-request"), "{summary}");
assert!(summary.contains("field=sliceLayout"), "{summary}");
assert!(summary.contains("只支持 grid-2x2"), "{summary}");
assert!(!summary.contains("private-operation-id"), "{summary}");
assert!(!summary.contains("private-key"), "{summary}");
assert!(!summary.contains("C:\\Users\\private"), "{summary}");
}
fn read_test_http_request(stream: &mut std::net::TcpStream) -> String {
stream
.set_nonblocking(false)
@@ -5876,6 +6083,57 @@ mod canvas_generation_tests {
assert!(slices.iter().all(|slice| slice.extension == "png"));
}
#[test]
fn strict_spritesheet_contract_requires_the_grid_2x2_provider_receipt() {
let canvas_context = ExternalCanvasGenerationContext {
project_id: "canvas-project".to_string(),
asset_folder_id: "asset-folder".to_string(),
canvas_name: "contract-test".to_string(),
};
let slices = (0..4)
.map(|index| {
let download = rgba_test_png(100 + index);
let validated = validate_platform_art_png_bytes_with_limits(
&download.bytes,
"strict layout receipt test slice",
)
.expect("fixture slice should validate");
PreparedPlatformArtAssetSlice {
name: format!("素材 {}", index + 1),
width: validated.width,
height: validated.height,
download,
resource_id: Some(format!("slice-resource-{index}")),
asset_object_id: Some(format!("slice-object-{index}")),
canvas_project_id: Some("canvas-project".to_string()),
task_id: Some("spritesheet-task".to_string()),
source_resource_id: Some("spritesheet-resource".to_string()),
content_sha256: validated.content_sha256,
pixel_sha256: validated.pixel_sha256,
has_visible_pixels: true,
extension: "png".to_string(),
}
})
.collect::<Vec<_>>();
let error = validate_strict_platform_art_spritesheet_contract(
&slices,
&canvas_context,
Some("canvas-project"),
Some("spritesheet-resource"),
Some("spritesheet-object"),
Some("spritesheet-task"),
"/api/external/v1/editor/icon-spritesheets/generations",
"icon-spritesheet",
None,
&["art-spec-resource".to_string()],
true,
true,
)
.expect_err("missing fixed-layout proof must not be accepted");
assert!(error.contains("grid-2x2"));
}
#[test]
fn canonical_spritesheet_rejects_task_id_conflicts_across_all_identity_copies() {
let generated = serde_json::json!({
@@ -7078,6 +7336,16 @@ mod canvas_generation_tests {
}
}
#[test]
fn canonical_art_spritesheet_descriptions_obey_external_editor_item_limit() {
let descriptions = canonical_art_spritesheet_icon_descriptions(&"原创玩法需求".repeat(128));
assert_eq!(descriptions.len(), 4);
assert!(descriptions
.iter()
.all(|description| description.chars().count() <= 200));
}
#[test]
fn derived_visual_asset_waits_for_registered_art_spec() {
let temporary = tempfile::tempdir().expect("create art dependency project");
@@ -7214,6 +7482,7 @@ mod canvas_generation_tests {
warning: None,
slice_warning: None,
slices: Vec::new(),
spritesheet_slice_layout: Some("grid-2x2".to_string()),
generation_route: "/api/external/v1/editor/icon-spritesheets/generations".to_string(),
generation_kind: "icon-spritesheet".to_string(),
reference_resource_ids: vec!["art-spec-resource".to_string()],
@@ -7529,6 +7798,7 @@ mod canvas_generation_tests {
warning: None,
slice_warning: None,
slices,
spritesheet_slice_layout: Some("grid-2x2".to_string()),
generation_route: "/api/external/v1/editor/icon-spritesheets/generations".to_string(),
generation_kind: "icon-spritesheet".to_string(),
reference_resource_ids: vec!["art-spec-resource".to_string()],
@@ -13,6 +13,10 @@ pub(crate) enum CliCommand {
agent_id: String,
prompt: String,
},
DirectCodexChat {
project_path: PathBuf,
prompt: String,
},
AgentTask {
project_path: PathBuf,
agent_id: String,
@@ -186,6 +190,7 @@ impl CliCommand {
..
} => Some((project_path, *initialize)),
Self::AgentChat { project_path, .. }
| Self::DirectCodexChat { project_path, .. }
| Self::AgentRuntimeStatus { project_path, .. }
| Self::AgentContextCompact { project_path, .. }
| Self::AgentGoalStatus { project_path, .. }
@@ -687,6 +692,19 @@ pub(crate) fn parse_cli_command(args: &[String]) -> Result<Option<CliCommand>, S
prompt: prompt.to_string(),
}));
}
if args.first().map(String::as_str) == Some("--direct-codex-chat") {
if args.len() < 3 {
return Err("用法:--direct-codex-chat <本地项目绝对路径> <聊天内容>".to_string());
}
let prompt = args[2..].join(" ");
if prompt.trim().is_empty() {
return Err("聊天内容不能为空".to_string());
}
return Ok(Some(CliCommand::DirectCodexChat {
project_path: PathBuf::from(&args[1]),
prompt: prompt.trim().to_string(),
}));
}
if args.first().map(String::as_str) == Some("--swarm-chat") {
const USAGE: &str = "用法:--swarm-chat [--init] [--autonomous-game-build] [--game-chat-smoke] <本地项目绝对路径> [parentAgentId]";
let mut rest = args[1..].to_vec();
@@ -929,6 +947,36 @@ pub(crate) fn run_cli_command(command: CliCommand) -> Result<(), String> {
println!("replyText={}", reply.reply_text);
Ok(())
}
CliCommand::DirectCodexChat {
project_path,
prompt,
} => {
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", true)?;
if !project_path.join(".agent/manifest.json").is_file() {
let project_name = project_path
.file_name()
.and_then(|value| value.to_str())
.unwrap_or("Codex 直连项目");
init_local_game_project_at(
&project_path,
&format!("direct-codex-{}", unix_millis()),
project_name,
)?;
}
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.map_err(|error| format!("创建 CLI runtime 失败:{error}"))?;
let reply_result = runtime
.block_on(async { run_direct_game_creator_turn_at(&project_path, &prompt).await });
let shutdown_result = shutdown_game_creator_codex_app_servers();
let reply = reply_result?;
shutdown_result?;
println!("direct-codex.chat.completed");
println!("projectPath={}", project_path.display());
println!("replyText={reply}");
Ok(())
}
CliCommand::AgentTask {
project_path,
agent_id,
@@ -1215,10 +1215,10 @@ const GAME_CREATOR_LOCAL_CONFIG_FILE_NAME: &str = "game-creator.config.local.jso
const GAME_CREATOR_AGENT_MODE_CODEX_APP_SERVER: &str = "codex_app_server";
const GAME_CREATOR_AGENT_MODE_CODEX_CLI: &str = "codex_cli";
const GAME_CREATOR_AGENT_MODE_PROVIDER: &str = "provider";
const DEFAULT_GAME_CREATOR_LLM_BASE_URL: &str = "https://api.openai.com/v1";
const DEFAULT_GAME_CREATOR_LLM_MODEL: &str = "gpt-4.1";
const DEFAULT_GAME_CREATOR_LLM_BASE_URL: &str = "https://dev.genarrative.world/gpt/v1";
const DEFAULT_GAME_CREATOR_LLM_MODEL: &str = "gpt-5.6-sol";
const DEFAULT_GAME_CREATOR_LLM_API_KIND: &str = "openai_responses";
const DEFAULT_GAME_CREATOR_LLM_REASONING_EFFORT: &str = "high";
const DEFAULT_GAME_CREATOR_LLM_REASONING_EFFORT: &str = "max";
const DEFAULT_GAME_CREATOR_LLM_CONTEXT_WINDOW_TOKENS: u64 = 128_000;
const DEFAULT_GAME_CREATOR_LLM_AUTO_COMPACT_TOKEN_LIMIT: u64 = 64_000;
const DEFAULT_GAME_CREATOR_LLM_TOOL_OUTPUT_TOKEN_LIMIT: u64 = 12_000;
@@ -1841,6 +1841,11 @@ where
}
fn handle_game_creator_gui_run_event(event: &tauri::RunEvent) {
if matches!(event, tauri::RunEvent::Exit) {
if let Err(error) = agent::shutdown_game_creator_codex_app_servers() {
eprintln!("agent.direct_codex.gui_exit.shutdown_failed: {error}");
}
}
match resolve_game_creator_gui_runner_shutdown(event, shutdown_external_agent_runner) {
GameCreatorGuiRunnerShutdownOutcome::NotRequested => {}
GameCreatorGuiRunnerShutdownOutcome::Requested => {
@@ -2192,6 +2197,7 @@ fn main() {
chat_with_game_creator_agent,
chat_with_game_creator_role_agent,
chat_with_game_creator_role_agent_stream,
chat_with_game_creator_direct_codex,
start_game_creator_agent_runtime_task,
start_game_creator_supervisor_runtime_task,
compact_game_creator_agent_runtime_context,
@@ -590,10 +590,18 @@ pub(crate) fn resolve_preview_path(root: &Path, url_path: &str) -> Result<PathBu
let mut file_path = root.to_path_buf();
let mut parts = relative.split('/');
let first = parts.next().ok_or_else(|| "预览路径非法".to_string())?;
if first != "game" && first != "assets" {
return Err("预览路径只能访问 game/ 或 assets/".to_string());
if first.is_empty() || first == "." || first == ".." || first.contains('\\') {
return Err("预览路径非法".to_string());
}
if first == "game" || first == "assets" {
file_path.push(first);
} else {
// `/` serves `game/index.html`, so browser-relative resources such as
// `style.css` and `scripts/game.js` must resolve from the same game root.
// Explicit `/assets/...` URLs keep their project-level asset mapping.
file_path.push("game");
file_path.push(first);
}
file_path.push(first);
for part in parts {
if part.is_empty() || part == "." || part == ".." || part.contains('\\') {
return Err("预览路径非法".to_string());
@@ -660,6 +660,74 @@ pub(crate) fn ensure_initial_game_iteration_version_at(
}
}
/// Appends the formal version produced by a successful direct Agent turn.
///
/// The project revision is the cross-surface ordering boundary: the launcher
/// rejects a same-revision manifest whose assets or versions differ from the
/// snapshot it already rendered. Therefore callers must advance the durable
/// project revision first and pass that new value here.
pub(crate) fn append_agent_game_iteration_version_at(
root: &Path,
project_revision: u64,
) -> Result<bool, String> {
if project_revision == 0 {
return Err("Agent 项目版本必须绑定大于 0 的项目 revision".to_string());
}
mutate_manifest_at(root, |manifest| {
let resource_bindings = manifest
.assets
.iter()
.map(|asset| GameIterationVersionResourceBinding {
slot_id: format!("asset:{}", asset.id),
resource_id: asset.id.clone(),
})
.collect::<Vec<_>>();
if let Some(existing) = manifest
.versions
.iter()
.find(|version| version.project_revision == project_revision)
{
if existing.resource_bindings == resource_bindings {
return Ok(false);
}
return Err(format!(
"项目 revision {project_revision} 已绑定不同的正式版本"
));
}
let (version_id, parent_version_id, created_reason) =
if let Some(parent) = manifest.versions.last() {
if project_revision <= parent.project_revision {
return Err(format!(
"Agent 项目版本 revision {project_revision} 必须大于父版本 revision {}",
parent.project_revision
));
}
(
format!("agent-{project_revision}"),
Some(parent.version_id.clone()),
GameIterationVersionCreatedReason::AgentRevision,
)
} else {
(
format!("initial-{project_revision}"),
None,
GameIterationVersionCreatedReason::Initial,
)
};
manifest.versions.push(GameIterationVersion {
version_id,
parent_version_id,
project_revision,
resource_bindings,
created_reason,
created_at: unix_timestamp(),
edit_prompt: None,
});
Ok(true)
})
}
pub(crate) fn ensure_manifest_has_seed_tasks(
root: &Path,
goal: Option<&str>,
@@ -2839,6 +2839,7 @@ fn preview_path_rejects_traversal() {
fs::create_dir_all(project.join("memory")).expect("memory dir");
fs::create_dir_all(project.join(".agent")).expect("agent dir");
fs::write(project.join("game/index.html"), "<!doctype html>").expect("index");
fs::write(project.join("game/style.css"), "body {}").expect("game style");
fs::write(project.join("assets/player.png"), b"png").expect("asset");
fs::write(project.join("memory/project.md"), "secret memory").expect("memory");
fs::write(project.join(".agent/run.latest.json"), "{}").expect("trace");
@@ -2861,6 +2862,13 @@ fn preview_path_rejects_traversal() {
.canonicalize()
.expect("canonical asset")
);
assert_eq!(
resolve_preview_path(&project, "/style.css?cache=1").unwrap(),
project
.join("game/style.css")
.canonicalize()
.expect("canonical game style")
);
fs::remove_dir_all(project).ok();
}
+228 -9
View File
@@ -115,6 +115,7 @@ import {
normalizeAgentRuntimeState,
projectNameFromPath,
projectProfessionalAgentLabel,
projectRuntimeVisibleError,
projectSupervisorPendingRepairMatchesProfessional,
projectSupervisorResponseStreamIdentity,
readProjectSupervisorActiveSessionId,
@@ -252,6 +253,7 @@ const LEGACY_GAME_CHAT_AUTO_PREVIEW_AUTHORIZATION_STORAGE_KEY =
'genarrative.game-chat.auto-preview-authorization.v1';
const GAME_CHAT_AUTO_PREVIEW_AUTHORIZATION_STORAGE_KEY =
'genarrative.game-chat.auto-preview-authorization.v2';
const DIRECT_CODEX_PRODUCT_RUNTIME = true;
type GameChatAutoPreviewAuthorization = {
afterRevision: number;
@@ -599,6 +601,11 @@ export function App({
onAgentRuntimeSummariesChange,
onAgentResultsChange,
}: AppProps = {}) {
const directCodexProductRuntime =
DIRECT_CODEX_PRODUCT_RUNTIME &&
projectSupervisorOnly &&
!supervisorChatOnly &&
!gameChatOnly;
const [devMode] = useState(() =>
projectSupervisorOnly ? false : isDeveloperMode(),
);
@@ -1314,6 +1321,26 @@ export function App({
}
initialProjectOpenedRef.current = true;
if (projectSupervisorOnly) {
if (directCodexProductRuntime) {
if (!isAbsoluteProjectPath(initialProjectPath)) {
setWorkspaceStatus('请提供工作区绝对路径');
return;
}
if (projectPathHasControlCharacter(initialProjectPath)) {
setWorkspaceStatus('工作区路径不能包含控制字符');
return;
}
// This component is mounted inside the already-created project
// workbench. Hydrate localProject before the first direct turn so the
// direct runtime cannot silently create a second workspace.
void openWorkspace(
initialProjectPath,
false,
'open',
initialProjectKind,
);
return;
}
if (gameChatOnly) {
void openGameChatProjectPath(initialProjectPath);
return;
@@ -2937,14 +2964,46 @@ export function App({
const loadVersion = projectSupervisorHistoryLoadVersionRef.current + 1;
projectSupervisorHistoryLoadVersionRef.current = loadVersion;
try {
const resumeError = await resumeProjectSupervisorRuntimeTasksIfNeeded(
invoke,
nextProjectPath,
);
const sessionId = await readProjectSupervisorActiveSession(
const resumeError = directCodexProductRuntime
? ''
: await resumeProjectSupervisorRuntimeTasksIfNeeded(
invoke,
nextProjectPath,
);
if (directCodexProductRuntime) {
projectSupervisorRuntimeResumeProjectPathRef.current = nextProjectPath;
agentRuntimeResumeProjectPathRef.current = nextProjectPath;
}
let sessionId = await readProjectSupervisorActiveSession(
invoke,
nextProjectPath,
);
let persistedSupervisorRuntimeResult: AgentRuntimeResult | null = null;
let runtimeError = '';
if (!sessionId && directCodexProductRuntime) {
try {
const runtimeResults = await invoke<AgentRuntimeResult[]>(
'read_game_creator_agent_runtimes',
{ projectPath: nextProjectPath },
);
persistedSupervisorRuntimeResult =
runtimeResults
.filter(
(runtimeResult) =>
runtimeResult.state.agentId ===
PROJECT_SUPERVISOR_AGENT_ID,
)
.sort(
(left, right) =>
right.state.updatedAt - left.state.updatedAt,
)[0] ?? null;
sessionId =
persistedSupervisorRuntimeResult?.state.sessionId || null;
} catch (error) {
runtimeError =
error instanceof Error ? error.message : String(error);
}
}
const projectConversation = await invoke<LocalConversationResult>(
'read_local_conversation',
{
@@ -2955,7 +3014,6 @@ export function App({
let supervisorConversation: LocalConversationResult | null = null;
let runtime: AgentRuntimeState | null = null;
let runtimeResponseStream: AgentRuntimeResponseStream | null = null;
let runtimeError = '';
if (sessionId) {
supervisorConversation = await invoke<LocalConversationResult>(
'read_local_conversation',
@@ -2965,6 +3023,14 @@ export function App({
sessionId,
},
);
}
if (persistedSupervisorRuntimeResult) {
runtime = agentRuntimeStateFromResult(
persistedSupervisorRuntimeResult,
);
runtimeResponseStream =
persistedSupervisorRuntimeResult.responseStream ?? null;
} else if (sessionId) {
try {
const runtimeResult = await invoke<AgentRuntimeResult>(
'read_game_creator_agent_runtime',
@@ -3209,6 +3275,9 @@ export function App({
},
]);
}
// Direct Codex must not resume legacy Supervisor work, but existing
// projects still need their conversation and run history hydrated as
// read-only evidence.
void loadProjectConversation(openedProject.projectPath);
void refreshAgentRunTrace(openedProject.projectPath);
} catch (error) {
@@ -5755,7 +5824,144 @@ export function App({
return sessionId;
}
async function refreshDirectProjectSurface(
invoke: TauriInvoke,
nextProjectPath: string,
) {
try {
await refreshManifest(nextProjectPath);
} catch {
// The direct turn result is still authoritative for the chat. A missing
// or legacy manifest must not hide the Codex reply.
}
try {
const current = await invoke<LocalPreviewStatus>(
'get_local_game_preview_status',
{ projectPath: nextProjectPath },
);
if (current.status === 'running' && current.url && current.port) {
updateClientPreview({
url: current.url,
port: current.port,
root: current.root ?? nextProjectPath,
});
setPreviewStatus(`运行中:127.0.0.1:${current.port}`);
return;
}
const started = await invoke<LocalPreviewResult>(
'start_local_game_preview',
{ projectPath: nextProjectPath },
);
updateClientPreview(started);
setPreviewStatus(`运行中:127.0.0.1:${started.port}`);
setCommandLog((currentLog) => [...currentLog, 'preview.start']);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setPreviewStatus(`预览未启动:${message}`);
setMessages((current) => [
...current,
{
role: 'assistant',
text: `智能创作已返回,但客户端预览启动失败:${message}`,
runtimeOwned: true,
updatedAt: Date.now(),
},
]);
}
}
async function executeChatAgentReply(prompt: string) {
// Product default: send the conversation directly to Codex app-server.
// The legacy Supervisor/harness path remains below for rollback and tests.
if (directCodexProductRuntime) {
const directInvoke = resolveTauriInvoke();
let directProjectPath = resolveChatProjectPath(localProject);
if (!directProjectPath && directInvoke) {
try {
const created = await directInvoke<InitLocalProjectResult>(
'create_automatic_local_game_project',
);
await openWorkspace(created.projectPath, false, 'open');
directProjectPath = created.projectPath;
} catch (error) {
const message =
error instanceof Error ? error.message : String(error);
setMessages((current) => [
...current,
{
role: 'assistant',
text: `自动创建 AGC 项目失败:${message}`,
runtimeOwned: true,
updatedAt: Date.now(),
},
]);
setProjectSupervisorRuntimeError(`智能创作执行失败:${message}`);
return;
}
}
if (directProjectPath && directInvoke) {
setChatAgentBusy(true);
setProjectSupervisorRuntimeError('');
try {
const reply = await directInvoke<string>(
'chat_with_game_creator_direct_codex',
{
projectPath: directProjectPath,
prompt,
},
);
if (localProjectPathRef.current === directProjectPath) {
setMessages((current) => [
...current,
{
role: 'assistant',
text: reply,
runtimeOwned: true,
updatedAt: Date.now(),
},
]);
await refreshDirectProjectSurface(directInvoke, directProjectPath);
}
} catch (error) {
const message =
error instanceof Error ? error.message : String(error);
const visibleMessage = projectRuntimeVisibleError(
message,
'陶泥儿智能创作',
true,
);
if (localProjectPathRef.current === directProjectPath) {
setProjectSupervisorRuntimeError(visibleMessage);
setMessages((current) => [
...current,
{
role: 'assistant',
text: visibleMessage,
runtimeOwned: true,
updatedAt: Date.now(),
},
]);
}
} finally {
setChatAgentBusy(false);
}
return;
}
const message = directInvoke
? '当前项目尚未准备好,无法启动智能创作。'
: '需要在 Tauri App 内运行,无法启动智能创作。';
setProjectSupervisorRuntimeError(message);
setMessages((current) => [
...current,
{
role: 'assistant',
text: message,
runtimeOwned: true,
updatedAt: Date.now(),
},
]);
return;
}
if (agentRuntimeNeedsUserInput(projectSupervisorRuntimeRef.current)) {
setProjectSupervisorRuntimeError('请先回答项目总控 Agent 当前的澄清问题');
return;
@@ -5951,7 +6157,11 @@ export function App({
useEffect(() => {
const latch = initialSupervisorMessageLatchRef.current;
if (!gameChatOnly || !latch.prompt || !localProject) {
if (
(!directCodexProductRuntime && !gameChatOnly) ||
!latch.prompt ||
!localProject
) {
return;
}
if (localProject.projectPath !== latch.projectPath) {
@@ -10334,6 +10544,9 @@ export function App({
return;
}
try {
if (directCodexProductRuntime) {
agentRuntimeResumeProjectPathRef.current = nextProjectPath;
}
if (agentRuntimeResumeProjectPathRef.current !== nextProjectPath) {
try {
const resumedRuntimes = await invoke<AgentRuntimeResult[]>(
@@ -11059,15 +11272,20 @@ export function App({
return (
<ProjectSupervisorView
chatInput={chatInput}
directCodex={directCodexProductRuntime}
hiddenConversationCount={hiddenConversationCount}
needsUserInput={projectSupervisorNeedsUserInput}
needsUserInput={
directCodexProductRuntime ? false : projectSupervisorNeedsUserInput
}
onCancelConfirmation={cancelUiCommandConfirmation}
onChatInputChange={setChatInput}
onConfirmConfirmation={confirmUiCommand}
onScroll={handleConversationScroll}
onShowEarlierMessages={showEarlierConversationMessages}
onSubmit={handleProjectSupervisorOnlySubmit}
pendingConfirmation={pendingUiConfirmation}
pendingConfirmation={
directCodexProductRuntime ? null : pendingUiConfirmation
}
transientReply={projectSupervisorTransientReply}
visibleMessages={visibleMessages}
visibleProfessionalAgentCards={visibleProfessionalAgentCards}
@@ -11077,6 +11295,7 @@ export function App({
error={projectSupervisorRuntimeError}
runtimeByAgentId={agentRuntimeById}
controlBusy={chatAgentBusy}
readOnly={directCodexProductRuntime}
professionalResultsByAgentId={professionalAgentResultsById}
onToolAction={handleProjectSupervisorToolAction}
onSupervisorRetry={handleProjectSupervisorRetry}
@@ -1729,6 +1729,90 @@ export function isMudPointInsufficientRuntimeError(message: string) {
);
}
function directPlatformFailureDetail(message: string) {
const trimmed = message.trim();
if (!trimmed) {
return null;
}
const lower = trimmed.toLowerCase();
if (
!(
lower.includes('陶泥儿美术包生成失败') ||
lower.includes('平台图片生成任务失败') ||
lower.includes('external editor') ||
lower.includes('game-chat 图集') ||
lower.includes('透明美术图集') ||
lower.includes('图集切片')
)
) {
return null;
}
if (
/(?:authorization|bearer|api[_ -]?key|token|secret|cookie|set-cookie|https?:\/\/|(?:^|[\\s:])\/?(?:users|home|var|tmp|private)\/|[A-Z]:\\Users\\)/i.test(
trimmed,
)
) {
return null;
}
const safe = trimmed
.replace(/(?:|;|\s)operationId\s*=\s*[^;\s]+/gi, '')
.replace(/(?:|;)\s*externalGenerationJobId\s*=\s*[^;\s]+/gi, '')
.replace(/\s+/g, ' ')
.trim()
.slice(0, 280);
return safe || null;
}
function directCodexFailureDetail(message: string) {
const trimmed = message.trim();
const marker = 'direct-codex-error:';
const markerIndex = trimmed.indexOf(marker);
if (markerIndex < 0) {
return null;
}
const detail = trimmed.slice(markerIndex + marker.length).trim();
if (!detail) {
return null;
}
if (
/(?:authorization|bearer|api[_ -]?key|token|secret|cookie|set-cookie)/i.test(
detail,
)
) {
return null;
}
const safe = detail
.replace(/https?:\/\/[^\s]+/gi, '[已隐藏链接]')
.replace(/(?:[A-Z]:\\|\\\\|\/(?:Users|home|var|tmp|private)\/)[^\s]+/gi, '[已隐藏路径]')
.replace(/\s+/g, ' ')
.trim()
.slice(0, 280);
return safe || null;
}
function directRuntimeFailureDetail(message: string) {
const trimmed = message.trim();
if (
!(
trimmed.startsWith('Codex 已返回,但客户端登记生成产物失败:') ||
trimmed.includes('陶泥儿美术包不完整,已终止代码生成') ||
trimmed.includes('陶泥儿规范图生成后未形成可用平台合同') ||
trimmed.includes('陶泥儿美术包生成返回后未形成可用的已登记平台素材合同')
)
) {
return null;
}
if (
/(?:authorization|bearer|api[_ -]?key|token|secret|cookie|set-cookie|https?:\/\/|(?:^|[\\s:])\/?(?:users|home|var|tmp|private)\/|[A-Z]:\\Users\\)/i.test(
trimmed,
)
) {
return null;
}
const safe = trimmed.replace(/\s+/g, ' ').trim().slice(0, 320);
return safe || null;
}
export function projectRuntimeVisibleError(
message: string,
subject: string,
@@ -1742,6 +1826,10 @@ export function projectRuntimeVisibleError(
if (isMudPointInsufficientRuntimeError(message)) {
return MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE;
}
const directFailureDetail = directPlatformFailureDetail(message);
if (directFailureDetail) {
return `${subject}${directFailureDetail}`;
}
const codexAppServerKind = visibleMessage.match(
/(?:^|[\s:])kind=codex-app-server-([a-z-]+)(?=\s|$)/,
)?.[1];
@@ -1749,20 +1837,49 @@ export function projectRuntimeVisibleError(
const detail = {
'context-window-exceeded': '模型上下文已超限,请缩小任务范围后重试',
'session-budget-exceeded': '本次会话预算已耗尽,请缩小任务范围或新建任务',
'usage-limit-exceeded': 'Codex 用量已达上限,请检查账户额度后重试',
'usage-limit-exceeded': '智能创作用量已达上限,请检查账户额度后重试',
unauthorized: 'Codex 鉴权失败,请重新登录或检查 API Key',
'bad-request': 'Codex 请求无效,请检查模型与运行时配置',
'cyber-policy': 'Codex 安全策略拒绝了本次请求,请调整任务内容',
'sandbox-error': 'Codex 隔离环境启动失败,请重试或检查本机环境',
'thread-rollback-failed': 'Codex 会话恢复失败,请新建任务后重试',
'bad-request': '智能创作请求无效,请检查模型与运行时配置',
'cyber-policy': '智能创作安全策略拒绝了本次请求,请调整任务内容',
'sandbox-error': '智能创作隔离环境启动失败,请重试或检查本机环境',
'thread-rollback-failed': '智能创作会话恢复失败,请新建任务后重试',
'active-turn-not-steerable':
'当前 Codex 任务无法追加指令,请等待结束后重试',
other: 'Codex 执行失败,请查看运行详情后重试',
'当前智能创作任务无法追加指令,请等待结束后重试',
other: '智能创作执行失败,请查看运行详情后重试',
}[codexAppServerKind];
if (detail) {
return `${subject} ${detail}`;
}
}
const directCodexAppServerKind = visibleMessage.match(
/codex-app-server-error:([a-z-]+)/,
)?.[1];
if (directCodexAppServerKind) {
const detail = {
'context-window-exceeded': '模型上下文已超限,请缩小任务范围后重试',
'session-budget-exceeded': '本次会话预算已耗尽,请缩小任务范围或新建任务',
'usage-limit-exceeded': '用量已达上限,请检查账户额度后重试',
unauthorized: '鉴权失败,请检查 API Key 或登录态',
'bad-request': '请求无效,请检查模型与运行时配置',
'cyber-policy': '安全策略拒绝了本次请求,请调整任务内容',
'sandbox-error': '工作区隔离启动失败,请检查项目目录后重试',
other: '未完成本次执行,请查看项目文件是否已修改后再重试',
}[directCodexAppServerKind];
if (detail) {
return `${subject}${detail}`;
}
}
if (visibleMessage.includes('codex-app-server-terminal-unknown:')) {
return `${subject}服务或网络在执行中断开,最终状态未知;请先检查项目文件是否已修改,再决定是否重试`;
}
const directCodexDetail = directCodexFailureDetail(visibleMessage);
if (directCodexDetail) {
return `${subject}Codex 执行失败:${directCodexDetail}`;
}
const directRuntimeDetail = directRuntimeFailureDetail(visibleMessage);
if (directRuntimeDetail) {
return `${subject}${directRuntimeDetail}`;
}
const exhaustedUpstreamRetry = visibleMessage.match(
/(?:^|[\s:])kind=upstream-(\d{3}) httpStatus=(\d{3}) fingerprint=[0-9a-f]{64} chars=\d+ retryAttempt=(\d+) maxRetries=(\d+) retryState=exhausted\s*$/,
);
@@ -663,6 +663,7 @@ export function ProjectSupervisorRuntimePanel({
error,
runtimeByAgentId,
controlBusy,
readOnly = false,
professionalResultsByAgentId,
onToolAction,
onSupervisorRetry,
@@ -674,6 +675,7 @@ export function ProjectSupervisorRuntimePanel({
error: string;
runtimeByAgentId: Record<string, AgentRuntimeState | undefined>;
controlBusy: boolean;
readOnly?: boolean;
professionalResultsByAgentId: Record<
string,
ProjectAgentResultSummary | undefined
@@ -776,6 +778,7 @@ export function ProjectSupervisorRuntimePanel({
? (runtime.taskQueue?.pending ?? 0)
: 0;
const canRetrySupervisor = Boolean(
!readOnly &&
runtime &&
!pendingToolAction &&
(runtime.status === 'failed' ||
@@ -843,7 +846,14 @@ export function ProjectSupervisorRuntimePanel({
{compactProgress ? (
<small aria-label="项目总控 Agent 进度">{compactProgress}</small>
) : null}
{(needsSupervisorReconciliation || canRetrySupervisor) && runtime ? (
{readOnly && runtime ? (
<small className="project-runtime-retry-feedback" role="status">
</small>
) : null}
{!readOnly &&
(needsSupervisorReconciliation || canRetrySupervisor) &&
runtime ? (
<div
className="project-runtime-recovery"
aria-label={
@@ -933,6 +943,7 @@ export function ProjectSupervisorRuntimePanel({
) : null}
{pendingToolAction &&
pendingActionPresentation &&
!readOnly &&
!needsSupervisorReconciliation ? (
<div
className="pending-command project-runtime-pending-command"
@@ -976,6 +987,7 @@ export function ProjectSupervisorRuntimePanel({
const professionalResult =
professionalResultsByAgentId[professionalRuntime.agentId];
const canRetryProfessional =
!readOnly &&
!supervisorIsTerminal &&
!professionalPendingAction &&
(professionalRuntime.status === 'failed' ||
@@ -1027,7 +1039,7 @@ export function ProjectSupervisorRuntimePanel({
professionalRuntime.updatedAt,
)}
</small>
{professionalPendingAction ? (
{professionalPendingAction && !readOnly ? (
<div
className="project-runtime-professional-actions"
aria-label={`${projectProfessionalAgentLabel(
@@ -1185,14 +1197,14 @@ export function ProjectSupervisorRuntimePanel({
{projectRuntimeVisibleError(professionalActionError, '专业 Agent')}
</small>
) : null}
{userInputRequest ? (
{userInputRequest && !readOnly ? (
<AgentRuntimeUserInputCard
key={`${userInputRequest.requestId}:${userInputRequest.responseId ?? 'pending'}`}
request={userInputRequest}
controlBusy={controlBusy}
onSubmit={onUserInput}
/>
) : needsUserInput ? (
) : needsUserInput && !readOnly ? (
<p className="agent-runtime-user-input-missing" role="status">
</p>
@@ -62,7 +62,6 @@ export function WorkspaceLauncherShell({
activeProjectAgentResults,
setAgentResults: setActiveProjectAgentResults,
resetLauncherHomeDraft,
createHomeDraft,
createHomeDraftAutomatically,
openProject,
} = homeProject;
@@ -259,7 +258,6 @@ export function WorkspaceLauncherShell({
onStatusChange={setStatus}
homeAgentModeItems={homeAgentModeItems}
recentProjectRows={recentProjectRows}
onCreateDraft={createHomeDraft}
onCreateDraftAutomatically={createHomeDraftAutomatically}
onProjectsOpen={() => setLauncherView('projects')}
onProjectOpen={(path) => {
@@ -300,6 +298,7 @@ export function WorkspaceLauncherShell({
initialProjectPath={currentProjectContext.projectPath}
initialProjectManifest={currentProjectContext.manifest}
initialProjectKind={currentProjectContext.projectKind}
initialSupervisorMessage={currentProjectContext.initialPrompt}
orchestrationMode="single-supervisor"
projectSupervisorOnly
onManifestChange={syncActiveProjectManifest}
@@ -35,6 +35,7 @@ export type ProjectSupervisorComponentProps = {
initialProjectPath?: string;
initialProjectManifest?: GameCreationAppManifest;
initialProjectKind?: 'web' | 'godot';
initialSupervisorMessage?: string;
orchestrationMode?: 'single-supervisor' | 'professional-dag';
projectSupervisorOnly?: boolean;
onManifestChange?: (
@@ -34,11 +34,7 @@ import type {
ProjectAgentResultSummary,
ProjectAgentRuntimeSummary,
} from '../../view/project-development';
import {
ensureProjectSupervisorActiveSessionId,
projectNameFromPath,
submitProjectSupervisorRuntimeTask,
} from '../agent-runtime';
import { projectNameFromPath } from '../agent-runtime';
import {
isAbsoluteProjectPath,
projectPathHasControlCharacter,
@@ -174,22 +170,11 @@ export function useHomeProjectCreation({
result.projectPath,
attachments,
);
const sessionId = await ensureProjectSupervisorActiveSessionId(
invoke,
result.projectPath,
const conversationContent = buildHomeConversationContent(
mode,
prompt,
attachments,
);
if (!sessionId) {
throw new Error('项目总控 Agent active Session 不可用');
}
await submitProjectSupervisorRuntimeTask({
invoke,
projectPath: result.projectPath,
sessionId,
prompt: buildHomeConversationContent(mode, prompt, attachments),
runtime: null,
runProfile: 'autonomous-game-build',
source: 'project-supervisor-game-chat',
});
enterProjectDevelopment({
projectPath: result.projectPath,
projectName:
@@ -202,7 +187,7 @@ export function useHomeProjectCreation({
),
mode,
initialPrompt:
prompt.trim() ||
conversationContent.trim() ||
(attachments.length > 0
? '用户上传了参考附件,等待后续补充需求。'
: ''),
@@ -265,7 +250,7 @@ export function useHomeProjectCreation({
prompt,
attachments,
);
setStatus('已创建项目并交给项目总控 Agent');
setStatus('已创建项目,正在开始智能创作');
} catch (error) {
throw new Error(
`项目已创建;首条需求投递失败:${
@@ -291,11 +276,11 @@ export function useHomeProjectCreation({
if (!invoke) {
throw new Error('需要在 Tauri App 内运行');
}
setStatus('正在启动项目总控 Agent');
setStatus('正在启动智能创作');
try {
await enterCreatedHomeProject(invoke, result, mode, prompt, attachments);
setStatus('已自动创建工作区并开始作');
return '已自动创建工作区并开始作';
setStatus('已自动创建工作区并开始智能创作');
return '已自动创建工作区并开始智能创作';
} catch (error) {
const message = `工作区已创建;首条需求投递失败:${
error instanceof Error ? error.message : String(error)
@@ -26,6 +26,7 @@ type RuntimePanelProps = ComponentProps<typeof ProjectSupervisorRuntimePanel>;
type ProjectSupervisorViewProps = RuntimePanelProps & {
chatInput: string;
directCodex?: boolean;
hiddenConversationCount: number;
needsUserInput: boolean;
onCancelConfirmation: () => void;
@@ -44,6 +45,7 @@ type ProjectSupervisorViewProps = RuntimePanelProps & {
export function ProjectSupervisorView({
chatInput,
directCodex = false,
hiddenConversationCount,
needsUserInput,
onCancelConfirmation,
@@ -99,7 +101,9 @@ export function ProjectSupervisorView({
</p>
) : null}
</div>
<ProjectSupervisorRuntimePanel {...runtimePanelProps} />
{directCodex ? null : (
<ProjectSupervisorRuntimePanel {...runtimePanelProps} />
)}
{pendingConfirmation ? (
<div className="pending-command">
<span>
@@ -120,7 +124,11 @@ export function ProjectSupervisorView({
disabled={runtimePanelProps.controlBusy || needsUserInput}
rows={3}
value={chatInput}
placeholder="告诉项目总控接下来要做什么"
placeholder={
directCodex
? '告诉陶泥儿接下来要做什么'
: '告诉项目总控接下来要做什么'
}
onChange={(event) => onChatInputChange(event.currentTarget.value)}
onKeyDown={(event) => {
if (
@@ -24,7 +24,6 @@ import {
import { resolveTauriInvoke } from '../../app/tauri';
import {
type GameCreatorAgentLlmConfig,
type GameCreatorAgentMode,
type GameCreatorAppConfig,
type GameCreatorAppConfigView,
type GameCreatorLlmApiKind,
@@ -69,10 +68,10 @@ const defaultRuntimeConfigDraft: GameCreatorAppConfig = {
agentMode: 'codex_app_server',
llm: {
apiKey: '',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-4.1',
baseUrl: 'https://dev.genarrative.world/gpt/v1',
model: 'gpt-5.6-sol',
apiKind: 'openai_responses',
reasoningEffort: 'high',
reasoningEffort: 'max',
stream: false,
webSearchEnabled: false,
contextWindowTokens: 128000,
@@ -90,14 +89,13 @@ const defaultRuntimeConfigDraft: GameCreatorAppConfig = {
mcpServers: {},
};
const runtimeAgentModes = new Set<GameCreatorAgentMode>([
'codex_app_server',
'codex_cli',
'provider',
]);
type RuntimeSettingsSection = 'general' | 'agents' | 'connections' | 'advanced';
type RuntimeConfigToast = {
tone: 'success' | 'error';
message: string;
};
const runtimeSettingsSections = [
{
id: 'general',
@@ -579,6 +577,8 @@ export function RuntimeConfigDialog({
}) {
const [runtimeConfigPath, setRuntimeConfigPath] = useState('');
const [runtimeConfigStatus, setRuntimeConfigStatus] = useState('未读取');
const [runtimeConfigToast, setRuntimeConfigToast] =
useState<RuntimeConfigToast | null>(null);
const [runtimeConfigDraft, setRuntimeConfigDraft] =
useState<GameCreatorAppConfig>(defaultRuntimeConfigDraft);
const [runtimeConfigBusy, setRuntimeConfigBusy] = useState(false);
@@ -615,10 +615,6 @@ export function RuntimeConfigDialog({
}));
}
function updateRuntimeAgentMode(agentMode: GameCreatorAgentMode) {
setRuntimeConfigDraft((current) => ({ ...current, agentMode }));
}
function updateRuntimeLlmProviderPreset(
presetId: RuntimeLlmProviderPresetId,
) {
@@ -830,10 +826,12 @@ export function RuntimeConfigDialog({
runtimeConfigBusyRef.current = true;
setRuntimeConfigBusy(true);
setRuntimeConfigToast(null);
setRuntimeConfigStatus('正在保存');
try {
const config = normalizeRuntimeConfigDraft({
...runtimeConfigDraft,
agentMode: 'codex_app_server',
mcpServers: materializeRuntimeMcpServers(
runtimeConfigDraft.mcpServers,
mcpStructuredDrafts,
@@ -851,11 +849,20 @@ export function RuntimeConfigDialog({
);
setMcpCatalog(null);
setRuntimeConfigStatus(`已保存:${result.path}`);
setRuntimeConfigToast({
tone: 'success',
message: '保存成功,新的运行时配置已生效',
});
onLog?.('runtime_config.save');
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setRuntimeConfigStatus(
error instanceof Error ? error.message : String(error),
message,
);
setRuntimeConfigToast({
tone: 'error',
message: `保存失败:${message}`,
});
} finally {
runtimeConfigBusyRef.current = false;
setRuntimeConfigBusy(false);
@@ -933,6 +940,24 @@ export function RuntimeConfigDialog({
role="presentation"
onMouseDown={(event) => closeDialogOnBackdropMouseDown(event, onClose)}
>
{runtimeConfigToast ? (
<div
className="runtime-settings-toast"
data-tone={runtimeConfigToast.tone}
role={runtimeConfigToast.tone === 'error' ? 'alert' : 'status'}
aria-label="运行时配置提示"
aria-live={
runtimeConfigToast.tone === 'error' ? 'assertive' : 'polite'
}
>
{runtimeConfigToast.tone === 'error' ? (
<CircleAlert size={16} aria-hidden="true" />
) : (
<CheckCircle2 size={16} aria-hidden="true" />
)}
<span>{runtimeConfigToast.message}</span>
</div>
) : null}
<form
className="settings-panel runtime-settings-panel"
role="dialog"
@@ -1004,27 +1029,11 @@ export function RuntimeConfigDialog({
<div className="settings-grid runtime-settings-fields">
{activeSection === 'general' ? (
<>
<label>
Agent
<select
aria-label="Agent 模式"
value={runtimeConfigDraft.agentMode}
onChange={(event) =>
updateRuntimeAgentMode(
event.currentTarget.value as GameCreatorAgentMode,
)
}
>
{!runtimeAgentModes.has(runtimeConfigDraft.agentMode) ? (
<option value={runtimeConfigDraft.agentMode}>
{runtimeConfigDraft.agentMode}
</option>
) : null}
<option value="codex_app_server">Codex App Server</option>
<option value="codex_cli">Codex CLI</option>
<option value="provider">HTTP Provider</option>
</select>
</label>
<div className="runtime-settings-readonly-field">
<span>Agent </span>
<strong></strong>
<small></small>
</div>
{runtimeConfigDraft.agentMode !== 'codex_cli' ? (
<>
<label>
+62 -43
View File
@@ -3369,6 +3369,36 @@ h2 {
backdrop-filter: blur(6px);
}
.runtime-settings-toast {
position: absolute;
top: 24px;
right: 24px;
z-index: 1;
display: inline-flex;
align-items: center;
gap: 8px;
max-width: min(420px, calc(100vw - 48px));
padding: 11px 14px;
border: 1px solid var(--platform-success-border);
border-radius: 12px;
background: var(--platform-success-fill);
box-shadow: var(--platform-panel-shadow);
color: var(--platform-success-text);
font-size: 12px;
font-weight: 700;
pointer-events: none;
}
.runtime-settings-toast[data-tone='error'] {
border-color: var(--platform-warm-border);
background: var(--platform-warm-bg);
color: var(--platform-warm-text);
}
.runtime-settings-toast span {
overflow-wrap: anywhere;
}
.settings-panel {
display: grid;
gap: 10px;
@@ -4614,6 +4644,13 @@ iframe.preview-frame {
padding: 0;
}
.runtime-settings-toast {
top: 12px;
right: 12px;
left: 12px;
max-width: none;
}
.runtime-settings-panel {
width: 100%;
height: 100dvh;
@@ -4685,7 +4722,7 @@ iframe.preview-frame {
max-width: none;
min-height: 0;
gap: 10px;
padding: 40px 8px 8px;
padding: 72px 8px 8px;
overflow: hidden;
color: var(--platform-text-base);
}
@@ -5660,6 +5697,23 @@ iframe.preview-frame {
user-select: text;
}
.game-resource-code-body {
min-width: 0;
overflow: auto;
border: 1px solid #eadbd4;
border-radius: 12px;
background: #2c2021;
color: #ffe8d8;
}
.game-resource-code-body pre {
min-width: max-content;
margin: 0;
padding: 14px;
font: 12px/1.55 ui-monospace, SFMono-Regular, Consolas, monospace;
white-space: pre;
}
.game-resource-document-body > :first-child {
margin-top: 0;
}
@@ -5765,8 +5819,10 @@ iframe.preview-frame {
.game-run-surface {
display: grid;
grid-template-rows: minmax(300px, 1fr) auto auto;
grid-template-rows: minmax(300px, 1fr) auto;
grid-row: 2 / -1;
gap: 12px;
height: 100%;
min-height: 0;
padding: 12px;
background: #fffdfa;
@@ -5774,10 +5830,7 @@ iframe.preview-frame {
.game-run-preview {
position: relative;
display: grid;
align-content: center;
justify-items: center;
gap: 9px;
display: block;
min-height: 300px;
border: 1px dashed #dfb59f;
border-radius: 14px;
@@ -5788,9 +5841,11 @@ iframe.preview-frame {
}
.game-run-preview iframe {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
min-height: 300px;
min-height: 0;
border: 0;
background: #fff;
}
@@ -5817,33 +5872,6 @@ iframe.preview-frame {
overflow-wrap: anywhere;
}
.game-run-slice-controls {
display: grid;
grid-template-columns: auto auto 1fr auto;
align-items: center;
gap: 8px;
}
.game-run-slice-controls button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
min-height: 34px;
padding: 0 10px;
border: 1px solid #e7d3c9;
border-radius: 999px;
background: #fff;
color: #855c4c;
cursor: pointer;
}
.game-run-slice-controls span {
color: #9a7d70;
font-size: 11px;
text-align: center;
}
.game-run-panels {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -6612,15 +6640,6 @@ iframe.preview-frame {
min-width: 460px;
}
.game-run-slice-controls {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.game-run-slice-controls span {
grid-column: 1 / -1;
grid-row: 1;
}
.game-run-panels {
grid-template-columns: 1fr;
}
@@ -46,7 +46,6 @@ type HomeViewProps = {
onStatusChange: (status: string) => void;
homeAgentModeItems: readonly HomeAgentModeItem[];
recentProjectRows: readonly HomeProjectRow[];
onCreateDraft: (draft: HomeDraft) => Promise<string>;
onCreateDraftAutomatically: (draft: HomeDraft) => Promise<string>;
onProjectsOpen: () => void;
onProjectOpen: (path: string) => void;
@@ -59,7 +58,6 @@ export default function HomeView({
onStatusChange,
homeAgentModeItems,
recentProjectRows,
onCreateDraft,
onCreateDraftAutomatically,
onProjectsOpen,
onProjectOpen,
@@ -118,7 +116,7 @@ export default function HomeView({
function handleHomeSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
void createFromHome(onCreateDraft, '请选择项目目录');
void createFromHome(onCreateDraftAutomatically, '正在创建工作区');
}
return (

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