games-development-ai/game-cloud/scripts/p2_seed_engine_bundle.mjs
lili b31cc37a4f chore(brand): 造梦AI→绘境AI 全活层品牌不变量收口(441 文件)
把活层退役旧品牌「造梦AI」统一改为现行「绘境AI」,brand-invariant 门红线归零。构成:
~412 Java @author 署名 + game-studio/index.html 浏览器标题 + game-runtime/package.json 描述
+ game-admin/.env VITE_APP_TITLE 运营后台标题 + contracts API title/@Schema 用户协议示例/events 描述
+ docs-design mockup 品牌位 + 各模块注释。全是注释/显示/元数据,零逻辑改动。
zaomeng 拼音持久化标识符按 2026-06-24 评审 GR-05 不动(347 处保留)。
白名单(docs/ip 法律备案 / _archive / 带日期留痕)与 AGENTS.md 门定义自身不动。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 08:29:11 -07:00

193 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* O-P2-1 灌库桩 —— P2 通用宿主:把 ref 引擎 bundle 文本灌进某已发布 version 的 package_json.engineBundle
* ============================================================================
* 【⚠️ P2 过渡桩,真产线 P3】
* 本脚本是 T1b-β P2「Runner v2 泛化宿主」的过渡手段execution spec
* docs/agent-specs/2026-06-13-P2-runner-v2泛化宿主-execution.md §6.1 B0 / O-P2-1 拍定):
* P2 阶段只跑 ref 一款 engine-aware 游戏,其 engineBundle 不经生成回调链产出,而是由本脚本
* 一次性把 game-runtime/games/wanglanmei-ref/dist/ref-bundle.iife.js 文本灌进 DB。
* 真产线(生成主线 agent 原生出 bundle 经 DifyCallbackTxService.buildGamePackage 随回调入参写入)留 P3。
*
* 【附着的已发布游戏从哪来——须先建四态脚手架mini-desktop 真机门逮)】
* 本桩零新建、仅「覆写某已发布 version 的 package_json 为含 engineBundle 的 GamePackage 文本」,
* 故依赖一款四态齐备(project.status=4 ∧ version.status=3 ∧ runtime_package.status=1 ∧ feed_rank 行)的已发布游戏。
* ⚠️ staging DB 已被 W-CLEAN 清空(0 version/0 package/0 feed_rank),不存在历史种子 9001。
* 故 P2 须先跑同目录 p2_scaffold_4state.sql 建一款脚手架游戏(gameId=9201),本桩再 9201 覆写其运行包。
* (真产线 P3engineBundle 由生成回调链经 DifyCallbackTxService.buildGamePackage 写入,无需脚手架。)
*
* 【为什么生成 SQL 文件而非直连 DB】
* staging DB 访问走 docker exec game-staging-mysql mysql见 memory golden-loop-b1-done无需在
* mini-desktop 装 mysql 驱动本脚本本机6c6g/mini-desktop 皆可)仅做「读 bundle + 组 GamePackage JSON +
* 产出可幂等重跑的 UPDATE SQL」把 265KB bundle 安全嵌进 SQL单引号/反斜杠转义),由 MySQL 端自算
* checksum/bundle_sizeSHA2/LENGTH 直接对所存文本求值),消除任何转义/编码导致的 sha256 不一致风险。
*
* 【完整性校验链不变量(门③依赖)】
* 宿主取包做 sha256(manifest 端点响应原文) == RuntimePackageRespVO.checksum= game_runtime_package.checksum 列)。
* 故 UPDATE 用 checksum = LOWER(SHA2(package_json, 256))、对刚写入的同一文本求值 → 列值与所存文本字节恒一致,
* 宿主校验必过。内层 manifest.checksum 字段仅作包内自述(不参与宿主校验,对齐 DifyCallbackTxService 注释)。
*
* 【用法mini-desktop staging
* 0) 建四态脚手架(W-CLEAN 后必需)docker exec -i game-staging-mysql mysql --default-character-set=utf8mb4 \
* -uroot -p"$MYSQL_ROOT_PASSWORD" < game-cloud/scripts/p2_scaffold_4state.sql (建 gameId=9201
* 1) 生成 SQLnode game-cloud/scripts/p2_seed_engine_bundle.mjs [gameId] > /tmp/p2_seed.sql
* gameId 缺省 9201=脚手架游戏bundle 路径固定 games/wanglanmei-ref/dist/ref-bundle.iife.js
* 2) 应用docker exec -i game-staging-mysql mysql --default-character-set=utf8mb4 -uroot -p"$MYSQL_ROOT_PASSWORD" < /tmp/p2_seed.sql
* (⚠️ --default-character-set=utf8mb4 必带:客户端默认 latin1 会把 UTF-8 bundle/中文 title 双编码,
* 致列字节 ≠ checksum 计算字节 → manifest sha256 不一致 → 宿主校验失败 → demo 兜底SQL 头亦自带 SET NAMES 双保险)
* $MYSQL_ROOT_PASSWORD 见 ~/game-staging/infra/.env内网非密
* 3) 重跑安全(幂等):再次执行步骤 1+2 即可(纯 UPDATE 覆写,无副作用)。
*
* @author 绘境AIP2 后端轨)
*/
import { readFileSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
// ---------- 常量(与 execution spec 冻结契约逐字一致)----------
/** O-P2-3 冻结iife 顶层全局名,宿主从 window.__GameBundle 取 GameHostFactory三轨此名必须逐字一致 */
const FROZEN_GLOBAL_NAME = '__GameBundle';
/** ref bundle 固定产物路径spec §3.1 / B0 钉死——build:ref-bundle 产物(--global-name=__GameBundle
* 非 build:ref-harness 的 ref-bundle.js那是 P1 取证 harness全局名 WanglanmeiRef。mustFix#1 纠正。 */
const REF_BUNDLE_REL = '../../game-runtime/games/wanglanmei-ref/dist/ref-bundle.iife.js';
/** checksum 自指消解占位64 位 '0'(与真实 sha256 hex 等长,对齐 DifyCallbackTxService.CHECKSUM_PLACEHOLDER */
const CHECKSUM_PLACEHOLDER = '0'.repeat(64);
/** meta.cover 占位1px 透明 gif data URI沿后端组包同款满足契约 format=uri */
const COVER_PLACEHOLDER =
'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
// ---------- 入参 ----------
const __dirname = dirname(fileURLToPath(import.meta.url));
// gameId 缺省 9201p2_scaffold_4state.sql 建的脚手架游戏);运行时可传第一个 CLI 参数覆盖
const gameId = String(process.argv[2] || '9201').trim();
// ---------- 步骤①:读 ref bundle 文本 + 校验全局名 ----------
const bundlePath = resolve(__dirname, REF_BUNDLE_REL);
let engineBundle;
try {
engineBundle = readFileSync(bundlePath, 'utf8');
} catch (e) {
// 读不到产物即写链断裂:显式失败(不灌空桩),提示先固化 build:ref-bundle 产线spec §6.1 B0
console.error(`[p2-seed][FATAL] 读不到 ref bundle${bundlePath}\n` +
` 请先在 game-runtime 跑 build:ref-bundle 产出该文件spec §6.1 B0。原始错误${e.message}`);
process.exit(1);
}
// 全局名硬校验bundle 必须以 FROZEN_GLOBAL_NAME__GameBundle导出 iife否则宿主取不到 GameHostFactory、
// 门② 必 fail。当前若 bundle 仍是 P1 旧全局名(如 WanglanmeiRef在此显式拦截、提示重建产线。
if (!engineBundle.includes(FROZEN_GLOBAL_NAME)) {
console.error(`[p2-seed][FATAL] ref bundle 未含冻结全局名 ${FROZEN_GLOBAL_NAME}O-P2-3\n` +
` 现状 bundle 可能是 P1 旧产物(旧全局名);须由宿主轨/产线以 esbuild --global-name=${FROZEN_GLOBAL_NAME}\n` +
` 重建 ${bundlePath}(导出 GameHostFactory 到 window.${FROZEN_GLOBAL_NAME})后再灌库。\n` +
` 当前不灌库(避免种入宿主取不到 factory 的死卡,门② 会 fail`);
process.exit(2);
}
// ---------- 步骤②:组 GamePackage JSON含 engineBundle键序对齐后端 buildGamePackage----------
// 用「保插入序」的对象逐键放入,序列化采用紧凑无空格(与后端 Jackson 默认无缩进口径一致,便于字节对齐)。
// versionId/gameId 用占位串:真实 versionId 在 SQL 端由子查询从 game_project.current_version_id 解析,
// 但 package_json 内的 versionId 字段须与之一致——故本桩在 SQL 端用 @vid 变量回填占位(见下方 SQL
function buildPackage(versionPlaceholder) {
const pkg = {};
pkg.schemaVersion = '1.0';
pkg.gameId = String(gameId);
pkg.versionId = versionPlaceholder; // SQL 端 REPLACE 回填真实 versionId见下
pkg.templateId = 'wanglanmei-ref'; // P2 ref 级固定模板标识(过渡口径)
pkg.gameConfig = { templateId: 'wanglanmei-ref', title: '王蓝莓小卖部(引擎真渲)' };
pkg.assets = [];
// manifestbundleSize/checksum 先占位(内层自述,非宿主校验面);保留三键齐全满足契约 required
pkg.manifest = {
runtimeVersion: '1.0.0',
entry: 'index.html',
preloadPolicy: 'eager',
bundleSize: 0,
checksum: CHECKSUM_PLACEHOLDER,
};
pkg.meta = {
title: '王蓝莓小卖部(引擎真渲)',
summary: 'P2 通用宿主过渡桩ref 引擎 bundle 经 bootGameHost 真渲',
cover: COVER_PLACEHOLDER,
ageRating: 'all',
};
// engineBundle 末位放入(落 checksum 之内,由整包 sha256 覆盖;与后端 put 落点同义)
pkg.engineBundle = engineBundle;
return pkg;
}
// 内层 manifest.checksum 自指消解:先以占位序列化 T0 → C=sha256(T0) → 写回得 T1包内自述用宿主用外层列值
// 注versionId 占位文本在 T0/T1 两次序列化一致,故内层 checksum 计算自洽SQL 端 REPLACE 占位为真实 versionId
// 会改变最终落库文本——但宿主校验的外层 checksum 由 MySQL 对「落库后的最终文本」SHA2 求值,故仍恒一致(见 SQL
const VID_PLACEHOLDER = '__VERSION_ID__';
const t0 = JSON.stringify(buildPackage(VID_PLACEHOLDER));
const innerChecksum = createHash('sha256').update(t0, 'utf8').digest('hex');
const pkgT1 = buildPackage(VID_PLACEHOLDER);
pkgT1.manifest.checksum = innerChecksum;
const t1 = JSON.stringify(pkgT1);
// ---------- 步骤③:转义为 SQL 字符串字面量MySQL转义 \ 和 ' ----------
// MySQL 默认 NO_BACKSLASH_ESCAPES 关闭 → 反斜杠是转义符,必须先转义 \,再转义单引号;换行/控制符 bundle 内已是
// 合法 JS 文本字符,作为字符串内容原样存即可(不破坏 JSON 文本语义)。
function sqlQuote(text) {
return "'" + text.replace(/\\/g, '\\\\').replace(/'/g, "\\'") + "'";
}
const t1Sql = sqlQuote(t1);
// ---------- 步骤④:产出幂等 UPDATE SQL ----------
// 设计:
// 1) 用 @vid 解析该 gameId 的当前生效版本game_project.current_version_id
// 2) 把 package_json 内的 versionId 占位 __VERSION_ID__ 用 REPLACE 回填为真实 @vid保 versionId 自洽);
// 3) checksum = LOWER(SHA2(package_json,256)) 对「回填后的最终文本」求值 → 与所存字节恒一致(宿主校验必过);
// 4) bundle_size = LENGTH(package_json)utf8mb4 下 LENGTH 返字节数,对齐契约 bundleSize 语义);
// 5) status = 1已发布9001 本已为 1置之幂等无副作用确保 play 场景门禁放行)。
// 安全:纯 UPDATE 单行uk_version 唯一),重跑覆写无副作用;事务包裹保原子。
const sql = `-- ============================================================================
-- O-P2-1 灌库桩 SQLP2 过渡桩,真产线 P3—— 由 game-cloud/scripts/p2_seed_engine_bundle.mjs 生成
-- 目标 gameId=${gameId}(缺省王蓝莓小卖部 A灌入 ref 引擎 bundle 到其当前生效版本的 package_json.engineBundle
-- 应用docker exec -i game-staging-mysql mysql -uroot -p"$MYSQL_ROOT_PASSWORD" < 本文件
-- 幂等:纯 UPDATE可重复执行
-- ============================================================================
USE \`ruoyi-vue-pro\`;
-- charset 防御mini-desktop 真机门逮):强制连接 charset=utf8mb4否则客户端默认 latin1 把 UTF-8 bundle/中文
-- title 双编码落 utf8mb4 列 → 列字节 ≠ SET @pj 计算 checksum 的字节 → stored checksum 与 manifest 端点
-- sha256(原文) 不一致 → 宿主 crypto.subtle 校验必失败 → demo 兜底 → 引擎永不渲。自带 SET NAMES 不依赖调用方加参数。
SET NAMES utf8mb4;
-- ① 解析该 gameId 当前生效版本 IDgame_project.current_version_id → game_version.id
SET @gid := ${Number(gameId)};
SET @vid := (SELECT current_version_id FROM game_project WHERE id = @gid AND deleted = b'0' LIMIT 1);
-- 防御:版本未解析(游戏不存在/未设当前版本)则报错中止,不盲写
SET @ok := IF(@vid IS NULL, (SELECT CONCAT('FATAL: gameId=', @gid, ' 无 current_version_id先发布该游戏') ), 'ok');
SELECT @vid AS resolved_version_id, @ok AS precheck;
-- ② 覆写该版本运行包行package_jsonversionId 占位回填真实 @vid+ 自洽 checksum/bundle_size + status=1
-- shouldFix#2把终文一次性装入会话变量 @pj避免单 UPDATE 内联 3× bundle(~265KB) 撞 max_allowed_packet旧配置 1MB 档会拒)
SET @pj := REPLACE(${t1Sql}, '${VID_PLACEHOLDER}', CAST(@vid AS CHAR));
START TRANSACTION;
UPDATE game_runtime_package
SET
package_json = @pj,
checksum = LOWER(SHA2(@pj, 256)),
bundle_size = LENGTH(@pj),
status = 1,
updater = 'p2-seed-engine-bundle',
update_time = NOW()
WHERE version_id = @vid AND deleted = b'0';
COMMIT;
-- ③ 回读核对(应见 status=1、checksum 64 位、package_json 含 engineBundle/__GameBundle
SELECT version_id, status, checksum, bundle_size,
(package_json LIKE '%"engineBundle"%') AS has_engine_bundle_key,
(package_json LIKE '%${FROZEN_GLOBAL_NAME}%') AS has_frozen_global_name
FROM game_runtime_package WHERE version_id = @vid AND deleted = b'0';
`;
// 产出到 stdout调用方重定向到 .sql 文件)
process.stdout.write(sql);
// 诊断信息走 stderr不污染 stdout 的纯 SQL
console.error(`[p2-seed] gameId=${gameId} bundle=${bundlePath} ` +
`bundleBytes=${Buffer.byteLength(engineBundle, 'utf8')} ` +
`pkgJsonBytes(占位态)=${Buffer.byteLength(t1, 'utf8')} innerChecksum=${innerChecksum}`);