合并主分支并重放资源栏目改动
合入已审查修复的 Game Agent 资源画板主线基线 保留资源栏目分页、悬浮导航与无限坐标交互 保留项目资源预览刷新与生成入口回归覆盖
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@genarrative/ai-game-creator-shell",
|
||||
"private": true,
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "node scripts/start-tauri-dev.mjs",
|
||||
|
||||
@@ -1733,12 +1733,12 @@ if (
|
||||
}
|
||||
|
||||
if (
|
||||
tauriConfig.version !== '0.1.1' ||
|
||||
packageConfig.version !== '0.1.1' ||
|
||||
cargoPackageVersion !== '0.1.1'
|
||||
tauriConfig.version !== '0.1.2' ||
|
||||
packageConfig.version !== '0.1.2' ||
|
||||
cargoPackageVersion !== '0.1.2'
|
||||
) {
|
||||
throw new Error(
|
||||
'AI game creator standard release must remain version 0.1.1 while game-chat uses its dedicated version',
|
||||
'AI game creator standard release must remain version 0.1.2 while game-chat uses its dedicated version',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1695,7 +1695,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "genarrative-ai-game-creator-shell"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
dependencies = [
|
||||
"agent-runtime-core",
|
||||
"axum",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "genarrative-ai-game-creator-shell"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ const DIRECT_CODEX_ART_ASSET_PATHS: [&str; 3] = [
|
||||
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
|
||||
DIRECT_CODEX_SPRITESHEET_ASSET_PATH,
|
||||
];
|
||||
const DIRECT_CODEX_ART_AGENT_ID: &str = "direct-codex-art";
|
||||
const DIRECT_CODEX_GAME_OUTPUTS: [(&str, &str, &str); 3] = [
|
||||
("game/index.html", "game-entry", "text/html"),
|
||||
("game/style.css", "game-style", "text/css"),
|
||||
@@ -1872,7 +1873,7 @@ fn direct_taonier_art_generation_runtime_context(
|
||||
_ => return Err("直连美术生成请求包含未声明的输出路径".to_string()),
|
||||
};
|
||||
Ok(PlatformArtGenerationRuntimeContext {
|
||||
agent_id: "direct-codex-art".to_string(),
|
||||
agent_id: DIRECT_CODEX_ART_AGENT_ID.to_string(),
|
||||
task_id: format!("direct-codex-art-{stage}"),
|
||||
session_id: project_id,
|
||||
run_id: stage.to_string(),
|
||||
@@ -2607,6 +2608,7 @@ async fn recover_direct_taonier_spritesheet_read_only_at(
|
||||
json_string_field(resource, "objectKey").as_deref(),
|
||||
&asset_object_id,
|
||||
)?;
|
||||
emit_game_creator_manifest_invalidated(root, DIRECT_CODEX_ART_AGENT_ID);
|
||||
emit_direct_game_creator_progress(
|
||||
root,
|
||||
"art.spritesheet.recovered",
|
||||
@@ -2744,6 +2746,7 @@ async fn generate_direct_taonier_art_asset_at(
|
||||
}
|
||||
Err(error) => return Err(direct_taonier_art_generation_failure(asset_label, error)),
|
||||
};
|
||||
emit_game_creator_manifest_invalidated(root, DIRECT_CODEX_ART_AGENT_ID);
|
||||
if generated.asset.local_path != output_path {
|
||||
return Err(format!(
|
||||
"陶泥儿美术包生成返回后未形成可用的已登记平台素材合同 {output_path},已终止代码生成"
|
||||
|
||||
@@ -20,8 +20,7 @@ pub(crate) use canvas_generation::{
|
||||
external_generation_submit_rejection_is_definitive,
|
||||
platform_art_generation_error_needs_reconciliation, prepare_external_canvas_generation_context,
|
||||
resolve_canvas_resource_download_with_access, submit_external_generation_request,
|
||||
wait_for_external_generation_result, wait_for_external_generation_result_with_access,
|
||||
ExternalCanvasGenerationContext,
|
||||
wait_for_external_generation_result_with_access, ExternalCanvasGenerationContext,
|
||||
ExternalGenerationInitialResponse,
|
||||
};
|
||||
pub(in crate::agent) use canvas_generation::{
|
||||
|
||||
@@ -261,10 +261,11 @@ pub(crate) use entrypoints::{
|
||||
chat_with_game_creator_role_agent_stream_at,
|
||||
chat_with_game_creator_role_agent_stream_for_session_at,
|
||||
configure_game_creator_manifest_invalidation_event_sink, emit_direct_game_creator_progress,
|
||||
emit_game_creator_agent_runtime_update, game_creator_agent_runtime_update_event,
|
||||
generate_local_game_draft_at, install_game_creator_manifest_invalidation_event_sink,
|
||||
read_game_creator_agent_runtime_at, read_game_creator_agent_runtime_for_session_at,
|
||||
read_game_creator_agent_runtimes_at, set_game_creator_agent_runtime_update_app_handle,
|
||||
emit_game_creator_agent_runtime_update, emit_game_creator_manifest_invalidated,
|
||||
game_creator_agent_runtime_update_event, generate_local_game_draft_at,
|
||||
install_game_creator_manifest_invalidation_event_sink, read_game_creator_agent_runtime_at,
|
||||
read_game_creator_agent_runtime_for_session_at, read_game_creator_agent_runtimes_at,
|
||||
set_game_creator_agent_runtime_update_app_handle,
|
||||
start_game_creator_manifest_invalidation_event_sink,
|
||||
validate_game_creator_manifest_invalidation_event_sink,
|
||||
};
|
||||
|
||||
@@ -717,6 +717,14 @@ pub(crate) async fn generate_local_project_asset_canvas_image(
|
||||
Ok(execution.result)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn finalize_local_project_asset_canvas_generation_failure(
|
||||
input: FinalizeAssetCanvasGenerationFailureInput,
|
||||
) -> Result<FinalizeAssetCanvasGenerationFailureResult, String> {
|
||||
let root = validated_local_project_directory_path(input.project_path.trim())?;
|
||||
finalize_asset_canvas_generation_failure_at(&root, &input).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn recover_local_project_asset_canvas_generations(
|
||||
app: tauri::AppHandle,
|
||||
|
||||
@@ -2370,6 +2370,7 @@ fn main() {
|
||||
import_local_project_asset_canvas_images,
|
||||
store_local_project_asset_canvas_media,
|
||||
stage_local_project_asset_canvas_image,
|
||||
finalize_local_project_asset_canvas_generation_failure,
|
||||
archive_failed_local_project_asset_canvas_generation,
|
||||
generate_local_project_asset_canvas_image,
|
||||
recover_local_project_asset_canvas_generations,
|
||||
|
||||
@@ -2216,7 +2216,7 @@ fn stage_asset_canvas_image_with_token_at(
|
||||
})
|
||||
}
|
||||
|
||||
fn rebind_asset_canvas_staged_image_revision_at(
|
||||
fn rebind_asset_canvas_staged_image_revision_locked(
|
||||
root: &Path,
|
||||
staged_image_token: &str,
|
||||
expected_project_id: &str,
|
||||
@@ -2224,11 +2224,6 @@ fn rebind_asset_canvas_staged_image_revision_at(
|
||||
expected_previous_revision: u64,
|
||||
next_revision: u64,
|
||||
) -> Result<(), String> {
|
||||
validate_uuid_v4(staged_image_token, "stagedImageToken")?;
|
||||
validate_safe_revision(expected_previous_revision, "staging 原草稿 revision")?;
|
||||
validate_safe_revision(next_revision, "staging 新草稿 revision")?;
|
||||
validate_asset_canvas_project_identity(root, expected_project_id)?;
|
||||
let _lock = acquire_asset_canvas_draft_lock(root)?;
|
||||
let manifest = validate_asset_canvas_project_identity(root, expected_project_id)?;
|
||||
let draft = read_asset_canvas_draft_locked(root, &manifest.project_id, draft_id)?
|
||||
.ok_or_else(|| "素材画布草稿不存在".to_string())?;
|
||||
@@ -2255,6 +2250,29 @@ fn rebind_asset_canvas_staged_image_revision_at(
|
||||
)
|
||||
}
|
||||
|
||||
fn rebind_asset_canvas_staged_image_revision_at(
|
||||
root: &Path,
|
||||
staged_image_token: &str,
|
||||
expected_project_id: &str,
|
||||
draft_id: &str,
|
||||
expected_previous_revision: u64,
|
||||
next_revision: u64,
|
||||
) -> Result<(), String> {
|
||||
validate_uuid_v4(staged_image_token, "stagedImageToken")?;
|
||||
validate_safe_revision(expected_previous_revision, "staging 原草稿 revision")?;
|
||||
validate_safe_revision(next_revision, "staging 新草稿 revision")?;
|
||||
validate_asset_canvas_project_identity(root, expected_project_id)?;
|
||||
let _lock = acquire_asset_canvas_draft_lock(root)?;
|
||||
rebind_asset_canvas_staged_image_revision_locked(
|
||||
root,
|
||||
staged_image_token,
|
||||
expected_project_id,
|
||||
draft_id,
|
||||
expected_previous_revision,
|
||||
next_revision,
|
||||
)
|
||||
}
|
||||
|
||||
fn draft_media_relative_path(
|
||||
draft_id: &str,
|
||||
media_ref: &AssetCanvasMediaRef,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -129,6 +129,8 @@ fn initialize_refine_fixture_with_resource_id(resource_id: Option<&str>) -> Fixt
|
||||
kind: "illustration".to_string(),
|
||||
media_type: "image/png".to_string(),
|
||||
local_path: "assets/source.png".to_string(),
|
||||
image_sequence_frames: None,
|
||||
image_sequence_duration_ms: None,
|
||||
source: GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Uploaded,
|
||||
canvas_project_id: None,
|
||||
@@ -748,6 +750,8 @@ fn legacy_refine_transaction_preserves_runtime_entry_for_next_commit() {
|
||||
kind: "illustration".to_string(),
|
||||
media_type: "image/png".to_string(),
|
||||
local_path: "assets/source.png".to_string(),
|
||||
image_sequence_frames: None,
|
||||
image_sequence_duration_ms: None,
|
||||
source: GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Uploaded,
|
||||
canvas_project_id: None,
|
||||
|
||||
@@ -14,6 +14,8 @@ use std::path::Path;
|
||||
const PROJECT_TEXT_PREVIEW_MAX_FILE_BYTES: u64 = 2 * 1024 * 1024;
|
||||
const PROJECT_MEDIA_PREVIEW_MAX_FILE_BYTES: u64 = 32 * 1024 * 1024;
|
||||
const PROJECT_RESOURCE_PREVIEW_READ_CHUNK_BYTES: usize = 64 * 1024;
|
||||
const PROJECT_MEDIA_PREVIEW_MAX_DIMENSION: u32 = 8_192;
|
||||
const PROJECT_MEDIA_PREVIEW_MAX_PIXELS: u64 = 32 * 1024 * 1024;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -30,6 +32,10 @@ pub(crate) struct LocalProjectMediaPreview {
|
||||
pub(crate) path: String,
|
||||
pub(crate) media_type: String,
|
||||
pub(crate) byte_len: u64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) pixel_width: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) pixel_height: Option<u32>,
|
||||
pub(crate) data_url: String,
|
||||
}
|
||||
|
||||
@@ -196,14 +202,135 @@ pub(crate) fn load_local_project_media_preview_with_cancellation(
|
||||
cancellation.check()?;
|
||||
let media_type = detect_project_media_type(&normalized, &bytes, kind)?;
|
||||
cancellation.check()?;
|
||||
let dimensions = (kind == ProjectMediaPreviewKind::Art)
|
||||
.then(|| detect_project_art_dimensions(&bytes, media_type))
|
||||
.flatten();
|
||||
Ok(LocalProjectMediaPreview {
|
||||
path: normalized,
|
||||
media_type: media_type.to_string(),
|
||||
byte_len: bytes.len() as u64,
|
||||
pixel_width: dimensions.map(|(width, _)| width),
|
||||
pixel_height: dimensions.map(|(_, height)| height),
|
||||
data_url: encode_project_resource_preview_data_url(media_type, &bytes, cancellation)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn detect_project_art_dimensions(bytes: &[u8], media_type: &str) -> Option<(u32, u32)> {
|
||||
let dimensions = match media_type {
|
||||
"image/gif" => detect_gif_dimensions(bytes),
|
||||
"image/bmp" => detect_bmp_dimensions(bytes),
|
||||
"image/avif" => detect_ispe_dimensions(bytes),
|
||||
"image/svg+xml" => detect_svg_dimensions(bytes),
|
||||
_ => None,
|
||||
}?;
|
||||
let (width, height) = dimensions;
|
||||
let pixels = u64::from(width).checked_mul(u64::from(height))?;
|
||||
if width == 0
|
||||
|| height == 0
|
||||
|| width > PROJECT_MEDIA_PREVIEW_MAX_DIMENSION
|
||||
|| height > PROJECT_MEDIA_PREVIEW_MAX_DIMENSION
|
||||
|| pixels > PROJECT_MEDIA_PREVIEW_MAX_PIXELS
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some((width, height))
|
||||
}
|
||||
|
||||
fn detect_gif_dimensions(bytes: &[u8]) -> Option<(u32, u32)> {
|
||||
if bytes.len() < 10 || !matches!(&bytes[..6], b"GIF87a" | b"GIF89a") {
|
||||
return None;
|
||||
}
|
||||
Some((
|
||||
u32::from(u16::from_le_bytes([bytes[6], bytes[7]])),
|
||||
u32::from(u16::from_le_bytes([bytes[8], bytes[9]])),
|
||||
))
|
||||
}
|
||||
|
||||
fn detect_bmp_dimensions(bytes: &[u8]) -> Option<(u32, u32)> {
|
||||
if bytes.len() < 26 || &bytes[..2] != b"BM" {
|
||||
return None;
|
||||
}
|
||||
let width = i32::from_le_bytes(bytes[18..22].try_into().ok()?);
|
||||
let height = i32::from_le_bytes(bytes[22..26].try_into().ok()?);
|
||||
Some((width.unsigned_abs(), height.unsigned_abs()))
|
||||
}
|
||||
|
||||
fn detect_ispe_dimensions(bytes: &[u8]) -> Option<(u32, u32)> {
|
||||
let marker = b"ispe";
|
||||
let index = bytes
|
||||
.windows(marker.len())
|
||||
.position(|window| window == marker)?;
|
||||
if index < 4 || index.checked_add(16)? > bytes.len() {
|
||||
return None;
|
||||
}
|
||||
Some((
|
||||
u32::from_be_bytes(bytes[index + 8..index + 12].try_into().ok()?),
|
||||
u32::from_be_bytes(bytes[index + 12..index + 16].try_into().ok()?),
|
||||
))
|
||||
}
|
||||
|
||||
fn detect_svg_dimensions(bytes: &[u8]) -> Option<(u32, u32)> {
|
||||
let text = std::str::from_utf8(bytes).ok()?;
|
||||
let lower = text.to_ascii_lowercase();
|
||||
let start = lower.find("<svg")?;
|
||||
let end = lower[start..].find('>')? + start;
|
||||
let root = &text[start..end];
|
||||
let width = svg_length_attribute(root, "width");
|
||||
let height = svg_length_attribute(root, "height");
|
||||
if let (Some(width), Some(height)) = (width, height) {
|
||||
return Some((width, height));
|
||||
}
|
||||
let view_box = svg_attribute(root, "viewbox")?;
|
||||
let mut values = view_box
|
||||
.split(|character: char| character.is_ascii_whitespace() || character == ',')
|
||||
.filter_map(|value| value.parse::<f64>().ok());
|
||||
let _min_x = values.next()?;
|
||||
let _min_y = values.next()?;
|
||||
let width = values.next()?;
|
||||
let height = values.next()?;
|
||||
Some((safe_svg_dimension(width)?, safe_svg_dimension(height)?))
|
||||
}
|
||||
|
||||
fn svg_length_attribute(root: &str, name: &str) -> Option<u32> {
|
||||
let value = svg_attribute(root, name)?;
|
||||
let value = value.trim().strip_suffix("px").unwrap_or(value.trim());
|
||||
safe_svg_dimension(value.parse::<f64>().ok()?)
|
||||
}
|
||||
|
||||
fn svg_attribute<'a>(root: &'a str, name: &str) -> Option<&'a str> {
|
||||
let lower = root.to_ascii_lowercase();
|
||||
let mut offset = 0;
|
||||
while let Some(relative) = lower[offset..].find(name) {
|
||||
let start = offset + relative;
|
||||
let before = start
|
||||
.checked_sub(1)
|
||||
.and_then(|index| lower.as_bytes().get(index));
|
||||
let after = lower.as_bytes().get(start + name.len());
|
||||
if before.is_some_and(|byte| byte.is_ascii_whitespace())
|
||||
&& after.is_some_and(|byte| *byte == b'=' || byte.is_ascii_whitespace())
|
||||
{
|
||||
let rest = root[start + name.len()..].trim_start();
|
||||
let rest = rest.strip_prefix('=')?.trim_start();
|
||||
let quote = rest.chars().next()?;
|
||||
if quote == '\'' || quote == '"' {
|
||||
let value = &rest[quote.len_utf8()..];
|
||||
let end = value.find(quote)?;
|
||||
return Some(&value[..end]);
|
||||
}
|
||||
}
|
||||
offset = start + name.len();
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn safe_svg_dimension(value: f64) -> Option<u32> {
|
||||
if !value.is_finite() || value <= 0.0 || value > f64::from(PROJECT_MEDIA_PREVIEW_MAX_DIMENSION)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(value.round() as u32)
|
||||
}
|
||||
|
||||
fn encode_project_resource_preview_data_url(
|
||||
media_type: &str,
|
||||
bytes: &[u8],
|
||||
@@ -539,6 +666,8 @@ mod tests {
|
||||
)
|
||||
.expect("safe svg");
|
||||
assert_eq!(preview.media_type, "image/svg+xml");
|
||||
assert_eq!(preview.pixel_width, None);
|
||||
assert_eq!(preview.pixel_height, None);
|
||||
assert!(preview.data_url.starts_with("data:image/svg+xml;base64,"));
|
||||
assert!(load_local_project_media_preview(
|
||||
root.path(),
|
||||
@@ -554,6 +683,54 @@ mod tests {
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn media_preview_reports_safe_dimensions_for_extended_art_images() {
|
||||
let root = tempfile::tempdir().expect("temp root");
|
||||
fs::create_dir_all(root.path().join("assets")).expect("assets dir");
|
||||
|
||||
let mut gif = b"GIF89a".to_vec();
|
||||
gif.extend_from_slice(&320_u16.to_le_bytes());
|
||||
gif.extend_from_slice(&180_u16.to_le_bytes());
|
||||
gif.extend_from_slice(&[0; 4]);
|
||||
fs::write(root.path().join("assets/scene.gif"), gif).expect("gif");
|
||||
|
||||
let mut bmp = vec![0_u8; 26];
|
||||
bmp[..2].copy_from_slice(b"BM");
|
||||
bmp[18..22].copy_from_slice(&640_i32.to_le_bytes());
|
||||
bmp[22..26].copy_from_slice(&(-360_i32).to_le_bytes());
|
||||
fs::write(root.path().join("assets/scene.bmp"), bmp).expect("bmp");
|
||||
|
||||
let mut avif = vec![0_u8; 32];
|
||||
avif[4..8].copy_from_slice(b"ftyp");
|
||||
avif[8..12].copy_from_slice(b"avif");
|
||||
avif[16..20].copy_from_slice(b"ispe");
|
||||
avif[24..28].copy_from_slice(&1920_u32.to_be_bytes());
|
||||
avif[28..32].copy_from_slice(&1080_u32.to_be_bytes());
|
||||
fs::write(root.path().join("assets/scene.avif"), avif).expect("avif");
|
||||
|
||||
fs::write(
|
||||
root.path().join("assets/scene.svg"),
|
||||
r#"<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600"><path d="M0 0"/></svg>"#,
|
||||
)
|
||||
.expect("svg");
|
||||
|
||||
for (path, expected_type, expected_dimensions) in [
|
||||
("assets/scene.gif", "image/gif", Some((320, 180))),
|
||||
("assets/scene.bmp", "image/bmp", Some((640, 360))),
|
||||
("assets/scene.avif", "image/avif", Some((1920, 1080))),
|
||||
("assets/scene.svg", "image/svg+xml", Some((800, 600))),
|
||||
] {
|
||||
let preview =
|
||||
load_local_project_media_preview(root.path(), path, ProjectMediaPreviewKind::Art)
|
||||
.expect("extended image preview");
|
||||
assert_eq!(preview.media_type, expected_type);
|
||||
assert_eq!(
|
||||
preview.pixel_width.zip(preview.pixel_height),
|
||||
expected_dimensions
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn resource_preview_rejects_symlink_and_hardlink_files() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Genarrative AI Game Creator",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.2",
|
||||
"identifier": "world.genarrative.ai-game-creator",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm --prefix ../.. run agc:serve",
|
||||
|
||||
@@ -6418,6 +6418,7 @@ export function App({
|
||||
}
|
||||
} finally {
|
||||
setChatAgentBusy(false);
|
||||
setDirectCodexProgress('');
|
||||
const activeTurn = activeDirectCodexTurnRef.current;
|
||||
if (
|
||||
!activeTurn ||
|
||||
|
||||
@@ -365,7 +365,11 @@ export interface AgentRuntimeResult {
|
||||
}
|
||||
|
||||
export type AgentRuntimeResponseStreamStatus =
|
||||
'streaming' | 'ready' | 'committed' | 'discarded' | 'failed';
|
||||
| 'streaming'
|
||||
| 'ready'
|
||||
| 'committed'
|
||||
| 'discarded'
|
||||
| 'failed';
|
||||
|
||||
export interface AgentRuntimeResponseStream {
|
||||
schemaVersion: string;
|
||||
@@ -486,13 +490,22 @@ export interface GameCreatorAgentLlmConfigStatus {
|
||||
}
|
||||
|
||||
export type GameCreatorLlmApiKind =
|
||||
'openai_responses' | 'openai_chat' | 'anthropic';
|
||||
| 'openai_responses'
|
||||
| 'openai_chat'
|
||||
| 'anthropic';
|
||||
export type GameCreatorAgentMode =
|
||||
'codex_app_server' | 'codex_cli' | 'provider';
|
||||
| 'codex_app_server'
|
||||
| 'codex_cli'
|
||||
| 'provider';
|
||||
export type RuntimeLlmProviderPresetId =
|
||||
'custom' | 'openai' | 'deepseek' | 'anthropic' | 'ark';
|
||||
| 'custom'
|
||||
| 'openai'
|
||||
| 'deepseek'
|
||||
| 'anthropic'
|
||||
| 'ark';
|
||||
export type RuntimeAgentLlmProviderPresetId =
|
||||
'inherit' | RuntimeLlmProviderPresetId;
|
||||
| 'inherit'
|
||||
| RuntimeLlmProviderPresetId;
|
||||
|
||||
export interface GameCreatorLlmConfig {
|
||||
apiKey: string;
|
||||
@@ -795,7 +808,12 @@ export interface AgentProgressEvent {
|
||||
}
|
||||
|
||||
export type GameCreatorDirectTurnUpdateStatus =
|
||||
'accepted' | 'running' | 'streaming' | 'finalizing' | 'completed' | 'failed';
|
||||
| 'accepted'
|
||||
| 'running'
|
||||
| 'streaming'
|
||||
| 'finalizing'
|
||||
| 'completed'
|
||||
| 'failed';
|
||||
|
||||
export type GameCreatorDirectTurnActivity =
|
||||
| 'request-accepted'
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -897,8 +897,8 @@
|
||||
}
|
||||
|
||||
.asset-canvas-surface__quick-edit-card {
|
||||
width: calc(100% - 16px);
|
||||
max-height: min(20rem, calc(100% - 16px));
|
||||
width: calc(100% - 24px);
|
||||
max-height: min(20rem, calc(100% - 24px));
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
|
||||
@@ -80,6 +80,15 @@ type GenerationCommandResult = {
|
||||
}>;
|
||||
};
|
||||
|
||||
type GenerationFailureCommandResult = {
|
||||
disposition:
|
||||
| 'failed'
|
||||
| 'reconciliation-required'
|
||||
| 'not-started'
|
||||
| 'already-terminal';
|
||||
draft: ImageCanvasDraft;
|
||||
};
|
||||
|
||||
type GenerationProgressEvent = ImageCanvasGenerationProgress & {
|
||||
schemaVersion: 'game-creator-asset-generation-progress.v1';
|
||||
projectId: string;
|
||||
@@ -249,6 +258,7 @@ export function createMockImageCanvasGenerationPort(): ImageCanvasGenerationPort
|
||||
message: '当前阶段使用明确 mock,不会提交真实 AI 生成请求',
|
||||
});
|
||||
return {
|
||||
settleGenerationFailure: unsupported,
|
||||
archiveFailedGeneration: unsupported,
|
||||
generateImage: unsupported,
|
||||
recoverImages: async () => ({
|
||||
@@ -328,6 +338,30 @@ export function createTauriImageCanvasHostAdapter(input: {
|
||||
);
|
||||
};
|
||||
const generation: ImageCanvasGenerationPort = {
|
||||
async settleGenerationFailure(failureInput) {
|
||||
try {
|
||||
const result = await invokeInput<GenerationFailureCommandResult>(
|
||||
'finalize_local_project_asset_canvas_generation_failure',
|
||||
{
|
||||
...baseScope(failureInput.scope),
|
||||
expectedDraftRevision: safeRevision(
|
||||
failureInput.expectedDraftRevision,
|
||||
'expectedDraftRevision',
|
||||
),
|
||||
generationId: failureInput.generationId,
|
||||
intentId: failureInput.intentId,
|
||||
errorCode: failureInput.errorCode,
|
||||
reconciliationRequired: failureInput.reconciliationRequired,
|
||||
},
|
||||
);
|
||||
return { status: 'ok', value: result };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return message.includes('draft-revision-conflict')
|
||||
? conflict('draft-revision', null, null)
|
||||
: failure(error);
|
||||
}
|
||||
},
|
||||
async archiveFailedGeneration(archiveInput) {
|
||||
try {
|
||||
const result = await invokeInput<{
|
||||
|
||||
+18
-18
@@ -201,10 +201,10 @@ function gameChatRuntimeActivityTimes(runtime: AgentRuntimeState) {
|
||||
function gameChatRuntimeIsExpectedWait(runtime: AgentRuntimeState) {
|
||||
return Boolean(
|
||||
runtime.pendingToolAction ||
|
||||
runtime.userInputRequest ||
|
||||
[runtime.status, runtime.phase, runtime.goalStatus].some((state) =>
|
||||
GAME_CHAT_EXPECTED_WAIT_STATES.has(state ?? ''),
|
||||
),
|
||||
runtime.userInputRequest ||
|
||||
[runtime.status, runtime.phase, runtime.goalStatus].some((state) =>
|
||||
GAME_CHAT_EXPECTED_WAIT_STATES.has(state ?? ''),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -984,11 +984,11 @@ export function SupervisorChatOnlyView({
|
||||
!directCodex && Boolean(expectedRunId && runtime?.runId !== expectedRunId);
|
||||
const running = Boolean(
|
||||
chatAgentBusy ||
|
||||
(!directCodex && synchronizingAcceptedRun) ||
|
||||
(runtime &&
|
||||
runtime.status !== 'needs-reconciliation' &&
|
||||
runtime.phase !== 'needs-reconciliation' &&
|
||||
!isAgentRuntimeTerminalState(runtime)),
|
||||
(!directCodex && synchronizingAcceptedRun) ||
|
||||
(runtime &&
|
||||
runtime.status !== 'needs-reconciliation' &&
|
||||
runtime.phase !== 'needs-reconciliation' &&
|
||||
!isAgentRuntimeTerminalState(runtime)),
|
||||
);
|
||||
const gameChatInterruptionText =
|
||||
!directCodex && gameChatMode && runtime
|
||||
@@ -1113,18 +1113,18 @@ export function SupervisorChatOnlyView({
|
||||
: null;
|
||||
const expectedRuntimeWait = Boolean(
|
||||
needsUserInput ||
|
||||
pendingConfirmation ||
|
||||
pendingCommand ||
|
||||
(activeRuntimeLanes.length > 0 &&
|
||||
activeRuntimeLanes.every(gameChatRuntimeIsExpectedWait)),
|
||||
pendingConfirmation ||
|
||||
pendingCommand ||
|
||||
(activeRuntimeLanes.length > 0 &&
|
||||
activeRuntimeLanes.every(gameChatRuntimeIsExpectedWait)),
|
||||
);
|
||||
const runtimeAppearsStalled = Boolean(
|
||||
running &&
|
||||
runtime &&
|
||||
!runtimeTerminal &&
|
||||
!expectedRuntimeWait &&
|
||||
inactiveRuntimeMs !== null &&
|
||||
inactiveRuntimeMs > GAME_CHAT_RUNTIME_STALL_THRESHOLD_MS,
|
||||
runtime &&
|
||||
!runtimeTerminal &&
|
||||
!expectedRuntimeWait &&
|
||||
inactiveRuntimeMs !== null &&
|
||||
inactiveRuntimeMs > GAME_CHAT_RUNTIME_STALL_THRESHOLD_MS,
|
||||
);
|
||||
const runStateLabel = (() => {
|
||||
if (gameChatInterruptionText) {
|
||||
|
||||
@@ -5052,6 +5052,13 @@ iframe.preview-frame {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.game-workbench-editor-shell {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: var(--game-workbench-stage-fill);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.game-workbench-stage > .game-resource-manager {
|
||||
grid-row: 3;
|
||||
min-height: 0;
|
||||
@@ -5062,13 +5069,6 @@ iframe.preview-frame {
|
||||
box-shadow: var(--platform-nav-active-shadow);
|
||||
}
|
||||
|
||||
.game-workbench-editor-shell {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: var(--game-workbench-stage-fill);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.game-workbench-stage[data-resource-view-state^='resources.asset-canvas'],
|
||||
.game-workbench-stage[data-resource-view-state='resources.ui-editor'] {
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
|
||||
@@ -46,8 +46,8 @@ import type {
|
||||
GameIterationVersion,
|
||||
ProjectResourceCanvasLayoutMode,
|
||||
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
|
||||
import { canonicalAssetBaseName } from '../../features/asset-canvas/assetCanvasNaming';
|
||||
import PRODUCT_LOGO from '../../../../../packages/shared/src/icons/taonier-product-ip.png';
|
||||
import { canonicalAssetBaseName } from '../../features/asset-canvas/assetCanvasNaming';
|
||||
import {
|
||||
type AssetCanvasCommitNotification,
|
||||
type AssetCanvasExitResult,
|
||||
@@ -201,6 +201,15 @@ type ResourceEditorRoute = {
|
||||
generationMode: 'create' | 'derive';
|
||||
};
|
||||
|
||||
function defaultCreatedResourceName(
|
||||
editKind: ResourceEditorRoute['capability']['editKind'],
|
||||
) {
|
||||
if (editKind === 'video') return '新视频';
|
||||
if (editKind === 'sound-effect') return '新音效';
|
||||
if (editKind === 'background-music') return '新背景音乐';
|
||||
return '新资源';
|
||||
}
|
||||
|
||||
type UiEditorRoute = {
|
||||
resourceId: string;
|
||||
resourceLabel: string;
|
||||
@@ -2361,7 +2370,7 @@ export default function ProjectDevelopmentView({
|
||||
}
|
||||
function closeOnEscape(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
restoreResourceListScrollRef.current = false;
|
||||
restoreResourceListScrollRef.current = true;
|
||||
setFocusedResourceId(null);
|
||||
}
|
||||
}
|
||||
@@ -3855,7 +3864,7 @@ export default function ProjectDevelopmentView({
|
||||
|
||||
function closeResourceFocus() {
|
||||
advanceFocusGeneration();
|
||||
restoreResourceListScrollRef.current = false;
|
||||
restoreResourceListScrollRef.current = true;
|
||||
setFocusedResourceId(null);
|
||||
}
|
||||
|
||||
@@ -4036,7 +4045,7 @@ export default function ProjectDevelopmentView({
|
||||
) : null}
|
||||
|
||||
{mode === 'resources' && uiEditorRoute ? (
|
||||
<div className="h-full min-h-0 min-w-0 w-full overflow-hidden rounded-2xl border border-(--platform-desktop-panel-border) bg-(--platform-desktop-panel-fill) shadow-(--platform-panel-shadow)">
|
||||
<div className="game-workbench-editor-shell h-full min-h-0 min-w-0 w-full overflow-hidden">
|
||||
<UiEditorPage
|
||||
projectPath={projectPath}
|
||||
resourceId={uiEditorRoute.resourceId}
|
||||
@@ -4062,28 +4071,19 @@ export default function ProjectDevelopmentView({
|
||||
) : mode === 'resources' && resourceEditorRoute ? (
|
||||
<ResourceEditSurface
|
||||
resourceLabel={
|
||||
resourceEditorRoute.resource
|
||||
? resourceEditorRoute.resource.label
|
||||
: ((
|
||||
{
|
||||
video: '新视频',
|
||||
'sound-effect': '新音效',
|
||||
'background-music': '新背景音乐',
|
||||
} as Record<string, string>
|
||||
)[resourceEditorRoute.capability.editKind] ?? '新资源')
|
||||
resourceEditorRoute.resource?.label ??
|
||||
defaultCreatedResourceName(
|
||||
resourceEditorRoute.capability.editKind,
|
||||
)
|
||||
}
|
||||
editKind={resourceEditorRoute.capability.editKind}
|
||||
generationMode={resourceEditorRoute.generationMode}
|
||||
initialAssetName={
|
||||
resourceEditorRoute.resource
|
||||
? defaultDerivedResourceName(resourceEditorRoute.resource)
|
||||
: ((
|
||||
{
|
||||
video: '新视频',
|
||||
'sound-effect': '新音效',
|
||||
'background-music': '新背景音乐',
|
||||
} as Record<string, string>
|
||||
)[resourceEditorRoute.capability.editKind] ?? '新资源')
|
||||
: defaultCreatedResourceName(
|
||||
resourceEditorRoute.capability.editKind,
|
||||
)
|
||||
}
|
||||
semanticNotice={resourceEditorRoute.capability.semanticNotice}
|
||||
onCancel={cancelResourceEditor}
|
||||
@@ -4119,7 +4119,9 @@ export default function ProjectDevelopmentView({
|
||||
) ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openCharacterAnimation(focusedResource)}
|
||||
onClick={() =>
|
||||
openCharacterAnimation(focusedResource)
|
||||
}
|
||||
>
|
||||
生成动画
|
||||
</button>
|
||||
|
||||
@@ -907,7 +907,8 @@ export function registerClientHomeTests() {
|
||||
];
|
||||
const streamedReply = '流式正文已经完整结束。';
|
||||
let streamHandler:
|
||||
((event: { payload: Record<string, unknown> }) => void) | null = null;
|
||||
| ((event: { payload: Record<string, unknown> }) => void)
|
||||
| null = null;
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'read_local_conversation') {
|
||||
@@ -1727,9 +1728,11 @@ export function registerHomeProjectCreationTests() {
|
||||
expect(persistedTurnCall).toBeGreaterThanOrEqual(0);
|
||||
expect(persistedTurnCall).toBeLessThan(directTurnCall);
|
||||
const persistedTurnArgs = invoke.mock.calls[persistedTurnCall]?.[1] as
|
||||
Record<string, unknown> | undefined;
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
const directTurnArgs = invoke.mock.calls[directTurnCall]?.[1] as
|
||||
Record<string, unknown> | undefined;
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
expect(persistedTurnArgs).toEqual({
|
||||
projectPath,
|
||||
agentId: null,
|
||||
|
||||
@@ -461,8 +461,8 @@ export function registerProjectConversationTests() {
|
||||
'未命名游戏原型',
|
||||
);
|
||||
let finishConversationRead:
|
||||
((value: { path: string; agentId: null; messages: [] }) => void) | null =
|
||||
null;
|
||||
| ((value: { path: string; agentId: null; messages: [] }) => void)
|
||||
| null = null;
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'append_local_permission_log') {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user