From 0cf649b0991ad15d7f9e9c81e352701c6cef9119 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E4=BA=94=E9=A6=99=E4=B8=B8=E5=AD=90?= <15518898337@163.com>
Date: Sun, 23 Aug 2026 17:21:09 +0800
Subject: [PATCH 1/2] =?UTF-8?q?=E8=AE=A9=E8=BF=90=E8=A1=8C=E8=A7=86?=
=?UTF-8?q?=E7=AA=97=E6=8C=89=E5=86=85=E5=AE=B9=E8=87=AA=E9=80=82=E5=BA=94?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
为本地预览注入只读尺寸桥并校验 iframe 消息来源
按可用区域等比缩放完整游戏画面并移除横纵滚动条
补充前端与 Rust 回归测试并同步运行视窗文档
---
.../src-tauri/src/preview.rs | 86 +++++++++
.../src-tauri/src/tests/project.rs | 22 +++
.../LocalGamePreviewFrame.tsx | 165 +++++++++++++++++-
apps/ai-game-creator-shell/src/styles.css | 24 ++-
.../tests/localGamePreviewFrame.test.ts | 58 ++++++
...AI游戏创作】项目开发工作台PRD-2026-07-20.md | 5 +-
.../shared-memory/decision-log.md | 6 +
...案】AI游戏创作智能体App实施计划-2026-06-24.md | 2 +-
8 files changed, 350 insertions(+), 18 deletions(-)
create mode 100644 apps/ai-game-creator-shell/tests/localGamePreviewFrame.test.ts
diff --git a/apps/ai-game-creator-shell/src-tauri/src/preview.rs b/apps/ai-game-creator-shell/src-tauri/src/preview.rs
index f8846b851..e9f664963 100644
--- a/apps/ai-game-creator-shell/src-tauri/src/preview.rs
+++ b/apps/ai-game-creator-shell/src-tauri/src/preview.rs
@@ -89,6 +89,53 @@ const PREVIEW_REQUEST_MAX_HEADER_BYTES: usize = 32 * 1024;
const PREVIEW_REQUEST_MAX_HEADER_LINES: usize = 100;
const PREVIEW_RESPONSE_DRAIN_TIMEOUT: Duration = Duration::from_millis(250);
const PREVIEW_RESPONSE_DRAIN_MAX_BYTES: usize = 32 * 1024;
+const PREVIEW_FIT_BRIDGE_PATH: &str = "/__genarrative/local-preview-fit.js";
+const PREVIEW_FIT_BRIDGE_SCRIPT: &str = r#"(() => {
+ const messageType = 'genarrative.local-preview-size.v1';
+ let ready = document.readyState === 'complete';
+ let frame = 0;
+ const publish = () => {
+ frame = 0;
+ if (!ready || window.parent === window) return;
+ const root = document.documentElement;
+ const body = document.body;
+ window.parent.postMessage({
+ type: messageType,
+ contentWidth: Math.ceil(Math.max(root?.scrollWidth || 0, body?.scrollWidth || 0)),
+ contentHeight: Math.ceil(Math.max(root?.scrollHeight || 0, body?.scrollHeight || 0)),
+ viewportWidth: window.innerWidth,
+ viewportHeight: window.innerHeight,
+ }, '*');
+ };
+ const schedule = () => {
+ if (frame === 0) frame = window.requestAnimationFrame(publish);
+ };
+ const activate = () => {
+ ready = true;
+ const style = document.createElement('style');
+ style.dataset.genarrativePreviewFit = 'true';
+ style.textContent = 'html,body{scrollbar-width:none}html::-webkit-scrollbar,body::-webkit-scrollbar{display:none}';
+ (document.head || document.documentElement).appendChild(style);
+ schedule();
+ if (typeof ResizeObserver === 'function') {
+ const resizeObserver = new ResizeObserver(schedule);
+ resizeObserver.observe(document.documentElement);
+ if (document.body) resizeObserver.observe(document.body);
+ }
+ const mutationObserver = new MutationObserver(schedule);
+ mutationObserver.observe(document.documentElement, {
+ childList: true,
+ subtree: true,
+ attributes: true,
+ characterData: true,
+ });
+ window.addEventListener('resize', schedule, { passive: true });
+ };
+ if (ready) activate();
+ else window.addEventListener('load', activate, { once: true });
+})();"#;
+const PREVIEW_FIT_BRIDGE_TAG: &str =
+ r#""#;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum PreviewListenerAcceptDisposition {
@@ -560,6 +607,20 @@ pub(crate) fn build_preview_response(root: &Path, method: &str, url_path: &str)
);
}
+ if url_path.split('?').next() == Some(PREVIEW_FIT_BRIDGE_PATH) {
+ let body = if is_head {
+ Vec::new()
+ } else {
+ PREVIEW_FIT_BRIDGE_SCRIPT.as_bytes().to_vec()
+ };
+ return http_response(
+ "200 OK",
+ "text/javascript; charset=utf-8",
+ &body,
+ PREVIEW_FIT_BRIDGE_SCRIPT.len(),
+ );
+ }
+
let file_path = match resolve_preview_path(root, url_path) {
Ok(path) => path,
Err(_) => {
@@ -574,11 +635,36 @@ pub(crate) fn build_preview_response(root: &Path, method: &str, url_path: &str)
return http_response("404 Not Found", "text/plain", body, b"not found".len());
}
};
+ let body = if content_type(&file_path).starts_with("text/html") {
+ inject_preview_fit_bridge(body)
+ } else {
+ body
+ };
let content_length = body.len();
let body = if is_head { Vec::new() } else { body };
http_response("200 OK", content_type(&file_path), &body, content_length)
}
+fn inject_preview_fit_bridge(body: Vec) -> Vec {
+ let html = match String::from_utf8(body) {
+ Ok(html) => html,
+ Err(error) => return error.into_bytes(),
+ };
+ if html.contains(PREVIEW_FIT_BRIDGE_PATH) {
+ return html.into_bytes();
+ }
+ let lowercase = html.to_ascii_lowercase();
+ let insertion = lowercase
+ .rfind("