把活层退役旧品牌「造梦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>
172 lines
9.9 KiB
JavaScript
172 lines
9.9 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* P3 canary 3a 灌库桩 —— adapt 自 p2_seed_engine_bundle.mjs
|
||
* ============================================================================
|
||
* 【目标】把 L1 便宜模型生成的 pong 游戏 bundle 文本,灌进已发布 version(gameId=9301)的
|
||
* package_json.engineBundle,使 pong 经 feed 通用宿主 bootGameHost 引擎真渲、真人浏览器真玩。
|
||
* (护城河可演示:便宜模型生成的游戏在真 feed 被真人玩。)
|
||
*
|
||
* 【与 P2 ref 灌库桩的差异(adapt 点)】
|
||
* - gameId 缺省 9301(p3_scaffold_pong.sql 建的脚手架游戏;P2 是 9201/ref);
|
||
* - bundle 路径指向 pong(game-runtime/games/_wg1-gen/pong/bundle.iife.js;P2 是 wanglanmei-ref/dist/ref-bundle.iife.js);
|
||
* - templateId/title/gameConfig 改 pong(前端宿主分流只看 pkg.engineBundle 有无,不看 templateId,此处仅元数据标识)。
|
||
* 其余防坑逻辑(utf8mb4 charset / checksum 自洽 / versionId 占位回填 / __GameBundle 全局名硬校验)逐字保留。
|
||
*
|
||
* 【依赖的已发布游戏】本桩零新建、仅覆写某已发布 version 的 package_json,故须先跑同目录
|
||
* p3_scaffold_pong.sql 建四态脚手架(gameId=9301: project=4 ∧ version=3 ∧ pkg=1 ∧ feed_rank)。
|
||
*
|
||
* 【完整性校验链不变量(防 demo 门③依赖)】
|
||
* 宿主取包做 sha256(manifest 端点响应原文) == game_runtime_package.checksum 列。
|
||
* 故 UPDATE 用 checksum = LOWER(SHA2(package_json,256)) 对刚写入的同一文本求值 → 列值与所存文本字节恒一致,
|
||
* 宿主 crypto.subtle 校验必过、不触 demo 兜底。
|
||
*
|
||
* 【用法(mini-desktop staging)】
|
||
* 0) 建四态脚手架:docker exec -i game-staging-mysql mysql --default-character-set=utf8mb4 \
|
||
* -uroot -p"$MYSQL_ROOT_PASSWORD" < game-cloud/scripts/p3_scaffold_pong.sql (建 gameId=9301)
|
||
* 1) 生成 SQL:node game-cloud/scripts/p3_seed_pong.mjs [gameId] > /tmp/p3_seed_pong.sql
|
||
* (gameId 缺省 9301;bundle 路径固定 games/_wg1-gen/pong/bundle.iife.js)
|
||
* 2) 应用:docker exec -i game-staging-mysql mysql --default-character-set=utf8mb4 -uroot -p"$MYSQL_ROOT_PASSWORD" < /tmp/p3_seed_pong.sql
|
||
* (⚠️ --default-character-set=utf8mb4 必带:客户端默认 latin1 会把 UTF-8 bundle/中文 title 双编码,
|
||
* 致列字节 ≠ checksum 计算字节 → manifest sha256 不一致 → 宿主校验失败 → demo 兜底;SQL 头亦自带 SET NAMES 双保险)
|
||
* 3) 重跑安全(幂等):再次执行步骤 1+2 即可(纯 UPDATE 覆写,无副作用)。
|
||
*
|
||
* @author 绘境AI(P3 canary)
|
||
*/
|
||
|
||
import { readFileSync } from 'node:fs';
|
||
import { createHash } from 'node:crypto';
|
||
import { dirname, resolve } from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
|
||
// ---------- 常量(与 P2 冻结契约逐字一致)----------
|
||
/** 冻结:iife 顶层全局名,宿主从 window.__GameBundle 取 GameHostFactory/bootGameHost(三轨此名必须逐字一致) */
|
||
const FROZEN_GLOBAL_NAME = '__GameBundle';
|
||
/** pong bundle 固定产物路径——esbuild --global-name=__GameBundle 出(W-G1 生成 worker 产出,九门已验)。 */
|
||
const PONG_BUNDLE_REL = '../../game-runtime/games/_wg1-gen/pong/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==';
|
||
/** pong 游戏标识(元数据;前端宿主分流不依赖此值,仅展示/契约 required 用) */
|
||
const PONG_TITLE = '弹球对战(Pong·引擎真渲)';
|
||
const PONG_TEMPLATE_ID = 'pong';
|
||
|
||
// ---------- 入参 ----------
|
||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||
// gameId 缺省 9301(p3_scaffold_pong.sql 建的脚手架游戏);运行时可传第一个 CLI 参数覆盖
|
||
const gameId = String(process.argv[2] || '9301').trim();
|
||
|
||
// ---------- 步骤①:读 pong bundle 文本 + 校验全局名 ----------
|
||
const bundlePath = resolve(__dirname, PONG_BUNDLE_REL);
|
||
let engineBundle;
|
||
try {
|
||
engineBundle = readFileSync(bundlePath, 'utf8');
|
||
} catch (e) {
|
||
// 读不到产物即写链断裂:显式失败(不灌空桩)
|
||
console.error(`[p3-seed-pong][FATAL] 读不到 pong bundle:${bundlePath}\n` +
|
||
` 请先在 game-runtime 跑 W-G1 生成 worker 产出该文件。原始错误:${e.message}`);
|
||
process.exit(1);
|
||
}
|
||
// 全局名硬校验:bundle 必须以 FROZEN_GLOBAL_NAME(__GameBundle)导出 iife,否则宿主取不到 factory、真接线必 fail。
|
||
if (!engineBundle.includes(FROZEN_GLOBAL_NAME)) {
|
||
console.error(`[p3-seed-pong][FATAL] pong bundle 未含冻结全局名 ${FROZEN_GLOBAL_NAME}。\n` +
|
||
` 须由 W-G1 产线以 esbuild --global-name=${FROZEN_GLOBAL_NAME} 重建 ${bundlePath}。\n` +
|
||
` 当前不灌库(避免种入宿主取不到 factory 的死卡)。`);
|
||
process.exit(2);
|
||
}
|
||
|
||
// ---------- 步骤②:组 GamePackage JSON(含 engineBundle;键序对齐后端 buildGamePackage)----------
|
||
// 用「保插入序」的对象逐键放入,序列化采用紧凑无空格(与后端 Jackson 默认无缩进口径一致,便于字节对齐)。
|
||
function buildPackage(versionPlaceholder) {
|
||
const pkg = {};
|
||
pkg.schemaVersion = '1.0';
|
||
pkg.gameId = String(gameId);
|
||
pkg.versionId = versionPlaceholder; // SQL 端 REPLACE 回填真实 versionId(见下)
|
||
pkg.templateId = PONG_TEMPLATE_ID; // pong 标识(过渡口径,前端分流不依赖)
|
||
pkg.gameConfig = { templateId: PONG_TEMPLATE_ID, title: PONG_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: PONG_TITLE,
|
||
summary: 'P3 canary:便宜模型生成的 pong 经 bootGameHost 引擎真渲入 feed 真玩',
|
||
cover: COVER_PLACEHOLDER,
|
||
ageRating: 'all',
|
||
};
|
||
// engineBundle 末位放入(落 checksum 之内,由整包 sha256 覆盖;与后端 put 落点同义)
|
||
pkg.engineBundle = engineBundle;
|
||
return pkg;
|
||
}
|
||
|
||
// 内层 manifest.checksum 自指消解:先以占位序列化 T0 → C=sha256(T0) → 写回得 T1(包内自述用,宿主用外层列值)。
|
||
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:转义 \ 和 ') ----------
|
||
function sqlQuote(text) {
|
||
return "'" + text.replace(/\\/g, '\\\\').replace(/'/g, "\\'") + "'";
|
||
}
|
||
const t1Sql = sqlQuote(t1);
|
||
|
||
// ---------- 步骤④:产出幂等 UPDATE SQL ----------
|
||
const sql = `-- ============================================================================
|
||
-- P3 canary 3a 灌库桩 SQL(pong 引擎游戏)—— 由 game-cloud/scripts/p3_seed_pong.mjs 生成
|
||
-- 目标 gameId=${gameId};灌入 pong 引擎 bundle 到其当前生效版本的 package_json.engineBundle
|
||
-- 应用:docker exec -i game-staging-mysql mysql --default-character-set=utf8mb4 -uroot -p"$MYSQL_ROOT_PASSWORD" < 本文件
|
||
-- 幂等:纯 UPDATE,可重复执行
|
||
-- ============================================================================
|
||
USE \`ruoyi-vue-pro\`;
|
||
-- charset 防御:强制连接 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,先跑 p3_scaffold_pong.sql') ), 'ok');
|
||
SELECT @vid AS resolved_version_id, @ok AS precheck;
|
||
|
||
-- ② 覆写该版本运行包行:package_json(versionId 占位回填真实 @vid)+ 自洽 checksum/bundle_size + status=1
|
||
-- 把终文一次性装入会话变量 @pj,避免单 UPDATE 内联 3× bundle(~265KB) 撞 max_allowed_packet
|
||
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 = 'p3-seed-pong',
|
||
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(`[p3-seed-pong] gameId=${gameId} bundle=${bundlePath} ` +
|
||
`bundleBytes=${Buffer.byteLength(engineBundle, 'utf8')} ` +
|
||
`pkgJsonBytes(占位态)=${Buffer.byteLength(t1, 'utf8')} innerChecksum=${innerChecksum}`);
|