Files
Genarrative/apps/ai-game-creator-shell/scripts/check-config.mjs
T
AIGameCreator App 588a529875 改用客户端运行时配置
移除 AI 游戏创作壳专属 .env 读取,改为默认配置模板与 Tauri 运行时配置。

新增主窗口配置面板,读写 LLM 与画板 API 配置。

更新 CLI smoke、门禁、测试和项目文档。
2026-06-30 15:13:51 +08:00

375 lines
11 KiB
JavaScript

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('../game-creator.config.json', 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-config.mjs --llm-status'
) {
throw new Error(
'AI game creator shell llm-status must use client config before checking LLM config',
);
}
if (
packageConfig.scripts?.['agent-run'] !==
'node scripts/run-cli-with-config.mjs --agent-run'
) {
throw new Error(
'AI game creator shell agent-run must use client config 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 runCliWithConfigSource = fs.readFileSync(
new URL('../scripts/run-cli-with-config.mjs', import.meta.url),
'utf8',
);
for (const snippet of [
'const port = 3080',
"response.body.includes('<title>AI 游戏创作</title>')",
'function isPortListening()',
'reuse existing Vite dev server',
'non-HTTP or unrecognized server',
"'--config', 'vite.config.ts'",
]) {
if (!devServerSource.includes(snippet)) {
throw new Error(
`AI game creator shell dev server wrapper drifted: ${snippet}`,
);
}
}
for (const snippet of [
"new URL('..', import.meta.url)",
"'--manifest-path'",
"'src-tauri/Cargo.toml'",
]) {
if (!runCliWithConfigSource.includes(snippet)) {
throw new Error(
`AI game creator shell config CLI wrapper drifted: ${snippet}`,
);
}
}
const tauriMainSource = fs.readFileSync(
new URL('../src-tauri/src/main.rs', import.meta.url),
'utf8',
);
for (const snippet of [
'const GAME_CREATOR_CONFIG_FILE_NAME: &str = "game-creator.config.json"',
'const GAME_CREATOR_LOCAL_CONFIG_FILE_NAME: &str = "game-creator.config.local.json"',
'const DEFAULT_GAME_CREATOR_APP_CONFIG_JSON: &str = include_str!("../../game-creator.config.json")',
'fn configure_game_creator_runtime_config_dir(',
'app.path().app_config_dir()?',
'fn load_game_creator_app_config()',
'fn read_game_creator_app_config()',
'fn write_game_creator_app_config(',
'fn writable_game_creator_config_path()',
'fn normalize_game_creator_app_config(',
'fn merge_game_creator_config_file(',
'.join("apps")',
'.join("ai-game-creator-shell")',
'configure_game_creator_runtime_config_dir(app.handle())?',
'read_game_creator_app_config,',
'write_game_creator_app_config,',
'build_game_creator_llm_client_from_config()?',
'let app_config = match load_game_creator_app_config()',
'#[cfg(debug_assertions)]\nfn developer_window_url()',
'tauri::WebviewUrl::App(PathBuf::from("index.html?dev"))',
'#[cfg(debug_assertions)]\nfn open_developer_window(app: &tauri::App)',
'tauri::WebviewWindowBuilder::new(app, "developer", developer_window_url())',
'open_developer_window(app)?;',
'fn append_local_permission_log_at(',
'"command.auto"',
'GameCreationAppPermission::Auto',
]) {
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'",
"'read_game_creator_app_config'",
"'write_game_creator_app_config'",
'aria-label="运行时配置"',
'LLM API Key',
'画板 API Key',
'runtime_config.save',
"'/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'",
"'command.auto'",
"'agent.run_status'",
'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'))`,
"const localConfigPath = path.join(appRoot, 'game-creator.config.local.json')",
"protocol: 'chat_completions'",
'stream: true',
'await restoreOptionalFile(localConfigPath, previousLocalConfig)',
"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}`,
);
}
}