根治 B(缩 M3 错面,结构性根除"点击在错 phase 消费/永不消费→启动不了"): - input 订阅收归 L1(_template/src/game.js wrapper):L3 只写实例方法 handleTap(x,y)/handleKey(key), L1 在 pointerdown/keydown 直达派发;_template/src/game-logic.js 范例改用 handleTap。 - tools.mjs check:拦 ctx.getInput + 必须暴露 ≥1 输入方法;prompt.mjs/README/skill 同步输入契约。 - 机制+门双实证(node 全链路:menu→点开始→play、点客加分、点空白不加分;check 对 getInput/零输入方法拦截)。 driver(play.cdp.cjs):occupied 族起局从固定点 (195,422) 改中线纵向扫点,robust 命中各游戏「开始」按钮 (原固定点落空→死菜单假阴;实测 wanglanmei「开门营业」在 y≈502)。 资产通道(boot-game-host.loadHostAssets + gen.mjs --assets-dir):M3 只决定怎么用资产、不造。 入库工具 publish-amgen.mjs:staged bundle → game_runtime_package(package_json.engineBundle,status=1)→ studio /play。 5 个 M3 footgun 蒸馏进 prompt.mjs + skill(均 wanglanmei-v2 实测翻车): ① scene update 回调签名 (dt) 非 (running,dt) ② ctx.random.next() 非 ctx.random() ③ ctx.time.nowMs() 非 ctx.nowMs() ④ _forensicsView 的 phase/targets 必须在 state() 内实时算(否则定格 boot 快照) ⑤ update(dt) 的 dt 是秒,毫秒计时要 ×1000。 验证:M3 生成 wanglanmei-tycoon-v2(+上述 5 footgun 修复)→ 九门 PASS(score 0→364,distinct 43/43) → 入库 → studio /play/80002/80002 加载+boot+渲染菜单真玩(art 走程序化回退,水彩 art 需引擎资产线 E2/E3)。 (root-fix A 门可信化先于 cbe42933 提交;SAA Java 驱动绑定另track 暂不入此 commit) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
123 lines
6.9 KiB
JavaScript
123 lines
6.9 KiB
JavaScript
/**
|
|
* publish-amgen.mjs — 把 amodel-gen(SAA 模式)产出的自包含 bundle 入库到 game_runtime_package,
|
|
* 使其在 studio /play/<gameId>/<versionId> 可试玩。复刻 SAA emit + publish_flip 的产物形态(package_json.engineBundle + status=1)。
|
|
*
|
|
* 用法(在 mini 上跑):
|
|
* node publish-amgen.mjs --staged <_wg1-gen/<gid> 绝对路径> --game-id <数字> --version-id <数字> --title "..." [--summary "..."] [--exec]
|
|
* 不带 --exec = 干跑(只打印 SQL 摘要 + 校验和,不写库);带 --exec = 真写库(经 docker exec mysql)。
|
|
*
|
|
* 注意:engineBundle 是大文本,SQL 字面量经 JS 侧转义(\\ 与 ')可靠回环;package_json 入 JSON 列由 MySQL 校验。
|
|
*/
|
|
'use strict';
|
|
import { readFileSync, existsSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { createHash } from 'node:crypto';
|
|
import { spawnSync } from 'node:child_process';
|
|
|
|
function arg(n, d) { const i = process.argv.indexOf('--' + n); return i >= 0 ? process.argv[i + 1] : d; }
|
|
const sha256 = (s) => createHash('sha256').update(s, 'utf8').digest('hex');
|
|
// MySQL 单引号字面量转义(默认 NO_BACKSLASH_ESCAPES off):\ → \\,' → \'。回环精确。
|
|
const sqlStr = (s) => "'" + String(s).replace(/\\/g, '\\\\').replace(/'/g, "\\'") + "'";
|
|
|
|
const staged = arg('staged');
|
|
const gameId = arg('game-id');
|
|
const versionId = arg('version-id');
|
|
const title = arg('title', '网兰梅的小卖部');
|
|
const summary = arg('summary', 'A-model 生成·街角小卖部经营(点客成单赚营业额)');
|
|
const templateId = arg('template-id', 'generic');
|
|
const doExec = process.argv.includes('--exec');
|
|
|
|
if (!staged || !gameId || !versionId) {
|
|
console.error('缺参:--staged <dir> --game-id <n> --version-id <n> [--title ..] [--exec]');
|
|
process.exit(2);
|
|
}
|
|
const bundlePath = join(staged, 'bundle.iife.js');
|
|
if (!existsSync(bundlePath)) { console.error('找不到 bundle.iife.js:' + bundlePath + '(先确认 gen stage 成功)'); process.exit(2); }
|
|
const engineBundle = readFileSync(bundlePath, 'utf8');
|
|
if (!/__GameBundle/.test(engineBundle)) { console.error('⚠ bundle 未见 __GameBundle 全局名,可能不是 SAA 信封产物'); }
|
|
|
|
const bundleSize = Buffer.byteLength(engineBundle, 'utf8');
|
|
const bundleChecksum = sha256(engineBundle); // manifest.checksum = bundle sha256(宿主完整性校验 T-RT-15)
|
|
const cover = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
|
|
|
|
// package_json 形态对齐线上已发布包(93142 实测):meta/assets/gameId/manifest/versionId/gameConfig/provenance/templateId/schemaVersion/engineBundle。
|
|
const pkg = {
|
|
meta: { cover, title, summary, ageRating: 'all' },
|
|
assets: [], // 本期引擎不消费 runtime 图资产(E2/E3 未做)→ 留空;游戏走程序化回退
|
|
gameId: String(gameId),
|
|
manifest: { entry: 'index.html', checksum: bundleChecksum, bundleSize, preloadPolicy: 'eager', runtimeVersion: '1.0.0' },
|
|
versionId: String(versionId),
|
|
gameConfig: { theme: 'sim', title, templateId, engineDriven: true },
|
|
provenance: { traceId: 'amgen-publish-' + gameId, promptHash: '', generatedAt: '' },
|
|
templateId,
|
|
schemaVersion: '1.0',
|
|
engineBundle,
|
|
};
|
|
const pkgJson = JSON.stringify(pkg);
|
|
const rowChecksum = sha256(pkgJson); // 行列 checksum = sha256(package_json)(publish_flip 自洽校验口径)
|
|
|
|
const sql = `USE \`ruoyi-vue-pro\`;
|
|
SET NAMES utf8mb4;
|
|
START TRANSACTION;
|
|
DELETE FROM game_feed_rank WHERE game_id = ${gameId};
|
|
DELETE FROM game_runtime_package WHERE game_id = ${gameId};
|
|
DELETE FROM game_version WHERE game_id = ${gameId};
|
|
DELETE FROM game_project WHERE id = ${gameId};
|
|
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
|
|
(${gameId}, 1, ${sqlStr(title)}, ${sqlStr(summary)}, ${sqlStr(cover)},
|
|
'amgen,sim,wanglanmei', ${sqlStr(templateId)}, 'all',
|
|
4, ${versionId}, NULL, b'0', 0, 0,
|
|
'0', NOW(), '0', NOW(), b'0', 0);
|
|
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
|
|
(${versionId}, ${gameId}, 2, NULL, '', ${bundleSize}, ${sqlStr(bundleChecksum)},
|
|
3, '0', NOW(), '0', NOW(), b'0', 0);
|
|
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
|
|
(${versionId}, ${gameId}, ${versionId}, ${sqlStr(templateId)}, '', 'index.html', '1.0.0', 'eager',
|
|
${bundleSize}, ${sqlStr(rowChecksum)}, 'allow-scripts', '', 1,
|
|
'0', NOW(), '0', NOW(), b'0', 0, ${sqlStr(pkgJson)});
|
|
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
|
|
(${versionId}, ${gameId}, ${versionId}, 0, 0.9000, 0.0000, 0.9000, 0, 1,
|
|
'0', NOW(), '0', NOW(), b'0', 0);
|
|
COMMIT;
|
|
SELECT 'project' k, id, status, current_version_id FROM game_project WHERE id=${gameId}
|
|
UNION ALL SELECT 'version', id, status, game_id FROM game_version WHERE id=${versionId}
|
|
UNION ALL SELECT 'pkg', id, status, version_id FROM game_runtime_package WHERE id=${versionId}
|
|
UNION ALL SELECT 'feed', id, status, version_id FROM game_feed_rank WHERE id=${versionId};
|
|
SELECT 'engineBundle_len' k, CHAR_LENGTH(JSON_UNQUOTE(JSON_EXTRACT(package_json,'$.engineBundle'))) v FROM game_runtime_package WHERE id=${versionId};
|
|
SELECT 'checksum_selfconsistent' k, (checksum = LOWER(SHA2(package_json,256))) v FROM game_runtime_package WHERE id=${versionId};
|
|
`;
|
|
|
|
console.log(`[publish] staged=${staged}`);
|
|
console.log(`[publish] gameId=${gameId} versionId=${versionId} title=${title}`);
|
|
console.log(`[publish] bundleSize=${bundleSize}B bundleChecksum=${bundleChecksum.slice(0, 16)}… rowChecksum=${rowChecksum.slice(0, 16)}…`);
|
|
console.log(`[publish] package_json 长度=${pkgJson.length} SQL 长度=${sql.length}`);
|
|
|
|
if (!doExec) {
|
|
console.log('\n[dry-run] 未带 --exec,未写库。带 --exec 真写库。');
|
|
process.exit(0);
|
|
}
|
|
|
|
// 取 MySQL root 密码(docker 容器 env)→ 管道 SQL 入 mysql。
|
|
const pwRes = spawnSync('docker', ['exec', 'game-staging-mysql', 'printenv', 'MYSQL_ROOT_PASSWORD'], { encoding: 'utf8' });
|
|
const pw = (pwRes.stdout || '').trim();
|
|
if (!pw) { console.error('取不到 MYSQL_ROOT_PASSWORD'); process.exit(1); }
|
|
const res = spawnSync('docker', ['exec', '-i', 'game-staging-mysql', 'mysql', '--default-character-set=utf8mb4', '-uroot', '-p' + pw, 'ruoyi-vue-pro'],
|
|
{ input: sql, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
process.stdout.write(res.stdout || '');
|
|
if (res.status !== 0) { console.error('[publish] mysql 退出码 ' + res.status + '\n' + (res.stderr || '')); process.exit(1); }
|
|
console.log('\n✅ 入库完成。studio 试玩:/play/' + gameId + '/' + versionId);
|