feat: P1-19 P1-20 实现 - 边界测试强制 + 高风险审计 + UI Skill 路由

P1-19.1: Arc 边界测试强制
- TaskNode 新增 test_required 字段
- _inject_boundary_tests() 为每个模块注入接口测试和单元测试任务
- Done When 验证必须包含"测试通过"

P1-19.2: AirRvr 高风险审计
- 新增 HighRiskAudit, HighRiskFinding 数据类
- ReviewReport 新增 highRiskAudit 字段,含 lifecycle/nullPointer/danglingPointer/exceptionSafety/concurrency + overallRisk + deliveryVerdict
- 序列化/反序列化支持

P1-19.3: block-release 集成
- dispatch_worker_group() 派发前扫描最新审查报告
- deliveryVerdict=block-release 时阻止所有后续派发
- 记录 eng.blocked 事件

P1-20: frontend-design Skill 集成
- is_ui_task() UI 任务检测
- ensure_frontend_design_skill() 自动安装 Skill
- route_ui_task() UI 任务路由决策
- enter_worker() 集成 UI 检测,skill 不可用时阻止执行
- commands/do.md 更新 UI 处理说明
- SKILL.md 新增 INV-12/INV-13/INV-14

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
AirLongDian
2026-06-10 17:06:48 +08:00
commit e73a4da354
54 changed files with 6054 additions and 0 deletions

View File

@@ -0,0 +1,161 @@
"""AirXDB screenshot capture backends with automatic fallback chain.
Three capture methods:
- KmsGrabCapture: DRM/KMS native screenshot via ffmpeg (no sudo)
- XvfbCapture: Xvfb virtual framebuffer screenshot
- FallbackCapture: text placeholder when no GUI capture is available
CaptureManager tries them in order (kms -> xvfb -> fallback) unless
a specific method is requested.
"""
from __future__ import annotations
import shutil
import subprocess
from dataclasses import dataclass
from pathlib import Path
@dataclass
class CaptureResult:
success: bool
output_path: Path | None
method: str # kms, xvfb, fallback
error: str | None = None
class KmsGrabCapture:
"""KMS/DRM screenshot via ffmpeg -f kmsgrab.
No sudo is ever used. The caller must have read access to
/dev/dri/cardX for this to work.
"""
def capture(self, output_path: Path) -> CaptureResult:
# 1. Detect whether the current user can access a DRI device
dri_dev = Path("/dev/dri/card0")
if dri_dev.exists():
try:
# Test read permission (no sudo)
open(dri_dev).close()
except PermissionError:
return CaptureResult(False, None, "kms", "no /dev/dri permission")
else:
return CaptureResult(False, None, "kms", "no GPU")
# 2. Use ffmpeg directly (no sudo)
try:
result = subprocess.run(
[
"ffmpeg", "-y", "-f", "kmsgrab", "-i", "-",
"-frames:v", "1",
"-vf", "hwdownload,format=bgr0",
"-f", "image2", str(output_path),
],
capture_output=True,
text=True,
timeout=30,
)
if result.returncode == 0 and output_path.exists():
return CaptureResult(True, output_path, "kms", None)
return CaptureResult(False, None, "kms", result.stderr)
except FileNotFoundError:
return CaptureResult(False, None, "kms", "ffmpeg not found")
except subprocess.TimeoutExpired:
return CaptureResult(False, None, "kms", "ffmpeg timed out")
except Exception as e:
return CaptureResult(False, None, "kms", str(e))
class XvfbCapture:
"""Xvfb virtual display screenshot via xvfb-run + scrot."""
def capture(
self,
output_path: Path,
width: int = 1920,
height: int = 1080,
) -> CaptureResult:
# Check xvfb-run availability
if not shutil.which("xvfb-run"):
return CaptureResult(False, None, "xvfb", "xvfb-run not found")
# Use scrot as the screenshot tool inside Xvfb
try:
result = subprocess.run(
[
"xvfb-run",
"--auto-servernum",
"--server-args",
f"-screen 0 {width}x{height}x24",
"--",
"scrot",
str(output_path),
],
capture_output=True,
text=True,
timeout=60,
)
if result.returncode == 0 and output_path.exists():
return CaptureResult(True, output_path, "xvfb", None)
return CaptureResult(False, None, "xvfb", result.stderr)
except FileNotFoundError:
return CaptureResult(False, None, "xvfb", "xvfb-run not found")
except subprocess.TimeoutExpired:
return CaptureResult(False, None, "xvfb", "xvfb-run timed out")
except Exception as e:
return CaptureResult(False, None, "xvfb", str(e))
class FallbackCapture:
"""Fallback: write a text placeholder when no GUI capture is available."""
def capture(
self,
output_path: Path,
description: str = "",
) -> CaptureResult:
output_path.write_text(
f"[GUI capture not available]\n{description}\n"
)
return CaptureResult(True, output_path, "fallback", None)
class CaptureManager:
"""Auto-select the best capture method with fallback chain.
Order: kms -> xvfb -> fallback.
Use ``prefer`` to force a specific method or "auto" for the chain.
"""
def __init__(self) -> None:
self.kms = KmsGrabCapture()
self.xvfb = XvfbCapture()
self.fallback = FallbackCapture()
def capture(
self,
output_path: Path,
prefer: str = "auto",
) -> CaptureResult:
"""
prefer: "kms" | "xvfb" | "fallback" | "auto"
auto mode tries kms -> xvfb -> fallback in order.
"""
if prefer in ("kms", "auto"):
result = self.kms.capture(output_path)
if result.success:
return result
if prefer == "kms":
return result # forced kms failed, return failure
if prefer in ("xvfb", "auto"):
result = self.xvfb.capture(output_path)
if result.success:
return result
if prefer == "xvfb":
return result # forced xvfb failed, return failure
# fallback
return self.fallback.capture(output_path, "all capture methods failed")