#!/usr/bin/env python3 """Bootstrap AirXDB GUI debug context files.""" from __future__ import annotations import argparse import json import os from datetime import datetime, timezone from pathlib import Path from typing import Dict, Tuple MARKER_BEGIN = "" MARKER_END = "" REQUIRED_MIDSCENE_ENV = [ "MIDSCENE_MODEL_NAME", "MIDSCENE_MODEL_BASE_URL", "MIDSCENE_MODEL_API_KEY", ] SEMANTIC_MIDSCENE_ENV = [ *REQUIRED_MIDSCENE_ENV, "MIDSCENE_MODEL_FAMILY", ] OPTIONAL_MIDSCENE_ENV = [ "MCP_SERVER_REQUEST_TIMEOUT", ] MODEL_FAMILY_VALUES = [ "doubao-vision", "doubao-seed", "gemini", "qwen2.5-vl", "qwen3-vl", "qwen3.5", "qwen3.6", "vlm-ui-tars", "vlm-ui-tars-doubao", "vlm-ui-tars-doubao-1.5", "glm-v", "auto-glm", "auto-glm-multilingual", "gpt-5", ] def now_iso() -> str: return datetime.now(timezone.utc).isoformat() def airxdb_agents_block() -> str: return f"""{MARKER_BEGIN} ## AirXDB GUI Debug Workflow 1. Use AirXDB for `/airxdb` sessions that debug browser or desktop GUI issues with Midscene.js. 2. Before working, load: - `AirPlan/AGENTS.md` - `AirPlan/docs/architecture/adr/` - `AirPlan/docs/architecture/c4/module.md` - `AirPlan/docs/debug/gui-debug-log.md` 3. Classify the target first: - Web + Playwright - Web + Chrome Bridge - Desktop Computer / Playground - MCP 4. Before semantic visual actions, require Midscene model config: - `MIDSCENE_MODEL_NAME` - `MIDSCENE_MODEL_BASE_URL` - `MIDSCENE_MODEL_API_KEY` - `MIDSCENE_MODEL_FAMILY` 5. Screenshot capture is allowed without semantic model config and should be used as AirDbg diagnostic evidence. 6. Reproduce visually first and keep Midscene report paths, screenshots, and commands. 7. Use AirXDB with AirDbg when GUI reproduction and code-level fixing are both needed. 8. Update ADR when long-term GUI automation or bridge/MCP choices become architecture context. 9. Update C4 module when UI automation boundaries, browser bridge layers, or desktop control boundaries change. 10. Keep GUI debug logs resumable and concise. {MARKER_END} """ def c4_module_template() -> str: return """# C4 Module ## System Context - TODO: Describe the product, users, and UI surfaces involved in the GUI issue. ## Containers - TODO: Describe browser, frontend app, desktop app, automation runner, and external services. ## Modules | Module | Responsibility | Public Interfaces | Dependencies | Data Ownership | GUI Debug Notes | | --- | --- | --- | --- | --- | --- | | TODO | TODO | TODO | TODO | TODO | TODO | ## Automation / Observability Boundaries - TODO: Record Playwright, Bridge, Desktop, MCP, report generation, and screenshot boundaries. ## Change Log - TODO: Record GUI-debug-related boundary changes. """ def adr_template() -> str: return """# ADR-0001: AirXDB GUI Debug Governance - Status: Accepted - Date: TODO ## Context GUI debugging needs stable visual reproduction, report evidence, and durable project context across sessions. ## Decision Use AirXDB to maintain `AirPlan/AGENTS.md`, C4 module docs, ADR records, and `AirPlan/docs/debug/gui-debug-log.md` during Midscene-based GUI debugging. ## Consequences - GUI issues can be reproduced with report evidence. - Long-term GUI automation choices become traceable. - AirDbg can consume AirXDB evidence for code-level fixes. ## Alternatives - Screenshot-only chat debugging: rejected because it is hard to resume and verify. """ def gui_debug_log_template() -> str: return """# GUI Debug Log Append entries for AirXDB sessions. ## Entry Template ### YYYY-MM-DD: short GUI issue title - Target surface: TODO - Midscene mode: Playwright / Bridge / Computer / MCP - Symptom: TODO - Expected: TODO - Actual: TODO - Reproduction: TODO - Report path: TODO - Key observations: TODO - Hand-off to AirDbg: TODO - Validation after fix: TODO - ADR/C4 updates: TODO - Residual risk: TODO """ def midscene_env_example() -> str: return """# AirXDB Midscene model configuration # # Copy this file to `.airxdb/midscene.local.env` and fill values locally, # or export the same variables in your shell before running AirXDB. # Never commit real API keys. MIDSCENE_MODEL_NAME= MIDSCENE_MODEL_BASE_URL= MIDSCENE_MODEL_API_KEY= # Required for semantic visual actions such as act, Tap, Input, # KeyboardPress, MouseMove with locate prompts, and aiLocate. # Common values: # - gpt-5 for GPT-5.x visual models, including gpt-5.4 # - qwen2.5-vl # - qwen3-vl # - gemini # - doubao-seed MIDSCENE_MODEL_FAMILY= # Optional: MCP_SERVER_REQUEST_TIMEOUT=120000 """ def airxdb_gitignore_template() -> str: return """midscene.local.env """ def write_if_missing(path: Path, content: str) -> bool: if path.exists(): return False path.parent.mkdir(parents=True, exist_ok=True) path.write_text(content, encoding="utf-8", newline="\n") return True def upsert_agents_md(path: Path) -> str: block = airxdb_agents_block().rstrip() + "\n" if path.exists(): original = path.read_text(encoding="utf-8") existed = True else: original = "# AGENTS.md\n\n" existed = False begin = original.find(MARKER_BEGIN) end = original.find(MARKER_END) if begin >= 0 and end > begin: end += len(MARKER_END) updated = original[:begin].rstrip() + "\n\n" + block + original[end:].lstrip() status = "updated" else: updated = original.rstrip() + "\n\n" + block status = "updated" if existed else "created" path.parent.mkdir(parents=True, exist_ok=True) path.write_text(updated, encoding="utf-8", newline="\n") return status def artifact_map(project_root: Path) -> Dict[str, Path]: return { "AGENTS.md": project_root / "AirPlan" / "AGENTS.md", "c4_module": project_root / "AirPlan" / "docs" / "architecture" / "c4" / "module.md", "adr_dir": project_root / "AirPlan" / "docs" / "architecture" / "adr", "adr_0001": project_root / "AirPlan" / "docs" / "architecture" / "adr" / "ADR-0001-airxdb-gui-debug-governance.md", "gui_debug_log": project_root / "AirPlan" / "docs" / "debug" / "gui-debug-log.md", "model_env_example": project_root / "AirPlan" / "state" / "airxdb" / "midscene.env.example", "airxdb_gitignore": project_root / "AirPlan" / "state" / "airxdb" / ".gitignore", "model_env_local": project_root / "AirPlan" / "state" / "airxdb" / "midscene.local.env", "state": project_root / "AirPlan" / "state" / "airxdb" / "state.json", } def load_local_midscene_env(project_root: Path) -> str: env_file = artifact_map(project_root)["model_env_local"] if not env_file.exists(): return "missing" loaded = 0 for raw_line in env_file.read_text(encoding="utf-8-sig").splitlines(): line = raw_line.strip() if not line or line.startswith("#") or "=" not in line: continue key, value = line.split("=", 1) key = key.strip() value = value.strip().strip('"').strip("'") if key and key not in os.environ and value: os.environ[key] = value loaded += 1 return f"loaded:{loaded}" def infer_model_family(model_name: str) -> str: normalized = model_name.strip().lower() if not normalized: return "" if normalized.startswith("gpt-5"): return "gpt-5" if "qwen3" in normalized: return "qwen3-vl" if "qwen2.5" in normalized or "qwen-2.5" in normalized: return "qwen2.5-vl" if "gemini" in normalized: return "gemini" if "doubao" in normalized: return "doubao-seed" if "glm" in normalized: return "glm-v" return "" def status_from_missing(missing: list[str], invalid: bool = False) -> str: if invalid: return "invalid:MIDSCENE_MODEL_FAMILY" return "ok" if not missing else "missing:" + ",".join(missing) def midscene_config_health() -> Dict[str, object]: checked_names = SEMANTIC_MIDSCENE_ENV + OPTIONAL_MIDSCENE_ENV present = {name: bool(os.environ.get(name)) for name in checked_names} missing_basic = [name for name in REQUIRED_MIDSCENE_ENV if not present[name]] missing_semantic = [name for name in SEMANTIC_MIDSCENE_ENV if not present[name]] model_name = os.environ.get("MIDSCENE_MODEL_NAME", "").strip() model_family = os.environ.get("MIDSCENE_MODEL_FAMILY", "").strip() valid_model_family = (not model_family) or model_family in MODEL_FAMILY_VALUES suggested_model_family = infer_model_family(model_name) basic_ready = not missing_basic semantic_ready = basic_ready and not missing_semantic and valid_model_family return { "ready": semantic_ready, "basicReady": basic_ready, "semanticReady": semantic_ready, "required": REQUIRED_MIDSCENE_ENV, "requiredForSemanticActions": SEMANTIC_MIDSCENE_ENV, "optional": OPTIONAL_MIDSCENE_ENV, "present": present, "missing": missing_basic, "missingForSemanticActions": missing_semantic, "validModelFamily": valid_model_family, "modelFamily": model_family, "suggestedModelFamily": suggested_model_family, "validModelFamilies": MODEL_FAMILY_VALUES, } def write_state(path: Path, enabled: bool, project_root: Path) -> None: artifacts = artifact_map(project_root) health = { name: artifacts[name].exists() for name in [ "AGENTS.md", "c4_module", "adr_dir", "adr_0001", "gui_debug_log", "model_env_example", ] } config_health = midscene_config_health() payload = { "enabled": enabled, "updatedAt": now_iso(), "projectRoot": str(project_root), "artifactHealth": health, "midsceneConfig": config_health, "screenshotCapture": { "requiresModelConfig": False, "recommendedScript": "airxdb_computer_mcp_smoke.py --action screenshot", }, } path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8") def enter_mode(project_root: Path) -> Tuple[str, Dict[str, str]]: artifacts = artifact_map(project_root) results: Dict[str, str] = {} results["midscene_local_env"] = load_local_midscene_env(project_root) results["AGENTS.md"] = upsert_agents_md(artifacts["AGENTS.md"]) results["c4_module"] = "created" if write_if_missing(artifacts["c4_module"], c4_module_template()) else "exists" artifacts["adr_dir"].mkdir(parents=True, exist_ok=True) results["adr_dir"] = "exists" results["adr_0001"] = "created" if write_if_missing(artifacts["adr_0001"], adr_template()) else "exists" results["gui_debug_log"] = "created" if write_if_missing(artifacts["gui_debug_log"], gui_debug_log_template()) else "exists" results["model_env_example"] = "created" if write_if_missing(artifacts["model_env_example"], midscene_env_example()) else "exists" results["airxdb_gitignore"] = "created" if write_if_missing(artifacts["airxdb_gitignore"], airxdb_gitignore_template()) else "exists" config_health = midscene_config_health() results["midscene_basic_config"] = status_from_missing(config_health["missing"]) # type: ignore[arg-type] results["midscene_semantic_config"] = status_from_missing( config_health["missingForSemanticActions"], # type: ignore[arg-type] invalid=not bool(config_health["validModelFamily"]), ) results["midscene_config"] = results["midscene_semantic_config"] write_state(artifacts["state"], True, project_root) return "enabled", results def exit_mode(project_root: Path) -> Tuple[str, Dict[str, str]]: artifacts = artifact_map(project_root) write_state(artifacts["state"], False, project_root) return "disabled", {} def status_mode(project_root: Path) -> Tuple[str, Dict[str, str]]: artifacts = artifact_map(project_root) local_env_status = load_local_midscene_env(project_root) state_file = artifacts["state"] enabled = False if state_file.exists(): try: payload = json.loads(state_file.read_text(encoding="utf-8")) enabled = bool(payload.get("enabled")) except json.JSONDecodeError: enabled = False results = { name: ("ok" if path.exists() else "missing") for name, path in artifacts.items() if name not in ["state", "model_env_local"] } results["midscene_local_env"] = local_env_status config_health = midscene_config_health() results["midscene_basic_config"] = status_from_missing(config_health["missing"]) # type: ignore[arg-type] results["midscene_semantic_config"] = status_from_missing( config_health["missingForSemanticActions"], # type: ignore[arg-type] invalid=not bool(config_health["validModelFamily"]), ) results["midscene_config"] = results["midscene_semantic_config"] return ("enabled" if enabled else "disabled"), results def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Manage AirXDB GUI debug artifacts.") parser.add_argument("--mode", choices=["enter", "exit", "status"], default="enter") parser.add_argument("--project", default=".") return parser.parse_args() def main() -> None: args = parse_args() project_root = Path(args.project).expanduser().resolve() if args.mode == "enter": mode_state, result = enter_mode(project_root) elif args.mode == "exit": mode_state, result = exit_mode(project_root) else: mode_state, result = status_mode(project_root) print(f"airxdb_mode={mode_state}") print(f"project_root={project_root}") for key, value in result.items(): print(f"{key}={value}") config_health = midscene_config_health() if mode_state == "enabled" and not config_health["semanticReady"]: missing = ",".join(config_health["missingForSemanticActions"]) # type: ignore[arg-type] print("midscene_config_required=true") print(f"missing_midscene_env={missing}") if not config_health["validModelFamily"]: print(f"invalid_midscene_model_family={config_health['modelFamily']}") if config_health["suggestedModelFamily"]: print(f"suggested_midscene_model_family={config_health['suggestedModelFamily']}") print("valid_midscene_model_families=" + ",".join(MODEL_FAMILY_VALUES)) print("midscene_config_prompt=请先提供 Midscene 模型配置:MIDSCENE_MODEL_NAME、MIDSCENE_MODEL_BASE_URL、MIDSCENE_MODEL_API_KEY、MIDSCENE_MODEL_FAMILY;可选 MCP_SERVER_REQUEST_TIMEOUT。不要把真实 API key 写入 ADR/C4/debug log。") if __name__ == "__main__": main()