430 lines
15 KiB
Python
430 lines
15 KiB
Python
#!/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()
|