f9d8142068
启动客户端时先检查平台登录态,未登录展示登录页,登录后进入启动器首页 独立客户端登录使用平台 auth 接口并对 refresh 做并发去重 Tauri 开发启动改为短命令 agc:serve,并先拉起配套 SpacetimeDB 与 api-server AGC Vite 代理读取 dev-stack 状态并暴露本地 marker,复用时校验 API target 恢复启动器项目目录输入、最近项目刷新移除和非空文件夹提醒交互 补充启动器和登录 gate 测试,并更新 AI 游戏创作 App 开发文档
681 lines
20 KiB
JavaScript
681 lines
20 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 defaultAppConfig = JSON.parse(
|
|
fs.readFileSync(
|
|
new URL('../game-creator.config.json', import.meta.url),
|
|
'utf8',
|
|
),
|
|
);
|
|
const rootPackageConfig = JSON.parse(
|
|
fs.readFileSync(new URL('../../../package.json', import.meta.url), 'utf8'),
|
|
);
|
|
const viteConfigSource = fs.readFileSync(
|
|
new URL('../vite.config.ts', import.meta.url),
|
|
'utf8',
|
|
);
|
|
const appInvokeSource = fs.readFileSync(
|
|
new URL('../src/App.tsx', import.meta.url),
|
|
'utf8',
|
|
);
|
|
const tauriHandlerSource = fs.readFileSync(
|
|
new URL('../src-tauri/src/main.rs', import.meta.url),
|
|
'utf8',
|
|
);
|
|
const tauriRustSource = readSourceTree(
|
|
new URL('../src-tauri/src/', import.meta.url),
|
|
'.rs',
|
|
);
|
|
const sharedContractSource = fs.readFileSync(
|
|
new URL(
|
|
'../../../packages/shared/src/contracts/gameCreationApp.ts',
|
|
import.meta.url,
|
|
),
|
|
'utf8',
|
|
);
|
|
const rustSharedContractSource = fs.readFileSync(
|
|
new URL(
|
|
'../../../server-rs/crates/shared-contracts/src/game_creation_app.rs',
|
|
import.meta.url,
|
|
),
|
|
'utf8',
|
|
);
|
|
const allowedUncalledTauriCommands = [];
|
|
const sourceExtensions = new Set([
|
|
'.json',
|
|
'.md',
|
|
'.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 readSourceTree(path, extension) {
|
|
const stat = fs.statSync(path);
|
|
if (stat.isDirectory()) {
|
|
return fs
|
|
.readdirSync(path, { withFileTypes: true })
|
|
.sort((left, right) => left.name.localeCompare(right.name))
|
|
.map((entry) =>
|
|
readSourceTree(
|
|
new URL(`${entry.name}${entry.isDirectory() ? '/' : ''}`, path),
|
|
extension,
|
|
),
|
|
)
|
|
.join('\n');
|
|
}
|
|
if (pathnameExtension(path.pathname) !== extension) {
|
|
return '';
|
|
}
|
|
return fs.readFileSync(path, 'utf8');
|
|
}
|
|
|
|
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}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function assertNoEnvironmentConfigFallbacks(paths) {
|
|
const allowedDevCheck = 'import.meta.env.DEV';
|
|
for (const path of paths.flatMap((entry) => collectFiles(entry))) {
|
|
const source = fs
|
|
.readFileSync(path, 'utf8')
|
|
.replaceAll(allowedDevCheck, '')
|
|
.replaceAll('game-creator.config.local.json', '');
|
|
if (/\bprocess\.env\b|\bdotenv\b/.test(source)) {
|
|
throw new Error(
|
|
`AI game creator shell must use runtime config, not environment config: ${path.pathname}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function assertNoNativeBrowserConfirm(paths) {
|
|
for (const path of paths.flatMap((entry) => collectFiles(entry))) {
|
|
const source = fs.readFileSync(path, 'utf8');
|
|
if (/\bwindow\.confirm\b/.test(source)) {
|
|
throw new Error(
|
|
`AI game creator shell confirmations must use in-app UI: ${path.pathname}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function extractConstArrayBlock(source, name) {
|
|
const start = source.indexOf(`const ${name}`);
|
|
if (start === -1) {
|
|
throw new Error(`Missing contract array: ${name}`);
|
|
}
|
|
const end = source.indexOf('];', start);
|
|
if (end === -1) {
|
|
throw new Error(`Missing contract array end: ${name}`);
|
|
}
|
|
return source.slice(start, end + 2);
|
|
}
|
|
|
|
function parseTsCommands(source) {
|
|
const block = extractConstArrayBlock(source, 'GAME_CREATION_APP_COMMANDS');
|
|
return Array.from(
|
|
block.matchAll(
|
|
/\{\s*id:\s*'([^']+)',\s*permission:\s*'([^']+)'\s*\}/g,
|
|
),
|
|
([, id, permission]) => ({ id, permission }),
|
|
);
|
|
}
|
|
|
|
function parseRustCommands(source) {
|
|
const block = extractConstArrayBlock(source, 'GAME_CREATION_APP_COMMANDS');
|
|
const permissionNames = {
|
|
Auto: 'auto',
|
|
Confirm: 'confirm',
|
|
Deny: 'deny',
|
|
};
|
|
return Array.from(
|
|
block.matchAll(
|
|
/command\(\s*"([^"]+)",\s*GameCreationAppPermission::(Auto|Confirm|Deny)\s*\)/g,
|
|
),
|
|
([, id, permission]) => ({ id, permission: permissionNames[permission] }),
|
|
);
|
|
}
|
|
|
|
function parseTsCapabilities(source) {
|
|
const block = extractConstArrayBlock(
|
|
source,
|
|
'GAME_CREATION_AGENT_CAPABILITIES',
|
|
);
|
|
return Array.from(
|
|
block.matchAll(
|
|
/\{\s*id:\s*'([^']+)',\s*area:\s*'([^']+)',\s*title:\s*'([^']+)',?\s*\}/g,
|
|
),
|
|
([, id, area, title]) => ({ id, area, title }),
|
|
);
|
|
}
|
|
|
|
function parseRustCapabilities(source) {
|
|
const block = extractConstArrayBlock(
|
|
source,
|
|
'GAME_CREATION_AGENT_CAPABILITIES',
|
|
);
|
|
return Array.from(
|
|
block.matchAll(
|
|
/capability\(\s*"([^"]+)",\s*"([^"]+)",\s*"([^"]+)",?\s*\)/g,
|
|
),
|
|
([, id, area, title]) => ({ id, area, title }),
|
|
);
|
|
}
|
|
|
|
function assertContractRecordsMatch(label, leftRecords, rightRecords) {
|
|
const normalize = (records) =>
|
|
records
|
|
.map((record) => JSON.stringify(record))
|
|
.sort((left, right) => left.localeCompare(right));
|
|
const left = normalize(leftRecords);
|
|
const right = normalize(rightRecords);
|
|
if (left.length === 0 || right.length === 0) {
|
|
throw new Error(`${label} parser returned no records`);
|
|
}
|
|
if (JSON.stringify(left) !== JSON.stringify(right)) {
|
|
throw new Error(
|
|
`${label} drifted between TypeScript and Rust contracts\nTS=${left.join(
|
|
'\n',
|
|
)}\nRust=${right.join('\n')}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function parseAppInvokeCommandNames(source) {
|
|
return Array.from(
|
|
source.matchAll(/invoke(?:<[^>]*>)?\(\s*['"]([a-z0-9_]+)['"]/g),
|
|
([, command]) => command,
|
|
);
|
|
}
|
|
|
|
function parseTauriHandlerCommandNames(source) {
|
|
const match = source.match(/tauri::generate_handler!\[([\s\S]*?)\]/);
|
|
if (!match) {
|
|
throw new Error('AI game creator shell Tauri handler list is missing');
|
|
}
|
|
return Array.from(
|
|
match[1].matchAll(/\b([a-z][a-z0-9_]+)\b/g),
|
|
([, command]) => command,
|
|
);
|
|
}
|
|
|
|
function parseRustFunctionNames(source) {
|
|
return Array.from(
|
|
source.matchAll(/\b(?:async\s+)?fn\s+([a-z][a-z0-9_]*)\s*\(/g),
|
|
([, name]) => name,
|
|
);
|
|
}
|
|
|
|
function assertCommandNamesSubset(label, leftNames, rightNames) {
|
|
const right = new Set(rightNames);
|
|
const missing = Array.from(new Set(leftNames))
|
|
.filter((name) => !right.has(name))
|
|
.sort((left, rightName) => left.localeCompare(rightName));
|
|
if (missing.length > 0) {
|
|
throw new Error(`${label} missing commands: ${missing.join(', ')}`);
|
|
}
|
|
}
|
|
|
|
assertNoOpenAiApiKeys([
|
|
new URL('../src/', import.meta.url),
|
|
new URL('../scripts/', import.meta.url),
|
|
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,
|
|
),
|
|
]);
|
|
|
|
assertNoEnvironmentConfigFallbacks([
|
|
new URL('../src/', import.meta.url),
|
|
new URL('../scripts/run-cli-with-config.mjs', import.meta.url),
|
|
new URL('../scripts/smoke-agent-run-local-provider.mjs', import.meta.url),
|
|
new URL('../scripts/start-dev-server.mjs', import.meta.url),
|
|
new URL('../src-tauri/src/', import.meta.url),
|
|
new URL('../package.json', import.meta.url),
|
|
new URL('../vite.config.ts', import.meta.url),
|
|
new URL('../src-tauri/Cargo.toml', import.meta.url),
|
|
new URL('../src-tauri/tauri.conf.json', import.meta.url),
|
|
]);
|
|
|
|
assertNoNativeBrowserConfirm([new URL('../src/', import.meta.url)]);
|
|
|
|
assertContractRecordsMatch(
|
|
'AI game creator shell command contract',
|
|
parseTsCommands(sharedContractSource),
|
|
parseRustCommands(rustSharedContractSource),
|
|
);
|
|
|
|
assertContractRecordsMatch(
|
|
'AI game creator shell capability contract',
|
|
parseTsCapabilities(sharedContractSource),
|
|
parseRustCapabilities(rustSharedContractSource),
|
|
);
|
|
|
|
assertCommandNamesSubset(
|
|
'AI game creator shell Tauri handler',
|
|
parseAppInvokeCommandNames(appInvokeSource),
|
|
parseTauriHandlerCommandNames(tauriHandlerSource),
|
|
);
|
|
|
|
assertCommandNamesSubset(
|
|
'AI game creator shell Tauri command implementation',
|
|
parseTauriHandlerCommandNames(tauriHandlerSource),
|
|
parseRustFunctionNames(tauriRustSource),
|
|
);
|
|
|
|
assertCommandNamesSubset(
|
|
'AI game creator shell App invoke or explicit native-only allowlist',
|
|
parseTauriHandlerCommandNames(tauriHandlerSource),
|
|
[
|
|
...parseAppInvokeCommandNames(appInvokeSource),
|
|
...allowedUncalledTauriCommands,
|
|
],
|
|
);
|
|
|
|
assertCommandNamesSubset(
|
|
'AI game creator shell explicit native-only allowlist',
|
|
allowedUncalledTauriCommands,
|
|
parseTauriHandlerCommandNames(tauriHandlerSource),
|
|
);
|
|
|
|
if (packageConfig.name !== '@genarrative/ai-game-creator-shell') {
|
|
throw new Error('AI game creator shell package name drifted');
|
|
}
|
|
|
|
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',
|
|
);
|
|
}
|
|
|
|
if (
|
|
!Array.isArray(tauriConfig.app?.windows) ||
|
|
tauriConfig.app.windows.length !== 1 ||
|
|
tauriConfig.app.windows[0]?.label !== 'launcher' ||
|
|
tauriConfig.app.windows[0]?.url !== 'index.html?launcher'
|
|
) {
|
|
throw new Error(
|
|
'AI game creator shell must start with only the launcher window',
|
|
);
|
|
}
|
|
|
|
if (defaultAppConfig.llm?.apiKey !== '') {
|
|
throw new Error('AI game creator shell default llm.apiKey must stay empty');
|
|
}
|
|
|
|
if (defaultAppConfig.editorApi?.apiKey !== '') {
|
|
throw new Error(
|
|
'AI game creator shell default editorApi.apiKey must stay empty',
|
|
);
|
|
}
|
|
|
|
if (
|
|
defaultAppConfig.llm?.requestTimeoutMs < 1000 ||
|
|
defaultAppConfig.llm?.maxRetries < 0 ||
|
|
defaultAppConfig.llm?.retryBackoffMs < 1
|
|
) {
|
|
throw new Error('AI game creator shell default LLM timing config is invalid');
|
|
}
|
|
|
|
const windows = tauriConfig.app?.windows ?? [];
|
|
if (
|
|
windows.length !== 1 ||
|
|
windows[0]?.label !== 'launcher' ||
|
|
windows[0]?.url !== 'index.html?launcher'
|
|
) {
|
|
throw new Error(
|
|
'AI game creator shell release config must expose only the launcher window',
|
|
);
|
|
}
|
|
|
|
const launcherWindow = windows[0];
|
|
if (
|
|
launcherWindow.width !== 820 ||
|
|
launcherWindow.height !== 640 ||
|
|
launcherWindow.minWidth !== 720 ||
|
|
launcherWindow.minHeight !== 520
|
|
) {
|
|
throw new Error('AI game creator shell launcher window must stay compact');
|
|
}
|
|
|
|
if (tauriConfig.build?.devUrl !== 'http://127.0.0.1:3080/') {
|
|
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',
|
|
) || tauriConfig.build?.beforeDevCommand?.includes('run agc:serve')
|
|
)
|
|
) {
|
|
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}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
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,',
|
|
'resolve_game_creator_llm_config_for_agent(app_config, "planner")',
|
|
'resolve_game_creator_llm_config_for_agent(app_config, "generator")',
|
|
'build_game_creator_llm_client_from_llm_config(&planner_llm, "agentLlm.planner")?',
|
|
'build_game_creator_llm_client_from_llm_config(&generator_llm, "agentLlm.generator")?',
|
|
'agentLlm.{agent_id}',
|
|
'let app_config = match load_game_creator_app_config()',
|
|
'fn append_local_permission_log_at(',
|
|
'"command.auto"',
|
|
'GameCreationAppPermission::Auto',
|
|
]) {
|
|
if (!tauriRustSource.includes(snippet)) {
|
|
throw new Error(
|
|
`AI game creator shell developer window guardrail drifted: ${snippet}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
for (const snippet of [
|
|
'open_developer_window(app)?;',
|
|
'tauri::WebviewWindowBuilder::new(app, "developer"',
|
|
]) {
|
|
if (tauriRustSource.includes(snippet)) {
|
|
throw new Error(
|
|
`AI game creator shell must not auto-open developer windows: ${snippet}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
const smokeAgentRunSource = fs.readFileSync(
|
|
new URL('./smoke-agent-run-local-provider.mjs', import.meta.url),
|
|
'utf8',
|
|
);
|
|
for (const snippet of [
|
|
'agentLlm',
|
|
'planner-smoke-model',
|
|
'generator-smoke-model',
|
|
'global-smoke-model-unused',
|
|
]) {
|
|
if (!smokeAgentRunSource.includes(snippet)) {
|
|
throw new Error(
|
|
`AI game creator local-provider smoke lost per-agent LLM coverage: ${snippet}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
const appSource = fs.readFileSync(
|
|
new URL('../src/App.tsx', import.meta.url),
|
|
'utf8',
|
|
);
|
|
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|blackboard] 内容:追加短期、长期或黑板记忆'",
|
|
"'/memory-set [short|long|blackboard] 内容:覆盖保存对应记忆'",
|
|
'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')",
|
|
"apiKind: 'openai_chat'",
|
|
'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}`,
|
|
);
|
|
}
|
|
}
|