Files
AirCoding/AirPlan/docs/spec/AirPlan-ParaV2/plugins/airxdb/scripts/airxdb_mode.py
AirCoding ae44be31d5 chore: push all design docs, V2 plan specs, and current working state
Includes AirPlan design documents, AircOding-alpha1-plan, AirPlanV2,
AirPlan-ParaV2, AirPlan-Para V1 reference docs, and all working code
changes across packages.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-12 17:12:29 +08:00

430 lines
15 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 = "<!-- AIRXDB:BEGIN -->"
MARKER_END = "<!-- AIRXDB: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()