7afec12958
拆出 CLI、命令、配置、Agent、素材、项目、预览和窗口模块 拆出 Rust 单测并保留 main.rs 作为 Tauri 薄入口 扩展壳配置与原生壳门禁的 Rust 源码扫描范围 同步 AI 游戏创作 App 技术方案和决策记录
963 lines
28 KiB
Rust
963 lines
28 KiB
Rust
#![cfg_attr(all(not(dev), target_os = "windows"), windows_subsystem = "windows")]
|
|
|
|
use std::collections::BTreeMap;
|
|
use std::fs;
|
|
use std::fs::File;
|
|
use std::io::{BufRead, BufReader, Read, Write};
|
|
use std::net::{TcpListener, TcpStream};
|
|
use std::path::{Path, PathBuf};
|
|
use std::sync::{mpsc, Mutex, OnceLock};
|
|
use std::thread;
|
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
|
|
|
use platform_agent::{
|
|
build_game_creation_seed_task_graph, plan_game_creation_agent_pass,
|
|
route_game_creation_repair_issues,
|
|
};
|
|
use platform_llm::{
|
|
LlmApiKind, LlmClient, LlmConfig, LlmMessage, LlmProvider, LlmRunRequest,
|
|
DEFAULT_RETRY_BACKOFF_MS,
|
|
};
|
|
use reqwest::header;
|
|
use serde::{Deserialize, Serialize};
|
|
use shared_contracts::game_creation_app::{
|
|
new_game_creation_app_manifest, new_game_creation_app_seed_tasks,
|
|
GameCreationAgentArtifactTrace, GameCreationAgentCapabilityDescriptor,
|
|
GameCreationAgentPassPlanTrace, GameCreationAgentRepairRouteTrace, GameCreationAgentRunStep,
|
|
GameCreationAgentRunTaskGraphTrace, GameCreationAgentRunTrace, GameCreationAgentToolCallTrace,
|
|
GameCreationAppAgentGroup, GameCreationAppAssetManifestEntry, GameCreationAppAssetSource,
|
|
GameCreationAppAssetSourceKind, GameCreationAppCommandRunState,
|
|
GameCreationAppCommandRunStatus, GameCreationAppLimitedRunCommandDescriptor,
|
|
GameCreationAppManifest, GameCreationAppPermission, GameCreationAppPreviewState,
|
|
GameCreationAppPreviewStatus, GameCreationAppTaskState, GameCreationAppTaskStatus,
|
|
GAME_CREATION_AGENT_CAPABILITIES, GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
|
|
GAME_CREATION_AGENT_TOOL_CALL_MAX, GAME_CREATION_APP_COMMANDS,
|
|
GAME_CREATION_APP_LIMITED_RUN_COMMANDS,
|
|
};
|
|
use tauri::{Emitter, Manager};
|
|
use tauri_plugin_dialog::DialogExt;
|
|
use tauri_plugin_opener::OpenerExt;
|
|
|
|
// 调试落盘模块(保存 LLM 原始输出 / 失败输入,排查截断、空返回等)放在 debug_drafts.rs。
|
|
// 用 #[cfg] 编译期门控:仅开发(debug)且非测试构建编入;生产 release 与 cargo test 下整体剔除。
|
|
mod agent;
|
|
mod assets;
|
|
mod cli;
|
|
mod commands;
|
|
mod config;
|
|
#[cfg(all(debug_assertions, not(test)))]
|
|
mod debug;
|
|
mod preview;
|
|
mod project;
|
|
mod windows;
|
|
|
|
use agent::*;
|
|
use assets::*;
|
|
use cli::*;
|
|
use commands::*;
|
|
use config::*;
|
|
use preview::*;
|
|
use project::*;
|
|
use windows::*;
|
|
|
|
#[derive(Debug, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct InitLocalProjectResult {
|
|
project_path: String,
|
|
manifest_path: String,
|
|
manifest: GameCreationAppManifest,
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct LocalProjectDirectoryStatus {
|
|
project_path: String,
|
|
exists: bool,
|
|
is_directory: bool,
|
|
is_game_creator_project: bool,
|
|
project_name: Option<String>,
|
|
manifest_error: Option<String>,
|
|
recent_run_status: Option<String>,
|
|
recent_run_stop_reason: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct LocalPreviewResult {
|
|
url: String,
|
|
port: u16,
|
|
root: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct LocalPreviewStatus {
|
|
status: String,
|
|
url: Option<String>,
|
|
port: Option<u16>,
|
|
root: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct GenerateLocalGameDraftResult {
|
|
project_path: String,
|
|
game_index_path: String,
|
|
design_path: String,
|
|
short_memory_path: String,
|
|
long_memory_path: String,
|
|
manifest: GameCreationAppManifest,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct GameCreatorAgentProgressEvent {
|
|
project_path: String,
|
|
stage: String,
|
|
message: String,
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct GameCreatorLlmConfigStatus {
|
|
configured: bool,
|
|
api_key_present: bool,
|
|
base_url: Option<String>,
|
|
model: Option<String>,
|
|
api_kind: String,
|
|
stream: bool,
|
|
error: Option<String>,
|
|
agents: Vec<GameCreatorAgentLlmConfigStatus>,
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct GameCreatorAgentLlmConfigStatus {
|
|
agent_id: String,
|
|
label: String,
|
|
configured: bool,
|
|
api_key_present: bool,
|
|
base_url: Option<String>,
|
|
model: Option<String>,
|
|
api_kind: String,
|
|
stream: bool,
|
|
error: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct GameCreatorAppConfigFile {
|
|
llm: Option<GameCreatorLlmConfigFile>,
|
|
agent_llm: Option<BTreeMap<String, GameCreatorLlmConfigFile>>,
|
|
editor_api: Option<GameCreatorEditorApiConfigFile>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct GameCreatorLlmConfigFile {
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
api_key: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
base_url: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
model: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
api_kind: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
stream: Option<bool>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
request_timeout_ms: Option<u64>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
max_retries: Option<u32>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
retry_backoff_ms: Option<u64>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct GameCreatorEditorApiConfigFile {
|
|
base_url: Option<String>,
|
|
api_key: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct GameCreatorAppConfig {
|
|
llm: GameCreatorLlmConfig,
|
|
#[serde(default)]
|
|
agent_llm: BTreeMap<String, GameCreatorLlmConfigFile>,
|
|
editor_api: GameCreatorEditorApiConfig,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct GameCreatorLlmConfig {
|
|
api_key: String,
|
|
base_url: String,
|
|
model: String,
|
|
api_kind: String,
|
|
stream: bool,
|
|
request_timeout_ms: u64,
|
|
max_retries: u32,
|
|
retry_backoff_ms: u64,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct GameCreatorEditorApiConfig {
|
|
base_url: String,
|
|
api_key: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct GameCreatorAppConfigView {
|
|
path: String,
|
|
config: GameCreatorAppConfig,
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct AgentRunControlResult {
|
|
run_id: String,
|
|
status: String,
|
|
lifecycle_status: String,
|
|
next_step: String,
|
|
message: String,
|
|
activity_path: String,
|
|
output_path: String,
|
|
context_bundle_path: String,
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct UploadLocalAssetResult {
|
|
id: String,
|
|
local_path: String,
|
|
absolute_path: String,
|
|
manifest_path: String,
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct ImportCanvasExportResult {
|
|
import_root: String,
|
|
metadata_path: String,
|
|
imported_count: usize,
|
|
assets: Vec<UploadLocalAssetResult>,
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct SyncCanvasProjectAssetsResult {
|
|
canvas_project_id: String,
|
|
import_root: String,
|
|
imported_count: usize,
|
|
assets: Vec<UploadLocalAssetResult>,
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq)]
|
|
struct GeneratedPlatformArtAsset {
|
|
asset: UploadLocalAssetResult,
|
|
resource_id: Option<String>,
|
|
asset_object_id: Option<String>,
|
|
task_id: Option<String>,
|
|
model: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct OpenCanvasProjectResult {
|
|
url: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct CanvasExportMetadata {
|
|
project_title: String,
|
|
exported_at: String,
|
|
layers: Vec<CanvasExportLayerMetadata>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct CanvasExportLayerMetadata {
|
|
title: String,
|
|
file: Option<String>,
|
|
visible: CanvasExportVisibleMetadata,
|
|
export_error: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct CanvasExportVisibleMetadata {
|
|
#[serde(rename = "type")]
|
|
layer_type: String,
|
|
model: String,
|
|
task: String,
|
|
object: String,
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct LocalGameMemoryResult {
|
|
scope: String,
|
|
path: String,
|
|
content: String,
|
|
exists: bool,
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct LocalAgentMemoryResult {
|
|
task_id: String,
|
|
path: String,
|
|
content: String,
|
|
exists: bool,
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct LimitedLocalCommandResult {
|
|
command_id: String,
|
|
status: String,
|
|
output: String,
|
|
log_path: String,
|
|
updated_at: u64,
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct LocalProjectFileEntry {
|
|
path: String,
|
|
kind: String,
|
|
size: u64,
|
|
modified_at: u64,
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct ListLocalProjectFilesResult {
|
|
project_path: String,
|
|
files: Vec<LocalProjectFileEntry>,
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct LocalProjectFileResult {
|
|
path: String,
|
|
absolute_path: String,
|
|
content: String,
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct LocalProjectFileMutationResult {
|
|
path: String,
|
|
absolute_path: String,
|
|
deleted: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct LocalConversationMessage {
|
|
role: String,
|
|
content: String,
|
|
agent_id: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct LocalConversationMessageRecord {
|
|
schema_version: String,
|
|
role: String,
|
|
content: String,
|
|
agent_id: Option<String>,
|
|
updated_at: u64,
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct LocalConversationResult {
|
|
path: String,
|
|
agent_id: Option<String>,
|
|
messages: Vec<LocalConversationMessageRecord>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct ProjectPermissionPolicy {
|
|
denied_commands: Vec<String>,
|
|
confirm_commands: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct ProjectPermissionPolicyView {
|
|
path: String,
|
|
policy: ProjectPermissionPolicy,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct LocalProjectIndexedFile {
|
|
path: String,
|
|
size: u64,
|
|
checksum: String,
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct LocalProjectIndexResult {
|
|
project_path: String,
|
|
index_path: String,
|
|
file_count: usize,
|
|
total_bytes: u64,
|
|
files: Vec<LocalProjectIndexedFile>,
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct LocalProjectCheckpointResult {
|
|
checkpoint_id: String,
|
|
checkpoint_path: String,
|
|
file_count: usize,
|
|
total_bytes: u64,
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct LocalProjectExportPackageResult {
|
|
package_path: String,
|
|
package_relative_path: String,
|
|
file_count: usize,
|
|
total_bytes: u64,
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct LocalProjectExportPackageSummary {
|
|
package_path: String,
|
|
package_relative_path: String,
|
|
total_bytes: u64,
|
|
modified_at: u64,
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct LocalProjectExportPackagesResult {
|
|
project_path: String,
|
|
packages: Vec<LocalProjectExportPackageSummary>,
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct LocalProjectDiffEntry {
|
|
path: String,
|
|
status: String,
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct LocalProjectDiffResult {
|
|
checkpoint_id: String,
|
|
added: Vec<LocalProjectDiffEntry>,
|
|
changed: Vec<LocalProjectDiffEntry>,
|
|
deleted: Vec<LocalProjectDiffEntry>,
|
|
}
|
|
|
|
#[derive(Debug, Eq, PartialEq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct LocalProjectRestoreResult {
|
|
checkpoint_id: String,
|
|
restored_count: usize,
|
|
deleted_count: usize,
|
|
}
|
|
|
|
const DEFAULT_GAME_INDEX_HTML: &str = r#"<!doctype html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Genarrative Game Draft</title>
|
|
<style>
|
|
body { margin: 0; display: grid; min-height: 100vh; place-items: center; background: #101827; color: #d9e7ff; font: 16px system-ui, sans-serif; }
|
|
main { width: min(720px, calc(100vw - 32px)); }
|
|
</style>
|
|
</head>
|
|
<body><main>还没有生成游戏。回到聊天输入创意并确认生成后,这里会写入可试玩原型。</main></body>
|
|
</html>
|
|
"#;
|
|
|
|
const DEFAULT_EDITOR_BASE_URL: &str = "http://127.0.0.1:3000";
|
|
const GAME_CREATOR_CONFIG_FILE_NAME: &str = "game-creator.config.json";
|
|
const GAME_CREATOR_LOCAL_CONFIG_FILE_NAME: &str = "game-creator.config.local.json";
|
|
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_API_KIND: &str = "openai_responses";
|
|
const DEFAULT_CANVAS_SYNC_API_BASE_URL: &str = "http://127.0.0.1:8082";
|
|
const DEFAULT_GAME_CREATOR_APP_CONFIG_JSON: &str = include_str!("../../game-creator.config.json");
|
|
const GAME_CREATOR_LLM_MAX_OUTPUT_TOKENS: u32 = 320000;
|
|
const GAME_CREATOR_PLANNER_MAX_OUTPUT_TOKENS: u32 = 900;
|
|
const GAME_CREATOR_ROLE_AGENT_MAX_OUTPUT_TOKENS: u32 = 1200;
|
|
const GAME_CREATOR_REQUIRED_LLM_AGENT_IDS: [&str; 2] = ["planner", "generator"];
|
|
const MIN_GAME_CREATOR_LLM_REQUEST_TIMEOUT_MS: u64 = 1_000;
|
|
const GAME_CREATOR_LLM_REQUEST_TIMEOUT_MS: u64 = 180_000;
|
|
const GAME_CREATOR_AGENT_LOOP_MAX_PASSES: u8 = 3;
|
|
const GAME_CREATOR_AGENT_TOOL_CALL_MAX: u16 = GAME_CREATION_AGENT_TOOL_CALL_MAX;
|
|
const GAME_CREATOR_AGENT_RUN_HISTORY_MAX_COUNT: usize = 100;
|
|
const GAME_CREATOR_AGENT_DB_SCHEMA_VERSION: &str = "game-creator-agent-db.v1";
|
|
const PROJECT_BLACKBOARD_MEMORY_PATH: &str = "memory/blackboard.md";
|
|
const PROJECT_PERMISSION_POLICY_PATH: &str = ".agent/policy.json";
|
|
const PROJECT_INDEX_PATH: &str = ".agent/project.index.json";
|
|
const PROJECT_WRITE_LOCK_PATH: &str = ".agent/project.lock";
|
|
const LOCAL_CONVERSATION_SCHEMA_VERSION: &str = "game-creator-conversation.v1";
|
|
const GAME_CREATOR_CONVERSATION_CONTEXT_MAX_MESSAGES: usize = 12;
|
|
const MAX_CANVAS_EXPORT_FILES: usize = 500;
|
|
const MAX_CANVAS_EXPORT_BYTES: u64 = 512 * 1024 * 1024;
|
|
const MAX_PROJECT_EXPORT_PACKAGE_FILES: usize = 1200;
|
|
const MAX_PROJECT_EXPORT_PACKAGE_BYTES: u64 = 512 * 1024 * 1024;
|
|
const GAME_CREATOR_AGENT_ARTIFACT_PATHS: [&str; 15] = [
|
|
".agent/agent.db",
|
|
".agent/spec.md",
|
|
".agent/findings.md",
|
|
".agent/logs/agent.log",
|
|
".agent/logs/command.log",
|
|
".agent/logs/preview.log",
|
|
"memory/session.md",
|
|
"memory/project.md",
|
|
PROJECT_BLACKBOARD_MEMORY_PATH,
|
|
"game/game_design.md",
|
|
"game/balance.json",
|
|
"assets/manifest.art.json",
|
|
"assets/manifest.audio.json",
|
|
"exports/README.md",
|
|
"game/index.html",
|
|
];
|
|
static GAME_CREATOR_RUNTIME_CONFIG_DIR: OnceLock<Mutex<Option<PathBuf>>> = OnceLock::new();
|
|
|
|
impl Default for GameCreatorAppConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
llm: GameCreatorLlmConfig::default(),
|
|
agent_llm: BTreeMap::new(),
|
|
editor_api: GameCreatorEditorApiConfig::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for GameCreatorLlmConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
api_key: String::new(),
|
|
base_url: DEFAULT_GAME_CREATOR_LLM_BASE_URL.to_string(),
|
|
model: DEFAULT_GAME_CREATOR_LLM_MODEL.to_string(),
|
|
api_kind: DEFAULT_GAME_CREATOR_LLM_API_KIND.to_string(),
|
|
stream: false,
|
|
request_timeout_ms: GAME_CREATOR_LLM_REQUEST_TIMEOUT_MS,
|
|
max_retries: 0,
|
|
retry_backoff_ms: DEFAULT_RETRY_BACKOFF_MS,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for GameCreatorEditorApiConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
base_url: DEFAULT_CANVAS_SYNC_API_BASE_URL.to_string(),
|
|
api_key: String::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
struct AgentRoleDefinition {
|
|
id: &'static str,
|
|
role: &'static str,
|
|
task_id: &'static str,
|
|
tool_id: &'static str,
|
|
brief_path_name: &'static str,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
struct AgentGroupDefinition {
|
|
id: &'static str,
|
|
label: &'static str,
|
|
role: &'static str,
|
|
brief_path_name: &'static str,
|
|
roles: &'static [AgentRoleDefinition],
|
|
}
|
|
|
|
static DESIGN_AGENT_ROLES: [AgentRoleDefinition; 2] = [
|
|
AgentRoleDefinition {
|
|
id: "director",
|
|
role: "Director",
|
|
task_id: "design-director",
|
|
tool_id: "agent.role.brief.design.director",
|
|
brief_path_name: "director.md",
|
|
},
|
|
AgentRoleDefinition {
|
|
id: "gameplay",
|
|
role: "Gameplay",
|
|
task_id: "design-foundation",
|
|
tool_id: "agent.role.brief.design.gameplay",
|
|
brief_path_name: "gameplay.md",
|
|
},
|
|
];
|
|
|
|
static BALANCE_AGENT_ROLES: [AgentRoleDefinition; 2] = [
|
|
AgentRoleDefinition {
|
|
id: "director",
|
|
role: "Director",
|
|
task_id: "balance-director",
|
|
tool_id: "agent.role.brief.balance.director",
|
|
brief_path_name: "director.md",
|
|
},
|
|
AgentRoleDefinition {
|
|
id: "difficulty",
|
|
role: "Difficulty",
|
|
task_id: "balance-seed",
|
|
tool_id: "agent.role.brief.balance.difficulty",
|
|
brief_path_name: "difficulty.md",
|
|
},
|
|
];
|
|
|
|
static ART_AGENT_ROLES: [AgentRoleDefinition; 3] = [
|
|
AgentRoleDefinition {
|
|
id: "director",
|
|
role: "Director",
|
|
task_id: "art-director",
|
|
tool_id: "agent.role.brief.art.director",
|
|
brief_path_name: "director.md",
|
|
},
|
|
AgentRoleDefinition {
|
|
id: "asset",
|
|
role: "Asset",
|
|
task_id: "art-asset-plan",
|
|
tool_id: "agent.role.brief.art.asset",
|
|
brief_path_name: "asset.md",
|
|
},
|
|
AgentRoleDefinition {
|
|
id: "polish",
|
|
role: "Polish",
|
|
task_id: "art-polish",
|
|
tool_id: "agent.role.brief.art.polish",
|
|
brief_path_name: "polish.md",
|
|
},
|
|
];
|
|
|
|
static AUDIO_AGENT_ROLES: [AgentRoleDefinition; 2] = [
|
|
AgentRoleDefinition {
|
|
id: "director",
|
|
role: "Director",
|
|
task_id: "audio-director",
|
|
tool_id: "agent.role.brief.audio.director",
|
|
brief_path_name: "director.md",
|
|
},
|
|
AgentRoleDefinition {
|
|
id: "sfx",
|
|
role: "SFX",
|
|
task_id: "audio-asset-plan",
|
|
tool_id: "agent.role.brief.audio.sfx",
|
|
brief_path_name: "sfx.md",
|
|
},
|
|
];
|
|
|
|
static CODE_AGENT_ROLES: [AgentRoleDefinition; 5] = [
|
|
AgentRoleDefinition {
|
|
id: "director",
|
|
role: "Director",
|
|
task_id: "code-director",
|
|
tool_id: "agent.role.brief.code.director",
|
|
brief_path_name: "director.md",
|
|
},
|
|
AgentRoleDefinition {
|
|
id: "code",
|
|
role: "Code",
|
|
task_id: "code-prototype",
|
|
tool_id: "agent.role.brief.code.code",
|
|
brief_path_name: "code.md",
|
|
},
|
|
AgentRoleDefinition {
|
|
id: "review",
|
|
role: "Review",
|
|
task_id: "quality-review",
|
|
tool_id: "agent.role.brief.code.review",
|
|
brief_path_name: "review.md",
|
|
},
|
|
AgentRoleDefinition {
|
|
id: "preview",
|
|
role: "Preview",
|
|
task_id: "preview-readiness",
|
|
tool_id: "agent.role.brief.code.preview",
|
|
brief_path_name: "preview.md",
|
|
},
|
|
AgentRoleDefinition {
|
|
id: "playtest",
|
|
role: "Playtest",
|
|
task_id: "preview-playtest",
|
|
tool_id: "agent.role.brief.code.playtest",
|
|
brief_path_name: "playtest.md",
|
|
},
|
|
];
|
|
|
|
static PUBLISHING_AGENT_ROLES: [AgentRoleDefinition; 2] = [
|
|
AgentRoleDefinition {
|
|
id: "director",
|
|
role: "Director",
|
|
task_id: "publish-strategy",
|
|
tool_id: "agent.role.brief.publishing.director",
|
|
brief_path_name: "director.md",
|
|
},
|
|
AgentRoleDefinition {
|
|
id: "publish",
|
|
role: "Publish",
|
|
task_id: "publish-package",
|
|
tool_id: "agent.role.brief.publishing.publish",
|
|
brief_path_name: "publish.md",
|
|
},
|
|
];
|
|
|
|
const GAME_CREATOR_AGENT_GROUP_DEFINITIONS: [AgentGroupDefinition; 6] = [
|
|
AgentGroupDefinition {
|
|
id: "design",
|
|
label: "策划组",
|
|
role: "Director + Gameplay",
|
|
brief_path_name: "design.md",
|
|
roles: &DESIGN_AGENT_ROLES,
|
|
},
|
|
AgentGroupDefinition {
|
|
id: "balance",
|
|
label: "数值组",
|
|
role: "Director + Difficulty",
|
|
brief_path_name: "balance.md",
|
|
roles: &BALANCE_AGENT_ROLES,
|
|
},
|
|
AgentGroupDefinition {
|
|
id: "art",
|
|
label: "美术组",
|
|
role: "Director + Asset + Polish",
|
|
brief_path_name: "art.md",
|
|
roles: &ART_AGENT_ROLES,
|
|
},
|
|
AgentGroupDefinition {
|
|
id: "audio",
|
|
label: "音乐组",
|
|
role: "Director + SFX",
|
|
brief_path_name: "audio.md",
|
|
roles: &AUDIO_AGENT_ROLES,
|
|
},
|
|
AgentGroupDefinition {
|
|
id: "code",
|
|
label: "程序组",
|
|
role: "Director + Code + Review + Preview + Playtest",
|
|
brief_path_name: "code.md",
|
|
roles: &CODE_AGENT_ROLES,
|
|
},
|
|
AgentGroupDefinition {
|
|
id: "publishing",
|
|
label: "运营组",
|
|
role: "Director + Publish",
|
|
brief_path_name: "publishing.md",
|
|
roles: &PUBLISHING_AGENT_ROLES,
|
|
},
|
|
];
|
|
|
|
struct GameCreatorLlmAgentStatusDefinition {
|
|
agent_id: String,
|
|
label: String,
|
|
}
|
|
|
|
fn game_creator_llm_agent_status_definitions() -> Vec<GameCreatorLlmAgentStatusDefinition> {
|
|
let mut agents = vec![
|
|
GameCreatorLlmAgentStatusDefinition {
|
|
agent_id: "planner".to_string(),
|
|
label: "Planner".to_string(),
|
|
},
|
|
GameCreatorLlmAgentStatusDefinition {
|
|
agent_id: "orchestrator".to_string(),
|
|
label: "Orchestrator".to_string(),
|
|
},
|
|
GameCreatorLlmAgentStatusDefinition {
|
|
agent_id: "generator".to_string(),
|
|
label: "Generator".to_string(),
|
|
},
|
|
GameCreatorLlmAgentStatusDefinition {
|
|
agent_id: "evaluator".to_string(),
|
|
label: "Evaluator".to_string(),
|
|
},
|
|
];
|
|
for group in GAME_CREATOR_AGENT_GROUP_DEFINITIONS {
|
|
for role in group.roles {
|
|
if agents.iter().any(|agent| agent.agent_id == role.task_id) {
|
|
continue;
|
|
}
|
|
agents.push(GameCreatorLlmAgentStatusDefinition {
|
|
agent_id: role.task_id.to_string(),
|
|
label: format!("{} / {}", group.label, role.role),
|
|
});
|
|
}
|
|
}
|
|
agents
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
struct AgentPassArtifactPaths {
|
|
draft_json: String,
|
|
design_markdown: String,
|
|
balance_json: String,
|
|
art_manifest_json: String,
|
|
audio_manifest_json: String,
|
|
publish_readme: String,
|
|
game_html: String,
|
|
handoff_markdown: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
struct AgentRoleBrief {
|
|
group_definition: AgentGroupDefinition,
|
|
role_definition: AgentRoleDefinition,
|
|
markdown: String,
|
|
relative_path: String,
|
|
memory_relative_path: String,
|
|
status: String,
|
|
tool_id: String,
|
|
summary: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
struct AgentGroupBrief {
|
|
definition: AgentGroupDefinition,
|
|
markdown: String,
|
|
relative_path: String,
|
|
role_briefs: Vec<AgentRoleBrief>,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
struct AgentPassAgenda {
|
|
relative_path: String,
|
|
task_graph_relative_path: String,
|
|
active_task_ids: Vec<String>,
|
|
carried_task_ids: Vec<String>,
|
|
dependency_waves: Vec<Vec<String>>,
|
|
repair_focus: Vec<String>,
|
|
repair_routes: Vec<GameCreationAgentRepairRouteTrace>,
|
|
summary: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct LlmGameDraft {
|
|
title: String,
|
|
design_markdown: String,
|
|
balance: serde_json::Value,
|
|
art_manifest: serde_json::Value,
|
|
audio_manifest: serde_json::Value,
|
|
publish_readme: String,
|
|
handoffs: Vec<LlmAgentHandoff>,
|
|
game_html: String,
|
|
handoff_summary: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct LlmAgentHandoff {
|
|
group: String,
|
|
role: String,
|
|
summary: String,
|
|
outputs: Vec<String>,
|
|
next: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
struct GameCreatorAgentLoopResult {
|
|
run_id: String,
|
|
draft: LlmGameDraft,
|
|
spec_markdown: String,
|
|
findings_markdown: String,
|
|
passes: u8,
|
|
steps: Vec<GameCreationAgentRunStep>,
|
|
}
|
|
|
|
fn main() {
|
|
let args = std::env::args().skip(1).collect::<Vec<_>>();
|
|
match parse_cli_command(&args) {
|
|
Ok(Some(command)) => {
|
|
if let Err(error) = run_cli_command(command) {
|
|
eprintln!("agent.run.failed: {error}");
|
|
std::process::exit(1);
|
|
}
|
|
return;
|
|
}
|
|
Ok(None) => {}
|
|
Err(error) => {
|
|
eprintln!("{error}");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
|
|
tauri::Builder::default()
|
|
.plugin(tauri_plugin_opener::init())
|
|
.plugin(tauri_plugin_dialog::init())
|
|
.manage(PreviewRegistry::default())
|
|
.setup(|app| {
|
|
configure_game_creator_runtime_config_dir(app.handle())?;
|
|
Ok(())
|
|
})
|
|
.invoke_handler(tauri::generate_handler![
|
|
init_local_game_project,
|
|
is_local_project_directory_non_empty,
|
|
inspect_local_project_directory,
|
|
pick_local_project_directory,
|
|
pick_local_file,
|
|
open_local_project_directory,
|
|
control_agent_run,
|
|
generate_local_game_draft,
|
|
check_game_creator_llm_config,
|
|
read_game_creator_app_config,
|
|
write_game_creator_app_config,
|
|
upload_local_asset,
|
|
register_local_asset,
|
|
import_canvas_asset,
|
|
import_canvas_export,
|
|
sync_canvas_project_assets,
|
|
generate_platform_art_asset,
|
|
open_canvas_project,
|
|
get_game_creation_agent_capabilities,
|
|
get_limited_local_commands,
|
|
run_limited_local_command,
|
|
append_local_permission_log,
|
|
list_local_project_files,
|
|
read_local_project_file,
|
|
write_local_project_file,
|
|
delete_local_project_file,
|
|
read_local_game_memory,
|
|
read_local_agent_memory,
|
|
write_local_agent_memory,
|
|
write_local_game_memory,
|
|
delete_local_game_memory,
|
|
read_local_conversation,
|
|
append_local_conversation_message,
|
|
build_local_project_index,
|
|
create_local_project_checkpoint,
|
|
export_local_project_package,
|
|
list_local_project_export_packages,
|
|
diff_local_project_checkpoint,
|
|
restore_local_project_checkpoint,
|
|
read_project_permission_policy,
|
|
write_project_permission_policy,
|
|
open_game_creator_workspace_window,
|
|
open_game_creator_launcher_window,
|
|
start_local_game_preview,
|
|
open_local_game_preview,
|
|
stop_local_game_preview,
|
|
get_local_game_preview_status,
|
|
get_local_game_manifest
|
|
])
|
|
.run(tauri::generate_context!())
|
|
.expect("failed to run Genarrative AI Game Creator shell");
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|