完善AI游戏创作App本地工作区与Agent协作

补齐工作区启动器、非空目录确认和主窗口切换入口

改为运行时配置文件并支持全局与单Agent LLM Provider

接入平台 External API 生成和同步美术资产

补齐本地对话记录、Agent私有记忆和项目黑板

完善Agent状态、历史Run、快照、文件和资产快捷入口

同步AI游戏创作App实施计划和共享决策记录
This commit is contained in:
AIGameCreator App
2026-07-03 01:32:51 +08:00
parent 76b5718d77
commit 998e33ca3d
21 changed files with 24613 additions and 974 deletions
@@ -9,6 +9,7 @@
"maxRetries": 0,
"retryBackoffMs": 500
},
"agentLlm": {},
"editorApi": {
"baseUrl": "http://127.0.0.1:8082",
"apiKey": ""
@@ -9,6 +9,12 @@ const tauriConfig = JSON.parse(
'utf8',
),
);
const defaultAppConfig = JSON.parse(
fs.readFileSync(
new URL('../game-creator.config.json', import.meta.url),
'utf8',
),
);
const rootPackageConfig = JSON.parse(
fs.readFileSync(new URL('../../../package.json', import.meta.url), 'utf8'),
);
@@ -16,6 +22,29 @@ const viteConfigSource = fs.readFileSync(
new URL('../vite.config.ts', import.meta.url),
'utf8',
);
const appInvokeSource = fs.readFileSync(
new URL('../src/App.tsx', import.meta.url),
'utf8',
);
const tauriHandlerSource = fs.readFileSync(
new URL('../src-tauri/src/main.rs', import.meta.url),
'utf8',
);
const sharedContractSource = fs.readFileSync(
new URL(
'../../../packages/shared/src/contracts/gameCreationApp.ts',
import.meta.url,
),
'utf8',
);
const rustSharedContractSource = fs.readFileSync(
new URL(
'../../../server-rs/crates/shared-contracts/src/game_creation_app.rs',
import.meta.url,
),
'utf8',
);
const allowedUncalledTauriCommands = [];
const sourceExtensions = new Set([
'.json',
'.md',
@@ -61,6 +90,149 @@ function assertNoOpenAiApiKeys(paths) {
}
}
function assertNoEnvironmentConfigFallbacks(paths) {
const allowedDevCheck = 'import.meta.env.DEV';
for (const path of paths.flatMap((entry) => collectFiles(entry))) {
const source = fs
.readFileSync(path, 'utf8')
.replaceAll(allowedDevCheck, '')
.replaceAll('game-creator.config.local.json', '');
if (/\bprocess\.env\b|\bdotenv\b/.test(source)) {
throw new Error(
`AI game creator shell must use runtime config, not environment config: ${path.pathname}`,
);
}
}
}
function assertNoNativeBrowserConfirm(paths) {
for (const path of paths.flatMap((entry) => collectFiles(entry))) {
const source = fs.readFileSync(path, 'utf8');
if (/\bwindow\.confirm\b/.test(source)) {
throw new Error(
`AI game creator shell confirmations must use in-app UI: ${path.pathname}`,
);
}
}
}
function extractConstArrayBlock(source, name) {
const start = source.indexOf(`const ${name}`);
if (start === -1) {
throw new Error(`Missing contract array: ${name}`);
}
const end = source.indexOf('];', start);
if (end === -1) {
throw new Error(`Missing contract array end: ${name}`);
}
return source.slice(start, end + 2);
}
function parseTsCommands(source) {
const block = extractConstArrayBlock(source, 'GAME_CREATION_APP_COMMANDS');
return Array.from(
block.matchAll(
/\{\s*id:\s*'([^']+)',\s*permission:\s*'([^']+)'\s*\}/g,
),
([, id, permission]) => ({ id, permission }),
);
}
function parseRustCommands(source) {
const block = extractConstArrayBlock(source, 'GAME_CREATION_APP_COMMANDS');
const permissionNames = {
Auto: 'auto',
Confirm: 'confirm',
Deny: 'deny',
};
return Array.from(
block.matchAll(
/command\(\s*"([^"]+)",\s*GameCreationAppPermission::(Auto|Confirm|Deny)\s*\)/g,
),
([, id, permission]) => ({ id, permission: permissionNames[permission] }),
);
}
function parseTsCapabilities(source) {
const block = extractConstArrayBlock(
source,
'GAME_CREATION_AGENT_CAPABILITIES',
);
return Array.from(
block.matchAll(
/\{\s*id:\s*'([^']+)',\s*area:\s*'([^']+)',\s*title:\s*'([^']+)',?\s*\}/g,
),
([, id, area, title]) => ({ id, area, title }),
);
}
function parseRustCapabilities(source) {
const block = extractConstArrayBlock(
source,
'GAME_CREATION_AGENT_CAPABILITIES',
);
return Array.from(
block.matchAll(
/capability\(\s*"([^"]+)",\s*"([^"]+)",\s*"([^"]+)",?\s*\)/g,
),
([, id, area, title]) => ({ id, area, title }),
);
}
function assertContractRecordsMatch(label, leftRecords, rightRecords) {
const normalize = (records) =>
records
.map((record) => JSON.stringify(record))
.sort((left, right) => left.localeCompare(right));
const left = normalize(leftRecords);
const right = normalize(rightRecords);
if (left.length === 0 || right.length === 0) {
throw new Error(`${label} parser returned no records`);
}
if (JSON.stringify(left) !== JSON.stringify(right)) {
throw new Error(
`${label} drifted between TypeScript and Rust contracts\nTS=${left.join(
'\n',
)}\nRust=${right.join('\n')}`,
);
}
}
function parseAppInvokeCommandNames(source) {
return Array.from(
source.matchAll(/invoke(?:<[^>]*>)?\(\s*['"]([a-z0-9_]+)['"]/g),
([, command]) => command,
);
}
function parseTauriHandlerCommandNames(source) {
const match = source.match(/tauri::generate_handler!\[([\s\S]*?)\]/);
if (!match) {
throw new Error('AI game creator shell Tauri handler list is missing');
}
return Array.from(
match[1].matchAll(/\b([a-z][a-z0-9_]+)\b/g),
([, command]) => command,
);
}
function parseRustFunctionNames(source) {
return Array.from(
source.matchAll(/\b(?:async\s+)?fn\s+([a-z][a-z0-9_]*)\s*\(/g),
([, name]) => name,
);
}
function assertCommandNamesSubset(label, leftNames, rightNames) {
const right = new Set(rightNames);
const missing = Array.from(new Set(leftNames))
.filter((name) => !right.has(name))
.sort((left, rightName) => left.localeCompare(rightName));
if (missing.length > 0) {
throw new Error(`${label} missing commands: ${missing.join(', ')}`);
}
}
assertNoOpenAiApiKeys([
new URL('../src/', import.meta.url),
new URL('../scripts/', import.meta.url),
@@ -92,6 +264,59 @@ assertNoOpenAiApiKeys([
),
]);
assertNoEnvironmentConfigFallbacks([
new URL('../src/', import.meta.url),
new URL('../scripts/run-cli-with-config.mjs', import.meta.url),
new URL('../scripts/smoke-agent-run-local-provider.mjs', import.meta.url),
new URL('../scripts/start-dev-server.mjs', import.meta.url),
new URL('../src-tauri/src/', import.meta.url),
new URL('../package.json', import.meta.url),
new URL('../vite.config.ts', import.meta.url),
new URL('../src-tauri/Cargo.toml', import.meta.url),
new URL('../src-tauri/tauri.conf.json', import.meta.url),
]);
assertNoNativeBrowserConfirm([new URL('../src/', import.meta.url)]);
assertContractRecordsMatch(
'AI game creator shell command contract',
parseTsCommands(sharedContractSource),
parseRustCommands(rustSharedContractSource),
);
assertContractRecordsMatch(
'AI game creator shell capability contract',
parseTsCapabilities(sharedContractSource),
parseRustCapabilities(rustSharedContractSource),
);
assertCommandNamesSubset(
'AI game creator shell Tauri handler',
parseAppInvokeCommandNames(appInvokeSource),
parseTauriHandlerCommandNames(tauriHandlerSource),
);
assertCommandNamesSubset(
'AI game creator shell Tauri command implementation',
parseTauriHandlerCommandNames(tauriHandlerSource),
parseRustFunctionNames(tauriHandlerSource),
);
assertCommandNamesSubset(
'AI game creator shell App invoke or explicit native-only allowlist',
parseTauriHandlerCommandNames(tauriHandlerSource),
[
...parseAppInvokeCommandNames(appInvokeSource),
...allowedUncalledTauriCommands,
],
);
assertCommandNamesSubset(
'AI game creator shell explicit native-only allowlist',
allowedUncalledTauriCommands,
parseTauriHandlerCommandNames(tauriHandlerSource),
);
if (packageConfig.name !== '@genarrative/ai-game-creator-shell') {
throw new Error('AI game creator shell package name drifted');
}
@@ -128,21 +353,54 @@ if (tauriConfig.app?.withGlobalTauri !== true) {
);
}
const windows = tauriConfig.app?.windows ?? [];
if (windows.length !== 1 || windows[0]?.label !== 'main') {
if (
!Array.isArray(tauriConfig.app?.windows) ||
tauriConfig.app.windows.length !== 1 ||
tauriConfig.app.windows[0]?.label !== 'launcher' ||
tauriConfig.app.windows[0]?.url !== 'index.html?launcher'
) {
throw new Error(
'AI game creator shell release config must expose only the chat main window',
'AI game creator shell must start with only the launcher window',
);
}
if (defaultAppConfig.llm?.apiKey !== '') {
throw new Error('AI game creator shell default llm.apiKey must stay empty');
}
if (defaultAppConfig.editorApi?.apiKey !== '') {
throw new Error(
'AI game creator shell default editorApi.apiKey must stay empty',
);
}
const mainWindow = windows[0];
if (
mainWindow.width !== 760 ||
mainWindow.height !== 820 ||
mainWindow.minWidth !== 420 ||
mainWindow.minHeight !== 560
defaultAppConfig.llm?.requestTimeoutMs < 1000 ||
defaultAppConfig.llm?.maxRetries < 0 ||
defaultAppConfig.llm?.retryBackoffMs < 1
) {
throw new Error('AI game creator shell main window must stay chat-sized');
throw new Error('AI game creator shell default LLM timing config is invalid');
}
const windows = tauriConfig.app?.windows ?? [];
if (
windows.length !== 1 ||
windows[0]?.label !== 'launcher' ||
windows[0]?.url !== 'index.html?launcher'
) {
throw new Error(
'AI game creator shell release config must expose only the launcher window',
);
}
const launcherWindow = windows[0];
if (
launcherWindow.width !== 820 ||
launcherWindow.height !== 640 ||
launcherWindow.minWidth !== 720 ||
launcherWindow.minHeight !== 520
) {
throw new Error('AI game creator shell launcher window must stay compact');
}
if (tauriConfig.build?.devUrl !== 'http://127.0.0.1:3080/') {
@@ -245,13 +503,12 @@ for (const snippet of [
'configure_game_creator_runtime_config_dir(app.handle())?',
'read_game_creator_app_config,',
'write_game_creator_app_config,',
'build_game_creator_llm_client_from_config()?',
'resolve_game_creator_llm_config_for_agent(app_config, "planner")',
'resolve_game_creator_llm_config_for_agent(app_config, "generator")',
'build_game_creator_llm_client_from_llm_config(&planner_llm, "agentLlm.planner")?',
'build_game_creator_llm_client_from_llm_config(&generator_llm, "agentLlm.generator")?',
'agentLlm.{agent_id}',
'let app_config = match load_game_creator_app_config()',
'#[cfg(debug_assertions)]\nfn developer_window_url()',
'tauri::WebviewUrl::App(PathBuf::from("index.html?dev"))',
'#[cfg(debug_assertions)]\nfn open_developer_window(app: &tauri::App)',
'tauri::WebviewWindowBuilder::new(app, "developer", developer_window_url())',
'open_developer_window(app)?;',
'fn append_local_permission_log_at(',
'"command.auto"',
'GameCreationAppPermission::Auto',
@@ -263,6 +520,34 @@ for (const snippet of [
}
}
for (const snippet of [
'open_developer_window(app)?;',
'tauri::WebviewWindowBuilder::new(app, "developer"',
]) {
if (tauriMainSource.includes(snippet)) {
throw new Error(
`AI game creator shell must not auto-open developer windows: ${snippet}`,
);
}
}
const smokeAgentRunSource = fs.readFileSync(
new URL('./smoke-agent-run-local-provider.mjs', import.meta.url),
'utf8',
);
for (const snippet of [
'agentLlm',
'planner-smoke-model',
'generator-smoke-model',
'global-smoke-model-unused',
]) {
if (!smokeAgentRunSource.includes(snippet)) {
throw new Error(
`AI game creator local-provider smoke lost per-agent LLM coverage: ${snippet}`,
);
}
}
const appSource = fs.readFileSync(
new URL('../src/App.tsx', import.meta.url),
'utf8',
@@ -284,8 +569,8 @@ for (const snippet of [
'function resolvePendingCommandProjectPath',
'resolveChatProjectPath(localProject) ?? draftProjectPath',
'`permission.cancel ${command.id} missing-project`',
"'/remember [short|long] 内容:追加短期或长期记忆'",
"'/memory-set [short|long] 内容:覆盖保存短期或长期记忆'",
"'/remember [short|long|blackboard] 内容:追加短期、长期或黑板记忆'",
"'/memory-set [short|long|blackboard] 内容:覆盖保存对应记忆'",
'function parseRememberInput',
"'/trace 或 /loop:查看最近一次 Agent loop trace'",
'async function executeAgentTraceChat',
@@ -23,6 +23,10 @@ const smokeAssetBytes = Buffer.concat([
]);
const smokeAudioAssetPath = 'assets/uploads/smoke-bounce.mp3';
const smokeAudioAssetBytes = 'SMOKE_LOCAL_AUDIO:bounce';
const smokeProjectConversationMarker =
'SMOKE_CONVERSATION_CONTEXT:moonlight-wok';
const smokeAgentConversationMarker =
'SMOKE_AGENT_CONVERSATION_CONTEXT:neon-kitchen';
function handoffs() {
return [
@@ -348,6 +352,18 @@ try {
requestBodies.every((body) => body.includes('"stream":true')),
'provider requests did not use streaming LLM mode',
);
assert(
requestBodies.some((body) => body.includes('"model":"planner-smoke-model"')),
'provider requests did not use planner agent LLM override',
);
assert(
requestBodies.some((body) => body.includes('"model":"generator-smoke-model"')),
'provider requests did not use generator agent LLM override',
);
assert(
requestBodies.every((body) => !body.includes('global-smoke-model-unused')),
'provider requests unexpectedly used global LLM config',
);
assert(
requestBodies.some(
(body) =>
@@ -357,6 +373,15 @@ try {
),
'provider requests missing local asset prompt context',
);
assert(
requestBodies.some((body) => body.includes(smokeProjectConversationMarker)) &&
requestBodies.some((body) => body.includes(smokeAgentConversationMarker)),
'provider requests missing recent conversation prompt context',
);
assert(
requestBodies.every((body) => !body.includes('sk-smoke-secret')),
'provider requests leaked sensitive conversation context',
);
assert(trace.status === 'preview-stopped', `trace status ${trace.status}`);
assert(trace.passes === 2, `trace passes ${trace.passes}`);
const expectedToolCallCount = trace.steps.reduce(
@@ -500,9 +525,11 @@ try {
step.inputPaths?.includes('memory/session.md') &&
step.inputPaths?.includes('memory/project.md') &&
step.inputPaths?.includes('memory/blackboard.md') &&
step.inputPaths?.includes('.agent/conversations/project.jsonl') &&
step.inputPaths?.includes('.agent/conversations/agents/') &&
step.inputPaths?.includes('.agent/manifest.json'),
),
'trace missing planner memory or manifest inputs',
'trace missing planner memory, conversation or manifest inputs',
);
assert(
trace.steps.some(
@@ -510,11 +537,13 @@ try {
step.agent === '策划组 / Director' &&
step.pass === 1 &&
step.inputPaths?.includes('memory/blackboard.md') &&
step.inputPaths?.includes('.agent/conversations/project.jsonl') &&
step.inputPaths?.includes('.agent/conversations/agents/') &&
step.inputPaths?.includes('memory/agents/design/director.md') &&
step.inputPaths?.includes('.agent/manifest.json') &&
step.inputPaths?.includes('.agent/passes/pass-1/agenda.md'),
),
'trace missing role brief manifest or agenda inputs',
'trace missing role brief conversation, manifest or agenda inputs',
);
assert(
trace.steps.some(
@@ -522,11 +551,13 @@ try {
step.agent === 'Generator' &&
step.pass === 2 &&
step.inputPaths?.includes('memory/blackboard.md') &&
step.inputPaths?.includes('.agent/conversations/project.jsonl') &&
step.inputPaths?.includes('.agent/conversations/agents/') &&
step.inputPaths?.includes('.agent/manifest.json') &&
step.inputPaths?.includes('.agent/passes/pass-2/agenda.md') &&
step.inputPaths?.includes('.agent/passes/pass-2/task-graph.json'),
),
'trace missing generator manifest, agenda or task graph inputs',
'trace missing generator conversation, manifest, agenda or task graph inputs',
);
assert(
gameHtml.includes('LOCAL_E2E_MECHANIC:reflect-kitchen'),
@@ -578,12 +609,28 @@ async function writeSmokeLocalConfig(baseUrl) {
`${JSON.stringify(
{
llm: {
apiKey: 'local-provider-key',
baseUrl,
model: 'local-game-creator-smoke',
apiKey: 'global-smoke-key-unused',
baseUrl: 'http://127.0.0.1:1/v1',
model: 'global-smoke-model-unused',
apiKind: 'openai_chat',
stream: true,
},
agentLlm: {
planner: {
apiKey: 'planner-smoke-key',
baseUrl,
model: 'planner-smoke-model',
apiKind: 'openai_chat',
stream: true,
},
generator: {
apiKey: 'generator-smoke-key',
baseUrl,
model: 'generator-smoke-model',
apiKind: 'openai_chat',
stream: true,
},
},
},
null,
2,
@@ -687,6 +734,7 @@ function runAgent() {
async function seedLocalAsset() {
await fs.mkdir(path.join(projectRoot, 'assets/uploads'), { recursive: true });
await fs.mkdir(path.join(projectRoot, '.agent'), { recursive: true });
await seedConversationContext();
await fs.writeFile(path.join(projectRoot, smokeAssetPath), smokeAssetBytes);
await fs.writeFile(
path.join(projectRoot, smokeAudioAssetPath),
@@ -722,6 +770,39 @@ async function seedLocalAsset() {
);
}
async function seedConversationContext() {
const projectConversationPath = path.join(
projectRoot,
'.agent/conversations/project.jsonl',
);
const agentConversationPath = path.join(
projectRoot,
'.agent/conversations/agents/art-asset-plan.jsonl',
);
await fs.mkdir(path.dirname(projectConversationPath), { recursive: true });
await fs.mkdir(path.dirname(agentConversationPath), { recursive: true });
await fs.writeFile(
projectConversationPath,
`${JSON.stringify({
schemaVersion: 'game-creator-conversation.v1',
role: 'user',
content: `玩家坚持使用 ${smokeProjectConversationMarker}`,
agentId: null,
updatedAt: 1,
})}\n`,
);
await fs.writeFile(
agentConversationPath,
`${JSON.stringify({
schemaVersion: 'game-creator-conversation.v1',
role: 'assistant',
content: `API Key sk-smoke-secret\n美术方向 ${smokeAgentConversationMarker}`,
agentId: 'art-asset-plan',
updatedAt: 2,
})}\n`,
);
}
function readHttpText(url) {
return new Promise((resolve, reject) => {
http
+143 -1
View File
@@ -1317,6 +1317,7 @@ dependencies = [
"shared-contracts",
"tauri",
"tauri-build",
"tauri-plugin-dialog",
"tauri-plugin-opener",
"tokio",
"zip",
@@ -2501,6 +2502,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [
"bitflags 2.13.0",
"block2",
"libc",
"objc2",
"objc2-core-foundation",
]
@@ -3205,6 +3207,30 @@ dependencies = [
"web-sys",
]
[[package]]
name = "rfd"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
dependencies = [
"block2",
"dispatch2",
"glib-sys",
"gobject-sys",
"gtk-sys",
"js-sys",
"log",
"objc2",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-foundation",
"raw-window-handle",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows-sys 0.60.2",
]
[[package]]
name = "rustc-hash"
version = "2.1.2"
@@ -4001,6 +4027,48 @@ dependencies = [
"walkdir",
]
[[package]]
name = "tauri-plugin-dialog"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884"
dependencies = [
"log",
"raw-window-handle",
"rfd",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"tauri-plugin-fs",
"thiserror 2.0.18",
"url",
]
[[package]]
name = "tauri-plugin-fs"
version = "2.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371"
dependencies = [
"anyhow",
"dunce",
"glob",
"log",
"objc2-foundation",
"percent-encoding",
"schemars 0.8.22",
"serde",
"serde_json",
"serde_repr",
"tauri",
"tauri-plugin",
"tauri-utils",
"thiserror 2.0.18",
"toml 1.1.2+spec-1.1.0",
"url",
]
[[package]]
name = "tauri-plugin-opener"
version = "2.5.4"
@@ -5147,6 +5215,15 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets 0.53.5",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
@@ -5195,13 +5272,30 @@ dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
"windows_i686_gnullvm 0.52.6",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
"windows-link 0.2.1",
"windows_aarch64_gnullvm 0.53.1",
"windows_aarch64_msvc 0.53.1",
"windows_i686_gnu 0.53.1",
"windows_i686_gnullvm 0.53.1",
"windows_i686_msvc 0.53.1",
"windows_x86_64_gnu 0.53.1",
"windows_x86_64_gnullvm 0.53.1",
"windows_x86_64_msvc 0.53.1",
]
[[package]]
name = "windows-threading"
version = "0.1.0"
@@ -5238,6 +5332,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
@@ -5256,6 +5356,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
@@ -5274,12 +5380,24 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
@@ -5298,6 +5416,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
@@ -5316,6 +5440,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
@@ -5334,6 +5464,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
@@ -5352,6 +5488,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "winnow"
version = "0.5.40"
@@ -15,6 +15,7 @@ platform-agent = { path = "../../../server-rs/crates/platform-agent" }
reqwest = { version = "0.12", default-features = false, features = ["json", "native-tls"] }
shared-contracts = { path = "../../../server-rs/crates/shared-contracts", default-features = false }
tauri = { version = "2.11.2", features = [] }
tauri-plugin-dialog = "2.7.1"
tauri-plugin-opener = "2.5.4"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
zip = { version = "2", default-features = false, features = ["deflate"] }
@@ -1,3 +1,3 @@
pub(crate) mod debug_drafts;
pub(crate) use debug_drafts::*;
pub(crate) use debug_drafts::*;
@@ -4,9 +4,9 @@
//! 且非测试构建中编入二进制;生产 release 与 cargo test 下整个模块与其调用处一并被剔除,
//! 不会往仓库写任何文件。
use crate::unix_millis;
use std::fs;
use std::path::PathBuf;
use crate::unix_millis;
// 找到仓库根(包含 apps/ai-game-creator-shell/src-tauri/Cargo.toml 的目录),
// 以便把草案落到仓库内而非 tmp 项目目录。
File diff suppressed because it is too large Load Diff
@@ -13,12 +13,13 @@
"withGlobalTauri": true,
"windows": [
{
"label": "main",
"label": "launcher",
"title": "AI 游戏创作",
"width": 760,
"height": 820,
"minWidth": 420,
"minHeight": 560
"url": "index.html?launcher",
"width": 820,
"height": 640,
"minWidth": 720,
"minHeight": 520
}
],
"security": {
File diff suppressed because it is too large Load Diff
+9 -2
View File
@@ -1,11 +1,18 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './App';
import { App, WorkspaceLauncher } from './App';
import './styles.css';
function shouldRenderMainApp() {
const params = new URLSearchParams(window.location.search);
return (
params.has('main') || params.has('dev') || window.location.hash === '#dev'
);
}
createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App />
{shouldRenderMainApp() ? <App /> : <WorkspaceLauncher />}
</React.StrictMode>,
);
File diff suppressed because it is too large Load Diff
@@ -18,6 +18,7 @@ describe('AI 游戏创作 Agent loop 摘要', () => {
runId: 'run-test',
commandId: 'game.generate_draft',
status: 'needs-revision',
lifecycleStatus: 'pending',
passes: 2,
maxPasses: 3,
toolCallCount: 12,
@@ -26,6 +27,27 @@ describe('AI 游戏创作 Agent loop 摘要', () => {
goal: '做一个弹幕厨房游戏',
coordination: 'Planner -> Orchestrator -> Generator -> Evaluator',
steps: [
...Array.from({ length: 7 }, (_, index) => ({
pass: 1,
agent: `LLM-${index + 1}`,
phase: 'llm',
taskId: null,
group: null,
role: null,
status: 'completed',
inputPaths: [],
outputPaths: [],
summary: `LLM 调用 ${index + 1}`,
toolCalls: [
{
toolId: `llm.call.${index + 1}`,
status: 'ok',
inputPaths: [],
outputPaths: [],
summary: `LLM 工具 ${index + 1}`,
},
],
})),
{
pass: 2,
agent: 'Orchestrator',
@@ -39,6 +61,19 @@ describe('AI 游戏创作 Agent loop 摘要', () => {
summary: '重跑程序链路及下游发布包装',
toolCalls: [],
},
...Array.from({ length: 4 }, (_, index) => ({
pass: 2,
agent: `Bridge-${index + 1}`,
phase: 'handoff',
taskId: null,
group: null,
role: null,
status: 'completed',
inputPaths: [],
outputPaths: [],
summary: `中间步骤 ${index + 1}`,
toolCalls: [],
})),
{
pass: 2,
agent: '美术组 / Asset',
@@ -66,15 +101,32 @@ describe('AI 游戏创作 Agent loop 摘要', () => {
summary:
'项目还没有画板回流资产;建议用户确认 /sync-canvas-project <画板项目ID>。',
},
...Array.from({ length: 5 }, (_, index) => ({
toolId: `agent.tool.suggest.extra.${index + 1}`,
status: 'suggested',
inputPaths: ['.agent/manifest.json'],
outputPaths: [],
summary: `额外建议命令 ${index + 1}`,
})),
],
},
],
artifacts: [
{
path: '.agent/older-artifact.json',
sizeBytes: 64,
checksum: 'fnv1a64:older',
},
{
path: '.agent/passes/pass-2/task-graph.json',
sizeBytes: 128,
checksum: 'fnv1a64:test',
},
...Array.from({ length: 4 }, (_, index) => ({
path: `.agent/passes/pass-2/artifact-${index + 1}.json`,
sizeBytes: 128 + index,
checksum: `fnv1a64:artifact-${index + 1}`,
})),
],
taskGraph: {
goal: '做一个弹幕厨房游戏',
@@ -93,6 +145,21 @@ describe('AI 游戏创作 Agent loop 摘要', () => {
],
reason: 'code-repair+dependency-impact',
},
{
issue: '缺少输入绑定',
taskIds: ['code-director'],
reason: 'input-binding',
},
{
issue: '缺少胜负条件',
taskIds: ['quality-review'],
reason: 'win-condition',
},
{
issue: '缺少发布说明',
taskIds: ['publish-package'],
reason: 'publish-readme',
},
],
tasks,
},
@@ -130,8 +197,43 @@ describe('AI 游戏创作 Agent loop 摘要', () => {
],
reason: 'code-repair+dependency-impact',
},
{
issue: '缺少输入绑定',
taskIds: ['code-director'],
reason: 'input-binding',
},
{
issue: '缺少胜负条件',
taskIds: ['quality-review'],
reason: 'win-condition',
},
{
issue: '缺少发布说明',
taskIds: ['publish-package'],
reason: 'publish-readme',
},
],
},
{
pass: 3,
mode: 'repair',
summary: '第 3 轮复核',
activeTaskIds: ['quality-review'],
carriedTaskIds: ['design-director'],
dependencyWaves: [['quality-review']],
repairFocus: [],
repairRoutes: [],
},
{
pass: 4,
mode: 'repair',
summary: '第 4 轮收尾',
activeTaskIds: ['publish-package'],
carriedTaskIds: [],
dependencyWaves: [['publish-package']],
repairFocus: [],
repairRoutes: [],
},
],
nextStep: 'repair-next-pass',
error: null,
@@ -140,7 +242,9 @@ describe('AI 游戏创作 Agent loop 摘要', () => {
const summary = summarizeAgentRunTrace(trace);
expect(summary).toContain('needs-revision · 2/3 轮 · max-passes-exhausted');
expect(summary).toContain(
'needs-revision / pending · 2/3 轮 · max-passes-exhausted',
);
expect(summary).toContain('工具调用:12/128');
expect(summary).toContain('任务:已完成 2');
expect(summary).toContain(
@@ -153,14 +257,28 @@ describe('AI 游戏创作 Agent loop 摘要', () => {
expect(summary).toContain(
'返工路线:code-repair+dependency-impact: 程序组 / Director 拆解程序实现(code-director), 程序组 / Review 执行质量评审(quality-review), 程序组 / Preview 执行静态自检(preview-readiness), 运营组 / Publish 整理发布包装(publish-package)',
);
expect(summary).toContain('还有 1 条路线');
expect(summary).not.toContain('publish-readme:');
expect(summary).toContain('建议命令:');
expect(summary).toContain('agent.tool.suggest.canvas.project_sync');
expect(summary).toContain('/sync-canvas-project <画板项目ID>');
expect(summary).toContain('agent.tool.suggest.extra.4');
expect(summary).not.toContain('agent.tool.suggest.extra.5');
expect(summary).toContain('还有 1 个建议命令');
expect(summary).toContain('编排轮次:');
expect(summary).toContain(
'pass 2 · repair · active 3 · carry 1 · waves 程序组 / Director 拆解程序实现(code-director) / 程序组 / Review 执行质量评审(quality-review) / 运营组 / Publish 整理发布包装(publish-package)',
'pass 2 · repair · active 程序组 / Director 拆解程序实现(code-director), 程序组 / Review 执行质量评审(quality-review), 运营组 / Publish 整理发布包装(publish-package) · carry 策划组 / Director 拆解创作方向(design-director) · waves 程序组 / Director 拆解程序实现(code-director) / 程序组 / Review 执行质量评审(quality-review) / 运营组 / Publish 整理发布包装(publish-package) · repair gameHtml 缺少 canvas · routes code-repair+dependency-impact: 程序组 / Director 拆解程序实现(code-director), 程序组 / Review 执行质量评审(quality-review), 程序组 / Preview 执行静态自检(preview-readiness), 运营组 / Publish 整理发布包装(publish-package)',
);
expect(summary).not.toContain('pass 1 · initial');
expect(summary).toContain('还有 1 个较早轮次');
expect(summary).toContain('.agent/passes/pass-2/task-graph.json');
expect(summary).toContain('Orchestrator #2 · completed · plan');
expect(summary).not.toContain('.agent/older-artifact.json');
expect(summary).toContain('还有 1 个较早产物');
expect(summary).not.toContain('Orchestrator #2 · completed · plan');
expect(summary).toContain('Bridge-1 #2 · completed · handoff');
expect(summary).toContain('还有 8 个较早步骤');
expect(summary).toContain('LLM-2 #1 · completed · llm · llm.call.2');
expect(summary).not.toContain('LLM-1 #1 · completed · llm · llm.call.1');
expect(summary).toContain('还有 1 个较早 LLM 步骤');
});
});
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest';
import {
deriveAgentStatusCards,
isAbsoluteProjectPath,
needsInitializedChatProject,
parseRememberInput,
@@ -8,6 +9,11 @@ import {
resolveChatProjectPath,
resolvePendingCommandProjectPath,
} from '../src/App';
import {
createGameCreationAppManifest,
GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
type GameCreationAgentRunTrace,
} from '../../../packages/shared/src/contracts/gameCreationApp';
describe('AI 游戏创作聊天记忆命令', () => {
it('recognizes local project absolute paths across desktop platforms', () => {
@@ -17,7 +23,7 @@ describe('AI 游戏创作聊天记忆命令', () => {
expect(isAbsoluteProjectPath('relative-game')).toBe(false);
});
it('defaults /remember to long memory and supports short memory scope', () => {
it('defaults /remember to long memory and supports short and blackboard scopes', () => {
expect(parseRememberInput('主角喜欢反弹弹幕')).toEqual({
scope: 'long',
content: '主角喜欢反弹弹幕',
@@ -34,6 +40,14 @@ describe('AI 游戏创作聊天记忆命令', () => {
scope: 'long',
content: '覆盖后的长期设定',
});
expect(parseRememberInput('blackboard 共享美术约束')).toEqual({
scope: 'blackboard',
content: '共享美术约束',
});
expect(parseRememberInput('黑板 统一使用俯视角')).toEqual({
scope: 'blackboard',
content: '统一使用俯视角',
});
});
it('describes append and replace memory writes before confirmation', () => {
@@ -59,6 +73,17 @@ describe('AI 游戏创作聊天记忆命令', () => {
'/tmp/game',
),
).toBe('覆盖保存到 /tmp/game/memory/session.md');
expect(
pendingCommandDetail(
{
id: 'memory.write',
scope: 'blackboard',
content: '共享美术约束',
mode: 'append',
},
'/tmp/game',
),
).toBe('追加到 /tmp/game/memory/blackboard.md');
});
it('requires an initialized local project before chat memory writes', () => {
@@ -66,6 +91,8 @@ describe('AI 游戏创作聊天记忆命令', () => {
expect(resolveChatProjectPath({ projectPath: '/tmp/game' })).toBe(
'/tmp/game',
);
expect(resolveChatProjectPath({ projectPath: 'relative-game' })).toBeNull();
expect(resolveChatProjectPath({ projectPath: '/tmp/bad\u0007game' })).toBeNull();
expect(parseRememberInput('long')).toEqual({
scope: 'long',
content: '',
@@ -83,6 +110,15 @@ describe('AI 游戏创作聊天记忆命令', () => {
'/tmp/game',
),
).toBe('删除 /tmp/game/memory/session.md');
expect(
pendingCommandDetail(
{
id: 'memory.delete',
scope: 'blackboard',
},
'/tmp/game',
),
).toBe('删除 /tmp/game/memory/blackboard.md');
});
it('describes local generation side effects before confirmation', () => {
@@ -99,6 +135,15 @@ describe('AI 游戏创作聊天记忆命令', () => {
);
});
it('describes preview side effects before confirmation', () => {
expect(pendingCommandDetail({ id: 'preview.start' }, '/tmp/game')).toBe(
'启动 /tmp/game/game/ 并交给外部浏览器',
);
expect(pendingCommandDetail({ id: 'preview.open' }, '/tmp/game')).toBe(
'打开 /tmp/game 的当前本地预览',
);
});
it('requires a project before chat commands write or run local artifacts', () => {
expect(needsInitializedChatProject('game.generate_draft')).toBe(true);
expect(needsInitializedChatProject('asset.upload')).toBe(true);
@@ -128,7 +173,7 @@ describe('AI 游戏创作聊天记忆命令', () => {
{ id: 'project.restore', checkpointId: 'checkpoint-1' },
'/tmp/game',
),
).toBe('从 checkpoint-1 恢复已跟踪项目文件');
).toBe('从 checkpoint-1 恢复 /tmp/game 的已跟踪项目文件');
expect(
pendingCommandDetail(
{
@@ -140,7 +185,38 @@ describe('AI 游戏创作聊天记忆命令', () => {
},
'/tmp/game',
),
).toBe('写入 /tmp/game/.agent/policy.json');
).toBe('写入 /tmp/game/.agent/policy.json · 拒绝:file.write · 确认:无');
});
it('describes canvas asset imports before confirmation', () => {
expect(
pendingCommandDetail(
{
id: 'canvas.asset_import',
localPath: 'assets/hero.png',
canvasProjectId: 'canvas-project-1',
canvasAssetId: '',
canvasAssetObjectId: 'asset-object-1',
kind: 'character',
mediaType: 'image/png',
},
'/tmp/game',
),
).toBe(
'导入 /tmp/game/assets/hero.png · 画板 canvas-project-1 / object:asset-object-1 · character · image/png',
);
expect(
pendingCommandDetail(
{
id: 'canvas.export_import',
exportPath: '/tmp/canvas-export.zip',
canvasProjectId: 'canvas-project-1',
},
'/tmp/game',
),
).toBe(
'导入 /tmp/canvas-export.zip 到 /tmp/game/assets/canvas-imports/ · 画板 canvas-project-1',
);
});
it('describes agent run lifecycle controls before confirmation', () => {
@@ -148,14 +224,16 @@ describe('AI 游戏创作聊天记忆命令', () => {
'标记 /tmp/game/.agent/run.latest.json 为 killed,并写入 activity/output',
);
expect(pendingCommandDetail({ id: 'agent.retry' }, '/tmp/game')).toBe(
'标记 /tmp/game/.agent/run.latest.json 为 pending,等待 runner claim',
'使用 /tmp/game/.agent/run.latest.json 的目标重新运行一次',
);
expect(
pendingCommandDetail(
{ id: 'agent.resume', detail: '继续修复输入监听' },
'/tmp/game',
),
).toBe('附加用户说明并标记 /tmp/game/.agent/run.latest.json 为 pending');
).toBe(
'附加说明「继续修复输入监听」,继续运行 /tmp/game/.agent/run.latest.json 的目标',
);
});
it('shows the authorized project path in pending write command details', () => {
@@ -174,4 +252,64 @@ describe('AI 游戏创作聊天记忆命令', () => {
),
).toBe('/new/game');
});
it('derives per-agent status cards from manifest and latest trace steps', () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const trace = {
schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
runId: 'run-agent-status',
commandId: 'game.generate_draft',
status: 'passed',
passes: 1,
maxPasses: 3,
toolCallCount: 1,
maxToolCalls: 128,
stopReason: 'evaluator-passed',
goal: '像素动作',
coordination: 'filesystem',
steps: [
{
pass: 1,
agent: 'Planner',
phase: 'plan',
taskId: 'design-director',
group: 'design',
role: 'Director',
status: 'completed',
inputPaths: [],
outputPaths: ['.agent/spec.md'],
summary: '拆解完成',
toolCalls: [],
},
],
artifacts: [],
taskGraph: {
goal: '像素动作',
readyTaskIds: [],
activeTaskIds: [],
carriedTaskIds: [],
repairFocus: [],
repairRoutes: [],
tasks: manifest.tasks,
},
passPlans: [],
nextStep: 'manual-playtest',
error: null,
updatedAt: 1,
} satisfies GameCreationAgentRunTrace;
const traced = deriveAgentStatusCards(manifest, trace);
expect(traced[0]).toMatchObject({
id: 'design-director',
title: '拆解创作方向',
status: 'completed',
summary: '拆解完成',
});
const fallback = deriveAgentStatusCards(manifest, null);
expect(fallback[0]?.summary).toBe('创作目标、范围和专业组分工明确');
});
});