把活层退役旧品牌「造梦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>
193 lines
13 KiB
JavaScript
193 lines
13 KiB
JavaScript
#!/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 覆写其运行包。
|
||
* (真产线 P3:engineBundle 由生成回调链经 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_size(SHA2/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) 生成 SQL:node 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 绘境AI(P2 后端轨)
|
||
*/
|
||
|
||
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 缺省 9201(p2_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 = [];
|
||
// manifest:bundleSize/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 灌库桩 SQL(P2 过渡桩,真产线 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 当前生效版本 ID(game_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_json(versionId 占位回填真实 @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}`);
|