From 72a6d635dae1c17c14d0c60bca265f6a75e8b1e5 Mon Sep 17 00:00:00 2001 From: AIGameCreator App Date: Thu, 25 Jun 2026 16:13:18 +0800 Subject: [PATCH] =?UTF-8?q?AI=20=E6=B8=B8=E6=88=8F=E5=88=9B=E4=BD=9C?= =?UTF-8?q?=E6=99=BA=E8=83=BD=E4=BD=93=20App=20v1=20=E6=9C=80=E5=B0=8F?= =?UTF-8?q?=E8=90=BD=E5=9C=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增独立 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 --- .gitignore | 4 + AGENTS.md | 1 + apps/ai-game-creator-shell/index.html | 13 + apps/ai-game-creator-shell/package.json | 27 + .../scripts/check-config.mjs | 351 + .../scripts/run-cli-with-env.mjs | 48 + .../smoke-agent-run-local-provider.mjs | 816 ++ .../scripts/start-dev-server.mjs | 109 + .../src-tauri/Cargo.lock | 5693 +++++++++++ .../src-tauri/Cargo.toml | 20 + apps/ai-game-creator-shell/src-tauri/build.rs | 3 + .../src-tauri/src/main.rs | 8510 +++++++++++++++++ .../src-tauri/tauri.conf.json | 41 + apps/ai-game-creator-shell/src/App.tsx | 3739 ++++++++ apps/ai-game-creator-shell/src/main.tsx | 11 + apps/ai-game-creator-shell/src/styles.css | 463 + apps/ai-game-creator-shell/src/vite-env.d.ts | 12 + .../tests/agentTraceSummary.test.ts | 152 + .../tests/appSurface.test.ts | 2196 +++++ .../tests/rememberCommand.test.ts | 132 + apps/ai-game-creator-shell/tsconfig.json | 19 + apps/ai-game-creator-shell/vite.config.ts | 21 + .../shared-memory/decision-log.md | 87 + ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 160 + package.json | 8 + .../src/contracts/gameCreationApp.test.ts | 346 + .../shared/src/contracts/gameCreationApp.ts | 471 + packages/shared/src/contracts/index.ts | 1 + scripts/check-native-shells.mjs | 105 + server-rs/Cargo.lock | 21 +- .../platform-agent/src/game_creation.rs | 1021 ++ server-rs/crates/platform-agent/src/lib.rs | 8 + server-rs/crates/platform-llm/Cargo.toml | 2 +- server-rs/crates/platform-llm/src/lib.rs | 16 +- .../shared-contracts/src/game_creation_app.rs | 973 ++ server-rs/crates/shared-contracts/src/lib.rs | 1 + vitest.config.ts | 1 + 37 files changed, 25597 insertions(+), 5 deletions(-) create mode 100644 apps/ai-game-creator-shell/index.html create mode 100644 apps/ai-game-creator-shell/package.json create mode 100644 apps/ai-game-creator-shell/scripts/check-config.mjs create mode 100644 apps/ai-game-creator-shell/scripts/run-cli-with-env.mjs create mode 100644 apps/ai-game-creator-shell/scripts/smoke-agent-run-local-provider.mjs create mode 100644 apps/ai-game-creator-shell/scripts/start-dev-server.mjs create mode 100644 apps/ai-game-creator-shell/src-tauri/Cargo.lock create mode 100644 apps/ai-game-creator-shell/src-tauri/Cargo.toml create mode 100644 apps/ai-game-creator-shell/src-tauri/build.rs create mode 100644 apps/ai-game-creator-shell/src-tauri/src/main.rs create mode 100644 apps/ai-game-creator-shell/src-tauri/tauri.conf.json create mode 100644 apps/ai-game-creator-shell/src/App.tsx create mode 100644 apps/ai-game-creator-shell/src/main.tsx create mode 100644 apps/ai-game-creator-shell/src/styles.css create mode 100644 apps/ai-game-creator-shell/src/vite-env.d.ts create mode 100644 apps/ai-game-creator-shell/tests/agentTraceSummary.test.ts create mode 100644 apps/ai-game-creator-shell/tests/appSurface.test.ts create mode 100644 apps/ai-game-creator-shell/tests/rememberCommand.test.ts create mode 100644 apps/ai-game-creator-shell/tsconfig.json create mode 100644 apps/ai-game-creator-shell/vite.config.ts create mode 100644 docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md create mode 100644 packages/shared/src/contracts/gameCreationApp.test.ts create mode 100644 packages/shared/src/contracts/gameCreationApp.ts create mode 100644 server-rs/crates/platform-agent/src/game_creation.rs create mode 100644 server-rs/crates/shared-contracts/src/game_creation_app.rs diff --git a/.gitignore b/.gitignore index ee516d36a..b28197188 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/AGENTS.md b/AGENTS.md index 65504fb28..21884a813 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 执行。 diff --git a/apps/ai-game-creator-shell/index.html b/apps/ai-game-creator-shell/index.html new file mode 100644 index 000000000..fa69c5471 --- /dev/null +++ b/apps/ai-game-creator-shell/index.html @@ -0,0 +1,13 @@ + + + + + + + AI 游戏创作 + + +
+ + + diff --git a/apps/ai-game-creator-shell/package.json b/apps/ai-game-creator-shell/package.json new file mode 100644 index 000000000..352721db4 --- /dev/null +++ b/apps/ai-game-creator-shell/package.json @@ -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" + } +} diff --git a/apps/ai-game-creator-shell/scripts/check-config.mjs b/apps/ai-game-creator-shell/scripts/check-config.mjs new file mode 100644 index 000000000..f64c5c3cd --- /dev/null +++ b/apps/ai-game-creator-shell/scripts/check-config.mjs @@ -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('AI 游戏创作')", + '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}`, + ); + } +} diff --git a/apps/ai-game-creator-shell/scripts/run-cli-with-env.mjs b/apps/ai-game-creator-shell/scripts/run-cli-with-env.mjs new file mode 100644 index 000000000..3d17d1f8f --- /dev/null +++ b/apps/ai-game-creator-shell/scripts/run-cli-with-env.mjs @@ -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); +}); diff --git a/apps/ai-game-creator-shell/scripts/smoke-agent-run-local-provider.mjs b/apps/ai-game-creator-shell/scripts/smoke-agent-run-local-provider.mjs new file mode 100644 index 000000000..a44f71820 --- /dev/null +++ b/apps/ai-game-creator-shell/scripts/smoke-agent-run-local-provider.mjs @@ -0,0 +1,816 @@ +import { spawn } from 'node:child_process'; +import { accessSync } from 'node:fs'; +import fs from 'node:fs/promises'; +import http from 'node:http'; +import os from 'node:os'; +import path from 'node:path'; + +const appRoot = path.resolve(new URL('..', import.meta.url).pathname); +const projectRoot = path.join( + os.tmpdir(), + `genarrative-ai-game-creator-smoke-${Date.now()}`, +); +const prompt = '像素风反弹弹幕厨房'; +const smokeAssetPath = 'assets/uploads/smoke-chef.png'; +const smokeAssetMarker = 'SMOKE_LOCAL_ASSET:chef'; +const smokeAssetBytes = Buffer.concat([ + Buffer.from( + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=', + 'base64', + ), + Buffer.from(smokeAssetMarker), +]); +const smokeAudioAssetPath = 'assets/uploads/smoke-bounce.mp3'; +const smokeAudioAssetBytes = 'SMOKE_LOCAL_AUDIO:bounce'; + +const groupRoles = [ + ['策划组', 'Director', 'design', 'director'], + ['策划组', 'Gameplay', 'design', 'gameplay'], + ['数值组', 'Director', 'balance', 'director'], + ['数值组', 'Difficulty', 'balance', 'difficulty'], + ['美术组', 'Director', 'art', 'director'], + ['美术组', 'Asset', 'art', 'asset'], + ['美术组', 'Polish', 'art', 'polish'], + ['音乐组', 'Director', 'audio', 'director'], + ['音乐组', 'SFX', 'audio', 'sfx'], + ['程序组', 'Director', 'code', 'director'], + ['程序组', 'Code', 'code', 'code'], + ['程序组', 'Preview', 'code', 'preview'], + ['程序组', 'Playtest', 'code', 'playtest'], + ['运营组', 'Director', 'publishing', 'director'], + ['运营组', 'Publish', 'publishing', 'publish'], +]; + +function roleBrief(pass, [label, role, group, id]) { + return [ + `本角色判断:pass ${pass} ${label} ${role} 负责 ${group}.${id}`, + `交付物:${group}/${id}`, + '下游约束:必须围绕反弹弹幕厨房,不能输出固定模板。', + '验收风险:缺输入、缺主循环或缺胜负状态都要返工。', + ].join('\n'); +} + +function handoffs() { + return [ + [ + 'design', + 'Gameplay', + '定义反弹厨房核心循环', + ['game/game_design.md'], + '交给数值、美术、音乐、程序组', + ], + [ + 'balance', + 'Difficulty', + '给出锅盖反弹速度、生命和得分口径', + ['game/balance.json'], + '交给程序组读取', + ], + [ + 'art', + 'Asset', + '规划像素厨师、夜间厨房和锅盖弹幕资产', + ['assets/manifest.art.json'], + '进入画板链路', + ], + [ + 'audio', + 'SFX', + '规划锅盖反弹音效和厨房节奏 BGM', + ['assets/manifest.audio.json'], + '进入音频链路', + ], + [ + 'code', + 'Code', + '生成 canvas 可玩原型', + ['game/index.html'], + '交给 Playtest', + ], + [ + 'publishing', + 'Publish', + '整理标题、标签和发布说明', + ['exports/README.md'], + '等待预览验收', + ], + ].map(([group, role, summary, outputs, next]) => ({ + group, + role, + summary, + outputs, + next, + })); +} + +function draft({ withInput }) { + const inputCode = withInput + ? "window.addEventListener('keydown', (event) => { if (event.key.toLowerCase() === 'r') resetGame(); player.x += event.key === 'ArrowRight' ? 8 : event.key === 'ArrowLeft' ? -8 : 0; });" + : ''; + return { + title: '反弹弹幕厨房', + designMarkdown: + '玩家控制像素厨师移动锅盖反弹月光弹幕,点亮三口锅后获胜,被弹幕击中耗尽生命则失败。', + balance: { + playerSpeed: 220, + playerLives: 3, + scorePerPot: 100, + difficultyRamp: '每 20 秒增加一枚弹幕', + }, + artManifest: { + source: 'local-provider', + items: [ + { kind: 'character', title: '像素厨师', status: 'needs-canvas' }, + { kind: 'scene', title: '夜间厨房', status: 'needs-canvas' }, + ], + }, + audioManifest: { + source: 'local-provider', + items: [ + { + kind: 'background-music', + title: '厨房节奏 BGM', + status: 'needs-canvas', + }, + { kind: 'sound-effect', title: '锅盖反弹音', status: 'needs-canvas' }, + ], + }, + publishReadme: + '## 标签\n\n弹幕 / 反弹 / 厨房\n\n## 下一步\n\n试玩锅盖反弹手感。', + handoffs: handoffs(), + handoffSummary: + '策划组 / Gameplay:反弹厨房核心循环\n数值组 / Difficulty:生命、速度和得分\n美术组 / Asset:像素厨房资产\n音乐组 / SFX:反弹音效\n程序组 / Code:canvas 原型\n运营组 / Publish:发布包装', + gameHtml: ` + + 反弹弹幕厨房 + + + + +`, + }; +} + +const responses = [ + '## 核心循环\n\n反弹弹幕点亮三口锅。\n\n## Evaluator 验收\n\n必须有输入监听、主循环、胜负状态和重开路径。', + ...groupRoles.map((role) => roleBrief(1, role)), + JSON.stringify(draft({ withInput: false })), + ...groupRoles + .filter(([, , group]) => group === 'code' || group === 'publishing') + .map((role) => roleBrief(2, role)), + JSON.stringify(draft({ withInput: true })), +]; + +let responseIndex = 0; +const requestBodies = []; +const server = http.createServer((request, response) => { + let requestBody = ''; + request.setEncoding('utf8'); + request.on('data', (chunk) => { + requestBody += chunk; + }); + request.on('end', () => { + requestBodies.push(requestBody); + let requestJson = null; + try { + requestJson = JSON.parse(requestBody); + } catch {} + const content = responses[responseIndex++]; + if (!content) { + response.writeHead(500, { 'content-type': 'application/json' }); + response.end(JSON.stringify({ error: 'local provider exhausted' })); + return; + } + if (requestJson?.stream === true) { + writeStreamingChatCompletion(response, content); + return; + } + const body = JSON.stringify({ + id: `chatcmpl-local-${responseIndex}`, + model: 'local-game-creator-smoke', + choices: [ + { + message: { content }, + finish_reason: 'stop', + }, + ], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + }); + response.writeHead(200, { + 'content-type': 'application/json', + 'content-length': Buffer.byteLength(body), + }); + response.end(body); + }); +}); + +function writeStreamingChatCompletion(response, content) { + response.writeHead(200, { + 'content-type': 'text/event-stream; charset=utf-8', + 'cache-control': 'no-cache', + }); + const splitAt = Math.max(1, Math.floor(content.length / 2)); + for (const chunk of [content.slice(0, splitAt), content.slice(splitAt)]) { + if (!chunk) { + continue; + } + response.write( + `data: ${JSON.stringify({ + id: `chatcmpl-local-${responseIndex}`, + model: 'local-game-creator-smoke', + choices: [{ delta: { content: chunk }, finish_reason: null }], + })}\n\n`, + ); + } + response.write( + `data: ${JSON.stringify({ + id: `chatcmpl-local-${responseIndex}`, + model: 'local-game-creator-smoke', + choices: [{ delta: {}, finish_reason: 'stop' }], + })}\n\n`, + ); + response.end('data: [DONE]\n\n'); +} + +await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); +const address = server.address(); +const baseUrl = `http://127.0.0.1:${address.port}`; + +try { + await seedLocalAsset(); + const { + output, + previewHtml, + previewUrl, + previewAsset, + previewAssetHead, + previewAudio, + previewAudioHead, + previewDom, + } = await runAgent(baseUrl); + const tracePath = path.join(projectRoot, '.agent/run.latest.json'); + const trace = JSON.parse(await fs.readFile(tracePath, 'utf8')); + const pass2TaskGraph = JSON.parse( + await fs.readFile( + path.join(projectRoot, '.agent/passes/pass-2/task-graph.json'), + 'utf8', + ), + ); + const agentDbRecords = ( + await fs.readFile(path.join(projectRoot, '.agent/agent.db'), 'utf8') + ) + .trim() + .split('\n') + .map((line) => JSON.parse(line)); + const previewLog = await fs.readFile( + path.join(projectRoot, '.agent/logs/preview.log'), + 'utf8', + ); + const gameHtml = await fs.readFile( + path.join(projectRoot, 'game/index.html'), + 'utf8', + ); + const agenda2 = await fs.readFile( + path.join(projectRoot, '.agent/passes/pass-2/agenda.md'), + 'utf8', + ); + + assert(output.includes('agent.run.completed'), 'CLI did not complete'); + assert( + previewUrl?.startsWith('http://127.0.0.1:'), + 'CLI did not print local preview URL', + ); + assert( + previewHtml.includes('LOCAL_E2E_MECHANIC:reflect-kitchen'), + 'local preview did not serve generated game', + ); + assert( + previewDom?.includes('data-smoke-frame=') && + previewDom.includes('data-smoke-audio="ready"'), + 'headless browser did not run generated preview frame', + ); + const canvasPixels = extractDomNumber(previewDom, 'smoke-canvas-pixels'); + const canvasColors = extractDomNumber(previewDom, 'smoke-canvas-colors'); + assert( + canvasPixels > 40 && canvasColors > 2, + `headless browser preview canvas looks blank: pixels=${canvasPixels}, colors=${canvasColors}`, + ); + assert( + previewAsset.includes(smokeAssetMarker), + 'local preview did not serve referenced project asset', + ); + assert( + previewAudio.includes(smokeAudioAssetBytes), + 'local preview did not serve referenced audio asset', + ); + assert( + previewAssetHead?.statusCode === 200 && + previewAssetHead.contentLength === String(smokeAssetBytes.length) && + previewAssetHead.contentType === 'image/png' && + previewAssetHead.body === '', + 'local preview HEAD did not preserve referenced asset content length', + ); + assert( + previewAudioHead?.statusCode === 200 && + previewAudioHead.contentLength === + String(Buffer.byteLength(smokeAudioAssetBytes)) && + previewAudioHead.contentType === 'audio/mpeg' && + previewAudioHead.body === '', + 'local preview HEAD did not preserve referenced audio asset metadata', + ); + assert( + responseIndex === responses.length, + `provider calls ${responseIndex}/${responses.length}`, + ); + assert( + requestBodies.every((body) => body.includes('"stream":true')), + 'provider requests did not use streaming LLM mode', + ); + assert( + requestBodies.some( + (body) => + body.includes('# 本地项目资产') && + body.includes(smokeAssetPath) && + body.includes(smokeAudioAssetPath), + ), + 'provider requests missing local asset prompt context', + ); + assert(trace.status === 'preview-stopped', `trace status ${trace.status}`); + assert(trace.passes === 2, `trace passes ${trace.passes}`); + const expectedToolCallCount = trace.steps.reduce( + (total, step) => total + (step.toolCalls?.length ?? 0), + 0, + ); + assert( + trace.toolCallCount === expectedToolCallCount, + `trace toolCallCount ${trace.toolCallCount}/${expectedToolCallCount}`, + ); + assert( + trace.toolCallCount <= trace.maxToolCalls, + `trace tool budget ${trace.toolCallCount}/${trace.maxToolCalls}`, + ); + const tracedGroups = new Set( + trace.steps.map((step) => step.group).filter(Boolean), + ); + for (const group of [ + 'design', + 'balance', + 'art', + 'audio', + 'code', + 'publishing', + ]) { + assert( + tracedGroups.has(group), + `trace missing professional group ${group}`, + ); + } + assert( + trace.taskGraph?.repairRoutes?.some( + (route) => + route.reason === 'code-runtime+dependency-impact' && + route.taskIds.includes('code-prototype') && + route.taskIds.includes('preview-readiness') && + route.taskIds.includes('publish-package'), + ), + 'trace missing code-runtime dependency-impact repair route', + ); + assert( + pass2TaskGraph.repairRoutes?.some( + (route) => + route.reason === 'code-runtime+dependency-impact' && + route.taskIds.includes('code-prototype') && + route.taskIds.includes('preview-readiness') && + route.taskIds.includes('publish-package'), + ), + 'pass task graph missing downstream-impact code repair route', + ); + assert( + trace.artifacts?.some((artifact) => artifact.path === '.agent/agent.db'), + 'trace artifacts missing .agent/agent.db', + ); + assert( + trace.artifacts?.some( + (artifact) => artifact.path === '.agent/passes/pass-2/task-graph.json', + ), + 'trace artifacts missing pass-2 task graph', + ); + assert( + trace.artifacts?.some( + (artifact) => artifact.path === '.agent/logs/preview.log', + ), + 'trace artifacts missing preview log', + ); + assert( + trace.artifacts?.some( + (artifact) => artifact.path === '.agent/logs/agent.log', + ) && + trace.artifacts?.some( + (artifact) => artifact.path === '.agent/logs/command.log', + ), + 'trace artifacts missing agent or command logs', + ); + assert( + previewLog.includes('preview.running') && + previewLog.includes('preview.stopped'), + 'preview log missing start or stop entries', + ); + assert( + agentDbRecords.some((record) => record.recordType === 'project.init') && + agentDbRecords.some( + (record) => record.recordType === 'game.generate_draft', + ), + 'agent.db missing init or generation records', + ); + assert( + agenda2.includes('activeTasks: code-director'), + 'repair agenda did not target code tasks', + ); + assert( + agenda2.includes('publish-package'), + 'repair agenda did not target downstream publishing tasks', + ); + assert( + agenda2.includes('carriedTasks: design-director'), + 'repair agenda did not carry design tasks', + ); + assert( + trace.steps.some( + (step) => + step.pass === 2 && + step.agent === '策划组 / Director' && + step.status === 'carried-over', + ), + 'trace missing carried-over design role', + ); + assert( + trace.steps.some( + (step) => + step.pass === 2 && + step.agent === '程序组 / Director' && + step.toolCalls?.[0]?.toolId === 'llm.chat.group.code.director', + ), + 'trace missing code repair role', + ); + assert( + trace.steps.some( + (step) => + step.pass === 2 && + step.agent === '运营组 / Publish' && + step.toolCalls?.[0]?.toolId === 'llm.chat.group.publishing.publish', + ), + 'trace missing downstream publishing repair role', + ); + assert( + trace.steps.some((step) => + step.toolCalls?.some( + (toolCall) => + toolCall.toolId === 'agent.tool.suggest.canvas.project_sync' && + toolCall.status === 'suggested', + ), + ), + 'trace missing canvas project sync tool suggestion', + ); + assert( + trace.steps.some( + (step) => + step.agent === 'Planner' && + step.inputPaths?.includes('memory/session.md') && + step.inputPaths?.includes('memory/project.md') && + step.inputPaths?.includes('.agent/manifest.json'), + ), + 'trace missing planner memory or manifest inputs', + ); + assert( + trace.steps.some( + (step) => + step.agent === '策划组 / Director' && + step.pass === 1 && + step.inputPaths?.includes('.agent/manifest.json') && + step.inputPaths?.includes('.agent/passes/pass-1/agenda.md'), + ), + 'trace missing role brief manifest or agenda inputs', + ); + assert( + trace.steps.some( + (step) => + step.agent === 'Generator' && + step.pass === 2 && + 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', + ); + assert( + gameHtml.includes('LOCAL_E2E_MECHANIC:reflect-kitchen'), + 'game html marker missing', + ); + assert( + gameHtml.includes('addEventListener'), + 'game html input listener missing', + ); + assert( + gameHtml.includes(`/${smokeAssetPath}`), + 'game html did not reference seeded local asset', + ); + assert( + gameHtml.includes(`/${smokeAudioAssetPath}`), + 'game html did not reference seeded local audio asset', + ); + + console.log('ai-game-creator-shell.agent-run.smoke=passed'); + console.log(`projectPath=${projectRoot}`); + console.log(`tracePath=${tracePath}`); +} finally { + server.close(); +} + +function runAgent(baseUrl) { + return new Promise((resolve, reject) => { + let previewReadStarted = false; + let previewUrl = ''; + let previewHtml = ''; + let previewAsset = ''; + let previewAssetHead = null; + let previewAudio = ''; + let previewAudioHead = null; + let previewDom = ''; + const child = spawn( + 'cargo', + [ + 'run', + '--manifest-path', + 'src-tauri/Cargo.toml', + '--', + '--agent-run', + projectRoot, + prompt, + ], + { + cwd: appRoot, + env: { + ...process.env, + GENARRATIVE_GAME_CREATOR_LLM_API_KEY: 'local-provider-key', + GENARRATIVE_GAME_CREATOR_LLM_BASE_URL: baseUrl, + GENARRATIVE_GAME_CREATOR_LLM_MODEL: 'local-game-creator-smoke', + GENARRATIVE_GAME_CREATOR_LLM_STREAM: 'true', + }, + stdio: ['pipe', 'pipe', 'pipe'], + }, + ); + let stdout = ''; + let stderr = ''; + let previewReadError = null; + child.stdout.on('data', (chunk) => { + stdout += chunk.toString(); + if (previewReadStarted) { + return; + } + const line = stdout + .split('\n') + .find((entry) => entry.startsWith('previewUrl=')); + if (!line) { + return; + } + previewReadStarted = true; + previewUrl = line.slice('previewUrl='.length).trim(); + Promise.all([ + readHttpText(previewUrl), + readHttpText(new URL(smokeAssetPath, previewUrl).toString()), + readHttpHead(new URL(smokeAssetPath, previewUrl).toString()), + readHttpText(new URL(smokeAudioAssetPath, previewUrl).toString()), + readHttpHead(new URL(smokeAudioAssetPath, previewUrl).toString()), + readBrowserDom(previewUrl), + ]) + .then(([html, asset, assetHead, audio, audioHead, dom]) => { + previewHtml = html; + previewAsset = asset; + previewAssetHead = assetHead; + previewAudio = audio; + previewAudioHead = audioHead; + previewDom = dom; + }) + .catch((error) => { + previewReadError = error; + }) + .finally(() => { + child.stdin.write('\n'); + }); + }); + child.stderr.on('data', (chunk) => { + stderr += chunk.toString(); + }); + child.on('error', reject); + child.on('close', (code) => { + const output = `${stdout}${stderr}`; + if (previewReadError) { + reject(previewReadError); + return; + } + if (code === 0) { + resolve({ + output, + previewHtml, + previewUrl, + previewAsset, + previewAssetHead, + previewAudio, + previewAudioHead, + previewDom, + }); + } else { + reject(new Error(output || `agent run exited with ${code}`)); + } + }); + }); +} + +async function seedLocalAsset() { + await fs.mkdir(path.join(projectRoot, 'assets/uploads'), { recursive: true }); + await fs.mkdir(path.join(projectRoot, '.agent'), { recursive: true }); + await fs.writeFile(path.join(projectRoot, smokeAssetPath), smokeAssetBytes); + await fs.writeFile( + path.join(projectRoot, smokeAudioAssetPath), + smokeAudioAssetBytes, + ); + await fs.writeFile( + path.join(projectRoot, '.agent/manifest.json'), + JSON.stringify( + { + schemaVersion: 'game-creation-app.manifest.v1', + projectId: 'local-provider-smoke', + name: '本地 Provider Smoke', + assets: [ + { + id: 'smoke-chef', + kind: 'character', + mediaType: 'image/png', + localPath: smokeAssetPath, + source: { kind: 'uploaded' }, + }, + { + id: 'smoke-bounce', + kind: 'sound-effect', + mediaType: 'audio/mpeg', + localPath: smokeAudioAssetPath, + source: { kind: 'uploaded' }, + }, + ], + }, + null, + 2, + ), + ); +} + +function readHttpText(url) { + return new Promise((resolve, reject) => { + http + .get(url, (response) => { + let body = ''; + response.setEncoding('utf8'); + response.on('data', (chunk) => { + body += chunk; + }); + response.on('end', () => resolve(body)); + }) + .on('error', reject); + }); +} + +function readHttpHead(url) { + return new Promise((resolve, reject) => { + const request = http.request(url, { method: 'HEAD' }, (response) => { + let body = ''; + response.setEncoding('utf8'); + response.on('data', (chunk) => { + body += chunk; + }); + response.on('end', () => + resolve({ + statusCode: response.statusCode, + contentLength: response.headers['content-length'], + contentType: response.headers['content-type'], + body, + }), + ); + }); + request.on('error', reject); + request.end(); + }); +} + +function readBrowserDom(url) { + return new Promise((resolve, reject) => { + const chrome = resolveChromeBin(); + const child = spawn( + chrome, + [ + '--headless', + '--disable-gpu', + '--disable-dev-shm-usage', + '--no-sandbox', + '--virtual-time-budget=1000', + '--dump-dom', + url, + ], + { stdio: ['ignore', 'pipe', 'pipe'] }, + ); + let stdout = ''; + let stderr = ''; + child.stdout.on('data', (chunk) => { + stdout += chunk.toString(); + }); + child.stderr.on('data', (chunk) => { + stderr += chunk.toString(); + }); + child.on('error', reject); + child.on('close', (code) => { + if (code === 0) { + resolve(stdout); + } else { + reject(new Error(stderr || `headless Chrome exited with ${code}`)); + } + }); + }); +} + +function resolveChromeBin() { + if (process.env.GENARRATIVE_GAME_CREATOR_CHROME_BIN) { + return process.env.GENARRATIVE_GAME_CREATOR_CHROME_BIN; + } + for (const candidate of [ + '/usr/bin/google-chrome', + '/usr/bin/google-chrome-stable', + '/usr/bin/chromium', + '/usr/bin/chromium-browser', + ]) { + try { + accessSync(candidate); + return candidate; + } catch {} + } + return 'google-chrome'; +} + +function extractDomNumber(dom, kebabName) { + const match = dom?.match(new RegExp(`data-${kebabName}="(\\d+)"`)); + return match ? Number(match[1]) : 0; +} + +function assert(condition, message) { + if (!condition) { + throw new Error(message); + } +} diff --git a/apps/ai-game-creator-shell/scripts/start-dev-server.mjs b/apps/ai-game-creator-shell/scripts/start-dev-server.mjs new file mode 100644 index 000000000..9a4914dcb --- /dev/null +++ b/apps/ai-game-creator-shell/scripts/start-dev-server.mjs @@ -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('AI 游戏创作') && + 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); +}); diff --git a/apps/ai-game-creator-shell/src-tauri/Cargo.lock b/apps/ai-game-creator-shell/src-tauri/Cargo.lock new file mode 100644 index 000000000..7a35bd5b7 --- /dev/null +++ b/apps/ai-game-creator-shell/src-tauri/Cargo.lock @@ -0,0 +1,5693 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "adobe-cmap-parser" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8abfa9a4688de8fc9f42b3f013b6fffec18ed8a554f5f113577e0b9b3212a3" +dependencies = [ + "pom", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "brotli" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.13.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce8d3bd5823c7504d3f579f13e7b2f3da252fcb938c594d5680ee508bf846f" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.2.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.1", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.118", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dbus" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.118", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.0", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31a88c8d26de40ed18fe748c547845aa39de1db3afd958f8cb91579f3644bcb" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.2+spec-1.1.0", + "vswhom", + "winreg 0.55.0", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "euclid" +version = "0.20.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bb7ef65b3777a325d1eeefefab5b6d4959da54747e33bd6258e789640f307ad" +dependencies = [ + "num-traits", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "genarrative-ai-game-creator-shell" +version = "0.1.0" +dependencies = [ + "platform-agent", + "platform-llm", + "reqwest 0.12.28", + "serde", + "serde_json", + "shared-contracts", + "tauri", + "tauri-build", + "tauri-plugin-opener", + "tokio", + "zip", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.14.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.4.2", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.4.2", + "http-body 1.0.1", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http 1.4.2", + "http-body 1.0.1", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper 0.14.32", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.10.1", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http 1.4.2", + "http-body 1.0.1", + "hyper 1.10.1", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.4", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "js-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.13.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "langchainrust" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac5ddb421c9c0311ee154af906747ba1fb5c3db83d7d71dcb38f40b557eb28e4" +dependencies = [ + "async-trait", + "bincode", + "chrono", + "csv", + "futures-util", + "pdf-extract", + "regex", + "reqwest 0.11.27", + "schemars 0.8.22", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "uuid", +] + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lopdf" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5c8ecfc6c72051981c0459f75ccc585e7ff67c70829560cda8e647882a9abff" +dependencies = [ + "encoding_rs", + "flate2", + "indexmap 2.14.0", + "itoa", + "log", + "md-5", + "nom", + "rangemap", + "time", + "weezl", +] + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.0", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "open" +version = "5.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "openssl" +version = "0.10.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" +dependencies = [ + "bitflags 2.13.0", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "pdf-extract" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbb3a5387b94b9053c1e69d8abfd4dd6dae7afda65a5c5279bc1f42ab39df575" +dependencies = [ + "adobe-cmap-parser", + "encoding_rs", + "euclid", + "lopdf", + "postscript", + "type1-encoding-parser", + "unicode-normalization", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "platform-agent" +version = "0.1.0" +dependencies = [ + "async-trait", + "langchainrust", + "platform-llm", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "platform-llm" +version = "0.1.0" +dependencies = [ + "log", + "reqwest 0.12.28", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "plist" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "pom" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60f6ce597ecdcc9a098e7fddacb1065093a3d66446fa16c675e7e71d1b5c28e6" + +[[package]] +name = "postscript" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78451badbdaebaf17f053fd9152b3ffb33b516104eacb45e7864aaa9c712f306" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.12+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.39.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rangemap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper-tls 0.5.0", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 0.1.2", + "system-configuration", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.4.2", + "web-sys", + "winreg 0.50.0", +] + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http 1.4.2", + "http-body 1.0.1", + "http-body-util", + "hyper 1.10.1", + "hyper-tls 0.6.0", + "hyper-util", + "js-sys", + "log", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.2", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.4.2", + "web-sys", +] + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http 1.4.2", + "http-body 1.0.1", + "http-body-util", + "hyper 1.10.1", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper 1.0.2", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.5.0", + "web-sys", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.118", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.13.0", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shared-contracts" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +dependencies = [ + "bitflags 2.13.0", + "block2", + "core-foundation 0.10.1", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2616f96cb644bf2c5c456d9de4d5d5100e592d7424c74d8b55c5cb96e359e93" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http 1.4.2", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest 0.13.4", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.18", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.118", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-opener" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17e1bea14edce6b793a04e2417e3fd924b9bc4faae83cdee7d714156cceeed29" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation", + "open", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "url", + "windows", + "zbus", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http 1.4.2", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe41e015bf8fc4d6477ff4926a0ef769dc64ff34c7b0038b6f7cacae892acb5c" +dependencies = [ + "gtk", + "http 1.4.2", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http 1.4.2", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.2+spec-1.1.0", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "time" +version = "0.3.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.4", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.3", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 1.0.2", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.0", + "bytes", + "futures-util", + "http 1.4.2", + "http-body 1.0.1", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "type1-encoding-parser" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa10c302f5a53b7ad27fd42a3996e23d096ba39b5b8dd6d9e683a05b01bee749" +dependencies = [ + "pom", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.118", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "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-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[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_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http 1.4.2", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.3", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" +dependencies = [ + "serde", + "winnow 1.0.3", + "zvariant", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zip" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +dependencies = [ + "arbitrary", + "crc32fast", + "crossbeam-utils", + "displaydoc", + "flate2", + "indexmap 2.14.0", + "memchr", + "thiserror 2.0.18", + "zopfli", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zvariant" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 1.0.3", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.118", + "winnow 1.0.3", +] diff --git a/apps/ai-game-creator-shell/src-tauri/Cargo.toml b/apps/ai-game-creator-shell/src-tauri/Cargo.toml new file mode 100644 index 000000000..43af73677 --- /dev/null +++ b/apps/ai-game-creator-shell/src-tauri/Cargo.toml @@ -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"] } diff --git a/apps/ai-game-creator-shell/src-tauri/build.rs b/apps/ai-game-creator-shell/src-tauri/build.rs new file mode 100644 index 000000000..d860e1e6a --- /dev/null +++ b/apps/ai-game-creator-shell/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/apps/ai-game-creator-shell/src-tauri/src/main.rs b/apps/ai-game-creator-shell/src-tauri/src/main.rs new file mode 100644 index 000000000..2897da85f --- /dev/null +++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs @@ -0,0 +1,8510 @@ +#![cfg_attr(all(not(dev), target_os = "windows"), windows_subsystem = "windows")] + +use std::fs; +use std::fs::File; +use std::io::{BufRead, BufReader, Read, Write}; +use std::net::{TcpListener, TcpStream}; +use std::path::{Path, PathBuf}; +use std::sync::{mpsc, Mutex}; +use std::thread; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use platform_agent::{ + build_game_creation_seed_task_graph, plan_game_creation_agent_pass, + route_game_creation_repair_issues, +}; +use platform_llm::{ + LlmClient, LlmConfig, LlmMessage, LlmProvider, LlmTextRequest, DEFAULT_REQUEST_TIMEOUT_MS, + DEFAULT_RETRY_BACKOFF_MS, +}; +use reqwest::header; +use serde::{Deserialize, Serialize}; +use shared_contracts::game_creation_app::{ + new_game_creation_app_manifest, new_game_creation_app_seed_tasks, + GameCreationAgentArtifactTrace, GameCreationAgentCapabilityDescriptor, + GameCreationAgentPassPlanTrace, GameCreationAgentRepairRouteTrace, GameCreationAgentRunStep, + GameCreationAgentRunTaskGraphTrace, GameCreationAgentRunTrace, GameCreationAgentToolCallTrace, + GameCreationAppAgentGroup, GameCreationAppAssetManifestEntry, GameCreationAppAssetSource, + GameCreationAppAssetSourceKind, GameCreationAppCommandRunState, + GameCreationAppCommandRunStatus, GameCreationAppLimitedRunCommandDescriptor, + GameCreationAppManifest, GameCreationAppPreviewState, GameCreationAppPreviewStatus, + GameCreationAppTaskState, GameCreationAppTaskStatus, GAME_CREATION_AGENT_CAPABILITIES, + GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, GAME_CREATION_AGENT_TOOL_CALL_MAX, + GAME_CREATION_APP_COMMANDS, GAME_CREATION_APP_LIMITED_RUN_COMMANDS, +}; +use tauri_plugin_opener::OpenerExt; + +#[derive(Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +struct InitLocalProjectResult { + project_path: String, + manifest_path: String, + manifest: GameCreationAppManifest, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +struct LocalPreviewResult { + url: String, + port: u16, + root: String, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +struct LocalPreviewStatus { + status: String, + url: Option, + port: Option, + root: Option, +} + +#[derive(Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +struct GenerateLocalGameDraftResult { + project_path: String, + game_index_path: String, + design_path: String, + short_memory_path: String, + long_memory_path: String, + manifest: GameCreationAppManifest, +} + +#[derive(Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +struct GameCreatorLlmConfigStatus { + configured: bool, + api_key_present: bool, + base_url: Option, + model: Option, + error: Option, +} + +#[derive(Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +struct UploadLocalAssetResult { + id: String, + local_path: String, + absolute_path: String, + manifest_path: String, +} + +#[derive(Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +struct ImportCanvasExportResult { + import_root: String, + metadata_path: String, + imported_count: usize, + assets: Vec, +} + +#[derive(Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +struct SyncCanvasProjectAssetsResult { + canvas_project_id: String, + import_root: String, + imported_count: usize, + assets: Vec, +} + +#[derive(Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +struct OpenCanvasProjectResult { + url: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct CanvasExportMetadata { + project_title: String, + exported_at: String, + layers: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct CanvasExportLayerMetadata { + title: String, + file: Option, + visible: CanvasExportVisibleMetadata, + export_error: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct CanvasExportVisibleMetadata { + #[serde(rename = "type")] + layer_type: String, + model: String, + task: String, + object: String, +} + +#[derive(Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +struct LocalGameMemoryResult { + scope: String, + path: String, + content: String, + exists: bool, +} + +#[derive(Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +struct LimitedLocalCommandResult { + command_id: String, + status: String, + output: String, + log_path: String, + updated_at: u64, +} + +#[derive(Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +struct LocalProjectFileEntry { + path: String, + kind: String, + size: u64, +} + +#[derive(Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +struct ListLocalProjectFilesResult { + project_path: String, + files: Vec, +} + +#[derive(Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +struct LocalProjectFileResult { + path: String, + absolute_path: String, + content: String, +} + +#[derive(Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +struct LocalProjectFileMutationResult { + path: String, + absolute_path: String, + deleted: bool, +} + +#[derive(Default)] +struct PreviewRegistry { + current: Mutex>, +} + +struct PreviewServer { + preview: LocalPreviewResult, + stop: mpsc::Sender<()>, +} + +impl PreviewRegistry { + fn set_running( + &self, + preview: LocalPreviewResult, + stop: mpsc::Sender<()>, + ) -> (LocalPreviewResult, Option) { + let mut current = self.current.lock().expect("preview registry lock"); + let previous_preview = if let Some(previous) = current.take() { + let preview = previous.preview; + let _ = previous.stop.send(()); + Some(preview) + } else { + None + }; + *current = Some(PreviewServer { + preview: preview.clone(), + stop, + }); + (preview, previous_preview) + } + + fn status(&self) -> LocalPreviewStatus { + let current = self.current.lock().expect("preview registry lock"); + if let Some(server) = current.as_ref() { + local_preview_status_from_result(&server.preview) + } else { + stopped_preview_status() + } + } + + fn stop(&self) -> LocalPreviewStatus { + let mut current = self.current.lock().expect("preview registry lock"); + if let Some(server) = current.take() { + let _ = server.stop.send(()); + } + stopped_preview_status() + } + + fn stop_for_project(&self, root: Option<&Path>) -> (LocalPreviewStatus, bool) { + let mut current = self.current.lock().expect("preview registry lock"); + let Some(server) = current.as_ref() else { + return (stopped_preview_status(), false); + }; + if let Some(root) = root { + let status = local_preview_status_from_result(&server.preview); + if ensure_preview_belongs_to_project(&status, root).is_err() { + return (stopped_preview_status(), false); + } + } + let Some(server) = current.take() else { + return (stopped_preview_status(), false); + }; + let _ = server.stop.send(()); + (stopped_preview_status(), true) + } +} + +fn stopped_preview_status() -> LocalPreviewStatus { + LocalPreviewStatus { + status: "stopped".to_string(), + url: None, + port: None, + root: None, + } +} + +fn local_preview_status_from_result(preview: &LocalPreviewResult) -> LocalPreviewStatus { + LocalPreviewStatus { + status: "running".to_string(), + url: Some(preview.url.clone()), + port: Some(preview.port), + root: Some(preview.root.clone()), + } +} + +fn preview_open_url(status: &LocalPreviewStatus) -> Result { + if status.status == "running" { + if let Some(url) = status.url.as_deref() { + if url.starts_with("http://127.0.0.1:") { + return Ok(url.to_string()); + } + } + } + Err("preview is not running".to_string()) +} + +fn ensure_preview_belongs_to_project( + status: &LocalPreviewStatus, + root: &Path, +) -> Result<(), String> { + if root.as_os_str().is_empty() { + return Err("项目目录不能为空".to_string()); + } + if !root.is_absolute() { + return Err("项目目录必须是绝对路径".to_string()); + } + let preview_root = status + .root + .as_deref() + .ok_or_else(|| "preview is not running".to_string())?; + let expected_root = root + .canonicalize() + .map_err(|error| format!("读取项目目录失败:{}: {error}", root.display()))?; + let actual_root = Path::new(preview_root) + .canonicalize() + .map_err(|error| format!("读取预览项目目录失败:{preview_root}: {error}"))?; + if actual_root == expected_root { + Ok(()) + } else { + Err("当前预览不属于已授权本地项目".to_string()) + } +} + +fn filter_preview_status_for_project( + status: LocalPreviewStatus, + project_path: Option<&str>, +) -> LocalPreviewStatus { + let Some(project_path) = project_path.map(str::trim).filter(|path| !path.is_empty()) else { + return status; + }; + if ensure_preview_belongs_to_project(&status, Path::new(project_path)).is_err() { + stopped_preview_status() + } else { + status + } +} + +const DEFAULT_GAME_INDEX_HTML: &str = r#" + + + + + Genarrative Game Draft + + +
还没有生成游戏。回到聊天输入创意并确认生成后,这里会写入可试玩原型。
+ +"#; + +const DEFAULT_EDITOR_BASE_URL: &str = "http://127.0.0.1:3000"; +const GAME_CREATOR_LLM_MAX_OUTPUT_TOKENS: u32 = 7000; +const GAME_CREATOR_AGENT_LOOP_MAX_PASSES: u8 = 3; +const GAME_CREATOR_AGENT_TOOL_CALL_MAX: u16 = GAME_CREATION_AGENT_TOOL_CALL_MAX; +const GAME_CREATOR_AGENT_DB_SCHEMA_VERSION: &str = "game-creator-agent-db.v1"; +const MAX_CANVAS_EXPORT_FILES: usize = 500; +const MAX_CANVAS_EXPORT_BYTES: u64 = 512 * 1024 * 1024; +const GAME_CREATOR_AGENT_ARTIFACT_PATHS: [&str; 14] = [ + ".agent/agent.db", + ".agent/spec.md", + ".agent/findings.md", + ".agent/logs/agent.log", + ".agent/logs/command.log", + ".agent/logs/preview.log", + "memory/session.md", + "memory/project.md", + "game/game_design.md", + "game/balance.json", + "assets/manifest.art.json", + "assets/manifest.audio.json", + "exports/README.md", + "game/index.html", +]; + +#[derive(Clone, Copy, Debug)] +struct AgentRoleDefinition { + id: &'static str, + role: &'static str, + task_id: &'static str, + tool_id: &'static str, + brief_path_name: &'static str, +} + +#[derive(Clone, Copy, Debug)] +struct AgentGroupDefinition { + id: &'static str, + label: &'static str, + role: &'static str, + brief_path_name: &'static str, + roles: &'static [AgentRoleDefinition], +} + +static DESIGN_AGENT_ROLES: [AgentRoleDefinition; 2] = [ + AgentRoleDefinition { + id: "director", + role: "Director", + task_id: "design-director", + tool_id: "llm.chat.group.design.director", + brief_path_name: "director.md", + }, + AgentRoleDefinition { + id: "gameplay", + role: "Gameplay", + task_id: "design-foundation", + tool_id: "llm.chat.group.design.gameplay", + brief_path_name: "gameplay.md", + }, +]; + +static BALANCE_AGENT_ROLES: [AgentRoleDefinition; 2] = [ + AgentRoleDefinition { + id: "director", + role: "Director", + task_id: "balance-director", + tool_id: "llm.chat.group.balance.director", + brief_path_name: "director.md", + }, + AgentRoleDefinition { + id: "difficulty", + role: "Difficulty", + task_id: "balance-seed", + tool_id: "llm.chat.group.balance.difficulty", + brief_path_name: "difficulty.md", + }, +]; + +static ART_AGENT_ROLES: [AgentRoleDefinition; 3] = [ + AgentRoleDefinition { + id: "director", + role: "Director", + task_id: "art-director", + tool_id: "llm.chat.group.art.director", + brief_path_name: "director.md", + }, + AgentRoleDefinition { + id: "asset", + role: "Asset", + task_id: "art-asset-plan", + tool_id: "llm.chat.group.art.asset", + brief_path_name: "asset.md", + }, + AgentRoleDefinition { + id: "polish", + role: "Polish", + task_id: "art-polish", + tool_id: "llm.chat.group.art.polish", + brief_path_name: "polish.md", + }, +]; + +static AUDIO_AGENT_ROLES: [AgentRoleDefinition; 2] = [ + AgentRoleDefinition { + id: "director", + role: "Director", + task_id: "audio-director", + tool_id: "llm.chat.group.audio.director", + brief_path_name: "director.md", + }, + AgentRoleDefinition { + id: "sfx", + role: "SFX", + task_id: "audio-asset-plan", + tool_id: "llm.chat.group.audio.sfx", + brief_path_name: "sfx.md", + }, +]; + +static CODE_AGENT_ROLES: [AgentRoleDefinition; 4] = [ + AgentRoleDefinition { + id: "director", + role: "Director", + task_id: "code-director", + tool_id: "llm.chat.group.code.director", + brief_path_name: "director.md", + }, + AgentRoleDefinition { + id: "code", + role: "Code", + task_id: "code-prototype", + tool_id: "llm.chat.group.code.code", + brief_path_name: "code.md", + }, + AgentRoleDefinition { + id: "preview", + role: "Preview", + task_id: "preview-readiness", + tool_id: "llm.chat.group.code.preview", + brief_path_name: "preview.md", + }, + AgentRoleDefinition { + id: "playtest", + role: "Playtest", + task_id: "preview-playtest", + tool_id: "llm.chat.group.code.playtest", + brief_path_name: "playtest.md", + }, +]; + +static PUBLISHING_AGENT_ROLES: [AgentRoleDefinition; 2] = [ + AgentRoleDefinition { + id: "director", + role: "Director", + task_id: "publish-strategy", + tool_id: "llm.chat.group.publishing.director", + brief_path_name: "director.md", + }, + AgentRoleDefinition { + id: "publish", + role: "Publish", + task_id: "publish-package", + tool_id: "llm.chat.group.publishing.publish", + brief_path_name: "publish.md", + }, +]; + +const GAME_CREATOR_AGENT_GROUP_DEFINITIONS: [AgentGroupDefinition; 6] = [ + AgentGroupDefinition { + id: "design", + label: "策划组", + role: "Director + Gameplay", + brief_path_name: "design.md", + roles: &DESIGN_AGENT_ROLES, + }, + AgentGroupDefinition { + id: "balance", + label: "数值组", + role: "Director + Difficulty", + brief_path_name: "balance.md", + roles: &BALANCE_AGENT_ROLES, + }, + AgentGroupDefinition { + id: "art", + label: "美术组", + role: "Director + Asset + Polish", + brief_path_name: "art.md", + roles: &ART_AGENT_ROLES, + }, + AgentGroupDefinition { + id: "audio", + label: "音乐组", + role: "Director + SFX", + brief_path_name: "audio.md", + roles: &AUDIO_AGENT_ROLES, + }, + AgentGroupDefinition { + id: "code", + label: "程序组", + role: "Director + Code + Preview + Playtest", + brief_path_name: "code.md", + roles: &CODE_AGENT_ROLES, + }, + AgentGroupDefinition { + id: "publishing", + label: "运营组", + role: "Director + Publish", + brief_path_name: "publishing.md", + roles: &PUBLISHING_AGENT_ROLES, + }, +]; + +#[derive(Clone, Debug)] +struct AgentPassArtifactPaths { + draft_json: String, + design_markdown: String, + balance_json: String, + art_manifest_json: String, + audio_manifest_json: String, + publish_readme: String, + game_html: String, + handoff_markdown: String, +} + +#[derive(Clone, Debug)] +struct AgentRoleBrief { + group_definition: AgentGroupDefinition, + role_definition: AgentRoleDefinition, + markdown: String, + relative_path: String, + status: String, + tool_id: String, + summary: String, +} + +#[derive(Clone, Debug)] +struct AgentGroupBrief { + definition: AgentGroupDefinition, + markdown: String, + relative_path: String, + role_briefs: Vec, +} + +#[derive(Clone, Debug)] +struct AgentPassAgenda { + relative_path: String, + task_graph_relative_path: String, + active_task_ids: Vec, + carried_task_ids: Vec, + dependency_waves: Vec>, + repair_focus: Vec, + repair_routes: Vec, + summary: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +struct LlmGameDraft { + title: String, + design_markdown: String, + balance: serde_json::Value, + art_manifest: serde_json::Value, + audio_manifest: serde_json::Value, + publish_readme: String, + handoffs: Vec, + game_html: String, + handoff_summary: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +struct LlmAgentHandoff { + group: String, + role: String, + summary: String, + outputs: Vec, + next: String, +} + +#[derive(Clone, Debug)] +struct GameCreatorAgentLoopResult { + run_id: String, + draft: LlmGameDraft, + spec_markdown: String, + findings_markdown: String, + passes: u8, + steps: Vec, +} + +#[derive(Debug, Eq, PartialEq)] +enum CliCommand { + LlmStatus, + AgentRun { + project_path: PathBuf, + prompt: String, + wait_for_enter: bool, + }, +} + +#[tauri::command] +fn init_local_game_project( + project_path: String, + project_id: String, + name: String, +) -> Result { + init_local_game_project_at( + Path::new(project_path.trim()), + project_id.trim(), + name.trim(), + ) +} + +#[tauri::command] +fn start_local_game_preview( + project_path: String, + registry: tauri::State<'_, PreviewRegistry>, +) -> Result { + let root = Path::new(project_path.trim()); + let (preview, stop) = start_local_game_preview_for_project(root)?; + if let Err(error) = record_preview_state( + root, + GameCreationAppPreviewStatus::Running, + Some(preview.url.clone()), + Some(preview.port), + ) { + let _ = stop.send(()); + return Err(error); + } + if let Err(error) = append_preview_log(root, "running", Some(&preview.url)) { + let _ = stop.send(()); + let _ = record_preview_state(root, GameCreationAppPreviewStatus::Stopped, None, None); + return Err(error); + } + let (preview, previous_preview) = registry.set_running(preview, stop); + if let Some(previous_preview) = previous_preview.as_ref() { + record_replaced_preview_stop(previous_preview); + } + if let Err(error) = append_preview_start_trace_step(root, &preview) { + let _ = registry.stop(); + let _ = record_preview_state(root, GameCreationAppPreviewStatus::Stopped, None, None); + return Err(error); + } + Ok(preview) +} + +#[tauri::command] +fn stop_local_game_preview( + project_path: Option, + registry: tauri::State<'_, PreviewRegistry>, +) -> Result { + let project_path = project_path + .as_deref() + .map(str::trim) + .filter(|path| !path.is_empty()); + let root = project_path.map(Path::new); + let (status, _) = registry.stop_for_project(root); + if let Some(root) = root { + record_preview_state(root, GameCreationAppPreviewStatus::Stopped, None, None)?; + append_preview_log(root, "stopped", None)?; + append_preview_stop_trace_step(root)?; + } + Ok(status) +} + +#[tauri::command] +fn get_local_game_preview_status( + registry: tauri::State<'_, PreviewRegistry>, + project_path: Option, +) -> LocalPreviewStatus { + filter_preview_status_for_project(registry.status(), project_path.as_deref()) +} + +#[tauri::command] +fn open_local_game_preview( + app: tauri::AppHandle, + registry: tauri::State<'_, PreviewRegistry>, + project_path: Option, +) -> Result { + let status = registry.status(); + let url = preview_open_url(&status)?; + if let Some(project_path) = project_path { + let project_path = project_path.trim(); + if !project_path.is_empty() { + ensure_preview_belongs_to_project(&status, Path::new(project_path))?; + } + } + app.opener() + .open_url(&url, None::<&str>) + .map_err(|error| format!("preview open failed: {error}"))?; + Ok(status) +} + +#[tauri::command] +fn get_local_game_manifest(project_path: String) -> Result { + read_manifest_for_project(Path::new(project_path.trim())) +} + +#[tauri::command] +async fn generate_local_game_draft( + project_path: String, + prompt: String, +) -> Result { + generate_local_game_draft_at(Path::new(project_path.trim()), prompt.trim()).await +} + +#[tauri::command] +fn check_game_creator_llm_config() -> GameCreatorLlmConfigStatus { + check_game_creator_llm_config_from_env() +} + +#[tauri::command] +fn upload_local_asset( + project_path: String, + file_name: String, + media_type: String, + bytes: Vec, +) -> Result { + upload_local_asset_at( + Path::new(project_path.trim()), + file_name.trim(), + media_type.trim(), + &bytes, + ) +} + +#[tauri::command] +fn register_local_asset( + project_path: String, + local_path: String, + kind: String, + media_type: String, + source_kind: String, + canvas_project_id: Option, + resource_id: Option, + asset_object_id: Option, + task_id: Option, + prompt: Option, + model: Option, +) -> Result { + register_local_asset_at( + Path::new(project_path.trim()), + local_path.trim(), + kind.trim(), + media_type.trim(), + source_kind.trim(), + GameCreationAppAssetSource { + kind: parse_asset_source_kind(source_kind.trim())?, + canvas_project_id: trim_optional_string(canvas_project_id), + resource_id: trim_optional_string(resource_id), + asset_object_id: trim_optional_string(asset_object_id), + task_id: trim_optional_string(task_id), + prompt: trim_optional_string(prompt), + model: trim_optional_string(model), + }, + ) +} + +#[tauri::command] +fn import_canvas_asset( + project_path: String, + local_path: String, + kind: String, + media_type: String, + canvas_project_id: String, + resource_id: Option, + asset_object_id: Option, + task_id: Option, + prompt: Option, + model: Option, +) -> Result { + import_canvas_asset_at( + Path::new(project_path.trim()), + local_path.trim(), + kind.trim(), + media_type.trim(), + canvas_project_id.trim(), + trim_optional_string(resource_id), + trim_optional_string(asset_object_id), + trim_optional_string(task_id), + trim_optional_string(prompt), + trim_optional_string(model), + ) +} + +#[tauri::command] +fn import_canvas_export( + project_path: String, + export_path: String, + canvas_project_id: String, +) -> Result { + import_canvas_export_at( + Path::new(project_path.trim()), + Path::new(export_path.trim()), + canvas_project_id.trim(), + ) +} + +#[tauri::command] +async fn sync_canvas_project_assets( + project_path: String, + canvas_project_id: String, + api_base_url: Option, + api_key: Option, +) -> Result { + sync_canvas_project_assets_at( + Path::new(project_path.trim()), + canvas_project_id.trim(), + api_base_url, + api_key, + ) + .await +} + +#[tauri::command] +fn open_canvas_project( + app: tauri::AppHandle, + canvas_project_id: Option, + editor_base_url: Option, +) -> Result { + let url = build_canvas_project_url(editor_base_url.as_deref(), canvas_project_id.as_deref())?; + app.opener() + .open_url(&url, None::<&str>) + .map_err(|error| format!("打开画板失败:{error}"))?; + Ok(OpenCanvasProjectResult { url }) +} + +#[tauri::command] +fn get_game_creation_agent_capabilities() -> Vec { + GAME_CREATION_AGENT_CAPABILITIES.to_vec() +} + +#[tauri::command] +fn get_limited_local_commands() -> Vec { + GAME_CREATION_APP_LIMITED_RUN_COMMANDS.to_vec() +} + +#[tauri::command] +fn run_limited_local_command( + project_path: String, + command_id: String, +) -> Result { + let root = Path::new(project_path.trim()); + let command_id = command_id.trim(); + let result = run_limited_local_command_at(root, command_id)?; + if command_id == "game.static_smoke" { + append_static_smoke_manual_trace_step(root, &result)?; + } + Ok(result) +} + +#[tauri::command] +fn append_local_permission_log( + project_path: String, + event: String, + command_id: String, +) -> Result<(), String> { + append_local_permission_log_at( + Path::new(project_path.trim()), + event.trim(), + command_id.trim(), + ) +} + +#[tauri::command] +fn list_local_project_files(project_path: String) -> Result { + list_local_project_files_at(Path::new(project_path.trim())) +} + +#[tauri::command] +fn read_local_project_file( + project_path: String, + relative_path: String, +) -> Result { + read_local_project_file_at(Path::new(project_path.trim()), relative_path.trim()) +} + +#[tauri::command] +fn write_local_project_file( + project_path: String, + relative_path: String, + content: String, +) -> Result { + write_local_project_file_at( + Path::new(project_path.trim()), + relative_path.trim(), + &content, + ) +} + +#[tauri::command] +fn delete_local_project_file( + project_path: String, + relative_path: String, +) -> Result { + delete_local_project_file_at(Path::new(project_path.trim()), relative_path.trim()) +} + +#[tauri::command] +fn read_local_game_memory( + project_path: String, + scope: String, +) -> Result { + read_local_game_memory_at(Path::new(project_path.trim()), scope.trim()) +} + +#[tauri::command] +fn write_local_game_memory( + project_path: String, + scope: String, + content: String, +) -> Result { + write_local_game_memory_at(Path::new(project_path.trim()), scope.trim(), &content) +} + +#[tauri::command] +fn delete_local_game_memory( + project_path: String, + scope: String, +) -> Result { + delete_local_game_memory_at(Path::new(project_path.trim()), scope.trim()) +} + +fn init_local_game_project_at( + root: &Path, + project_id: &str, + name: &str, +) -> Result { + if root.as_os_str().is_empty() { + return Err("项目目录不能为空".to_string()); + } + if !root.is_absolute() { + return Err("项目目录必须是绝对路径".to_string()); + } + if project_id.is_empty() { + return Err("项目 ID 不能为空".to_string()); + } + if name.is_empty() { + return Err("项目名称不能为空".to_string()); + } + + for relative in ["game", "assets", "memory", "exports", ".agent/logs"] { + fs::create_dir_all(root.join(relative)).map_err(|error| { + format!( + "创建本地项目目录失败:{}: {error}", + root.join(relative).display() + ) + })?; + } + + let index_path = root.join("game/index.html"); + if !index_path.exists() { + fs::write(&index_path, DEFAULT_GAME_INDEX_HTML) + .map_err(|error| format!("写入默认游戏入口失败:{}: {error}", index_path.display()))?; + } + + let agent_db_path = root.join(".agent/agent.db"); + if !agent_db_path.exists() { + append_agent_db_record( + root, + serde_json::json!({ + "recordType": "project.init", + "projectId": project_id, + "name": name, + }), + )?; + } + + let manifest_path = root.join(".agent/manifest.json"); + if !manifest_path.exists() { + let manifest = new_game_creation_app_manifest(project_id, name); + write_manifest(&manifest_path, &manifest)?; + } + let manifest = ensure_manifest_has_seed_tasks(root, None)?; + + Ok(InitLocalProjectResult { + project_path: root.to_string_lossy().into_owned(), + manifest_path: manifest_path.to_string_lossy().into_owned(), + manifest, + }) +} + +fn append_agent_db_record(root: &Path, mut record: serde_json::Value) -> Result<(), String> { + let path = root.join(".agent/agent.db"); + let object = record + .as_object_mut() + .ok_or_else(|| "Agent DB record 必须是 JSON object".to_string())?; + object.insert( + "schemaVersion".to_string(), + serde_json::Value::String(GAME_CREATOR_AGENT_DB_SCHEMA_VERSION.to_string()), + ); + object.insert( + "updatedAt".to_string(), + serde_json::Value::Number(serde_json::Number::from(unix_timestamp())), + ); + let mut file = fs::OpenOptions::new() + .create(true) + .append(true) + .open(&path) + .map_err(|error| format!("打开 Agent 本地索引失败:{}: {error}", path.display()))?; + serde_json::to_writer(&mut file, &record) + .map_err(|error| format!("序列化 Agent 本地索引失败:{error}"))?; + file.write_all(b"\n") + .map_err(|error| format!("写入 Agent 本地索引失败:{}: {error}", path.display())) +} + +async fn generate_local_game_draft_at( + root: &Path, + prompt: &str, +) -> Result { + let prompt = prompt.trim(); + if prompt.is_empty() { + return Err("创作想法不能为空".to_string()); + } + + init_local_game_project_at(root, "local-project-draft", "未命名游戏原型")?; + let short_memory = read_optional_text(&root.join("memory/session.md"))?; + let long_memory = read_optional_text(&root.join("memory/project.md"))?; + let asset_context = render_local_asset_prompt_context(root)?; + let long_memory = append_prompt_context(&asset_context, &long_memory); + let client = build_game_creator_llm_client_from_env()?; + let loop_result = + run_game_creator_agent_loop_at(root, &client, prompt, &short_memory, &long_memory).await?; + let mut loop_result = loop_result; + let mut result = write_local_game_draft_at(root, prompt, &loop_result.draft)?; + append_local_artifact_write_step(root, prompt, &mut loop_result)?; + append_static_smoke_step(root, prompt, &mut loop_result)?; + append_agent_loop_log(root, &loop_result)?; + result.manifest = read_manifest_for_project(root)?; + Ok(result) +} + +fn write_local_game_draft_at( + root: &Path, + prompt: &str, + draft: &LlmGameDraft, +) -> Result { + let prompt = prompt.trim(); + if prompt.is_empty() { + return Err("创作想法不能为空".to_string()); + } + validate_llm_game_draft(prompt, draft)?; + init_local_game_project_at(root, "local-project-draft", "未命名游戏原型")?; + let timestamp = unix_timestamp(); + let title = draft.title.trim(); + let handoff_summary = draft.handoff_summary.trim(); + let design_path = root.join("game/game_design.md"); + let balance_path = root.join("game/balance.json"); + let art_manifest_path = root.join("assets/manifest.art.json"); + let audio_manifest_path = root.join("assets/manifest.audio.json"); + let publish_readme_path = root.join("exports/README.md"); + let agent_log_path = root.join(".agent/logs/agent.log"); + let short_memory_path = root.join("memory/session.md"); + let long_memory_path = root.join("memory/project.md"); + let game_index_path = root.join("game/index.html"); + + append_markdown_entry( + &short_memory_path, + "# 短期记忆\n\n", + &format!("- {timestamp}: {prompt}\n"), + "写入短期记忆失败", + )?; + append_markdown_entry( + &long_memory_path, + "# 项目长期记忆\n\n## 当前约束\n\n- Web 小游戏原型\n- 本地 HTTP 预览\n\n## 创作目标记录\n\n", + &format!("- {timestamp}: {prompt}\n"), + "写入长期记忆失败", + )?; + fs::write( + &design_path, + format!( + "# 游戏设计草案\n\n## 原始想法\n\n{prompt}\n\n## Agent 协作交接\n\n{handoff_summary}\n\n## LLM 生成草案\n\n{}\n", + draft.design_markdown.trim() + ), + ) + .map_err(|error| format!("写入游戏设计失败:{}: {error}", design_path.display()))?; + fs::write( + &balance_path, + serde_json::to_string_pretty(&draft.balance) + .map_err(|error| format!("生成数值配置失败:{error}"))?, + ) + .map_err(|error| format!("写入数值配置失败:{}: {error}", balance_path.display()))?; + fs::write( + &art_manifest_path, + serde_json::to_string_pretty(&draft.art_manifest) + .map_err(|error| format!("生成美术清单失败:{error}"))?, + ) + .map_err(|error| format!("写入美术清单失败:{}: {error}", art_manifest_path.display()))?; + fs::write( + &audio_manifest_path, + serde_json::to_string_pretty(&draft.audio_manifest) + .map_err(|error| format!("生成音乐音效清单失败:{error}"))?, + ) + .map_err(|error| { + format!( + "写入音乐音效清单失败:{}: {error}", + audio_manifest_path.display() + ) + })?; + fs::write( + &publish_readme_path, + format!( + "# 发布包装草案\n\n## 标题\n\n{title}\n\n## 简介\n\n{prompt}\n\n## Agent 协作交接\n\n{handoff_summary}\n\n{}\n", + draft.publish_readme.trim() + ), + ) + .map_err(|error| { + format!( + "写入发布包装草案失败:{}: {error}", + publish_readme_path.display() + ) + })?; + + fs::write(&game_index_path, draft.game_html.trim()) + .map_err(|error| format!("写入游戏入口失败:{}: {error}", game_index_path.display()))?; + fs::OpenOptions::new() + .create(true) + .append(true) + .open(&agent_log_path) + .and_then(|mut file| { + file.write_all( + format!("{timestamp} game.generate_draft llm\n{handoff_summary}\n").as_bytes(), + ) + }) + .map_err(|error| format!("写入 Agent 日志失败:{}: {error}", agent_log_path.display()))?; + append_agent_db_record( + root, + serde_json::json!({ + "recordType": "game.generate_draft", + "goal": prompt, + "title": title, + "paths": [ + "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", + ], + }), + )?; + let manifest = record_draft_task_progress(root, prompt, timestamp, &agent_log_path)?; + + Ok(GenerateLocalGameDraftResult { + project_path: root.to_string_lossy().into_owned(), + game_index_path: game_index_path.to_string_lossy().into_owned(), + design_path: design_path.to_string_lossy().into_owned(), + short_memory_path: short_memory_path.to_string_lossy().into_owned(), + long_memory_path: long_memory_path.to_string_lossy().into_owned(), + manifest, + }) +} + +fn build_game_creator_llm_client_from_env() -> Result { + load_game_creator_local_env()?; + let api_key = read_first_non_empty_env(&[ + "GENARRATIVE_GAME_CREATOR_LLM_API_KEY", + "GENARRATIVE_LLM_API_KEY", + "LLM_API_KEY", + "OPENAI_API_KEY", + ]) + .ok_or_else(|| { + "LLM 未配置:请设置 GENARRATIVE_GAME_CREATOR_LLM_API_KEY / GENARRATIVE_LLM_API_KEY / LLM_API_KEY / OPENAI_API_KEY".to_string() + })?; + let base_url = read_first_non_empty_env(&[ + "GENARRATIVE_GAME_CREATOR_LLM_BASE_URL", + "GENARRATIVE_LLM_BASE_URL", + "LLM_BASE_URL", + "OPENAI_BASE_URL", + ]) + .ok_or_else(|| { + "LLM base_url 未配置:请设置 GENARRATIVE_GAME_CREATOR_LLM_BASE_URL / GENARRATIVE_LLM_BASE_URL / LLM_BASE_URL".to_string() + })?; + let model = read_first_non_empty_env(&[ + "GENARRATIVE_GAME_CREATOR_LLM_MODEL", + "GENARRATIVE_LLM_MODEL", + "LLM_MODEL", + "OPENAI_MODEL", + ]) + .ok_or_else(|| { + "LLM model 未配置:请设置 GENARRATIVE_GAME_CREATOR_LLM_MODEL / GENARRATIVE_LLM_MODEL / LLM_MODEL".to_string() + })?; + let request_timeout_ms = read_u64_env( + &[ + "GENARRATIVE_GAME_CREATOR_LLM_REQUEST_TIMEOUT_MS", + "GENARRATIVE_LLM_REQUEST_TIMEOUT_MS", + "LLM_REQUEST_TIMEOUT_MS", + ], + DEFAULT_REQUEST_TIMEOUT_MS, + )?; + let retry_backoff_ms = read_u64_env( + &[ + "GENARRATIVE_GAME_CREATOR_LLM_RETRY_BACKOFF_MS", + "GENARRATIVE_LLM_RETRY_BACKOFF_MS", + "LLM_RETRY_BACKOFF_MS", + ], + DEFAULT_RETRY_BACKOFF_MS, + )?; + let max_retries = read_u32_env( + &[ + "GENARRATIVE_GAME_CREATOR_LLM_MAX_RETRIES", + "GENARRATIVE_LLM_MAX_RETRIES", + "LLM_MAX_RETRIES", + ], + 0, + )?; + let config = LlmConfig::new( + LlmProvider::OpenAiCompatible, + base_url, + api_key, + model, + request_timeout_ms, + max_retries, + retry_backoff_ms, + ) + .map_err(|error| format!("LLM 配置无效:{error}"))?; + + LlmClient::new(config).map_err(|error| format!("LLM client 初始化失败:{error}")) +} + +fn check_game_creator_llm_config_from_env() -> GameCreatorLlmConfigStatus { + let local_env_error = load_game_creator_local_env().err(); + let api_key = read_first_non_empty_env(&[ + "GENARRATIVE_GAME_CREATOR_LLM_API_KEY", + "GENARRATIVE_LLM_API_KEY", + "LLM_API_KEY", + "OPENAI_API_KEY", + ]); + let base_url = read_first_non_empty_env(&[ + "GENARRATIVE_GAME_CREATOR_LLM_BASE_URL", + "GENARRATIVE_LLM_BASE_URL", + "LLM_BASE_URL", + "OPENAI_BASE_URL", + ]); + let model = read_first_non_empty_env(&[ + "GENARRATIVE_GAME_CREATOR_LLM_MODEL", + "GENARRATIVE_LLM_MODEL", + "LLM_MODEL", + "OPENAI_MODEL", + ]); + let mut status = + check_game_creator_llm_config_values(api_key.clone(), base_url.clone(), model.clone()); + if let Some(error) = local_env_error { + status.configured = false; + status.error = Some(error); + return status; + } + if status.configured { + if let Err(error) = build_game_creator_llm_client_from_env() { + status.configured = false; + status.error = Some(error); + } + } + status +} + +fn check_game_creator_llm_config_values( + api_key: Option, + base_url: Option, + model: Option, +) -> GameCreatorLlmConfigStatus { + let api_key_present = api_key + .as_ref() + .is_some_and(|value| !value.trim().is_empty()); + let error = match (api_key.as_deref(), base_url.as_deref(), model.as_deref()) { + (None, _, _) => Some( + "LLM 未配置:请设置 GENARRATIVE_GAME_CREATOR_LLM_API_KEY / GENARRATIVE_LLM_API_KEY / LLM_API_KEY / OPENAI_API_KEY" + .to_string(), + ), + (_, None, _) => Some( + "LLM base_url 未配置:请设置 GENARRATIVE_GAME_CREATOR_LLM_BASE_URL / GENARRATIVE_LLM_BASE_URL / LLM_BASE_URL" + .to_string(), + ), + (_, _, None) => Some( + "LLM model 未配置:请设置 GENARRATIVE_GAME_CREATOR_LLM_MODEL / GENARRATIVE_LLM_MODEL / LLM_MODEL" + .to_string(), + ), + (Some(api_key), Some(base_url), Some(model)) => LlmConfig::new( + LlmProvider::OpenAiCompatible, + base_url.to_string(), + api_key.to_string(), + model.to_string(), + DEFAULT_REQUEST_TIMEOUT_MS, + 0, + DEFAULT_RETRY_BACKOFF_MS, + ) + .and_then(LlmClient::new) + .err() + .map(|error| format!("LLM 配置无效:{error}")), + }; + + GameCreatorLlmConfigStatus { + configured: error.is_none(), + api_key_present, + base_url, + model, + error, + } +} + +#[cfg(test)] +async fn request_llm_game_draft_with_client( + client: &LlmClient, + prompt: &str, + short_memory: &str, + long_memory: &str, +) -> Result { + request_generator_game_draft_with_client( + client, + prompt, + short_memory, + long_memory, + "", + "", + "", + "", + ) + .await +} + +async fn run_game_creator_agent_loop_at( + root: &Path, + client: &LlmClient, + prompt: &str, + short_memory: &str, + long_memory: &str, +) -> Result { + let spec_path = root.join(".agent/spec.md"); + let findings_path = root.join(".agent/findings.md"); + let run_id = format!("game-generate-draft-{}", unix_millis()); + let mut steps = Vec::new(); + let planner_spec = request_planner_spec_with_client(client, prompt, short_memory, long_memory) + .await + .map(|spec| render_planner_spec(prompt, &spec))?; + fs::write(&spec_path, &planner_spec) + .map_err(|error| format!("写入 Planner 规格失败:{}: {error}", spec_path.display()))?; + steps.push(with_task_context( + agent_trace_step( + 0, + "Planner", + "completed", + &[ + "memory/session.md", + "memory/project.md", + ".agent/manifest.json", + ], + &[".agent/spec.md"], + "完成玩法规格和专业组分工", + "llm.chat.planner", + ), + "design", + "Director", + Some("design-director"), + "planning", + )); + + let mut latest_findings = + render_evaluator_findings(0, &["暂无上一轮问题,Generator 可开始首轮实现。"]); + fs::write(&findings_path, &latest_findings).map_err(|error| { + format!( + "写入 Evaluator 结果失败:{}: {error}", + findings_path.display() + ) + })?; + steps.push(agent_trace_step( + 0, + "Evaluator", + "waiting", + &[], + &[".agent/findings.md"], + "初始化评估反馈文件", + "file.write.findings", + )); + write_agent_run_trace(root, &run_id, prompt, "running", 0, &steps, None)?; + + let mut last_error = "Evaluator 未产出可用结果".to_string(); + for pass in 1..=GAME_CREATOR_AGENT_LOOP_MAX_PASSES { + let spec_markdown = read_optional_text(&spec_path)?; + let findings_markdown = read_optional_text(&findings_path)?; + let agenda = write_agent_pass_agenda(root, pass, &findings_markdown)?; + steps.push(agent_trace_step_owned( + pass, + "Orchestrator", + "completed", + vec![ + ".agent/spec.md".to_string(), + ".agent/findings.md".to_string(), + ".agent/manifest.json".to_string(), + ], + vec![ + agenda.relative_path.clone(), + agenda.task_graph_relative_path.clone(), + ], + &format!( + "{};waves={};repairFocus={};repairRoutes={};carried={}", + agenda.summary, + agenda.dependency_waves.len(), + agenda.repair_focus.len(), + agenda.repair_routes.len(), + agenda.carried_task_ids.len() + ), + "agent.task_graph.plan_pass", + )); + let group_briefs = match request_agent_group_briefs_with_client( + root, + client, + prompt, + short_memory, + long_memory, + &spec_markdown, + &findings_markdown, + &agenda, + pass, + ) + .await + { + Ok(briefs) => briefs, + Err(error) => { + let issues = vec![format!("专业组协作输出不可用:{error}")]; + steps.push(agent_trace_step( + pass, + "专业组协作", + "failed", + &[".agent/spec.md", ".agent/findings.md"], + &[], + &issues[0], + "llm.chat.group", + )); + latest_findings = render_evaluator_findings(pass, &issues); + fs::write(&findings_path, &latest_findings).map_err(|write_error| { + format!( + "写入 Evaluator 结果失败:{}: {write_error}", + findings_path.display() + ) + })?; + steps.push(agent_trace_step( + pass, + "Evaluator", + "needs-revision", + &[], + &[".agent/findings.md"], + "记录专业组协作失败原因", + "file.write.findings", + )); + last_error = issues.join(";"); + write_agent_run_trace(root, &run_id, prompt, "needs-revision", pass, &steps, None)?; + continue; + } + }; + append_group_brief_steps(root, pass, &agenda.relative_path, &group_briefs, &mut steps); + let group_briefs_markdown = render_agent_group_briefs_context(&group_briefs); + match request_generator_game_draft_with_client( + client, + prompt, + short_memory, + long_memory, + &spec_markdown, + &findings_markdown, + &group_briefs_markdown, + &read_optional_text(&root.join(&agenda.relative_path))?, + ) + .await + { + Ok(draft) => { + let pass_artifacts = write_agent_pass_artifacts(root, pass, &draft)?; + let mut generator_input_paths = vec![ + "memory/session.md".to_string(), + "memory/project.md".to_string(), + ".agent/manifest.json".to_string(), + ".agent/spec.md".to_string(), + ".agent/findings.md".to_string(), + agenda.relative_path.clone(), + agenda.task_graph_relative_path.clone(), + ]; + generator_input_paths + .extend(group_briefs.iter().map(|brief| brief.relative_path.clone())); + steps.push(agent_trace_step_owned( + pass, + "Generator", + "completed", + generator_input_paths, + vec![pass_artifacts.draft_json.clone()], + "生成结构化游戏草案", + "llm.chat.generator", + )); + append_collaboration_steps(pass, &draft, &pass_artifacts, &mut steps); + let issues = evaluate_game_draft(prompt, &draft); + latest_findings = render_evaluator_findings(pass, &issues); + fs::write(&findings_path, &latest_findings).map_err(|error| { + format!( + "写入 Evaluator 结果失败:{}: {error}", + findings_path.display() + ) + })?; + steps.push(agent_trace_step_owned( + pass, + "Evaluator", + if issues.is_empty() { + "passed" + } else { + "needs-revision" + }, + vec![ + pass_artifacts.game_html.clone(), + pass_artifacts.design_markdown.clone(), + pass_artifacts.balance_json.clone(), + pass_artifacts.art_manifest_json.clone(), + pass_artifacts.audio_manifest_json.clone(), + ], + vec![".agent/findings.md".to_string()], + if issues.is_empty() { + "静态验收通过" + } else { + "发现问题,要求下一轮 Generator 修复" + }, + "evaluator.static_html_check", + )); + if issues.is_empty() { + write_agent_run_trace(root, &run_id, prompt, "passed", pass, &steps, None)?; + return Ok(GameCreatorAgentLoopResult { + run_id, + draft, + spec_markdown, + findings_markdown: latest_findings, + passes: pass, + steps, + }); + } + last_error = issues.join(";"); + write_agent_run_trace(root, &run_id, prompt, "needs-revision", pass, &steps, None)?; + } + Err(error) => { + let issues = vec![format!("Generator 输出不可用:{error}")]; + steps.push(agent_trace_step( + pass, + "Generator", + "failed", + &[".agent/spec.md", ".agent/findings.md"], + &[], + &issues[0], + "llm.chat.generator", + )); + latest_findings = render_evaluator_findings(pass, &issues); + fs::write(&findings_path, &latest_findings).map_err(|write_error| { + format!( + "写入 Evaluator 结果失败:{}: {write_error}", + findings_path.display() + ) + })?; + steps.push(agent_trace_step( + pass, + "Evaluator", + "needs-revision", + &[], + &[".agent/findings.md"], + "记录 Generator 失败原因", + "file.write.findings", + )); + last_error = issues.join(";"); + write_agent_run_trace(root, &run_id, prompt, "needs-revision", pass, &steps, None)?; + } + } + } + + let final_error = format!( + "Agent loop 已重试 {GAME_CREATOR_AGENT_LOOP_MAX_PASSES} 轮但仍未通过 Evaluator:{last_error}" + ); + write_agent_run_trace( + root, + &run_id, + prompt, + "failed", + GAME_CREATOR_AGENT_LOOP_MAX_PASSES, + &steps, + Some(&final_error), + )?; + Err(final_error) +} + +async fn request_planner_spec_with_client( + client: &LlmClient, + prompt: &str, + short_memory: &str, + long_memory: &str, +) -> Result { + let request = LlmTextRequest::new(vec![ + LlmMessage::system(game_creator_planner_system_prompt()), + LlmMessage::user(game_creator_planner_user_prompt( + prompt, + short_memory, + long_memory, + )), + ]) + .with_max_tokens(1800); + let response = request_game_creator_llm_text(client, request) + .await + .map_err(|error| format!("Planner 生成失败:{error}"))?; + let spec = response.content.trim(); + if spec.is_empty() { + Err("Planner 未返回规格".to_string()) + } else { + Ok(spec.to_string()) + } +} + +async fn request_generator_game_draft_with_client( + client: &LlmClient, + prompt: &str, + short_memory: &str, + long_memory: &str, + spec_markdown: &str, + findings_markdown: &str, + group_briefs_markdown: &str, + agenda_markdown: &str, +) -> Result { + let request = LlmTextRequest::new(vec![ + LlmMessage::system(game_creator_system_prompt()), + LlmMessage::user(game_creator_generator_user_prompt( + prompt, + short_memory, + long_memory, + spec_markdown, + findings_markdown, + group_briefs_markdown, + agenda_markdown, + )), + ]) + .with_max_tokens(GAME_CREATOR_LLM_MAX_OUTPUT_TOKENS); + let response = request_game_creator_llm_text(client, request) + .await + .map_err(|error| format!("LLM 生成失败:{error}"))?; + parse_llm_game_draft_response(response.content.as_str()) +} + +async fn request_game_creator_llm_text( + client: &LlmClient, + request: LlmTextRequest, +) -> Result { + if game_creator_llm_stream_enabled() { + client.stream_text(request, |_| {}).await + } else { + client.request_text(request).await + } +} + +fn game_creator_llm_stream_enabled() -> bool { + read_first_non_empty_env(&[ + "GENARRATIVE_GAME_CREATOR_LLM_STREAM", + "GENARRATIVE_LLM_STREAM", + "LLM_STREAM", + ]) + .is_some_and(|value| { + matches!( + value.trim().to_ascii_lowercase().as_str(), + "1" | "true" | "yes" | "on" | "stream" + ) + }) +} + +async fn request_agent_group_briefs_with_client( + root: &Path, + client: &LlmClient, + prompt: &str, + short_memory: &str, + long_memory: &str, + spec_markdown: &str, + findings_markdown: &str, + agenda: &AgentPassAgenda, + pass: u8, +) -> Result, String> { + let mut briefs = Vec::new(); + let mut completed_group_context = String::new(); + let agenda_markdown = read_optional_text(&root.join(&agenda.relative_path))?; + for definition in GAME_CREATOR_AGENT_GROUP_DEFINITIONS { + let mut role_briefs = Vec::new(); + let mut completed_role_context = String::new(); + for role_definition in definition.roles { + let mut should_run = agenda + .active_task_ids + .iter() + .any(|task_id| task_id == role_definition.task_id); + if !should_run { + if let Some((source_path, source_markdown)) = + read_previous_agent_role_brief(root, pass, definition, *role_definition)? + { + let markdown = render_carryover_role_brief( + definition, + *role_definition, + &source_path, + &source_markdown, + ); + let relative_path = write_agent_role_brief( + root, + pass, + definition, + *role_definition, + &markdown, + )?; + let role_brief = AgentRoleBrief { + group_definition: definition, + role_definition: *role_definition, + markdown, + relative_path, + status: "carried-over".to_string(), + tool_id: format!( + "agent.task_graph.carryover.{}.{}", + definition.id, role_definition.id + ), + summary: format!( + "沿用上一轮 {} / {} brief,未命中本轮修复范围", + definition.label, role_definition.role + ), + }; + completed_role_context.push_str(&render_agent_role_brief_context(&role_brief)); + role_briefs.push(role_brief); + continue; + } + should_run = true; + } + if !should_run { + continue; + } + let request = LlmTextRequest::new(vec![ + LlmMessage::system(game_creator_role_system_prompt( + definition, + *role_definition, + )), + LlmMessage::user(game_creator_role_user_prompt( + definition, + *role_definition, + prompt, + short_memory, + long_memory, + spec_markdown, + findings_markdown, + &agenda_markdown, + &completed_group_context, + &completed_role_context, + )), + ]) + .with_max_tokens(900); + let response = request_game_creator_llm_text(client, request) + .await + .map_err(|error| { + format!( + "{} / {} 生成失败:{error}", + definition.label, role_definition.role + ) + })?; + let markdown = response.content.trim(); + if markdown.is_empty() { + return Err(format!( + "{} / {} 未返回 brief", + definition.label, role_definition.role + )); + } + let relative_path = + write_agent_role_brief(root, pass, definition, *role_definition, markdown)?; + let role_brief = AgentRoleBrief { + group_definition: definition, + role_definition: *role_definition, + markdown: markdown.to_string(), + relative_path, + status: "completed".to_string(), + tool_id: role_definition.tool_id.to_string(), + summary: markdown + .lines() + .map(str::trim) + .find(|line| !line.is_empty()) + .unwrap_or("完成组内角色 brief") + .to_string(), + }; + completed_role_context.push_str(&render_agent_role_brief_context(&role_brief)); + role_briefs.push(role_brief); + } + let markdown = render_agent_group_brief_markdown(&role_briefs); + let relative_path = write_agent_group_brief(root, pass, definition, &markdown)?; + completed_group_context.push_str(&format!( + "## {} / {}\n\n{}\n\n", + definition.label, + definition.role, + markdown.trim() + )); + briefs.push(AgentGroupBrief { + definition, + markdown, + relative_path, + role_briefs, + }); + } + Ok(briefs) +} + +fn evaluate_game_draft(prompt: &str, draft: &LlmGameDraft) -> Vec { + let mut issues = Vec::new(); + if let Err(error) = validate_llm_game_draft(prompt, draft) { + issues.push(error); + } + let html = draft.game_html.to_ascii_lowercase(); + if !["keydown", "keyup", "pointer", "mousedown", "touch", "click"] + .iter() + .any(|needle| html.contains(needle)) + { + issues.push("gameHtml 缺少明确的键盘、鼠标或触摸输入监听".to_string()); + } + issues +} + +fn render_planner_spec(prompt: &str, spec: &str) -> String { + format!( + "# Planner Spec\n\n## 用户需求\n\n{}\n\n## 规格\n\n{}\n", + prompt.trim(), + spec.trim() + ) +} + +fn render_evaluator_findings(pass: u8, issues: &[impl AsRef]) -> String { + let issue_values = issues + .iter() + .map(|issue| issue.as_ref().trim()) + .filter(|issue| !issue.is_empty()) + .collect::>(); + let actionable_issues = issue_values + .iter() + .filter(|issue| !issue.contains("暂无上一轮问题")) + .map(|issue| (*issue).to_string()) + .collect::>(); + let mut output = format!( + "# Evaluator Findings\n\n- pass: {pass}\n- status: {}\n\n", + if issue_values.is_empty() { + "passed" + } else { + "needs-revision" + } + ); + if issue_values.is_empty() { + output + .push_str("## 结果\n\n- 本地静态验收通过:HTML 自包含、包含 canvas、主循环和输入。\n"); + } else { + output.push_str("## 问题\n\n"); + for issue in &issue_values { + output.push_str("- "); + output.push_str(issue); + output.push('\n'); + } + } + output.push_str("\n## Repair Routes\n\n```json\n"); + if actionable_issues.is_empty() { + output.push_str("[]"); + } else { + let routes = build_game_creation_seed_task_graph("AI 游戏创作") + .map(|graph| route_game_creation_repair_issues(&graph, &actionable_issues)) + .unwrap_or_default(); + match serde_json::to_string_pretty(&routes) { + Ok(payload) => output.push_str(&payload), + Err(_) => output.push_str("[]"), + } + } + output.push_str("\n```\n"); + output +} + +fn write_agent_pass_agenda( + root: &Path, + pass: u8, + findings_markdown: &str, +) -> Result { + let graph = build_game_creation_seed_task_graph("AI 游戏创作") + .map_err(|error| format!("构建 Agent 编排任务图失败:{error}"))?; + let pass_plan = plan_game_creation_agent_pass(&graph, pass, findings_markdown); + let repair_routes = pass_plan + .repair_routes + .iter() + .map(|route| GameCreationAgentRepairRouteTrace { + issue: route.issue.clone(), + task_ids: route.task_ids.clone(), + reason: route.reason.clone(), + }) + .collect::>(); + let relative_path = format!(".agent/passes/pass-{pass}/agenda.md"); + let markdown = render_agent_pass_agenda_markdown( + pass, + &pass_plan.mode, + &pass_plan.active_task_ids, + &pass_plan.carried_task_ids, + &pass_plan.dependency_waves, + &pass_plan.repair_focus, + &repair_routes, + ); + write_agent_pass_file(root, &relative_path, &markdown)?; + let task_graph_relative_path = format!(".agent/passes/pass-{pass}/task-graph.json"); + write_agent_pass_file( + root, + &task_graph_relative_path, + &render_agent_pass_task_graph_json( + pass, + &pass_plan.mode, + &pass_plan.summary, + &pass_plan.active_task_ids, + &pass_plan.carried_task_ids, + &pass_plan.dependency_waves, + &pass_plan.repair_focus, + &repair_routes, + )?, + )?; + Ok(AgentPassAgenda { + relative_path, + task_graph_relative_path, + active_task_ids: pass_plan.active_task_ids, + carried_task_ids: pass_plan.carried_task_ids, + dependency_waves: pass_plan.dependency_waves, + repair_focus: pass_plan.repair_focus, + repair_routes, + summary: pass_plan.summary, + }) +} + +fn render_agent_pass_agenda_markdown( + pass: u8, + mode: &str, + active_task_ids: &[String], + carried_task_ids: &[String], + dependency_waves: &[Vec], + issues: &[String], + repair_routes: &[GameCreationAgentRepairRouteTrace], +) -> String { + let mut output = format!( + "# Orchestrator Agenda\n\n- pass: {pass}\n- mode: {}\n- activeTasks: {}\n- carriedTasks: {}\n\n", + mode, + active_task_ids.join(", "), + if carried_task_ids.is_empty() { + "none".to_string() + } else { + carried_task_ids.join(", ") + } + ); + output.push_str("## Repair Focus\n\n"); + if issues.is_empty() { + output.push_str("- 首轮生成,所有组内角色参与。\n"); + } else { + for issue in issues { + output.push_str("- "); + output.push_str(issue); + output.push('\n'); + } + } + output.push_str("\n## Dependency Waves\n\n"); + for (index, wave) in dependency_waves.iter().enumerate() { + output.push_str(&format!( + "- wave {}: {}\n", + index + 1, + if wave.is_empty() { + "none".to_string() + } else { + wave.join(", ") + } + )); + } + output.push_str("\n## Repair Routes\n\n"); + if repair_routes.is_empty() { + output.push_str("- none\n"); + } else { + for route in repair_routes { + output.push_str(&format!( + "- issue: {}\n taskIds: {}\n reason: {}\n", + route.issue, + route.task_ids.join(", "), + route.reason + )); + } + } + output.push_str( + "\n## Rule\n\n- activeTasks 调用对应角色 LLM。\n- carriedTasks 沿用上一轮 brief,避免无关角色重复返工。\n- dependencyWaves 是按任务依赖排序后的执行层级,Generator 必须优先服从较早 wave 的约束。\n", + ); + output +} + +fn render_agent_pass_task_graph_json( + pass: u8, + mode: &str, + summary: &str, + active_task_ids: &[String], + carried_task_ids: &[String], + dependency_waves: &[Vec], + issues: &[String], + repair_routes: &[GameCreationAgentRepairRouteTrace], +) -> Result { + serde_json::to_string_pretty(&serde_json::json!({ + "schemaVersion": "game-creator-agent-pass-task-graph.v1", + "pass": pass, + "mode": mode, + "summary": summary, + "activeTaskIds": active_task_ids, + "carriedTaskIds": carried_task_ids, + "repairFocus": issues, + "repairRoutes": repair_routes, + "dependencyWaves": dependency_waves, + })) + .map(|payload| format!("{payload}\n")) + .map_err(|error| format!("生成 Agent pass task graph 失败:{error}")) +} + +fn read_previous_agent_role_brief( + root: &Path, + pass: u8, + group_definition: AgentGroupDefinition, + role_definition: AgentRoleDefinition, +) -> Result, String> { + if pass <= 1 { + return Ok(None); + } + let relative_path = format!( + ".agent/passes/pass-{}/groups/{}/{}", + pass - 1, + group_definition.id, + role_definition.brief_path_name + ); + match fs::read_to_string(root.join(&relative_path)) { + Ok(content) => Ok(Some((relative_path, content))), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(error) => Err(format!( + "读取上一轮角色 brief 失败:{relative_path}: {error}" + )), + } +} + +fn render_carryover_role_brief( + group_definition: AgentGroupDefinition, + role_definition: AgentRoleDefinition, + source_path: &str, + source_markdown: &str, +) -> String { + format!( + "本角色判断:沿用上一轮 {} / {} 输出。\n交付物:{}\n下游约束:本轮未命中该任务,Generator 只在必要时读取此约束。\n验收风险:如果 Evaluator 后续命中该组,下一轮必须重新激活。\n\n## 上一轮 brief\n\n{}", + group_definition.label, + role_definition.role, + source_path, + source_markdown.trim() + ) +} + +fn write_agent_pass_artifacts( + root: &Path, + pass: u8, + draft: &LlmGameDraft, +) -> Result { + let relative_dir = format!(".agent/passes/pass-{pass}"); + let pass_dir = root.join(&relative_dir); + fs::create_dir_all(&pass_dir) + .map_err(|error| format!("创建 Agent pass 目录失败:{}: {error}", pass_dir.display()))?; + + let paths = AgentPassArtifactPaths { + draft_json: format!("{relative_dir}/draft.json"), + design_markdown: format!("{relative_dir}/design.md"), + balance_json: format!("{relative_dir}/balance.json"), + art_manifest_json: format!("{relative_dir}/manifest.art.json"), + audio_manifest_json: format!("{relative_dir}/manifest.audio.json"), + publish_readme: format!("{relative_dir}/README.md"), + game_html: format!("{relative_dir}/game.html"), + handoff_markdown: format!("{relative_dir}/handoff.md"), + }; + + write_agent_pass_file( + root, + &paths.draft_json, + &format!( + "{}\n", + serde_json::to_string_pretty(draft) + .map_err(|error| format!("序列化 Agent pass draft 失败:{error}"))? + ), + )?; + write_agent_pass_file( + root, + &paths.design_markdown, + &format!("# 策划组 / Gameplay\n\n{}\n", draft.design_markdown.trim()), + )?; + write_agent_pass_file( + root, + &paths.balance_json, + &format!( + "{}\n", + serde_json::to_string_pretty(&draft.balance) + .map_err(|error| format!("序列化 Agent pass 数值失败:{error}"))? + ), + )?; + write_agent_pass_file( + root, + &paths.art_manifest_json, + &format!( + "{}\n", + serde_json::to_string_pretty(&draft.art_manifest) + .map_err(|error| format!("序列化 Agent pass 美术清单失败:{error}"))? + ), + )?; + write_agent_pass_file( + root, + &paths.audio_manifest_json, + &format!( + "{}\n", + serde_json::to_string_pretty(&draft.audio_manifest) + .map_err(|error| format!("序列化 Agent pass 音乐清单失败:{error}"))? + ), + )?; + write_agent_pass_file( + root, + &paths.publish_readme, + &format!("# 运营组 / Publish\n\n{}\n", draft.publish_readme.trim()), + )?; + write_agent_pass_file(root, &paths.game_html, &draft.game_html)?; + write_agent_pass_file( + root, + &paths.handoff_markdown, + &render_agent_pass_handoff(pass, draft), + )?; + + Ok(paths) +} + +fn write_agent_pass_file(root: &Path, relative_path: &str, content: &str) -> Result<(), String> { + let path = root.join(relative_path); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).map_err(|error| { + format!( + "创建 Agent pass 文件目录失败:{}: {error}", + parent.display() + ) + })?; + } + fs::write(&path, content) + .map_err(|error| format!("写入 Agent pass 文件失败:{}: {error}", path.display())) +} + +fn write_agent_group_brief( + root: &Path, + pass: u8, + definition: AgentGroupDefinition, + markdown: &str, +) -> Result { + let relative_path = format!( + ".agent/passes/pass-{pass}/groups/{}", + definition.brief_path_name + ); + write_agent_pass_file( + root, + &relative_path, + &format!( + "# {} / {}\n\n{}\n", + definition.label, + definition.role, + markdown.trim() + ), + )?; + Ok(relative_path) +} + +fn write_agent_role_brief( + root: &Path, + pass: u8, + group_definition: AgentGroupDefinition, + role_definition: AgentRoleDefinition, + markdown: &str, +) -> Result { + let relative_path = format!( + ".agent/passes/pass-{pass}/groups/{}/{}", + group_definition.id, role_definition.brief_path_name + ); + write_agent_pass_file( + root, + &relative_path, + &format!( + "# {} / {}\n\n- task: {}\n\n{}\n", + group_definition.label, + role_definition.role, + role_definition.task_id, + markdown.trim() + ), + )?; + Ok(relative_path) +} + +fn append_group_brief_steps( + root: &Path, + pass: u8, + agenda_relative_path: &str, + briefs: &[AgentGroupBrief], + steps: &mut Vec, +) { + let canvas_asset_media_types = project_canvas_asset_media_types(root); + for brief in briefs { + for role_brief in &brief.role_briefs { + let input_paths = vec![ + "memory/session.md".to_string(), + "memory/project.md".to_string(), + ".agent/manifest.json".to_string(), + ".agent/spec.md".to_string(), + ".agent/findings.md".to_string(), + agenda_relative_path.to_string(), + ]; + let output_paths = vec![role_brief.relative_path.clone()]; + let mut step = with_task_context( + agent_trace_step_owned( + pass, + &format!( + "{} / {}", + role_brief.group_definition.label, role_brief.role_definition.role + ), + &role_brief.status, + input_paths.clone(), + output_paths, + &role_brief.summary, + &role_brief.tool_id, + ), + role_brief.group_definition.id, + role_brief.role_definition.role, + Some(role_brief.role_definition.task_id), + "role-brief", + ); + if let Some(tool_call) = + suggested_canvas_tool_call(role_brief, &input_paths, &canvas_asset_media_types) + { + step.tool_calls.push(tool_call); + } + steps.push(step); + } + let role_paths = brief + .role_briefs + .iter() + .map(|role_brief| role_brief.relative_path.clone()) + .collect::>(); + steps.push(with_task_context( + agent_trace_step_owned( + pass, + &format!("{} / GroupCoordinator", brief.definition.label), + "completed", + role_paths, + vec![brief.relative_path.clone()], + "汇总组内角色 brief,交给 Generator", + &format!("agent.group.aggregate.{}", brief.definition.id), + ), + brief.definition.id, + "GroupCoordinator", + None, + "group-aggregate", + )); + } +} + +fn project_canvas_asset_media_types(root: &Path) -> Vec { + read_manifest_for_project(root) + .map(|manifest| { + manifest + .assets + .iter() + .filter(|asset| asset.source.kind == GameCreationAppAssetSourceKind::Canvas) + .map(|asset| asset.media_type.clone()) + .collect() + }) + .unwrap_or_default() +} + +fn suggested_canvas_tool_call( + role_brief: &AgentRoleBrief, + input_paths: &[String], + canvas_asset_media_types: &[String], +) -> Option { + if role_brief.status != "completed" + || role_has_canvas_assets(role_brief, canvas_asset_media_types) + { + return None; + } + let tool_id = match ( + role_brief.group_definition.id, + role_brief.role_definition.id, + ) { + ("art", "asset") | ("audio", "sfx") => "agent.tool.suggest.canvas.project_sync", + _ => return None, + }; + Some(GameCreationAgentToolCallTrace { + tool_id: tool_id.to_string(), + status: "suggested".to_string(), + input_paths: input_paths.to_vec(), + output_paths: Vec::new(), + summary: "项目还没有对应类型的画板回流素材;建议用户确认 /sync-canvas-project <画板项目ID> 后同步画板资源到本地 assets/。" + .to_string(), + }) +} + +fn role_has_canvas_assets(role_brief: &AgentRoleBrief, media_types: &[String]) -> bool { + match ( + role_brief.group_definition.id, + role_brief.role_definition.id, + ) { + ("art", "asset") => media_types.iter().any(|media_type| { + let media_type = media_type.as_str(); + media_type.starts_with("image/") + || media_type == "application/vnd.genarrative.image-sequence" + }), + ("audio", "sfx") => media_types + .iter() + .any(|media_type| media_type.starts_with("audio/")), + _ => false, + } +} + +fn render_agent_group_brief_markdown(role_briefs: &[AgentRoleBrief]) -> String { + let mut output = String::new(); + for role_brief in role_briefs { + output.push_str(&format!( + "## {} / {}\n\n- task: {}\n- artifact: {}\n\n{}\n\n", + role_brief.group_definition.label, + role_brief.role_definition.role, + role_brief.role_definition.task_id, + role_brief.relative_path, + role_brief.markdown.trim() + )); + } + output +} + +fn render_agent_role_brief_context(role_brief: &AgentRoleBrief) -> String { + format!( + "## {} / {}\n\n{}\n\n", + role_brief.group_definition.label, + role_brief.role_definition.role, + role_brief.markdown.trim() + ) +} + +fn render_agent_group_briefs_context(briefs: &[AgentGroupBrief]) -> String { + let mut output = String::new(); + for brief in briefs { + output.push_str(&format!( + "## {} / {}\n\n{}\n\n", + brief.definition.label, + brief.definition.role, + brief.markdown.trim() + )); + } + output +} + +fn render_agent_pass_handoff(pass: u8, draft: &LlmGameDraft) -> String { + let mut output = format!("# Agent Handoff\n\n- pass: {pass}\n\n"); + for handoff in &draft.handoffs { + output.push_str(&format!( + "## {} / {}\n\n{}\n\n- outputs: {}\n- next: {}\n\n", + handoff_group_label(&handoff.group), + handoff.role.trim(), + handoff.summary.trim(), + handoff + .outputs + .iter() + .map(|output| output.trim()) + .filter(|output| !output.is_empty()) + .collect::>() + .join(", "), + handoff.next.trim() + )); + } + output.push_str("## 总结\n\n"); + output.push_str(draft.handoff_summary.trim()); + output.push('\n'); + output +} + +fn append_collaboration_steps( + pass: u8, + draft: &LlmGameDraft, + paths: &AgentPassArtifactPaths, + steps: &mut Vec, +) { + let design = + handoff_summary_for_group(draft, "design", "拆出核心循环、胜负条件和第一版关卡目标"); + let balance = handoff_summary_for_group(draft, "balance", "沉淀速度、生命、得分和难度参数"); + let art = handoff_summary_for_group(draft, "art", "整理角色、场景、UI 和动画资产需求"); + let audio = handoff_summary_for_group(draft, "audio", "整理 BGM 和核心交互音效需求"); + let code = + handoff_summary_for_group(draft, "code", "生成可由本地 HTTP server 预览的 canvas 原型"); + let publishing = + handoff_summary_for_group(draft, "publishing", "整理标题、标签、说明和发布前检查"); + steps.push(with_task_context( + agent_trace_step_owned( + pass, + "策划组 / Gameplay", + "completed", + vec![".agent/spec.md".to_string(), paths.draft_json.clone()], + vec![ + paths.design_markdown.clone(), + paths.handoff_markdown.clone(), + ], + &design, + "agent.handoff.design", + ), + "design", + "Gameplay", + Some("design-foundation"), + "handoff", + )); + steps.push(with_task_context( + agent_trace_step_owned( + pass, + "数值组 / Difficulty", + "completed", + vec![paths.design_markdown.clone()], + vec![paths.balance_json.clone(), paths.handoff_markdown.clone()], + &balance, + "agent.handoff.balance", + ), + "balance", + "Difficulty", + Some("balance-seed"), + "handoff", + )); + steps.push(with_task_context( + agent_trace_step_owned( + pass, + "美术组 / Asset", + "completed", + vec![paths.design_markdown.clone()], + vec![ + paths.art_manifest_json.clone(), + paths.handoff_markdown.clone(), + ], + &art, + "agent.handoff.art", + ), + "art", + "Asset", + Some("art-asset-plan"), + "handoff", + )); + steps.push(with_task_context( + agent_trace_step_owned( + pass, + "音乐组 / SFX", + "completed", + vec![paths.design_markdown.clone()], + vec![ + paths.audio_manifest_json.clone(), + paths.handoff_markdown.clone(), + ], + &audio, + "agent.handoff.audio", + ), + "audio", + "SFX", + Some("audio-asset-plan"), + "handoff", + )); + steps.push(with_task_context( + agent_trace_step_owned( + pass, + "程序组 / Code", + "completed", + vec![ + paths.design_markdown.clone(), + paths.balance_json.clone(), + paths.art_manifest_json.clone(), + paths.audio_manifest_json.clone(), + ], + vec![paths.game_html.clone(), paths.handoff_markdown.clone()], + &code, + "agent.handoff.code", + ), + "code", + "Code", + Some("code-prototype"), + "handoff", + )); + steps.push(with_task_context( + agent_trace_step_owned( + pass, + "运营组 / Publish", + "completed", + vec![paths.design_markdown.clone(), paths.game_html.clone()], + vec![paths.publish_readme.clone(), paths.handoff_markdown.clone()], + &publishing, + "agent.handoff.publish", + ), + "publishing", + "Publish", + Some("publish-package"), + "handoff", + )); +} + +fn handoff_summary_for_group(draft: &LlmGameDraft, group: &str, fallback: &str) -> String { + draft + .handoffs + .iter() + .find(|handoff| handoff.group.trim() == group) + .map(|handoff| handoff.summary.trim()) + .filter(|summary| !summary.is_empty()) + .unwrap_or(fallback) + .to_string() +} + +fn handoff_group_label(group: &str) -> &'static str { + match group.trim() { + "design" => "策划组", + "balance" => "数值组", + "art" => "美术组", + "audio" => "音乐组", + "code" => "程序组", + "publishing" => "运营组", + _ => "专业组", + } +} + +fn append_agent_loop_log( + root: &Path, + loop_result: &GameCreatorAgentLoopResult, +) -> Result<(), String> { + let agent_log_path = root.join(".agent/logs/agent.log"); + let timestamp = unix_timestamp(); + fs::OpenOptions::new() + .create(true) + .append(true) + .open(&agent_log_path) + .and_then(|mut file| { + file.write_all( + format!( + "{timestamp} agent.loop passes={}\nPlanner -> .agent/spec.md\n组内角色 briefs -> .agent/passes/pass-*/groups//*.md\n专业组汇总 -> .agent/passes/pass-*/groups/*.md\nGenerator -> .agent/passes/pass-*/draft.json\n专业组 handoffs -> .agent/passes/pass-*/handoff.md\nEvaluator -> .agent/findings.md\n{}\n{}\n", + loop_result.passes, + loop_result.spec_markdown.trim(), + loop_result.findings_markdown.trim() + ) + .as_bytes(), + ) + }) + .map_err(|error| format!("写入 Agent loop 日志失败:{}: {error}", agent_log_path.display()))?; + append_agent_loop_memory(root, loop_result) +} + +fn append_agent_loop_memory( + root: &Path, + loop_result: &GameCreatorAgentLoopResult, +) -> Result<(), String> { + let trace_path = root.join(".agent/run.latest.json"); + let trace_content = fs::read_to_string(&trace_path).map_err(|error| { + format!( + "读取 Agent run trace 失败:{}: {error}", + trace_path.display() + ) + })?; + let trace = + serde_json::from_str::(&trace_content).map_err(|error| { + format!( + "解析 Agent run trace 失败:{}: {error}", + trace_path.display() + ) + })?; + let timestamp = unix_timestamp(); + let final_artifacts = [ + "game/index.html", + "game/game_design.md", + "game/balance.json", + "assets/manifest.art.json", + "assets/manifest.audio.json", + "exports/README.md", + ] + .into_iter() + .filter(|path| { + trace + .artifacts + .iter() + .any(|artifact| artifact.path == *path) + }) + .collect::>() + .join(", "); + let active_tasks = join_or_none(&trace.task_graph.active_task_ids); + let carried_tasks = join_or_none(&trace.task_graph.carried_task_ids); + let short_entry = format!( + "\n## Agent Run {}\n\n- 时间:{}\n- 标题:{}\n- 状态:{};loop:{}/{};下一步:{}\n- activeTasks:{}\n- carryOverTasks:{}\n- 产物:{}\n", + loop_result.run_id, + timestamp, + loop_result.draft.title.trim(), + trace.status, + trace.passes, + trace.max_passes, + trace.next_step, + active_tasks, + carried_tasks, + if final_artifacts.is_empty() { + "none" + } else { + final_artifacts.as_str() + } + ); + append_markdown_entry( + &root.join("memory/session.md"), + "# 短期记忆\n\n", + &short_entry, + "写入短期 Agent 记忆失败", + )?; + + let long_entry = format!( + "\n## 最近稳定原型\n\n- 时间:{}\n- 标题:{}\n- runId:{}\n- 通过轮次:{}/{}\n- 可试玩入口:game/index.html\n- 设计:game/game_design.md\n- 数值:game/balance.json\n- 美术:assets/manifest.art.json\n- 音乐音效:assets/manifest.audio.json\n- 发布包装:exports/README.md\n", + timestamp, + loop_result.draft.title.trim(), + loop_result.run_id, + trace.passes, + trace.max_passes + ); + append_markdown_entry( + &root.join("memory/project.md"), + "# 项目长期记忆\n\n## 当前约束\n\n- Web 小游戏原型\n- 本地 HTTP 预览\n\n", + &long_entry, + "写入长期 Agent 记忆失败", + ) +} + +fn join_or_none(values: &[String]) -> String { + if values.is_empty() { + "none".to_string() + } else { + values.join(", ") + } +} + +fn agent_trace_step( + pass: u8, + agent: &str, + status: &str, + input_paths: &[&str], + output_paths: &[&str], + summary: &str, + tool_id: &str, +) -> GameCreationAgentRunStep { + agent_trace_step_owned( + pass, + agent, + status, + input_paths + .iter() + .map(|path| (*path).to_string()) + .collect::>(), + output_paths + .iter() + .map(|path| (*path).to_string()) + .collect::>(), + summary, + tool_id, + ) +} + +fn agent_trace_step_owned( + pass: u8, + agent: &str, + status: &str, + input_paths: Vec, + output_paths: Vec, + summary: &str, + tool_id: &str, +) -> GameCreationAgentRunStep { + GameCreationAgentRunStep { + pass, + agent: agent.to_string(), + phase: infer_agent_trace_phase(tool_id).to_string(), + task_id: None, + group: None, + role: None, + status: status.to_string(), + input_paths: input_paths.clone(), + output_paths: output_paths.clone(), + summary: summary.to_string(), + tool_calls: vec![GameCreationAgentToolCallTrace { + tool_id: tool_id.to_string(), + status: status.to_string(), + input_paths, + output_paths, + summary: summary.to_string(), + }], + } +} + +fn infer_agent_trace_phase(tool_id: &str) -> &'static str { + if tool_id == "llm.chat.planner" { + "planning" + } else if tool_id.starts_with("agent.task_graph.") { + "orchestration" + } else if tool_id.starts_with("llm.chat.group.") { + "role-brief" + } else if tool_id.starts_with("agent.group.aggregate.") { + "group-aggregate" + } else if tool_id == "llm.chat.generator" { + "generation" + } else if tool_id.starts_with("agent.handoff.") { + "handoff" + } else if tool_id.starts_with("evaluator.") || tool_id == "file.write.findings" { + "evaluation" + } else if tool_id == "file.write.local_artifacts" { + "artifact-write" + } else if tool_id == "game.static_smoke" { + "playtest" + } else if tool_id.starts_with("preview.") { + "preview" + } else { + "tool" + } +} + +fn with_task_context( + mut step: GameCreationAgentRunStep, + group_id: &str, + role: &str, + task_id: Option<&str>, + phase: &str, +) -> GameCreationAgentRunStep { + step.phase = phase.to_string(); + step.group = game_creation_agent_group_from_id(group_id); + step.role = Some(role.to_string()); + step.task_id = task_id.map(str::to_string); + step +} + +fn game_creation_agent_group_from_id(group_id: &str) -> Option { + match group_id { + "design" => Some(GameCreationAppAgentGroup::Design), + "balance" => Some(GameCreationAppAgentGroup::Balance), + "art" => Some(GameCreationAppAgentGroup::Art), + "audio" => Some(GameCreationAppAgentGroup::Audio), + "code" => Some(GameCreationAppAgentGroup::Code), + "publishing" => Some(GameCreationAppAgentGroup::Publishing), + _ => None, + } +} + +fn append_static_smoke_step( + root: &Path, + prompt: &str, + loop_result: &mut GameCreatorAgentLoopResult, +) -> Result<(), String> { + match run_limited_local_command_at(root, "game.static_smoke") { + Ok(smoke) => { + loop_result.steps.push(with_task_context( + agent_trace_step( + loop_result.passes, + "Playtest", + "completed", + &["game/index.html"], + &[".agent/logs/command.log", ".agent/manifest.json"], + &smoke.output, + "game.static_smoke", + ), + "code", + "Preview", + Some("preview-readiness"), + "playtest", + )); + write_agent_run_trace( + root, + &loop_result.run_id, + prompt, + "passed", + loop_result.passes, + &loop_result.steps, + None, + ) + } + Err(error) => { + loop_result.steps.push(with_task_context( + agent_trace_step( + loop_result.passes, + "Playtest", + "failed", + &["game/index.html"], + &[".agent/logs/command.log"], + &error, + "game.static_smoke", + ), + "code", + "Preview", + Some("preview-readiness"), + "playtest", + )); + write_agent_run_trace( + root, + &loop_result.run_id, + prompt, + "failed", + loop_result.passes, + &loop_result.steps, + Some(&error), + )?; + Err(format!("生成后自检失败:{error}")) + } + } +} + +fn append_static_smoke_manual_trace_step( + root: &Path, + result: &LimitedLocalCommandResult, +) -> Result<(), String> { + append_agent_run_trace_step( + root, + "passed", + "preview-playtest", + with_task_context( + agent_trace_step( + 0, + "Playtest", + "completed", + &["game/index.html"], + &[".agent/logs/command.log", ".agent/manifest.json"], + &result.output, + "game.static_smoke", + ), + "code", + "Preview", + Some("preview-readiness"), + "playtest", + ), + None, + ) +} + +fn append_local_artifact_write_step( + root: &Path, + prompt: &str, + loop_result: &mut GameCreatorAgentLoopResult, +) -> Result<(), String> { + loop_result.steps.push(agent_trace_step_owned( + loop_result.passes, + "ArtifactWriter", + "completed", + vec![ + format!(".agent/passes/pass-{}/draft.json", loop_result.passes), + format!(".agent/passes/pass-{}/handoff.md", loop_result.passes), + ], + vec![ + "memory/session.md".to_string(), + "memory/project.md".to_string(), + "game/game_design.md".to_string(), + "game/balance.json".to_string(), + "assets/manifest.art.json".to_string(), + "assets/manifest.audio.json".to_string(), + "exports/README.md".to_string(), + "game/index.html".to_string(), + ".agent/manifest.json".to_string(), + ], + "把通过 Evaluator 的草案写入本地项目产物", + "file.write.local_artifacts", + )); + write_agent_run_trace( + root, + &loop_result.run_id, + prompt, + "artifacts-written", + loop_result.passes, + &loop_result.steps, + None, + ) +} + +fn append_preview_start_trace_step( + root: &Path, + preview: &LocalPreviewResult, +) -> Result<(), String> { + append_agent_run_trace_step( + root, + "preview-running", + "manual-playtest", + with_task_context( + agent_trace_step( + 0, + "Preview", + "running", + &["game/index.html"], + &[".agent/manifest.json", ".agent/logs/preview.log"], + &format!("本地 HTTP 预览已启动:{}", preview.url), + "preview.start", + ), + "code", + "Playtest", + Some("preview-playtest"), + "preview", + ), + None, + ) +} + +fn append_preview_stop_trace_step(root: &Path) -> Result<(), String> { + append_agent_run_trace_step( + root, + "preview-stopped", + "inspect-artifacts", + with_task_context( + agent_trace_step( + 0, + "Preview", + "stopped", + &[".agent/manifest.json"], + &[".agent/manifest.json", ".agent/logs/preview.log"], + "本地 HTTP 预览已停止", + "preview.stop", + ), + "code", + "Playtest", + Some("preview-playtest"), + "preview", + ), + None, + ) +} + +fn append_preview_log(root: &Path, status: &str, url: Option<&str>) -> Result<(), String> { + let log_path = root.join(".agent/logs/preview.log"); + if let Some(parent) = log_path.parent() { + fs::create_dir_all(parent) + .map_err(|error| format!("创建预览日志目录失败:{}: {error}", parent.display()))?; + } + let line = match url { + Some(url) => format!("{} preview.{status} {url}\n", unix_timestamp()), + None => format!("{} preview.{status}\n", unix_timestamp()), + }; + fs::OpenOptions::new() + .create(true) + .append(true) + .open(&log_path) + .and_then(|mut file| file.write_all(line.as_bytes())) + .map_err(|error| format!("写入预览日志失败:{}: {error}", log_path.display())) +} + +fn record_replaced_preview_stop(preview: &LocalPreviewResult) { + let root = Path::new(&preview.root); + let _ = record_preview_state(root, GameCreationAppPreviewStatus::Stopped, None, None); + let _ = append_preview_log(root, "stopped", None); + let _ = append_preview_stop_trace_step(root); +} + +fn append_agent_run_trace_step( + root: &Path, + status: &str, + next_step: &str, + step: GameCreationAgentRunStep, + error: Option<&str>, +) -> Result<(), String> { + let trace_path = root.join(".agent/run.latest.json"); + let content = match fs::read_to_string(&trace_path) { + Ok(content) => content, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()), + Err(error) => { + return Err(format!( + "读取 Agent run trace 失败:{}: {error}", + trace_path.display() + )) + } + }; + let mut trace = + serde_json::from_str::(&content).map_err(|error| { + format!( + "解析 Agent run trace 失败:{}: {error}", + trace_path.display() + ) + })?; + trace.steps.push(step); + trace.status = status.to_string(); + trace.next_step = next_step.to_string(); + trace.error = error.map(str::to_string); + trace.stop_reason = agent_run_stop_reason(status, error).to_string(); + trace.tool_call_count = count_agent_tool_calls(&trace.steps)?; + trace.max_tool_calls = GAME_CREATOR_AGENT_TOOL_CALL_MAX; + trace.artifacts = collect_agent_run_artifacts(root)?; + trace.task_graph = + build_agent_run_task_graph_trace(root, &trace.goal, trace.passes, &trace.steps)?; + trace.pass_plans = collect_agent_pass_plan_traces(root, trace.passes)?; + trace.updated_at = unix_timestamp(); + write_agent_run_trace_payload(root, &trace) +} + +#[derive(Default)] +struct AgentAgendaSnapshot { + active_task_ids: Vec, + carried_task_ids: Vec, + repair_focus: Vec, + repair_routes: Vec, +} + +#[derive(Default, Deserialize)] +#[serde(rename_all = "camelCase")] +struct AgentPassTaskGraphSnapshot { + #[serde(default)] + pass: u8, + #[serde(default)] + mode: String, + #[serde(default)] + summary: String, + #[serde(default)] + active_task_ids: Vec, + #[serde(default)] + carried_task_ids: Vec, + #[serde(default)] + dependency_waves: Vec>, + #[serde(default)] + repair_focus: Vec, + #[serde(default)] + repair_routes: Vec, +} + +fn build_agent_run_task_graph_trace( + root: &Path, + goal: &str, + pass: u8, + steps: &[GameCreationAgentRunStep], +) -> Result { + let agenda = read_agent_agenda_snapshot(root, pass)?; + let mut tasks = new_game_creation_app_seed_tasks(); + + for task_id in &agenda.active_task_ids { + set_task_status_if_current(&mut tasks, task_id, GameCreationAppTaskStatus::Running); + } + for task_id in &agenda.carried_task_ids { + set_task_status_if_current(&mut tasks, task_id, GameCreationAppTaskStatus::Completed); + } + + for step in steps { + let Some(task_id) = step.task_id.as_deref() else { + continue; + }; + let Some(status) = task_status_from_agent_step(step, task_id) else { + continue; + }; + set_task_status_if_current(&mut tasks, task_id, status); + } + + if task_has_status( + &tasks, + "preview-readiness", + GameCreationAppTaskStatus::Completed, + ) && !steps.iter().any(|step| { + step.task_id.as_deref() == Some("preview-playtest") + && step.phase == "preview" + && step.status == "running" + }) { + set_task_status_if_current( + &mut tasks, + "preview-playtest", + GameCreationAppTaskStatus::WaitingForConfirmation, + ); + } + + Ok(GameCreationAgentRunTaskGraphTrace { + goal: goal.trim().to_string(), + ready_task_ids: ready_task_ids_for_tasks(&tasks), + active_task_ids: agenda.active_task_ids, + carried_task_ids: agenda.carried_task_ids, + repair_focus: agenda.repair_focus, + repair_routes: agenda.repair_routes, + tasks, + }) +} + +fn read_agent_agenda_snapshot(root: &Path, pass: u8) -> Result { + if pass == 0 { + return Ok(AgentAgendaSnapshot::default()); + } + let task_graph_relative_path = format!(".agent/passes/pass-{pass}/task-graph.json"); + match fs::read_to_string(root.join(&task_graph_relative_path)) { + Ok(content) => { + let snapshot: AgentPassTaskGraphSnapshot = + serde_json::from_str(&content).map_err(|error| { + format!("解析 Agent task graph 失败:{task_graph_relative_path}: {error}") + })?; + return Ok(AgentAgendaSnapshot { + active_task_ids: snapshot.active_task_ids, + carried_task_ids: snapshot.carried_task_ids, + repair_focus: snapshot.repair_focus, + repair_routes: snapshot.repair_routes, + }); + } + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + Err(error) => { + return Err(format!( + "读取 Agent task graph 失败:{task_graph_relative_path}: {error}" + )) + } + } + let relative_path = format!(".agent/passes/pass-{pass}/agenda.md"); + let agenda = match fs::read_to_string(root.join(&relative_path)) { + Ok(agenda) => agenda, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + return Ok(AgentAgendaSnapshot::default()) + } + Err(error) => return Err(format!("读取 Agent agenda 失败:{relative_path}: {error}")), + }; + let mut snapshot = AgentAgendaSnapshot::default(); + let mut in_repair_focus = false; + for line in agenda.lines().map(str::trim) { + if let Some(value) = line.strip_prefix("- activeTasks:") { + snapshot.active_task_ids = parse_agent_task_id_list(value); + continue; + } + if let Some(value) = line.strip_prefix("- carriedTasks:") { + snapshot.carried_task_ids = parse_agent_task_id_list(value); + continue; + } + if line == "## Repair Focus" { + in_repair_focus = true; + continue; + } + if line.starts_with("## ") { + in_repair_focus = false; + } + if in_repair_focus { + if let Some(issue) = line.strip_prefix("- ") { + let issue = issue.trim(); + if !issue.is_empty() && !issue.contains("首轮生成") { + snapshot.repair_focus.push(issue.to_string()); + } + } + } + } + Ok(snapshot) +} + +fn collect_agent_pass_plan_traces( + root: &Path, + passes: u8, +) -> Result, String> { + let mut plans = Vec::new(); + for pass in 1..=passes { + let relative_path = format!(".agent/passes/pass-{pass}/task-graph.json"); + let content = match fs::read_to_string(root.join(&relative_path)) { + Ok(content) => content, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue, + Err(error) => { + return Err(format!( + "读取 Agent pass plan 失败:{relative_path}: {error}" + )) + } + }; + let snapshot: AgentPassTaskGraphSnapshot = serde_json::from_str(&content) + .map_err(|error| format!("解析 Agent pass plan 失败:{relative_path}: {error}"))?; + plans.push(GameCreationAgentPassPlanTrace { + pass: if snapshot.pass == 0 { + pass + } else { + snapshot.pass + }, + mode: snapshot.mode, + summary: snapshot.summary, + active_task_ids: snapshot.active_task_ids, + carried_task_ids: snapshot.carried_task_ids, + dependency_waves: snapshot.dependency_waves, + repair_focus: snapshot.repair_focus, + repair_routes: snapshot.repair_routes, + }); + } + Ok(plans) +} + +fn parse_agent_task_id_list(value: &str) -> Vec { + value + .split(',') + .map(str::trim) + .filter(|task_id| !task_id.is_empty() && *task_id != "none") + .map(str::to_string) + .collect() +} + +fn task_status_from_agent_step( + step: &GameCreationAgentRunStep, + task_id: &str, +) -> Option { + if step.status == "failed" { + return Some(GameCreationAppTaskStatus::Failed); + } + if step.status == "running" { + return Some(GameCreationAppTaskStatus::Running); + } + if step.status == "stopped" && step.phase == "preview" { + return Some(GameCreationAppTaskStatus::WaitingForConfirmation); + } + if step.status == "carried-over" { + return Some(GameCreationAppTaskStatus::Completed); + } + if step.status != "completed" && step.status != "passed" { + return None; + } + + match step.phase.as_str() { + "planning" | "handoff" | "playtest" => Some(GameCreationAppTaskStatus::Completed), + "preview" => Some(GameCreationAppTaskStatus::Running), + "role-brief" if role_brief_completes_task(task_id) => { + Some(GameCreationAppTaskStatus::Completed) + } + "role-brief" => Some(GameCreationAppTaskStatus::Running), + _ => None, + } +} + +fn role_brief_completes_task(task_id: &str) -> bool { + matches!( + task_id, + "balance-director" + | "art-director" + | "art-polish" + | "audio-director" + | "code-director" + | "publish-strategy" + ) +} + +fn set_task_status_if_current( + tasks: &mut [GameCreationAppTaskState], + task_id: &str, + status: GameCreationAppTaskStatus, +) { + if let Some(task) = tasks.iter_mut().find(|task| task.id == task_id) { + if should_replace_task_status(&task.status, &status) { + task.status = status; + } + } +} + +fn should_replace_task_status( + current: &GameCreationAppTaskStatus, + next: &GameCreationAppTaskStatus, +) -> bool { + use GameCreationAppTaskStatus as Status; + status_rank(next) >= status_rank(current) + || matches!( + (current, next), + (Status::Running, Status::Completed) + | (Status::WaitingForConfirmation, Status::Running) + | (Status::Pending, _) + ) +} + +fn status_rank(status: &GameCreationAppTaskStatus) -> u8 { + match status { + GameCreationAppTaskStatus::Pending => 0, + GameCreationAppTaskStatus::Running => 1, + GameCreationAppTaskStatus::WaitingForConfirmation => 2, + GameCreationAppTaskStatus::Completed => 3, + GameCreationAppTaskStatus::Failed => 4, + } +} + +fn task_has_status( + tasks: &[GameCreationAppTaskState], + task_id: &str, + status: GameCreationAppTaskStatus, +) -> bool { + tasks + .iter() + .find(|task| task.id == task_id) + .is_some_and(|task| task.status == status) +} + +fn ready_task_ids_for_tasks(tasks: &[GameCreationAppTaskState]) -> Vec { + tasks + .iter() + .filter(|task| { + task.status == GameCreationAppTaskStatus::Pending + && task.dependencies.iter().all(|dependency| { + task_has_status(tasks, dependency, GameCreationAppTaskStatus::Completed) + }) + }) + .map(|task| task.id.clone()) + .collect() +} + +fn write_agent_run_trace( + root: &Path, + run_id: &str, + prompt: &str, + status: &str, + passes: u8, + steps: &[GameCreationAgentRunStep], + error: Option<&str>, +) -> Result<(), String> { + let tool_call_count = count_agent_tool_calls(steps)?; + let trace = GameCreationAgentRunTrace { + schema_version: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION.to_string(), + run_id: run_id.to_string(), + command_id: "game.generate_draft".to_string(), + status: status.to_string(), + passes, + max_passes: GAME_CREATOR_AGENT_LOOP_MAX_PASSES, + tool_call_count, + max_tool_calls: GAME_CREATOR_AGENT_TOOL_CALL_MAX, + stop_reason: agent_run_stop_reason(status, error).to_string(), + goal: prompt.trim().to_string(), + coordination: "filesystem".to_string(), + steps: steps.to_vec(), + artifacts: collect_agent_run_artifacts(root)?, + task_graph: build_agent_run_task_graph_trace(root, prompt, passes, steps)?, + pass_plans: collect_agent_pass_plan_traces(root, passes)?, + next_step: match status { + "passed" => "preview-playtest", + "artifacts-written" => "game.static_smoke", + "failed" => "inspect-error", + _ => "generator-revision", + } + .to_string(), + error: error.map(str::to_string), + updated_at: unix_timestamp(), + }; + write_agent_run_trace_payload(root, &trace) +} + +fn agent_run_stop_reason(status: &str, error: Option<&str>) -> &'static str { + match status { + "running" => "loop-running", + "needs-revision" => "evaluator-needs-revision", + "passed" => "evaluator-passed", + "artifacts-written" => "artifacts-written", + "preview-running" => "preview-running", + "preview-stopped" => "preview-stopped", + "failed" if error.is_some_and(|message| message.contains("已重试")) => { + "max-passes-exhausted" + } + "failed" => "failed", + _ => "unknown", + } +} + +fn count_agent_tool_calls(steps: &[GameCreationAgentRunStep]) -> Result { + let count = steps.iter().try_fold(0u16, |current, step| { + let step_count = u16::try_from(step.tool_calls.len()) + .map_err(|_| "Agent 工具调用数超过上限".to_string())?; + current + .checked_add(step_count) + .ok_or_else(|| "Agent 工具调用数超过上限".to_string()) + })?; + if count > GAME_CREATOR_AGENT_TOOL_CALL_MAX { + return Err(format!( + "Agent 工具调用预算超限:{count}/{GAME_CREATOR_AGENT_TOOL_CALL_MAX}" + )); + } + Ok(count) +} + +fn write_agent_run_trace_payload( + root: &Path, + trace: &GameCreationAgentRunTrace, +) -> Result<(), String> { + if trace.run_id.contains('/') || trace.run_id.contains('\\') || trace.run_id.contains("..") { + return Err("Agent run_id 非法".to_string()); + } + let payload = serde_json::to_string_pretty(&trace) + .map_err(|error| format!("生成 Agent run trace 失败:{error}"))?; + let latest_path = root.join(".agent/run.latest.json"); + fs::write(&latest_path, &payload).map_err(|error| { + format!( + "写入 Agent run trace 失败:{}: {error}", + latest_path.display() + ) + })?; + let run_dir = root.join(".agent/runs"); + fs::create_dir_all(&run_dir).map_err(|error| { + format!( + "创建 Agent run history 目录失败:{}: {error}", + run_dir.display() + ) + })?; + let run_path = run_dir.join(format!("{}.json", trace.run_id)); + fs::write(&run_path, payload).map_err(|error| { + format!( + "写入 Agent run history 失败:{}: {error}", + run_path.display() + ) + }) +} + +fn collect_agent_run_artifacts(root: &Path) -> Result, String> { + let mut relative_paths = GAME_CREATOR_AGENT_ARTIFACT_PATHS + .iter() + .map(|path| (*path).to_string()) + .collect::>(); + let passes_dir = root.join(".agent/passes"); + if passes_dir.exists() { + let mut pass_dirs = fs::read_dir(&passes_dir) + .map_err(|error| { + format!( + "读取 Agent pass 目录失败:{}: {error}", + passes_dir.display() + ) + })? + .collect::, _>>() + .map_err(|error| { + format!( + "读取 Agent pass 目录失败:{}: {error}", + passes_dir.display() + ) + })?; + pass_dirs.sort_by_key(|entry| entry.path()); + for pass_dir in pass_dirs { + let pass_path = pass_dir.path(); + let metadata = fs::symlink_metadata(&pass_path).map_err(|error| { + format!( + "读取 Agent pass 元数据失败:{}: {error}", + pass_path.display() + ) + })?; + if metadata.file_type().is_symlink() { + return Err("Agent pass 目录不能是符号链接".to_string()); + } + if !metadata.is_dir() { + continue; + } + let mut dirs = vec![pass_path]; + while let Some(dir) = dirs.pop() { + let mut entries = fs::read_dir(&dir) + .map_err(|error| { + format!("读取 Agent pass 文件失败:{}: {error}", dir.display()) + })? + .collect::, _>>() + .map_err(|error| { + format!("读取 Agent pass 文件失败:{}: {error}", dir.display()) + })?; + entries.sort_by_key(|entry| entry.path()); + for entry in entries { + let entry_path = entry.path(); + let metadata = fs::symlink_metadata(&entry_path).map_err(|error| { + format!( + "读取 Agent pass 文件元数据失败:{}: {error}", + entry_path.display() + ) + })?; + if metadata.file_type().is_symlink() { + return Err("Agent pass artifact 不能是符号链接".to_string()); + } + if metadata.is_dir() { + dirs.push(entry_path); + } else if metadata.is_file() { + relative_paths.push(relative_project_path(root, &entry_path)?); + } + } + } + } + } + + relative_paths.sort(); + relative_paths.dedup(); + let mut artifacts = Vec::new(); + for relative_path in relative_paths { + let path = root.join(&relative_path); + let metadata = match fs::symlink_metadata(&path) { + Ok(metadata) => metadata, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue, + Err(error) => { + return Err(format!( + "读取 Agent artifact 元数据失败:{}: {error}", + path.display() + )) + } + }; + if metadata.file_type().is_symlink() { + return Err("Agent artifact 不能是符号链接".to_string()); + } + if !metadata.is_file() { + continue; + } + let bytes = fs::read(&path) + .map_err(|error| format!("读取 Agent artifact 失败:{}: {error}", path.display()))?; + artifacts.push(GameCreationAgentArtifactTrace { + path: relative_path, + size_bytes: metadata.len(), + checksum: format!("fnv1a64:{:016x}", fnv1a64(&bytes)), + }); + } + Ok(artifacts) +} + +fn fnv1a64(bytes: &[u8]) -> u64 { + let mut hash = 0xcbf29ce484222325_u64; + for byte in bytes { + hash ^= u64::from(*byte); + hash = hash.wrapping_mul(0x100000001b3); + } + hash +} + +fn parse_llm_game_draft_response(content: &str) -> Result { + let payload = + extract_json_payload(content).ok_or_else(|| "LLM 返回不是 JSON 对象".to_string())?; + serde_json::from_str::(payload) + .map_err(|error| format!("解析 LLM 游戏草案失败:{error}")) +} + +fn extract_json_payload(content: &str) -> Option<&str> { + let trimmed = content.trim(); + let without_fence = trimmed + .strip_prefix("```json") + .or_else(|| trimmed.strip_prefix("```")) + .and_then(|value| value.strip_suffix("```")) + .map(str::trim) + .unwrap_or(trimmed); + let start = without_fence.find('{')?; + let end = without_fence.rfind('}')?; + if start > end { + return None; + } + Some(&without_fence[start..=end]) +} + +fn validate_llm_game_draft(prompt: &str, draft: &LlmGameDraft) -> Result<(), String> { + if draft.title.trim().is_empty() { + return Err("LLM 草案缺少标题".to_string()); + } + if draft.design_markdown.trim().is_empty() { + return Err("LLM 草案缺少设计说明".to_string()); + } + if !draft.balance.is_object() { + return Err("LLM 草案 balance 必须是 JSON object".to_string()); + } + if !draft.art_manifest.is_object() { + return Err("LLM 草案 artManifest 必须是 JSON object".to_string()); + } + if !draft.audio_manifest.is_object() { + return Err("LLM 草案 audioManifest 必须是 JSON object".to_string()); + } + if draft.publish_readme.trim().is_empty() { + return Err("LLM 草案缺少发布说明".to_string()); + } + if draft.handoff_summary.trim().is_empty() { + return Err("LLM 草案缺少多智能体交接摘要".to_string()); + } + validate_llm_agent_handoffs(draft)?; + + let html = draft.game_html.trim(); + let lower_html = html.to_ascii_lowercase(); + if !lower_html.contains("')) && html.contains(prompt) { + return Err("LLM 草案 gameHtml 包含未转义的用户输入".to_string()); + } + validate_playable_game_html(html, "LLM 草案 gameHtml")?; + validate_non_placeholder_game_html(html, "LLM 草案 gameHtml")?; + + Ok(()) +} + +fn validate_playable_game_html(html: &str, label: &str) -> Result<(), String> { + let lower_html = html.to_ascii_lowercase(); + if !contains_any( + &lower_html, + &[ + "目标", + "任务", + "goal", + "objective", + "点亮", + "收集", + "抵达", + "获胜", + "通关", + "连击", + "combo", + "survive", + ], + ) { + return Err(format!("{label} 必须展示明确目标")); + } + if !contains_any( + &lower_html, + &[ + "胜利", + "获胜", + "失败", + "game over", + "win", + "lose", + "victory", + "defeat", + ], + ) { + return Err(format!("{label} 必须包含失败或胜利状态")); + } + if !contains_any( + &lower_html, + &["重开", "重新开始", "restart", "reset", "again", "再来"], + ) { + return Err(format!("{label} 必须包含重开路径")); + } + Ok(()) +} + +fn validate_safe_game_html_runtime(html: &str, label: &str) -> Result<(), String> { + let lower_html = html.to_ascii_lowercase(); + if lower_html.contains(" + +"# + .to_string(), + } + } + + fn fake_agent_handoffs() -> Vec { + vec![ + handoff( + "design", + "Gameplay", + "定义反弹循环", + ["game/game_design.md"], + "交给数值和程序组", + ), + handoff( + "balance", + "Difficulty", + "设置生命和速度", + ["game/balance.json"], + "交给程序组读取", + ), + handoff( + "art", + "Asset", + "规划厨房角色和场景资产", + ["assets/manifest.art.json"], + "进入画板链路", + ), + handoff( + "audio", + "SFX", + "规划反弹音效和 BGM", + ["assets/manifest.audio.json"], + "进入音频生成链路", + ), + handoff( + "code", + "Code", + "生成 canvas 原型", + ["game/index.html"], + "交给 Playtest", + ), + handoff( + "publishing", + "Publish", + "整理标题和标签", + ["exports/README.md"], + "等待预览验收", + ), + ] + } + + fn handoff( + group: &str, + role: &str, + summary: &str, + outputs: [&str; N], + next: &str, + ) -> LlmAgentHandoff { + LlmAgentHandoff { + group: group.to_string(), + role: role.to_string(), + summary: summary.to_string(), + outputs: outputs.map(str::to_string).to_vec(), + next: next.to_string(), + } + } + + fn write_test_canvas_export_zip(path: &Path) { + let file = File::create(path).expect("create canvas export zip"); + let mut writer = zip::ZipWriter::new(file); + let options = SimpleFileOptions::default(); + writer + .start_file("月光画布-画布素材/images/001-月光主角.png", options) + .expect("start image file"); + writer.write_all(b"fake-png").expect("write image"); + writer + .start_file("月光画布-画布素材/media/002-玻璃月光.mp3", options) + .expect("start audio file"); + writer.write_all(b"fake-mp3").expect("write audio"); + writer + .start_file("月光画布-画布素材/manifest.txt", options) + .expect("start manifest"); + writer + .write_all("项目:月光画布\n素材数量:2\n".as_bytes()) + .expect("write manifest"); + writer + .start_file("月光画布-画布素材/metadata.json", options) + .expect("start metadata"); + writer + .write_all( + serde_json::json!({ + "projectTitle": "月光画布", + "exportedAt": "2026-06-24T00:00:00.000Z", + "layers": [ + { + "title": "月光主角", + "file": "images/001-月光主角.png", + "visible": { + "type": "角色", + "generationInputs": null, + "model": "gpt-image-2", + "task": "42", + "object": "asset-object-1", + "resolution": "512 x 512 px" + } + }, + { + "title": "玻璃月光 BGM", + "file": "media/002-玻璃月光.mp3", + "visible": { + "type": "音乐", + "generationInputs": null, + "model": "-", + "task": "-", + "object": "-", + "duration": "12s" + } + } + ], + "failedImages": [] + }) + .to_string() + .as_bytes(), + ) + .expect("write metadata"); + writer.finish().expect("finish canvas export zip"); + } + + fn spawn_mock_llm_server(response_content: String) -> String { + spawn_mock_llm_server_responses(vec![response_content]) + } + + fn spawn_mock_llm_server_responses(response_contents: Vec) -> String { + spawn_mock_llm_server_responses_with_capture(response_contents, None) + } + + fn spawn_mock_llm_server_responses_with_capture( + response_contents: Vec, + request_sender: Option>, + ) -> String { + let listener = TcpListener::bind(("127.0.0.1", 0)).expect("mock llm bind"); + let base_url = format!("http://{}", listener.local_addr().expect("mock llm addr")); + std::thread::spawn(move || { + for response_content in response_contents { + let (mut stream, _) = listener.accept().expect("mock llm accept"); + let mut request_buffer = [0_u8; 8192]; + let read_len = stream.read(&mut request_buffer).unwrap_or(0); + if let Some(sender) = request_sender.as_ref() { + let _ = sender + .send(String::from_utf8_lossy(&request_buffer[..read_len]).into_owned()); + } + let body = serde_json::json!({ + "id": "chatcmpl_game_creator_mock", + "model": "mock-game-model", + "choices": [ + { + "message": { "content": response_content }, + "finish_reason": "stop" + } + ], + "usage": { "prompt_tokens": 11, "completion_tokens": 22, "total_tokens": 33 } + }) + .to_string(); + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", + body.len(), + body + ); + stream + .write_all(response.as_bytes()) + .expect("mock llm response"); + } + }); + base_url + } + + fn spawn_mock_external_canvas_api_server() -> String { + let listener = TcpListener::bind(("127.0.0.1", 0)).expect("mock canvas api bind"); + let base_url = format!( + "http://{}", + listener.local_addr().expect("mock canvas api addr") + ); + let signed_url = format!("{base_url}/signed/hero.png"); + let project_body = serde_json::json!({ + "project": { + "projectId": "canvas-project-1", + "title": "月光画板", + "resources": [ + { + "resourceId": "resource-1", + "projectId": "canvas-project-1", + "imageSrc": "/generated/canvas/hero.png", + "objectKey": "generated/canvas/hero.png", + "assetObjectId": "asset-object-1", + "width": 64, + "height": 64, + "sourceType": "generated", + "prompt": "像素月光主角", + "actualPrompt": "透明 PNG 像素月光主角", + "model": "gpt-image-2", + "provider": "vector-engine", + "taskId": "task-1", + "assetKind": "character" + } + ], + "updatedAt": "2026-06-25T00:00:00Z" + } + }) + .to_string(); + let read_body = serde_json::json!({ + "read": { + "provider": "aliyun-oss", + "bucket": "mock", + "endpoint": "mock", + "host": "mock", + "objectKey": "generated/canvas/hero.png", + "expiresAt": "2026-06-25T00:10:00Z", + "signedUrl": signed_url + } + }) + .to_string(); + std::thread::spawn(move || { + for _ in 0..3 { + let (mut stream, _) = listener.accept().expect("mock canvas api accept"); + let mut request_buffer = [0_u8; 8192]; + let read_len = stream.read(&mut request_buffer).unwrap_or(0); + let request = String::from_utf8_lossy(&request_buffer[..read_len]); + let (content_type, body) = if request + .starts_with("GET /api/external/v1/editor/projects/canvas-project-1 ") + { + ("application/json", project_body.as_bytes().to_vec()) + } else if request.starts_with( + "GET /api/external/v1/assets/read-url?objectKey=generated%2Fcanvas%2Fhero.png ", + ) { + ("application/json", read_body.as_bytes().to_vec()) + } else if request.starts_with("GET /signed/hero.png ") { + ("image/png", b"fake-png".to_vec()) + } else { + ("text/plain", b"not found".to_vec()) + }; + let status = if content_type == "text/plain" { + "404 Not Found" + } else { + "200 OK" + }; + let response = format!( + "HTTP/1.1 {status}\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", + body.len() + ); + stream + .write_all(response.as_bytes()) + .expect("mock canvas api header"); + stream.write_all(&body).expect("mock canvas api body"); + } + }); + base_url + } + + fn fake_group_brief_responses(pass: u8) -> Vec { + GAME_CREATOR_AGENT_GROUP_DEFINITIONS + .iter() + .flat_map(|definition| { + definition.roles.iter().map(move |role| { + format!( + "本角色判断:pass {pass} {} {} brief\n交付物:{}\n下游约束:必须响应月光厨房玩法\n验收风险:不能空转", + definition.label, role.role, role.brief_path_name + ) + }) + }) + .collect() + } + + fn fake_group_brief_responses_for_group(pass: u8, group_id: &str) -> Vec { + GAME_CREATOR_AGENT_GROUP_DEFINITIONS + .iter() + .filter(|definition| definition.id == group_id) + .flat_map(|definition| { + definition.roles.iter().map(move |role| { + format!( + "本角色判断:pass {pass} {} {} 修复 brief\n交付物:{}\n下游约束:必须修复 Evaluator 命中的问题\n验收风险:不能空转", + definition.label, role.role, role.brief_path_name + ) + }) + }) + .collect() + } + + #[tokio::test] + async fn request_llm_game_draft_uses_openai_compatible_provider_output() { + let response_content = + serde_json::to_string(&fake_llm_game_draft()).expect("fake draft json"); + let base_url = spawn_mock_llm_server(response_content); + let config = LlmConfig::new( + LlmProvider::OpenAiCompatible, + base_url, + "test-key".to_string(), + "mock-game-model".to_string(), + DEFAULT_REQUEST_TIMEOUT_MS, + 0, + DEFAULT_RETRY_BACKOFF_MS, + ) + .expect("llm config"); + let client = LlmClient::new(config).expect("llm client"); + + let draft = request_llm_game_draft_with_client( + &client, + "做一个月光厨房弹幕游戏", + "", + "# 项目长期记忆\n", + ) + .await + .expect("llm draft"); + + assert_eq!(draft.title, "月光弹幕厨房"); + assert!(draft + .game_html + .contains("MOCK_UNIQUE_MECHANIC:moon-kitchen-reflect")); + assert_eq!(draft.balance["source"], "llm"); + assert_eq!(draft.handoffs.len(), 6); + assert!(draft + .handoffs + .iter() + .any(|handoff| handoff.group == "publishing")); + } + + #[tokio::test] + async fn generate_local_game_draft_sends_asset_context_to_llm() { + let _env_guard = TEST_ENV_LOCK.lock().expect("test env lock"); + let root = unique_project_path(); + let uploaded = upload_local_asset_at(&root, "../角色.png", "image/png", b"fake-png") + .expect("asset upload"); + let mut responses = vec![ + "## 核心循环\n\n用上传角色图做主角。\n\n## Evaluator 验收\n\n必须使用本地资产。" + .to_string(), + ]; + responses.extend(fake_group_brief_responses(1)); + responses.push(serde_json::to_string(&fake_llm_game_draft()).expect("draft json")); + let (sender, receiver) = mpsc::channel(); + let base_url = spawn_mock_llm_server_responses_with_capture(responses, Some(sender)); + let previous_api_key = std::env::var("GENARRATIVE_GAME_CREATOR_LLM_API_KEY").ok(); + let previous_base_url = std::env::var("GENARRATIVE_GAME_CREATOR_LLM_BASE_URL").ok(); + let previous_model = std::env::var("GENARRATIVE_GAME_CREATOR_LLM_MODEL").ok(); + std::env::set_var("GENARRATIVE_GAME_CREATOR_LLM_API_KEY", "test-key"); + std::env::set_var("GENARRATIVE_GAME_CREATOR_LLM_BASE_URL", base_url); + std::env::set_var("GENARRATIVE_GAME_CREATOR_LLM_MODEL", "mock-game-model"); + + let result = generate_local_game_draft_at(&root, "用上传角色图做主角").await; + + restore_env("GENARRATIVE_GAME_CREATOR_LLM_API_KEY", previous_api_key); + restore_env("GENARRATIVE_GAME_CREATOR_LLM_BASE_URL", previous_base_url); + restore_env("GENARRATIVE_GAME_CREATOR_LLM_MODEL", previous_model); + result.expect("generated draft"); + + let requests = receiver.try_iter().collect::>(); + let planner_request = requests.first().expect("planner request"); + assert!(planner_request.contains("# 本地项目资产")); + assert!(planner_request.contains(&uploaded.local_path)); + assert!(planner_request.contains("source=uploaded")); + + fs::remove_dir_all(root).ok(); + } + + #[test] + fn llm_config_check_reports_status_without_leaking_key() { + let missing = check_game_creator_llm_config_values(None, None, None); + assert!(!missing.configured); + assert!(!missing.api_key_present); + assert!(missing.error.unwrap().contains("LLM 未配置")); + + let configured = check_game_creator_llm_config_values( + Some("unit-test-api-key".to_string()), + Some("http://127.0.0.1:1/v1".to_string()), + Some("mock-game-model".to_string()), + ); + assert!(configured.configured); + assert!(configured.api_key_present); + assert_eq!( + configured.base_url.as_deref(), + Some("http://127.0.0.1:1/v1") + ); + assert_eq!(configured.model.as_deref(), Some("mock-game-model")); + assert!(!serde_json::to_string(&configured) + .unwrap() + .contains("unit-test-api-key")); + } + + #[tokio::test] + async fn agent_loop_writes_spec_findings_and_retries_generator() { + let root = unique_project_path(); + init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init"); + let mut first_draft = fake_llm_game_draft(); + first_draft.game_html = r#" + + + +

目标:反弹月光弹幕点亮三口锅。胜利 / 失败后按 R 重开。

+ + +"# + .to_string(); + let fixed_draft = fake_llm_game_draft(); + let mut responses = vec![ + "## 核心循环\n\n反弹月光弹幕点亮三口锅。\n\n## Evaluator 验收\n\n必须有输入监听。" + .to_string(), + ]; + responses.extend(fake_group_brief_responses(1)); + responses.push(serde_json::to_string(&first_draft).expect("first draft json")); + responses.extend(fake_group_brief_responses_for_group(2, "code")); + responses.extend(fake_group_brief_responses_for_group(2, "publishing")); + responses.push(serde_json::to_string(&fixed_draft).expect("fixed draft json")); + let base_url = spawn_mock_llm_server_responses(responses); + let config = LlmConfig::new( + LlmProvider::OpenAiCompatible, + base_url, + "test-key".to_string(), + "mock-game-model".to_string(), + DEFAULT_REQUEST_TIMEOUT_MS, + 0, + DEFAULT_RETRY_BACKOFF_MS, + ) + .expect("llm config"); + let client = LlmClient::new(config).expect("llm client"); + + let mut loop_result = run_game_creator_agent_loop_at( + &root, + &client, + "做一个月光厨房弹幕游戏", + "", + "# 项目长期记忆\n", + ) + .await + .expect("agent loop"); + let result = write_local_game_draft_at(&root, "做一个月光厨房弹幕游戏", &loop_result.draft) + .expect("write draft"); + append_local_artifact_write_step(&root, "做一个月光厨房弹幕游戏", &mut loop_result) + .expect("artifact write trace"); + append_static_smoke_step(&root, "做一个月光厨房弹幕游戏", &mut loop_result) + .expect("static smoke trace"); + append_agent_loop_log(&root, &loop_result).expect("loop log"); + + assert_eq!(loop_result.passes, 2); + assert_eq!(result.project_path, root.to_string_lossy().into_owned()); + let spec = fs::read_to_string(root.join(".agent/spec.md")).expect("spec"); + assert!(spec.contains("# Planner Spec")); + assert!(spec.contains("反弹月光弹幕")); + let findings = fs::read_to_string(root.join(".agent/findings.md")).expect("findings"); + assert!(findings.contains("pass: 2")); + assert!(findings.contains("status: passed")); + let game_html = fs::read_to_string(root.join("game/index.html")).expect("game html"); + assert!(game_html.contains("MOCK_UNIQUE_MECHANIC:moon-kitchen-reflect")); + let agent_log = fs::read_to_string(root.join(".agent/logs/agent.log")).expect("agent log"); + assert!(agent_log.contains("agent.loop passes=2")); + assert!(agent_log.contains("Planner -> .agent/spec.md")); + assert!(agent_log.contains("组内角色 briefs -> .agent/passes/pass-*/groups//*.md")); + assert!(agent_log.contains("专业组汇总 -> .agent/passes/pass-*/groups/*.md")); + assert!(agent_log.contains("Generator -> .agent/passes/pass-*/draft.json")); + assert!(agent_log.contains("专业组 handoffs -> .agent/passes/pass-*/handoff.md")); + assert!(agent_log.contains("Evaluator -> .agent/findings.md")); + let session_memory = + fs::read_to_string(root.join("memory/session.md")).expect("session memory"); + assert!(session_memory.contains("## Agent Run game-generate-draft-")); + assert!(session_memory.contains("状态:passed;loop:2/3;下一步:preview-playtest")); + assert!(session_memory.contains("activeTasks:")); + assert!(session_memory.contains("carryOverTasks:")); + assert!(session_memory.contains("game/index.html")); + let project_memory = + fs::read_to_string(root.join("memory/project.md")).expect("project memory"); + assert!(project_memory.contains("## 最近稳定原型")); + assert!(project_memory.contains("月光弹幕厨房")); + assert!(project_memory.contains("通过轮次:2/3")); + assert!(project_memory.contains("可试玩入口:game/index.html")); + let first_design_director = + fs::read_to_string(root.join(".agent/passes/pass-1/groups/design/director.md")) + .expect("design director brief"); + assert!(first_design_director.contains("策划组 / Director")); + assert!(first_design_director.contains("pass 1 策划组 Director brief")); + let first_design_brief = + fs::read_to_string(root.join(".agent/passes/pass-1/groups/design.md")) + .expect("design brief"); + assert!(first_design_brief.contains("策划组 / Director")); + assert!(first_design_brief.contains("策划组 / Gameplay")); + let first_handoff = + fs::read_to_string(root.join(".agent/passes/pass-1/handoff.md")).expect("handoff"); + assert!(first_handoff.contains("策划组 / Gameplay")); + assert!(first_handoff.contains("程序组 / Code")); + assert!(first_handoff.contains("outputs: game/game_design.md")); + assert!(first_handoff.contains("next: 交给数值和程序组")); + let first_agenda = + fs::read_to_string(root.join(".agent/passes/pass-1/agenda.md")).expect("agenda 1"); + assert!(first_agenda.contains("mode: initial")); + assert!(first_agenda.contains("activeTasks: design-director")); + assert!(first_agenda.contains("wave 1: design-director")); + assert!(first_agenda.contains("wave 11: publish-package")); + let first_task_graph: Value = serde_json::from_str( + &fs::read_to_string(root.join(".agent/passes/pass-1/task-graph.json")) + .expect("task graph 1"), + ) + .expect("task graph json 1"); + assert_eq!( + first_task_graph["schemaVersion"], + "game-creator-agent-pass-task-graph.v1" + ); + assert_eq!(first_task_graph["dependencyWaves"][0][0], "design-director"); + let second_agenda = + fs::read_to_string(root.join(".agent/passes/pass-2/agenda.md")).expect("agenda 2"); + assert!(second_agenda.contains("mode: repair")); + assert!(second_agenda.contains("activeTasks: code-director")); + assert!(second_agenda.contains("carriedTasks: design-director")); + let second_task_graph: Value = serde_json::from_str( + &fs::read_to_string(root.join(".agent/passes/pass-2/task-graph.json")) + .expect("task graph 2"), + ) + .expect("task graph json 2"); + assert!(second_task_graph["dependencyWaves"] + .as_array() + .unwrap() + .iter() + .any(|wave| wave + .as_array() + .unwrap() + .iter() + .any(|task_id| task_id == "code-prototype"))); + assert_eq!( + second_task_graph["repairRoutes"][0]["reason"], + "code-runtime+dependency-impact" + ); + assert!(second_task_graph["repairRoutes"][0]["taskIds"] + .as_array() + .unwrap() + .iter() + .any(|task_id| task_id == "preview-readiness")); + assert!(second_task_graph["repairRoutes"][0]["taskIds"] + .as_array() + .unwrap() + .iter() + .any(|task_id| task_id == "publish-package")); + let trace: Value = + serde_json::from_str(&fs::read_to_string(root.join(".agent/run.latest.json")).unwrap()) + .expect("run trace json"); + assert_eq!( + trace["schemaVersion"], + GAME_CREATION_AGENT_RUN_SCHEMA_VERSION + ); + assert_eq!(trace["status"], "passed"); + assert_eq!(trace["passes"], 2); + assert_eq!( + trace["maxPasses"], + serde_json::json!(GAME_CREATOR_AGENT_LOOP_MAX_PASSES) + ); + assert_eq!( + trace["maxToolCalls"], + serde_json::json!(GAME_CREATOR_AGENT_TOOL_CALL_MAX) + ); + let expected_tool_call_count: usize = trace["steps"] + .as_array() + .unwrap() + .iter() + .map(|step| step["toolCalls"].as_array().unwrap().len()) + .sum(); + assert_eq!( + trace["toolCallCount"], + serde_json::json!(expected_tool_call_count) + ); + assert_eq!(trace["stopReason"], "evaluator-passed"); + assert_eq!(trace["coordination"], "filesystem"); + assert_eq!(trace["nextStep"], "preview-playtest"); + assert_eq!(trace["taskGraph"]["goal"], "做一个月光厨房弹幕游戏"); + assert!(trace["taskGraph"]["activeTaskIds"] + .as_array() + .unwrap() + .iter() + .any(|task_id| task_id == "code-director")); + assert!(trace["taskGraph"]["carriedTaskIds"] + .as_array() + .unwrap() + .iter() + .any(|task_id| task_id == "design-director")); + assert!(trace["taskGraph"]["repairFocus"] + .as_array() + .unwrap() + .iter() + .any(|issue| issue + .as_str() + .is_some_and(|issue| issue.contains("输入监听")))); + assert!(trace["taskGraph"]["repairRoutes"] + .as_array() + .unwrap() + .iter() + .any(|route| route["reason"] == "code-runtime+dependency-impact" + && route["taskIds"] + .as_array() + .unwrap() + .iter() + .any(|task_id| task_id == "code-prototype"))); + assert!(trace["taskGraph"]["tasks"] + .as_array() + .unwrap() + .iter() + .any(|task| task["id"] == "preview-readiness" && task["status"] == "completed")); + assert!(trace["taskGraph"]["tasks"] + .as_array() + .unwrap() + .iter() + .any(|task| task["id"] == "preview-playtest" + && task["status"] == "waiting-for-confirmation")); + assert_eq!(trace["passPlans"].as_array().unwrap().len(), 2); + assert_eq!(trace["passPlans"][0]["mode"], "initial"); + assert_eq!( + trace["passPlans"][0]["summary"], + first_task_graph["summary"] + ); + assert_eq!( + trace["passPlans"][0]["dependencyWaves"][0][0], + "design-director" + ); + assert_eq!(trace["passPlans"][1]["mode"], "repair"); + assert!(trace["passPlans"][1]["activeTaskIds"] + .as_array() + .unwrap() + .iter() + .any(|task_id| task_id == "code-prototype")); + assert!(trace["passPlans"][1]["activeTaskIds"] + .as_array() + .unwrap() + .iter() + .any(|task_id| task_id == "publish-package")); + assert!(trace["passPlans"][1]["carriedTaskIds"] + .as_array() + .unwrap() + .iter() + .any(|task_id| task_id == "design-director")); + assert!(trace["passPlans"][1]["dependencyWaves"] + .as_array() + .unwrap() + .iter() + .any(|wave| wave + .as_array() + .unwrap() + .iter() + .any(|task_id| task_id == "code-prototype"))); + let steps = trace["steps"].as_array().unwrap(); + assert_eq!(steps.len(), 64); + assert_eq!(steps[0]["agent"], "Planner"); + assert_eq!(steps[0]["phase"], "planning"); + assert_eq!(steps[0]["taskId"], "design-director"); + assert_eq!(steps[0]["toolCalls"][0]["toolId"], "llm.chat.planner"); + assert!(steps[0]["inputPaths"] + .as_array() + .unwrap() + .iter() + .any(|path| path == ".agent/manifest.json")); + assert!(steps.iter().any(|step| step["agent"] == "Orchestrator" + && step["pass"].as_u64() == Some(1) + && step["toolCalls"][0]["toolId"] == "agent.task_graph.plan_pass" + && step["outputPaths"][0] == ".agent/passes/pass-1/agenda.md")); + assert!(steps.iter().any(|step| step["agent"] == "Orchestrator" + && step["pass"].as_u64() == Some(2) + && step["summary"] + .as_str() + .is_some_and(|summary| summary.contains("carry-over")))); + assert!(steps.iter().any(|step| step["agent"] == "策划组 / Director" + && step["phase"] == "role-brief" + && step["taskId"] == "design-director" + && step["group"] == "design" + && step["role"] == "Director" + && step["toolCalls"][0]["toolId"] == "llm.chat.group.design.director" + && step["inputPaths"] + .as_array() + .unwrap() + .iter() + .any(|path| path == ".agent/passes/pass-1/agenda.md") + && step["outputPaths"][0] == ".agent/passes/pass-1/groups/design/director.md")); + assert!(steps.iter().any(|step| step["agent"] == "策划组 / Director" + && step["pass"].as_u64() == Some(2) + && step["status"] == "carried-over" + && step["phase"] == "role-brief" + && step["toolCalls"][0]["toolId"] == "agent.task_graph.carryover.design.director" + && step["outputPaths"][0] == ".agent/passes/pass-2/groups/design/director.md")); + assert!(steps.iter().any(|step| step["agent"] == "程序组 / Director" + && step["pass"].as_u64() == Some(2) + && step["status"] == "completed" + && step["toolCalls"][0]["toolId"] == "llm.chat.group.code.director")); + assert!(steps.iter().any(|step| step["agent"] == "运营组 / Publish" + && step["pass"].as_u64() == Some(2) + && step["status"] == "completed" + && step["toolCalls"][0]["toolId"] == "llm.chat.group.publishing.publish")); + assert!(steps + .iter() + .any(|step| step["agent"] == "策划组 / GroupCoordinator" + && step["toolCalls"][0]["toolId"] == "agent.group.aggregate.design" + && step["outputPaths"][0] == ".agent/passes/pass-1/groups/design.md")); + assert!(steps.iter().any(|step| { + step["agent"] == "Generator" + && step["toolCalls"][0]["toolId"] == "llm.chat.generator" + && step["inputPaths"] + .as_array() + .unwrap() + .iter() + .any(|path| path == ".agent/manifest.json") + && step["inputPaths"] + .as_array() + .unwrap() + .iter() + .any(|path| path == ".agent/passes/pass-1/groups/design.md") + })); + assert!(steps.iter().any(|step| { + step["agent"] == "Generator" + && step["pass"].as_u64() == Some(2) + && step["inputPaths"] + .as_array() + .unwrap() + .iter() + .any(|path| path == ".agent/passes/pass-2/agenda.md") + })); + assert!(steps.iter().any(|step| step["agent"] == "策划组 / Gameplay" + && step["phase"] == "handoff" + && step["taskId"] == "design-foundation" + && step["toolCalls"][0]["toolId"] == "agent.handoff.design" + && step["summary"] == "定义反弹循环")); + assert!(steps + .iter() + .any(|step| step["agent"] == "数值组 / Difficulty")); + assert!(steps.iter().any(|step| step["agent"] == "美术组 / Asset")); + assert!(steps.iter().any(|step| step["agent"] == "美术组 / Asset" + && step["toolCalls"] + .as_array() + .unwrap() + .iter() + .any( + |tool_call| tool_call["toolId"] == "agent.tool.suggest.canvas.project_sync" + && tool_call["status"] == "suggested" + ))); + assert!(steps.iter().any(|step| step["agent"] == "音乐组 / SFX")); + assert!(steps.iter().any(|step| step["agent"] == "音乐组 / SFX" + && step["toolCalls"] + .as_array() + .unwrap() + .iter() + .any( + |tool_call| tool_call["toolId"] == "agent.tool.suggest.canvas.project_sync" + && tool_call["summary"] + .as_str() + .is_some_and(|summary| summary.contains("/sync-canvas-project")) + ))); + assert!(steps.iter().any(|step| step["agent"] == "程序组 / Code")); + assert!(steps.iter().any(|step| step["agent"] == "运营组 / Publish")); + assert!(steps + .iter() + .any(|step| step["agent"] == "Evaluator" && step["status"] == "needs-revision")); + assert!(steps + .iter() + .any(|step| step["agent"] == "Evaluator" && step["status"] == "passed")); + let playtest = steps + .iter() + .find(|step| step["agent"] == "Playtest") + .expect("playtest step"); + assert_eq!(playtest["toolCalls"][0]["toolId"], "game.static_smoke"); + let artifact_writer = steps + .iter() + .find(|step| step["agent"] == "ArtifactWriter") + .expect("artifact writer step"); + assert_eq!( + artifact_writer["toolCalls"][0]["toolId"], + "file.write.local_artifacts" + ); + assert!(artifact_writer["outputPaths"] + .as_array() + .unwrap() + .iter() + .any(|path| path == "game/index.html")); + let artifacts = trace["artifacts"].as_array().unwrap(); + let game_artifact = artifacts + .iter() + .find(|artifact| artifact["path"] == "game/index.html") + .expect("game artifact"); + assert!(game_artifact["sizeBytes"].as_u64().unwrap() > 0); + assert!(game_artifact["checksum"] + .as_str() + .unwrap() + .starts_with("fnv1a64:")); + assert!(artifacts + .iter() + .any(|artifact| artifact["path"] == ".agent/passes/pass-1/game.html")); + assert!(artifacts + .iter() + .any(|artifact| artifact["path"] == ".agent/passes/pass-1/agenda.md")); + assert!(artifacts + .iter() + .any(|artifact| artifact["path"] == ".agent/passes/pass-1/task-graph.json")); + assert!(artifacts + .iter() + .any(|artifact| artifact["path"] == ".agent/passes/pass-2/agenda.md")); + assert!(artifacts + .iter() + .any(|artifact| artifact["path"] == ".agent/passes/pass-2/task-graph.json")); + assert!(artifacts + .iter() + .any(|artifact| artifact["path"] == ".agent/passes/pass-1/groups/design/director.md")); + assert!(artifacts + .iter() + .any(|artifact| artifact["path"] == ".agent/passes/pass-1/groups/design.md")); + assert!(artifacts + .iter() + .any(|artifact| artifact["path"] == ".agent/passes/pass-2/handoff.md")); + let run_history_path = root.join(format!( + ".agent/runs/{}.json", + trace["runId"].as_str().unwrap() + )); + let run_history: Value = + serde_json::from_str(&fs::read_to_string(run_history_path).unwrap()) + .expect("run history json"); + assert_eq!(run_history["runId"], trace["runId"]); + assert_eq!(run_history["status"], "passed"); + assert_eq!(run_history["stopReason"], "evaluator-passed"); + assert_eq!(run_history["steps"].as_array().unwrap().len(), 64); + let manifest: Value = + serde_json::from_str(&fs::read_to_string(root.join(".agent/manifest.json")).unwrap()) + .expect("manifest json"); + assert!(manifest["commandRuns"] + .as_array() + .unwrap() + .iter() + .any(|run| run["commandId"] == "game.static_smoke")); + + fs::remove_dir_all(root).ok(); + } + + #[tokio::test] + async fn generate_local_game_draft_fails_after_max_passes_without_final_artifacts() { + let _env_guard = TEST_ENV_LOCK.lock().expect("test env lock"); + let root = unique_project_path(); + let mut invalid_draft = fake_llm_game_draft(); + invalid_draft.game_html = r#" + + + +

目标:点亮厨房。胜利 / 失败后按 R 重开。

+ + +"# + .to_string(); + let invalid_draft_json = serde_json::to_string(&invalid_draft).expect("invalid draft json"); + let mut responses = vec!["## 核心循环\n\n点亮厨房,但必须通过 Evaluator。".to_string()]; + responses.extend(fake_group_brief_responses(1)); + responses.push(invalid_draft_json.clone()); + responses.extend(fake_group_brief_responses_for_group(2, "code")); + responses.extend(fake_group_brief_responses_for_group(2, "publishing")); + responses.push(invalid_draft_json.clone()); + responses.extend(fake_group_brief_responses_for_group(3, "code")); + responses.extend(fake_group_brief_responses_for_group(3, "publishing")); + responses.push(invalid_draft_json); + let base_url = spawn_mock_llm_server_responses(responses); + let previous_api_key = std::env::var("GENARRATIVE_GAME_CREATOR_LLM_API_KEY").ok(); + let previous_base_url = std::env::var("GENARRATIVE_GAME_CREATOR_LLM_BASE_URL").ok(); + let previous_model = std::env::var("GENARRATIVE_GAME_CREATOR_LLM_MODEL").ok(); + std::env::set_var("GENARRATIVE_GAME_CREATOR_LLM_API_KEY", "test-key"); + std::env::set_var("GENARRATIVE_GAME_CREATOR_LLM_BASE_URL", base_url); + std::env::set_var("GENARRATIVE_GAME_CREATOR_LLM_MODEL", "mock-game-model"); + + let error = generate_local_game_draft_at(&root, "做一个会失败三轮的厨房游戏") + .await + .expect_err("max-pass failure should bubble out"); + + restore_env("GENARRATIVE_GAME_CREATOR_LLM_API_KEY", previous_api_key); + restore_env("GENARRATIVE_GAME_CREATOR_LLM_BASE_URL", previous_base_url); + restore_env("GENARRATIVE_GAME_CREATOR_LLM_MODEL", previous_model); + assert!(error.contains("已重试")); + assert!(error.contains(&GAME_CREATOR_AGENT_LOOP_MAX_PASSES.to_string())); + assert!(!root.join("memory/session.md").exists()); + assert!(!root.join("memory/project.md").exists()); + assert!(!root.join("game/game_design.md").exists()); + assert!(!root.join("game/balance.json").exists()); + assert!(!root.join("assets/manifest.art.json").exists()); + assert!(!root.join("assets/manifest.audio.json").exists()); + assert!(!root.join("exports/README.md").exists()); + let game_html = + fs::read_to_string(root.join("game/index.html")).expect("default game html"); + assert!(!game_html.contains("MOCK_UNIQUE_MECHANIC:moon-kitchen-reflect")); + assert!(!game_html.contains("点亮厨房")); + let trace: Value = + serde_json::from_str(&fs::read_to_string(root.join(".agent/run.latest.json")).unwrap()) + .expect("run trace json"); + assert_eq!(trace["status"], "failed"); + assert_eq!( + trace["passes"], + serde_json::json!(GAME_CREATOR_AGENT_LOOP_MAX_PASSES) + ); + assert_eq!(trace["stopReason"], "max-passes-exhausted"); + assert_eq!(trace["nextStep"], "inspect-error"); + assert!(trace["error"] + .as_str() + .is_some_and(|message| message.contains("Evaluator"))); + assert!( + trace["passPlans"].as_array().unwrap().len() + == usize::from(GAME_CREATOR_AGENT_LOOP_MAX_PASSES) + ); + assert!(trace["artifacts"] + .as_array() + .unwrap() + .iter() + .any(|artifact| artifact["path"] == ".agent/passes/pass-3/game.html")); + assert!(!trace["steps"] + .as_array() + .unwrap() + .iter() + .any(|step| step["agent"] == "ArtifactWriter")); + assert!(!trace["steps"] + .as_array() + .unwrap() + .iter() + .any(|step| step["agent"] == "Playtest")); + let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db"); + let records = agent_db + .lines() + .map(|line| serde_json::from_str::(line).expect("agent db record")) + .collect::>(); + assert_eq!(records.len(), 1); + assert_eq!(records[0]["recordType"], "project.init"); + + fs::remove_dir_all(root).ok(); + } + + #[test] + fn canvas_sync_suggestion_is_media_type_aware() { + let art_group = GAME_CREATOR_AGENT_GROUP_DEFINITIONS + .iter() + .find(|definition| definition.id == "art") + .copied() + .expect("art group"); + let audio_group = GAME_CREATOR_AGENT_GROUP_DEFINITIONS + .iter() + .find(|definition| definition.id == "audio") + .copied() + .expect("audio group"); + let art_role = ART_AGENT_ROLES + .iter() + .find(|role| role.id == "asset") + .copied() + .expect("art asset role"); + let audio_role = AUDIO_AGENT_ROLES + .iter() + .find(|role| role.id == "sfx") + .copied() + .expect("audio sfx role"); + let art_brief = AgentRoleBrief { + group_definition: art_group, + role_definition: art_role, + markdown: String::new(), + relative_path: ".agent/passes/pass-1/groups/art/asset.md".to_string(), + status: "completed".to_string(), + tool_id: art_role.tool_id.to_string(), + summary: String::new(), + }; + let audio_brief = AgentRoleBrief { + group_definition: audio_group, + role_definition: audio_role, + markdown: String::new(), + relative_path: ".agent/passes/pass-1/groups/audio/sfx.md".to_string(), + status: "completed".to_string(), + tool_id: audio_role.tool_id.to_string(), + summary: String::new(), + }; + let input_paths = vec![".agent/manifest.json".to_string()]; + let image_canvas_assets = vec!["image/png".to_string()]; + let audio_canvas_assets = vec!["audio/wav".to_string()]; + + assert!( + suggested_canvas_tool_call(&art_brief, &input_paths, &image_canvas_assets).is_none() + ); + assert!( + suggested_canvas_tool_call(&audio_brief, &input_paths, &image_canvas_assets).is_some() + ); + assert!( + suggested_canvas_tool_call(&audio_brief, &input_paths, &audio_canvas_assets).is_none() + ); + assert!( + suggested_canvas_tool_call(&art_brief, &input_paths, &audio_canvas_assets).is_some() + ); + } + + #[test] + fn init_local_game_project_creates_manifest_and_dirs() { + let root = unique_project_path(); + + let result = + init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init"); + + assert_eq!(result.project_path, root.to_string_lossy().into_owned()); + assert!(root.join("game").is_dir()); + assert!(root.join("assets").is_dir()); + assert!(root.join("memory").is_dir()); + assert!(root.join("exports").is_dir()); + assert!(root.join(".agent/logs").is_dir()); + assert!(root.join(".agent/agent.db").is_file()); + assert!(root.join("game/index.html").is_file()); + let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db"); + let first_record: Value = + serde_json::from_str(agent_db.lines().next().unwrap()).expect("agent db record"); + assert_eq!( + first_record["schemaVersion"], + GAME_CREATOR_AGENT_DB_SCHEMA_VERSION + ); + assert_eq!(first_record["recordType"], "project.init"); + assert_eq!(first_record["projectId"], "project-1"); + + let manifest: Value = + serde_json::from_str(&fs::read_to_string(root.join(".agent/manifest.json")).unwrap()) + .expect("manifest json"); + assert_eq!(manifest["projectId"], "project-1"); + assert_eq!(manifest["name"], "像素动作原型"); + assert_eq!(manifest["assets"].as_array().unwrap().len(), 0); + assert_eq!(manifest["tasks"].as_array().unwrap().len(), 15); + assert_eq!(manifest["tasks"][0]["id"], "design-director"); + + fs::remove_dir_all(root).ok(); + } + + #[test] + fn init_local_game_project_requires_absolute_path() { + let error = init_local_game_project_at(Path::new("relative-game"), "project-1", "demo") + .expect_err("relative path should fail"); + + assert!(error.contains("绝对路径")); + } + + #[test] + fn generate_local_game_draft_writes_memory_design_and_game() { + let root = unique_project_path(); + let draft = fake_llm_game_draft(); + let result = write_local_game_draft_at(&root, "像素风横版动作 + +"# + .to_string(); + + let error = validate_llm_game_draft("像素厨房弹幕", &draft) + .expect_err("missing goal and terminal states should fail"); + + assert!(error.contains("必须展示明确目标")); + } + + #[test] + fn validate_llm_game_draft_accepts_win_condition_as_goal() { + let mut draft = fake_llm_game_draft(); + draft.game_html = r#" + + + +

三连击获胜,生命耗尽失败,按 R 重开。

+ + +"# + .to_string(); + + validate_llm_game_draft("像素厨房弹幕", &draft) + .expect("win condition should count as a clear goal"); + } + + #[test] + fn validate_llm_game_draft_rejects_empty_input_handler() { + let mut draft = fake_llm_game_draft(); + draft.game_html = r#" + + + +

目标:点亮厨房。胜利 / 失败后按 R 重开。

+ + +"# + .to_string(); + + let error = validate_llm_game_draft("像素厨房弹幕", &draft) + .expect_err("empty input listener should fail"); + + assert!(error.contains("输入监听不能是空实现")); + } + + #[test] + fn validate_llm_game_draft_rejects_fixed_placeholder_template_terms() { + let mut draft = fake_llm_game_draft(); + draft.game_html = draft.game_html.replace("月光弹幕厨房", "星核传送门"); + + let error = validate_llm_game_draft("像素厨房弹幕", &draft) + .expect_err("fixed placeholder template should fail"); + + assert!(error.contains("占位或固定模板")); + } + + #[test] + fn validate_llm_game_draft_requires_canvas_drawing() { + let mut draft = fake_llm_game_draft(); + draft.game_html = r#" + + + +

目标:点亮厨房。胜利 / 失败后按 R 重开。

+ + +"# + .to_string(); + + let error = + validate_llm_game_draft("像素厨房弹幕", &draft).expect_err("blank canvas should fail"); + + assert!(error.contains("canvas 上绘制画面")); + } + + #[test] + fn validate_llm_game_draft_rejects_forbidden_runtime_apis() { + let mut draft = fake_llm_game_draft(); + draft.game_html = draft + .game_html + .replace("const marker =", "fetch('/secret');\n const marker ="); + + let error = validate_llm_game_draft("像素厨房弹幕", &draft) + .expect_err("fetch should fail validation"); + + assert!(error.contains("fetch(")); + } + + #[test] + fn evaluator_findings_include_structured_repair_routes() { + let findings = + render_evaluator_findings(1, &["LLM 草案 gameHtml 必须包含游戏主循环和输入监听"]); + + assert!(findings.contains("## Repair Routes")); + assert!(findings.contains("\"taskIds\"")); + assert!(findings.contains("\"code-prototype\"")); + + let graph = build_game_creation_seed_task_graph("像素厨房弹幕").expect("task graph"); + let plan = plan_game_creation_agent_pass(&graph, 2, &findings); + assert_eq!(plan.mode, "repair"); + assert!(plan.active_task_ids.contains(&"code-prototype".to_string())); + assert!(plan + .active_task_ids + .contains(&"publish-package".to_string())); + assert_eq!( + plan.repair_routes[0].reason, + "code-runtime+dependency-impact" + ); + } + + #[test] + fn upload_local_asset_writes_file_and_manifest_entry() { + let root = unique_project_path(); + let result = upload_local_asset_at(&root, "../角色.png", "image/png", b"fake-png") + .expect("asset upload"); + + assert_eq!(fs::read(&result.absolute_path).unwrap(), b"fake-png"); + assert!(result.local_path.starts_with("assets/uploads/upload-")); + assert!(result.local_path.ends_with("_角色.png")); + + let manifest: Value = + serde_json::from_str(&fs::read_to_string(root.join(".agent/manifest.json")).unwrap()) + .expect("manifest json"); + assert_eq!(manifest["assets"][0]["id"], result.id); + assert_eq!(manifest["assets"][0]["mediaType"], "image/png"); + assert_eq!(manifest["assets"][0]["source"]["kind"], "uploaded"); + assert_eq!(manifest["assets"][0]["localPath"], result.local_path); + let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db"); + assert!(agent_db.lines().any(|line| { + let record: Value = serde_json::from_str(line).expect("agent db record"); + record["recordType"] == "asset.register" + && record["assetId"] == result.id + && record["localPath"] == result.local_path + && record["source"]["kind"] == "uploaded" + })); + + upload_local_asset_at(&root, "sound.wav", "audio/wav", b"fake-wav").expect("asset upload"); + let manifest: Value = + serde_json::from_str(&fs::read_to_string(root.join(".agent/manifest.json")).unwrap()) + .expect("manifest json"); + assert_eq!(manifest["assets"].as_array().unwrap().len(), 2); + + fs::remove_dir_all(root).ok(); + } + + #[test] + fn local_asset_prompt_context_summarizes_uploaded_and_canvas_assets() { + let root = unique_project_path(); + let uploaded = upload_local_asset_at(&root, "../角色.png", "image/png", b"fake-png") + .expect("asset upload"); + write_local_project_file_at(&root, "assets/canvas-hero.png", "fake-image") + .expect("canvas asset file"); + import_canvas_asset_at( + &root, + "assets/canvas-hero.png", + "character", + "image/png", + "canvas-project-1", + Some("resource-1".to_string()), + Some("asset-object-1".to_string()), + Some("task-1".to_string()), + None, + Some("gpt-image-2".to_string()), + ) + .expect("canvas asset import"); + + let context = render_local_asset_prompt_context(&root).expect("asset context"); + assert!(context.contains("# 本地项目资产")); + assert!(context.contains(&uploaded.local_path)); + assert!(context.contains("source=uploaded")); + assert!(context.contains("assets/canvas-hero.png")); + assert!(context.contains("source=canvas")); + assert!(context.contains("canvasProjectId=canvas-project-1")); + assert!(context.contains("resourceId=resource-1")); + assert!(context.contains("assetObjectId=asset-object-1")); + + let prompt_context = append_prompt_context(&context, &"长期记忆\n".repeat(400)); + let truncated_context = truncate_prompt_context(&prompt_context); + assert!(truncated_context.contains("# 本地项目资产")); + assert!(truncated_context.contains(&uploaded.local_path)); + + fs::remove_dir_all(root).ok(); + } + + #[test] + fn upload_local_asset_rejects_empty_file() { + let root = unique_project_path(); + let error = + upload_local_asset_at(&root, "empty.txt", "text/plain", b"").expect_err("empty file"); + + assert!(error.contains("不能为空")); + } + + #[test] + fn register_local_asset_records_existing_asset_with_canvas_source() { + let root = unique_project_path(); + init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init"); + write_local_project_file_at(&root, "assets/hero.png", "fake-image").expect("asset file"); + + let result = register_local_asset_at( + &root, + "assets/hero.png", + "character", + "image/png", + "canvas", + GameCreationAppAssetSource { + kind: GameCreationAppAssetSourceKind::Canvas, + canvas_project_id: Some("canvas-project-1".to_string()), + resource_id: Some("resource-1".to_string()), + asset_object_id: Some("asset-object-1".to_string()), + task_id: Some("task-1".to_string()), + prompt: Some("像素主角".to_string()), + model: Some("image-model".to_string()), + }, + ) + .expect("asset register"); + + let manifest: Value = + serde_json::from_str(&fs::read_to_string(root.join(".agent/manifest.json")).unwrap()) + .expect("manifest json"); + assert_eq!(manifest["assets"][0]["id"], result.id); + assert_eq!(manifest["assets"][0]["kind"], "character"); + assert_eq!(manifest["assets"][0]["localPath"], "assets/hero.png"); + assert_eq!(manifest["assets"][0]["source"]["kind"], "canvas"); + assert_eq!( + manifest["assets"][0]["source"]["canvasProjectId"], + "canvas-project-1" + ); + + let updated = register_local_asset_at( + &root, + "assets/hero.png", + "ui", + "image/png", + "generated", + GameCreationAppAssetSource { + kind: GameCreationAppAssetSourceKind::Generated, + canvas_project_id: None, + resource_id: None, + asset_object_id: None, + task_id: None, + prompt: None, + model: None, + }, + ) + .expect("asset update"); + assert_eq!(updated.id, result.id); + let manifest: Value = + serde_json::from_str(&fs::read_to_string(root.join(".agent/manifest.json")).unwrap()) + .expect("manifest json"); + assert_eq!(manifest["assets"].as_array().unwrap().len(), 1); + assert_eq!(manifest["assets"][0]["kind"], "ui"); + assert_eq!(manifest["assets"][0]["source"]["kind"], "generated"); + + fs::remove_dir_all(root).ok(); + } + + #[test] + fn register_local_asset_rejects_missing_or_unsafe_path() { + let root = unique_project_path(); + let source = GameCreationAppAssetSource { + kind: GameCreationAppAssetSourceKind::Generated, + canvas_project_id: None, + resource_id: None, + asset_object_id: None, + task_id: None, + prompt: None, + model: None, + }; + + assert!(register_local_asset_at( + &root, + "../outside.png", + "asset", + "image/png", + "generated", + source.clone() + ) + .is_err()); + assert!(register_local_asset_at( + &root, + "assets/missing.png", + "asset", + "image/png", + "generated", + source + ) + .is_err()); + } + + #[test] + fn import_canvas_asset_registers_canvas_source_metadata() { + let root = unique_project_path(); + init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init"); + write_local_project_file_at(&root, "assets/canvas-hero.png", "fake-image") + .expect("canvas asset file"); + + let result = import_canvas_asset_at( + &root, + "assets/canvas-hero.png", + "character", + "image/png", + "canvas-project-1", + Some("resource-1".to_string()), + Some("asset-object-1".to_string()), + Some("task-1".to_string()), + Some("像素主角".to_string()), + Some("image-model".to_string()), + ) + .expect("canvas import"); + + let manifest: Value = + serde_json::from_str(&fs::read_to_string(root.join(".agent/manifest.json")).unwrap()) + .expect("manifest json"); + assert_eq!(manifest["assets"][0]["id"], result.id); + assert_eq!(manifest["assets"][0]["source"]["kind"], "canvas"); + assert_eq!( + manifest["assets"][0]["source"]["canvasProjectId"], + "canvas-project-1" + ); + assert_eq!(manifest["assets"][0]["source"]["resourceId"], "resource-1"); + assert_eq!( + manifest["assets"][0]["source"]["assetObjectId"], + "asset-object-1" + ); + let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db"); + assert!(agent_db.lines().any(|line| { + let record: Value = serde_json::from_str(line).expect("agent db record"); + record["recordType"] == "asset.register" + && record["assetId"] == result.id + && record["source"]["kind"] == "canvas" + && record["source"]["canvasProjectId"] == "canvas-project-1" + })); + + fs::remove_dir_all(root).ok(); + } + + #[test] + fn import_canvas_asset_requires_traceable_canvas_ids() { + let root = unique_project_path(); + write_local_project_file_at(&root, "assets/canvas-hero.png", "fake-image") + .expect("canvas asset file"); + + let missing_project = import_canvas_asset_at( + &root, + "assets/canvas-hero.png", + "character", + "image/png", + "", + Some("resource-1".to_string()), + None, + None, + None, + None, + ) + .expect_err("missing canvas project should fail"); + assert!(missing_project.contains("画板项目")); + + let missing_asset = import_canvas_asset_at( + &root, + "assets/canvas-hero.png", + "character", + "image/png", + "canvas-project-1", + None, + None, + None, + None, + None, + ) + .expect_err("missing canvas asset ids should fail"); + assert!(missing_asset.contains("至少需要一个")); + + fs::remove_dir_all(root).ok(); + } + + #[test] + fn import_canvas_export_zip_copies_files_and_registers_assets() { + let root = unique_project_path(); + let zip_path = root.with_extension("zip"); + write_test_canvas_export_zip(&zip_path); + + let result = import_canvas_export_at(&root, &zip_path, "canvas-project-1") + .expect("canvas export import"); + + assert_eq!(result.imported_count, 2); + assert!(result.import_root.starts_with("assets/canvas-imports/")); + assert!(root.join(&result.metadata_path).is_file()); + assert!(root + .join(&result.import_root) + .join("images/001-月光主角.png") + .is_file()); + assert!(root + .join(&result.import_root) + .join("media/002-玻璃月光.mp3") + .is_file()); + assert!(result + .assets + .iter() + .any(|asset| asset.local_path.ends_with("images/001-月光主角.png"))); + assert!(result + .assets + .iter() + .any(|asset| asset.local_path.ends_with("media/002-玻璃月光.mp3"))); + + let manifest: Value = + serde_json::from_str(&fs::read_to_string(root.join(".agent/manifest.json")).unwrap()) + .expect("manifest json"); + let assets = manifest["assets"].as_array().unwrap(); + assert_eq!(assets.len(), 2); + assert!(assets.iter().any(|asset| { + asset["kind"] == "character" + && asset["mediaType"] == "image/png" + && asset["source"]["kind"] == "canvas" + && asset["source"]["canvasProjectId"] == "canvas-project-1" + && asset["source"]["assetObjectId"] == "asset-object-1" + && asset["source"]["taskId"] == "42" + && asset["source"]["model"] == "gpt-image-2" + })); + assert!(assets.iter().any(|asset| { + asset["kind"] == "audio" + && asset["mediaType"] == "audio/mpeg" + && asset["source"]["assetObjectId"] + .as_str() + .is_some_and(|value| value.starts_with("canvas-export:media/")) + })); + + let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db"); + let records = agent_db + .lines() + .map(|line| serde_json::from_str::(line).expect("agent db record")) + .collect::>(); + assert!(records.iter().any(|record| { + record["recordType"] == "canvas.export_import" + && record["canvasProjectId"] == "canvas-project-1" + && record["projectTitle"] == "月光画布" + && record["importedCount"] == 2 + })); + assert!(!agent_db.contains(zip_path.to_string_lossy().as_ref())); + + fs::remove_file(zip_path).ok(); + fs::remove_dir_all(root).ok(); + } + + #[tokio::test] + async fn sync_canvas_project_assets_downloads_external_resources() { + let root = unique_project_path(); + let base_url = spawn_mock_external_canvas_api_server(); + + let result = sync_canvas_project_assets_at( + &root, + "canvas-project-1", + Some(base_url), + Some("test-editor-api-key".to_string()), + ) + .await + .expect("canvas project sync"); + + assert_eq!(result.canvas_project_id, "canvas-project-1"); + assert_eq!(result.imported_count, 1); + assert!(result.import_root.starts_with("assets/canvas-sync/")); + assert_eq!( + fs::read(&result.assets[0].absolute_path).unwrap(), + b"fake-png" + ); + + let manifest: Value = + serde_json::from_str(&fs::read_to_string(root.join(".agent/manifest.json")).unwrap()) + .expect("manifest json"); + assert_eq!(manifest["assets"][0]["kind"], "character"); + assert_eq!(manifest["assets"][0]["mediaType"], "image/png"); + assert_eq!(manifest["assets"][0]["source"]["kind"], "canvas"); + assert_eq!( + manifest["assets"][0]["source"]["canvasProjectId"], + "canvas-project-1" + ); + assert_eq!(manifest["assets"][0]["source"]["resourceId"], "resource-1"); + assert_eq!( + manifest["assets"][0]["source"]["assetObjectId"], + "asset-object-1" + ); + assert_eq!(manifest["assets"][0]["source"]["model"], "gpt-image-2"); + + let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db"); + assert!(agent_db.contains("\"recordType\":\"canvas.project_sync\"")); + assert!(!agent_db.contains("test-editor-api-key")); + + fs::remove_dir_all(root).ok(); + } + + #[test] + fn canvas_project_url_defaults_to_local_editor_route() { + let url = build_canvas_project_url(None, Some("canvas-project-1")).expect("canvas url"); + + assert_eq!( + url, + "http://127.0.0.1:3000/editor/canvas?projectid=canvas-project-1" + ); + } + + #[test] + fn canvas_project_url_normalizes_localhost_base() { + let url = build_canvas_project_url( + Some("http://localhost:3100/old/path?ignored=1#fragment"), + Some("项目 1"), + ) + .expect("canvas url"); + + assert_eq!( + url, + "http://localhost:3100/editor/canvas?projectid=%E9%A1%B9%E7%9B%AE+1" + ); + } + + #[test] + fn canvas_project_url_rejects_non_local_editor_base() { + assert!(build_canvas_project_url(Some("https://example.com"), Some("p")).is_err()); + assert!(build_canvas_project_url(Some("file:///tmp/editor"), Some("p")).is_err()); + assert!(build_canvas_project_url(Some("http://192.168.1.5:3000"), Some("p")).is_err()); + } + + #[test] + fn local_game_memory_can_read_write_and_delete_long_memory() { + let root = unique_project_path(); + + let missing = read_local_game_memory_at(&root, "long").expect("read missing memory"); + assert_eq!(missing.scope, "long"); + assert!(!missing.exists); + + let written = + write_local_game_memory_at(&root, "long", "# 项目长期记忆\n").expect("write memory"); + assert!(written.exists); + assert_eq!(written.content, "# 项目长期记忆\n"); + + let read = read_local_game_memory_at(&root, "project").expect("read memory"); + assert_eq!(read.scope, "long"); + assert_eq!(read.content, "# 项目长期记忆\n"); + + let deleted = delete_local_game_memory_at(&root, "long").expect("delete memory"); + assert!(!deleted.exists); + assert!(!root.join("memory/project.md").exists()); + + fs::remove_dir_all(root).ok(); + } + + #[test] + fn local_game_memory_rejects_unknown_scope() { + let root = unique_project_path(); + let error = + read_local_game_memory_at(&root, "notes").expect_err("unknown memory scope fails"); + + assert!(error.contains("short 或 long")); + } + + #[cfg(unix)] + #[test] + fn local_game_memory_rejects_symlinked_memory_dir() { + use std::os::unix::fs::symlink; + + let root = unique_project_path(); + let outside = unique_project_path(); + fs::create_dir_all(&root).expect("project dir"); + fs::create_dir_all(&outside).expect("outside dir"); + symlink(&outside, root.join("memory")).expect("memory symlink"); + + assert!(write_local_game_memory_at(&root, "long", "secret").is_err()); + assert!(!outside.join("project.md").exists()); + + fs::remove_dir_all(root).ok(); + fs::remove_dir_all(outside).ok(); + } + + #[test] + fn local_project_file_commands_read_write_list_and_delete_text_files() { + let root = unique_project_path(); + + let written = write_local_project_file_at(&root, "game/notes.txt", "hello") + .expect("write project file"); + assert_eq!(written.path, "game/notes.txt"); + assert!(!written.deleted); + + let read = read_local_project_file_at(&root, "game/notes.txt").expect("read project file"); + assert_eq!(read.content, "hello"); + + let listed = list_local_project_files_at(&root).expect("list project files"); + assert!(listed + .files + .iter() + .any(|file| file.path == "game/notes.txt" && file.kind == "file")); + + let deleted = + delete_local_project_file_at(&root, "game/notes.txt").expect("delete project file"); + assert!(deleted.deleted); + assert!(!root.join("game/notes.txt").exists()); + + fs::remove_dir_all(root).ok(); + } + + #[test] + fn local_project_file_commands_reject_unsafe_paths() { + let root = unique_project_path(); + + assert!(read_local_project_file_at(&root, "../secret.txt").is_err()); + assert!(read_local_project_file_at(&root, "/tmp/secret.txt").is_err()); + assert!(write_local_project_file_at(&root, "game\\secret.txt", "x").is_err()); + assert!(write_local_project_file_at(&root, "C:/secret.txt", "x").is_err()); + } + + #[test] + fn limited_local_command_runs_static_game_smoke_and_writes_log() { + let root = unique_project_path(); + init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init"); + fs::write( + root.join("game/index.html"), + fake_llm_game_draft().game_html, + ) + .expect("write playable game html"); + + let result = + run_limited_local_command_at(&root, "game.static_smoke").expect("static smoke"); + + assert_eq!(result.command_id, "game.static_smoke"); + assert_eq!(result.status, "completed"); + assert!(result.output.contains("game/index.html")); + let log = fs::read_to_string(root.join(".agent/logs/command.log")).expect("command log"); + assert!(log.contains("command.run_limited game.static_smoke")); + let manifest: Value = + serde_json::from_str(&fs::read_to_string(root.join(".agent/manifest.json")).unwrap()) + .expect("manifest json"); + assert_eq!(manifest["commandRuns"][0]["commandId"], "game.static_smoke"); + assert_eq!(manifest["commandRuns"][0]["status"], "completed"); + + fs::remove_dir_all(root).ok(); + } + + #[test] + fn local_permission_log_appends_to_command_log() { + let root = unique_project_path(); + init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init"); + + append_local_permission_log_at(&root, "permission.pending", "preview.start") + .expect("pending log"); + append_local_permission_log_at(&root, "permission.confirm", "preview.start") + .expect("confirm log"); + append_local_permission_log_at(&root, "permission.cancel", "memory.write") + .expect("cancel log"); + + let log = fs::read_to_string(root.join(".agent/logs/command.log")).expect("command log"); + assert!(log.contains("permission.pending preview.start")); + assert!(log.contains("permission.confirm preview.start")); + assert!(log.contains("permission.cancel memory.write")); + + fs::remove_dir_all(root).ok(); + } + + #[test] + fn local_permission_log_rejects_unknown_event_and_command() { + let root = unique_project_path(); + init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init"); + + let event_error = append_local_permission_log_at(&root, "permission.grant", "preview.start") + .expect_err("unknown event should fail"); + let command_error = + append_local_permission_log_at(&root, "permission.pending", "shell.exec") + .expect_err("unknown command should fail"); + + assert!(event_error.contains("不支持的权限日志事件")); + assert!(command_error.contains("不支持的内置命令")); + + fs::remove_dir_all(root).ok(); + } + + #[test] + fn limited_local_command_appends_playtest_to_existing_trace() { + let root = unique_project_path(); + init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init"); + fs::write( + root.join("game/index.html"), + fake_llm_game_draft().game_html, + ) + .expect("write playable game html"); + write_agent_run_trace( + &root, + "run-1", + "像素风横版动作", + "passed", + 1, + &[agent_trace_step( + 1, + "Generator", + "completed", + &[".agent/spec.md"], + &["game/index.html"], + "生成可运行原型", + "llm.chat.generator", + )], + None, + ) + .expect("run trace"); + + let result = run_limited_local_command( + root.to_string_lossy().into_owned(), + "game.static_smoke".to_string(), + ) + .expect("static smoke"); + + assert_eq!(result.status, "completed"); + let trace: Value = + serde_json::from_str(&fs::read_to_string(root.join(".agent/run.latest.json")).unwrap()) + .expect("run trace json"); + assert_eq!(trace["status"], "passed"); + assert!(trace["steps"].as_array().unwrap().iter().any(|step| { + step["agent"] == "Playtest" + && step["phase"] == "playtest" + && step["taskId"] == "preview-readiness" + && step["toolCalls"][0]["toolId"] == "game.static_smoke" + })); + + fs::remove_dir_all(root).ok(); + } + + #[test] + fn limited_local_command_rejects_placeholder_game_smoke() { + let root = unique_project_path(); + init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init"); + + let error = run_limited_local_command_at(&root, "game.static_smoke") + .expect_err("placeholder game should fail smoke"); + + assert!(error.contains("可渲染画布")); + fs::remove_dir_all(root).ok(); + } + + #[test] + fn limited_local_command_rejects_forbidden_runtime_apis() { + let root = unique_project_path(); + init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init"); + let html = fake_llm_game_draft() + .game_html + .replace("const marker =", "fetch('/secret');\n const marker ="); + fs::write(root.join("game/index.html"), html).expect("write game html"); + + let error = run_limited_local_command_at(&root, "game.static_smoke") + .expect_err("fetch should fail smoke"); + + assert!(error.contains("fetch(")); + fs::remove_dir_all(root).ok(); + } + + #[test] + fn limited_local_command_rejects_blank_canvas_game_smoke() { + let root = unique_project_path(); + init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init"); + let html = r#" + + + +

目标:点亮厨房。胜利 / 失败后按 R 重开。

+ + +"#; + fs::write(root.join("game/index.html"), html).expect("write game html"); + + let error = run_limited_local_command_at(&root, "game.static_smoke") + .expect_err("blank canvas should fail smoke"); + + assert!(error.contains("canvas 上绘制画面")); + fs::remove_dir_all(root).ok(); + } + + #[test] + fn limited_local_command_rejects_unknown_command() { + let root = unique_project_path(); + let error = run_limited_local_command_at(&root, "npm.run.build") + .expect_err("unknown command should fail"); + + assert!(error.contains("不支持")); + } + + #[test] + fn preview_state_is_persisted_to_manifest() { + let root = unique_project_path(); + + record_preview_state( + &root, + GameCreationAppPreviewStatus::Running, + Some("http://127.0.0.1:3210/".to_string()), + Some(3210), + ) + .expect("preview state"); + let manifest: Value = + serde_json::from_str(&fs::read_to_string(root.join(".agent/manifest.json")).unwrap()) + .expect("manifest json"); + assert_eq!(manifest["preview"]["status"], "running"); + assert_eq!(manifest["preview"]["port"], 3210); + + record_preview_state(&root, GameCreationAppPreviewStatus::Stopped, None, None) + .expect("preview stopped"); + let manifest: Value = + serde_json::from_str(&fs::read_to_string(root.join(".agent/manifest.json")).unwrap()) + .expect("manifest json"); + assert_eq!(manifest["preview"]["status"], "stopped"); + assert!(manifest["preview"].get("url").is_none()); + + fs::remove_dir_all(root).ok(); + } + + #[test] + fn preview_start_appends_to_existing_agent_run_trace() { + let root = unique_project_path(); + init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init"); + write_agent_run_trace( + &root, + "run-1", + "像素风横版动作", + "passed", + 1, + &[agent_trace_step( + 1, + "Playtest", + "completed", + &["game/index.html"], + &[".agent/logs/command.log"], + "静态入口自检通过", + "game.static_smoke", + )], + None, + ) + .expect("run trace"); + + append_preview_start_trace_step( + &root, + &LocalPreviewResult { + url: "http://127.0.0.1:3210/".to_string(), + port: 3210, + root: root.join("game").to_string_lossy().into_owned(), + }, + ) + .expect("append preview trace"); + + let trace: Value = + serde_json::from_str(&fs::read_to_string(root.join(".agent/run.latest.json")).unwrap()) + .expect("run trace json"); + assert_eq!(trace["status"], "preview-running"); + assert_eq!(trace["stopReason"], "preview-running"); + assert_eq!(trace["nextStep"], "manual-playtest"); + assert_eq!(trace["steps"].as_array().unwrap().len(), 2); + assert_eq!(trace["steps"][1]["agent"], "Preview"); + assert_eq!(trace["steps"][1]["toolCalls"][0]["toolId"], "preview.start"); + assert!(trace["steps"][1]["outputPaths"] + .as_array() + .unwrap() + .iter() + .any(|path| path == ".agent/logs/preview.log")); + assert!(trace["steps"][1]["summary"] + .as_str() + .unwrap() + .contains("http://127.0.0.1:3210/")); + let run_history: Value = + serde_json::from_str(&fs::read_to_string(root.join(".agent/runs/run-1.json")).unwrap()) + .expect("run history json"); + assert_eq!(run_history["status"], "preview-running"); + assert_eq!(run_history["stopReason"], "preview-running"); + assert_eq!(run_history["nextStep"], "manual-playtest"); + assert_eq!(run_history["steps"].as_array().unwrap().len(), 2); + + append_preview_stop_trace_step(&root).expect("append preview stop trace"); + let trace: Value = + serde_json::from_str(&fs::read_to_string(root.join(".agent/run.latest.json")).unwrap()) + .expect("run trace json"); + assert_eq!(trace["status"], "preview-stopped"); + assert_eq!(trace["stopReason"], "preview-stopped"); + assert_eq!(trace["nextStep"], "inspect-artifacts"); + assert_eq!(trace["steps"].as_array().unwrap().len(), 3); + assert_eq!(trace["steps"][2]["agent"], "Preview"); + assert_eq!(trace["steps"][2]["status"], "stopped"); + assert_eq!(trace["steps"][2]["toolCalls"][0]["toolId"], "preview.stop"); + assert!(trace["steps"][2]["outputPaths"] + .as_array() + .unwrap() + .iter() + .any(|path| path == ".agent/logs/preview.log")); + + fs::remove_dir_all(root).ok(); + } + + #[test] + fn preview_log_records_start_and_stop_events() { + let root = unique_project_path(); + + append_preview_log(&root, "running", Some("http://127.0.0.1:3210/")) + .expect("preview start log"); + append_preview_log(&root, "stopped", None).expect("preview stop log"); + + let log = fs::read_to_string(root.join(".agent/logs/preview.log")).expect("preview log"); + assert!(log.contains("preview.running http://127.0.0.1:3210/")); + assert!(log.contains("preview.stopped")); + + fs::remove_dir_all(root).ok(); + } + + #[test] + fn preview_start_trace_noops_without_agent_run_trace() { + let root = unique_project_path(); + init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init"); + + append_preview_start_trace_step( + &root, + &LocalPreviewResult { + url: "http://127.0.0.1:3210/".to_string(), + port: 3210, + root: root.join("game").to_string_lossy().into_owned(), + }, + ) + .expect("missing trace should not block preview"); + + assert!(!root.join(".agent/run.latest.json").exists()); + + fs::remove_dir_all(root).ok(); + } + + #[test] + fn preview_registry_reports_status_and_stops_previous_server() { + let registry = PreviewRegistry::default(); + assert_eq!(registry.status().status, "stopped"); + + let (first_stop, first_receiver) = mpsc::channel(); + let (_first_preview, previous) = registry.set_running( + LocalPreviewResult { + url: "http://127.0.0.1:1/".to_string(), + port: 1, + root: "/tmp/game-one/game".to_string(), + }, + first_stop, + ); + assert!(previous.is_none()); + assert_eq!(registry.status().port, Some(1)); + + let (second_stop, _second_receiver) = mpsc::channel(); + let (_second_preview, previous) = registry.set_running( + LocalPreviewResult { + url: "http://127.0.0.1:2/".to_string(), + port: 2, + root: "/tmp/game-two/game".to_string(), + }, + second_stop, + ); + assert!(first_receiver.try_recv().is_ok()); + assert_eq!( + previous.expect("previous preview").root, + "/tmp/game-one/game" + ); + assert_eq!(registry.status().port, Some(2)); + + assert_eq!(registry.stop().status, "stopped"); + assert_eq!(registry.status().status, "stopped"); + } + + #[test] + fn replaced_preview_records_stopped_state() { + let root = unique_project_path(); + init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init"); + record_preview_state( + &root, + GameCreationAppPreviewStatus::Running, + Some("http://127.0.0.1:3210/".to_string()), + Some(3210), + ) + .expect("preview running state"); + + record_replaced_preview_stop(&LocalPreviewResult { + url: "http://127.0.0.1:3210/".to_string(), + port: 3210, + root: root.to_string_lossy().into_owned(), + }); + + let manifest: Value = + serde_json::from_str(&fs::read_to_string(root.join(".agent/manifest.json")).unwrap()) + .expect("manifest json"); + assert_eq!(manifest["preview"]["status"], "stopped"); + let log = fs::read_to_string(root.join(".agent/logs/preview.log")).expect("preview log"); + assert!(log.contains("preview.stopped")); + + fs::remove_dir_all(root).ok(); + } + + #[test] + fn preview_registry_does_not_stop_other_project_preview() { + let first = unique_project_path(); + let second = unique_project_path(); + fs::create_dir_all(&first).expect("first project"); + fs::create_dir_all(&second).expect("second project"); + let registry = PreviewRegistry::default(); + let (first_stop, first_receiver) = mpsc::channel(); + registry.set_running( + LocalPreviewResult { + url: "http://127.0.0.1:1/".to_string(), + port: 1, + root: first.to_string_lossy().into_owned(), + }, + first_stop, + ); + + let (status, stopped) = registry.stop_for_project(Some(&second)); + + assert_eq!(status.status, "stopped"); + assert!(!stopped); + assert!(first_receiver.try_recv().is_err()); + assert_eq!(registry.status().port, Some(1)); + + let (_status, stopped) = registry.stop_for_project(Some(&first)); + assert!(stopped); + assert!(first_receiver.try_recv().is_ok()); + assert_eq!(registry.status().status, "stopped"); + + fs::remove_dir_all(first).ok(); + fs::remove_dir_all(second).ok(); + } + + #[test] + fn preview_open_url_requires_running_localhost_preview() { + assert_eq!( + preview_open_url(&LocalPreviewStatus { + status: "running".to_string(), + url: Some("http://127.0.0.1:3001/".to_string()), + port: Some(3001), + root: Some("/tmp/game".to_string()), + }) + .unwrap(), + "http://127.0.0.1:3001/" + ); + + assert!(preview_open_url(&stopped_preview_status()).is_err()); + assert!(preview_open_url(&LocalPreviewStatus { + status: "running".to_string(), + url: Some("https://example.com/".to_string()), + port: Some(443), + root: Some("/tmp/game".to_string()), + }) + .is_err()); + } + + #[test] + fn preview_project_guard_rejects_other_project_preview() { + let first = unique_project_path(); + let second = unique_project_path(); + fs::create_dir_all(&first).expect("first project"); + fs::create_dir_all(&second).expect("second project"); + let status = LocalPreviewStatus { + status: "running".to_string(), + url: Some("http://127.0.0.1:3001/".to_string()), + port: Some(3001), + root: Some(first.to_string_lossy().into_owned()), + }; + + ensure_preview_belongs_to_project(&status, &first).expect("same project preview"); + let error = ensure_preview_belongs_to_project(&status, &second) + .expect_err("other project preview should fail"); + + assert!(error.contains("不属于已授权本地项目")); + + fs::remove_dir_all(first).ok(); + fs::remove_dir_all(second).ok(); + } + + #[test] + fn preview_status_filter_hides_other_project_preview() { + let first = unique_project_path(); + let second = unique_project_path(); + fs::create_dir_all(&first).expect("first project"); + fs::create_dir_all(&second).expect("second project"); + let status = LocalPreviewStatus { + status: "running".to_string(), + url: Some("http://127.0.0.1:3001/".to_string()), + port: Some(3001), + root: Some(first.to_string_lossy().into_owned()), + }; + + assert_eq!( + filter_preview_status_for_project(status.clone(), Some(first.to_str().unwrap())).status, + "running" + ); + assert_eq!( + filter_preview_status_for_project(status, Some(second.to_str().unwrap())).status, + "stopped" + ); + + fs::remove_dir_all(first).ok(); + fs::remove_dir_all(second).ok(); + } + + #[test] + fn preview_path_rejects_traversal() { + let project = unique_project_path(); + fs::create_dir_all(project.join("game")).expect("game dir"); + fs::create_dir_all(project.join("assets")).expect("assets dir"); + fs::create_dir_all(project.join("memory")).expect("memory dir"); + fs::create_dir_all(project.join(".agent")).expect("agent dir"); + fs::write(project.join("game/index.html"), "").expect("index"); + fs::write(project.join("assets/player.png"), b"png").expect("asset"); + fs::write(project.join("memory/project.md"), "secret memory").expect("memory"); + fs::write(project.join(".agent/run.latest.json"), "{}").expect("trace"); + + assert_eq!( + resolve_preview_path(&project, "/").unwrap(), + project + .join("game/index.html") + .canonicalize() + .expect("canonical index") + ); + assert!(resolve_preview_path(&project, "/../secret.txt").is_err()); + assert!(resolve_preview_path(&project, "/%2e%2e/secret.txt").is_err()); + assert!(resolve_preview_path(&project, "/memory/project.md").is_err()); + assert!(resolve_preview_path(&project, "/.agent/run.latest.json").is_err()); + assert_eq!( + resolve_preview_path(&project, "/assets/player.png?cache=1").unwrap(), + project + .join("assets/player.png") + .canonicalize() + .expect("canonical asset") + ); + + fs::remove_dir_all(project).ok(); + } + + #[cfg(unix)] + #[test] + fn preview_path_rejects_symlink_escape() { + use std::os::unix::fs::symlink; + + let project = unique_project_path(); + let outside_secret = project.with_extension("secret.txt"); + fs::create_dir_all(project.join("game")).expect("game dir"); + fs::create_dir_all(project.join("assets")).expect("assets dir"); + fs::write(project.join("game/index.html"), "").expect("index"); + fs::write(&outside_secret, "secret").expect("secret"); + symlink(&outside_secret, project.join("assets/leak.txt")).expect("symlink"); + + assert!(resolve_preview_path(&project, "/assets/leak.txt").is_err()); + + fs::remove_file(outside_secret).ok(); + fs::remove_dir_all(project).ok(); + } + + #[cfg(unix)] + #[test] + fn preview_path_rejects_symlink_to_private_project_dirs() { + use std::os::unix::fs::symlink; + + let project = unique_project_path(); + fs::create_dir_all(project.join("game")).expect("game dir"); + fs::create_dir_all(project.join("assets")).expect("assets dir"); + fs::create_dir_all(project.join("memory")).expect("memory dir"); + fs::write(project.join("game/index.html"), "").expect("index"); + fs::write(project.join("memory/project.md"), "secret memory").expect("memory"); + symlink( + project.join("memory/project.md"), + project.join("assets/memory-link.md"), + ) + .expect("memory symlink"); + + assert!(resolve_preview_path(&project, "/assets/memory-link.md").is_err()); + + fs::remove_dir_all(project).ok(); + } + + #[cfg(unix)] + #[test] + fn preview_path_rejects_symlinked_allowed_root_dir() { + use std::os::unix::fs::symlink; + + let project = unique_project_path(); + fs::create_dir_all(project.join("game")).expect("game dir"); + fs::create_dir_all(project.join("memory")).expect("memory dir"); + fs::write(project.join("game/index.html"), "").expect("index"); + fs::write(project.join("memory/project.md"), "secret memory").expect("memory"); + symlink(project.join("memory"), project.join("assets")).expect("assets symlink"); + + assert!(resolve_preview_path(&project, "/assets/project.md").is_err()); + + fs::remove_dir_all(project).ok(); + } + + #[test] + fn developer_window_uses_dev_route() { + assert_eq!(developer_window_url().to_string(), "index.html?dev"); + } + + #[test] + fn cli_agent_run_requires_project_and_prompt() { + assert_eq!( + parse_cli_command(&["--llm-status".to_string()]) + .expect("parse llm status") + .expect("llm status command"), + CliCommand::LlmStatus + ); + let args = vec![ + "--agent-run".to_string(), + "/tmp/genarrative-cli-game".to_string(), + "做一个弹幕厨房".to_string(), + "带反弹".to_string(), + ]; + let command = parse_cli_command(&args) + .expect("parse cli") + .expect("cli command"); + + assert_eq!( + command, + CliCommand::AgentRun { + project_path: PathBuf::from("/tmp/genarrative-cli-game"), + prompt: "做一个弹幕厨房 带反弹".to_string(), + wait_for_enter: true, + } + ); + let no_wait = parse_cli_command(&[ + "--agent-run".to_string(), + "--no-wait".to_string(), + "/tmp/genarrative-cli-game".to_string(), + "做一个弹幕厨房".to_string(), + ]) + .expect("parse no wait") + .expect("cli command"); + assert_eq!( + no_wait, + CliCommand::AgentRun { + project_path: PathBuf::from("/tmp/genarrative-cli-game"), + prompt: "做一个弹幕厨房".to_string(), + wait_for_enter: false, + } + ); + assert!(parse_cli_command(&[]).expect("parse no cli").is_none()); + assert!(parse_cli_command(&["--agent-run".to_string()]).is_err()); + } + + #[test] + fn local_preview_server_serves_game_index() { + let root = unique_project_path(); + init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init"); + fs::write(root.join("assets/player.png"), b"PNGDATA").expect("asset"); + + let (preview, stop) = start_local_game_preview_for_project(&root).expect("preview start"); + let mut stream = TcpStream::connect(("127.0.0.1", preview.port)).expect("preview connect"); + stream + .write_all(b"GET / HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n") + .expect("request"); + let mut response = String::new(); + stream.read_to_string(&mut response).expect("response"); + + assert!(response.contains("200 OK"), "{response}"); + assert!(response.contains("还没有生成游戏")); + + let mut stream = TcpStream::connect(("127.0.0.1", preview.port)).expect("preview connect"); + stream + .write_all(b"GET /assets/player.png HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n") + .expect("asset request"); + let mut response = String::new(); + stream + .read_to_string(&mut response) + .expect("asset response"); + + assert!(response.contains("200 OK"), "{response}"); + assert!(response.contains("PNGDATA"), "{response}"); + assert_eq!(preview.root, root.to_string_lossy()); + + let _ = stop.send(()); + fs::remove_dir_all(root).ok(); + } + + #[test] + fn preview_content_type_covers_common_game_assets() { + assert_eq!(content_type(Path::new("hero.webp")), "image/webp"); + assert_eq!(content_type(Path::new("cover.jpg")), "image/jpeg"); + assert_eq!(content_type(Path::new("bgm.mp3")), "audio/mpeg"); + assert_eq!(content_type(Path::new("hit.wav")), "audio/wav"); + assert_eq!(content_type(Path::new("intro.mp4")), "video/mp4"); + } + + #[test] + fn local_preview_head_preserves_asset_content_length() { + let root = unique_project_path(); + init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init"); + fs::write(root.join("assets/player.png"), b"PNGDATA").expect("asset"); + + let response = build_preview_response(&root, "HEAD", "/assets/player.png"); + let response = String::from_utf8(response).expect("head response"); + + assert!(response.contains("200 OK"), "{response}"); + assert!(response.contains("Content-Type: image/png"), "{response}"); + assert!(response.contains("Content-Length: 7"), "{response}"); + assert!(!response.contains("PNGDATA"), "{response}"); + assert!(response.ends_with("\r\n\r\n"), "{response}"); + + fs::remove_dir_all(root).ok(); + } + + #[test] + fn local_preview_serves_generated_playable_game() { + let root = unique_project_path(); + write_local_game_draft_at(&root, "像素风横版动作", &fake_llm_game_draft()) + .expect("draft should generate"); + + let (preview, stop) = start_local_game_preview_for_project(&root).expect("preview start"); + let mut stream = TcpStream::connect(("127.0.0.1", preview.port)).expect("preview connect"); + stream + .write_all(b"GET / HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n") + .expect("request"); + let mut response = String::new(); + stream.read_to_string(&mut response).expect("response"); + + assert!(response.contains("200 OK"), "{response}"); + assert!(response.contains("requestAnimationFrame(frame)")); + assert!(response.contains("MOCK_UNIQUE_MECHANIC:moon-kitchen-reflect")); + assert!(response.contains("月光弹幕厨房")); + assert!(response.contains("player.hp")); + assert!(!response.contains(" + + + ) : null} +
+ + setChatInput(event.currentTarget.value)} + /> + +
+ + + {devMode ? ( +
+
+
+

专业组

+ +
+ {taskRowsFromManifest(manifest).map((task) => ( +
+ {taskGroupLabels[task.group]} + {`${task.title} · ${task.status}`} +
+ ))} +
+ +
+

Agent 能力

+ {GAME_CREATION_AGENT_CAPABILITIES.map((capability) => ( +
+ {capability.title} + {capability.area} +
+ ))} +
+ +
+
+

编排 Trace

+ +
+

{agentRunStatus}

+ {agentRunTrace ? ( + <> +

{agentRunTrace.runId}

+

+ {`loop: ${agentRunTrace.passes}/${agentRunTrace.maxPasses} · ${agentRunTrace.stopReason} · next: ${agentRunTrace.nextStep}`} +

+

+ {`工具调用:${agentRunTrace.toolCallCount}/${agentRunTrace.maxToolCalls}`} +

+ {agentRunTrace.error ? ( +

{agentRunTrace.error}

+ ) : null} + {agentRunHistory.length > 0 ? ( +
+ {agentRunHistory.slice(0, 6).map((runFile) => ( + + ))} +
+ ) : null} +
+ {agentRunTrace.artifacts.slice(0, 8).map((artifact) => ( + + {`${artifact.path} · ${artifact.sizeBytes}B · ${artifact.checksum}`} + + ))} +
+
+ + {`active: ${ + agentRunTrace.taskGraph.activeTaskIds.join(', ') || 'none' + }`} + + + {`carry-over: ${ + agentRunTrace.taskGraph.carriedTaskIds.join(', ') || + 'none' + }`} + + + {`ready: ${ + agentRunTrace.taskGraph.readyTaskIds.join(', ') || 'none' + }`} + + {agentRunTrace.taskGraph.repairFocus.length > 0 ? ( + + {`repair: ${agentRunTrace.taskGraph.repairFocus.join( + ' / ', + )}`} + + ) : null} + {agentRunTrace.taskGraph.repairRoutes.map((route) => ( + + {`route: ${route.taskIds.join(', ')} · ${route.reason}`} + + ))} +
+ {agentRunTrace.passPlans.length > 0 ? ( +
+ {agentRunTrace.passPlans.map((plan) => ( + + {`pass ${plan.pass}: ${plan.mode} · active ${ + plan.activeTaskIds.length + } · carry ${plan.carriedTaskIds.length} · waves ${ + plan.dependencyWaves + .map((wave) => wave.join('+')) + .join(' / ') || 'none' + }`} + + ))} +
+ ) : null} + {agentRunTrace.steps.map((step, index) => ( +
+ {`${step.agent} #${step.pass}`} + {`${step.status} · ${step.summary}`} + + {[ + step.phase, + step.taskId, + step.group && step.role + ? `${taskGroupLabels[step.group]} / ${step.role}` + : null, + ] + .filter(Boolean) + .join(' · ')} + + + {step.toolCalls + .map( + (toolCall) => `${toolCall.toolId}:${toolCall.status}`, + ) + .join(', ')} + + {`in: ${step.inputPaths.join(', ') || 'none'}`} + {`out: ${step.outputPaths.join(', ') || 'none'}`} +
+ ))} + + ) : null} +
+ +
+

本地项目

+
+ setProjectPath(event.currentTarget.value)} + /> + +
+

{projectStatus}

+

{assetStatus}

+
+ + setAssetLocalPath(event.currentTarget.value) + } + /> + setAssetKind(event.currentTarget.value)} + /> + + setAssetMediaType(event.currentTarget.value) + } + /> + + + setAssetCanvasProjectId(event.currentTarget.value) + } + /> + + setAssetResourceId(event.currentTarget.value) + } + /> + + setAssetObjectId(event.currentTarget.value) + } + /> + + setEditorBaseUrl(event.currentTarget.value) + } + /> + + + +
+ game/ + assets/ + memory/ + .agent/manifest.json + {uploadedAssets.map((asset) => ( + {asset.localPath} + ))} + {localProject ? ( +

{localProject.manifestPath}

+ ) : null} +
+ +
+
+

项目文件

+
+ + + + +
+
+ setFilePath(event.currentTarget.value)} + /> +