Files
Genarrative/apps/ai-game-creator-shell/src-tauri/src/browser/model.rs
T
kdletters e424c5539e 修复游戏图集事务与俄罗斯方块验收
为九个固定图集合同路径补齐持久事务、原子标记、锁守卫和崩溃恢复。

统一校验主图与切片的资源、资产和任务身份,避免远端请求期间占用项目写锁。

收紧俄罗斯方块静态语义、浏览器动作因果和固定场景合同,拒绝注释与伪 telemetry 绕过。

迁移旧 Generic 俄罗斯方块根合同和试玩子任务合同,并失效旧场景回执。

补齐事务故障、恶意脚本、真实 Chrome、game-chat 修复和合同迁移回归测试。

同步 AI 游戏创作技术方案与共享决策记录。
2026-08-04 18:58:48 +08:00

272 lines
8.0 KiB
Rust

use std::collections::HashSet;
use std::path::PathBuf;
use std::time::Duration;
use serde::{Deserialize, Deserializer, Serialize};
pub(super) const RESULT_SCHEMA_VERSION: &str = "browser-validation.v1";
pub(super) const DEFAULT_SETTLE_MS: u64 = 800;
pub(super) const MAX_SETTLE_MS: u64 = 30_000;
pub(super) const MAX_EXPECTED_TEXT_ITEMS: usize = 32;
pub(super) const MAX_EXPECTED_TEXT_CHARS: usize = 512;
pub(super) const MAX_URL_CHARS: usize = 2_048;
pub(super) const BROWSER_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum BrowserValidationViewport {
Desktop,
Mobile,
}
pub(super) const REQUIRED_VIEWPORTS: [BrowserValidationViewport; 2] = [
BrowserValidationViewport::Desktop,
BrowserValidationViewport::Mobile,
];
impl BrowserValidationViewport {
pub(super) fn dimensions(self) -> (u32, u32, bool) {
match self {
Self::Desktop => (1280, 720, false),
Self::Mobile => (390, 844, true),
}
}
pub(super) fn file_stem(self) -> &'static str {
match self {
Self::Desktop => "desktop",
Self::Mobile => "mobile",
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum BrowserPlaytestScenario {
GenericV1,
TetrisV1,
LaneDefenseV1,
}
impl BrowserPlaytestScenario {
pub(super) fn as_str(self) -> &'static str {
match self {
Self::GenericV1 => "generic-v1",
Self::TetrisV1 => "tetris-v1",
Self::LaneDefenseV1 => "lane-defense-v1",
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct BrowserValidationInput {
pub url: String,
#[serde(deserialize_with = "deserialize_fixed_viewports")]
pub viewports: Vec<BrowserValidationViewport>,
#[serde(default)]
pub expected_text: Vec<String>,
#[serde(default = "default_settle_ms")]
pub settle_ms: u64,
#[serde(default = "default_fail_on_console_error")]
pub fail_on_console_error: bool,
#[serde(default)]
pub playtest_scenario: Option<BrowserPlaytestScenario>,
pub evidence_root: PathBuf,
}
fn default_settle_ms() -> u64 {
DEFAULT_SETTLE_MS
}
fn default_fail_on_console_error() -> bool {
true
}
pub(super) fn validate_fixed_viewports(
viewports: &[BrowserValidationViewport],
) -> Result<(), String> {
if viewports.len() != REQUIRED_VIEWPORTS.len() {
return Err("viewports 必须且只能同时包含 desktop 和 mobile".to_string());
}
let mut unique = HashSet::new();
if viewports.iter().any(|viewport| !unique.insert(*viewport)) {
return Err("viewports 不能重复".to_string());
}
if REQUIRED_VIEWPORTS
.iter()
.any(|required| !unique.contains(required))
{
return Err("viewports 只能包含 desktop 和 mobile".to_string());
}
Ok(())
}
fn deserialize_fixed_viewports<'de, D>(
deserializer: D,
) -> Result<Vec<BrowserValidationViewport>, D::Error>
where
D: Deserializer<'de>,
{
let viewports = Vec::<BrowserValidationViewport>::deserialize(deserializer)?;
validate_fixed_viewports(&viewports).map_err(serde::de::Error::custom)?;
Ok(viewports)
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum DiscoveredBrowserKind {
Chrome,
Edge,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DiscoveredBrowser {
pub kind: DiscoveredBrowserKind,
pub executable_path: PathBuf,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BrowserIdentity {
pub kind: DiscoveredBrowserKind,
pub product: String,
pub protocol_version: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BrowserValidationEvidencePaths {
pub root: PathBuf,
pub report_path: PathBuf,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BrowserExpectedTextMatch {
pub text: String,
pub found: bool,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum BrowserPlaytestPhase {
Ready,
Playing,
Won,
Lost,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct BrowserPlaytestAssertion {
pub name: String,
pub passed: bool,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct BrowserPlaytestResult {
pub scenario: BrowserPlaytestScenario,
pub scenario_fingerprint: String,
pub passed: bool,
pub initial_sequence: Option<u64>,
pub initial_phase: Option<BrowserPlaytestPhase>,
pub initial_level: Option<u64>,
pub final_sequence: Option<u64>,
pub final_phase: Option<BrowserPlaytestPhase>,
pub final_level: Option<u64>,
pub assertions: Vec<BrowserPlaytestAssertion>,
pub diagnostics: Vec<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BrowserConsoleMessage {
pub level: String,
pub text: String,
pub source_url: Option<String>,
pub line_number: Option<u32>,
pub column_number: Option<u32>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BrowserException {
pub text: String,
pub source_url: Option<String>,
pub line_number: u32,
pub column_number: u32,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BrowserFailedRequest {
pub url: String,
pub method: String,
pub resource_type: String,
pub error_text: String,
pub status_code: Option<u16>,
pub canceled: bool,
pub blocked_by_policy: bool,
pub fatal: bool,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BrowserCanvasProbe {
pub width: u32,
pub height: u32,
pub css_width: f64,
pub css_height: f64,
pub visible_area: f64,
pub sample_count: u32,
pub non_empty_pixel_count: u32,
pub non_empty: Option<bool>,
pub probe_error: Option<String>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BrowserViewportValidationResult {
pub viewport: BrowserValidationViewport,
pub width: u32,
pub height: u32,
pub final_url: String,
pub title: String,
pub ready_state: String,
pub visible_text_summary: String,
pub visible_text_character_count: usize,
pub dom_character_count: usize,
pub expected_text: Vec<BrowserExpectedTextMatch>,
pub console_errors: Vec<BrowserConsoleMessage>,
pub console_warnings: Vec<BrowserConsoleMessage>,
pub exceptions: Vec<BrowserException>,
pub failed_requests: Vec<BrowserFailedRequest>,
pub canvases: Vec<BrowserCanvasProbe>,
pub blocked_popup_count: u32,
pub blocked_dialog_count: u32,
pub blocked_download_count: u32,
pub blocked_permission_count: u32,
pub blocked_service_worker_count: u32,
pub screenshot_path: PathBuf,
pub passed: bool,
pub diagnostics: Vec<String>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BrowserValidationResult {
pub schema_version: String,
pub url: String,
pub browser: BrowserIdentity,
pub passed: bool,
pub viewport_results: Vec<BrowserViewportValidationResult>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub playtest: Option<BrowserPlaytestResult>,
pub diagnostics: Vec<String>,
pub evidence: BrowserValidationEvidencePaths,
pub completed_at_unix_ms: u64,
}