合并master统一工作区与Skill指纹

保留master的npm workspaces与内置Skill指纹修复

恢复并保留双方项目决策记录

补记精修最终图稳定运行入口决策
This commit is contained in:
2026-08-22 17:16:57 +08:00
48 changed files with 3541 additions and 7157 deletions
-9
View File
@@ -78,9 +78,6 @@ jobs:
- name: Install npm dependencies
run: bash scripts/ci-npm-ci-with-retry.sh
- name: Install AI game creator dependencies
run: bash scripts/ci-npm-ci-with-retry.sh --prefix apps/ai-game-creator-shell
- name: Run repository checks
run: npm run check:repository-ci
@@ -100,9 +97,6 @@ jobs:
- name: Install npm dependencies
run: bash scripts/ci-npm-ci-with-retry.sh
- name: Install AI game creator dependencies
run: bash scripts/ci-npm-ci-with-retry.sh --prefix apps/ai-game-creator-shell
- name: Run frontend and script tests
run: npm run test
@@ -213,9 +207,6 @@ jobs:
- name: Install npm dependencies
run: bash scripts/ci-npm-ci-with-retry.sh
- name: Install AI game creator dependencies
run: bash scripts/ci-npm-ci-with-retry.sh --prefix apps/ai-game-creator-shell
- name: Prepare native Rust dependencies
shell: bash
run: |
+2
View File
@@ -24,6 +24,8 @@
npm install
```
该命令会按根 `package.json` 的 npm workspaces 一次安装主站、Admin、Mobile、Desktop、AGC、Preview Deployer、内部 packages 与工具依赖;仓库只使用根 `package-lock.json`,不要在子目录单独执行 `npm install` / `npm ci` 或提交嵌套 lockfile。
准备环境变量:
- 复制 `.env.example``.env.local`
+5 -1
View File
@@ -10,6 +10,7 @@
"preview": "vite preview --host 127.0.0.1"
},
"dependencies": {
"@genarrative/shared": "0.1.0",
"@vitejs/plugin-react": "^5.0.4",
"lucide-react": "^0.546.0",
"react": "^19.0.0",
@@ -17,8 +18,11 @@
"vite": "^6.2.0"
},
"devDependencies": {
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"typescript": "~5.8.2"
"typescript": "~5.8.2",
"vitest": "^0.34.6"
}
}
File diff suppressed because it is too large Load Diff
+8 -2
View File
@@ -32,10 +32,13 @@
"agent-runtime:supervisor-swarm-tool-plan-handoff-runner-kill-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-tool-plan-handoff-runner-kill",
"agent-runtime:steer-real-e2e": "node scripts/agent-runtime-steer-real-e2e.mjs",
"agent-runtime:steer-runner-kill-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite steer-runner-kill",
"typecheck": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json --noEmit && node scripts/check-config.mjs"
"typecheck": "tsc -p tsconfig.json --noEmit && node scripts/check-config.mjs"
},
"dependencies": {
"@cubone/react-file-manager": "^1.35.0",
"@genarrative/image-canvas-core": "0.1.0",
"@genarrative/image-canvas-react": "0.1.0",
"@genarrative/shared": "0.1.0",
"@lexical/react": "^0.47.0",
"@lexical/utils": "^0.47.0",
"@tauri-apps/api": "^2.11.1",
@@ -60,9 +63,12 @@
"@openai/codex": "0.147.0",
"@tailwindcss/vite": "^4.1.14",
"@tauri-apps/cli": "^2.11.2",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"tailwindcss": "^4.1.14",
"typescript": "~5.8.2"
"typescript": "~5.8.2",
"vitest": "^0.34.6"
}
}
+14 -4
View File
@@ -47,6 +47,10 @@ fn stage_bundled_codex_cli(manifest_dir: &std::path::Path) {
let app_root = manifest_dir
.parent()
.expect("AI 游戏创作 Tauri manifest 必须位于应用目录下");
let repo_root = app_root
.parent()
.and_then(|apps_dir| apps_dir.parent())
.expect("AI 游戏创作应用必须位于仓库 apps 目录下");
let source_candidates = [
app_root.join(
"node_modules/@openai/codex-win32-x64/vendor/x86_64-pc-windows-msvc",
@@ -54,6 +58,12 @@ fn stage_bundled_codex_cli(manifest_dir: &std::path::Path) {
app_root.join(
"node_modules/@openai/codex/node_modules/@openai/codex-win32-x64/vendor/x86_64-pc-windows-msvc",
),
repo_root.join(
"node_modules/@openai/codex-win32-x64/vendor/x86_64-pc-windows-msvc",
),
repo_root.join(
"node_modules/@openai/codex/node_modules/@openai/codex-win32-x64/vendor/x86_64-pc-windows-msvc",
),
];
let source = source_candidates
.iter()
@@ -65,12 +75,12 @@ fn stage_bundled_codex_cli(manifest_dir: &std::path::Path) {
.cloned()
.unwrap_or_else(|| {
panic!(
"内置 Codex CLI 缺失;请先在 apps/ai-game-creator-shell 执行 npm ci(已检查:{}",
"内置 Codex CLI 缺失;请先在仓库根目录执行 npm ci(已检查:{}",
source_candidates
.iter()
.map(|path| path.display().to_string())
.collect::<Vec<_>>()
.join("")
.map(|path| path.display().to_string())
.collect::<Vec<_>>()
.join("")
)
});
let target_dir = manifest_dir.join("resources/codex/win-x64");
@@ -1,6 +1,6 @@
{
"schemaVersion": "agc-skill-pack.v1",
"version": "2026-08-22.1",
"version": "2026-08-22.2",
"skills": [
{
"name": "agc-project-structure",
+7 -2
View File
@@ -1,4 +1,5 @@
import { existsSync, readFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -8,6 +9,10 @@ import { defineConfig } from 'vite';
const appRoot = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(appRoot, '../..');
const appRequire = createRequire(resolve(appRoot, 'package.json'));
const reactFileManagerPackageRoot = dirname(
appRequire.resolve('@cubone/react-file-manager/package.json'),
);
function resolveDevApiTarget() {
const statePath = resolve(repoRoot, '.app/dev-stack.json');
@@ -75,8 +80,8 @@ export default defineConfig({
{
find: /^@cubone\/react-file-manager$/,
replacement: resolve(
appRoot,
'node_modules/@cubone/react-file-manager/dist/react-file-manager.es.js',
reactFileManagerPackageRoot,
'dist/react-file-manager.es.js',
),
},
{
File diff suppressed because it is too large Load Diff
+4
View File
@@ -19,6 +19,7 @@
},
"dependencies": {
"@expo/metro-runtime": "^56.0.15",
"@genarrative/shared": "0.1.0",
"expo": "^56.0.12",
"expo-camera": "56.0.8",
"expo-clipboard": "^56.0.4",
@@ -37,7 +38,10 @@
"react-native-webview": "^13.16.1"
},
"devDependencies": {
"@testing-library/react": "^16.3.2",
"@types/react": "^19.2.14",
"eas-cli": "^20.3.0",
"pngjs": "^7.0.0",
"typescript": "~5.8.2",
"vitest": "^0.34.6"
}
File diff suppressed because it is too large Load Diff
+57 -21
View File
@@ -1,15 +1,17 @@
import {spawnSync} from 'node:child_process';
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import { createRequire } from 'node:module';
import { dirname, resolve } from 'node:path';
const appConfigPath = new URL('../app.json', import.meta.url);
const packagePath = new URL('../package.json', import.meta.url);
const configPluginsPackagePath = new URL(
'../../../node_modules/@expo/config-plugins/package.json',
import.meta.url,
const shellRequire = createRequire(packagePath);
const configPluginsPackagePath = shellRequire.resolve(
'@expo/config-plugins/package.json',
);
const expoPrivacyInfoPluginPath = new URL(
'../../../node_modules/@expo/config-plugins/build/ios/PrivacyInfo.js',
import.meta.url,
const expoPrivacyInfoPluginPath = resolve(
dirname(configPluginsPackagePath),
'build/ios/PrivacyInfo.js',
);
const sharedContractPath = new URL(
'../../../packages/shared/src/contracts/hostBridge.ts',
@@ -43,7 +45,9 @@ if (result.error) {
}
if (result.signal) {
throw new Error(`Expo config smoke was terminated by signal ${result.signal}`);
throw new Error(
`Expo config smoke was terminated by signal ${result.signal}`,
);
}
if ((result.status ?? 0) !== 0) {
@@ -194,7 +198,9 @@ const sharedPublicWebOrigin = extractStringConstExport(
);
const sharedPublicWebOriginUrl = new URL(sharedPublicWebOrigin);
if (sharedPublicWebOriginUrl.protocol !== 'https:') {
throw new Error('shared HostBridge public web origin must use https for mobile app links');
throw new Error(
'shared HostBridge public web origin must use https for mobile app links',
);
}
const sharedPublicWebHost = sharedPublicWebOriginUrl.hostname;
const sharedPublicWebAssociatedDomain = `applinks:${sharedPublicWebHost}`;
@@ -206,7 +212,11 @@ assertEqual(expoConfig.version, packageConfig.version, 'version');
assertEqual(expoConfig.version, appConfig.version, 'app version');
assertEqual(expoConfig.orientation, 'default', 'orientation');
assertEqual(expoConfig.userInterfaceStyle, 'automatic', 'userInterfaceStyle');
assertSameList(expoConfig.assetBundlePatterns, ['**/*'], 'asset bundle patterns');
assertSameList(
expoConfig.assetBundlePatterns,
['**/*'],
'asset bundle patterns',
);
assertEqual(expoConfig.icon, './assets/icon.png', 'icon');
assertEqual(expoConfig.splash?.image, './assets/icon.png', 'splash image');
assertEqual(expoConfig.splash?.resizeMode, 'contain', 'splash resize mode');
@@ -216,10 +226,14 @@ if (Object.keys(expoConfig.updates ?? {}).some((key) => key !== 'enabled')) {
throw new Error('Expo config OTA update metadata must stay empty');
}
if ('runtimeVersion' in expoConfig) {
throw new Error('Expo config runtimeVersion must not be set without a real OTA release channel');
throw new Error(
'Expo config runtimeVersion must not be set without a real OTA release channel',
);
}
if ('releaseChannel' in expoConfig || 'channel' in expoConfig) {
throw new Error('Expo config release channel must not be set without a real release process');
throw new Error(
'Expo config release channel must not be set without a real release process',
);
}
assertEqual(
expoConfig.extra?.genarrativeHostBridgeVersion,
@@ -259,7 +273,9 @@ const configPluginsMajor = Number(
String(configPluginsPackageConfig.version).split('.')[0],
);
if (!Number.isFinite(configPluginsMajor) || configPluginsMajor < 50) {
throw new Error('Expo config plugins must support built-in iOS privacy manifests');
throw new Error(
'Expo config plugins must support built-in iOS privacy manifests',
);
}
for (const snippet of [
'function withPrivacyInfo(config)',
@@ -269,7 +285,9 @@ for (const snippet of [
'mergePrivacyInfo(existing, privacyManifests)',
]) {
if (!expoPrivacyInfoPluginSource.includes(snippet)) {
throw new Error(`Expo config plugins PrivacyInfo support missing ${snippet}`);
throw new Error(
`Expo config plugins PrivacyInfo support missing ${snippet}`,
);
}
}
@@ -300,7 +318,9 @@ assertSameSet(
'Android explicit permissions',
);
if (appConfig.android?.permissions?.includes('android.permission.CAMERA')) {
throw new Error('Expo source config must not hard-code Android CAMERA permission');
throw new Error(
'Expo source config must not hard-code Android CAMERA permission',
);
}
assertIncludes(
expoConfig.android?.blockedPermissions,
@@ -312,11 +332,23 @@ assertIncludes(
'android.permission.READ_EXTERNAL_STORAGE',
'Android external download blocked permissions',
);
if (expoConfig.android?.blockedPermissions?.includes('android.permission.RECORD_AUDIO')) {
throw new Error('Expo config Android permissions must not block RECORD_AUDIO needed by same-origin H5 microphone gameplay');
if (
expoConfig.android?.blockedPermissions?.includes(
'android.permission.RECORD_AUDIO',
)
) {
throw new Error(
'Expo config Android permissions must not block RECORD_AUDIO needed by same-origin H5 microphone gameplay',
);
}
if (expoConfig.android?.blockedPermissions?.includes('android.permission.POST_NOTIFICATIONS')) {
throw new Error('Expo config Android permissions must not block POST_NOTIFICATIONS needed by local notification.showLocal delivery');
if (
expoConfig.android?.blockedPermissions?.includes(
'android.permission.POST_NOTIFICATIONS',
)
) {
throw new Error(
'Expo config Android permissions must not block POST_NOTIFICATIONS needed by local notification.showLocal delivery',
);
}
assertIncludes(
expoConfig.android?.blockedPermissions,
@@ -352,7 +384,9 @@ assertEqual(
const appLinkFilters = expoConfig.android?.intentFilters ?? [];
if (appLinkFilters.length !== 1) {
throw new Error('Expo config Android app link filter must be the only intent filter');
throw new Error(
'Expo config Android app link filter must be the only intent filter',
);
}
const [appLinkFilter] = appLinkFilters;
@@ -428,7 +462,9 @@ assertEqual(
);
if (findPlugin('expo-updates')) {
throw new Error('Expo config must not include expo-updates without a real release channel');
throw new Error(
'Expo config must not include expo-updates without a real release channel',
);
}
console.log('[mobile-shell:expo-config] OK');
+2
View File
@@ -18,6 +18,8 @@
"vite": "^6.2.0"
},
"devDependencies": {
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"typescript": "~5.8.2",
+13 -5
View File
@@ -13,7 +13,7 @@ Docker Compose
└─ otelcol :4317/4318debug exporter,接收 traces / metrics / logs
```
当前容器模拟参数按 `genarrative-release` 服务器采样值收口为 2 vCPU / 2 GiB RAM / 4096 soft nofile / 768 worker_connections,并已在 compose 里落实到 `spacetimedb cpus=1.0 mem_limit=896m``api-server cpus=2.0 mem_limit=1g``external-generation-worker cpus=2.0 mem_limit=1g``nginx cpus=0.5 mem_limit=128m``otelcol cpus=0.25 mem_limit=128m`。SpacetimeDB 同时设置 `--page_pool_max_size=402653184`,给 reducer、订阅与运行时保留更多非 page pool 内存。
当前容器模拟参数按 `genarrative-release` 服务器采样值保留 CPU、`nofile` 与 Nginx 连接口径,并已在 compose 里落实到 `spacetimedb cpus=1.0 mem_limit=2g``api-server cpus=2.0 mem_limit=1g``external-generation-worker cpus=2.0 mem_limit=1g``nginx cpus=0.5 mem_limit=128m``otelcol cpus=0.25 mem_limit=128m`。SpacetimeDB 同时设置 `--page_pool_max_size=402653184`,给 reducer、订阅与运行时保留更多非 page pool 内存;当前完整模块首次实例化的 cgroup 峰值会超过旧 `896m` 上限,因此完整容器和分支预览统一使用 `2g`,避免 publish 期间被 OOM 杀死
容器 `api-server` 默认 `GENARRATIVE_API_WORKER_THREADS=4`,用于让 Tokio 在 2 vCPU 配额内有更多 I/O 调度 worker;该值不会突破 compose 里的 `cpus=2.0` CPU 上限。
容器默认 `GENARRATIVE_EXTERNAL_GENERATION_MODE=queue`,用于验证不经过 BgFilter 的 `api-server -> external_generation_job -> external-generation-worker` 链路;会触发 BgFilter 的任务不属于当前 compose 验收范围。如只想本地同步排查非 BgFilter provider / OSS / SpacetimeDB 写回,可在本机 env 临时改为 `inline`,但该模式不会覆盖 worker 动态扩缩容验证。
Collector 镜像使用 `otel/opentelemetry-collector-contrib:0.151.0`
@@ -56,7 +56,15 @@ Linux Docker Engine 若要从宿主机 CLI 连到容器内服务,直接用 `ht
## 构建工具链
`api-server` 容器镜像只构建 Linux release API 二进制,不构建 `spacetime-module`。当前 `api-server -> spacetime-client -> spacetimedb-sdk 2.7.0` 依赖链继续兼容 Rust 1.93,因此 `deploy/container/api-server.Dockerfile` 的 Rust builder 固定为 `rust:1.93-bookworm`。镜像构建阶段会同时复制 `public/`,用于满足 API 二进制里 `include_bytes!` 引用的内置素材;不要把 `public/generated-*` 放入镜像上下文。如果本机 Docker Hub 拉取失败,可以先在本机准备同名本地 builder 镜像,但不要把临时 bootstrap 容器或私有 registry 凭据写入仓库。
`api-server` 容器镜像只构建 Linux release API 二进制,不构建 `spacetime-module`。当前 `api-server -> spacetime-client -> spacetimedb-sdk 2.7.0` 依赖链继续兼容 Rust 1.93,因此 `deploy/container/api-server.Dockerfile` 的 Rust builder 固定为 `rust:1.93-bookworm`Web builder 显式安装并校验 npm `10.9.7`,再按唯一根 workspace lock 执行一次 `npm ci`,不依赖 Node 基础镜像隐含的 npm 版本。镜像构建阶段会同时复制 `public/`,用于满足 API 二进制里 `include_bytes!` 引用的内置素材;不要把 `public/generated-*` 放入镜像上下文。如果本机 Docker Hub 拉取失败,可以先在本机准备同名本地 builder 镜像,但不要把临时 bootstrap 容器或私有 registry 凭据写入仓库。
### Jenkins 预览 secrets 镜像边界
Jenkins 分支预览构建固定从宿主 `/data/jenkins/preview-secrets/.env.secrets.local` 读取 secrets。目录由 Jenkins 运行账号所有且权限为 `0700`,文件由同一账号所有且权限为 `0600`;构建入口对缺失、链接、非普通文件、owner 不匹配和过宽权限均失败关闭。不要把真实值写入本 README、仓库示例或 Jenkins 参数。
该文件不复制到源码 checkout 和 Docker build context,而是以 BuildKit `secret` mount 只提供给 `api-runtime` stage。构建会把它安装到 API 运行镜像的 `/srv/genarrative/.env.secrets.local`owner 为 `genarrative`、权限为 `0400`。Web builder、`nginx-runtime`、SpacetimeDB 和其它运行镜像不得获得该 mount 或目标文件;构建日志和 artifact 也不得回显或保存文件内容。容器的显式运行环境变量优先于该内置文件,可按预览实例覆盖其中的值。
修改宿主固定文件后必须重新构建并替换 API 镜像;重启旧容器不会读取宿主新内容。这个镜像不是可公开分发的无密钥产物:镜像持有者可以提取 `/srv/genarrative/.env.secrets.local`。只允许在当前受信任内网 Docker 主机使用,禁止 push 或 `docker save`、artifact 导出到跨信任边界的 registry、主机或存储。
### Gitea CI 预构建 Job 镜像
@@ -71,9 +79,9 @@ bash scripts/gitea-ci-job-image.sh export /仓库外受控路径/genarrative-git
bash scripts/gitea-ci-job-image.sh load-runner
```
默认构建 tag 为 `genarrative/gitea-project-ci:20260807.1`。脚本通过 NUL 分隔白名单 tar 流只发送 Dockerfile、checkout 脚本、根与 AI 游戏创作壳的 npm manifests/lock,以及 server-rs、桌面壳和 AI 游戏创作壳的 Cargo manifests/lock当前构建 context 约 `2.13 MB`不会把业务源码、素材或本地私密文件发送给 Docker daemon。镜像除固定工具链外,还按上述五份 lock 预热 npm / Cargo 下载缓存;三个 `cargo fetch --locked` 最多执行 5 次整命令级有界重试,再分别以断网 `cargo fetch --locked` 验证缓存闭合,不包含 `node_modules` 或 Cargo `target``build` 完成后会自动运行环境校验,`load-runner` 还会比对宿主和 runner 内层的完整 Image ID,并在内层执行 bwrap 与 Chrome headless canary。当前验证镜像约 `1.85 GB`,完整 Image ID 为 `sha256:8b4b30f5a096522942947927b06cf47bdb1a1016dde9a3573f9780e79d8e40cf`。执行这些命令不要求必须使用 root,但执行账号必须有权访问宿主 Docker API 并管理 runner 容器;没有该权限时交给 runner 运维人员执行。
默认构建 tag 为 `genarrative/gitea-project-ci:20260807.1`。脚本通过 NUL 分隔白名单 tar 流只发送 Dockerfile、checkout 脚本、根 workspace 的唯一 npm lock 与全部 workspace manifest,以及 server-rs、桌面壳和 AI 游戏创作壳的 Cargo manifests/lock;不会把业务源码、素材或本地私密文件发送给 Docker daemon。镜像显式安装并精确校验 `npm 10.9.7`,不依赖 Node 发行包隐含的 npm 版本;除固定工具链外,还按一份 npm workspace lock 与三份 Cargo lock 预热下载缓存。npm 只执行一次忽略 lifecycle scripts 的 workspace `npm ci`三个 `cargo fetch --locked` 最多执行 5 次整命令级有界重试,再分别以断网 `cargo fetch --locked` 验证缓存闭合,镜像不包含 `node_modules` 或 Cargo `target``build` 完成后会自动运行环境校验,`load-runner` 还会比对宿主和 runner 内层的完整 Image ID,并在内层执行 bwrap 与 Chrome headless canary。workspace lock 或 manifest 变化落地后必须按下述顺序重建并装载镜像;过渡期旧固定镜像缺少 `GENARRATIVE_GITEA_CI_NPM_VERSION` 时,校验只输出 `npm_version=partial` 和 Actions warning,继续由当前 job 的根 `npm ci` 验证唯一 lock,不能据此宣称 npm 版本或新依赖缓存已经闭合。执行这些命令不要求必须使用 root,但执行账号必须有权访问宿主 Docker API 并管理 runner 容器;没有该权限时交给 runner 运维人员执行。
runner 配置保留原 `ubuntu-latest` 映射,另外增加 `genarrative-ci:docker://sha256:8b4b30f5a096522942947927b06cf47bdb1a1016dde9a3573f9780e79d8e40cf`。内层 Docker 数据必须持久化,`force_pull` 保持 `false`;该精确 Image ID 在内层不存在时 job 应直接失败,不回退到浮动 tag 或现场拉取。四个 job 使用镜像内 `genarrative-gitea-checkout` 直接从当前 Gitea 拉取事件 commit,带 5 次有界重试,不再运行时下载 GitHub checkout action;随后以 `GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1` 执行 `scripts/check-gitea-ci-job-image.sh`,同时校验工具链、五份缓存锁命中状态、bwrap 和 Chrome headless。锁不匹配时校验会输出 `partial` 和醒目的 Actions warning,提示在可信分支落地后刷新镜像。各 job 仍运行干净的 `npm ci` 以校验当前 lockfile 并隔离 PR 依赖,但统一通过 `scripts/ci-npm-ci-with-retry.sh` 最多执行 3 次整命令级有界重试,并使用镜像内 npm cache 和 `prefer-offline`锁文件新增依赖时允许经受控网络补齐,本阶段不启用共享 Actions cache。
runner 配置保留原 `ubuntu-latest` 映射,`genarrative-ci` 继续映射到经 `build / verify / load-runner` 验证并写入配置的完整 Image ID。内层 Docker 数据必须持久化,`force_pull` 保持 `false`;该精确 Image ID 在内层不存在时 job 应直接失败,不回退到浮动 tag 或现场拉取。四个 job 使用镜像内 `genarrative-gitea-checkout` 直接从当前 Gitea 拉取事件 commit,带 5 次有界重试,不再运行时下载 GitHub checkout action;随后以 `GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1` 执行 `scripts/check-gitea-ci-job-image.sh`,同时校验工具链、一份 npm workspace 缓存锁、三份 Cargo 缓存锁、bwrap 和 Chrome headless。锁不匹配时校验会输出 `partial` 和醒目的 Actions warning,提示在可信分支落地后刷新镜像。各 job 仍各自运行一次干净的 `npm ci`,以唯一 workspace lock 校验全部 App/package/tool 依赖并隔离 PR 依赖统一通过 `scripts/ci-npm-ci-with-retry.sh` 最多执行 3 次整命令级有界重试,并使用镜像内 npm cache 和 `prefer-offline`锁文件新增依赖时允许经受控网络补齐,本阶段不启用共享 Actions cache。
更新顺序固定为:
@@ -245,5 +253,5 @@ npm run container:logs -- otelcol
- 不改生产 systemd 单元。
- 不改 Jenkins 发布主流程。
- 不要求真实 HTTPS 证书。
- 不把真实 `.env``.env.local``.env.secrets.local``deploy/container/api-server.env` 放入 Docker build context。
- 不把真实 `.env``.env.local``.env.secrets.local``deploy/container/api-server.env` 放入 Docker build contextJenkins 预览只使用上述 BuildKit secret 边界将固定文件内置到 `api-runtime`
- 不在容器镜像里内置 SpacetimeDB 数据或 token。
+23
View File
@@ -1,3 +1,5 @@
# syntax=docker/dockerfile:1.7
FROM rust:1.93-bookworm AS rust-builder
WORKDIR /workspace
@@ -21,6 +23,15 @@ COPY --from=rust-builder /tmp/api-server /usr/local/bin/api-server
RUN mkdir -p /var/lib/genarrative/auth /var/lib/genarrative/tracking-outbox /var/lib/genarrative/wallet-refund-outbox && \
chown -R genarrative:genarrative /srv/genarrative /var/lib/genarrative
ARG GENARRATIVE_PREVIEW_SECRETS_SHA256=
RUN --mount=type=secret,id=genarrative_preview_secrets,required=false \
if [ -n "${GENARRATIVE_PREVIEW_SECRETS_SHA256}" ]; then \
test -f /run/secrets/genarrative_preview_secrets; \
test "$(sha256sum /run/secrets/genarrative_preview_secrets | cut -d ' ' -f 1)" = "${GENARRATIVE_PREVIEW_SECRETS_SHA256}"; \
install -o genarrative -g genarrative -m 0400 \
/run/secrets/genarrative_preview_secrets /srv/genarrative/.env.secrets.local; \
fi
USER genarrative
EXPOSE 8082
@@ -34,8 +45,20 @@ CMD ["api-server"]
FROM node:22-bookworm-slim AS web-builder
WORKDIR /workspace
ARG NPM_VERSION=10.9.7
RUN npm install --global "npm@${NPM_VERSION}" --no-audit --no-fund && \
test "$(npm --version)" = "${NPM_VERSION}"
COPY package.json package-lock.json ./
COPY apps/admin-web/package.json ./apps/admin-web/package.json
COPY apps/ai-game-creator-shell/package.json ./apps/ai-game-creator-shell/package.json
COPY apps/desktop-shell/package.json ./apps/desktop-shell/package.json
COPY apps/mobile-shell/package.json ./apps/mobile-shell/package.json
COPY apps/preview-deployer-web/package.json ./apps/preview-deployer-web/package.json
COPY packages/image-canvas-core/package.json ./packages/image-canvas-core/package.json
COPY packages/image-canvas-react/package.json ./packages/image-canvas-react/package.json
COPY packages/shared/package.json ./packages/shared/package.json
COPY tools/spine-json-export-validator/package.json ./tools/spine-json-export-validator/package.json
RUN npm ci
COPY index.html metadata.json tsconfig.json vite.config.ts ./
+1 -1
View File
@@ -16,7 +16,7 @@ services:
"--non-interactive",
]
cpus: "1.0"
mem_limit: 896m
mem_limit: 2g
ports:
- "${GENARRATIVE_CONTAINER_SPACETIME_PORT:-13101}:3101"
volumes:
+20 -18
View File
@@ -56,6 +56,7 @@ FROM ${RUNNER_IMAGE}
ARG NODE_VERSION=22.23.1
ARG NODE_LINUX_X64_SHA256=9749e988f437343b7fa832c69ded82a312e41a03116d766797ac14f6f9eee578
ARG NPM_VERSION=10.9.7
ARG GOOGLE_CHROME_VERSION=150.0.7871.181-1
ARG GOOGLE_LINUX_SIGNING_KEY_FINGERPRINT=EB4C1BFD4F042F6DDDCCEC917721F63BD38B4796
@@ -122,26 +123,34 @@ RUN node_archive="node-v${NODE_VERSION}-linux-x64.tar.xz" \
&& ln -sfn /usr/local/lib/genarrative-node/bin/node /usr/local/bin/node \
&& ln -sfn /usr/local/lib/genarrative-node/bin/npm /usr/local/bin/npm \
&& ln -sfn /usr/local/lib/genarrative-node/bin/npx /usr/local/bin/npx \
&& ln -sfn /usr/local/lib/genarrative-node/bin/corepack /usr/local/bin/corepack
&& ln -sfn /usr/local/lib/genarrative-node/bin/corepack /usr/local/bin/corepack \
&& npm install --global "npm@${NPM_VERSION}" --no-audit --no-fund \
&& test "$(npm --version)" = "${NPM_VERSION}"
COPY --from=rust-dependency-cache /usr/local/cargo /usr/local/cargo
COPY --from=rust-dependency-cache /usr/local/rustup /usr/local/rustup
ARG NPM_LOCK_SHA256
ARG AGC_NPM_LOCK_SHA256
ARG SERVER_RUST_LOCK_SHA256
ARG DESKTOP_RUST_LOCK_SHA256
ARG AGC_RUST_LOCK_SHA256
COPY package.json package-lock.json /usr/local/share/genarrative-ci/npm/
COPY apps/ai-game-creator-shell/package.json apps/ai-game-creator-shell/package-lock.json /usr/local/share/genarrative-ci/agc-npm/
COPY apps/admin-web/package.json /usr/local/share/genarrative-ci/npm/apps/admin-web/package.json
COPY apps/ai-game-creator-shell/package.json /usr/local/share/genarrative-ci/npm/apps/ai-game-creator-shell/package.json
COPY apps/desktop-shell/package.json /usr/local/share/genarrative-ci/npm/apps/desktop-shell/package.json
COPY apps/mobile-shell/package.json /usr/local/share/genarrative-ci/npm/apps/mobile-shell/package.json
COPY apps/preview-deployer-web/package.json /usr/local/share/genarrative-ci/npm/apps/preview-deployer-web/package.json
COPY packages/image-canvas-core/package.json /usr/local/share/genarrative-ci/npm/packages/image-canvas-core/package.json
COPY packages/image-canvas-react/package.json /usr/local/share/genarrative-ci/npm/packages/image-canvas-react/package.json
COPY packages/shared/package.json /usr/local/share/genarrative-ci/npm/packages/shared/package.json
COPY tools/spine-json-export-validator/package.json /usr/local/share/genarrative-ci/npm/tools/spine-json-export-validator/package.json
COPY server-rs/Cargo.lock /usr/local/share/genarrative-ci/locks/server-rs.Cargo.lock
COPY apps/desktop-shell/src-tauri/Cargo.lock /usr/local/share/genarrative-ci/locks/desktop-shell.Cargo.lock
COPY apps/ai-game-creator-shell/src-tauri/Cargo.lock /usr/local/share/genarrative-ci/locks/ai-game-creator-shell.Cargo.lock
COPY deploy/container/gitea-ci-checkout.sh /usr/local/bin/genarrative-gitea-checkout
RUN test -n "${NPM_LOCK_SHA256}" \
&& test -n "${AGC_NPM_LOCK_SHA256}" \
&& test -n "${SERVER_RUST_LOCK_SHA256}" \
&& test -n "${DESKTOP_RUST_LOCK_SHA256}" \
&& test -n "${AGC_RUST_LOCK_SHA256}" \
@@ -149,10 +158,6 @@ RUN test -n "${NPM_LOCK_SHA256}" \
"${NPM_LOCK_SHA256}" \
/usr/local/share/genarrative-ci/npm/package-lock.json \
| sha256sum --check --strict \
&& printf '%s %s\n' \
"${AGC_NPM_LOCK_SHA256}" \
/usr/local/share/genarrative-ci/agc-npm/package-lock.json \
| sha256sum --check --strict \
&& printf '%s %s\n' \
"${SERVER_RUST_LOCK_SHA256}" \
/usr/local/share/genarrative-ci/locks/server-rs.Cargo.lock \
@@ -172,14 +177,11 @@ RUN test -n "${NPM_LOCK_SHA256}" \
--no-fund \
--prefer-offline \
--prefix /usr/local/share/genarrative-ci/npm \
&& rm -rf /usr/local/share/genarrative-ci/npm/node_modules \
&& npm ci \
--ignore-scripts \
--no-audit \
--no-fund \
--prefer-offline \
--prefix /usr/local/share/genarrative-ci/agc-npm \
&& rm -rf /usr/local/share/genarrative-ci/agc-npm/node_modules \
&& rm -rf \
/usr/local/share/genarrative-ci/npm/node_modules \
/usr/local/share/genarrative-ci/npm/apps/*/node_modules \
/usr/local/share/genarrative-ci/npm/packages/*/node_modules \
/usr/local/share/genarrative-ci/npm/tools/*/node_modules \
&& npm cache verify
RUN install -m 0755 /usr/local/cargo/bin/rustup /usr/local/bin/rustup \
@@ -188,10 +190,10 @@ RUN install -m 0755 /usr/local/cargo/bin/rustup /usr/local/bin/rustup \
done
ENV CARGO_HOME=/usr/local/cargo \
GENARRATIVE_GITEA_CI_AGC_NPM_LOCK_SHA256=${AGC_NPM_LOCK_SHA256} \
GENARRATIVE_GITEA_CI_AGC_RUST_LOCK_SHA256=${AGC_RUST_LOCK_SHA256} \
GENARRATIVE_GITEA_CI_DESKTOP_RUST_LOCK_SHA256=${DESKTOP_RUST_LOCK_SHA256} \
GENARRATIVE_GITEA_CI_NPM_LOCK_SHA256=${NPM_LOCK_SHA256} \
GENARRATIVE_GITEA_CI_NPM_VERSION=${NPM_VERSION} \
GENARRATIVE_GITEA_CI_SERVER_RUST_LOCK_SHA256=${SERVER_RUST_LOCK_SHA256} \
NPM_CONFIG_AUDIT=false \
NPM_CONFIG_FETCH_RETRIES=10 \
@@ -214,7 +216,7 @@ LABEL org.opencontainers.image.vendor="GenarrativeAI" \
com.genarrative.ci.definition-sha256="${IMAGE_REVISION}"
RUN node --version \
&& npm --version \
&& test "$(npm --version)" = "${NPM_VERSION}" \
&& rustup show active-toolchain \
&& rustc --version \
&& cargo --version \
@@ -12,13 +12,29 @@
!server-rs/crates/*/
!server-rs/crates/*/Cargo.toml
!apps/
!apps/admin-web/
!apps/admin-web/package.json
!apps/ai-game-creator-shell/
!apps/ai-game-creator-shell/package.json
!apps/ai-game-creator-shell/package-lock.json
!apps/ai-game-creator-shell/src-tauri/
!apps/ai-game-creator-shell/src-tauri/Cargo.toml
!apps/ai-game-creator-shell/src-tauri/Cargo.lock
!apps/desktop-shell/
!apps/desktop-shell/package.json
!apps/desktop-shell/src-tauri/
!apps/desktop-shell/src-tauri/Cargo.toml
!apps/desktop-shell/src-tauri/Cargo.lock
!apps/mobile-shell/
!apps/mobile-shell/package.json
!apps/preview-deployer-web/
!apps/preview-deployer-web/package.json
!packages/
!packages/image-canvas-core/
!packages/image-canvas-core/package.json
!packages/image-canvas-react/
!packages/image-canvas-react/package.json
!packages/shared/
!packages/shared/package.json
!tools/
!tools/spine-json-export-validator/
!tools/spine-json-export-validator/package.json
+1
View File
@@ -54,6 +54,7 @@
### 测试与协作
- [npm workspaces 统一依赖边界](./technical/【技术方案】npm-workspaces统一依赖边界-2026-08-21.md)
- [React 组件测试准则](./technical/【前端测试】React组件测试准则-2026-06-26.md)
- [AI Web 工程静态预览验收清单](./technical/【测试用例】AIWeb工程静态预览MVP验收清单-2026-06-13.md)
- [当前阶段规划](./planning/README.md)
@@ -1,5 +1,19 @@
# 决策记录
## 2026-08-20 UI Editor LLM 递归输出与参考图单文件限制
- 背景:结构识别、界面语义建议和多图合并直接把 LLM 工具 arguments 反序列化为递归树;结构识别与语义建议还在 async command 中同步读取并 base64 编码参考图。模型异常输出或过大图片可能造成不受控内存、栈和 async worker 占用。
- 决策:三个工具调用的 arguments 统一限制为 `1 MiB`,先解析通用 JSON 并迭代检查,再进入递归业务类型。结构识别按每棵树独立限制 `512` 个 LLM 节点 / `32` 层,不跨树求和且不计 Rust 页面根;语义建议限制 `4` 节点 / `4` 层;合并计划限制 `512` 节点 / `32` 层。超限整次拒绝,不截断或交付部分结果,日志不记录 arguments 正文。
- 输入边界:`merge_ui` 继续直接接收 `State`,不修改 Tauri/frontend IPC 参数;进入 Rust 后、发起 LLM 前按每棵源树独立限制 `512` 节点 / `32` 层,不跨树求和,并限制 `2 MiB` 序列化投影。UI 设计参考图只设单张 `5 MiB` 上限,不设批次合计或像素数上限;元数据检查、有限读取和 base64 编码进入 blocking worker,不新增命令超时。
- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`
## 2026-08-15 完美像素编码前按整数倍 nearest 放大到接近源图
- 背景:2026-08-10 起成功产物直接落逻辑网格 PNG,画布按资源实际宽高显示,结果会明显小于源图。用户要求保持逻辑图宽高比,并把产物放大到接近原图;禁止再走非整数 nearest 拉回精确源尺寸(会让逻辑块宽窄不一)。
- 决策:`style="pixelArt"` 与手动 `POST /api/editor/images/pixel-art-snaps` 仍共用 `snap_pixel_art_with_grid_policy`。检测、切线、采样、Alpha、strict 拒兜底不变。`resample` 之后、`encode_png` 之前,用单一整数 N 做 nearest 放大:`N*``(C·W + R·H) / (C² + R²)`,在 `floor` / `ceil`(小于 1 当 1)中取距离平方更小者,并列取较小 N;超单边 `10000` 或总像素 `8294400` 则降 N,最低 `N=1`。只持久化这一张 PNG。手动算法指纹升为 `perfect-pixel-v3`
- 不做:改 walker、透明补边、裁切、横纵不同倍率、Lanczos / bilinear、另存逻辑图、前端框缩放、失败路径、新测试。
- 关联文档:`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md``docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md``docs/【编辑器】画板图标素材生成入口设计-2026-06-15.md``docs/【编辑器】图片画布结构化持久化与迁移回滚方案-2026-07-19.md``docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`
## 2026-08-15 AGC game-chat 主代码 Run 接管直属美术 delivery
- 背景:真实 `gpt-5.6-sol / max` 验收中,`art-director` 失败后已形成 `ready + needs-repair` delivery,但认领、合同读取、claim observation 和完成 blocker 均硬编码为 Supervisor-only;实际直属父 Run `code-prototype` 无法消费回执,随后又发起 29 次 Provider 请求。
@@ -39,7 +53,6 @@
- 验证方式:覆盖 Codex 分类与敏感诱饵、失败事件公共摘要、最近任务与各正式卡片、game-chat 阶段记录、待核对状态、final-reply fallback 白名单及 malformed 响应完整重试;运行 Rust 定向测试、前端模型/AppSurface 定向测试、Shell typecheck、编码和 diff 门禁。
- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`
## 2026-08-10 资源管理评审阻塞项按第二轮正式合同修复
- 背景:资源管理第一轮实现后,人工验证继续暴露 WebView 默认缩放、预览队列饥饿、过滤后媒体残留播放、外层滚动串 scope、超深依赖坐标越过 Rust 上限和暂时错误无法重试等问题。部分 PRD / 技术方案仍描述第一轮的中央媒体预览、单全局 Overlay 和统一 section scope,已经与第二轮代码及验收结论冲突。
@@ -83,6 +96,7 @@
- 替代关系:本条替代下方 2026-08-07 阶段二中“缩放只等于可视高度”和“只为完整可见卡片建立端点”的显示口径;其中曾采用的全局 SVG、四 viewport 联合 clip 和单全局 Observer 又由上方“依赖 SVG 改为分区 plane 所有”决定替代。高度模型、四分区、会话隔离、内外滚动和无布局 CAS 等其它决定继续有效。阶段一媒体卡、阶段三确定性聚类、Rust `dependencyDepths`、producer 截断降级、历史手动坐标与 type sidecar 均不变。
- 验证方式:纯倍率模型覆盖按钮 / wheel 边界;AppSurface 覆盖项目、mode、section 隔离、普通 wheel、Ctrl wheel、WebKit gesture 与零布局写入;SVG 覆盖同 plane 倍率、橙色 marker、直线 / 小圆角横纵路由、自环间隙、双向边界继续线、分区原生裁剪、每区单 Observer、task-flow 零渲染和 4096 精确关系有界输出。
- 关联文档:`docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md``docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`
## 2026-08-11 Tauri 无限画布以可证明事务和分类恢复收口
- 终态与队列:资源编辑账本正式区分可继续阶段、`reconciliation-required``remote-failed``archived`。远端明确失败只保存稳定分类与终态时间,不得再 POST、轮询或重新扣费;只有该终态能由用户显式归档并移出活动恢复队列,归档保留账本且不伪装 `committed`。结果未知和需对账项继续失败关闭。
@@ -7204,6 +7218,31 @@
- HTTP 传输边界:根 H5 包不得依赖 Tauri guest 插件;AGC 独立包保留 `@tauri-apps/plugin-http`。Rust 插件显式关闭默认特性,只启用 `charset``cookies``http2``rustls-tls`,避免 `reqwest/system-proxy` 通过 Cargo feature union 把画布、Provider、Runtime 与本地回环夹具统一接入 OS 自动系统代理;如未来产品要求正式客户端继承系统代理,必须按各客户端明确设计并单独完成跨平台验证。
- 运行决策:Godot 项目提交给 Project Supervisor 时使用 `standard` Run Profile,避免触发 Web 专用 `game/index.html`、HTTP preview 与自主 Web 完成门。Godot 编辑器启动和内嵌运行预览不在本切片范围。
## 2026-08-18 UI 编辑器进入工作台主区域并保持三栏
- 布局决策:打开 UI 编辑器时复用现有 `uiEditorRoute`,将编辑器提升到项目工作台的主内容层;不再嵌在 `game-workbench-stage` 与聊天栏的并排布局中,项目工作台的 Agent dock 也不占用编辑器空间。应用壳顶部 chrome 与左侧全局导航保持现状。
- 三栏决策:Input / Preview / Inspector 永远同时存在,使用 Tailwind `minmax(0, 24fr) / minmax(0, 44fr) / minmax(0, 32fr)` 流式压缩;不得加入 JavaScript 窗口尺寸检测、设备分支或横向滚动。窄窗口下由 `min-w-0`、换行、纵向堆叠和各面板纵向滚动保持可操作性。
- 内部约束:编辑器内表单、Transform 网格、操作区和预览标题必须允许收缩与换行;禁止重新引入 `min-w-5xl`、固定三栏像素下限或会撑开工作区的面板最小宽度。
## 2026-08-18 UI Editor 从属页面、手势与保存失败边界
- 决策:`slave_to` 必须保持有向无环;前端状态层是交互时的最终不变量,Inspector 只作当前图及其后代的候选过滤,Rust UI State 持久化层再次拒绝环,防止绕过前端的坏状态落盘。
- 交互:节点拖拽和缩放使用单一内部手势状态机,任一时刻只允许一个指针手势。界面/树切换、pointer cancel 与卸载统一释放指针捕获;有限几何和正缩放是 State 更新的前置条件。
- 保存:冲突和异常显示在编辑器页面级错误条;未知异常只显示固定安全摘要。保存并返回仅在成功后离开,失败后保留返回确认,允许稳定重试。
- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md` 的“2026-08-18 UI Editor 从属页面、手势与保存失败边界”。
## 2026-08-18 UI Editor 左侧节点树跨界面移动
- 决策:左侧 UI 节点树使用 UI-only 的虚拟超级节点统一承载现有 `ui_trees` 页面根节点;超级节点和页面根节点不可拖动,普通节点可以在不同页面根节点之间移动,整个子树随节点移动并保留原 `transform`。跨界面“保留”只指复制原局部布局参数,不代表源、目标画布之间存在可比较的页面位置;不同界面图的画布尺寸、像素密度和父节点坐标空间独立,跨界面坐标换算不在本次范围内。
- 同界面重新挂载与跨界面移动分开处理:同界面重新挂载必须先做页面矩形到目标父节点局部坐标的换算,保持页面位置稳定;跨界面移动不做该换算,仅保留原局部 `transform`
- 状态边界:虚拟超级节点只改变树视图,不写入 UI Editor State 或持久化契约;跨界面移动不自动切换当前 active image。`NodeId` 在 State 内全局唯一,节点显示 / 隐藏直接使用页面会话级 `Set<NodeId>`,跨界面移动时隐藏状态随节点保留;清空、重新加载或节点失效时清理。跨界面坐标换算不在本次范围内,后续需单独设计。
- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md` 的“2026-08-18 UI Editor 左侧节点树跨界面移动”。
## 2026-08-17 UI Editor 文本字体来源显式化
- 决策:UI Editor 的 Rust 领域类型以 `FontSource::SystemFont | FontSource::Bound(FontAssetId)` 表达 Text 的字体来源,`SystemFont` 是有效的明确选择,不再以 nullable `font` 承担业务语义;TypeScript 类型必须由 `ts-rs` 从该 Rust 类型生成。
- 统计与校验:两种字体来源都计入一个已完成字体槽;只有 `Bound(id)` 校验对应字体资源存在。缺失图片资源仍是绑定总览中唯一的待处理槽,系统字体文本不进入待处理导航。
## 2026-08-14 AGC Web game-chat fresh-init 真实验收基线
- fresh-init 决策:`supervisor-game-chat-single-main-playable` 的 disposable 项目只预置 Git、`AGENTS.md`、三份隔离 evidence 与敏感诱饵,不再预写 `package.json``verify-e2e.mjs``game/index.html`;入口必须由正式 `--init` 写入生产 `DEFAULT_GAME_INDEX_HTML`。canonical 副本由无 Provider self-test 与 Rust 常量逐字节比对,防止验收基线静默漂移。
@@ -7233,7 +7272,6 @@
- 决策:只有同一可信根 Supervisor 能调用 `agent.acceptance_update`,且该控制面动作必须独占一轮,不能夹带 `plan_update`、legacy plan 或回复。requiredEvidence 采用 `tool:<Runtime 工具名>`,合同冻结前必须命中 Runtime 允许的持久证据工具集合并拒绝拼错、控制面和纯协调工具;动态 MCP catalog 不得冻结为不可变 requiredEvidence。passed 节点必须引用当前根任务树中对应工具的真实成功动作回执;回执同时记录动作执行边界的 `projectRevisionBefore / projectRevisionAfter`,非 mutation evidence 必须满足 before、after 与验收时 current revision 完全相同,mutation evidence 的 after 必须等于 current revision,旧 revision 或执行后延迟落账的回执不可重放。failed、not-observed、缺失节点以及落后当前 project revision 的整图确认状态均阻断普通完成、finalization 与恢复。项目 revision 变化后由 Supervisor 只更新受影响节点并确认当前图,未提交的 passed 节点保持不变;Runtime 不替 Supervisor 推断影响范围、选择具体 Agent 或实现方式。
- 决策:不可变 Goal Contract 的根 Run 收到 steer 时,必须按旧 rootRunId 串行化整个转换,并在持锁后重新确认该旧根仍是 Session 当前权威 Run,避免不同 steerId 并发创建多个 replacement。随后为绑定旧 rootRunId 的全部非终态静态、ready 和 isolated 后代写入取消栅栏、打断 Provider 并逐个收束,再终止旧根;所有项目修改入口在看到取消栅栏后立即失败关闭。Runtime 必须确认旧树所有成员都已进入终态或 `needs-reconciliation`,超时则保持等待并拒绝启动 replacement;只有旧树停稳后,才在同一 Session、source 和 Run Profile 启动新根 Run 重新理解完整目标。
## 2026-08-08 External v1 图片编辑来源字段允许原地收紧
- 背景:图片编辑主来源已经从可由客户端提交 objectKey 和类型提示的 `sourceImageSrc / sourceResourceId / assetKind`,收紧为服务端按项目资源 ID 或素材 ID 解析权威对象与类型的必填 `sourceReferenceId`。这会让严格 External v1 客户端立即失败,属于现役版本策略明确列出的 breaking change2026-07-31 的历史豁免不能自动覆盖本次变更。
@@ -8702,6 +8740,7 @@
- 2026-06-19 桌面壳外链打开 helper 共用:Tauri WebView 外域拦截和 HostBridge `app.openExternalUrl` 都必须复用 `open_normalized_desktop_external_url` 执行系统外链打开动作;HostBridge 分支仍先用 `normalize_external_url` 保留 payload 错误语义并把 opener 错误回传给 H5WebView 拦截保持 best-effort 静默处理。桌面壳配置检查会拒绝 `dispatch.rs` 直接调用 `app.opener().open_url` 绕过该 helper,避免两条离壳路径漂移。
> 2026-07-18 覆盖说明:本段后续关于微信 `navigation.openNativePage`、生成结果订阅页、`[subscribe-message]` 日志和订阅页路由门禁的 2026-06 决策均已由旧创作模板退役决策废止,只作为历史记录。Expo / Tauri 的同源 H5 受控导航及微信登录、支付、分享能力继续有效。
- 2026-06-20 H5 原生导航预校验:`navigateHostNativePage()``native_app` 下发送 `navigation.openNativePage` 前必须先拒绝空值、控制字符、协议相对 URL、外域绝对 URL 和非 `http:` / `https:` 协议目标;同源绝对 URL、`/path` 和保留给桌面壳兼容的相对 route 继续交给 Expo / Tauri 壳二次归一并补写宿主上下文。微信小程序分支仍按小程序页面 URL 语义走 `wx.miniProgram.navigateTo`,不套原生 App 同源 H5 预校验。根级 `npm run check:native-shells` 会反查 H5 facade 仍使用 `normalizeNativeAppPageUrl(...)` 且发送归一后的 URL,避免明显不安全目标触达原生壳。
- 2026-06-20 微信受控原生页能力声明:微信小程序壳真实 capability profile 声明 `navigation.openNativePage`,用于承接已经登记并测试的小程序原生页 flow;当前订阅生成结果通知页通过 H5 `requestGenerationResultSubscribePermission()` 调用 `navigateHostNativePage()` 打开 `/pages/subscribe-message/index`,小程序页再调用真实 `wx.requestSubscribeMessage` 并按既有结果协议回灌。根级 `npm run check:native-shells` 必须把该能力反查到共享 profile、微信 `WECHAT_HOST_CAPABILITIES` 镜像、订阅页协议常量、H5 入口、小程序 host-bridge / shell / page 文件和相关测试;该能力不代表开放任意小程序页面跳转。
- 2026-06-18 能力声明收紧:`packages/shared/src/contracts/hostBridge.ts` 提供 HostBridge method / capability 白名单,H5 的 `getHostRuntime()` 会解析并过滤 `hostCapabilities``openHostShare``writeHostClipboardText``requestHostHapticsImpact``setHostAppTitle``exportHostTextFile` 等 native 能力只在宿主声明对应 capability 后调用。发布分享弹窗只有声明 `share.open` 时才显示受控分享动作,并按 `hostShell` 区分 Expo 系统分享面板和 Tauri 剪贴板复制表达,避免旧壳或裁剪壳露出不可用入口。
@@ -14116,6 +14155,7 @@
- 分类:内部稳定 reason code 固定为 `translation_invalid / translation_upstream_failed / translation_budget_exhausted / elevenlabs_http_failed / invalid_audio / duration_probe_failed / oss_failed / writeback_failed`。MIME、空 body 和大小归 `invalid_audio`MP3 识别、帧读取和时长门禁归 `duration_probe_failed`。普通用户继续只读稳定短文案,不暴露 endpoint、上游正文或凭据。
- 跨入口:画布 Agent `generate-sound-effect` 与站内 / External v1 共用 canonical Prompt、固定模型、nullable `0.5-30` 小数时长和 Loop;省略 duration 为手动 `5s`,显式 null 为自动。SFX 参数解析必须保留该 null,不能被通用 null-default 兼容层改写。最终仍进入相同 `editor_sound_effect_generation` queue payload,不新增 Agent 专属链路。
- 发布边界:T6 工程实施和 mock / loopback 门禁不等于真实 provider 或生产验收。发布前关闭 SFX 入队,使用显式 `--server` / `--server-url` 只读查询 `external_generation_job` 中 pending / running 的 `editor_sound_effect_generation`,清零后按 api-server / Worker → Web 顺序部署并灰度;禁止 `--root-dir`、删除任务伪造 drain 或自动回退 Vidu。本次没有 SpacetimeDB schema、migration 或 bindings 变更。
## 2026-08-06 编辑器生成结果使用 durable receipt 与统一原子提交
- 背景:图片、改图、去背景、图集 / UI 多产物、角色动作、视频、音效和背景音乐在 OSS 结果可用后,仍分段 confirm object、创建 project resource / account asset、保存 canvas 和 complete job。任一中间失败都会留下部分业务事实;只把 `external_generation_job` 当 operation journal 又无法覆盖无 job 的 inline,也无法独立证明某批 resource/asset/canvas 已作为一笔提交完成。
@@ -14272,6 +14312,12 @@
- 2026-08-19 追加:同一默认认证切换也覆盖资源编辑和 autonomous main-loop fixture。`resource_editor` 的 External Editor 视频提交/轮询/服务身份恢复测试同样使用 task-local 凭据;平台账号语义测试使用隔离测试会话。main-loop 的视觉任务配置测试不再通过旧 `editorApi` 文件伪造登录态。所有 loopback listener 在 accept 时设有 5 秒 deadline,并把 accepted stream 恢复为 blocking,避免 Windows `WouldBlock(10035)` 或请求未发出时无限等待。
- 定向验证:`project::asset_canvas::generation::tests::` 14/14、`project::resource_editor::tests::` 36/36、`agent::runtime_driver::main_loop_tests::` 48/48、`agent::runtime_protocol::autonomous_completion_contract_tests::` 107/107 通过。此前一次 Windows 全量 Native Rust 为 1811 passed、108 failed、15 ignored;失败集合仍包含 Provider/mock 调度与既有专业链断言。HEAD 基线独立复现 `tests::project::generate_platform_art_asset_downloads_and_registers_external_image` 的同一登录态缺失,故不能把全量结果伪报为本次 fixture 修复引入;本次新增认证/accept deadline 相关用例均已隔离通过,最后两个 autonomous completion fixture 的认证迁移已单独通过,完整套件未在该两行测试改动后重新执行。
## 2026-08-20 AGC 本机开发者凭据目录 ACL 自动收紧
- 缺失当前服务器对应的本机开发者凭据时,客户端仍必须在请求远端创建 Key 前完成私有目录准备。既有 `~/.config/genarrative` 若 owner 已严格匹配当前进程 `TokenUser`,客户端自动把 DACL 收紧为禁止继承且仅当前用户 Full Control,用户不再需要手工执行 PowerShell ACL 修复。
- 自动收紧不等于接管:owner 不匹配、链接、reparse point、非目录或无法安全写入 DACL 时继续在远端请求前失败关闭;客户端不得删除、移动、覆盖或读取旧凭据内容,也不得因收紧失败自动创建远端 Key。
- Windows 回归测试必须构造“owner 为当前用户但仍继承 ACL”的既有目录,先证明严格校验失败,再通过正式目录准备入口收紧并复核私有 DACL。
## 2026-08-20 AGC 图片精修候选与最终图事务
- 只有可栅格编辑的图片资源拥有持续精修草稿;同一 `sourceAssetId` 只恢复一个活动 refine 草稿。生成成功只把 PNG 写入草稿私有 `draft-media` 并追加候选图层,不自动修改 manifest,也不自动关闭画布。
@@ -14280,27 +14326,18 @@
- 最终图事务完成或恢复后,草稿必须回到 `editing`,清空 `pendingCommit`,保留其它候选与 generation records,并在 `lastCommit.sourceLayerId / mediaSha256` 记录当前最终候选;不能把持续精修草稿永久停在 `committed`
- 资源详情保持非模态,不能卸载资源工具栏或背景画板。图片卡展示尺寸来自安全预览元数据,受 `220x180`、最小短边 `96``1:2..2:1` 约束;碰撞、世界范围和依赖连线共同消费同一实际矩形。
## 2026-08-20 AGC 本机开发者凭据目录 ACL 自动收紧
- 缺失当前服务器对应的本机开发者凭据时,客户端仍必须在请求远端创建 Key 前完成私有目录准备。既有 `~/.config/genarrative` 若 owner 已严格匹配当前进程 `TokenUser`,客户端自动把 DACL 收紧为禁止继承且仅当前用户 Full Control,用户不再需要手工执行 PowerShell ACL 修复。
- 自动收紧不等于接管:owner 不匹配、链接、reparse point、非目录或无法安全写入 DACL 时继续在远端请求前失败关闭;客户端不得删除、移动、覆盖或读取旧凭据内容,也不得因收紧失败自动创建远端 Key。
- Windows 回归测试必须构造“owner 为当前用户但仍继承 ACL”的既有目录,先证明严格校验失败,再通过正式目录准备入口收紧并复核私有 DACL。
# 2026-08-21 Game Agent 精修来源、批量导入与失败任务归档
## 2026-08-21 Game Agent 精修来源、批量导入与失败任务归档
- refine 主来源不能仅凭已有 Editor Resource ID 直接复用;历史 Game Agent 私有 kind 必须基于本地正式图片重新登记为 External v1 canonical kind,并使用绑定服务身份、owner、Editor Project、源 SHA256 和 canonical kind 的本地私有缓存避免重复上传。不得扩大 External v1 快速编辑白名单或向 edit DTO 补发 `assetKind`
- Tauri 图片导入采用原生多选与 Rust 批量事务:安全读取、媒体安装、图层绑定和一次 draft revision 推进必须作为一个可恢复单元,前端只 hydrate 权威 draft,不再依赖隐藏 input 加后续 autosave 完成正式绑定。
- 生成失败任务的“删除”固定为归档私有 ledger 并移出 draft 公开投影;只允许明确 `failed`,结果未知和 `reconciliation-required` 必须继续留在恢复队列。任务侧栏折叠是会话 UI 状态,不进入业务持久化。
## 2026-08-21 Game Agent 图片生成恢复与正式图来源身份
- 图片 generation 恢复是任务级后台工作:关键提交事务恢复和草稿 hydrate 完成后,画布立即进入 editing;`recoverImages` 继续恢复原 operation 并更新任务投影,但不得锁住整张画布。
- 精修候选设为最终图保持入口资产 ID。入口 manifest 已有有效 `source.resourceId` 时原样保留;只有未登记来源才使用 `local-asset:<assetId>`。提交与恢复回读使用同一解析规则。
- Game Agent 任务列表视觉复用现役美术画布的右上角独立按钮、白色面板、活动/完成双 Tab 和状态图标;失败归档是 Game Agent 的业务扩展。阻断性画布错误通过 body portal 覆盖整个 Tauri WebView。
## 2026-08-21 Game Agent 资源自由画板使用稳定隐藏边界
- 依赖画板的导航范围由当前项目全部资源的权威世界 extent 决定,并在屏幕坐标外扩 96px 安全留白;搜索过滤、详情卡和临时可见性只影响展示,不缩小导航边界。
@@ -14311,3 +14348,28 @@
- Direct Codex 的进度事件只表达加载文案,不作为资源事务真相;规范图、背景图、核心图集及已付费源图恢复只有在本地文件和 manifest 登记成功后,才发送现有 `game-creator-manifest-invalidated`,身份固定为 `direct-codex-art`
- App 继续复用按项目 single-flight 的 `refreshManifest`。单张资源提交事件负责生成中的即时投影;Direct Codex 命令无论成功、失败或超时 reject 都做一次最终 manifest 对账,失败路径不启动 preview,已提交资源不得被后续代码生成失败遮蔽。
- UI Editor 融合保留资源工作台 toolbar 和页面状态:UI 资源与普通资源一样先开详情卡,点击“编辑资源”后才进入 UI Editor;编辑器打开时使用单列全宽 stage,并禁止从父 toolbar 绕过未保存返回确认。
## 2026-08-22 Game Agent 精修最终图使用稳定运行入口
- 图片精修的 manifest 继续指向不可变正式版本 `assets/canvas/<name>--<commitId>.png`;游戏源码已引用的原路径(例如 `assets/direct-game-background.png`)是稳定运行入口,不要求代码改写。
- “设为最终图”提交、幂等重放和事务恢复都会校验不可变版本的字节、SHA-256、尺寸后刷新稳定入口。稳定入口缺失或损坏时,以不可变版本为恢复来源。
## 2026-08-21 JavaScript 工程统一为 npm workspaces
- 决策:根、Admin、AGC、Desktop、Mobile、Preview Deployer、三个 `packages/*` 和 Spine validator 统一进入显式 npm workspaces;固定 `packageManager=npm@10.9.7`,CI 镜像显式安装并校验同版 npm。仓库只提交根 `package-lock.json`,安装、CI、Jenkins 和容器缓存都只从根执行一次 `npm ci`
- 依赖边界:每个 workspace manifest 拥有自身直接依赖,根不再为子 App 重复声明。内部私有包使用匹配版本的普通 semver `0.1.0`,由 npm 自动链接;当前 npm 不接受 `workspace:*`。npm 默认 hoist,因此依赖所有权按 manifest 和 lock 的 workspace entry 检查,不能按统一 `node_modules` 或 lock 全局包条目判断。
- 原生边界:根 H5 与 Desktop manifest 继续禁止 Tauri JS guestAGC workspace 可以声明;统一 lock 出现 AGC guest 是合法聚合结果。Expo 沿用默认 npm monorepo 支持。AGC Cubone bundle、TypeScript、Tauri CLI 与 Windows Codex sidecar 都必须兼容根提升位置,不得依赖子 App 固定 `node_modules` 层级。
- 锁与平台:删除 AGC 和 Spine 子 lock;统一根 lock 必须保留 optional、bundled 和跨平台二进制节点。Linux 干净安装不能替代 Windows AGC sidecar、Android Expo/EAS 或可用 macOS/iOS runner 的平台构建证据。
- 权威方案:`docs/technical/【技术方案】npm-workspaces统一依赖边界-2026-08-21.md`
## 2026-08-22 完整容器 SpacetimeDB 内存上限统一为 2 GiB
- 决策:`deploy/container/docker-compose.loadtest.yml` 的 SpacetimeDB `mem_limit` 从旧压测采样值 `896m` 调整为 `2g`,与分支预览 override 一致;CPU、page pool、API、worker、Nginx 与 Collector 配额保持不变。
- 依据:当前完整模块首次 publish / init 的进程 RSS 会超过 `896m`cgroup 会直接 OOM kill `spacetimedb-standalone`,客户端表现为上传连接提前关闭,后续重试连接拒绝。提高 ping 或 publish 重试次数不能修复内存上限。
- 边界:这是本地/预发完整容器的模块实例化门槛,不修改生产服务资源合同;门禁同时锁定基础 Compose 与预览 override 均为 `2g`
## 2026-08-22 Jenkins 预览只向 API 运行镜像内置固定 secrets
- 决策:预览 secrets 权威源固定为 Jenkins 宿主 `/data/jenkins/preview-secrets/.env.secrets.local`;目录 / 文件由 Jenkins 运行账号所有且权限分别为 `0700` / `0600`,缺失、链接、非普通文件、owner 异常或权限过宽时构建失败关闭。
- 构建边界:只通过 BuildKit secret mount 把文件提供给 `api-runtime` stage,并安装为 `/srv/genarrative/.env.secrets.local` (`genarrative:genarrative`, `0400`)。文件不进 Git、build context、日志或 artifact,不进入 Web / Nginx、SpacetimeDB 或其它镜像。容器显式运行 env 优先覆盖内置值。
- 更新与分发:固定源文件更新后必须重建并替换镜像,只重启容器无效。镜像可读者必然可提取内置 secrets,因此只允许留在当前受信任内网 Docker 主机,禁止 push、`docker save` 或作为 artifact 导出到跨信任边界的 registry、主机或存储。
@@ -45,7 +45,7 @@ hermes
npm install
```
仓库当前不使用 npm workspaces,根目录 `npm install` 是统一安装入口。子包新增运行时依赖时,必须同步写入根 `package.json` 和根 `package-lock.json`;不能只修改子包 `package.json`
仓库使用 npm workspaces,根目录 `npm install` / `npm ci` 是全部 App、内部包和工具的唯一安装入口,唯一 npm lockfile 为根 `package-lock.json`。子包新增直接依赖时只修改所属 workspace 的 `package.json`,再从根更新 lock;禁止提交 workspace 内嵌套 `package-lock.json` 或继续执行子目录 `npm ci --prefix`。完整边界见 [`npm workspaces 统一依赖边界`](../../technical/【技术方案】npm-workspaces统一依赖边界-2026-08-21.md)
完整联调开发环境:
@@ -525,7 +525,7 @@ npm run check:native-shells
```
该命令会覆盖 H5 HostBridge 关键测试、微信 / Expo / Tauri 三端桥接层文件结构门禁、完整相对路径文档反查、微信 capability 到真实 WebView / 支付 / 分享页面流程和测试清单的映射门禁、H5 HostBridge 事件订阅双能力门控反查、H5 `navigation.canGoBack` 消费 hook 与直达二级页返回锚点测试、移动端和桌面端单端源码清单门禁、Expo 壳 typecheck / test / EAS build config smoke / config smoke / Metro export smoke、Tauri 壳 typecheck / cargo test、桌面壳 release `--no-bundle` 构建烟测,以及可分发壳与 H5 HostBridge 真实调用链的临时替身词扫描,确认 Expo managed config、移动端 EAS 原生包构建 profile、移动端 iOS / Android production bundle、打包 H5 资产、Tauri release 入口、H5 页面内导航保留完整原生宿主上下文和 H5 HostBridge 真实调用链没有漂移;扫描范围包含微信小程序壳生产 `.js`、Tauri `Info.plist`、共享 HostBridge 契约、H5 native transport,并自动覆盖已接入真实宿主能力 facade 的 H5 生产调用链文件,但不扫描 Expo export、Tauri `target/`、Cargo / Metro 缓存或 release 构建产物。移动壳配置检查必须反查 EAS 生产 profile、文本 / 文档 / 图片 / 音频导入边界都来自共享 HostBridge 契约。登录与支付外链跳转必须保持在该调用链扫描内,`src/services/authService.ts``src/services/payment/paymentRedirect.ts` 是必扫文件;`AuthGate` 的登录成功、退出登录、身份边界刷新和登录状态异常重试都必须通过 `app.reloadWebView` 优先路径,并由 `src/components/auth/AuthGate.test.tsx` 进入该门禁。壳源码和配置继续严格禁止 mock / fake / placeholder / stub / TODO / FIXME / 占位 / 模拟 / 伪造 / 未实现 / 临时;H5 业务调用链允许正常表单 `placeholder` 属性、业务占位图文案和真实兼容 / 故障语义中的“未实现”“临时”表述,但仍禁止 mock / fake / stub / TODO / FIXME / 模拟 / 伪造等替身痕迹。
根仓 Vitest 加载独立 AI 游戏客户端源码时,不得为了模块解析把 `@tauri-apps/api``@tauri-apps/plugin-*` 加入根 H5 依赖;根测试只通过 `vitest.config.ts` 的精确别名使用无副作用测试替身,独立客户端的正式 Tauri guest 依赖继续只由 `apps/ai-game-creator-shell/package.json` 与其 lock 管理。隔离 worktree 验收前需分别执行根 `npm ci``npm ci --prefix apps/ai-game-creator-shell`
根仓 Vitest 加载独立 AI 游戏客户端源码时,不得为了模块解析把 `@tauri-apps/api``@tauri-apps/plugin-*` 加入根 H5 或 Desktop workspace 的 manifest;根测试只通过 `vitest.config.ts` 的精确别名使用无副作用测试替身,正式 Tauri guest 只由 `apps/ai-game-creator-shell/package.json` 声明。统一根 lock 出现 AGC 的 Tauri guest 解析是正常聚合结果,依赖归属按 workspace manifest 检查。隔离 worktree 验收前只从仓库根执行一次 `npm ci`
反馈页上传凭证在原生壳声明 `file.importImage` 时必须优先走宿主图片导入;移动壳声明 `file.captureImage` 时才显示拍摄凭证入口,并把拍摄图片同样转为 `File` 后复用反馈页原有数量、大小、MIME、data URL 预览和提交 payload 校验。
Expo / Tauri 声明 `navigation.openNativePage` 时,只用于现役同源 H5 路由的受控导航和宿主上下文续接;微信小程序不再声明该能力。旧儿童动作 Demo、模板工作台、生成页、结果页和运行态不得作为 HostBridge 导航验收入口。
H5 支付链接跳转在原生壳声明 `app.openExternalUrl` 时必须优先走宿主系统浏览器;原生壳未接真实支付 SDK 前不得声明 `payment.request`,也不得把外部 H5 支付跳转伪装成原生支付成功。
+23 -8
View File
@@ -4203,11 +4203,11 @@
- 现象:`Repository checks``Frontend tests``Backend tests``Native shell tests` 都从全新 job 容器开始,apt、setup-node、rustup 和原生系统库在不同 job 里重复安装;后端与原生壳的安装时间可达数分钟,并把软件源和代理瞬时失败放大为四份。
- 原因:Gitea Actions job 彼此隔离,上一个 job 在容器内安装的包不会自动进入下一个 job;把同一套不随 PR 变化的工具链写在 workflow step 中,必然每次重做。
- 处理:用 `deploy/container/gitea-ci-job.Dockerfile` 预装 Node 22、Rust 1.96、`rustfmt`、Chrome、`bwrap``rg``ffmpeg``clang/lld` 和 Tauri / 后端系统依赖,并按锁预热根与 AI 游戏创作壳 npm、server-rs、桌面壳与 AI 游戏创作壳 Cargo 下载缓存。四个 job 统一 `runs-on: genarrative-ci`,先用镜像内脚本直接从 Gitea checkout,再以 runtime 模式运行 `scripts/check-gitea-ci-job-image.sh`,同时检查份缓存锁、工具链、完整 bwrap 与 Chrome headless。`RUSTUP_AUTO_INSTALL=0``rust-toolchain.toml` 变更时先重建镜像,不把下载 fallback 放回 job。
- 处理:用 `deploy/container/gitea-ci-job.Dockerfile` 预装 Node 22、固定 npm、Rust 1.96、`rustfmt`、Chrome、`bwrap``rg``ffmpeg``clang/lld` 和 Tauri / 后端系统依赖,并按锁预热唯一根 npm workspace、server-rs、桌面壳与 AI 游戏创作壳 Cargo 四份下载缓存。四个 job 统一 `runs-on: genarrative-ci`,先用镜像内脚本直接从 Gitea checkout,再以 runtime 模式运行 `scripts/check-gitea-ci-job-image.sh`,同时检查份缓存锁、工具链、完整 bwrap 与 Chrome headless。`RUSTUP_AUTO_INSTALL=0``rust-toolchain.toml` 变更时先重建镜像,不把下载 fallback 放回 job。
- 依赖边界:每个 job 仍必须各自执行 `npm ci`,让当前 lockfile 和 PR 依赖在干净环境中验证;区别是命中镜像 cache 时只做本地解包,锁新增依赖时才走受控网络。不要把 `node_modules` 或 Cargo `target` 烘进镜像,也不要向不受信任 PR 挂载跨 job 可写 cache。
- 锁漂移边界:runtime 校验输出任一 `*_cache_lock=partial` 说明镜像内 lock 与当前 checkout 不同,不代表新增依赖已经缓存;必须同时输出 Actions warning,提示可信分支落地后刷新镜像。必须在新镜像中对 server-rs、桌面壳和 AI 游戏创作壳当前 lock 执行真实 `cargo fetch --locked --offline``cargo metadata --no-deps` 不会证明依赖 archive 可用,不能作为替代。
- 构建网络边界:`CARGO_NET_RETRY` 只覆盖部分 crate 下载,registry `config.json` / index TLS 握手仍可能直接终止整次 fetch。Dockerfile 对每个 `cargo fetch --locked` 再做最多 5 次整命令级有界重试,最终仍执行断网 fetch,不能降低为无锁重试或省略离线闭合验证。
- 验证:workflow 不再出现 GitHub checkout action、apt、setup-node 或 rustup 安装 step;镜像能按份当前 lock 完成缓存闭合,四个 job 的环境校验、经 3 次整命令级有界重试保护的干净 `npm ci` 和原有测试门禁仍全部执行。
- 验证:workflow 不再出现 GitHub checkout action、apt、setup-node 或 rustup 安装 step;镜像能按份当前 lock 完成缓存闭合,四个 job 的环境校验、经 3 次整命令级有界重试保护的单次根 `npm ci` 和原有测试门禁仍全部执行。
## Gitea Actions HTTPS CONNECT 隧道必须双向收束 socket2026-08-07
@@ -4216,16 +4216,16 @@
- 处理:CONNECT 一开始就为 client socket 注册 `error / close`,解析完成后为 upstream socket注册同样的双向销毁处理;DNS 返回、写 200 和开始 pipe 前都检查 client 是否已销毁。任一端 error、close 或 timeout 都幂等 destroy 两端,不把普通客户端 reset 写成错误日志。不要用进程级 `uncaughtException` 吞掉问题,也不要只增加 npm/Cargo 重试掩盖 gateway 崩溃。
- 验证:在独立 canary 和正式 gateway 上分别并发制造至少 500 次“CONNECT 后立即断开”,随后确认容器仍运行、restart count 不增加、日志无 EPIPE;再通过同一 proxy 对 npm registry 与 crates index 建立完整 TLS 隧道。切换前仍须确认 Gitea 无活跃 run 且 Runner 内层无 job 容器。
## 独立 AGC lockfile 不能丢失可选 WASM 包的 bundled 依赖节点(2026-08-21
## 统一 npm workspace lock 不能丢失可选 WASM 包的 bundled 依赖节点(2026-08-21
- 现象:`npm ci --prefix apps/ai-game-creator-shell` 在安装前失败,报告 lockfile 缺少 `@emnapi/core` / `@emnapi/runtime`;错误版本可能是 registry 当前满足 `^1.11.1` 的最新版,而不是原 lock 中曾记录的版本。
- 原因:重写或解决 `apps/ai-game-creator-shell/package-lock.json` 冲突时,保留了 `@tailwindcss/oxide-wasm32-wasi` 对 bundled `@emnapi` 包的声明,却删掉了对应嵌套 package 节点。npm 会重新解析当前 registry 版本并判定 manifest 与 lock 不同步;这不是单一 npm 版本问题,也不表示应用应直接依赖两个 `@emnapi` 包。
- 处理:只在最新目标分支执行 `npm install --package-lock-only --ignore-scripts --prefix apps/ai-game-creator-shell`,保留 npm 对 bundled 节点及 `peer` / `optional` 标记的完整规范化结果;确认子包 `package.json` 没有变化,不要手工只补报错中的两个版本。
- 验证:至少用 Jenkins 对应 npm major 和当前开发 npm 分别执行干净的 `npm ci --prefix apps/ai-game-creator-shell`,再运行 AGC typecheck、编码检查和 `git diff --check`根目录 `npm ci` 不能替代独立子包 lock 验证
- 现象:`npm ci` 在安装前失败,报告统一 lock 缺少 `@emnapi/core` / `@emnapi/runtime`;错误版本可能是 registry 当前满足 `^1.11.1` 的最新版,而不是原 lock 中曾记录的版本。
- 原因:重写或解决根 workspace lock 冲突时,保留了 AGC 使用的 `@tailwindcss/oxide-wasm32-wasi` 对 bundled `@emnapi` 包的声明,却删掉了对应嵌套 package 节点。npm 会重新解析当前 registry 版本并判定 manifest 与 lock 不同步;这不是单一 npm 版本问题,也不表示应用应直接依赖两个 `@emnapi` 包。
- 处理:只在最新目标分支的仓库根执行固定 npm 的 `npm install --package-lock-only --ignore-scripts`,保留 npm 对全部 workspaces、bundled 节点及 `peer` / `optional` 标记的完整规范化结果;确认各 workspace manifest 没有意外变化,不要手工只补报错中的两个版本。
- 验证:至少用 Jenkins 对应固定 npm 和当前开发环境分别执行干净的 `npm ci`,核对 bundled 节点后再运行 `npm run check:npm-workspaces`AGC typecheck、编码检查和 `git diff --check`禁止恢复独立 AGC lock 或子目录 `npm ci`
## Windows 专属 Tauri resource 不能写进通用配置(2026-08-21)
- 现象:Linux CI 已完成 AGC `npm ci`,却在 Tauri custom build command 中报 `resources/codex/win-x64/...exe doesn't exist`Windows 侧车的 Rust staging 受 `cfg(windows)` 保护,因此非 Windows 构建不会生成这些文件。
- 现象:Linux CI 已完成根 workspace `npm ci`,却在 Tauri custom build command 中报 `resources/codex/win-x64/...exe doesn't exist`Windows 侧车的 Rust staging 受 `cfg(windows)` 保护,因此非 Windows 构建不会生成这些文件。
- 原因:Tauri 会在所有平台校验通用 `tauri.conf.json` 的 bundle resource 源路径;把 Windows x64 资源映射写进通用配置,等于要求 Linux / macOS 也预先拥有不属于其安装闭包的 Windows 可执行文件。
- 处理:通用配置只保留跨平台 bundle 项;Windows 原生侧车的完整白名单放入 Tauri 自动合并的 `tauri.windows.conf.json`。不要提交二进制占位文件,也不要让非 Windows build script 下载或伪造 Windows 资源。
- 验证:配置门禁断言通用配置没有 Windows resource、Windows 平台配置保留完整固定白名单;Linux 运行原生壳门禁必须越过 Tauri resource 校验,Windows release 仍由 build script 对 npm 原生包、SHA-256 清单和目标布局失败关闭。
@@ -4235,6 +4235,7 @@
- 现象:内置 Skill 文件集合没有缺失,原生测试却统一报内容指纹不匹配;另一个测试在 Linux 上把 `C:\\temp\\SKILL.md` 判为安全相对路径,受控资源工具可能继续处理 Windows 盘符或反斜杠遍历形式。
- 原因:审核文件定稿后未按最终字节重新生成 manifest SHA-256;同时 `std::path::Path` 只按当前宿主语义解析路径,Linux 不会把 Windows 盘符和反斜杠视为绝对路径或分隔符。
- 处理:Skill 文件变化与 manifest 指纹更新必须同次提交,并提升审核包版本;资源引用只接受使用 `/` 的普通相对段,显式拒绝反斜杠、冒号盘符、UNC、绝对路径和父目录段,再查询审核清单。不要先把反斜杠替换成 `/` 后再做安全检查。
- 回归补充:即使 Skill 文件本轮没有变化,也不能从旧提交或旧构建结果复制清单指纹;必须对当前工作树最终字节现场重算,并在提交前运行原生 Skill Pack 校验。运行时只报告排序后的首个不匹配项,不能据此假定其余 Skill 已通过。
- 验证:逐项按排序后的 `relativePath + NUL + file bytes + NUL` 重算并核对 manifestRust 单测同时覆盖 POSIX 绝对路径、`..``C:\\...``C:/...`、UNC 和反斜杠相对路径,受控 MCP 工具也必须把 Windows 绝对路径投影为 `isError=true`
## Gitea CI 预构建镜像不能只靠 tag 判断内容
@@ -4858,3 +4859,17 @@
- 原因:两个 Cargo workspace 拥有独立 target 和锁文件,AGC 又以 path dependency 复用若干 server-rs crate;更主要的是全量 / 分组 `cargo test` 继承增量编译,每组 crate / feature / profile hash 都可以留下新会话,Cargo 不会按仓库期望自动收缩这些历史目录。
- 处理:保留两个 workspace 的产品 / 发布边界;两边 `[profile.test]` 关闭 incremental 并固定 `debug=1`AGC dev profile 与 server-rs 对齐调试信息级别。日常用 `npm run audit:rust-build-cache` 只读核对;需要回收时先停止 Cargo / rustc,再显式运行 `npm run clean:rust-incremental -- --apply`,只删两个固定增量目录。
- 验证:清理前后各跑一次只读审计并核对磁盘可用空间;分别运行 server-rs 与 AGC 定向 `cargo test`,确认 test profile 不再生成持久 `debug/incremental` 堆积。共享 `CARGO_TARGET_DIR` 必须另做并发启动基准,不得为节省磁盘直接改变生产产物路径。
## BuildKit secret 不等于镜像内 secrets 不可提取(2026-08-22
- 现象:构建时使用 BuildKit secret mount,日志和普通 build context 都没有出现明文,于是误以为最终镜像也能不可提取地保存 secrets,随后将镜像 push 或导出给不同信任域。
- 原因:BuildKit secret mount 只避免秘密作为 `ARG` / `COPY` 进入构建上下文和中间指令;一旦 Dockerfile 把 mount 的内容安装到最终 rootfs,任何能读取、保存或运行该镜像的主体都可以提取它。
- 处理:预览固定 secrets 只从 Jenkins 宿主受控路径读取,严格校验目录 `0700`、文件 `0600`、owner、普通文件与非链接边界;只将其安装到 `api-runtime:/srv/genarrative/.env.secrets.local` 并设为 `0400`,明确排除 Nginx、Web、artifact 和其它镜像。镜像禁止推送或导出到跨信任边界。
- 更新与验证:源文件变更不会改动已存镜像,必须重建并替换容器;不能用重启代替。验收同时扫描 transcript/context/artifact 零泄漏,检查只有 API 最终 rootfs 存在目标文件,并验证容器显式运行 env 优先覆盖内置值。
## SpacetimeDB ping 健康不代表完整模块能在内存上限内实例化(2026-08-22)
- 现象:空库 `/v1/ping` 已成功且容器显示 healthy,但 `spacetime publish``Publishing module...` 后连接提前关闭,紧接着端口拒绝连接。
- 原因:当前完整模块 init 的 RSS 会超过基础 Compose 旧 `896m` cgroup 上限;内核 OOM kill SpacetimeDB,客户端只看到传输错误,容易被误判为网络竞态。
- 处理:先查 kernel journal 的 `Memory cgroup out of memory` 和目标容器 ID,再把本地/预发完整容器 SpacetimeDB 上限统一为 `2g`;保留 page pool 限制。不要只增加 publish 重试,也不要把 `/healthz` 或首页改成数据库就绪探针。
- 验证:用新空卷完成模块 publish、五服务启动和 Web/API smoke,并确认容器未 OOM、SpacetimeDB 与 API/Nginx 最终 healthy。
@@ -45,9 +45,17 @@ SpacetimeDB 与 OTLP 不映射宿主端口;Jenkins 通过受控 Compose 网络
SpacetimeDB 2.7 CLI 发布到受控 Compose 网络地址时固定使用 `--yes=remote,migrate,break-clients`,避免 Jenkins 等待非本地目标交互确认;该预览路径不传 `--delete-data`。Jenkins 同时固定 `GENARRATIVE_PREVIEW_WEB_HOST=192.168.35.82`,不得用默认路由自动探测结果生成页面链接,以免 VPN 或容器网卡地址泄漏到同事可见 URL。
预览 Compose override 将 SpacetimeDB 内存上限设为 `2g`基础 loadtest Compose 的 `896m` 是压测采样口径,当前完整模块首次发布和实例化会超过该上限;预览环境若沿用值,容器会被 cgroup OOM 杀死并使模块上传中断。该覆盖只作用于分支预览实例,不修改生产或压测基线
基础 loadtest Compose 与预览 Compose override 将 SpacetimeDB 内存上限设为 `2g`当前完整模块首次发布和实例化的 cgroup 峰值会超过旧 `896m` 上限;完整容器或预览环境若沿用值,容器会被 OOM 杀死并使模块上传中断。预览 override 继续显式锁定该值并取消宿主端口映射;这不修改生产服务资源合同
每个预览实例使用独立 SpacetimeDB 空库,不继承生产账号和短信凭据。Jenkins 在实例私有 `api-server.env` 中开启预览专用认证:未注册的中国大陆手机号首次使用 6 到 128 位密码时自动创建预览账号;短信入口使用 `mock` provider 与固定预览验证码 `123456`,不向真实手机发送短信。该设置不写入公共 env 示例生产配置或镜像层;实例重建会重建独立数据库,原预览账号不保留。
每个预览实例使用独立 SpacetimeDB 空库,不继承生产账号据。Jenkins 在实例私有 `api-server.env` 中开启预览专用认证:未注册的中国大陆手机号首次使用 6 到 128 位密码时自动创建预览账号;短信入口强制使用 `mock` provider 与固定预览验证码 `123456`,不调用镜像内置的真实短信凭据。该预览认证覆盖不写入公共 env 示例生产配置;实例重建会重建独立数据库,原预览账号不保留。
## 预览 secrets 内置
Jenkins 节点上的预览 secrets 权威来源固定为 `/data/jenkins/preview-secrets/.env.secrets.local`。该文件不进 Git、Docker build context、构建日志或 artifact;构建时只通过 BuildKit `secret` mount 临时提供给 `api-runtime` stage,并在该运行镜像中安装为 `/srv/genarrative/.env.secrets.local`,权限固定为 `0400``nginx-runtime`、Web 静态产物、SpacetimeDB 镜像及其它镜像不得包含该文件。
宿主固定目录应由 Jenkins 运行账号所有且权限为 `0700`,源文件权限为 `0600`;缺失、不是普通文件、owner 不匹配或权限过宽时,预览构建必须失败关闭。源文件变更后必须重新构建并替换预览镜像,只重启容器不会刷新已内置的内容。容器启动时显式注入的运行环境变量优先级高于镜像内的 `.env.secrets.local`,用于按实例覆盖非通用值。
这种方案只隐藏构建传输过程,不能让内置后的 secrets 对镜像持有者保密:能读取、保存或运行 `api-runtime` 镜像的人可以提取该文件。因此该镜像只能留在当前受信任内网 Docker 主机,禁止 push 到公共或跨信任边界的 registry,也禁止通过 `docker save`/构建 artifact 导出传播。需要跨边界分发时必须改用不含 secrets 的镜像与运行时密钥注入。
## Jenkins 参数与产物
@@ -110,6 +118,7 @@ Jenkins 在构建完成、归档 artifact 和更新 REST 状态之间可能短
- Jenkins service account 只授予 `shared/Genarrative-Preview-Deployer``Job/Read``Job/Build` 和读取构建产物所需权限,不授 `Overall/Administer``Job/Configure``Job/Delete`
- 后端固定 Jenkins origin、Job 路径和参数白名单;客户端不能传 URL、Job 名、Compose project、容器名、宿主端口或 Jenkins 凭据。
- Git 查询固定使用本机 Gitea SSH 地址和服务端只读凭据;客户端不能传 remote、SSH 参数或凭据。Git 缓存只写入预览控制服务的受控状态目录,搜索接口需要控制台会话且结果有数量上限。
- 预览 secrets 只从固定宿主路径读取,构建前校验 owner、类型和权限;不允许分支、Jenkins 参数或控制面请求改写 secrets 路径、BuildKit secret ID 或镜像内目标路径。
- Jenkins POST 支持动态 CrumbAPI Token 即使免 Crumb,也不能把 Token 放进 URL 或日志。
- API 默认只接受同源请求,写请求校验 Origin;内网本身不作为认证。
- 同一 deployment 的发布和卸载串行执行;重复请求必须幂等或明确返回冲突。
@@ -120,5 +129,6 @@ Jenkins 在构建完成、归档 artifact 和更新 REST 状态之间可能短
- 后端:输入校验、登录会话、Origin、Crumb、Jenkins `401/403/404/5xx`、queue 到 build 状态机、artifact schema、卸载所有权和幂等测试。
- 前端:登录、分支与可选 commit、自动刷新、排队/构建/成功/失败状态、内网链接、卸载确认和刷新恢复测试。
- Jenkins:两个分支依次发布后在不同端口并存;同一分支换 commit 优先复用端口;非分支 commit 被拒绝;卸载只删除目标实例并释放端口。
- 预览认证:新手机号可以首次密码登录并重复使用同一密码;错误密码被拒绝;获取验证码后使用 `123456` 可完成登录;容器不包含生产短信凭据。
- 预览认证:新手机号可以首次密码登录并重复使用同一密码;错误密码被拒绝;获取验证码后使用 `123456` 可完成登录;短信入口保持 `mock` 且不调用内置的真实短信凭据。
- secrets:构建 transcript、context 和 artifact 零出现原文;只有 `api-runtime` 包含 `0400` 的目标文件,`nginx-runtime` 与其它镜像均不包含;修改固定源文件后旧镜像不变、重建新镜像后摘要更新;显式运行 env 可覆盖内置值。
- 通用:`npm run check:encoding`、相关 typecheck/build/test、Rust 定向测试和 `git diff --check`
@@ -1207,3 +1207,10 @@ game-project/
- **真实 Provider 开发验收入口**`--game-chat-smoke` 是受限 CLI 标记,只允许与默认 `project-supervisor``--swarm-chat --autonomous-game-build` 组合,将新根 Run 绑定为 `project-supervisor-game-chat`,不作为产品 UI、公开 API 或通用 source 覆盖能力。现有 playable harness 通过 `npm run ai-game-creator-shell:agent-runtime:supervisor-game-chat-single-main-playable-real-e2e` 显式启动该模式,启动前同时校验 `project-supervisor``code-prototype` 的 Provider 配置,并在隔离 AppData 中验收唯一单主 child、当前 revision 的 static smoke 及 desktop/mobile 试玩回执。只有显式执行这条真实 E2E 命令才会发起 Provider 请求;普通 self-test 不读取凭据、不调用 Provider。现场 smoke 若在配置门因缺少 API Key 阻断,必须报告 `providerUsed=false`,只能证明入口、验收逻辑与无 Provider 自测已落地,不能宣称真实现场验收完成。
- **fresh-init 取证边界**`supervisor-game-chat-single-main-playable` 不再预写 `package.json``verify-e2e.mjs``game/index.html`,由正式 `--init` 生成生产 `DEFAULT_GAME_INDEX_HTML`。self-test 必须逐字节核对 harness 中的 canonical 默认入口与 Rust 生产常量,并证明空项目仍保留 Git、`AGENTS.md`、隔离 evidence 和敏感诱饵基线;真实报告必须同时证明初始 SHA-256 命中生产默认入口、根下唯一固定 child 为 `code-prototype`、最终入口已变化、static-smoke SHA-256 绑定最终入口且 desktop/mobile 各自通过。
- **不外推范围**:上述真实 Provider 命令只验收普通 Web 工作台所采用的 `project-supervisor-game-chat + autonomous-game-build` 单 Supervisor 链。显式 `professional-dag` 与固定 16 节点 CLI/GUI 链的 owner-artifact verify、产物所有权和 path-scope 仍是独立未解决项;seeded deterministic E2E、普通 self-test 或本 game-chat 报告都不得冒充该链已修复或已完成真实验收。
## 2026-08-21 npm workspace 安装与打包边界
- AGC 进入仓库根 npm workspaces`apps/ai-game-creator-shell/package.json` 继续独占其 Tauri guest、Codex CLI 和 App 直接依赖,但不再维护独立 `package-lock.json`。开发、CI、Jenkins 与打包前只从仓库根执行一次 `npm ci`;禁止恢复子目录独立安装。
- 统一根 lock 出现 `@tauri-apps/api``@tauri-apps/plugin-*``@openai/codex` 是 AGC workspace 的合法聚合结果,不代表根 H5 或 Desktop 获得这些能力。配置门禁必须按 workspace manifest/lock entry 判断归属。
- npm 默认 hoist。Vite/Vitest 对 `@cubone/react-file-manager` 的已发布 ESM bundle、AGC TypeScript/Tauri CLI 和 Windows `@openai/codex-win32-x64` sidecar 解析必须兼容 workspace 本地与根提升位置,不得硬编码依赖只存在于 `apps/ai-game-creator-shell/node_modules`
- Windows game-chat release 仍必须核对固定 Codex 文件和 SHA-256 manifestLinux 根 lock 与 typecheck 通过不能替代 Windows sidecar 打包 smoke。完整安装、锁与 CI 口径见 [`npm workspaces 统一依赖边界`](./【技术方案】npm-workspaces统一依赖边界-2026-08-21.md)。
@@ -0,0 +1,126 @@
# npm workspaces 统一依赖边界
更新时间:`2026-08-21`
## 目标
Genarrative 的 JavaScript 工程统一使用 npm workspaces。仓库只提交根 `package-lock.json`,开发、CI、Jenkins 和容器构建都从仓库根执行一次干净安装;各 App、内部包和工具仍由自己的 `package.json` 声明直接依赖和脚本。
本方案只迁移包管理与依赖边界,不切换 pnpm,不改变 Cargo workspace、SpacetimeDB schema、前后端 DTO 或业务运行时。
## Workspace 范围
`package.json` 固定声明:
```json
{
"packageManager": "npm@10.9.7",
"workspaces": [
"apps/admin-web",
"apps/ai-game-creator-shell",
"apps/desktop-shell",
"apps/mobile-shell",
"apps/preview-deployer-web",
"packages/image-canvas-core",
"packages/image-canvas-react",
"packages/shared",
"tools/spine-json-export-validator"
]
}
```
当前纳入:
- `apps/admin-web`
- `apps/ai-game-creator-shell`
- `apps/desktop-shell`
- `apps/mobile-shell`
- `apps/preview-deployer-web`
- `packages/image-canvas-core`
- `packages/image-canvas-react`
- `packages/shared`
- `tools/spine-json-export-validator`
`.rag/runtime``.worktrees/``tmp/`、构建目录和各级 `node_modules` 不属于 workspace。RAG 继续保持独立、gitignored 的本地运行时,不进入根依赖。
## Manifest 与依赖所有权
- 根 manifest 只声明根 H5、仓库级脚本和统一测试/格式化工具的直接依赖,不再为 Mobile、AGC 或工具重复声明其专属依赖。
- 每个 App 在自己的 manifest 声明运行时直接依赖;测试只由根统一 runner 承担的工具可以留在根,App 自己提供测试脚本时必须声明其直接测试依赖。
- `@genarrative/image-canvas-react` 必须显式依赖 `@genarrative/image-canvas-core`;主站和 AGC 必须显式声明它们直接消费的内部画布包。
- npm `10.9.7` 不支持依赖值 `workspace:*`。内部 workspace 依赖使用匹配本地包版本的普通 semver,例如 `"@genarrative/image-canvas-core": "0.1.0"`npm 在根安装时自动生成本地 `link`
- npm 默认提升依赖,统一根 `node_modules` 中存在某包不代表根 H5 拥有该依赖。归属门禁必须检查对应 workspace manifest 和根 lock 中的 workspace package entry,不能按统一 lock 的全局 `node_modules/*` 条目判断归属。
`packages/shared` 本次纳入统一安装和 lock,但不顺手重写现有源码 import;后续若要把所有相对源码引用改为 `@genarrative/shared`,需先补完整 exports/build 合同并单独实施。
## 唯一 Lockfile
- 唯一权威 npm lockfile 为根 `package-lock.json`
- 删除 `apps/ai-game-creator-shell/package-lock.json``tools/spine-json-export-validator/package-lock.json`
- 新增或修改任一 workspace 依赖后,只能从仓库根使用固定 npm 版本更新根 lock。
- 仓库门禁必须校验 workspace 清单、固定 `packageManager`、根 lock 的 workspace package/link 条目,并拒绝受管 workspace 再提交嵌套 `package-lock.json``npm-shrinkwrap.json`
- optional dependency、平台二进制和 bundled dependency 仍由根 lock 完整记录;Linux 生成 lock 后仍需 Windows/macOS 对应构建门禁,不能把单平台安装等同于跨平台通过。
## 安装与脚本
标准安装入口:
```bash
npm ci
```
开发者首次拉取或主动更新依赖时可使用根 `npm install`。Husky 的 `prepare` 只在根执行一次;各 workspace 不重复安装 hook。
现有 `npm run agc``npm run mobile-shell:*``npm run desktop-shell:*``npm run preview-deployer:web:*``npm run spine-export-validator:*` 等对外入口保持名称稳定。内部可继续使用 `npm --prefix`,或使用 `npm run <script> --workspace=<name>`;无论哪种写法,都必须保证 Expo、EAS 和 Tauri 命令在目标 App cwd 中执行。
AGC 的 TypeScript、Vite/Vitest bundle 和 Windows Codex sidecar 不允许硬编码依赖一定位于子 App 的 `node_modules`。解析必须兼容 npm 将依赖提升到根安装树,并在错误文案中统一要求从仓库根安装。
## 原生壳边界
- Mobile workspace 继续使用 Expo 默认 Metro 配置;当前不增加 `node-linker` 或自定义 `watchFolders`。首次迁移验收清理 Metro cache。
- Desktop H5 仍不得声明 `@tauri-apps/api` 或任何 `@tauri-apps/plugin-*` guest 依赖。
- AGC 可以在自己的 workspace manifest 声明 Tauri guest;统一根 lock 出现这些解析包是正常结果,不代表 Desktop 或根 H5 获得 guest 权限。
- 根 H5 manifest 也不得直接声明 Tauri guest。配置门禁按根、Desktop、AGC 三个 manifest 的所有权分别判断。
- Windows AGC release 必须从 workspace 或根提升位置找到 `@openai/codex-win32-x64` 并把固定 sidecar 资源打包;Windows 构建 smoke 是迁移完成条件,不由 Linux lock 检查替代。
## CI、Jenkins 与容器
- Gitea 四个 job 每个只执行一次带重试的根 `npm ci`,不再单独安装 AGC。
- Jenkins Web Build 在安装前必须精确校验 npm `10.9.7``RUN_NPM_CI` 只控制一次根 `npm ci`
- Gitea CI 镜像只维护一个 npm lock SHA 与一份 npm cache;预热上下文必须包含根 lock 和全部 workspace manifests,使根 `npm ci` 能解析 workspace。
- CI 镜像仍分别维护 server-rs、Desktop Tauri、AGC Tauri 三份 Cargo lock cachenpm 单锁不改变 Rust lock 边界。
- API 镜像的 Web builder 必须显式安装并校验 npm `10.9.7`,再复制全部 workspace manifests、执行根 `npm ci`,之后才复制源码并构建主站与后台。
- 根 lock 或任一 workspace manifest 变化都需要刷新 CI 镜像 npm cache。缓存未命中只能报告 `partial` 并受控补齐,不能跳过当前 lock 的干净安装。
## 验收
最低本地门禁:
```bash
npm ci
npm ls --workspaces --include-workspace-root --depth=0
npm run check:npm-workspaces
npm run typecheck
npm run test
npm run admin-web:typecheck
npm run preview-deployer:web:test
npm run mobile-shell:typecheck
npm run mobile-shell:test
npm run desktop-shell:typecheck
npm run ai-game-creator-shell:typecheck
npm run spine-export-validator:typecheck
npm run check:native-shells
npm run check:repository-ci
npm run check:production-ops
npm run check:encoding
git diff --check
```
干净安装验收必须在没有历史根或子 App `node_modules` 的隔离工作树执行。平台补充门禁:
- Linux:根、后台、预览部署器、Spine 工具构建,Desktop/AGC Tauri release smoke。
- Windows x64:根 `npm ci` 后执行 AGC game-chat release,核对 Codex sidecar 完整性。
- AndroidExpo config/export,并在可用 EAS 环境执行一次本地 Android build。
- macOS/iOS:在可用 runner 执行 simulator build;缺少 runner 时必须标记未验证。
任何 workspace 仍需要第二次 `npm ci --prefix`、嵌套 lock、未声明直接依赖或依赖某个固定 `node_modules` 层级时,迁移都不能视为完成。

Some files were not shown because too many files have changed in this diff Show More