""" AirDo mode — V2 任务执行器。 V2 改进:全专家插件强制路由(L1 代码级),task_id 注入防护,UI 任务 frontend-design Skill 路由(P1-20)。 路由规则:GUI→XDB, network→NDB, C/C++→SDB, blocked/failed→Dbg, done→Rvr """ from __future__ import annotations import subprocess import sys from pathlib import Path from air_runtime.io import atomic_json_write, safe_json_load from air_runtime.paths import airplan_root, event_log_path from air_runtime.events import EventLog, TASK_COMPLETED, TASK_BLOCKED, TASK_ENTERED, TASK_FINISHED from air_runtime.contracts import WorkerResult, now_iso from air_runtime.utils import sanitize_task_id, session_stamp # GUI 任务检测关键词 GUI_INDICATORS = { "gui", "ui", "render", "layout", "dialog", "osd", "overlay", "visual", "screenshot", "display", "widget", "pane", "toolbar", "settings_dialog", "界面", "渲染", "布局", "按钮", "对话框", "窗口", "菜单", "控件", } # 网络任务检测关键词 NETWORK_INDICATORS = { "network", "rtsp", "http", "tcp", "udp", "tls", "dns", "proxy", "socket", "stream", "port", "网络", "抓包", "rtmp", "webrtc", "sip", } # C/C++ 文件扩展名 CPP_EXTENSIONS = {".cpp", ".cxx", ".cc", ".c", ".hpp", ".hxx", ".h", ".h++"} def _has_gui_indicators(task_text: str, files_dirs: str) -> bool: text = f"{task_text} {files_dirs}".lower() return any(kw in text for kw in GUI_INDICATORS) def _has_network_indicators(task_text: str, files_dirs: str) -> bool: text = f"{task_text} {files_dirs}".lower() return any(kw in text for kw in NETWORK_INDICATORS) def _has_cpp_files(files_changed: list[str]) -> bool: return any( any(f.endswith(ext) for ext in CPP_EXTENSIONS) for f in files_changed ) # P1-20: UI 任务检测关键词(支持中英文) UI_TASK_INDICATORS = ( # 英文关键词 "gui", "ui", "render", "layout", "dialog", "osd", "overlay", "visual", "screenshot", "display", "widget", "pane", "toolbar", "settings_dialog", "canvas", "button", "window", "popup", "menu", "drm", "kms", "opengl", "vulkan", "frontend", "react", "vue", "angular", "web", "css", "html", # 中文关键词 "界面", "UI", "界面设计", "前端", "界面开发", "按钮", "对话框", "窗口", "菜单", "控件", "渲染", "布局", "登录界面", "界面组件", ) def is_ui_task(task_text: str) -> bool: """P1-20: 检测任务是否涉及 UI/前端界面设计。""" text = task_text.lower() return any(kw in text for kw in UI_TASK_INDICATORS) def ensure_frontend_design_skill() -> bool: """P1-20: 检测 frontend-design Skill 是否存在,不存在则尝试自动安装。""" # 检查 skill 是否已安装(检查 ~/.claude/skills/frontend-design 或类似路径) import os home = Path.home() skill_path = home / ".claude" / "skills" / "frontend-design" if skill_path.exists(): return True # 尝试自动安装 import logging logging.info("frontend-design skill not found, attempting auto-install...") try: result = subprocess.run( ["claude", "plugin", "install", "frontend-design"], capture_output=True, text=True, timeout=60, ) if result.returncode == 0: logging.info("frontend-design skill installed successfully") return True logging.warning("frontend-design skill install failed: %s", result.stderr) except Exception as e: logging.warning("frontend-design skill install error: %s", e) return False def route_ui_task(task_text: str, task_id: str) -> dict: """P1-20: UI 任务路由决策。检测 UI 任务并确保 frontend-design Skill 可用。""" if not is_ui_task(task_text): return {"target": "execute", "skill": None, "is_ui_task": False} # 是 UI 任务,检查 skill 可用性 if ensure_frontend_design_skill(): return {"target": "execute", "skill": "frontend-design", "is_ui_task": True} # Skill 不可用,阻止任务 return { "target": "blocked", "reason": "UI task requires frontend-design skill but installation failed", "skill": "frontend-design", "is_ui_task": True, } def _paths(project_root: Path, task_id: str) -> dict[str, Path]: root = airplan_root(project_root) / "state" / "airdo" task_dir = root / "tasks" / task_id return { "root": root, "state": root / "state.json", "task_dir": task_dir, "brief": task_dir / "brief.md", "handoff": task_dir / "subagent-handoff.md", "result": task_dir / "result.json", "worker_state": task_dir / "worker-state.json", } def _ensure_dirs(paths: dict[str, Path]) -> None: paths["task_dir"].mkdir(parents=True, exist_ok=True) def enter_worker(project_root: Path, task_id: str, task_text: str = "") -> dict: """P1-20: 新增 task_text 参数用于 UI 任务检测。""" tid = sanitize_task_id(task_id) paths = _paths(project_root, tid) _ensure_dirs(paths) # P1-20: UI 任务检测和路由 ui_routing = {"target": "execute", "skill": None, "is_ui_task": False} if task_text: ui_routing = route_ui_task(task_text, tid) if ui_routing.get("target") == "blocked": # UI 任务但 skill 不可用,阻止执行 worker_state = { "taskId": tid, "status": "blocked", "enteredAt": now_iso(), "resultPath": str(paths["result"]), "blockReason": ui_routing.get("reason", "frontend-design skill unavailable"), "uiRouting": ui_routing, } atomic_json_write(paths["worker_state"], worker_state) atomic_json_write(paths["state"], {"enabled": True, "activeTaskId": tid, "updatedAt": now_iso(), "blocked": True}) log = EventLog(event_log_path(project_root)) log.emit(TASK_BLOCKED, {"taskId": tid, "reason": ui_routing.get("reason")}) return { "taskId": tid, "status": "blocked", "blockReason": ui_routing.get("reason"), "uiRouting": ui_routing, } worker_state = { "taskId": tid, "status": "implementing", "enteredAt": now_iso(), "resultPath": str(paths["result"]), "uiRouting": ui_routing, } atomic_json_write(paths["worker_state"], worker_state) atomic_json_write(paths["state"], {"enabled": True, "activeTaskId": tid, "updatedAt": now_iso()}) log = EventLog(event_log_path(project_root)) log.emit(TASK_ENTERED, {"taskId": tid, "uiRouting": ui_routing}) return { "taskId": tid, "briefPath": str(paths["brief"]), "handoffPath": str(paths["handoff"]), "resultPath": str(paths["result"]), "workerStatePath": str(paths["worker_state"]), "uiRouting": ui_routing, } def finish_worker(project_root: Path, task_id: str, result_path: Path | None = None) -> dict: """V2 核心改进:全专家插件强制路由。 路由规则(按优先级): 1. blocked/failed → AirDbg(调试定位根因) 2. done 无证据 → AirDbg(审查验证) 3. GUI 任务 → AirXDB(截图取证) 4. 网络任务 → AirNDB(抓包取证) 5. C/C++ 任务 → AirSDB(静态分析) 6. 所有 done 任务 → AirRvr(需求一致性审查) 无强制路由时才允许 merge。 """ tid = sanitize_task_id(task_id) paths = _paths(project_root, tid) # 加载 result if result_path and result_path.exists(): result_data = safe_json_load(result_path) elif paths["result"].exists(): result_data = safe_json_load(paths["result"]) else: result_data = {"taskId": tid, "status": "blocked", "summary": "no result found"} if not isinstance(result_data, dict): result_data = {"taskId": tid, "status": "blocked"} result = WorkerResult.from_dict(result_data) status = result.status # 从 task-graph.json 获取任务描述用于分类 task_text = "" files_dirs = "" tg_json = airplan_root(project_root) / "state" / "airarc" / "reviews" / "task-graph.json" if tg_json.exists(): from air_runtime.task_graph import TaskGraph graph = TaskGraph.load(tg_json) node = graph.nodes.get(tid) if node: task_text = node.task files_dirs = node.files_dirs decisions = [] # 1. blocked/failed → 强制 AirDbg(最高优先级) if status in ("blocked", "failed"): decisions.append({ "target": "airdbg", "forced": True, "reason": f"status={status} — AirDbg mandatory before return", }) # 2. done 但无实质验证 → 强制 AirDbg elif status == "done": if not result.validations and not result.files_changed: decisions.append({ "target": "airdbg", "forced": True, "reason": "done without evidence — mandatory debug review", }) # 3. GUI 任务 → 强制 AirXDB 截图 if _has_gui_indicators(task_text, files_dirs): xdb_sessions = result_data.get("xdbSessions") or result_data.get("xdb_sessions") or [] if not xdb_sessions: decisions.append({ "target": "airxdb", "forced": True, "reason": "GUI task requires screenshot evidence", }) # 4. 网络任务 → 强制 AirNDB 抓包 if _has_network_indicators(task_text, files_dirs): ndb_sessions = result_data.get("ndbSessions") or result_data.get("ndb_sessions") or [] if not ndb_sessions: decisions.append({ "target": "airndb", "forced": True, "reason": "network task requires packet capture evidence", }) # 5. C/C++ 任务 → 强制 AirSDB 静态分析 if _has_cpp_files(result.files_changed): sdb_reports = result_data.get("sdbReports") or result_data.get("sdb_reports") or [] if not sdb_reports: decisions.append({ "target": "airsdb", "forced": True, "reason": "C/C++ task requires static analysis", }) # 6. 所有 done 任务 → 强制 AirRvr 审查(已完成则跳过) rvr_reviewed = ( result_data.get("rvrReviewed") or result_data.get("rvr_reviewed") or result_data.get("rvrReviews") or result_data.get("rvr_reviews") or [] ) if not rvr_reviewed: decisions.append({ "target": "airrvr", "forced": True, "reason": "completed task requires requirements review", }) # 无强制路由时才允许 merge if not decisions: decisions.append({"target": "merge", "forced": False}) # 持久化 finalized = result.to_dict() finalized["routingDecisions"] = decisions finalized["routingDecision"] = decisions[0] # 向后兼容:主路由决策 finalized["finalizedAt"] = now_iso() atomic_json_write(paths["result"], finalized) atomic_json_write(paths["worker_state"], { "taskId": tid, "status": "finished", "resultPath": str(paths["result"]), "routingDecisions": decisions, "routingDecision": decisions[0], }) log = EventLog(event_log_path(project_root)) log.emit(TASK_FINISHED, { "taskId": tid, "status": status, "routingTargets": [d["target"] for d in decisions], }) # emit task.completed / task.blocked based on final status if status == "done": log.emit(TASK_COMPLETED, {"taskId": tid, "routingTargets": [d["target"] for d in decisions]}) elif status in ("blocked", "failed"): log.emit(TASK_BLOCKED, {"taskId": tid, "status": status}) return { "taskId": tid, "status": status, "finalizedResultPath": str(paths["result"]), "workerStatePath": str(paths["worker_state"]), "routingDecisions": decisions, "routingDecision": decisions[0], } def status_worker(project_root: Path) -> dict: paths = _paths(project_root, "_") state = safe_json_load(paths["state"]) or {} task_ids = [] if paths["root"].joinpath("tasks").exists(): task_ids = [d.name for d in paths["root"].joinpath("tasks").iterdir() if d.is_dir()] return { "enabled": state.get("enabled", False), "activeTaskId": state.get("activeTaskId", ""), "taskIds": task_ids, } def main(args) -> None: project_root = Path(args.project).expanduser().resolve() sub = args.sub or "status" tid = args.task_id if sub == "status": s = status_worker(project_root) print("airplan_mode=do") print(f"enabled={s['enabled']}") print(f"active_task_id={s['activeTaskId']}") print(f"known_tasks={','.join(s['taskIds'])}") elif sub == "enter": task_text = getattr(args, "task_text", "") or "" result = enter_worker(project_root, tid, task_text=task_text) print("airplan_mode=do") print(f"task_id={result['taskId']}") print(f"brief_path={result['briefPath']}") print(f"result_path={result['resultPath']}") print(f"worker_state_path={result['workerStatePath']}") elif sub == "finish": rpath = Path(args.result).expanduser().resolve() if args.result else None finalized = finish_worker(project_root, tid, rpath) targets = [d["target"] for d in finalized.get("routingDecisions", [])] print("airplan_mode=do") print(f"task_id={finalized['taskId']}") print(f"status={finalized['status']}") print(f"routing_targets={','.join(targets)}") print(f"routing_forced={any(d.get('forced') for d in finalized.get('routingDecisions', []))}")