diff --git a/apps/ai-game-creator-shell/scripts/vite-watch.test.mjs b/apps/ai-game-creator-shell/scripts/vite-watch.test.mjs new file mode 100644 index 000000000..42fa9a5d4 --- /dev/null +++ b/apps/ai-game-creator-shell/scripts/vite-watch.test.mjs @@ -0,0 +1,113 @@ +import assert from 'node:assert/strict'; +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { dirname, join } from 'node:path'; +import { test } from 'node:test'; +import { setTimeout as delay } from 'node:timers/promises'; +import { fileURLToPath } from 'node:url'; + +import { createServer, loadConfigFromFile, normalizePath } from 'vite'; + +test( + 'AGC 排除 Rust 构建目录且保留源码与共享组件监听', + { timeout: 30_000 }, + async () => { + const loaded = await loadConfigFromFile( + { command: 'serve', mode: 'development' }, + fileURLToPath(new URL('../vite.config.ts', import.meta.url)), + ); + assert.ok(loaded); + assert.notEqual(loaded.config.server?.watch, null); + assert.notEqual(loaded.config.server?.hmr, false); + assert.ok( + [loaded.config.server?.watch?.ignored] + .flat() + .includes('**/src-tauri/target/**'), + ); + + const fixture = await mkdtemp(join(tmpdir(), 'agc-vite-watch-')); + const root = join(fixture, 'apps', 'ai-game-creator-shell'); + const source = join(root, 'src', 'main.js'); + const css = join(root, 'src', 'styles.css'); + const shared = join(fixture, 'packages', 'shared', 'src', 'component.js'); + const target = join(root, 'src-tauri', 'target'); + const artifact = join(target, 'debug', 'incremental', 'cache.bin'); + let server; + try { + for (const file of [source, css, shared, artifact]) { + await mkdir(dirname(file), { recursive: true }); + await writeFile( + file, + file === css ? 'body { color: red; }' : 'export default 1;', + ); + } + // 使用真实 Vite watcher 和实际配置,仅将扫描根替换为小型夹具; + // 不加载业务插件、后端或原生窗口,也不扫描开发机上的大型 target。 + server = await createServer({ + configFile: false, + envFile: false, + root, + logLevel: 'silent', + server: { + watch: loaded.config.server?.watch, + middlewareMode: true, + hmr: false, + fs: { allow: [fixture] }, + }, + optimizeDeps: { noDiscovery: true, include: [] }, + }); + const waitForWatchedFile = async (file) => { + const normalized = normalizePath(file); + for (let attempt = 0; attempt < 100; attempt += 1) { + if ( + Object.entries(server.watcher.getWatched()).some( + ([directory, names]) => + names.some( + (name) => normalizePath(join(directory, name)) === normalized, + ), + ) + ) + return; + await delay(50); + } + assert.fail(`源码必须仍被监听:${normalized}`); + }; + await waitForWatchedFile(source); + + // 真实模块转换应将 root 外的共享源码加入监听。 + await server.transformRequest(`/@fs/${normalizePath(shared)}`); + for (const file of [source, css, shared]) { + const normalized = normalizePath(file); + await waitForWatchedFile(file); + const changed = new Promise((resolve, reject) => { + const timer = setTimeout(() => { + server.watcher.off('change', onChange); + reject(new Error(`未收到源码变更:${normalized}`)); + }, 5_000); + function onChange(path) { + if (normalizePath(path) !== normalized) return; + clearTimeout(timer); + server.watcher.off('change', onChange); + resolve(); + } + server.watcher.on('change', onChange); + }); + await writeFile( + file, + file === css ? 'body { color: blue; }' : 'export default 2;', + ); + await changed; + } + const targetPath = normalizePath(target); + const targetDirectories = Object.keys(server.watcher.getWatched()) + .map(normalizePath) + .filter( + (path) => path === targetPath || path.startsWith(`${targetPath}/`), + ); + assert.deepEqual(targetDirectories, [], 'Rust target 不应创建目录监听器'); + } finally { + await server?.close(); + await rm(fixture, { recursive: true, force: true }); + } + }, +); diff --git a/apps/ai-game-creator-shell/vite.config.ts b/apps/ai-game-creator-shell/vite.config.ts index 6e68153ba..fa2246ed0 100644 --- a/apps/ai-game-creator-shell/vite.config.ts +++ b/apps/ai-game-creator-shell/vite.config.ts @@ -154,6 +154,9 @@ export default defineConfig({ host: '127.0.0.1', port: 3080, strictPort: true, + watch: { + ignored: ['**/src-tauri/target/**'], + }, fs: { allow: [repoRoot], }, diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index e8d132d9f..99b49877b 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -1,5 +1,9 @@ # 踩坑与排障记录 +## AGC Windows 开发态首次页面加载缓慢 + +Vite 默认监听应用根下的 Rust `src-tauri/target`,构建产物较多时会创建大量 Windows 文件监听器。AGC 配置通过 `server.watch.ignored: ['**/src-tauri/target/**']` 排除此目录,不关闭业务源码、CSS、共享组件监听或 HMR。排查时区分后端就绪、Vite 扫描和原生窗口首绘;监听目录回归不能代替实机首绘测量,验证入口见本地开发运维文档。 + ## Windows 已登记生图资产未刷新 Direct 工具桥会 canonicalize 项目根,事件中的路径可能带 `\\?\` / `\\?\UNC\`,而前端项目路径仍是普通盘符或 UNC。失效监听不能直接比较原始字符串;识别为同一项目后,用当前项目路径重读 manifest,保留项目切换与 revision 门禁。普通 `agc_generate_image` 成功提交也必须发出失效通知,不能依赖整轮 Agent 结束。回归需覆盖两种 Windows 前缀、其它项目事件拒收,以及 Agent 尚未结束和后续失败时已登记图片卡片仍可见。 diff --git a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index e036f5205..181ca4bfc 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -20,6 +20,8 @@ Stdb 发布以 root 准备文件、再切换 `spacetimedb` 用户执行时,WAS ## 本地启动 +AGC Vite 的 `server.watch.ignored` 排除 `**/src-tauri/target/**`,避免递归监听 Rust 构建产物、在 Windows 上创建大量文件监听器并拖慢首次页面加载。保留业务源码、CSS 与仓库共享组件的监听及热更新;不通过关闭 watcher 或 HMR 规避问题。监听回归使用 `node --test apps/ai-game-creator-shell/scripts/vite-watch.test.mjs`,验证构建目录被排除、应用源码及根目录外的共享源码仍能触发变更;原生窗口首绘耗时另行实测,不把监听测试耗时当作启动性能指标。 + AGC `backend` 模式与 `all` / `api-server` 一样,必须同时探测 API 和 BgFilter worker 端口,漂移后的 worker 地址同时传给 API、worker 和 readiness 检查。不能因为旧 worker 的 `/readyz` 可访问,就把新启动失败的同端口 worker 视为就绪;AGC 前端会等待完整配套后端,worker 失败可能最终表现为 Tauri 等待前端 180 秒超时。 `npm run agc` 外层启动器先执行 `agc:serve` 并等待前端与配套后端就绪,再启动 Tauri,同时清空本次 CLI 的 `beforeDevCommand`,避免重复拉起服务和把数据库发布时间计入 Tauri 的 180 秒前端等待。准备阶段最多等待 660 秒(后端门禁仍为 600 秒),退出时清理本次启动的服务树,不停止复用的服务。AGC 自动发布显式使用 `--preserve-database`,schema 冲突须人工确认迁移,不自动清空数据。