Merge branch 'master' into codex/fix-agc-skill-fingerprints
Project CI / Native shell tests (pull_request) Failing after 2m52s
Project CI / Repository checks (pull_request) Successful in 4m42s
Project CI / Frontend tests (pull_request) Successful in 5m4s
Project CI / Backend tests (pull_request) Successful in 6m34s

This commit is contained in:
2026-08-22 16:53:18 +08:00
47 changed files with 3485 additions and 7144 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");
+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)
@@ -14317,3 +14317,23 @@
- 缺失当前服务器对应的本机开发者凭据时,客户端仍必须在请求远端创建 Key 前完成私有目录准备。既有 `~/.config/genarrative` 若 owner 已严格匹配当前进程 `TokenUser`,客户端自动把 DACL 收紧为禁止继承且仅当前用户 Full Control,用户不再需要手工执行 PowerShell ACL 修复。
- 自动收紧不等于接管:owner 不匹配、链接、reparse point、非目录或无法安全写入 DACL 时继续在远端请求前失败关闭;客户端不得删除、移动、覆盖或读取旧凭据内容,也不得因收紧失败自动创建远端 Key。
- Windows 回归测试必须构造“owner 为当前用户但仍继承 ACL”的既有目录,先证明严格校验失败,再通过正式目录准备入口收紧并复核私有 DACL。
## 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 支付跳转伪装成原生支付成功。
+22 -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 清单和目标布局失败关闭。
@@ -4825,3 +4825,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`
@@ -1206,3 +1206,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` 层级时,迁移都不能视为完成。
@@ -409,13 +409,13 @@ GameBridge 禁止:
2026-06-19 追加:移动壳 H5 入口 query 和 `host.getRuntime` 回包统一读取 `MOBILE_SHELL_HOST_VERSION`,该值由移动壳 `app.json` 的 Expo `version` 配置解析,异常配置只回退到与 `app.json` / `package.json` 一致的受检 fallback。配置检查会拒绝在 `apps/mobile-shell/src/shell/ShellApp.tsx``apps/mobile-shell/src/host-bridge/appearance.ts``apps/mobile-shell/src/host-bridge/badge.ts``apps/mobile-shell/src/host-bridge/bridge.ts``runtime.ts` 内重新散落硬编码版本,避免升级移动安装包时 H5 首屏上下文和宿主 runtime 回读版本不一致。该收口不引入 `expo-constants`、OTA 更新、渠道分发或应用安装信息业务。
2026-06-18 追加:移动壳默认显式关闭 Expo OTA 更新,直到存在真实发布通道、更新端点、签名 / 回滚策略和团队发布流程后再接入。`app.json` 只允许 `updates.enabled=false`,不得配置 `runtimeVersion`、release channel、EAS channel、`expo-updates` 插件或移动端 crash / analytics / CodePush 依赖;`apps/mobile-shell/scripts/check-config.mjs` 和 Expo public config smoke 会共同拒绝这些发布通道能力被提前打开,移动壳生产入口、HostBridge 和 URL/runtime 配置也不得提前初始化 Sentry、Firebase Analytics、PostHog、Amplitude、Segment、CodePush 或 Expo Updates。由于移动壳运行依赖会从根安装树解析,根 H5 `package.json` 也不得直接安装这些移动端发布通道、崩溃上报、analytics 或 CodePush SDK;根 `package-lock.json` 也不得解析 `expo-updates`、Sentry、Firebase Analytics、PostHog、Amplitude、Segment、CodePush 等真实发布 / 观测 SDK`expo-application` 可能由 Expo 自身传递解析,但项目不得把它作为 direct dependency 主动用于渠道逻辑。
2026-06-18 追加2026-08-21 按 npm workspaces 更新:移动壳默认显式关闭 Expo OTA 更新,直到存在真实发布通道、更新端点、签名 / 回滚策略和团队发布流程后再接入。`app.json` 只允许 `updates.enabled=false`,不得配置 `runtimeVersion`、release channel、EAS channel、`expo-updates` 插件或移动端 crash / analytics / CodePush 依赖;`apps/mobile-shell/scripts/check-config.mjs` 和 Expo public config smoke 会共同拒绝这些发布通道能力被提前打开,移动壳生产入口、HostBridge 和 URL/runtime 配置也不得提前初始化 Sentry、Firebase Analytics、PostHog、Amplitude、Segment、CodePush 或 Expo Updates。根 H5 与 Mobile workspace 的 manifest 都不得直接声明这些 SDK;统一根 lock 可能因其它合法传递依赖出现同名包,禁用能力归属应检查 workspace manifest 和 Mobile 依赖闭包,不能把聚合 lock 的全局存在性直接当成 Mobile 启用`expo-application` 可能由 Expo 自身传递解析,但项目不得把它作为 direct dependency 主动用于渠道逻辑。
2026-06-18 追加:移动壳可分发身份、外观和默认权限进入配置门禁。Expo `name` 固定为 `Genarrative``slug` 固定为 `genarrative-mobile-shell``userInterfaceStyle` 固定为 `automatic``assetBundlePatterns` 固定为 `["**/*"]``extra.genarrativeHostBridgeVersion` 固定为 `1`;源 `app.json` 的 Android `permissions` 只允许显式声明 `android.permission.RECORD_AUDIO`,用于同源 H5 实时声音玩法,最终 Expo public config 只允许 `android.permission.CAMERA``android.permission.RECORD_AUDIO` 两类权限,其中相机来自扫码 / 拍摄能力,麦克风来自同源 H5 实时玩法。所有其它当前不需要的高风险权限只能通过 `blockedPermissions` 阻断。后续新增权限必须先有真实宿主能力、系统权限说明和 H5 fallback 方案,再补配置与检查。`apps/mobile-shell/scripts/check-config.mjs` 会检查源 `app.json``apps/mobile-shell/scripts/check-expo-config.mjs` 会检查 Expo CLI 最终解析出的 public config,避免 config plugin 或 Expo 解析阶段引入身份、资源或权限漂移。
2026-06-18 追加:移动壳命令入口进入配置门禁。`apps/mobile-shell/package.json``dev``android``ios``test``config:smoke``export:smoke``typecheck` 以及根 `package.json``mobile-shell:*` 入口必须保持指向真实 Expo / RN / Vitest / Expo config / Metro export / 配置检查流程,不能替换成只跑静态脚本或绕过生产 bundler 的快捷命令。
2026-06-18 追加:移动壳关键依赖版本进入配置门禁。`apps/mobile-shell/package.json` 和根 `package.json`Expo SDK 56、React 19、React Native 0.86、`react-native-webview` 13.16、`react-native-safe-area-context`、Expo Camera / Clipboard / DocumentPicker / FileSystem / Haptics / ImagePicker / Linking / Network / Notifications / Sharing / StatusBar、TypeScript 和 Vitest 版本必须保持一致;根 `package-lock.json` 里的实际解析版本必须由配置检查锁定。升级这些依赖必须同步审查 WebView 安全开关、Expo managed config、production export、HostBridge 能力实现和 H5 fallback,不能只更新依赖声明或锁文件。
2026-06-18 追加2026-08-21 按 npm workspaces 更新:移动壳关键依赖版本进入配置门禁。Expo SDK 56、React 19、React Native 0.86、`react-native-webview` 13.16、`react-native-safe-area-context`、Expo Camera / Clipboard / DocumentPicker / FileSystem / Haptics / ImagePicker / Linking / Network / Notifications / Sharing / StatusBar、TypeScript 和 Vitest `apps/mobile-shell/package.json` 拥有,根 H5 不再重复声明移动端专属依赖;统一根 `package-lock.json` 中 Mobile workspace entry 与实际解析版本必须由配置检查锁定。升级这些依赖必须同步审查 WebView 安全开关、Expo managed config、production export、HostBridge 能力实现和 H5 fallback,不能只更新依赖声明或锁文件。
### Phase 3Tauri 桌面壳 MVP
@@ -486,9 +486,9 @@ GameBridge 禁止:
2026-06-18 追加:桌面壳 capability 作用域进入配置门禁。Tauri 配置只能声明一个 `label=main` 的主窗口,`src-tauri/capabilities/` 只能存在 `main.json`,该 capability 的 `identifier` 必须为 `main``windows` 必须只包含 `main``permissions` 必须只包含 `allow-host-bridge-request`。这保证 opener、clipboard、dialog、notification 等插件只由 Rust 内部通过受控 HostBridge 分发使用,不把 core 默认命令、插件 JS guest API 或额外窗口权限授给 H5 主站。
2026-06-18 追加:桌面壳 JS guest 依赖进入门禁。`apps/desktop-shell/package.json`根 H5 `package.json` 和根 `package-lock.json` 不得安装或解析 `@tauri-apps/api` 或任何 `@tauri-apps/plugin-*`,避免生产前端绕过 `nativeAppHostBridge` 直接调用 Tauri JS 客户端 APITauri CLI 仍只作为构建工具留在 devDependencies,桌面系统能力继续由 Rust 侧 Cargo 插件和唯一 `host_bridge_request` command 承接。
2026-06-18 追加2026-08-21 按 npm workspaces 更新:桌面壳 JS guest 依赖进入门禁。`apps/desktop-shell/package.json`根 H5 `package.json` 不得声明 `@tauri-apps/api` 或任何 `@tauri-apps/plugin-*`,避免生产前端绕过 `nativeAppHostBridge` 直接调用 Tauri JS 客户端 API统一根 `package-lock.json` 会合法聚合 AGC workspace 的 Tauri guest,不能按全局 lock 条目禁止。Tauri CLI 仍只作为构建工具留在 devDependencies,桌面系统能力继续由 Rust 侧 Cargo 插件和唯一 `host_bridge_request` command 承接。
2026-06-18 追加:桌面壳关键依赖版本进入配置门禁。`apps/desktop-shell/package.json` `package.json` 的 Tauri CLI 和 TypeScript 版本必须一致;根 `package-lock.json` 里的实际解析版本必须一致;`src-tauri/Cargo.toml``tauri-build``tauri``base64``serde``serde_json` 以及 clipboard、dialog、notification、opener、single-instance、window-state 插件版本和 Tauri `tray-icon` feature 都由 `apps/desktop-shell/scripts/check-config.mjs` 固定检查,`src-tauri/Cargo.lock` 中桌面壳 direct dependency 的实际解析版本也必须同步受检。升级这些依赖必须同步审查 capability、CSP、唯一 command、插件初始化、release build smoke 和本文档。
2026-06-18 追加2026-08-21 按 npm workspaces 更新:桌面壳关键依赖版本进入配置门禁。Tauri CLI 和 TypeScript 由 `apps/desktop-shell/package.json` 声明,统一`package-lock.json` 中 Desktop workspace entry 与实际解析版本必须一致;根 H5 只有自身确实使用同一构建工具时才单独声明,不再为 Desktop 镜像依赖。`src-tauri/Cargo.toml``tauri-build``tauri``base64``serde``serde_json` 以及 clipboard、dialog、notification、opener、single-instance、window-state 插件版本和 Tauri `tray-icon` feature 都由 `apps/desktop-shell/scripts/check-config.mjs` 固定检查,`src-tauri/Cargo.lock` 中桌面壳 direct dependency 的实际解析版本也必须同步受检。升级这些依赖必须同步审查 capability、CSP、唯一 command、插件初始化、release build smoke 和本文档。
2026-06-18 追加:H5 到 Tauri 的 command 名进入共享契约。`packages/shared/src/contracts/hostBridge.ts` 导出 `HOST_BRIDGE_TAURI_COMMAND='host_bridge_request'``nativeAppHostBridge` 只能通过该常量调用 Tauri 注入的 `core.invoke`;桌面壳配置检查会对齐共享常量、Tauri build manifest、Rust `generate_handler!` 和 H5 transport,禁止 H5 侧写死或调用其它 Tauri command。

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