完善AI游戏创作App本地工作区与Agent协作
补齐工作区启动器、非空目录确认和主窗口切换入口 改为运行时配置文件并支持全局与单Agent LLM Provider 接入平台 External API 生成和同步美术资产 补齐本地对话记录、Agent私有记忆和项目黑板 完善Agent状态、历史Run、快照、文件和资产快捷入口 同步AI游戏创作App实施计划和共享决策记录
This commit is contained in:
@@ -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
@@ -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": {
|
||||
|
||||
+6420
-665
File diff suppressed because it is too large
Load Diff
@@ -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('创作目标、范围和专业组分工明确');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user