Files
Genarrative/apps/ai-game-creator-shell/src-tauri/src/browser/model.rs
T
kdletters 485ed50b26
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (push) Has been cancelled
Project CI / AI game creator shell Rust shard 1/4 (push) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (push) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (push) Has been cancelled
AGC 七项交付效率优化并升级捆绑 Codex 到 0.155.1 (#439)
## 背景

净月潭案例(2026-09-19 21:53 → 09-20 01:26,3 小时 33 分)的问题不是模型慢,而是环境未就绪、验收靠模型自述、预算只约束单个工具入口、工具调用被 SDK 全局串行闸门卡住。本 PR 落地确认后的七项交付效率合同,并把捆绑 Codex 升到当前 npm latest。

## 结果

- 新建 Web 游戏在正式生成前由宿主自动预检:捆绑 Node/npm、真实 Vite 构建、受限浏览器桌面/移动截图;失败不启动生成或付费素材。
- 首个副作用前冻结交付合同,宿主保存权威证据与预算;证据齐全后先封口、排空并取得执行器完整退出证明,再产出交付报告,模型回复不再当作验收。
- 原生 shell、内置浏览器与托管命令、第三方 MCP 共用同一执行许可和累计执行时间;`validation.maxRuns` 改为执行/返修批次语义,另设 `maxTurnSeconds` 墙钟上限。
- 所有工具可并发:移除 SDK 全局串行的 `apply_patch`/`update_plan` 注册,改由宿主 `agc_apply_patch`(官方 parser + 当前回合写许可 + 受控进程树 + 短项目事务)和 `agc_update_plan` 提供等价能力;真实请求目录里已无串行注册,长 MCP 与补丁/计划实测在同一响应内重叠。
- 付费提交与本地写入绑定原回合原租约:容量与同动作锁等待可取消,封口、取消或预算耗尽后零新增提交;已越过提交边界的请求保留 operation ID 走 GET 对账,不自动重放。
- 新增请求与工具分段计时、有界并行批读和首轮上下文预取,未知耗时不补零。
- 捆绑 Codex 0.147.0 → 0.155.1:固定版本收敛到 `build_support/codex_bundle.rs` 单一声明,vendor 解析源码按 `rust-v0.155.1` 逐字节重取并更新 UPSTREAM 证据,适配 0.155 统一 exec(`exec_command`/`write_stdin`);macOS 侧车最小系统版本仍为 15.0。

## 验证

- 生产 CLI → 捆绑 Codex 0.155.1 → 本地 Responses/MCP 夹具 9/9:补丁、完成收尾、批次、只读/可写 MCP 并发、原生命令并发、原生资源并发、统一 exec 会话、期限终止(真实重叠 775 / 764 / 999 ms)。
- 真实模型目录 2/2;vendor 上游库 111 项;宿主定向与回归 106 项;真实 Windows 进程与取消 21 项;Node 侧门禁 52 项。
- 发行载荷:artifact-only 重建 NSIS,解包验证侧车清单 `codex-cli 0.155.1`、6 个组件哈希、打包后 `bin/codex.exe --version`、Node 运行时 2130 文件与双端 PNG、`--environment-check` ready。
- `check-config`、TypeScript、编码(4991 文件)、文档索引、`git diff --check`、`cargo fmt --check` 全部通过。

## 未覆盖

- 真实陶泥儿登录态 Provider 生成尚未执行(本机 `--llm-status` 返回 `authentication-required`)。
- macOS 侧车只在本机做静态 Mach-O 与清单解析,未在 macOS 上跑 `check-macos-bundle.mjs`。
- 全仓库聚合套件在本机仍因负载敏感的后台 mock-provider 用例而红,与本次改动无关:改动前的旧二进制同样失败,换单线程后相关用例 3/3 通过。

---------

Co-authored-by: kdletters <61648117+kdletters@users.noreply.github.com>
Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/439
2026-09-21 02:29:29 +08:00

312 lines
9.4 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,
RunnerV1,
}
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",
Self::RunnerV1 => "runner-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>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub runner_evidence: Option<BrowserRunnerEvidence>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct BrowserRunnerEvidence {
pub seed: u32,
pub course_fingerprint: String,
pub input_kind: String,
pub short_apex_milli_px: u64,
pub long_apex_milli_px: u64,
pub short_jump_cut_count: u64,
pub fair_window_ms: i64,
pub sampled_ticks: u64,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct BrowserViewportPlaytestResult {
pub viewport: BrowserValidationViewport,
pub result: BrowserPlaytestResult,
}
pub(crate) fn required_viewport_playtests_passed(
scenario: BrowserPlaytestScenario,
results: &[BrowserViewportPlaytestResult],
) -> bool {
results.len() == REQUIRED_VIEWPORTS.len()
&& REQUIRED_VIEWPORTS.iter().all(|viewport| {
let mut matching = results.iter().filter(|entry| entry.viewport == *viewport);
matching
.next()
.is_some_and(|entry| entry.result.scenario == scenario && entry.result.passed)
&& matching.next().is_none()
})
}
#[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>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub viewport_playtests: Vec<BrowserViewportPlaytestResult>,
pub diagnostics: Vec<String>,
pub evidence: BrowserValidationEvidencePaths,
pub completed_at_unix_ms: u64,
}