zizi ce9085df6b feat(runner-v2): P3 3a+3b-A 派发面 plumbing——便宜模型生成游戏经后端 dispatch 自动入 feed 真玩(零回归验)
3a canary:pong 真 bundle→buildGamePackage→发布→feed 引擎真渲真玩(gameId 9301,brick-2 真值通路 mvn 13/13 验)。3b-A 派发面:执行器对 generic dispatch 外置 worker→worker 回调→buildGamePackage→feed,stub worker e2e 实证(任务129→版本93111→gameId 9302 引擎真渲 CDP 11/11)。

改动:① 点亮 AigcTemplateConstants(List.of(generic))+PromptResourceLoader ② 派发面 AigcGenerateExecutor(dispatchGeneric 分支:仅 dispatchTemplateId=generic 走、其余进程内 LLM 旧路逐行不变=零回归)+WorkerDispatchClient+AigcExecutorProperties/Configuration ③ 内网回调 AdminAigcTaskController @PermitAll /dify/callback-internal(注入系统身份解 updater-null=部署窗口#3 同根因复发,镜像 executeWithSystemIdentity)④ stub worker scripts/wg1_stub_worker.py + 灌库/发布桩 p3_*/p3b_* + config worker-url ⑤ 单测 +3 派发面用例(dispatch/失败/零回归 clicker 不 dispatch)⑥ 蒸馏 ai-generation-pipeline.md §⓪′ 派发面范式+2 红线。

零回归硬约束:AigcGenerateExecutorTest 20/0/0、AigcTaskServiceImplTest 9/0/0,clicker 仍进程内(dispatchSender 零交互)。主笔亲核 gating diff(:342-344 仅 generic)+肉眼复核 feed 真玩截图。🔴 安全红线(留 3b-B):/dify/callback-internal 跳 RBAC 仅恃内网兜底,真 worker/外部接入前必补 HMAC/mTLS 服务间签名。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:19:25 +00:00

172 lines
9.9 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
/**
* 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}`);