AI 游戏创作智能体 App v1 最小落地

新增独立 Tauri 壳 apps/ai-game-creator-shell,只保留聊天入口,正式用户窗口不承载游戏预览画面,开发模式通过独立窗口展示任务、文件、记忆、预览和日志

新增游戏创作专业组与种子任务图契约,覆盖策划/美术/程序/数值/音乐/运营 6 组下 15 个组内角色

新增共享契约本地项目 manifest、内置命令权限枚举、run trace schema 和 ready-task 选择器

扩展平台 LLM 支持流式请求,增加 GENARRATIVE_GAME_CREATOR_LLM_STREAM 开关

新增 AI 游戏创作 App 的聊天命令集:/help /capabilities /audit /project /llm-status /status /files /assets /read /tasks /trace /smoke /run /preview /preview-status /preview-open /preview-stop /memory /remember /forget-memory /canvas /sync-canvas-project /import-canvas-asset /import-canvas-export

新增本地项目初始化、权限 gate(pending/confirm/cancel)、文件上传、受限命令白名单 game.static_smoke 和本地 HTTP 预览

新增 Planner / 6 组角色 agent / Generator / Evaluator / ArtifactWriter / Playtest 文件驱动 loop,最多 3 轮返工,结构化 repairRoutes

新增实时 run trace 写入 .agent/run.latest.json 与 .agent/runs/,包含 taskGraph、passPlans 和步级 toolCalls

新增短期/长期记忆 memory/session.md 和 memory/project.md,支持聊天读取/追加/覆盖/删除

新增画板对接:canvas.project_open、canvas.project_sync、canvas.asset_import、canvas.export_import

新增 npm run ai-game-creator-shell:check 开发验收入口,覆盖 typecheck、单元测试、端到端 smoke

新增 check:native-shells 静态守门:release 只登记 main 聊天窗口,CSP 禁止内嵌预览,开发窗口只在 debug 下打开,用户侧预览命令必须调用 open_local_game_preview
This commit is contained in:
AIGameCreator App
2026-06-25 16:13:18 +08:00
parent 2511618add
commit 72a6d635da
37 changed files with 25597 additions and 5 deletions
+4
View File
@@ -31,6 +31,10 @@ temp*build*/
/apps/desktop-shell/src-tauri/target/
/apps/desktop-shell/src-tauri/gen/
/apps/desktop-shell/src-tauri/permissions/autogenerated/
/apps/ai-game-creator-shell/src-tauri/target/
/apps/ai-game-creator-shell/src-tauri/gen/
/apps/ai-game-creator-shell/src-tauri/logs/
/apps/ai-game-creator-shell/logs/
/apps/mobile-shell/.expo/
/apps/mobile-shell/.expo-export-smoke/
/server-rs/.spacetimedb/
+1
View File
@@ -33,6 +33,7 @@
- Issue 使用自托管 Gitea;优先用 Gitea UI/API 或 `tea` CLI,不使用 GitHub `gh` 或 GitLab `glab`,除非仓库已迁移。默认 triage 标签:`needs-triage``needs-info``ready-for-agent``ready-for-human``wontfix`
- 需要仓库级 Hermes skills/plugins 时,再读取 [`.hermes/README.md`](.hermes/README.md)。
- 涉及 AI 游戏创作独立 App、多智能体 Runtime、本地项目产物或本地 HTTP 预览时,先读取 [`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`](docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md)。
- 新增、补齐、迁移或重构玩法入口、玩法类型、创作工作台、生成页、结果页、发布、运行态、作品架、广场或公开 read model 前,必须读取并按 [`genarrative-play-type-integration`](.codex/skills/genarrative-play-type-integration/SKILL.md) 执行。
- 涉及 `npm run dev` / `npm run dev:spacetime` / `npm run dev:api-server` / `npm run dev:web` / `npm run dev:admin-web` 的端口探测、端口漂移、SpacetimeDB publish server、api-server 环境变量、Vite 代理目标或后台 dev 端口时,按 [`.hermes/skills/genarrative-dev-stack-port-routing/SKILL.md`](.hermes/skills/genarrative-dev-stack-port-routing/SKILL.md) 执行。
- 涉及 SpacetimeDB 的设计、实现、脚本、调试、发布、绑定生成、schema、reducer、procedure、view 或 Rust API 时,必须读取并按 [`spacetimedb-cli`](.codex/skills/spacetimedb-cli/SKILL.md)、[`spacetimedb-rust`](.codex/skills/spacetimedb-rust/SKILL.md)、[`spacetimedb-concepts`](.codex/skills/spacetimedb-concepts/SKILL.md) 中相关 skill 执行。
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="data:," />
<title>AI 游戏创作</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+27
View File
@@ -0,0 +1,27 @@
{
"name": "@genarrative/ai-game-creator-shell",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "npm --prefix ../.. exec tauri -- dev",
"dev-server": "node scripts/start-dev-server.mjs",
"build": "npm --prefix ../.. exec tauri -- build",
"llm-status": "node scripts/run-cli-with-env.mjs --llm-status",
"agent-run": "node scripts/run-cli-with-env.mjs --agent-run",
"agent-run:smoke": "node scripts/smoke-agent-run-local-provider.mjs",
"typecheck": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json --noEmit && node scripts/check-config.mjs"
},
"dependencies": {
"@vitejs/plugin-react": "^5.0.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"vite": "^6.2.0"
},
"devDependencies": {
"@tauri-apps/cli": "^2.11.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"typescript": "~5.8.2"
}
}
@@ -0,0 +1,351 @@
import fs from 'node:fs';
const packageConfig = JSON.parse(
fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
);
const tauriConfig = JSON.parse(
fs.readFileSync(
new URL('../src-tauri/tauri.conf.json', import.meta.url),
'utf8',
),
);
const rootPackageConfig = JSON.parse(
fs.readFileSync(new URL('../../../package.json', import.meta.url), 'utf8'),
);
const viteConfigSource = fs.readFileSync(
new URL('../vite.config.ts', import.meta.url),
'utf8',
);
const sourceExtensions = new Set([
'.json',
'.md',
'.mjs',
'.rs',
'.toml',
'.ts',
'.tsx',
]);
function collectFiles(path) {
const stat = fs.statSync(path);
if (stat.isDirectory()) {
return fs.readdirSync(path, { withFileTypes: true }).flatMap((entry) => {
if (entry.name === 'node_modules' || entry.name === 'target') {
return [];
}
return collectFiles(
new URL(`${entry.name}${entry.isDirectory() ? '/' : ''}`, path),
);
});
}
if (sourceExtensions.has(pathnameExtension(path.pathname))) {
return [path];
}
return [];
}
function pathnameExtension(pathname) {
const index = pathname.lastIndexOf('.');
return index === -1 ? '' : pathname.slice(index);
}
function assertNoOpenAiApiKeys(paths) {
const secretPattern = /sk-[A-Za-z0-9_-]{20,}/;
for (const path of paths.flatMap((entry) => collectFiles(entry))) {
const source = fs.readFileSync(path, 'utf8');
if (secretPattern.test(source)) {
throw new Error(
`AI game creator shell source must not contain API keys: ${path.pathname}`,
);
}
}
}
assertNoOpenAiApiKeys([
new URL('../src/', import.meta.url),
new URL('../scripts/', 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),
new URL(
'../../../packages/shared/src/contracts/gameCreationApp.ts',
import.meta.url,
),
new URL(
'../../../packages/shared/src/contracts/gameCreationApp.test.ts',
import.meta.url,
),
new URL(
'../../../server-rs/crates/platform-agent/src/game_creation.rs',
import.meta.url,
),
new URL(
'../../../server-rs/crates/shared-contracts/src/game_creation_app.rs',
import.meta.url,
),
new URL(
'../../../docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md',
import.meta.url,
),
]);
if (packageConfig.name !== '@genarrative/ai-game-creator-shell') {
throw new Error('AI game creator shell package name drifted');
}
if (
packageConfig.scripts?.['llm-status'] !==
'node scripts/run-cli-with-env.mjs --llm-status'
) {
throw new Error(
'AI game creator shell llm-status must load gitignored local env before checking LLM config',
);
}
if (
packageConfig.scripts?.['agent-run'] !==
'node scripts/run-cli-with-env.mjs --agent-run'
) {
throw new Error(
'AI game creator shell agent-run must load gitignored local env before running the provider path',
);
}
if (tauriConfig.productName !== 'Genarrative AI Game Creator') {
throw new Error('AI game creator shell productName drifted');
}
if (tauriConfig.identifier !== 'world.genarrative.ai-game-creator') {
throw new Error('AI game creator shell identifier drifted');
}
if (tauriConfig.app?.withGlobalTauri !== true) {
throw new Error(
'AI game creator shell must expose window.__TAURI__ for local commands',
);
}
const windows = tauriConfig.app?.windows ?? [];
if (windows.length !== 1 || windows[0]?.label !== 'main') {
throw new Error(
'AI game creator shell release config must expose only the chat main window',
);
}
const mainWindow = windows[0];
if (
mainWindow.width !== 760 ||
mainWindow.height !== 820 ||
mainWindow.minWidth !== 420 ||
mainWindow.minHeight !== 560
) {
throw new Error('AI game creator shell main window must stay chat-sized');
}
if (tauriConfig.build?.devUrl !== 'http://127.0.0.1:3080/') {
throw new Error(
'AI game creator shell Tauri devUrl must stay on the fixed Vite dev port',
);
}
if (!viteConfigSource.includes("host: '127.0.0.1'")) {
throw new Error(
'AI game creator shell Vite dev server must bind to localhost',
);
}
if (!viteConfigSource.includes('port: 3080')) {
throw new Error(
'AI game creator shell Vite dev port must match Tauri devUrl',
);
}
if (!viteConfigSource.includes('strictPort: true')) {
throw new Error(
'AI game creator shell Vite dev server must not drift away from Tauri devUrl',
);
}
if (
!tauriConfig.build?.beforeDevCommand?.includes(
'run ai-game-creator-shell:dev-server',
)
) {
throw new Error(
'AI game creator shell beforeDevCommand must reuse or start the fixed Vite dev server',
);
}
if (
!tauriConfig.build?.beforeBuildCommand?.includes('--config vite.config.ts')
) {
throw new Error(
'AI game creator shell beforeBuildCommand must resolve vite config from app root',
);
}
const devServerSource = fs.readFileSync(
new URL('../scripts/start-dev-server.mjs', import.meta.url),
'utf8',
);
const runCliWithEnvSource = fs.readFileSync(
new URL('../scripts/run-cli-with-env.mjs', import.meta.url),
'utf8',
);
for (const snippet of [
'const port = 3080',
"response.body.includes('<title>AI 游戏创作</title>')",
'function isPortListening()',
'reuse existing Vite dev server',
'non-HTTP or unrecognized server',
"'--config', 'vite.config.ts'",
]) {
if (!devServerSource.includes(snippet)) {
throw new Error(
`AI game creator shell dev server wrapper drifted: ${snippet}`,
);
}
}
for (const snippet of [
"path.join(repoRoot, '.env.secrets.local')",
"path.join(appRoot, '.env.secrets.local')",
'dotenv.config({ path: envPath, override: false })',
"'--manifest-path'",
"'src-tauri/Cargo.toml'",
]) {
if (!runCliWithEnvSource.includes(snippet)) {
throw new Error(
`AI game creator shell local env CLI wrapper drifted: ${snippet}`,
);
}
}
const tauriMainSource = fs.readFileSync(
new URL('../src-tauri/src/main.rs', import.meta.url),
'utf8',
);
for (const snippet of [
'fn load_game_creator_local_env()',
'fn load_game_creator_env_file(path: &Path)',
'directory.join(".env.secrets.local")',
'.join("apps")',
'.join("ai-game-creator-shell")',
'load_game_creator_local_env()?;',
'let local_env_error = load_game_creator_local_env().err();',
'local.env.load.failed',
'#[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)?;',
]) {
if (!tauriMainSource.includes(snippet)) {
throw new Error(
`AI game creator shell developer window guardrail drifted: ${snippet}`,
);
}
}
const appSource = fs.readFileSync(
new URL('../src/App.tsx', import.meta.url),
'utf8',
);
for (const snippet of [
"'game.run_local'",
"'/run:运行自检,启动本地 HTTP 预览并交给外部浏览器'",
'async function openPreviewInExternalBrowser',
"'open_local_game_preview'",
'已交给外部浏览器打开。',
'async function executeRunLocal',
'function needsInitializedChatProject',
'function resolvePendingCommandProjectPath',
'resolveChatProjectPath(localProject) ?? draftProjectPath',
'`permission.cancel ${command.id} missing-project`',
"'/remember [short|long] 内容:追加短期或长期记忆'",
"'/memory-set [short|long] 内容:覆盖保存短期或长期记忆'",
'function parseRememberInput',
"'/trace 或 /loop:查看最近一次 Agent loop trace'",
'async function executeAgentTraceChat',
"relativePath: '.agent/logs/command.log'",
"'permission.pending'",
"'permission.confirm'",
"'permission.cancel'",
'function summarizeAgentRunTrace',
'工具调用:${agentRunTrace.toolCallCount}/${agentRunTrace.maxToolCalls}',
'agentRunTrace.error ?',
'className="trace-error"',
'agentRunTrace.taskGraph.repairRoutes.map',
"in: ${step.inputPaths.join(', ') || 'none'}",
"out: ${step.outputPaths.join(', ') || 'none'}",
]) {
if (!appSource.includes(snippet)) {
throw new Error(
`AI game creator shell trace panel guardrail drifted: ${snippet}`,
);
}
}
for (const script of [
'ai-game-creator-shell:dev',
'ai-game-creator-shell:dev-server',
'ai-game-creator-shell:build',
'ai-game-creator-shell:typecheck',
'ai-game-creator-shell:agent-run:smoke',
'ai-game-creator-shell:check',
]) {
if (!rootPackageConfig.scripts?.[script]) {
throw new Error(`root package missing ${script}`);
}
}
if (
rootPackageConfig.scripts?.['ai-game-creator-shell:build'] !==
'npm --prefix apps/ai-game-creator-shell run build --'
) {
throw new Error(
'root ai-game-creator-shell:build script must forward build args',
);
}
const agentRunSmokeSource = fs.readFileSync(
new URL('../scripts/smoke-agent-run-local-provider.mjs', import.meta.url),
'utf8',
);
for (const snippet of [
"const smokeAssetMarker = 'SMOKE_LOCAL_ASSET:chef'",
"const smokeAudioAssetPath = 'assets/uploads/smoke-bounce.mp3'",
"mediaType: 'audio/mpeg'",
'document.body.dataset.smokeFrame = String(frameCount)',
"ctx.fillStyle = '#ff00ff'",
'ctx.fillRect(4, 4, 8, 24)',
'const sample = ctx.getImageData(4, 4, 24, 24).data',
'chef.complete && chef.naturalWidth > 0',
'document.body.dataset.smokeCanvasPixels = String(litPixels)',
'document.body.dataset.smokeCanvasColors = String(colors.size)',
'readBrowserDom(previewUrl)',
"extractDomNumber(previewDom, 'smoke-canvas-pixels')",
'--dump-dom',
'readHttpHead(new URL(smokeAssetPath, previewUrl).toString())',
'readHttpHead(new URL(smokeAudioAssetPath, previewUrl).toString())',
'function writeStreamingChatCompletion',
'requestJson?.stream === true',
`requestBodies.every((body) => body.includes('"stream":true'))`,
"GENARRATIVE_GAME_CREATOR_LLM_STREAM: 'true'",
"method: 'HEAD'",
'previewAssetHead.contentLength === String(smokeAssetBytes.length)',
"previewAudioHead.contentType === 'audio/mpeg'",
"previewAssetHead.body === ''",
'trace missing professional group ${group}',
]) {
if (!agentRunSmokeSource.includes(snippet)) {
throw new Error(
`AI game creator shell agent-run smoke drifted: ${snippet}`,
);
}
}
@@ -0,0 +1,48 @@
import { spawn } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import dotenv from 'dotenv';
const appRoot = path.resolve(fileURLToPath(new URL('..', import.meta.url)));
const repoRoot = path.resolve(appRoot, '../..');
for (const envPath of [
path.join(repoRoot, '.env.secrets.local'),
path.join(appRoot, '.env.secrets.local'),
]) {
if (fs.existsSync(envPath)) {
dotenv.config({ path: envPath, override: false });
}
}
const cargo = process.platform === 'win32' ? 'cargo.exe' : 'cargo';
const child = spawn(
cargo,
[
'run',
'--manifest-path',
'src-tauri/Cargo.toml',
'--',
...process.argv.slice(2),
],
{
cwd: appRoot,
stdio: 'inherit',
env: process.env,
},
);
for (const signal of ['SIGINT', 'SIGTERM']) {
process.on(signal, () => {
child.kill(signal);
});
}
child.on('exit', (code, signal) => {
if (signal) {
process.exit(1);
}
process.exit(code ?? 0);
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,109 @@
import { spawn } from 'node:child_process';
import http from 'node:http';
import net from 'node:net';
import { fileURLToPath } from 'node:url';
const appRoot = fileURLToPath(new URL('..', import.meta.url));
const host = '127.0.0.1';
const port = 3080;
const devUrl = `http://${host}:${port}/`;
function readExistingServer() {
return new Promise((resolve) => {
const request = http.get(
{
host,
port,
path: '/',
timeout: 1000,
},
(response) => {
let body = '';
response.setEncoding('utf8');
response.on('data', (chunk) => {
if (body.length < 4096) {
body += chunk;
}
});
response.on('end', () => {
resolve({
statusCode: response.statusCode ?? 0,
body,
});
});
},
);
request.on('timeout', () => {
request.destroy();
resolve(null);
});
request.on('error', () => resolve(null));
});
}
function isAiGameCreatorServer(response) {
return (
response &&
response.statusCode >= 200 &&
response.statusCode < 500 &&
response.body.includes('<title>AI 游戏创作</title>') &&
response.body.includes('/src/main.tsx')
);
}
function isPortListening() {
return new Promise((resolve) => {
const socket = net.connect({ host, port });
socket.once('connect', () => {
socket.destroy();
resolve(true);
});
socket.once('error', () => resolve(false));
socket.setTimeout(1000, () => {
socket.destroy();
resolve(true);
});
});
}
const existing = await readExistingServer();
if (existing) {
if (isAiGameCreatorServer(existing)) {
console.log(`[ai-game-creator-shell] reuse existing Vite dev server ${devUrl}`);
process.exit(0);
}
console.error(
`[ai-game-creator-shell] ${devUrl} is already in use by another server. Stop that process before starting Tauri dev.`,
);
process.exit(1);
}
if (await isPortListening()) {
console.error(
`[ai-game-creator-shell] ${devUrl} is already in use by a non-HTTP or unrecognized server. Stop that process before starting Tauri dev.`,
);
process.exit(1);
}
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
const child = spawn(
npm,
['--prefix', '../..', 'exec', 'vite', '--', '--config', 'vite.config.ts'],
{
cwd: appRoot,
stdio: 'inherit',
},
);
for (const signal of ['SIGINT', 'SIGTERM']) {
process.on(signal, () => {
child.kill(signal);
});
}
child.on('exit', (code, signal) => {
if (signal) {
process.exit(1);
}
process.exit(code ?? 0);
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,20 @@
[package]
name = "genarrative-ai-game-creator-shell"
version = "0.1.0"
edition = "2021"
publish = false
[build-dependencies]
tauri-build = { version = "2.6.2", features = [] }
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
platform-llm = { path = "../../../server-rs/crates/platform-llm" }
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-opener = "2.5.4"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
zip = { version = "2", default-features = false, features = ["deflate"] }
@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,41 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Genarrative AI Game Creator",
"version": "0.1.0",
"identifier": "world.genarrative.ai-game-creator",
"build": {
"beforeDevCommand": "npm --prefix ../.. run ai-game-creator-shell:typecheck && npm --prefix ../.. run ai-game-creator-shell:dev-server",
"beforeBuildCommand": "npm --prefix ../.. run ai-game-creator-shell:typecheck && npm --prefix ../.. exec vite -- build --config vite.config.ts",
"devUrl": "http://127.0.0.1:3080/",
"frontendDist": "../dist"
},
"app": {
"withGlobalTauri": true,
"windows": [
{
"label": "main",
"title": "AI 游戏创作",
"width": 760,
"height": 820,
"minWidth": 420,
"minHeight": 560
}
],
"security": {
"csp": "default-src 'self' customprotocol: asset:; img-src 'self' asset: data: blob:; media-src 'self' asset: data: blob:; connect-src 'self' http://127.0.0.1:* ws://127.0.0.1:*; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-src 'self'",
"devCsp": "default-src 'self' customprotocol: asset: http://127.0.0.1:*; img-src 'self' asset: http://127.0.0.1:* data: blob:; media-src 'self' asset: http://127.0.0.1:* data: blob:; connect-src 'self' http://127.0.0.1:* ws://127.0.0.1:*; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-src 'self' http://127.0.0.1:*"
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"../../desktop-shell/src-tauri/icons/32x32.png",
"../../desktop-shell/src-tauri/icons/128x128.png",
"../../desktop-shell/src-tauri/icons/128x128@2x.png",
"../../desktop-shell/src-tauri/icons/icon.icns",
"../../desktop-shell/src-tauri/icons/icon.ico",
"../../desktop-shell/src-tauri/icons/icon.png"
]
}
}
File diff suppressed because it is too large Load Diff
+11
View File
@@ -0,0 +1,11 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './App';
import './styles.css';
createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
+463
View File
@@ -0,0 +1,463 @@
:root {
color: #18202f;
background: #f5f7fb;
font-family: Inter, 'PingFang SC', 'Microsoft YaHei', system-ui, sans-serif;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
}
button,
input,
select,
textarea {
font: inherit;
}
.app-shell {
min-height: 100vh;
background: #f5f7fb;
}
.app-shell--user {
display: flex;
justify-content: center;
padding: 24px;
}
.app-shell--dev {
display: grid;
grid-template-columns: minmax(360px, 0.85fr) minmax(520px, 1.15fr);
gap: 1px;
background: #d9dee8;
}
.chat-pane {
display: flex;
flex-direction: column;
gap: 16px;
min-width: 0;
padding: 18px;
background: #fff;
}
.app-shell--user .chat-pane {
width: min(900px, 100%);
min-height: calc(100vh - 48px);
border: 1px solid #d9dee8;
}
.app-shell--dev .chat-pane {
min-height: 100vh;
}
.chat-header,
.panel-header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
}
.panel-header {
margin-bottom: 10px;
}
.panel-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.panel-header button,
.local-project-form button,
.panel-actions select {
height: 32px;
padding: 0 12px;
border: 1px solid #cfd7e6;
border-radius: 6px;
}
.panel-header button,
.local-project-form button {
color: #fff;
background: #1f6feb;
}
.panel-actions select {
background: #fff;
}
h1,
h2,
p {
margin: 0;
}
h1 {
font-size: 24px;
}
h2 {
margin-bottom: 14px;
font-size: 16px;
}
.chat-header span,
.task-pane span,
.log-pane p {
color: #647084;
}
.message-list {
flex: 1;
border: 1px solid #dde3ee;
padding: 14px;
overflow: auto;
}
.message + .message {
margin-top: 10px;
}
.message {
overflow-wrap: anywhere;
white-space: pre-wrap;
}
.message--user {
color: #1f6feb;
}
.pending-command {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
padding: 10px;
border: 1px solid #dde3ee;
background: #f8fafd;
}
.pending-command span {
display: grid;
gap: 3px;
min-width: 0;
margin-right: auto;
color: #647084;
}
.pending-command small {
overflow-wrap: anywhere;
font-size: 12px;
}
.pending-command button {
height: 32px;
padding: 0 12px;
border: 1px solid #cfd7e6;
border-radius: 6px;
color: #fff;
background: #1f6feb;
}
.composer {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
gap: 10px;
}
.composer input,
.composer button,
.composer-upload-button {
height: 40px;
border: 1px solid #cfd7e6;
border-radius: 6px;
}
.composer input {
min-width: 0;
padding: 0 12px;
}
.composer button,
.composer-upload-button {
display: inline-grid;
padding: 0 18px;
place-items: center;
color: #fff;
background: #1f6feb;
cursor: pointer;
}
.composer-upload-button input {
display: none;
}
.developer-pane {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
align-content: start;
gap: 1px;
min-width: 0;
background: #d9dee8;
}
.developer-panel {
min-width: 0;
padding: 18px;
background: #fff;
}
.developer-panel--wide {
grid-column: 1 / -1;
}
.task-pane article {
display: grid;
grid-template-columns: 84px 1fr;
gap: 12px;
padding: 10px 0;
border-bottom: 1px solid #edf1f7;
}
.capability-pane article {
display: grid;
grid-template-columns: 1fr auto;
gap: 12px;
padding: 8px 0;
border-bottom: 1px solid #edf1f7;
}
.capability-pane span {
color: #647084;
font-size: 12px;
}
.trace-pane article {
display: grid;
gap: 4px;
padding: 8px 0;
border-bottom: 1px solid #edf1f7;
}
.trace-pane span,
.trace-pane small {
color: #647084;
}
.trace-pane small {
overflow-wrap: anywhere;
font-size: 12px;
}
.trace-error {
margin-bottom: 10px;
padding: 8px;
border: 1px solid #efb3b3;
border-radius: 6px;
color: #8a1f1f;
background: #fff5f5;
overflow-wrap: anywhere;
}
.trace-artifacts {
display: grid;
gap: 4px;
margin-bottom: 10px;
}
.trace-task-graph {
display: grid;
gap: 4px;
margin-bottom: 10px;
padding: 8px;
border: 1px solid #dce3ee;
border-radius: 6px;
background: #f8fafc;
}
.trace-history {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 10px;
}
.trace-history button {
min-width: 0;
max-width: 100%;
padding: 5px 8px;
overflow-wrap: anywhere;
border: 1px solid #cfd7e6;
border-radius: 6px;
background: #fff;
color: #18202f;
font-size: 12px;
text-align: left;
}
.asset-pane code {
display: block;
margin-bottom: 10px;
padding: 9px 10px;
border: 1px solid #e1e7f0;
background: #f8fafd;
}
.local-project-form {
display: grid;
grid-template-columns: 1fr auto;
gap: 8px;
margin-bottom: 10px;
}
.local-project-form input,
.local-project-form button {
min-width: 0;
height: 36px;
border: 1px solid #cfd7e6;
border-radius: 6px;
}
.local-project-form input {
padding: 0 10px;
}
.asset-register-form {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
margin-bottom: 10px;
}
.asset-register-form input,
.asset-register-form select,
.asset-register-form button {
min-width: 0;
height: 34px;
border: 1px solid #cfd7e6;
border-radius: 6px;
}
.asset-register-form input,
.asset-register-form select {
padding: 0 10px;
}
.asset-register-form button {
color: #fff;
background: #1f6feb;
}
.status-line,
.manifest-path {
margin-bottom: 10px;
color: #647084;
font-size: 13px;
}
.manifest-path {
overflow-wrap: anywhere;
}
.file-pane input,
.file-pane textarea,
.memory-pane textarea {
display: block;
width: 100%;
margin-bottom: 10px;
padding: 10px;
border: 1px solid #cfd7e6;
border-radius: 6px;
}
.file-pane input {
height: 36px;
}
.file-pane textarea,
.memory-pane textarea {
min-height: 150px;
resize: vertical;
}
.file-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.file-list button {
padding: 6px 10px;
border: 1px solid #cfd7e6;
border-radius: 6px;
background: #fff;
}
.limited-command-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 10px;
}
.limited-command-list button {
height: 32px;
padding: 0 12px;
border: 1px solid #cfd7e6;
border-radius: 6px;
color: #fff;
background: #1f6feb;
}
.preview-frame {
display: grid;
width: 100%;
min-height: 180px;
place-items: center;
border: 1px solid #cfd7e6;
background: #101827;
color: #d9e7ff;
}
iframe.preview-frame {
display: block;
height: 180px;
}
@media (max-width: 1100px) {
.app-shell--user {
padding: 0;
}
.app-shell--user .chat-pane {
min-height: 100vh;
border: 0;
}
.app-shell--dev {
grid-template-columns: 1fr;
}
.developer-pane {
grid-template-columns: 1fr;
}
.developer-panel--wide {
grid-column: auto;
}
.composer {
grid-template-columns: 1fr auto;
}
.composer-upload-button {
grid-column: 1 / -1;
}
}
+12
View File
@@ -0,0 +1,12 @@
/// <reference types="vite/client" />
interface Window {
__TAURI__?: {
core?: {
invoke?: <Result = unknown>(
command: string,
args?: Record<string, unknown>,
) => Promise<Result>;
};
};
}
@@ -0,0 +1,152 @@
import { describe, expect, it } from 'vitest';
import {
createGameCreationAppSeedTasks,
GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
type GameCreationAgentRunTrace,
} from '../../../packages/shared/src/contracts/gameCreationApp';
import { summarizeAgentRunTrace } from '../src/App';
describe('AI 游戏创作 Agent loop 摘要', () => {
it('shows repair loop state in the chat trace summary', () => {
const tasks = createGameCreationAppSeedTasks();
tasks[0]!.status = 'completed';
tasks[1]!.status = 'completed';
const trace: GameCreationAgentRunTrace = {
schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
runId: 'run-test',
commandId: 'game.generate_draft',
status: 'needs-revision',
passes: 2,
maxPasses: 3,
toolCallCount: 12,
maxToolCalls: 128,
stopReason: 'max-passes-exhausted',
goal: '做一个弹幕厨房游戏',
coordination: 'Planner -> Orchestrator -> Generator -> Evaluator',
steps: [
{
pass: 2,
agent: 'Orchestrator',
phase: 'plan',
taskId: 'code-director',
group: 'code',
role: 'Code',
status: 'completed',
inputPaths: ['.agent/findings.md'],
outputPaths: ['.agent/passes/pass-2/task-graph.json'],
summary: '重跑程序链路及下游发布包装',
toolCalls: [],
},
{
pass: 2,
agent: '美术组 / Asset',
phase: 'role-brief',
taskId: 'art-asset-plan',
group: 'art',
role: 'Asset',
status: 'completed',
inputPaths: ['.agent/manifest.json'],
outputPaths: ['.agent/passes/pass-2/groups/art/asset.md'],
summary: '需要回流画板角色素材',
toolCalls: [
{
toolId: 'llm.chat.group.art.asset',
status: 'completed',
inputPaths: ['.agent/manifest.json'],
outputPaths: ['.agent/passes/pass-2/groups/art/asset.md'],
summary: '规划角色资产',
},
{
toolId: 'agent.tool.suggest.canvas.project_sync',
status: 'suggested',
inputPaths: ['.agent/manifest.json'],
outputPaths: [],
summary:
'项目还没有画板回流资产;建议用户确认 /sync-canvas-project <画板项目ID>。',
},
],
},
],
artifacts: [
{
path: '.agent/passes/pass-2/task-graph.json',
sizeBytes: 128,
checksum: 'fnv1a64:test',
},
],
taskGraph: {
goal: '做一个弹幕厨房游戏',
readyTaskIds: ['code-director'],
activeTaskIds: ['code-director', 'publish-package'],
carriedTaskIds: ['design-director'],
repairFocus: ['gameHtml 缺少 canvas'],
repairRoutes: [
{
issue: 'gameHtml 缺少 canvas',
taskIds: ['code-director', 'preview-readiness', 'publish-package'],
reason: 'code-repair+dependency-impact',
},
],
tasks,
},
passPlans: [
{
pass: 1,
mode: 'initial',
summary: '第 1 轮全量调度',
activeTaskIds: ['design-director', 'code-director'],
carriedTaskIds: [],
dependencyWaves: [['design-director'], ['code-director']],
repairFocus: [],
repairRoutes: [],
},
{
pass: 2,
mode: 'repair',
summary: '第 2 轮重跑程序链路及下游发布包装',
activeTaskIds: ['code-director', 'publish-package'],
carriedTaskIds: ['design-director'],
dependencyWaves: [['code-director'], ['publish-package']],
repairFocus: ['gameHtml 缺少 canvas'],
repairRoutes: [
{
issue: 'gameHtml 缺少 canvas',
taskIds: ['code-director', 'preview-readiness', 'publish-package'],
reason: 'code-repair+dependency-impact',
},
],
},
],
nextStep: 'repair-next-pass',
error: null,
updatedAt: 1,
};
const summary = summarizeAgentRunTrace(trace);
expect(summary).toContain('needs-revision · 2/3 轮 · max-passes-exhausted');
expect(summary).toContain('工具调用:12/128');
expect(summary).toContain('任务:已完成 2');
expect(summary).toContain(
'active 任务:程序组 / Director 拆解程序实现(code-director), 运营组 / Publish 整理发布包装(publish-package)',
);
expect(summary).toContain(
'carry-over 任务:策划组 / Director 拆解创作方向(design-director)',
);
expect(summary).toContain('返工焦点:gameHtml 缺少 canvas');
expect(summary).toContain(
'返工路线:code-repair+dependency-impact: 程序组 / Director 拆解程序实现(code-director), 程序组 / Preview 执行静态自检(preview-readiness), 运营组 / Publish 整理发布包装(publish-package)',
);
expect(summary).toContain('建议命令:');
expect(summary).toContain('agent.tool.suggest.canvas.project_sync');
expect(summary).toContain('/sync-canvas-project <画板项目ID>');
expect(summary).toContain('编排轮次:');
expect(summary).toContain(
'pass 2 · repair · active 2 · carry 1 · waves 程序组 / Director 拆解程序实现(code-director) / 运营组 / Publish 整理发布包装(publish-package)',
);
expect(summary).toContain('.agent/passes/pass-2/task-graph.json');
expect(summary).toContain('Orchestrator #2 · completed · plan');
});
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,132 @@
import { describe, expect, it } from 'vitest';
import {
isAbsoluteProjectPath,
needsInitializedChatProject,
parseRememberInput,
pendingCommandDetail,
resolveChatProjectPath,
resolvePendingCommandProjectPath,
} from '../src/App';
describe('AI 游戏创作聊天记忆命令', () => {
it('recognizes local project absolute paths across desktop platforms', () => {
expect(isAbsoluteProjectPath('/tmp/game')).toBe(true);
expect(isAbsoluteProjectPath('C:\\Games\\demo')).toBe(true);
expect(isAbsoluteProjectPath('D:/Games/demo')).toBe(true);
expect(isAbsoluteProjectPath('relative-game')).toBe(false);
});
it('defaults /remember to long memory and supports short memory scope', () => {
expect(parseRememberInput('主角喜欢反弹弹幕')).toEqual({
scope: 'long',
content: '主角喜欢反弹弹幕',
});
expect(parseRememberInput('short 本轮先修输入手感')).toEqual({
scope: 'short',
content: '本轮先修输入手感',
});
expect(parseRememberInput('长期 保留厨房主题')).toEqual({
scope: 'long',
content: '保留厨房主题',
});
expect(parseRememberInput('project 覆盖后的长期设定')).toEqual({
scope: 'long',
content: '覆盖后的长期设定',
});
});
it('describes append and replace memory writes before confirmation', () => {
expect(
pendingCommandDetail(
{
id: 'memory.write',
scope: 'long',
content: '保留厨房主题',
mode: 'append',
},
'/tmp/game',
),
).toBe('追加到 /tmp/game/memory/project.md');
expect(
pendingCommandDetail(
{
id: 'memory.write',
scope: 'short',
content: '覆盖本轮上下文',
mode: 'replace',
},
'/tmp/game',
),
).toBe('覆盖保存到 /tmp/game/memory/session.md');
});
it('requires an initialized local project before chat memory writes', () => {
expect(resolveChatProjectPath(null)).toBeNull();
expect(resolveChatProjectPath({ projectPath: '/tmp/game' })).toBe(
'/tmp/game',
);
expect(parseRememberInput('long')).toEqual({
scope: 'long',
content: '',
});
expect(parseRememberInput('short 覆盖本轮上下文')).toEqual({
scope: 'short',
content: '覆盖本轮上下文',
});
expect(
pendingCommandDetail(
{
id: 'memory.delete',
scope: 'short',
},
'/tmp/game',
),
).toBe('删除 /tmp/game/memory/session.md');
});
it('describes local generation side effects before confirmation', () => {
expect(
pendingCommandDetail(
{
id: 'game.generate_draft',
prompt: '做一个厨房弹幕游戏',
},
'/tmp/game',
),
).toBe(
'调用 LLM,写入 /tmp/game/game、assets、memory、exports,通过自检后启动本地 HTTP 预览并交给外部浏览器',
);
});
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);
expect(needsInitializedChatProject('command.run_limited')).toBe(true);
expect(needsInitializedChatProject('preview.open')).toBe(true);
expect(needsInitializedChatProject('preview.start')).toBe(true);
expect(needsInitializedChatProject('canvas.project_sync')).toBe(true);
expect(needsInitializedChatProject('canvas.export_import')).toBe(true);
expect(needsInitializedChatProject('memory.write')).toBe(true);
expect(needsInitializedChatProject('memory.delete')).toBe(true);
expect(needsInitializedChatProject('project.create')).toBe(false);
expect(needsInitializedChatProject('canvas.project_open')).toBe(false);
});
it('shows the authorized project path in pending write command details', () => {
expect(
resolvePendingCommandProjectPath(
{ id: 'game.generate_draft', prompt: '做一个厨房弹幕游戏' },
{ projectPath: '/authorized/game' },
'/draft/dev-input',
),
).toBe('/authorized/game');
expect(
resolvePendingCommandProjectPath(
{ id: 'project.create', projectPath: '/new/game' },
{ projectPath: '/authorized/game' },
'/draft/dev-input',
),
).toBe('/new/game');
});
});
+19
View File
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": false,
"module": "ESNext",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"isolatedModules": true,
"moduleDetection": "force",
"allowJs": false,
"strict": true,
"noUncheckedIndexedAccess": true,
"jsx": "react-jsx",
"noEmit": true,
"types": ["vite/client"]
},
"include": ["src", "vite.config.ts"]
}
+21
View File
@@ -0,0 +1,21 @@
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
const appRoot = dirname(fileURLToPath(import.meta.url));
export default defineConfig({
root: appRoot,
plugins: [react()],
server: {
host: '127.0.0.1',
port: 3080,
strictPort: true,
},
build: {
outDir: resolve(appRoot, 'dist'),
emptyOutDir: true,
},
});
@@ -16,6 +16,31 @@
---
## 2026-06-25 AI 游戏创作 App 真实 LLM 联调用流式请求
- 背景:AI 游戏创作 App 的真实 OpenAI-compatible provider 验收中,小请求可返回,但 Planner 等稍长非流式请求会在上游响应前被网关空闲连接切断,表现为 TLS record 解密失败;本地无密钥 provider smoke 不能覆盖该真实网关行为。
- 决策:`platform-llm` 文本 client 使用系统 TLS backend,并保留底层错误链用于排障;AI 游戏创作 App 增加 `GENARRATIVE_GAME_CREATOR_LLM_STREAM=true` 开关,打开后 Planner、组内角色和 Generator 走流式请求。默认本地 smoke 继续使用非流式 OpenAI-compatible 测试 provider,避免把测试桩改重。
- 影响范围:`server-rs/crates/platform-llm``apps/ai-game-creator-shell/src-tauri/src/main.rs` 和 AI 游戏创作智能体 App 实施计划。
- 验证方式:运行 `cargo test -p platform-llm --manifest-path server-rs/Cargo.toml request_text_parses_non_stream_response`,并用真实 OpenAI-compatible 环境变量执行 `npm run ai-game-creator-shell:agent-run -- --no-wait /tmp/genarrative-ai-game-real-loop-test-6 "做一个像素风反弹弹幕厨房小游戏..."`,确认 36 个 trace step、36 次 tool call、`game.static_smoke``preview.start``preview.stop` 完成。
- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`
## 2026-06-24 AI 游戏创作 App 生成编排使用文件驱动 loop
- 背景:AI 游戏创作 App 的 `game.generate_draft` 已接入 LLM,但单次请求仍不能体现 Planner / Generator / Evaluator 的协作闭环,也无法把评估反馈作为下一轮生成输入。
- 决策:v1 使用最小文件驱动 loop,不引入 LangChain、AutoGen、Microsoft Agent Framework 或 OpenAI Agents SDK sidecar。Planner 写 `.agent/spec.md`;每轮先调用策划、数值、美术、音乐、程序、运营 6 组下的 15 个角色 agent,角色 brief 写到 `.agent/passes/pass-N/groups/<group>/*.md`,再由 `GroupCoordinator` 汇总到 `.agent/passes/pass-N/groups/*.md`Generator 读取 spec、`.agent/findings.md` 和 6 组汇总 brief 生成结构化游戏草案。LLM JSON 必须带 `handoffs` 数组并覆盖 `design``balance``art``audio``code``publishing` 6 个专业组;每轮再把这些结构化交接快照写到 `.agent/passes/pass-N/`。Evaluator 做本地静态验收并写 `.agent/findings.md`,最多 3 轮;返工轮必须把 findings 转成结构化 `repairRoutes`,记录每条问题命中的 taskIds 和 reason,再据此选择 activeTaskIds。每次运行另写 `.agent/run.latest.json``.agent/runs/<runId>.json`,记录 step、角色级 `toolCalls`、组汇总、专业组交接、输入输出路径、artifact 字节数与 `fnv1a64:` checksum;每个 step 带 phase、taskId、group 和 roletrace 顶层 `taskGraph` 记录 goal、readyTaskIds、activeTaskIds、carriedTaskIds、repairFocus、repairRoutes 和当前任务状态,`passPlans` 逐轮记录 mode、summary、activeTaskIds、carriedTaskIds、dependencyWaves、repairFocus 和 repairRouteslatest 是当前指针,runs 目录保留历史 trace,作为开发窗口和后续工具调用 trace 的事实源,schema 由共享 TS/Rust 契约 `game-creator-agent-run.v1` 固定。最终产物写盘时追加 `ArtifactWriter / file.write.local_artifacts` step,随后自动跑白名单 `game.static_smoke`,检查 `game/index.html` 具备 canvas、canvas 渲染上下文、绘制调用、主循环、输入监听、明确目标、失败或胜利状态和重开路径,且不使用远程资源、`eval``new Function``localStorage``fetch``WebSocket``ServiceWorker`,再把 Playtest 工具调用写回 trace;通过后把 runId、状态、轮次、下一步、active / carry-over 任务和最终本地产物摘要追加到 `memory/session.md``memory/project.md`,让下一次 Planner / 角色 agent / Generator 从记忆输入直接看到上一轮稳定原型;后续 `preview.start` 会在已有 trace 上追加 Preview 工具调用和本地预览 URL。
- 决策补充:普通用户聊天 `/trace` 读取同一份 `.agent/run.latest.json`,但摘要必须把 activeTaskIds、carriedTaskIds、repairRoutes 和 dependencyWaves 从内部 taskId 映射成专业组 / 角色 / 任务名,确保不打开开发窗口也能看出 6 组 agent、组内角色、返工路线和 carry-over 真实发生。
- 影响范围:`apps/ai-game-creator-shell/src-tauri/src/main.rs``packages/shared/src/contracts/gameCreationApp.ts``server-rs/crates/shared-contracts/src/game_creation_app.rs` 和 AI 游戏创作智能体 App 实施计划。
- 验证方式:运行 AI 游戏创作壳 Rust 测试、共享契约 TS/Rust 测试、壳 typecheck、编码检查和 `git diff --check`
- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`
## 2026-06-24 AI 游戏创作 App 编排 v1 使用 ready-task 选择器
- 背景:AI 游戏创作 App 已有专业组任务拆分和依赖字段,但如果没有当前可执行任务选择器,“任务编排”只停留在静态清单,普通用户在聊天里也看不到下一步由哪组 agent 接手。
- 决策:v1 编排先使用最小 ready-task 规则:只选择 `pending` 且所有依赖任务均为 `completed` 的任务;共享 TS/Rust 契约和 `platform-agent` 都提供同一语义的选择器,聊天 `/tasks` 只展示下一步可执行专业组,不新增独立编排面板或外部 agent 框架。
- 影响范围:`packages/shared/src/contracts/gameCreationApp.ts``server-rs/crates/shared-contracts/src/game_creation_app.rs``server-rs/crates/platform-agent/src/game_creation.rs``apps/ai-game-creator-shell/src/App.tsx` 和 AI 游戏创作智能体 App 实施计划。
- 验证方式:运行共享契约测试、`platform-agent``shared-contracts` 的 Rust 测试、AI 游戏创作壳 typecheck、`npm run check:encoding``git diff --check`
- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`
## 2026-06-24 外部生成队列升级为正式生成任务列表
- 背景:外部生成队列已经承载画板和玩法的付费生成,但前端只展示排队概览,缺少可追溯任务列表、后端确认状态、完成提示补弹和退款记录到任务的追踪关系。
@@ -569,6 +594,7 @@
- 影响范围:`src/components/common/PublishShareModal.tsx``src/components/common/publishShareModalModel.ts``src/components/common/publishShareCardImage.ts``src/services/wechatMiniProgramShareGrid.ts``src/services/wechatMiniProgramShareTarget.ts``miniprogram/pages/web-view/``miniprogram/pages/share-grid/`、推荐页 runtime CSS 和平台玩法链路文档。
- 验证方式:`npm run test -- src/components/common/PublishShareModal.test.tsx miniprogram/pages/web-view/index.test.js src/services/wechatMiniProgramShareTarget.test.ts``npm run test -- miniprogram/pages/share-grid/index.test.js``npm run test -- src/index.test.ts -t "mini program recommend runtime"``npm run typecheck``npm run check:encoding`
- 关联文档:`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`
## 2026-06-08 微信能力按领域收口
- 背景:微信登录、订阅消息、普通微信支付和小程序虚拟支付能力曾分散在 `api-server` 根模块、`platform-auth``platform-wechat`,支付协议细节和业务 handler 边界不够清晰。
@@ -2764,6 +2790,7 @@
- 决策补充:Pingora release readiness 分为源码全量门禁和 current release runtime-only 门禁。源码 checkout / CI / 构建环境继续运行默认 `check-pingora-release-readiness.mjs`,覆盖 Cargo、npm、Docker、Nginx 静态 / 真机校验和发布包构建烟测;目标机 `/opt/genarrative/current` 的启用前基础门禁和启用后 `--require-direct` 复核必须调用随包 `scripts/check-pingora-release-readiness.mjs --release-runtime-only`,只执行 current release 自审、启用前直连彩排状态、live canary、access log 对账、direct preflight、health patrol env 复核和 direct live smoke。未带 `--require-direct` 时 runtime-only 必须自动运行随包 `scripts/ops/pingora-direct-rehearsal-status.mjs --expect-public-gateway nginx --require-pingora-shadow --require-realpath-canary --require-current-release-gateway --fail-on-critical`,确认公网 `80/443` 仍由 Nginx 接流、Pingora shadow `127.0.0.1:18081`、realpath canary `127.0.0.1:18083` 和 current release 自审均通过;启用后 `--require-direct` 复核不再要求 Nginx 接公网彩排状态,改为检查 direct preflight、health patrol 直连模式和 direct live smoke。API release / current release 必须随包携带 `scripts/check-pingora-release-readiness.mjs``scripts/check-pingora-canary-live.mjs`、canary access log 对账脚本、直连彩排状态脚本和 direct preflight / live 子脚本;runtime-only 模式不得依赖源码 checkout、npm project root、Docker 或目标机 Nginx 静态校验。
- 决策补充:Pingora 直连静态响应必须显式写入缓存头。HTML、目录 index 和 SPA fallback 默认 `Cache-Control: no-cache``/assets/*``/admin/assets/*` 中带 Vite 指纹文件名的资源默认 `Cache-Control: public, max-age=31536000, immutable`;非指纹静态和 ACME challenge 默认 `no-cache`。三档由 `GENARRATIVE_PINGORA_GATEWAY_HTML_CACHE_CONTROL``GENARRATIVE_PINGORA_GATEWAY_ASSET_CACHE_CONTROL``GENARRATIVE_PINGORA_GATEWAY_STATIC_CACHE_CONTROL` 覆盖,配置值不能包含换行或 NUL`npm run check:pingora-gateway-smoke` 必须覆盖这些缓存头,避免直连后入口 HTML 被长期缓存或指纹资源失去长期缓存收益。
- 决策补充:Pingora 直连同一公网 IP 上的多域名时,必须先保住非主站域名的 Host 语义。dev 上 `dev.genarrative.world``git.genarrative.world` 共用 `80/443`,因此 direct env 必须配置 `GENARRATIVE_PINGORA_GATEWAY_GITEA_HOSTS=git.genarrative.world``GENARRATIVE_PINGORA_GATEWAY_GITEA_UPSTREAM=127.0.0.1:3000`,命中 Gitea Host 的请求整站代理到 Gitea,且不走应用维护页、API body limit 或网关接流保护。当前 Pingora TLS listener 只加载一组 cert/key;同时接管 `dev.genarrative.world``git.genarrative.world` 前,证书必须覆盖两个域名,不能使用单域名 SAN 证书。
## 2026-06-11 资产计费边界改为 fail-closed 并补偿退款
- 背景:图片 / 资产生成入口曾在钱包或 SpacetimeDB 预扣费连通性异常时允许继续生成,且失败后同步退款如果遇到 SpacetimeDB 短暂不可用缺少本地补偿;拼图首图后台任务还使用 api-server 进程内 HashSet 互斥,多实例下不能防重复。
@@ -3714,3 +3741,63 @@
- 指标口径:生产素材数统计 `editor_project_resource.source_type = generated`;消耗泥点数统计 `profile_wallet_ledger.source_type = asset_operation_consume` 的负向流水绝对值;总注册用户和新增用户数均来自 `profile_dashboard_state`,其中新增用户数按 `created_at` 落入当前时间窗统计;访问次数只统计 `tracking_daily_stat.scope_kind = site`;访问人数和当前使用人数按登录用户去重,匿名访问人数需要未来补 visitor id 后才能统计。
- 影响范围:`/admin/api/dashboard``shared-contracts` admin DTO、`apps/admin-web` 默认路由和 Dashboard 页面、后台运营文档。
- 验证方式:`cargo test -p api-server --manifest-path server-rs/Cargo.toml admin``npm run admin-web:typecheck``npx vitest run apps/admin-web/src/pages/AdminDashboardPage.test.tsx apps/admin-web/src/app/adminRoutes.test.ts --reporter verbose``npm run check:encoding``git diff --check`
## 2026-06-24 AI 游戏创作智能体使用独立 Tauri App
- 背景:AI 游戏创作需要本地项目落盘、受限命令、本地 HTTP 预览、多智能体编排和短期 / 长期记忆;这些能力不应塞进现有 `apps/desktop-shell` 主站宿主壳。
- 决策:AI 游戏创作桌面入口新建 `apps/ai-game-creator-shell`。普通用户界面只保留聊天和上传入口;任务、能力、文件、记忆、预览和日志只放在开发模式或开发窗口。Agent 能力、manifest、内置命令和权限枚举写入 `packages/shared/src/contracts/gameCreationApp.ts``server-rs/crates/shared-contracts/src/game_creation_app.rs`;专业组和种子任务图写入 `server-rs/crates/platform-agent/src/game_creation.rs``canvas.project_open` 只允许打开本机 Genarrative 编辑器 `/editor/canvas?projectid=...`,默认本机端口为 `3000`,不得扩展成任意 URL 打开能力。
- 本地边界:生成代码、上传资产、短期记忆、长期记忆和预览入口必须保存到用户授权的本地项目目录;正式预览使用只读 `127.0.0.1:<port>` HTTP server,不使用 `file://``game.generate_draft``asset.upload` 这类 `confirm` 命令先在聊天区形成待确认命令,用户确认后才写本地产物;开发窗口中的 `confirm` 命令使用原生确认门,取消时只写日志不执行。`command.run_limited` 只执行白名单内置命令,当前最小真实命令是 `game.static_smoke`,用于检查 `game/index.html` 是否具备 canvas、canvas 渲染上下文、绘制调用、主循环、输入监听、明确目标、失败或胜利状态和重开路径,且不使用远程资源、`eval``new Function``localStorage``fetch``WebSocket``ServiceWorker`,不得把任意 shell 执行暴露给普通用户界面。`.agent/manifest.json` 是本地最小状态源,记录专业组种子任务、资产、预览状态和受限命令运行结果;开发窗口专业组面板读取 manifest task state,不使用前端硬编码作为真相源。`file.list/read/write/delete` 只能访问本地项目目录内的相对路径,禁止绝对路径、`..`、反斜杠和符号链接逃逸。`asset.register` 只登记项目目录内已经存在的文件,并可记录 `uploaded``generated``canvas` 来源元数据。`canvas.asset_import` 是画板回流的本地落点,只导入项目内已有文件为 `canvas` 来源资产,并要求画板项目 ID 与 resourceId / assetObjectId 可追踪;`canvas.export_import` 复用现有画板素材导出 ZIP,把 `metadata.json` 引用的 `images/``media/``sequences/` 文件复制到本地项目 `assets/canvas-imports/` 并登记为 `canvas` 来源资产。
- 2026-06-24 调整:`game.generate_draft` 必须作为一次本地 agent 协作回合记录,用户确认后同时写入短期记忆、长期记忆、设计草案、数值配置、美术清单、音乐音效清单、发布包装草案、可运行 HTML、`.agent/logs/agent.log` 和 manifest `commandRuns`;manifest 任务状态必须反映策划、数值、美术、音乐、程序组首轮完成,预览试玩等待确认。
- 2026-06-24 调整:`game.generate_draft` 必须通过 OpenAI-compatible LLM 生成结构化 JSON 草案,读取 `GENARRATIVE_GAME_CREATOR_LLM_*` 或既有 `GENARRATIVE_LLM_*` / `LLM_*` / `OPENAI_*` 环境变量;LLM 配置缺失、上游失败、返回非 JSON、HTML 非自包含、缺少 `canvas` / `requestAnimationFrame` 或把危险用户输入原样写入 HTML 时直接失败,不得静默回退固定模板并声称 AI 生成。
- 2026-06-24 调整:`game.generate_draft` 生成的 `game/index.html` 必须是可试玩原型,至少具备输入、主循环、目标、失败或胜利状态和重开路径;不得退回按钮计分、纯展示页或占位式游戏。
- 2026-06-24 调整:`game.generate_draft` 的设计草案、发布包装草案和 agent log 必须包含专业组 / 角色 / 产物交接摘要,作为 6 组 agent 协作的最小可追踪证据。
- 2026-06-25 调整:`game.generate_draft` 的 loop 不能只由 Generator 在提示词里模拟六组协作;每一轮必须在 Planner 之后分别调用策划、数值、美术、音乐、程序、运营 6 组下的角色 agent 产出 brief,写入 `.agent/passes/pass-N/groups/<group>/*.md`,再汇总为 `.agent/passes/pass-N/groups/*.md`,由 Generator 读取这些汇总 brief、spec、findings 和记忆整合成结构化草案。`.agent/run.latest.json` 必须记录 `llm.chat.group.<group>.<role>` toolCall 和 brief artifact,作为多智能体协作的最小真实证据。
- 2026-06-25 调整:专业组不再只对应单个 group call。共享契约、`platform-agent` 和本地 manifest 的种子任务图扩展为 6 组下 15 个组内角色任务,覆盖 `Director``Gameplay``Difficulty``Asset``Polish``SFX``Code``Preview``Playtest``Publish` 等角色;每轮 loop 先分别调用角色 agent,写入 `.agent/passes/pass-N/groups/<group>/*.md`,再由 `GroupCoordinator` 汇总为 `.agent/passes/pass-N/groups/*.md` 给 Generator 使用。`.agent/run.latest.json` 必须同时记录角色级 `llm.chat.group.<group>.<role>` toolCall、6 个组汇总 step 和这些角色 brief artifact,避免退回“6 组名义协作、组内无任务图”的实现。
- 2026-06-25 调整:每轮 loop 先由 Orchestrator 写 `.agent/passes/pass-N/agenda.md`。首轮 agenda 全量激活 15 个组内角色任务;返工轮从 `.agent/findings.md` 提取 Evaluator 问题,按任务图重跑命中的角色任务及其下游影响任务,未命中角色写入 carry-over brief 并在 trace 中标记 `carried-over``agent.task_graph.carryover.<group>.<role>`,避免把返工实现成无差别全员重跑,也避免上游产物变化后下游程序预览或运营包装沿用旧 brief。Generator 必须把本轮 agenda 作为输入路径读取。
- 2026-06-25 调整:每轮 Orchestrator 还必须写 `.agent/passes/pass-N/task-graph.json`,记录 activeTaskIds、carriedTaskIds、repairFocus 和按任务依赖排序的 dependencyWaves`.agent/run.latest.json` 顶层 `taskGraph` 必须同步 goal、readyTaskIds、activeTaskIds、carriedTaskIds、repairFocus 和当前任务状态。所有新 step 必须写 phase、taskId、group、role,开发窗口用这些结构化字段展示编排状态,不能只解析 summary 字符串。
- 2026-06-25 调整:返工轮不能只保留自然语言 repairFocusOrchestrator 必须把每条 Evaluator 问题转成 `repairRoutes[]`,写明 issue、taskIds 和 reason,并把同一结构写入 pass 级 `task-graph.json` 与 run 级 `taskGraph`,开发窗口展示该路由,测试必须断言输入 / canvas / 静态 smoke 类问题会路由到程序组角色任务。
- 2026-06-25 调整:返工路由、activeTaskIds、carriedTaskIds 和 dependencyWaves 的 pass 级决策下沉到 `server-rs/crates/platform-agent/src/game_creation.rs` 的纯编排内核,`apps/ai-game-creator-shell` 只调用该内核并负责 `.agent/passes/pass-N/agenda.md``task-graph.json`、run trace 和本地工具执行,避免 agent loop 语义只停留在 Tauri shell 私有实现。
- 2026-06-25 调整:`Evaluator``.agent/findings.md` 时必须附带 `## Repair Routes` JSON,包含 issue、taskIds 和 reason`platform-agent` 下一轮优先解析该结构化路由并清理未知 taskId / 重复 taskId,只有缺失或解析失败时才退回关键词路由,避免 loop 返工范围依赖自然语言猜测。
- 2026-06-25 调整:`platform-agent` 会把结构化 `repairRoutes[].taskIds` 自动扩展为下游影响闭包,并在 reason 上追加 `dependency-impact`;例如 `art-asset-plan` 变化会继续激活 `art-polish`、程序组预览链路和运营发布包装,`code-prototype` 变化会继续激活预览和运营包装,但不会倒回去重跑无关上游。
- 2026-06-25 调整:`game.static_smoke` 不能只检查 canvas、RAF 和输入事件字样;还必须拒绝空输入监听以及明显占位 / placeholder / 固定星核传送门模板词,避免占位页或固定模板被当作可试玩原型进入预览。
- 2026-06-25 调整:`game.generate_draft` 的 agent loop 跑满 3 轮仍未通过 Evaluator 时必须整体失败,只保留 `.agent/passes/pass-N/` 中间快照和失败 run trace,不写入 `memory/session.md``memory/project.md``game/game_design.md``game/balance.json`、资产清单、发布 README,也不把默认 `game/index.html` 覆盖成最终游戏产物;测试必须断言失败 trace 的 `stopReason=max-passes-exhausted`,且没有 `ArtifactWriter` / `Playtest` step。
- 2026-06-25 调整:`.agent/agent.db` 先作为最小 append-only JSONL 本地索引,不引入 SQLite 或新依赖。项目初始化写 `project.init`,每次 `game.generate_draft` 追加目标、标题和本地产物路径,上传 / 登记 / 画板导入资产时追加 `asset.register``asset.update`;正式状态仍以 `.agent/manifest.json``.agent/run.latest.json``.agent/runs/` 为主要事实源。
- 2026-06-25 调整:`game.generate_draft` 生成前必须从 `.agent/manifest.json` 派生本地资产上下文,把上传、登记和画板回流资产的 id、kind、mediaType、localPath、source 以及 canvasProjectId / resourceId / assetObjectId 等追踪字段传给 Planner、组内角色 agent 和 Generator;资产上下文必须排在长期记忆前,避免长期记忆过长时被 prompt 截断;trace 中 Planner、角色 agent 和 Generator 的 `inputPaths` 必须显式包含 `.agent/manifest.json`,避免开发窗口看不到资产上下文来源;不要新增平行资产记忆文件,也不要读取二进制资产内容塞进 prompt。
- 2026-06-25 调整:新增 `npm run ai-game-creator-shell:agent-run:smoke` 作为无密钥开发验证入口。脚本在本机启动 OpenAI-compatible 测试 provider,预置一个本地上传图片和一个本地上传音频,并复用真实 `--agent-run`、本地落盘、`game.static_smoke` 和本地 HTTP 预览;脚本会断言 provider 请求体包含图片与音频资产上下文、生成 HTML 引用 `/assets/...`、预览服务能用 `GET` 读取这些资产、用 `HEAD` 返回真实资源长度和对应 MIME、headless Chrome 打开预览后至少执行一帧游戏 JS,且通过确定性亮色探针采样证明 canvas 不是空白画布、第二轮重跑 Evaluator 命中任务及其下游影响任务,未受影响组 carry-over,再自动给 CLI 发送回车停止预览。该脚本仅验证 runtime,不作为产品生成 fallback。
- 2026-06-25 调整:新增根级 `npm run ai-game-creator-shell:check` 作为 v1 开发验收入口,串起壳 typecheck、`platform-agent` 编排测试、`shared-contracts` 契约测试、Tauri Rust 测试和无密钥本地 provider 端到端 smoke,避免测试口径散落成多条手工命令。
- 2026-06-25 调整:`scripts/check-native-shells.mjs` 的 AI 游戏创作项从单独 typecheck 升级为 `npm run ai-game-creator-shell:check`,让原生壳总门禁覆盖 agent loop、本地落盘、静态自检和本地 HTTP 预览 smoke。
- 2026-06-25 调整:普通用户通过聊天输入 `/llm-status` 触发只读 `llm.config_check`,用于检查 LLM base_url、model 和 API Key 是否已从环境变量读取;状态消息不得显示或保存 API Key。终端可用 `npm run ai-game-creator-shell:llm-status` 做同类配置自检,缺配置时以非零状态退出。生成仍只从环境变量读取配置,不新增仓库文件、项目文件或普通用户界面里的 secret 持久化。
- 2026-06-25 调整:`npm run ai-game-creator-shell:dev` 固定加载 `http://127.0.0.1:3080/`Vite 继续 `strictPort` 与 Tauri `devUrl` 对齐。`beforeDevCommand` 改为先复用已经跑在 3080 且页面标题为 `AI 游戏创作` 的本 app Vite server,避免上次 Tauri 退出后遗留的同 app Vite 进程导致二次启动失败;如果 3080 是其它服务,仍直接失败并要求释放端口,不做端口漂移。
- 2026-06-25 调整:`preview.start` / `preview.stop` 必须追加 `.agent/logs/preview.log`,并把该日志列入 Preview trace step 的输出路径和 artifact 清单;这样 `preview-playtest` 任务声明的日志产物与实际本地 HTTP 预览行为一致。
- 2026-06-25 调整:AI 游戏创作 App v1 仍只维护一个全局本地 HTTP 预览实例;启动新项目预览替换旧预览时,必须 best-effort 把旧项目的 manifest preview 状态、`.agent/logs/preview.log` 和 run trace 记录为 stopped,避免旧项目状态残留 `running`。旧项目目录已删除时不阻断新预览启动。
- 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-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 面板仍只在开发窗口展示,普通用户窗口不新增面板。
- 2026-06-25 调整:普通用户通过聊天输入 `/import-canvas-export /绝对/画板素材.zip 画板项目ID` 触发待确认 `canvas.export_import`,读取现有 `/editor/canvas` 素材导出 ZIP。导入命令只读取用户指定 ZIP,写入当前本地项目 `assets/canvas-imports/`,基础护栏限制路径逃逸、文件数量和解压体积;导出包没有真实 resourceId 时,用 `canvas-export:<file>` 作为可追踪 assetObjectId,不伪造后端画板资源行。
- 2026-06-25 调整:普通用户通过聊天输入 `/sync-canvas-project 画板项目ID` 触发待确认 `canvas.project_sync`,复用现有 `/api/external/v1/editor/projects/{projectId}` 读取画板项目快照,再用 `/api/external/v1/assets/read-url` 对 objectKey 或 legacy path 换签,下载资源到本地项目 `assets/canvas-sync/` 并登记为 `canvas` 来源资产;该命令从 `GENARRATIVE_GAME_CREATOR_EDITOR_API_KEY``GENARRATIVE_EXTERNAL_API_KEY` 读取平台 API Key,默认 base URL 为 `http://127.0.0.1:8082`,可用 `GENARRATIVE_GAME_CREATOR_EDITOR_API_BASE_URL` 覆盖。API Key 不写入 manifest、agent.db、trace 或日志。该路径不伪装浏览器登录态,也不绕过画板生成、钱包扣费或外部生成 worker;它只同步用户 API Key 已有权限读取的画板资源。
- 2026-06-25 调整:美术组 `Asset` 和音乐组 `SFX` 角色在 loop 中读取 `.agent/manifest.json`;当本地项目还没有对应类型的 `canvas` 来源资产时,角色 step 会追加 `agent.tool.suggest.canvas.project_sync` toolCall:美术组需要 `image/*``application/vnd.genarrative.image-sequence`,音乐组需要 `audio/*`。该 toolCall 只作为 trace 中的建议,不自动调用 External Editor API,也不绕过用户确认。
- 2026-06-24 调整:同一本地项目多次 `game.generate_draft` 必须追加 `memory/session.md``memory/project.md`,不得覆盖历史对话和创作目标记录。
- 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 调整:普通用户通过聊天输入 `/read 本地相对路径` 触发只读 `file.read`,只在聊天消息里展示项目内文本文件并截断长文本;不得开放聊天里的文件写入或删除能力。
- 2026-06-24 调整:普通用户通过聊天输入 `/tasks` 触发只读 `task.list`,只在聊天消息里展示专业组、角色、任务状态和产物交接;不得把任务面板暴露到普通用户窗口。
- 2026-06-24 调整:普通用户只能通过聊天触发内置命令;当前 `/smoke` 映射到白名单 `command.run_limited game.static_smoke` 并走待确认卡片,不允许扩展成任意 shell 或自由命令解析。
- 2026-06-24 调整:普通用户通过聊天输入 `/project /绝对路径` 触发 `project.create` 待确认命令,用于授权并初始化本地项目目录;相对路径不会生成待确认命令;不要把开发窗口项目路径输入框暴露到正式用户界面。
- 2026-06-25 调整:普通用户侧所有会写入、运行、查看 / 打开预览或导入本地产物的命令必须先完成 `/project` 初始化,包括 `game.generate_draft``asset.upload``game.run_local``command.run_limited``preview.start``preview.status``preview.open``preview.stop``memory.write``memory.delete``canvas.project_sync``canvas.asset_import``canvas.export_import`;没有已授权本地项目时只提示设置项目,不得落到默认 `/tmp` 草稿目录。
- 2026-06-24 调整:终端测试入口使用同一个 Tauri Rust 二进制的 `--agent-run <本地项目绝对路径> <创作需求>`,只复用现有 `game.generate_draft``game.static_smoke` 和本地 HTTP 预览链路,不另建第二套 agent runtime;LLM 配置只从环境变量读取,不写入仓库或项目文件。需要自动验证时可追加 `--no-wait`,生成预览 trace 后立即停止本地预览,避免命令卡在回车等待。
- 2026-06-24 调整:AI 游戏创作 App 的 release 配置只登记 `main` 聊天窗口,主窗口保持聊天尺寸;任务、文件、记忆、预览、日志和能力面板只能通过 debug/dev 下额外创建的 `developer` 窗口或 Vite dev `?dev/#dev` 查看,不进入普通用户窗口。
- 2026-06-24 调整:`check:native-shells` 必须静态守住 AI 游戏创作 App 的用户 / 开发边界:release 只保留 `main` 聊天窗口,`developer` 窗口只在 debug 创建,开发面板只能在 `devMode` 分支渲染。
- 2026-06-25 调整:`check:native-shells``ai-game-creator-shell:check` 之后必须追加 `ai-game-creator-shell:build -- --no-bundle`,让原生壳总门禁同时证明 AI 游戏创作独立 Tauri 壳能完成 release 编译,而不是只证明前端 / Rust 逻辑测试通过。
- 2026-06-24 调整:普通用户通过聊天输入 `/preview` 触发待确认 `preview.start`,完成 `/project` 初始化后可通过 `/open-preview` 触发待确认 `preview.open` 并只打开当前已授权项目对应的 `127.0.0.1` 本地预览,通过 `/preview-status` 查询当前项目预览,通过 `/preview-stop` 停止当前项目预览;预览 iframe 和状态面板仍只在开发窗口可见,不能把 `preview.open` 扩展成任意 URL 打开能力,也不能打开、展示或停止其它本地项目遗留的全局预览。
- 2026-06-25 调整:`/preview-status` 虽然是只读命令,也必须写入 `preview.status` 命令日志并向聊天返回错误,不得因查询失败产生未捕获异常或无审计记录。
- 2026-06-24 调整:普通用户通过聊天输入 `/memory [short]` 读取长期或短期记忆,通过 `/remember 内容` 待确认追加长期记忆,通过 `/forget-memory [short]` 待确认删除记忆;不得为了记忆查看或编辑暴露独立用户面板。
- 2026-06-25 调整:`/remember` 支持可选 scope`/remember short 内容` 追加短期记忆,`/remember long 内容` 或未写 scope 时追加长期记忆;仍统一走待确认 `memory.write`,不暴露独立用户面板。
- 2026-06-24 调整:普通用户通过聊天输入 `/canvas 画板项目ID` 触发待确认 `canvas.project_open`,只打开本机 Genarrative 编辑器 `/editor/canvas?projectid=...`;不得把它扩展成远程站点或任意 URL 打开能力。
- 2026-06-24 调整:普通用户通过聊天输入 `/import-canvas-asset 本地路径 画板项目ID 资源ID|object:资产对象ID [kind] [mediaType]` 触发待确认 `canvas.asset_import`,只登记项目目录内已有文件为 `canvas` 来源资产;只有 `assetObjectId` 时使用 `object:` 前缀,不伪造 resourceId;画板导出包回流使用 `/import-canvas-export /绝对/画板素材.zip 画板项目ID`
- 验证方式:`npm run ai-game-creator-shell:typecheck``cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml``npm run test -- packages/shared/src/contracts/gameCreationApp.test.ts``cargo test -p shared-contracts game_creation_app --manifest-path server-rs/Cargo.toml``cargo test -p platform-agent --manifest-path server-rs/Cargo.toml``npm run check:encoding``git diff --check`
@@ -0,0 +1,160 @@
# AI 游戏创作智能体 App 实施计划
## 目标
在 Genarrative 内建设独立桌面 App:普通用户只看到聊天入口,通过聊天、上传文件和确认操作,让平台生成保存在本地的可运行 Web 游戏原型,并通过本地 HTTP server 预览。任务、文件、预览和日志等工程面板放到开发构建的独立开发窗口。v1 只做 Web 小游戏原型闭环,不扩展 Unity、Godot、云同步或插件市场。
## 技术选择
- 桌面壳:新建 `apps/ai-game-creator-shell`,与现有 `apps/desktop-shell` 分离,避免把游戏创作本地能力塞进主站宿主壳;正式用户窗口只加载聊天页,开发构建单独打开开发窗口承载任务、文件、预览和日志面板。
- 平台后端:继续使用 `server-rs + Axum + SpacetimeDB`
- 本地能力:使用 Tauri Rust command;正式用户 App 只启动 `127.0.0.1` 本地 HTTP preview 并交给外部浏览器,不在正式用户窗口内承载游戏预览画面。
- Agent Runtime:扩展 `server-rs/crates/platform-agent`,不引入 LangChain、AutoGen、Microsoft Agent Framework 或 OpenAI Agents SDK sidecar 作为核心。
- 设计参考:借鉴 OpenAI Agents SDK 的 Agent、Tools、Handoffs、Guardrails、Tracing 抽象,但运行时由 Genarrative 自己掌控。
## Runtime 边界
Agent Runtime 负责:
- 总任务拆分和任务图状态流转。
- 6 个专业组调度:策划组、美术组、程序组、数值组、音乐组、运营组。
- 工具注册、权限 gate、工具调用预算和执行日志。
- 短期记忆、长期记忆、本地 manifest 和产物索引。
- 本地项目文件、运行命令、HTTP 预览和验收自检。
专业组不是 6 个单 Agent。每组内部按任务动态使用 Director、Gameplay、Code、Asset、Preview、Playtest、Polish、Publish 等角色模板。
## Agent 能力清单
- 用户能力:聊天入口、上传文件;正式用户窗口不展示任务、文件、预览、日志或能力清单。
- 命令能力:内置命令调用、权限 gate、执行日志;v1 只允许白名单受限命令,不执行任意 shell。
- 编排能力:任务拆分、任务图依赖、专业组调度、多智能体协作。
- 任务图能力:每轮 Orchestrator agenda、ready / active task 选择、Evaluator 结构化返工路由、返工轮 carry-over。
- 记忆能力:短期记忆 `memory/session.md`、长期记忆 `memory/project.md`,支持读取、保存和删除。
- 本地能力:生成代码和资产落盘、本地 manifest、受限运行命令、本地 HTTP 预览。
- 文件能力:`file.list/read/write/delete` 只允许访问项目目录内的相对路径,拒绝绝对路径、`..`、反斜杠和符号链接。
- 创作能力:策划、美术、程序、数值、音乐、运营 6 组专业 agent,组内按任务使用 Director、Gameplay、Code、Asset、Preview、Playtest、Polish、Publish 等角色模板。
## 本地项目结构
```text
game-project/
game/
assets/
memory/
exports/
.agent/
agent.db
manifest.json
run.latest.json
runs/
logs/
```
生成代码、图片、音频、关卡数据、配置和导出包必须落到用户授权的本地项目目录。正式预览只使用 `127.0.0.1:<port>` 本地 HTTP server,不使用 `file://`
## 画板对接
美术组和音乐组复用现有画板能力,不另建平行资产系统。
- 美术组通过画板链路生成角色、场景、UI、图标、动画和宣传素材。
- 音乐组复用画板已有 `audio``sound-effect``background-music` 能力。
- 默认后台生成,用户需要精修时打开画板继续编辑。
- `canvas.project_open` 只打开本机 Genarrative 编辑器的 `/editor/canvas?projectid=...`,默认地址为 `http://127.0.0.1:3000`,开发者可在开发窗口改成本机端口;不允许打开远程站点或任意 URL。
- 画板资源回流到本地项目 `assets/`,并在 manifest 中记录画板项目、资源 ID、assetObjectId、prompt、model、taskId 和 assetKind;当前最小落地提供 `asset.register` 登记项目内已有资产,并提供 `canvas.export_import` 读取现有画板素材导出 ZIP。
- `canvas.project_sync` 复用 Genarrative External Editor API,读取用户平台 API Key 可访问的画板项目快照,通过 `/api/external/v1/assets/read-url` 换签并把资源下载到本地项目 `assets/canvas-sync/`;默认 API base URL 为 `http://127.0.0.1:8082`,可用 `GENARRATIVE_GAME_CREATOR_EDITOR_API_BASE_URL` 覆盖,API Key 从 `GENARRATIVE_GAME_CREATOR_EDITOR_API_KEY``GENARRATIVE_EXTERNAL_API_KEY` 读取,不写入项目文件、trace、manifest 或日志。
- Agent loop 中美术组 `Asset` 和音乐组 `SFX` 会读取 `.agent/manifest.json`;当本地项目尚无对应类型的 `canvas` 来源资产时,会在 `.agent/run.latest.json` 的角色 step 中追加 `agent.tool.suggest.canvas.project_sync` 建议:美术组需要 `image/*``application/vnd.genarrative.image-sequence`,音乐组需要 `audio/*`。该建议提示用户通过聊天确认 `/sync-canvas-project <画板项目ID>` 回流画板资源,只进入 trace,不自动调用外部 API,也不绕过用户确认。
- `canvas.asset_import` 当前作为最小真实链路:导入项目目录内已有文件为 `canvas` 来源资产,并要求记录画板项目 ID 以及 resourceId 或 assetObjectId。
- `canvas.export_import` 复用 `/editor/canvas` 已有素材导出 ZIP 格式,读取根 `metadata.json`、复制 `images/` / `media/` / `sequences/` 到本地项目 `assets/canvas-imports/`,再按导出层登记为 `canvas` 来源资产;导出包不保存真实 resourceId 时,使用 `canvas-export:<file>` 作为可追踪 assetObjectId,不伪造后端资源行。
## 分阶段实施
1.`platform-agent` 建立游戏创作专业组与种子任务图契约。
2. 在共享契约中补本地项目 manifest、内置命令和权限枚举。
3. 扩展 `apps/ai-game-creator-shell` 的本地能力:项目目录、文件写入、受限命令、本地 HTTP 预览。
4. 用户侧只保留聊天界面;任务、文件/资产、预览和日志只在开发构建的独立开发窗口展示。
5. 将美术组、音乐组接入现有画板与外部生成队列。
## v1 验收
- 用户能创建本地 Web 游戏项目。
- 用户侧只看到聊天框;开发环境通过独立窗口查看任务拆分、专业组、状态、产物和错误。
- 生成代码和资产进入用户本地项目目录。
- 本地 HTTP 预览能启动,并在外部浏览器展示可玩原型。
- 美术/音乐资产能从画板链路回流到本地项目。
- 长期记忆可查看、可编辑、可删除。
## v1 验收证据矩阵
- `npm run ai-game-creator-shell:check`:覆盖壳 typecheck、聊天命令单测、用户 / 开发窗口 UI 边界 smoke、聊天侧 `/capabilities` 展示标准 Agent 能力清单且不打开开发面板、聊天侧 `/audit` 从 manifest / 本地文件 / `.agent/run.latest.json` 分别汇总用户面、6 组任务配置、6 组协作证据、任务编排、loop、记忆、本地产物、HTTP 预览、画板回流和权限日志证据且不打开开发面板;未生成 `.agent/run.latest.json` 前,`/audit` 只能标记任务配置通过,不能把 6 组协作证据误判为通过;trace 已存在但状态为 `failed``needs-revision``running``max-passes-exhausted` 或缺少 `Evaluator passed` 步骤时,`/audit` 不能把 loop 误判为通过。聊天侧 `/llm-status` 只显示 base_url / model / API Key 已读取状态且不泄露密钥本体、聊天侧长期记忆查看 / 追加 / 覆盖 / 删除的授权本地项目路径、上传资产写入后的 manifest 刷新和 `/assets` 聊天可见性、`/smoke` 聊天侧确认后只通过授权本地项目路径执行白名单 `game.static_smoke``/run` 聊天侧确认后通过授权本地项目路径执行 `game.static_smoke`、启动 `127.0.0.1` 本地预览并交给外部浏览器、`/preview` 聊天侧确认后通过授权本地项目路径启动 `127.0.0.1` 本地预览并交给外部浏览器、`/status` 聊天侧项目 / 任务 / 资产 / 预览 / 最近命令汇总、`/files` 聊天侧本地文件列表、`/read` 聊天侧文件读取的授权本地项目路径、`/tasks` 聊天侧任务拆分与下一步专业组展示的授权本地项目路径、聊天确认生成后自动读取 `.agent/run.latest.json` 并在普通聊天消息里展示 loop 轮次 / 工具调用 / active 任务 / carry-over / 返工焦点 / 画板同步建议命令 / 本地产物、`/trace` 聊天侧读取 `.agent/run.latest.json` 并展示 loop 轮次 / active 任务 / 返工路线 / dependency waves 的授权本地项目路径、`platform-agent` 编排测试、共享契约测试、Tauri 本地能力测试和无密钥本地 provider 端到端 smoke;用于证明独立 App、真实 LLM-compatible loop、本地落盘、自检和 HTTP 预览闭环,并覆盖 loop 跑满 3 轮失败时不会写入最终游戏产物。
- `npm run check:native-shells`:覆盖 AI 游戏创作壳的 release/dev 窗口边界、正式用户 App 不嵌入游戏预览 iframe、用户侧预览命令交给外部浏览器和 Tauri release `--no-bundle` 构建 smoke;用于证明正式用户窗口只登记 `main` 聊天窗口,开发面板只在 debug/dev 路径打开,独立壳能完成 release 编译。
- `npm run check:encoding``git diff --check`:覆盖中文文档、中文命令文案和补丁空白;用于避免乱码、尾随空白和无关格式漂移。
- `npm run ai-game-creator-shell:llm-status`:只检查 LLM 环境变量是否就绪,不请求上游、不显示 API Key;用于本机联调前确认配置。CLI 和桌面 App 内的 `/llm-status` / 生成入口都会先读取仓库根目录或 `apps/ai-game-creator-shell/` 下 gitignored 的 `.env.secrets.local`,再检查当前进程环境。
- `npm run ai-game-creator-shell:agent-run -- --no-wait /绝对项目路径 "游戏创作需求"`:使用真实 OpenAI-compatible 配置跑一次本地生成、落盘、自检和预览;用于人工验收真实 provider 路径。真实 provider 可放在 gitignored 的 `.env.secrets.local` 中,至少包含 `GENARRATIVE_GAME_CREATOR_LLM_API_KEY``GENARRATIVE_GAME_CREATOR_LLM_BASE_URL``GENARRATIVE_GAME_CREATOR_LLM_MODEL`,真实网关长请求若在非流式响应前被 60 秒空闲连接切断,联调时设置 `GENARRATIVE_GAME_CREATOR_LLM_STREAM=true`
## 当前最小落地
- `apps/ai-game-creator-shell` 是独立 Tauri App,不复用 `apps/desktop-shell`
- 本地项目初始化会创建 `game/``assets/``memory/``exports/``.agent/logs/`,写入 `.agent/manifest.json`,生成 append-only JSONL 本地产物索引 `.agent/agent.db`,并生成默认 `game/index.html`
- 普通用户可在聊天框输入 `/project /绝对路径` 生成待确认的 `project.create` 命令,用于授权并初始化本地项目目录;相对路径不会生成待确认命令;开发窗口仍可直接编辑项目路径。
- 普通用户侧的生成、上传、运行、自检、预览状态 / 启动 / 打开 / 停止、记忆写入和画板资产导入都必须先完成 `/project` 初始化;未初始化时只提示设置本地项目,不落到默认临时目录。
- 终端可用 `npm run ai-game-creator-shell:llm-status` 检查 LLM 环境变量是否就绪;CLI 和桌面 App 内的 `/llm-status` / 生成入口都会先读取 gitignored 的 `.env.secrets.local`,不请求上游、不显示 API Key,缺配置时以非零状态退出或在聊天里提示未就绪。
- 终端可用 `npm run ai-game-creator-shell:check` 跑 v1 开发验收:壳 typecheck、`platform-agent` 编排测试、共享契约测试、Tauri Rust 测试和无密钥本地 provider 端到端 smoke。
- 终端可用 `npm run ai-game-creator-shell:agent-run -- /绝对项目路径 "游戏创作需求"` 跑一次真实 LLM 生成、落盘、`game.static_smoke` 和本地 HTTP 预览;该入口读取当前环境和 gitignored 的 `.env.secrets.local`,不把 API Key 写入仓库或项目文件。自动验证可加 `--no-wait`,例如 `npm run ai-game-creator-shell:agent-run -- --no-wait /tmp/genarrative-ai-game-test "像素风反弹弹幕厨房"`,生成预览 trace 后立即停止本地预览,避免终端卡在回车等待。真实 OpenAI-compatible 网关建议设置 `GENARRATIVE_GAME_CREATOR_LLM_STREAM=true` 跑 Planner、组内角色和 Generator,避免长请求非流式空闲断连。
- 终端可用 `npm run ai-game-creator-shell:agent-run:smoke` 跑一次无密钥本地端到端 smoke:脚本启动本机 OpenAI-compatible SSE 流式测试 provider,预置一个本地上传图片和一个本地上传音频,复用真实 `--agent-run`、Planner / Orchestrator / 角色 agent / Generator / Evaluator loop、本地落盘、`game.static_smoke` 和本地 HTTP 预览,并断言每次 provider 请求都使用 `stream: true`、provider prompt 收到图片与音频资产上下文、生成 HTML 引用这些资产、预览服务能用 `GET` 读取 `/assets/...`、用 `HEAD` 返回真实资源长度和对应 MIME、headless Chrome 打开预览后至少执行一帧游戏 JS,且通过确定性亮色探针采样证明 canvas 不是空白画布、`.agent/run.latest.json` 的 step group 覆盖 design / balance / art / audio / code / publishing 六组、第二轮会重跑 Evaluator 命中任务及其下游影响任务,未受影响角色 carry-over;随后脚本自动给 CLI 发送回车停止预览。该脚本只用于开发验证,不进入产品生成路径。
- `npm run ai-game-creator-shell:dev` 的 Tauri `devUrl` 固定为 `http://127.0.0.1:3080/`Vite 必须 `strictPort` 对齐;`beforeDevCommand` 先复用已经跑在 3080 且页面标题为 `AI 游戏创作` 的本 app Vite server,否则才启动新的 Vite,若端口被其它服务占用则直接失败并提示释放端口。
- `.agent/manifest.json` 会保存 6 个专业组下 15 个组内角色任务状态,当前覆盖 `Director``Gameplay``Difficulty``Asset``Polish``SFX``Code``Preview``Playtest``Publish`;开发窗口的专业组面板读取 manifest,而不是前端硬编码。
- 共享契约和 `platform-agent` 会按任务依赖与 `completed` 状态计算当前可执行任务,作为 v1 的最小编排选择器;每轮 `Orchestrator` 的 activeTaskIds、carriedTaskIds、repairRoutes 和 dependencyWaves 由 `platform-agent` 纯编排内核产出,`apps/ai-game-creator-shell` 只负责写入 `.agent/passes/pass-N/` 和执行本地工具;`Evaluator` 会在 `.agent/findings.md` 写出 `## Repair Routes` JSON,下一轮编排优先采用该结构化 taskIds,解析不到时才退回关键词路由;返工路由会按任务图自动扩展下游影响任务,例如美术资产变化会继续触发程序预览和运营包装重算。
- `game.generate_draft` 使用 OpenAI-compatible LLM 配置生成结构化 JSON 草案,读取 `GENARRATIVE_GAME_CREATOR_LLM_API_KEY` / `GENARRATIVE_LLM_API_KEY` / `LLM_API_KEY` / `OPENAI_API_KEY``GENARRATIVE_GAME_CREATOR_LLM_BASE_URL` / `GENARRATIVE_LLM_BASE_URL` / `LLM_BASE_URL` / `OPENAI_BASE_URL``GENARRATIVE_GAME_CREATOR_LLM_MODEL` / `GENARRATIVE_LLM_MODEL` / `LLM_MODEL` / `OPENAI_MODEL``GENARRATIVE_GAME_CREATOR_LLM_STREAM=true` 时 Planner、组内角色和 Generator 使用流式请求;缺少配置或模型返回非法 JSON 时直接失败,不静默回退固定模板。
- 聊天输入 `/llm-status` 会触发只读 `llm.config_check`,确认 LLM base_url、model 和 API Key 是否已从环境变量读取;状态消息不会显示或保存 API Key。
- `game.generate_draft` 的 LLM JSON 必须包含 `handoffs` 数组,覆盖 `design``balance``art``audio``code``publishing` 6 个专业组;每组必须给出 role、summary、outputs 和 next,缺组或交接内容不完整会判定为模型输出无效并进入返工。
- `game.generate_draft` 的真实生成路径使用最小 Planner / Orchestrator / 组内角色 agent / Generator / Evaluator loopPlanner 写 `.agent/spec.md`;每轮 Orchestrator 先写 `.agent/passes/pass-N/agenda.md``.agent/passes/pass-N/task-graph.json`,首轮全量调度 15 个角色任务,返工轮按 `.agent/findings.md` 生成结构化 `repairRoutes`,重跑命中问题的角色任务及其下游依赖任务,其余角色 brief 从上一轮 carry-over`task-graph.json` 记录 activeTaskIds、carriedTaskIds、repairFocus、repairRoutes 和按依赖排序的 dependencyWaves;角色 brief 写入 `.agent/passes/pass-N/groups/<group>/*.md`,再汇总为 `.agent/passes/pass-N/groups/*.md`;Generator 必须读取用户需求、记忆、`.agent/spec.md`、本轮 `agenda.md``task-graph.json``.agent/findings.md` 和 6 组汇总 brief 后返回结构化 JSON;每轮会把 Generator 草案拆成 6 组交接快照,写入 `.agent/passes/pass-N/`;Evaluator 做本地静态验收并写 `.agent/findings.md`
- loop 最多执行 3 轮;Evaluator 发现 HTML 非自包含、缺少 `canvas`、缺少 `requestAnimationFrame`、缺少输入监听或用户输入未转义时,把问题写入 `.agent/findings.md` 并让下一轮 Generator 修复。3 轮仍失败则 `game.generate_draft` 失败,不写最终游戏产物。
- loop 每次运行会写 `.agent/run.latest.json``.agent/runs/<runId>.json`,记录 `Planner` / `Orchestrator` agenda / 15 个组内角色 brief 或 carry-over / 6 个 `GroupCoordinator` 汇总 / `Generator` / 6 个专业组交接 / `Evaluator` / `ArtifactWriter` / `Playtest` step、每步 `toolCalls`、输入文件、输出文件、状态、轮次、maxPasses、toolCallCount、maxToolCalls、stopReason、nextStep 和错误摘要;Planner、角色 agent 和 Generator 的 `inputPaths` 必须包含对应记忆文件、`.agent/manifest.json` 和 agenda 等上下文来源;每个 step 必须带 phase、taskId、group 和 role`.agent/run.latest.json.taskGraph` 必须记录 goal、readyTaskIds、activeTaskIds、carriedTaskIds、repairFocus、repairRoutes 和当前任务状态;`.agent/run.latest.json.passPlans` 必须按轮记录 mode、summary、activeTaskIds、carriedTaskIds、dependencyWaves、repairFocus 和 repairRoutes,作为 `/trace` 与开发窗口判断编排 loop 是否真实发生的直接证据;`run.latest.json` 是当前指针,`.agent/runs/` 保留历史 run trace;开发窗口读取 latest 展示编排过程,并复用 `file.list/read` 列出和载入历史 run,普通用户窗口不展示。
- `.agent/run.latest.json``artifacts` 使用结构化记录,包含相对路径、字节数和 `fnv1a64:` checksum;除最终本地产物外,也会收集 `.agent/passes/pass-N/` 快照,便于确认返工前后的产物差异。
- 通过 Evaluator 和 `game.static_smoke` 后,Agent loop 会把本次 runId、状态、轮次、下一步、active / carry-over 任务和最终本地产物摘要追加到 `memory/session.md``memory/project.md`;下一次 Planner、组内角色和 Generator 会通过记忆输入自然读取上一轮稳定原型状态,而不只依赖开发窗口 trace。
- `.agent/agent.db` 当前作为最小本地索引文件使用 JSONL:初始化写入 `project.init`,每次 `game.generate_draft` 追加目标、标题和本地产物路径,上传 / 登记 / 画板导入资产时追加 `asset.register``asset.update`v1 不引入 SQLite 依赖。
- `.agent/run.latest.json` 的 schema 固定为共享契约 `GAME_CREATION_AGENT_RUN_SCHEMA_VERSION = game-creator-agent-run.v1`;TS 与 Rust 都从共享契约读取 run trace 类型,避免开发窗口和 Tauri 写入结构漂移。
- `game.generate_draft` 写入最终产物后会复用白名单受限命令 `game.static_smoke` 做一次生成后自检,至少检查 `game/index.html` 包含 canvas、canvas 渲染上下文、绘制调用、主循环、非空输入监听、明确目标、失败或胜利状态和重开路径,且不使用远程资源、`eval``new Function``localStorage``fetch``WebSocket``ServiceWorker`,也不得包含占位 / placeholder / 固定星核传送门模板词,并把该工具调用写入 `.agent/run.latest.json``.agent/logs/command.log`;自检失败则本次命令失败,不继续启动预览。
- `ArtifactWriter` step 使用 `file.write.local_artifacts` 工具调用记录最终写入的 `memory/``game/``assets/``exports/``.agent/manifest.json` 路径;写入完成后 `nextStep` 指向 `game.static_smoke`
- `preview.start` / `preview.stop` 会追加 `.agent/logs/preview.log`,并在 `.agent/run.latest.json` 已存在时追加 `Preview` step 和 `preview.*` toolCall,记录本地 HTTP 预览 URL 与停止事件;单全局本地预览被新项目替换时,会 best-effort 把旧项目 manifest、preview log 和 trace 记录为 stopped,避免旧项目残留 running;本地 HTTP server 的 `/` 映射到 `game/index.html`,只允许读取 canonical 后仍位于项目真实 `game/` 或真实 `assets/` 下的文件,拒绝 `memory/``.agent/``exports/``..`、一级 `game` / `assets` 符号链接目录和内部符号链接越界,并为常见图片、音频、视频和 Web 资源返回对应 MIME;静态 `HEAD` 返回真实 `Content-Length` 但不返回 body,确保浏览器和媒体资源探测可用;上传和画板回流资产可被生成游戏引用但不会暴露记忆或 trace;没有 run trace 的手动预览启动不阻断。
- 聊天输入会生成待确认的 `game.generate_draft` 内置命令;用户确认后才把 LLM 返回的结构化草案写入短期记忆 `memory/session.md`、长期记忆 `memory/project.md`、设计草案 `game/game_design.md`、数值配置 `game/balance.json`、美术清单 `assets/manifest.art.json`、音乐音效清单 `assets/manifest.audio.json`、发布包装草案 `exports/README.md` 和可运行 `game/index.html`。生成完成后,普通聊天消息会自动展示最近一次 Agent loop 的轮次、工具调用、active / carry-over 任务、返工焦点、画板同步建议命令和本地产物摘要;完整证据仍由 `/trace` 读取同一份 `.agent/run.latest.json`
- `game.generate_draft``game/index.html` 必须是可试玩原型,至少包含输入、主循环、目标、失败或胜利状态和重开路径;不能只输出按钮计分或纯展示页。
- `game.generate_draft` 会校验 LLM 输出:`balance`、美术清单和音乐清单必须是 JSON object`gameHtml` 必须是自包含 HTML、包含 `canvas``requestAnimationFrame`,不得加载远程脚本或资源,不得使用 `eval` / `new Function` / `localStorage` / `fetch` / `WebSocket` / `ServiceWorker`,不得把包含 `<` / `>` 的用户输入原样写入 HTML。
- 同一项目内多次 `game.generate_draft` 不覆盖记忆文件,而是继续追加短期对话记录和长期创作目标记录,保留用户迭代历史。
- `game.generate_draft` 会把本轮协作写入 `.agent/logs/agent.log` 和 manifest `commandRuns`,并将已完成的组内角色任务标记为完成,`preview-readiness` 在生成后等待本地 `game.static_smoke`,预览试玩任务等待用户确认。
- `game.generate_draft` 会把组内角色 brief、专业组汇总 brief、专业组 / 角色 / 产物交接摘要写入 `.agent/passes/pass-N/groups/``game/game_design.md``exports/README.md``.agent/logs/agent.log`,用于追踪 6 组 agent 的最小协作结果。
- 文件选择会生成待确认的 `asset.upload` 内置命令;用户确认后才把文件写入 `assets/uploads/` 并登记到 manifest。
- `game.generate_draft` 会从 `.agent/manifest.json` 派生本地资产摘要,把已上传、已登记和画板回流资产的 id、类型、媒体类型、本地路径、来源和画板追踪 ID 注入 Planner / 组内角色 / Generator 共用的上下文;资产摘要排在长期记忆前,避免长期记忆过长时被 prompt 截断;不新增平行资产记忆文件。
- 聊天输入 `/help` 会在聊天里列出当前可用内置命令,普通用户不需要打开任何开发面板来发现命令;`/capabilities` 会直接列出聊天 / 上传 / 内置命令、任务拆分、编排、Planner / Generator / Evaluator loop、工具预算、多智能体协作、组内角色协作、短期 / 长期记忆、本地产物、本地 HTTP 预览、画板同步、权限 gate 和 trace 日志等标准 Agent 能力;`/audit` 会只读聚合 manifest、文件列表和最近 run trace,逐项显示当前本地项目是否已经具备这些能力证据,并区分“6 组任务已配置”“最近 run trace 已实际覆盖 6 组协作”和“最近 loop 是否真正通过 Evaluator”。
- 聊天区待确认命令必须先记录 `permission.pending`,用户点击后再记录 `permission.confirm``permission.cancel`;已授权本地项目内的权限事件同步追加到 `.agent/logs/command.log`,待确认卡片显示本地写入目标路径。
- 聊天输入 `/status` 会读取 `.agent/manifest.json` 并在聊天里汇总项目目录、任务状态、资产数量、预览状态和最近命令,不向普通用户暴露任务或文件面板。
- 聊天输入 `/files` 会通过 `file.list` 只读列出本地项目内的文件摘要;普通用户仍不暴露文件读写面板。
- 聊天输入 `/assets` 会读取 `.agent/manifest.json` 并在聊天里列出本地项目资产路径、类型和来源;普通用户仍不暴露资产面板。
- 聊天输入 `/read 本地相对路径` 会通过 `file.read` 只读返回项目内文本文件内容并在聊天中截断长文本;普通用户仍不暴露文件写入或删除能力。
- 聊天输入 `/tasks` 会读取 `.agent/manifest.json` 并在聊天里列出专业组、角色、任务状态、产物交接和下一步可执行任务;普通用户仍不暴露任务面板。
- 聊天输入 `/trace` 会通过 `agent.trace_read` 只读读取 `.agent/run.latest.json`,在聊天里汇总最近一次 loop 的轮次、stopReason、nextStep、active/carry-over 任务、repairRoutes、agent 建议命令和最近 stepactive、carry-over、repairRoutes 和 dependencyWaves 必须把 taskId 映射成专业组 / 角色 / 任务名,避免普通用户只能看到内部 id;普通用户仍不暴露开发 trace 面板。
- 聊天输入 `/smoke` 会生成待确认的 `command.run_limited` 内置命令,当前只映射到白名单 `game.static_smoke`,不开放任意命令解析。
- 聊天输入 `/run` 会生成待确认的 `game.run_local` 内置命令,确认后复用白名单 `game.static_smoke` 运行当前 `game/index.html`,通过后启动只读本地 HTTP 预览并交给外部浏览器;该命令不开放任意 shell。
- 聊天输入 `/preview` 会生成待确认的 `preview.start` 内置命令,确认后启动只读本地 HTTP 预览并交给外部浏览器;`/open-preview` 在本地项目已初始化后会生成待确认的 `preview.open`,并且只打开当前已授权项目对应的 `127.0.0.1` 本地预览;`/preview-status` 只查询当前已授权项目对应的本地 HTTP 预览并写入 `preview.status` 命令日志;`/preview-stop` 只停止当前项目预览,不打开、展示或停止其它项目遗留的全局预览,不向普通用户暴露预览面板。
- 聊天输入 `/memory [short]` 读取长期或短期记忆;`/remember [short|long] 内容` 生成待确认的 `memory.write` 并追加短期或长期记忆,未写 scope 时默认追加长期记忆;`/memory-set [short|long] 内容` 生成待确认的 `memory.write` 并覆盖保存对应记忆;`/forget-memory [short]` 生成待确认的 `memory.delete`
- 聊天输入 `/canvas 画板项目ID` 会生成待确认的 `canvas.project_open`,只打开本机 Genarrative 编辑器里的指定画板项目,不开放任意 URL。
- 聊天输入 `/sync-canvas-project 画板项目ID` 会生成待确认的 `canvas.project_sync`,通过 External Editor API 把该画板项目资源下载到 `assets/canvas-sync/` 并登记为画板来源资产。
- 聊天输入 `/import-canvas-asset 本地路径 画板项目ID 资源ID|object:资产对象ID [kind] [mediaType]` 会生成待确认的 `canvas.asset_import`,只把项目目录内已有文件登记为画板来源资产;画板只有 `assetObjectId` 时使用 `object:` 前缀,不伪造 resourceId。
- 聊天输入 `/import-canvas-export /绝对/画板素材.zip 画板项目ID` 会生成待确认的 `canvas.export_import`,把画板导出的素材 ZIP 解包到 `assets/canvas-imports/` 并登记为画板来源资产。
- 开发窗口里的 `confirm` 权限命令使用原生确认门,取消时只写命令日志,不执行本地写入、删除、预览或画板打开操作。
- 原生文件上传会写入 `assets/uploads/`,并追加登记到 `.agent/manifest.json`
- 开发模式可把项目内已有文件登记为资产;`asset.register` 支持 `uploaded``generated``canvas` 三种来源,并可记录画板 project/resource/object 元数据。
- 开发模式可执行 `canvas.project_open`,打开本机 Genarrative 编辑器里的指定画板项目。
- 开发模式可执行 `canvas.project_sync`,同步平台画板项目中的已有资源到本地项目资产目录;该命令要求 External API Key 具备 `editor:project``editor:asset` 相关 scope。
- 开发模式可执行 `canvas.asset_import`,将项目内已有文件按画板来源导入 manifest;也可执行 `canvas.export_import`,把现有画板素材导出 ZIP 回流为本地项目资产。后续若要从独立 App 发起新的画板生成,再复用 External Editor API 的生成接口,不改本地产物与 manifest 落点。
- 普通模式只渲染聊天区;上传文件入口在聊天输入区内,Tauri 主窗口保持聊天窗口尺寸,不承载工具台布局,也不承载游戏预览画面。
- 聊天生成草案后会尝试启动只读 `127.0.0.1:<port>` 静态 HTTP server,调用系统外部浏览器打开预览,并把预览地址回到聊天消息;外部浏览器打开失败时保留本地 URL 供用户手动复制。
- 开发模式仅在 Vite dev 环境响应 `?dev``#dev`Tauri dev 会自动额外打开 `developer` 窗口显示专业组、本地项目、预览 iframe 和内置命令日志;正式构建忽略 dev 参数,release 配置只登记 `main` 聊天窗口。
- `check:native-shells` 会运行 `ai-game-creator-shell:check``ai-game-creator-shell:build -- --no-bundle`,并静态检查 release 只登记 `main` 聊天窗口、开发窗口只在 debug 下打开,开发面板必须挂在 `devMode` 分支内,正式用户 App 不能嵌入游戏预览 iframerelease CSP 也不能允许 `frame-src http://127.0.0.1:*`,用户侧预览命令必须调用 `open_local_game_preview` 交给系统外部浏览器。
- 共享契约提供 `GAME_CREATION_AGENT_CAPABILITIES` 和内置命令权限枚举;开发模式会展示能力列表。
- 共享契约提供 manifest task schema 和 ready-task 选择器,用于记录任务拆分、专业组、角色模板、依赖、产物、验收条件和当前可执行任务。
- 开发模式可读取、保存、删除短期记忆和长期记忆文件;普通用户通过聊天命令完成同类能力。
- 共享契约提供 `GAME_CREATION_APP_LIMITED_RUN_COMMANDS`;当前真实命令为 `game.static_smoke`,用于检查 `game/index.html` 的可玩原型门槛并写入 `.agent/logs/command.log`
- `.agent/manifest.json` 会记录当前 `preview` 状态和 `commandRuns` 受限命令运行结果,作为本地产物索引的最小真相源。
- 开发模式可通过本地项目文件面板执行 `file.list/read/write/delete`,普通用户界面不暴露文件面板。
+8
View File
@@ -120,6 +120,14 @@
"desktop-shell:stage-release-binary": "npm --prefix apps/desktop-shell run stage-release-binary",
"desktop-shell:typecheck": "npm --prefix apps/desktop-shell run typecheck",
"desktop-shell:test": "cargo test --manifest-path apps/desktop-shell/src-tauri/Cargo.toml",
"ai-game-creator-shell:dev": "npm --prefix apps/ai-game-creator-shell run dev",
"ai-game-creator-shell:dev-server": "npm --prefix apps/ai-game-creator-shell run dev-server",
"ai-game-creator-shell:build": "npm --prefix apps/ai-game-creator-shell run build --",
"ai-game-creator-shell:llm-status": "npm --prefix apps/ai-game-creator-shell run llm-status --",
"ai-game-creator-shell:agent-run": "npm --prefix apps/ai-game-creator-shell run agent-run --",
"ai-game-creator-shell:agent-run:smoke": "npm --prefix apps/ai-game-creator-shell run agent-run:smoke",
"ai-game-creator-shell:typecheck": "npm --prefix apps/ai-game-creator-shell run typecheck",
"ai-game-creator-shell:check": "npm run ai-game-creator-shell:typecheck && npm run test -- apps/ai-game-creator-shell/tests && cargo test -p platform-agent --manifest-path server-rs/Cargo.toml game_creation && cargo test -p shared-contracts --manifest-path server-rs/Cargo.toml game_creation_app && cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml && npm run ai-game-creator-shell:agent-run:smoke",
"check:native-shells": "node scripts/check-native-shells.mjs"
},
"dependencies": {

Some files were not shown because too many files have changed in this diff Show More