完善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('创作目标、范围和专业组分工明确');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,10 +16,22 @@
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-01 AI 游戏创作 App v1 使用本地 JSONL 对话和派生 Agent 状态
|
||||
|
||||
- 背景:AI 游戏创作 App 已有 Godcoder 式本地工程护栏、项目黑板、角色私有记忆、manifest 和 run trace;新增结构化对话记录、agent 状态列表和单 agent 对话入口时,需要避免引入平行状态源或提前承诺后台 runner 能力。
|
||||
- 决策:v1 结构化对话记录统一使用本地 `.agent/conversations/` append-only JSONL。普通聊天写 `.agent/conversations/project.jsonl`;从 agent 状态列表进入单个 agent 后,用户消息、agent 回复、工具建议和错误只写对应 `.agent/conversations/agents/<agentId>.jsonl`。Agent 状态列表从 `.agent/manifest.json` 的任务 / 角色清单和 `.agent/run.latest.json` / `.agent/runs/<runId>.json` 的 step、taskGraph、passPlans、lifecycleStatus 派生,并把 `taskGraph.tasks` 的任务状态与 active / carry-over / ready 编排标记显示在主窗口和单 agent 对话入口中;单 agent 最近证据里的安全相对输入 / 输出路径只填入 `/read <path>` 草稿,仍由用户发送并走既有 `file.read` / `agent.trace_read` 权限流。不新增独立状态数据库。项目黑板和角色私有记忆继续只保存稳定摘要,不承载原始对话流水。
|
||||
- 补充:App 启动先进入独立启动器窗口;用户选择工作区后,Tauri 关闭启动器并打开主窗口,主窗口读取该工作区的主 conversation、manifest 和 run trace。启动器“新建项目”先调用 `init_local_game_project` 初始化成功,默认项目名取目标文件夹名,再写最近项目并打开主窗口;遇到非空目录时先弹确认,取消或初始化失败则不打开主窗口、不写最近项目。最近工作区只保存在本机 WebView storage,可单项移除或清空,不进入项目文件或共享记忆;已初始化项目优先显示 manifest 项目名并保留路径副信息,`.agent/run.latest.json` 可读时显示最近 run 状态。最近项目路径缺失、不是目录、缺少可读 `.agent/manifest.json` 或检查失败时禁用打开,刷新只重新执行只读检查;“显示”只用系统文件管理器打开已确认存在的本地目录,未初始化但存在的目录也可显示,避免把历史路径误当新项目重建。
|
||||
- 补充:主窗口“显示目录”复用同一只读目录打开能力,只打开当前本地项目目录,不初始化项目、不写项目文件、不切换工作区;主窗口头部只读显示 manifest 项目名、项目路径和最近 `.agent/run.latest.json` 的 run 状态摘要,并通过“刷新状态”重新读取同一 trace,不新增状态数据库。最近项目资产入口只读展示 localPath、kind、mediaType 和 source.kind,点击仍走原 `file.read` 权限流。
|
||||
- 补充:启动器和主窗口共用同一个运行时配置弹窗,配置只读写 Tauri 应用配置目录中的 `game-creator.config.json`,不写入项目文件或对话历史。
|
||||
- 补充:启动器“打开”只进入已初始化且 `.agent/manifest.json` 可读的 AI 游戏项目;路径不存在、不是文件夹或只是普通文件夹时不打开主窗口、不创建目录,用户需要创建或初始化时走“新建项目”。
|
||||
- 影响范围:`apps/ai-game-creator-shell` 的主窗口 agent 状态列表、单 agent 对话入口、本地项目文件结构、共享契约和 AI 游戏创作 App 实施计划。
|
||||
- 验证方式:文档更新先运行 `npm run check:encoding` 和 `git diff --check`;后续工程落地时补充壳 typecheck、Tauri Rust 测试和对话 JSONL / 状态派生的定向测试。
|
||||
- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。
|
||||
|
||||
## 2026-06-30 AI 游戏创作 App 使用客户端配置文件
|
||||
|
||||
- 背景:`apps/ai-game-creator-shell` 是客户端 App,不应通过 `.env` 或进程环境变量承载 LLM / 画板同步配置;旧口径会让本地 secrets、CLI wrapper 和桌面 App 启动逻辑混在一起。
|
||||
- 决策:仓库内 `apps/ai-game-creator-shell/game-creator.config.json` 只作为默认模板;发布 App 启动时在 Tauri 应用配置目录写入默认 `game-creator.config.json`,真实密钥和本机覆盖项都保存在该运行时配置文件中。主窗口提供“配置”面板读写该运行时 JSON;开发 CLI 无 AppHandle 时才回退读取仓库旁边的模板和 gitignored 本机覆盖文件。`llm.apiKey/baseUrl/model/protocol/stream/requestTimeoutMs/maxRetries/retryBackoffMs` 驱动真实 LLM 路径,`editorApi.baseUrl/apiKey` 驱动画板项目同步;`/llm-status` 只展示 baseUrl、model、protocol 和 API Key 是否存在,不显示密钥。
|
||||
- 决策:仓库内 `apps/ai-game-creator-shell/game-creator.config.json` 只作为默认模板;发布 App 启动时在 Tauri 应用配置目录写入默认 `game-creator.config.json`,真实密钥和本机覆盖项都保存在该运行时配置文件中。主窗口提供“配置”面板读写该运行时 JSON;开发 CLI 无 AppHandle 时才回退读取仓库旁边的模板和 gitignored 本机覆盖文件。`llm.apiKey/baseUrl/model/apiKind/stream/requestTimeoutMs/maxRetries/retryBackoffMs` 驱动全局 LLM 路径,`agentLlm.<agentId>` 可为 Planner、Generator 和角色 agent 单独覆盖 API Key、base URL、模型、API 类型和流式请求,空项继承全局配置;`editorApi.baseUrl/apiKey` 驱动画板项目同步;`/llm-status` 只展示全局和各 agent resolved 后的 baseUrl、model、apiKind、stream 和 API Key 是否存在,不显示密钥。生成游戏或平台美术遇到 LLM / editorApi 缺配置错误时,主窗口自动打开运行时配置弹窗,但错误消息仍只显示缺失项,不回显密钥值。
|
||||
- 影响范围:AI 游戏创作 App 的 Tauri Rust 配置加载、主窗口配置面板、CLI wrapper、agent-run smoke、`check-config` 门禁、`.gitignore` 和实施计划文档。
|
||||
- 验证方式:运行 `npm run ai-game-creator-shell:typecheck`、`cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml`、`npm run check:encoding` 和 `git diff --check`。
|
||||
- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。
|
||||
@@ -3841,7 +3853,7 @@
|
||||
- 2026-06-25 调整:正式用户 App 不承载游戏预览画面,release CSP 不允许 `frame-src http://127.0.0.1:*`;只有开发窗口 / dev CSP 可以嵌入本地预览 iframe。`/preview`、`/run` 和生成完成后的用户侧路径只启动 `127.0.0.1` HTTP preview 并通过 `open_local_game_preview` 交给系统外部浏览器。
|
||||
- 2026-06-25 调整:`project.create` 成功后的 durable 权限证据必须在聊天 `/project` 和开发窗口初始化两条入口统一写入 `.agent/logs/command.log`,避免同一能力因为入口不同导致 `/audit` 或开发排障证据不一致。
|
||||
- 2026-06-25 调整:`.agent/run.latest.json` 和 `.agent/runs/<runId>.json` 必须记录 loop 的 `maxPasses` 与 `stopReason`,开发窗口直接展示该状态,避免只从 summary 文案推断 loop 是否跑满、通过、返工、写入产物或进入预览。本地 HTTP 预览的 `/` 映射到 `game/index.html`,路径解析必须 canonicalize 项目根目录和目标文件,只允许访问项目内 `game/` 与 `assets/`,拒绝 `memory/`、`.agent/`、`exports/`、`..`、反斜杠和符号链接越界;常见图片、音频、视频和 Web 资源必须返回对应 MIME。这样上传和画板回流资产能被生成游戏引用,但记忆、trace 和导出包不会被预览服务暴露。
|
||||
- 2026-06-26 调整:AI 游戏创作 App 借鉴 Harbour 的控制平面思想,但不搬 Harbour 后台。最近 run 在 `.agent/run.latest.json` 增加可选 `lifecycleStatus`,并通过 `/agent-status`、`/agent-kill`、`/agent-retry`、`/agent-resume [说明]` 控制本地生命周期,写入 `.agent/activity.jsonl`、`.agent/output.jsonl` 和 `.agent/context.bundle.json`。v1 的 kill/retry/resume 只更新本地状态和上下文包,不伪装成能中断已发出的上游 LLM 请求;后续引入独立 runner 后再把 `pending` 接入 claim。
|
||||
- 2026-06-26 调整,2026-07-03 更新:AI 游戏创作 App 借鉴 Harbour 的控制平面思想,但不搬 Harbour 后台。最近 run 在 `.agent/run.latest.json` 增加可选 `lifecycleStatus`,并通过 `/agent-status`、`/agent-kill`、`/agent-retry`、`/agent-resume [说明]` 控制本地生命周期,写入 `.agent/activity.jsonl`、`.agent/output.jsonl` 和 `.agent/context.bundle.json`;聊天里的状态 / 控制结果可填入 `/read .agent/output.jsonl` 草稿继续查看 run 输出,但不直接读取文件或绕过 `file.read` 策略。v1 的 kill/retry/resume 只更新本地状态和上下文包,不伪装成能中断已发出的上游 LLM 请求;后续引入独立 runner 后再把 `pending` 接入 claim。
|
||||
- 2026-06-25 调整:本地 HTTP 预览静态 `HEAD` 必须返回与 `GET` 相同的真实 `Content-Length`,但不返回 body;浏览器、图片、音频和视频探测不能拿到 `Content-Length: 0` 的假响应。
|
||||
- 2026-06-25 调整:普通用户通过聊天输入 `/run` 触发待确认 `game.run_local`,确认后只能复用白名单 `game.static_smoke` 自检当前 `game/index.html`,通过后启动 `127.0.0.1` 本地 HTTP 预览。独立执行 `game.static_smoke` 时如果已有 `.agent/run.latest.json`,必须追加 `Playtest / game.static_smoke` trace step,避免“运行了代码但编排 trace 不可见”。
|
||||
- 2026-06-25 调整:普通用户通过聊天输入 `/trace` 触发只读 `agent.trace_read`,读取 `.agent/run.latest.json` 并在聊天里摘要 loop 轮次、stopReason、nextStep、active / carry-over 任务、repairRoutes、agent 建议命令和最近 step。trace 面板仍只在开发窗口展示,普通用户窗口不新增面板。
|
||||
@@ -3852,11 +3864,12 @@
|
||||
- 2026-06-24 调整:同一本地项目多次 `game.generate_draft` 必须追加 `memory/session.md` 与 `memory/project.md`,不得覆盖历史对话和创作目标记录。
|
||||
- 2026-07-01 调整:AI 游戏创作 App 在 `memory/session.md` 与 `memory/project.md` 之外新增项目级黑板 `memory/blackboard.md`,只记录重要跨 agent 决策、依赖和风险摘要;每个角色 agent 拥有私有记忆 `memory/agents/<group>/<role>.md`。角色 brief 必须读取自己的私有记忆和项目黑板;`game.generate_draft` 通过 Evaluator 与 `game.static_smoke` 后,追加项目黑板摘要和各角色成功产出摘要,不得覆盖既有记忆。失败 run 仍只保留 trace 和 pass 快照,不写最终记忆摘要。
|
||||
- 2026-07-01 调整:AI 游戏创作 App 借鉴 Godcoder 的本地工程护栏,但只收敛到五项本地机制:`ArtifactWriter` 写入前 checkpoint、写入后 diff、用户确认 restore;进入 LLM 前过滤密钥和本机配置痕迹;`.agent/agent.db` 继续作为轻量 JSONL 项目索引,`/index` 额外刷新 `.agent/project.index.json`;同一项目写入通过 `.agent/project.lock` 串行化;`.agent/policy.json` 记录项目级命令拒绝 / 确认策略。v1 不引入通用 IDE 插件、云工作区、SQLite 或任意 shell 代理。
|
||||
- 2026-07-03 调整:主窗口最近 checkpoint 列表必须直接展示 checkpoint id、文件数、大小和创建时间,并只提供填充对比与确认回滚的轻量操作;回滚仍走 `project.restore` 确认卡,不在列表按钮中直接写项目文件。
|
||||
- 2026-06-24 调整:普通用户通过聊天输入 `/help` 发现可用内置命令;命令发现必须留在聊天消息里,不得因此暴露开发面板。
|
||||
- 2026-06-24 调整:聊天区待确认命令的日志语义必须区分 `permission.pending`、`permission.confirm` 和 `permission.cancel`;待确认卡片必须展示本地写入目标路径,避免用户在不知道落盘位置时确认。
|
||||
- 2026-06-24 调整:普通用户通过聊天输入 `/status` 读取 `.agent/manifest.json` 的项目状态摘要,只在聊天消息里展示项目目录、任务状态、资产数量、预览状态和最近命令;不得为了状态查看暴露任务、文件或日志面板。
|
||||
- 2026-06-24 调整:普通用户通过聊天输入 `/files` 触发只读 `file.list`,只在聊天消息里展示本地项目文件摘要;不得把文件读写面板暴露到普通用户窗口。
|
||||
- 2026-06-24 调整:普通用户通过聊天输入 `/assets` 触发只读 `asset.list`,只在聊天消息里展示本地项目资产路径、类型和来源;不得把资产面板暴露到普通用户窗口。
|
||||
- 2026-06-24 调整,2026-07-03 更新:普通用户通过聊天输入 `/assets` 触发只读 `asset.list`,只在聊天消息里展示本地项目资产路径、类型和来源;资产列表消息可以填入首个资产的 `/read` 草稿,方便从聊天继续查看资产文本元数据,但仍不直接读取文件或绕过聊天命令;不得把资产面板暴露到普通用户窗口。
|
||||
- 2026-06-24 调整:普通用户通过聊天输入 `/read 本地相对路径` 触发只读 `file.read`,只在聊天消息里展示项目内文本文件并截断长文本;不得开放聊天里的文件写入或删除能力。
|
||||
- 2026-06-24 调整:普通用户通过聊天输入 `/tasks` 触发只读 `task.list`,只在聊天消息里展示专业组、角色、任务状态和产物交接;不得把任务面板暴露到普通用户窗口。
|
||||
- 2026-06-24 调整:普通用户只能通过聊天触发内置命令;当前 `/smoke` 映射到白名单 `command.run_limited game.static_smoke` 并走待确认卡片,不允许扩展成任意 shell 或自由命令解析。
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -68,6 +68,11 @@ describe('AI 游戏创作 App 共享契约', () => {
|
||||
GAME_CREATION_APP_COMMANDS.find((command) => command.id === 'asset.list')
|
||||
?.permission,
|
||||
).toBe('auto');
|
||||
expect(
|
||||
GAME_CREATION_APP_COMMANDS.find(
|
||||
(command) => command.id === 'asset.register',
|
||||
)?.permission,
|
||||
).toBe('confirm');
|
||||
expect(
|
||||
GAME_CREATION_APP_COMMANDS.find(
|
||||
(command) => command.id === 'preview.open',
|
||||
@@ -86,6 +91,16 @@ describe('AI 游戏创作 App 共享契约', () => {
|
||||
GAME_CREATION_APP_COMMANDS.find((command) => command.id === 'memory.read')
|
||||
?.permission,
|
||||
).toBe('auto');
|
||||
expect(
|
||||
GAME_CREATION_APP_COMMANDS.find(
|
||||
(command) => command.id === 'conversation.read',
|
||||
)?.permission,
|
||||
).toBe('auto');
|
||||
expect(
|
||||
GAME_CREATION_APP_COMMANDS.find(
|
||||
(command) => command.id === 'conversation.write',
|
||||
)?.permission,
|
||||
).toBe('auto');
|
||||
expect(
|
||||
GAME_CREATION_APP_COMMANDS.find(
|
||||
(command) => command.id === 'canvas.project_open',
|
||||
@@ -96,6 +111,11 @@ describe('AI 游戏创作 App 共享契约', () => {
|
||||
(command) => command.id === 'canvas.project_sync',
|
||||
)?.permission,
|
||||
).toBe('confirm');
|
||||
expect(
|
||||
GAME_CREATION_APP_COMMANDS.find(
|
||||
(command) => command.id === 'canvas.asset_generate',
|
||||
)?.permission,
|
||||
).toBe('confirm');
|
||||
expect(
|
||||
GAME_CREATION_APP_COMMANDS.find(
|
||||
(command) => command.id === 'canvas.export_import',
|
||||
@@ -124,11 +144,17 @@ describe('AI 游戏创作 App 共享契约', () => {
|
||||
'repair-loop-carryover',
|
||||
'short-term-memory',
|
||||
'long-term-memory',
|
||||
'conversation-history',
|
||||
'canvas-project-sync',
|
||||
'local-preview',
|
||||
'developer-window',
|
||||
]),
|
||||
);
|
||||
expect(
|
||||
GAME_CREATION_AGENT_CAPABILITIES.find(
|
||||
(capability) => capability.id === 'conversation-history',
|
||||
)?.title,
|
||||
).toBe('对话记录上下文');
|
||||
});
|
||||
|
||||
it('keeps at least one concrete limited run command for local verification', () => {
|
||||
|
||||
@@ -39,7 +39,7 @@ export const GAME_CREATION_APP_COMMANDS = [
|
||||
{ id: 'file.delete', permission: 'confirm' },
|
||||
{ id: 'asset.list', permission: 'auto' },
|
||||
{ id: 'asset.upload', permission: 'confirm' },
|
||||
{ id: 'asset.register', permission: 'auto' },
|
||||
{ id: 'asset.register', permission: 'confirm' },
|
||||
{ id: 'preview.start', permission: 'confirm' },
|
||||
{ id: 'preview.open', permission: 'confirm' },
|
||||
{ id: 'preview.stop', permission: 'auto' },
|
||||
@@ -48,10 +48,13 @@ export const GAME_CREATION_APP_COMMANDS = [
|
||||
{ id: 'canvas.project_open', permission: 'confirm' },
|
||||
{ id: 'canvas.project_sync', permission: 'confirm' },
|
||||
{ id: 'canvas.asset_import', permission: 'confirm' },
|
||||
{ id: 'canvas.asset_generate', permission: 'confirm' },
|
||||
{ id: 'canvas.export_import', permission: 'confirm' },
|
||||
{ id: 'memory.read', permission: 'auto' },
|
||||
{ id: 'memory.write', permission: 'confirm' },
|
||||
{ id: 'memory.delete', permission: 'confirm' },
|
||||
{ id: 'conversation.read', permission: 'auto' },
|
||||
{ id: 'conversation.write', permission: 'auto' },
|
||||
] as const satisfies readonly GameCreationAppCommandDescriptor[];
|
||||
|
||||
export interface GameCreationAgentCapabilityDescriptor {
|
||||
@@ -106,6 +109,11 @@ export const GAME_CREATION_AGENT_CAPABILITIES = [
|
||||
},
|
||||
{ id: 'short-term-memory', area: 'agent-runtime', title: '短期记忆' },
|
||||
{ id: 'long-term-memory', area: 'agent-runtime', title: '长期记忆' },
|
||||
{
|
||||
id: 'conversation-history',
|
||||
area: 'agent-runtime',
|
||||
title: '对话记录上下文',
|
||||
},
|
||||
{ id: 'local-artifacts', area: 'local-runtime', title: '本地产物保存' },
|
||||
{ id: 'project-checkpoints', area: 'local-runtime', title: '项目快照与恢复' },
|
||||
{ id: 'project-index', area: 'local-runtime', title: '本地项目索引' },
|
||||
|
||||
@@ -1938,8 +1938,12 @@ function assertGeneratedNativeShellArtifactsAreIgnored() {
|
||||
|
||||
function assertAiGameCreatorShellUserDevBoundary() {
|
||||
const windows = aiGameCreatorShellTauriConfig.app?.windows ?? [];
|
||||
if (windows.length !== 1 || windows[0]?.label !== 'main') {
|
||||
throw new Error('AI game creator release shell must register only the main chat window');
|
||||
if (
|
||||
windows.length !== 1 ||
|
||||
windows[0]?.label !== 'launcher' ||
|
||||
windows[0]?.url !== 'index.html?launcher'
|
||||
) {
|
||||
throw new Error('AI game creator release shell must register only the launcher window');
|
||||
}
|
||||
const releaseCsp = aiGameCreatorShellTauriConfig.app?.security?.csp ?? '';
|
||||
const devCsp = aiGameCreatorShellTauriConfig.app?.security?.devCsp ?? '';
|
||||
@@ -2015,6 +2019,33 @@ function assertAiGameCreatorShellUserDevBoundary() {
|
||||
throw new Error(`AI game creator developer window boundary drifted: missing ${snippet}`);
|
||||
}
|
||||
}
|
||||
|
||||
const workspaceWindowCommandIndex = aiGameCreatorShellTauriSource.indexOf(
|
||||
'fn open_game_creator_workspace_window(',
|
||||
);
|
||||
const launcherWindowCommandIndex = aiGameCreatorShellTauriSource.indexOf(
|
||||
'fn open_game_creator_launcher_window(',
|
||||
);
|
||||
const developerWindowIndex = aiGameCreatorShellTauriSource.indexOf(
|
||||
'#[cfg(debug_assertions)]\nfn open_developer_window(',
|
||||
);
|
||||
const workspaceWindowCommandSource = aiGameCreatorShellTauriSource.slice(
|
||||
workspaceWindowCommandIndex,
|
||||
launcherWindowCommandIndex,
|
||||
);
|
||||
const launcherWindowCommandSource = aiGameCreatorShellTauriSource.slice(
|
||||
launcherWindowCommandIndex,
|
||||
developerWindowIndex,
|
||||
);
|
||||
if (
|
||||
workspaceWindowCommandIndex < 0 ||
|
||||
launcherWindowCommandIndex < 0 ||
|
||||
developerWindowIndex < 0 ||
|
||||
!workspaceWindowCommandSource.includes('window.close().map_err(|error| error.to_string())?;') ||
|
||||
!launcherWindowCommandSource.includes('window.close().map_err(|error| error.to_string())?;')
|
||||
) {
|
||||
throw new Error('AI game creator workspace switch must close the source window');
|
||||
}
|
||||
}
|
||||
|
||||
function collectProductionShellFiles(entryPath) {
|
||||
|
||||
@@ -21,7 +21,7 @@ pub struct GameCreationAppCommandDescriptor {
|
||||
pub permission: GameCreationAppPermission,
|
||||
}
|
||||
|
||||
pub const GAME_CREATION_APP_COMMANDS: [GameCreationAppCommandDescriptor; 39] = [
|
||||
pub const GAME_CREATION_APP_COMMANDS: [GameCreationAppCommandDescriptor; 42] = [
|
||||
command("help.show", GameCreationAppPermission::Auto),
|
||||
command("project.create", GameCreationAppPermission::Confirm),
|
||||
command("project.status", GameCreationAppPermission::Auto),
|
||||
@@ -48,7 +48,7 @@ pub const GAME_CREATION_APP_COMMANDS: [GameCreationAppCommandDescriptor; 39] = [
|
||||
command("file.delete", GameCreationAppPermission::Confirm),
|
||||
command("asset.list", GameCreationAppPermission::Auto),
|
||||
command("asset.upload", GameCreationAppPermission::Confirm),
|
||||
command("asset.register", GameCreationAppPermission::Auto),
|
||||
command("asset.register", GameCreationAppPermission::Confirm),
|
||||
command("preview.start", GameCreationAppPermission::Confirm),
|
||||
command("preview.open", GameCreationAppPermission::Confirm),
|
||||
command("preview.stop", GameCreationAppPermission::Auto),
|
||||
@@ -57,10 +57,13 @@ pub const GAME_CREATION_APP_COMMANDS: [GameCreationAppCommandDescriptor; 39] = [
|
||||
command("canvas.project_open", GameCreationAppPermission::Confirm),
|
||||
command("canvas.project_sync", GameCreationAppPermission::Confirm),
|
||||
command("canvas.asset_import", GameCreationAppPermission::Confirm),
|
||||
command("canvas.asset_generate", GameCreationAppPermission::Confirm),
|
||||
command("canvas.export_import", GameCreationAppPermission::Confirm),
|
||||
command("memory.read", GameCreationAppPermission::Auto),
|
||||
command("memory.write", GameCreationAppPermission::Confirm),
|
||||
command("memory.delete", GameCreationAppPermission::Confirm),
|
||||
command("conversation.read", GameCreationAppPermission::Auto),
|
||||
command("conversation.write", GameCreationAppPermission::Auto),
|
||||
];
|
||||
|
||||
const fn command(
|
||||
@@ -78,7 +81,7 @@ pub struct GameCreationAgentCapabilityDescriptor {
|
||||
pub title: &'static str,
|
||||
}
|
||||
|
||||
pub const GAME_CREATION_AGENT_CAPABILITIES: [GameCreationAgentCapabilityDescriptor; 26] = [
|
||||
pub const GAME_CREATION_AGENT_CAPABILITIES: [GameCreationAgentCapabilityDescriptor; 27] = [
|
||||
capability("chat", "user", "聊天入口"),
|
||||
capability("file-upload", "user", "上传文件"),
|
||||
capability("built-in-commands", "agent-runtime", "内置命令调用"),
|
||||
@@ -108,6 +111,7 @@ pub const GAME_CREATION_AGENT_CAPABILITIES: [GameCreationAgentCapabilityDescript
|
||||
),
|
||||
capability("short-term-memory", "agent-runtime", "短期记忆"),
|
||||
capability("long-term-memory", "agent-runtime", "长期记忆"),
|
||||
capability("conversation-history", "agent-runtime", "对话记录上下文"),
|
||||
capability("local-artifacts", "local-runtime", "本地产物保存"),
|
||||
capability("project-checkpoints", "local-runtime", "项目快照与恢复"),
|
||||
capability("project-index", "local-runtime", "本地项目索引"),
|
||||
@@ -696,6 +700,15 @@ mod tests {
|
||||
.expect("command should exist");
|
||||
assert_eq!(asset_list.permission, GameCreationAppPermission::Auto);
|
||||
|
||||
let asset_register = GAME_CREATION_APP_COMMANDS
|
||||
.iter()
|
||||
.find(|command| command.id == "asset.register")
|
||||
.expect("command should exist");
|
||||
assert_eq!(
|
||||
asset_register.permission,
|
||||
GameCreationAppPermission::Confirm
|
||||
);
|
||||
|
||||
let preview_open = GAME_CREATION_APP_COMMANDS
|
||||
.iter()
|
||||
.find(|command| command.id == "preview.open")
|
||||
@@ -720,6 +733,17 @@ mod tests {
|
||||
.expect("command should exist");
|
||||
assert_eq!(memory_read.permission, GameCreationAppPermission::Auto);
|
||||
|
||||
for command_id in ["conversation.read", "conversation.write"] {
|
||||
let conversation_command = GAME_CREATION_APP_COMMANDS
|
||||
.iter()
|
||||
.find(|command| command.id == command_id)
|
||||
.expect("command should exist");
|
||||
assert_eq!(
|
||||
conversation_command.permission,
|
||||
GameCreationAppPermission::Auto
|
||||
);
|
||||
}
|
||||
|
||||
let canvas_project_open = GAME_CREATION_APP_COMMANDS
|
||||
.iter()
|
||||
.find(|command| command.id == "canvas.project_open")
|
||||
@@ -738,6 +762,15 @@ mod tests {
|
||||
GameCreationAppPermission::Confirm
|
||||
);
|
||||
|
||||
let canvas_asset_generate = GAME_CREATION_APP_COMMANDS
|
||||
.iter()
|
||||
.find(|command| command.id == "canvas.asset_generate")
|
||||
.expect("command should exist");
|
||||
assert_eq!(
|
||||
canvas_asset_generate.permission,
|
||||
GameCreationAppPermission::Confirm
|
||||
);
|
||||
|
||||
let canvas_export_import = GAME_CREATION_APP_COMMANDS
|
||||
.iter()
|
||||
.find(|command| command.id == "canvas.export_import")
|
||||
@@ -770,12 +803,20 @@ mod tests {
|
||||
"repair-loop-carryover",
|
||||
"short-term-memory",
|
||||
"long-term-memory",
|
||||
"conversation-history",
|
||||
"canvas-project-sync",
|
||||
"local-preview",
|
||||
"developer-window",
|
||||
] {
|
||||
assert!(ids.contains(&expected), "missing {expected}");
|
||||
}
|
||||
assert_eq!(
|
||||
GAME_CREATION_AGENT_CAPABILITIES
|
||||
.iter()
|
||||
.find(|capability| capability.id == "conversation-history")
|
||||
.map(|capability| capability.title),
|
||||
Some("对话记录上下文")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user