diff --git a/contracts/game-package.schema.json b/contracts/game-package.schema.json index 6df910ed..d31b879b 100644 --- a/contracts/game-package.schema.json +++ b/contracts/game-package.schema.json @@ -91,6 +91,10 @@ } } }, + "engineBundle": { + "description": "引擎+包装层+游戏代码的可执行 iife bundle 文本(T1b-β P2 通用宿主载体;additive 可选,存量消费者无此字段时走 startRuntime 旧路/demo 兜底)。MVP 默认=随 manifest 内嵌经 DB-manifest 端点 /app-api/runtime/package/{versionId}/manifest 下发(落 game_runtime_package.package_json LONGTEXT,sha256 整包 checksum 覆盖其完整性,不接 OSS、不增第二校验面);切 OSS 后改为 packageUrl 外链。iife 顶层全局名固定为 __GameBundle(宿主据此从 window.__GameBundle 取 GameHostFactory,见 game-runtime/src/core/game-host.d.ts)。后端写权=DifyCallbackTxService.buildGamePackage put(P2 阶段 ref 一款经 O-P2-1 灌库桩注入,真产线 P3 接生成主线)。", + "type": "string" + }, "provenance": { "description": "可追溯来源(质量门禁与缓存命中用)。相同 promptHash 命中缓存可跳过 LLM 生成。", "type": "object", diff --git a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/cn/wanxiang/game/module/aigc/service/callback/DifyCallbackTxService.java b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/cn/wanxiang/game/module/aigc/service/callback/DifyCallbackTxService.java index c993ec72..c67bb264 100644 --- a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/cn/wanxiang/game/module/aigc/service/callback/DifyCallbackTxService.java +++ b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/cn/wanxiang/game/module/aigc/service/callback/DifyCallbackTxService.java @@ -309,6 +309,20 @@ public class DifyCallbackTxService { } pkg.put("provenance", provenance); + // engineBundle(契约#4 additive 可选,T1b-β P2 通用宿主载体):引擎+包装层+游戏代码的 iife bundle 文本, + // 随 manifest 内嵌经 DB-manifest 端点下发,由整包 sha256 checksum 覆盖完整性(故须落在下方 checksum 三段 + // 序列化 T_base→T0→T1 之前,bundle 字节才被 checksum 与 bundleSize 共同涵盖)。 + // 【P2 阶段·过渡桩】真实 bundle 来源走 O-P2-1 灌库桩脚本(scripts/p2_seed_engine_bundle.* 一次性把 + // ref-bundle.js 文本灌进某 version 的 package_json.engineBundle),不经本回调链——故本处 bundle 文本不可得, + // 按 spec「不可得则省略、不可硬编死值」省略该键(null 不 put,存量回调零行为变化、字节布局不变)。 + // 【P3 真产线】生成主线 agent 原生出 bundle 后,由回调入参(契约#6 output 增 additive 字段,P3 自身 spec 落) + // 携带 bundle 文本,此处改为从入参取值后 put——届时本 null 占位替换为入参取值,conditional put 骨架不变。 + String engineBundle = resolveEngineBundleText(reqVO); // P2 恒返回 null(无回调入参来源);P3 改为取回调 bundle 文本 + if (StringUtils.hasText(engineBundle)) { + // 仅在 bundle 文本可得时写入:保证 P2「省略」语义 + P3「随入参写入」语义同一条 conditional 分支承载(无孤儿设计) + pkg.put("engineBundle", engineBundle); + } + try { // T_base:bundleSize=0 + checksum 占位 → 基准文本,bundleSize := 其 UTF-8 字节数(确定性近似) String tBase = PACKAGE_MAPPER.writeValueAsString(pkg); @@ -355,6 +369,24 @@ public class DifyCallbackTxService { return null; } + /** + * 解析引擎 bundle 文本(契约#4 GamePackage.engineBundle 的写入源) + * + * 【P2 阶段·过渡桩,真产线 P3】恒返回 null:P2 仅跑 ref 一款,其 engineBundle 由 O-P2-1 灌库桩脚本 + * (scripts/p2_seed_engine_bundle.*)一次性灌进 DB package_json,不经本回调链;故本回调组包时 bundle 文本 + * 不可得,返回 null 使上游 conditional put 省略该键(存量回调零行为变化)。 + * 【P3 真产线】生成主线 agent 原生产出 bundle 后,由回调入参携带 bundle 文本(契约#6 output 增 additive 字段, + * P3 自身 spec 落实),此方法改为 {@code return reqVO.getEngineBundle();}——上游 conditional put 骨架不变, + * 仅本取值源从 null 改为入参,最小切换、无孤儿设计。 + * + * @param reqVO 回调入参(P3 将从其新增字段取 bundle 文本;P2 不读其任何字段,恒 null) + * @return 引擎 bundle 文本;P2 阶段恒 null(省略 engineBundle 键) + */ + private static String resolveEngineBundleText(DifyCallbackReqVO reqVO) { + // P2 过渡桩:bundle 来源 = 灌库脚本直写 DB,非回调入参 → 此处无来源,返回 null(不硬编码任何死值) + return null; + } + /** * 置任务失败终态(status=3 + failure_reason + finishTime;failed 分支与业务性失败/补偿共用) * diff --git a/game-cloud/scripts/p2_scaffold_4state.sql b/game-cloud/scripts/p2_scaffold_4state.sql new file mode 100644 index 00000000..b09a8ebc --- /dev/null +++ b/game-cloud/scripts/p2_scaffold_4state.sql @@ -0,0 +1,70 @@ +-- ============================================================================ +-- P2 真机门·四态脚手架(W-CLEAN 清库后重建一款已发布游戏供 engineBundle 灌库桩附着) +-- 缘由:seed 脚本 p2_seed_engine_bundle.mjs 原假设已存在已发布种子(9001 四态齐备), +-- 但 staging DB 已被 W-CLEAN 清空(0 version/0 package/0 feed_rank),故须先建四态脚手架。 +-- (mini-desktop 2026-06-13 P2 真机门实测可用,gate 报告逮出后落库归仓。) +-- 四态:game_project.status=4(PUBLISHED) ∧ game_version.status=3 ∧ game_runtime_package.status=1 ∧ game_feed_rank 行 +-- ID:gameId=9201 / versionId=92010 / pkgId=92010 / feedRankId=92010(高位区,零碰撞) +-- engineBundle 由后续 p2_seed_engine_bundle.mjs 9201 覆写 package_json(本脚本先建空 package_json 占位行) +-- 幂等:先 DELETE 同 ID 行再 INSERT(纯重建,可重复执行) +-- tenant:staging application-staging.yaml tenant.enable=false → tenant_id=0 即可 +-- 用法:docker exec -i game-staging-mysql mysql --default-character-set=utf8mb4 -uroot -p"$MYSQL_ROOT_PASSWORD" < 本文件 +-- ============================================================================ +USE `ruoyi-vue-pro`; +-- charset 防御(与 seed 脚本同口径):强制 utf8mb4,避免中文 title 被客户端默认 latin1 双编码 +SET NAMES utf8mb4; + +START TRANSACTION; + +-- 清旧(幂等重建) +DELETE FROM game_feed_rank WHERE game_id = 9201; +DELETE FROM game_runtime_package WHERE game_id = 9201; +DELETE FROM game_version WHERE game_id = 9201; +DELETE FROM game_project WHERE id = 9201; + +-- ① project:已发布(status=4),current_version_id 指向 92010,launch_zone_id 留 NULL(→feed zone 0) +INSERT INTO game_project + (id, creator_user_id, title, summary, cover_url, tags, template_id, age_rating, + status, current_version_id, launch_zone_id, featured, play_count, like_count, + creator, create_time, updater, update_time, deleted, tenant_id) +VALUES + (9201, 1, '王蓝莓小卖部(引擎真渲)', 'P2 通用宿主真机门:ref 引擎 bundle 经 bootGameHost 真渲入 feed', + 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==', + 'P2,engine,wanglanmei', 'wanglanmei-ref', 'all', + 4, 92010, NULL, b'0', 0, 0, + 'p2-scaffold', NOW(), 'p2-scaffold', NOW(), b'0', 0); + +-- ② version:已发布(status=3),package_url 空(走 DB-manifest 端点),checksum 先占位(seed 脚本覆写运行包不动版本) +INSERT INTO game_version + (id, game_id, version_no, gen_task_id, package_url, bundle_size, checksum, + status, creator, create_time, updater, update_time, deleted, tenant_id) +VALUES + (92010, 9201, 1, NULL, '', 0, '', + 3, 'p2-scaffold', NOW(), 'p2-scaffold', NOW(), b'0', 0); + +-- ③ runtime_package:先建 status=1 行 + 空 package_json 占位(engineBundle 由 seed 脚本 UPDATE 覆写本行) +-- sandbox_attr/allow_origins 取既有发布默认;package_url 空=DB-manifest 内嵌路 +INSERT INTO game_runtime_package + (id, game_id, version_id, template_id, package_url, entry, runtime_version, preload_policy, + bundle_size, checksum, sandbox_attr, allow_origins, status, + creator, create_time, updater, update_time, deleted, tenant_id, package_json) +VALUES + (92010, 9201, 92010, 'wanglanmei-ref', '', 'index.html', '1.0.0', 'eager', + 0, '', 'allow-scripts', '', 1, + 'p2-scaffold', NOW(), 'p2-scaffold', NOW(), b'0', 0, NULL); + +-- ④ feed_rank:zone_id=0(混合流) + status=1 入流 + sort_score=0 +INSERT INTO game_feed_rank + (id, game_id, version_id, zone_id, quality_score, boost, sort_score, pinned, status, + creator, create_time, updater, update_time, deleted, tenant_id) +VALUES + (92010, 9201, 92010, 0, 0.0000, 0.0000, 0.0000, 0, 1, + 'p2-scaffold', NOW(), 'p2-scaffold', NOW(), b'0', 0); + +COMMIT; + +-- 回读四态 +SELECT 'project' k, id, status, current_version_id FROM game_project WHERE id=9201 +UNION ALL SELECT 'version', id, status, game_id FROM game_version WHERE id=92010 +UNION ALL SELECT 'runtime_pkg', id, status, version_id FROM game_runtime_package WHERE id=92010 +UNION ALL SELECT 'feed_rank', id, status, zone_id FROM game_feed_rank WHERE id=92010; diff --git a/game-cloud/scripts/p2_seed_engine_bundle.mjs b/game-cloud/scripts/p2_seed_engine_bundle.mjs new file mode 100644 index 00000000..1f4fe9af --- /dev/null +++ b/game-cloud/scripts/p2_seed_engine_bundle.mjs @@ -0,0 +1,192 @@ +#!/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}`); diff --git a/game-runtime/games/wanglanmei-ref/BUILD.md b/game-runtime/games/wanglanmei-ref/BUILD.md index 76ebbe83..d4254bcc 100644 --- a/game-runtime/games/wanglanmei-ref/BUILD.md +++ b/game-runtime/games/wanglanmei-ref/BUILD.md @@ -12,13 +12,32 @@ cd game-runtime && node --test "games/wanglanmei-ref/test/*.test.mjs" ## 1. 构建(esbuild 锁参,沿 scripts/build.mjs 同参数:bundle+minify+iife+es2019+sourcemap=false) -在 `game-runtime/` 目录下(需 `npm i -D esbuild`,mini-desktop 已具备): +在 `game-runtime/` 目录下(需 `npm i -D esbuild`,mini-desktop 已具备)。**P2 起本目录产两套 bundle,各服务一个消费方,已固化为 npm script:** + +### 1.1 P1 ref 取证 harness bundle(`WanglanmeiRef` · index.html 用) ```bash -node scripts/build.mjs games/wanglanmei-ref/entry.js games/wanglanmei-ref/dist/ref-bundle.js --global-name=WanglanmeiRef +npm run build:ref-harness +# 等价:node scripts/build.mjs games/wanglanmei-ref/entry.js games/wanglanmei-ref/dist/ref-bundle.js --global-name=WanglanmeiRef ``` -产物:`games/wanglanmei-ref/dist/ref-bundle.js`(iife 全局 `WanglanmeiRef`,含 core 协议 + 所用插件 + 游戏全部代码;资产不打包,运行时相对路径取 `./assets/`)。 +产物:`games/wanglanmei-ref/dist/ref-bundle.js`(iife 全局 `WanglanmeiRef`,导出 `bootRef`;含 core 协议 + 所用插件 + 游戏全部代码 + 引擎;资产不打包,运行时相对路径取 `./assets/`)。**index.html 与 §2 真人试玩/取证走它**(P1 real 引擎门 6/6 沿用,P2 不动)。 + +### 1.2 P2 studio-consumable bundle(`__GameBundle` · studio host 引擎包分支用 · T1b-β P2) + +```bash +npm run build:ref-bundle +# 等价:node scripts/build.mjs games/wanglanmei-ref/entry-bundle.js games/wanglanmei-ref/dist/ref-bundle.iife.js --global-name=__GameBundle +``` + +产物:`games/wanglanmei-ref/dist/ref-bundle.iife.js`(iife 全局 **`__GameBundle`**,契约冻结名;入口 `entry-bundle.js`)。 + +- **studio 接缝(契约冻结·对齐 `game-studio/src/host/contract.ts:204` 与 `game-package.schema.json` engineBundle)**: + - `window.__GameBundle.GameHostFactory` = `GameHostFactory`(= game-shop 工厂;宿主据此建游戏实例,契约 `game-host.d.ts:59`);`window.__GameBundle.default` 同此引用。 + - `window.__GameBundle.bootGameHost({ canvas, statusEl?, seed? })` = P2 boot 入口(studio iframe 内调它启动;引擎已内联进本 bundle,studio 仓无需 littlejsengine)。 +- **载体(O-P2-2 ① DB-manifest 内嵌)**:bundle 文本灌进 GamePackage `engineBundle` 字段 → studio iframe 内联 ` 转义,防止资源字段意外闭合脚本标签。 const ctxJson = safeJson({ pkg, traceId, gameId, versionId }); - // CSP:禁止一切网络出站(connect-src 'none'),仅允许内联脚本执行(沙箱受控)。 - // 注意:img-src 放开 data:/blob:/https,便于 demo 资源占位加载,但 connect 仍禁。 + // P2 引擎包分支:bundle 文本作独立内联 (含大小写/含空白变体) + // 与