Files
nmfs-agents/services/edge_validator/main.py
T
qiuruiandClaude Sonnet 4.6 df59fc0c83 feat: NMFS Agents 全量入库(org v2 + dashboard + 多队列 + 组织架构)
包含从项目创建至今的全部代码首次入库:

核心框架:
- TaskQueue(SQLite,6 种状态,原子 dequeue)
- Executor(asyncio 并发,semaphore 限流,agent_role 路由)
- Scheduler(APScheduler,三队列独立调度)
- Watchdog(超时检测 + 优先级防饥饿)
- Scanner(项目扫描,README/TODO/CLAUDE.md 提取)

Agent 体系(20+ 角色):
- 项目交付组:architect/developer/tester/productizer
- 基础技术组:base-architect/validator/hw/os/kernel/lowlevel/system-tester
- 算法组:algo-antishake/position/nav
- 洞察组:planner/vision-analyst/media-producer
- 市场组:market-pm/sport/elder/safety
- 组织层:boss/group-leader/senior-dev/ops
- 平台扩展:nrf-dev/esp32-dev
- 三专项调研:rtp-researcher/net-researcher/kernel-analyzer

工具层:
- ProjectMemory(goals/facts/history SQLite)
- VersionPipeline(并行版本流水线 + 反幻觉门控)
- EventBus(Redis Streams,5 consumer groups)
- DailyReport(append-only 日报 + Boss 决策视图)
- AgentScorer(4 维评分:完成度/质量/自主性/协作)
- DeviceAgent(SSH rsync + 板端执行)
- EnvCollector(本机+SSH 环境采集)
- ArchVersion(快照/优化/promote/rollback)

Dashboard(React + Vite + Tailwind):
- 看板/项目/配置/洞察/日报/Visual 六标签页
- WebSocket 实时更新
- Markdown 渲染(react-markdown + remark-gfm)

测试:314 passed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 07:24:19 +08:00

208 lines
7.2 KiB
Python
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.
"""EdgeValidatorService: 部署在 RK3588 上,为开发机 agents 提供 HTTP 接口。
启动:uvicorn main:app --host 0.0.0.0 --port 8899
"""
from __future__ import annotations
import base64
import glob
import re
import subprocess
import time
from datetime import datetime
from pathlib import Path
from typing import Optional
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
app = FastAPI(title="EdgeValidatorService", version="1.0.0")
app.add_middleware(CORSMiddleware, allow_origins=["*"],
allow_methods=["*"], allow_headers=["*"])
# 设备配置(本机 + 接入的 MCU)
SERIAL_DEVICES: dict[str, dict] = {}
# 推理日志路径
LOG_PATHS = {
"default": "/tmp/infer.log",
"yolo": "/tmp/yolo_infer.log",
}
class RunTestRequest(BaseModel):
cmd: str
cwd: str = "/home/pi/Desktop"
timeout: int = 120
class FlashRequest(BaseModel):
firmware_b64: str
offset: str = "0x0"
port: str = "/dev/ttyUSB0"
# ── 端点 ──────────────────────────────────────────────────────────
@app.get("/health")
def health():
return {"status": "ok", "timestamp": datetime.utcnow().isoformat()}
@app.get("/devices")
def list_devices():
"""列出识别到的串口设备"""
serial_ports = glob.glob("/dev/ttyUSB*") + glob.glob("/dev/ttyACM*")
return {"rk3588": "linux/ssh", "serial_ports": serial_ports,
"configured_mcus": list(SERIAL_DEVICES.keys())}
@app.get("/metrics/{device_name}")
def get_metrics(device_name: str):
if device_name == "rk3588":
return _linux_metrics()
if device_name in SERIAL_DEVICES:
return _mcu_metrics(SERIAL_DEVICES[device_name])
raise HTTPException(404, f"未知设备: {device_name}")
@app.post("/run-test/{device_name}")
def run_test(device_name: str, req: RunTestRequest):
if device_name == "rk3588":
return _run_local(req)
if device_name in SERIAL_DEVICES:
return _run_serial(SERIAL_DEVICES[device_name], req)
raise HTTPException(404, f"未知设备: {device_name}")
@app.post("/screenshot")
def screenshot():
"""截取 RK3588 屏幕(需要 scrot 或 ffmpeg"""
import tempfile
import os
tmp = tempfile.mktemp(suffix=".png")
r = subprocess.run(["scrot", tmp], capture_output=True, timeout=10)
if r.returncode != 0:
r = subprocess.run(
f"cat /dev/fb0 | ffmpeg -vcodec rawvideo -f rawvideo "
f"-pix_fmt rgb32 -s 1920x1080 -i - -f image2 -vcodec png {tmp} -y",
shell=True, capture_output=True, timeout=15,
)
if r.returncode != 0:
raise HTTPException(500, "截图失败,请确认 scrot 或 ffmpeg 已安装")
with open(tmp, "rb") as f:
img_b64 = base64.b64encode(f.read()).decode()
os.unlink(tmp)
return {"image_b64": img_b64, "format": "png"}
@app.get("/logs/{device_name}/{n}")
def get_logs(device_name: str, n: int = 50):
log_path = LOG_PATHS.get(device_name, LOG_PATHS["default"])
try:
r = subprocess.run(["tail", f"-{n}", log_path],
capture_output=True, text=True, timeout=5)
return {"lines": r.stdout.splitlines(), "file": log_path}
except Exception as e:
return {"lines": [], "error": str(e), "file": log_path}
# ── 内部函数 ──────────────────────────────────────────────────────
def _run_local(req: RunTestRequest) -> dict:
t0 = time.monotonic()
r = subprocess.run(
req.cmd, shell=True, capture_output=True, text=True,
cwd=req.cwd, timeout=req.timeout,
)
duration = time.monotonic() - t0
stdout = r.stdout
pass_ = r.returncode == 0 and not re.search(r"\b(FAILED|ERROR)\b", stdout + r.stderr)
return {"rc": r.returncode, "stdout": stdout, "stderr": r.stderr,
"duration_s": round(duration, 2), "pass_": pass_}
def _run_serial(dev: dict, req: RunTestRequest) -> dict:
try:
import serial # type: ignore
except ImportError:
return {"rc": 1, "stdout": "", "stderr": "pyserial 未安装",
"duration_s": 0, "pass_": False}
t0 = time.monotonic()
lines: list[str] = []
try:
ser = serial.Serial(dev["port"], dev.get("baud", 115200), timeout=req.timeout)
ser.write((req.cmd + "\r\n").encode())
deadline = time.monotonic() + req.timeout
while time.monotonic() < deadline:
line = ser.readline().decode(errors="replace").strip()
if line:
lines.append(line)
if line.startswith(("PASS", "FAIL", "ERROR")):
break
ser.close()
except Exception as e:
return {"rc": 1, "stdout": "\n".join(lines), "stderr": str(e),
"duration_s": time.monotonic() - t0, "pass_": False}
stdout = "\n".join(lines)
rc = 0 if any(l.startswith("PASS") for l in lines) else 1
return {"rc": rc, "stdout": stdout, "stderr": "",
"duration_s": round(time.monotonic() - t0, 2), "pass_": rc == 0}
def _linux_metrics() -> dict:
m: dict = {"device": "rk3588", "timestamp": datetime.utcnow().isoformat()}
try:
with open("/proc/stat") as f:
parts = f.readline().split()
user, nice, sys_, idle = int(parts[1]), int(parts[2]), int(parts[3]), int(parts[4])
total = user + nice + sys_ + idle
m["cpu_pct"] = round(100 * (user + sys_) / total, 1) if total else 0
except Exception:
m["cpu_pct"] = None
try:
r = subprocess.run(["free", "-m"], capture_output=True, text=True, timeout=3)
for line in r.stdout.splitlines():
if line.startswith("Mem:"):
parts = line.split()
m["mem_used_mb"] = float(parts[2])
break
except Exception:
m["mem_used_mb"] = None
try:
r = subprocess.run(
"tail -10 /tmp/infer.log 2>/dev/null | grep -oP 'fps=\\K[0-9.]+' | tail -1",
shell=True, capture_output=True, text=True, timeout=3,
)
raw = r.stdout.strip()
m["fps"] = float(raw) if raw else None
except Exception:
m["fps"] = None
return m
def _mcu_metrics(dev: dict) -> dict:
m: dict = {"device": dev.get("name", "mcu"),
"timestamp": datetime.utcnow().isoformat()}
try:
import serial # type: ignore
ser = serial.Serial(dev["port"], dev.get("baud", 115200), timeout=3)
ser.write(b"GET_METRICS\r\n")
for _ in range(20):
line = ser.readline().decode(errors="replace").strip()
if line.startswith("METRICS:"):
for kv in line[8:].split():
k, _, v = kv.partition("=")
if k == "fps":
m["fps"] = float(v)
elif k == "latency_ms":
m["inference_latency_ms"] = float(v)
elif k == "ram_kb":
m["ram_used_kb"] = int(v)
break
ser.close()
except Exception as e:
m["error"] = str(e)
return m