From 927475f2c78c51d0122b5b8e5623b3656a6a0bd3 Mon Sep 17 00:00:00 2001 From: Linghong Date: Mon, 28 Sep 2026 22:23:04 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20Web=20=E6=B8=B8?= =?UTF-8?q?=E6=88=8F=E9=A2=84=E6=A3=80=E7=9A=84=E7=94=A8=E6=88=B7=E7=9B=AE?= =?UTF-8?q?=E5=BD=95=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为 Node/npm 版本探测设置临时用户目录并隔离配置、缓存和工作目录 补充真实运行时探测及子进程隔离环境回归断言 同步技术方案与排障记忆 --- .../src-tauri/src/environment_check.rs | 46 +++++++++++++++++++ docs/project-memory/shared-memory/pitfalls.md | 6 ++- ...¹案】AI游戏创作智能体App实施计划-2026-06-24.md | 6 +++ 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/environment_check.rs b/apps/ai-game-creator-shell/src-tauri/src/environment_check.rs index facfc30f6..e9105fc34 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/environment_check.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/environment_check.rs @@ -355,10 +355,25 @@ fn valid_version(value: &str) -> bool { } async fn probe_version(runtime: &NodeRuntime, npm: bool) -> Result { + // npm 初始化也会查询用户目录;显式隔离,避免依赖 Windows 后备查询。 + let home = tempfile::tempdir().map_err(|_| "runtime-probe-home-unavailable")?; + let config = home.path().join("user.npmrc"); + let global_config = home.path().join("global.npmrc"); + fs::write(&config, b"").map_err(|_| "runtime-probe-home-unavailable")?; + fs::write(&global_config, b"").map_err(|_| "runtime-probe-home-unavailable")?; let mut command = tokio::process::Command::new(&runtime.node); command .env_clear() .env("PATH", &runtime.safe_path) + .current_dir(home.path()) + .env("HOME", home.path()) + .env("USERPROFILE", home.path()) + .env("APPDATA", home.path()) + .env("LOCALAPPDATA", home.path()) + .env("npm_config_userconfig", &config) + .env("npm_config_globalconfig", &global_config) + .env("npm_config_cache", home.path().join("npm-cache")) + .env("npm_config_update_notifier", "false") .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::null()) @@ -651,5 +666,36 @@ mod tests { )); assert!(valid_version(&probe_version(&runtime, true).await.unwrap())); assert_eq!(fs::read_dir(root.path()).unwrap().count(), 0); + let fixture = root.path().join("probe.cjs"); + fs::write( + &fixture, + r#" +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const home = process.env.HOME; +assert.ok(home && fs.statSync(home).isDirectory()); +for (const key of ['USERPROFILE', 'APPDATA', 'LOCALAPPDATA']) { + assert.equal(process.env[key], home); +} +assert.equal(fs.realpathSync(process.cwd()), fs.realpathSync(home)); +assert.equal(fs.realpathSync(require('node:os').homedir()), fs.realpathSync(home)); +for (const key of ['npm_config_userconfig', 'npm_config_globalconfig']) { + assert.equal(path.dirname(process.env[key]), home); + assert.equal(fs.readFileSync(process.env[key], 'utf8'), ''); +} +assert.equal(process.env.npm_config_cache, path.join(home, 'npm-cache')); +console.log('1.0.0'); +"#, + ) + .unwrap(); + let fixture_runtime = NodeRuntime { + npm_cli: fixture, + ..runtime + }; + assert_eq!( + probe_version(&fixture_runtime, true).await.unwrap(), + "1.0.0" + ); } } diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 313cdf806..b416e507f 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -1,5 +1,9 @@ # 踩坑与排障记录 +## AGC 版本探测必须显式提供隔离用户目录 + +清空子进程环境后缺少 `USERPROFILE/HOME` 会让 npm 依赖 Windows 后备用户查询,部分机器报 `uv_os_homedir` / `ENOMEM`。正常机器探测成功不能证明环境完整,需同时验证子进程环境。版本探测使用临时用户目录和空 npm 配置,详见 [AGC 实施计划](../../technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md) 的“Web 环境版本探测的用户目录隔离”。 + ## 美术包固定 PNG 路径与平台返回格式不一致 - 平台生成成功仍可能返回 JPEG/WebP;直接按 `assets/art-spec.png` 等固定路径保存会在扩展名校验时失败,图片尚未落盘但已付费结果账本仍存在,不能据 manifest 为空认定没有生成结果。 @@ -6078,4 +6082,4 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/` - **现象**:网页发布页飞快双击「提交审核」,服务端出现两份游戏(各带一个版本)。`isSubmitting` 是 React 状态,双击发生在同一次渲染窗口里时还没生效,再叠上 `prepareGamePackage` 的 ZIP 压缩耗时,两次点击各走一遍创建工作。 - **处理**:`GamePublishPage` 加同步 `submitInFlightRef` 在途守卫(进入提交前同步置位、`finally` 复位),并让同一次发布(含失败后重试)复用同一组 Idempotency-Key(`publishKeyRef`,成功后清空),服务端按幂等重放;`check:game-distribution-web-publish-recovery-e2e` 断言「双击只落一份游戏与一个版本」,`GamePublishPage.test.tsx` 15 passed。 -- **教训**:任何「点一次会建资源」的入口都需要同步守卫或稳定幂等键,只靠 React 状态禁用按钮不够。 \ No newline at end of file +- **教训**:任何「点一次会建资源」的入口都需要同步守卫或稳定幂等键,只靠 React 状态禁用按钮不够。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index de8f53ce6..74c97f257 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -1,5 +1,11 @@ # AI 游戏创作智能体 App 实施计划 +## 2026-09-28 Web 环境版本探测的用户目录隔离 + +Node/npm 版本探测清空继承环境后,必须设置客户端创建的临时 `HOME`、`USERPROFILE`、`APPDATA` 和 `LOCALAPPDATA`,并指定空的用户及全局 npm 配置、临时缓存和临时工作目录。临时目录保留到探测子进程退出,不依赖 Windows 用户资料查询,也不读取用户或项目的 `.npmrc`。探测继续使用校验后的客户端运行时;初始化临时环境失败返回 `runtime-probe-home-unavailable`,不回退到系统 Node 或真实用户目录。 + +验证使用真实 Node/npm 的 `environment_check::tests::real_node_npm_environment_versions`,同时用 Node 脚本检查隔离目录及空配置,避免正常机器的 Windows 后备查询掩盖回归。此测试需要本地 Node/npm,默认忽略,定向验收时显式运行。 + ## 2026-09-28 AGC 客户端现行分工:离线前端、事件驱动与维护态出口 本节是 AGC 客户端「React 只做展示与交互」的现行口径,取代更早文档里渲染层直连平台网络、按固定频率轮询、自行解析维护响应的做法。 -- 2.52.0 From 172ef2a16b79048349982a95c555101649328842 Mon Sep 17 00:00:00 2001 From: Linghong Date: Mon, 28 Sep 2026 23:24:52 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=8E=AF=E5=A2=83?= =?UTF-8?q?=E6=8E=A2=E6=B5=8B=E6=B5=8B=E8=AF=95=E5=A4=B9=E5=85=B7=E8=A7=A6?= =?UTF-8?q?=E5=8F=91=E9=85=8D=E7=BD=AE=E6=A3=80=E6=9F=A5=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 Node 环境断言移入独立测试夹具并通过 include_str 引用 保持生产配置检查规则与预检实现不变 同步定向验证入口说明 --- .../src-tauri/src/environment_check.rs | 19 +----------------- .../tests/fixtures/environment-probe.cjs | 20 +++++++++++++++++++ ...¹案】AI游戏创作智能体App实施计划-2026-06-24.md | 2 +- 3 files changed, 22 insertions(+), 19 deletions(-) create mode 100644 apps/ai-game-creator-shell/src-tauri/tests/fixtures/environment-probe.cjs diff --git a/apps/ai-game-creator-shell/src-tauri/src/environment_check.rs b/apps/ai-game-creator-shell/src-tauri/src/environment_check.rs index e9105fc34..290594405 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/environment_check.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/environment_check.rs @@ -669,24 +669,7 @@ mod tests { let fixture = root.path().join("probe.cjs"); fs::write( &fixture, - r#" -const assert = require('node:assert/strict'); -const fs = require('node:fs'); -const path = require('node:path'); -const home = process.env.HOME; -assert.ok(home && fs.statSync(home).isDirectory()); -for (const key of ['USERPROFILE', 'APPDATA', 'LOCALAPPDATA']) { - assert.equal(process.env[key], home); -} -assert.equal(fs.realpathSync(process.cwd()), fs.realpathSync(home)); -assert.equal(fs.realpathSync(require('node:os').homedir()), fs.realpathSync(home)); -for (const key of ['npm_config_userconfig', 'npm_config_globalconfig']) { - assert.equal(path.dirname(process.env[key]), home); - assert.equal(fs.readFileSync(process.env[key], 'utf8'), ''); -} -assert.equal(process.env.npm_config_cache, path.join(home, 'npm-cache')); -console.log('1.0.0'); -"#, + include_str!("../tests/fixtures/environment-probe.cjs"), ) .unwrap(); let fixture_runtime = NodeRuntime { diff --git a/apps/ai-game-creator-shell/src-tauri/tests/fixtures/environment-probe.cjs b/apps/ai-game-creator-shell/src-tauri/tests/fixtures/environment-probe.cjs new file mode 100644 index 000000000..26ccd2acb --- /dev/null +++ b/apps/ai-game-creator-shell/src-tauri/tests/fixtures/environment-probe.cjs @@ -0,0 +1,20 @@ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); + +const home = process.env.HOME; +assert.ok(home && fs.statSync(home).isDirectory()); +for (const key of ['USERPROFILE', 'APPDATA', 'LOCALAPPDATA']) { + assert.equal(process.env[key], home); +} +assert.equal(fs.realpathSync(process.cwd()), fs.realpathSync(home)); +assert.equal( + fs.realpathSync(require('node:os').homedir()), + fs.realpathSync(home), +); +for (const key of ['npm_config_userconfig', 'npm_config_globalconfig']) { + assert.equal(path.dirname(process.env[key]), home); + assert.equal(fs.readFileSync(process.env[key], 'utf8'), ''); +} +assert.equal(process.env.npm_config_cache, path.join(home, 'npm-cache')); +process.stdout.write('1.0.0\n'); diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 74c97f257..8ee04d922 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -4,7 +4,7 @@ Node/npm 版本探测清空继承环境后,必须设置客户端创建的临时 `HOME`、`USERPROFILE`、`APPDATA` 和 `LOCALAPPDATA`,并指定空的用户及全局 npm 配置、临时缓存和临时工作目录。临时目录保留到探测子进程退出,不依赖 Windows 用户资料查询,也不读取用户或项目的 `.npmrc`。探测继续使用校验后的客户端运行时;初始化临时环境失败返回 `runtime-probe-home-unavailable`,不回退到系统 Node 或真实用户目录。 -验证使用真实 Node/npm 的 `environment_check::tests::real_node_npm_environment_versions`,同时用 Node 脚本检查隔离目录及空配置,避免正常机器的 Windows 后备查询掩盖回归。此测试需要本地 Node/npm,默认忽略,定向验收时显式运行。 +验证使用真实 Node/npm 的 `environment_check::tests::real_node_npm_environment_versions`,同时用 `src-tauri/tests/fixtures/environment-probe.cjs` 检查隔离目录及空配置,避免正常机器的 Windows 后备查询掩盖回归。测试通过 `include_str!` 引用独立夹具,不把 Node 环境断言内嵌到生产源码扫描范围。此测试需要本地 Node/npm,默认忽略,定向验收时显式运行;另运行 `npm run ai-game-creator-shell:typecheck` 覆盖配置静态检查。 ## 2026-09-28 AGC 客户端现行分工:离线前端、事件驱动与维护态出口 -- 2.52.0