Files
nmfs-agents/docs/plans/2026-02-27-rockchip-agents-impl.md
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

52 KiB
Raw Blame History

Rockchip Agents 实施计划

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: 构建一个多角色 AI Agent 系统,通过 Claude API 持续优化 /data/rockchip/ 下的模型项目,支持飞书通知/交互和设备端双侧同步。

Architecture: 分四阶段实施。Phase 1 建立基础框架(Scanner + Queue + Developer Agent + Feishu);Phase 2 加入 Device Agent 和三模式;Phase 3 加入 Productizer + Architect AgentPhase 4 完成 Manager Agent 全协调。

Tech Stack: Python 3.10, anthropic SDK, APScheduler, aiohttp, paramiko, SQLite3, pytest

设计文档: docs/plans/2026-02-27-agents-design.md


环境准备(执行前手动完成)

cd /data/rockchip/agents
python3.10 -m venv venv
source venv/bin/activate
pip install anthropic apscheduler aiohttp paramiko pyyaml pytest pytest-asyncio

Phase 1:基础框架

Task 1: 项目脚手架

Files:

  • Create: pyproject.toml
  • Create: src/rockchip_agents/__init__.py
  • Create: src/rockchip_agents/core/__init__.py
  • Create: src/rockchip_agents/agents/__init__.py
  • Create: src/rockchip_agents/tools/__init__.py
  • Create: configs/projects.yaml
  • Create: configs/agents.yaml
  • Create: configs/devices.yaml
  • Create: scripts/run.py

Step 1: 创建 pyproject.toml

[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.backends.legacy:build"

[project]
name = "rockchip-agents"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = [
    "anthropic>=0.40.0",
    "apscheduler>=3.10.0",
    "aiohttp>=3.9.0",
    "paramiko>=3.4.0",
    "pyyaml>=6.0",
]

[project.optional-dependencies]
dev = ["pytest>=8.0", "pytest-asyncio>=0.23"]

[tool.setuptools.packages.find]
where = ["src"]

Step 2: 安装包(开发模式)

source venv/bin/activate && pip install -e ".[dev]"

Expected: Successfully installed rockchip-agents-0.1.0

Step 3: 创建配置文件 configs/projects.yaml

projects:
  yolo:
    path: /data/rockchip/yolo
    device_workspace: /home/pi/Desktop/yolo
    mode: confirm
    description: "RKNN YOLO 目标检测,yolo12n 板端零检出待修复"
  mediapipe:
    path: /data/rockchip/mediapipe
    device_workspace: /home/pi/Desktop/mediapipe
    mode: auto
    description: "MediaPipe 关键点检测,Pose FPS 偏低"
  embedding:
    path: /data/rockchip/embedding
    device_workspace: /home/pi/Desktop/embed-db
    mode: report
    description: "向量数据库,基础功能完整"
  info-privacy:
    path: /data/rockchip/info-privacy
    device_workspace: /home/pi/Desktop/info-privacy
    mode: confirm
    description: "隐私信息遮罩"
  logodetect-x86:
    path: /data/rockchip/logodetect-x86
    mode: report
    description: "Logo 检测训练端,纯 x86 无设备同步"

Step 4: 创建配置文件 configs/agents.yaml

scheduler:
  interval_hours: 1
  max_concurrent: 3

claude:
  model: claude-sonnet-4-6
  # api_key: 从 ANTHROPIC_API_KEY 环境变量读取

feishu:
  # webhook_url: 从 FEISHU_WEBHOOK_URL 环境变量读取
  # bot_token: 从 FEISHU_BOT_TOKEN 环境变量读取
  callback_port: 8765

Step 5: 创建配置文件 configs/devices.yaml

devices:
  rk3588:
    host: 192.168.0.127
    user: pi
    password: "123123"
    workspace: /home/pi/Desktop
    pre_cmd: "clashon && clashproxy on"

Step 6: 创建 scripts/run.py

#!/usr/bin/env python3
"""Rockchip Agents 启动入口"""
from __future__ import annotations

import argparse
import logging

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")


def main() -> None:
    parser = argparse.ArgumentParser(description="Rockchip Agents")
    sub = parser.add_subparsers(dest="cmd")
    sub.add_parser("start", help="启动定时调度器")
    sub.add_parser("scan", help="立即扫描所有项目")
    sub.add_parser("run-next", help="执行队列中下一个任务")
    args = parser.parse_args()
    if args.cmd == "start":
        from rockchip_agents.core.scheduler import start
        start()
    elif args.cmd == "scan":
        from rockchip_agents.core.scanner import Scanner
        from rockchip_agents.config import load_config
        cfg = load_config()
        scanner = Scanner(cfg)
        tasks = scanner.scan_all()
        for t in tasks:
            print(f"[{t['project']}] {t['title']} (priority={t['priority']})")
    elif args.cmd == "run-next":
        import asyncio
        from rockchip_agents.core.executor import Executor
        from rockchip_agents.config import load_config
        cfg = load_config()
        asyncio.run(Executor(cfg).run_next())
    else:
        parser.print_help()


if __name__ == "__main__":
    main()

Task 2: Config 加载器

Files:

  • Create: src/rockchip_agents/config.py
  • Create: tests/test_config.py

Step 1: 写失败测试 tests/test_config.py

from __future__ import annotations

import pytest
from pathlib import Path
from rockchip_agents.config import load_config, AgentsConfig


def test_load_config_returns_typed_config(tmp_path):
    (tmp_path / "projects.yaml").write_text(
        "projects:\n  yolo:\n    path: /data/rockchip/yolo\n    mode: confirm\n"
    )
    (tmp_path / "agents.yaml").write_text(
        "scheduler:\n  interval_hours: 1\n  max_concurrent: 3\n"
        "claude:\n  model: claude-sonnet-4-6\n"
        "feishu:\n  callback_port: 8765\n"
    )
    (tmp_path / "devices.yaml").write_text(
        "devices:\n  rk3588:\n    host: 192.168.0.127\n    user: pi\n    password: '123123'\n"
        "    workspace: /home/pi/Desktop\n    pre_cmd: 'echo'\n"
    )
    cfg = load_config(tmp_path)
    assert "yolo" in cfg.projects
    assert cfg.projects["yolo"].mode == "confirm"
    assert cfg.projects["yolo"].path == Path("/data/rockchip/yolo")
    assert cfg.scheduler.interval_hours == 1
    assert cfg.claude.model == "claude-sonnet-4-6"
    assert "rk3588" in cfg.devices
    assert cfg.devices["rk3588"].host == "192.168.0.127"

Step 2: 运行确认失败

source venv/bin/activate && pytest tests/test_config.py -v

Expected: FAIL — "ModuleNotFoundError: rockchip_agents.config"

Step 3: 实现 src/rockchip_agents/config.py

from __future__ import annotations

import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional

import yaml

CONFIG_DIR = Path(__file__).parent.parent.parent / "configs"


@dataclass
class ProjectConfig:
    path: Path
    mode: str = "report"          # auto | confirm | report
    device_workspace: Optional[str] = None
    description: str = ""


@dataclass
class SchedulerConfig:
    interval_hours: int = 1
    max_concurrent: int = 3


@dataclass
class ClaudeConfig:
    model: str = "claude-sonnet-4-6"
    api_key: str = field(default_factory=lambda: os.environ.get("ANTHROPIC_API_KEY", ""))


@dataclass
class FeishuConfig:
    webhook_url: str = field(default_factory=lambda: os.environ.get("FEISHU_WEBHOOK_URL", ""))
    bot_token: str = field(default_factory=lambda: os.environ.get("FEISHU_BOT_TOKEN", ""))
    callback_port: int = 8765


@dataclass
class DeviceConfig:
    host: str
    user: str
    password: str
    workspace: str
    pre_cmd: str = ""


@dataclass
class AgentsConfig:
    projects: dict[str, ProjectConfig]
    scheduler: SchedulerConfig
    claude: ClaudeConfig
    feishu: FeishuConfig
    devices: dict[str, DeviceConfig]


def load_config(config_dir: Path | None = None) -> AgentsConfig:
    d = config_dir or CONFIG_DIR
    with open(d / "projects.yaml") as f:
        proj_data = yaml.safe_load(f)
    with open(d / "agents.yaml") as f:
        agent_data = yaml.safe_load(f)
    with open(d / "devices.yaml") as f:
        dev_data = yaml.safe_load(f)

    projects = {
        name: ProjectConfig(
            path=Path(v["path"]),
            mode=v.get("mode", "report"),
            device_workspace=v.get("device_workspace"),
            description=v.get("description", ""),
        )
        for name, v in proj_data["projects"].items()
    }
    sched = agent_data.get("scheduler", {})
    claude_d = agent_data.get("claude", {})
    feishu_d = agent_data.get("feishu", {})
    devices = {
        name: DeviceConfig(**v)
        for name, v in (dev_data.get("devices") or {}).items()
    }
    return AgentsConfig(
        projects=projects,
        scheduler=SchedulerConfig(**sched),
        claude=ClaudeConfig(model=claude_d.get("model", "claude-sonnet-4-6")),
        feishu=FeishuConfig(callback_port=feishu_d.get("callback_port", 8765)),
        devices=devices,
    )

Step 4: 运行确认通过

pytest tests/test_config.py -v

Expected: PASS


Task 3: 任务队列

Files:

  • Create: src/rockchip_agents/core/queue.py
  • Create: tests/test_queue.py

Step 1: 写失败测试 tests/test_queue.py

from __future__ import annotations

import pytest
from rockchip_agents.core.queue import TaskQueue, Task


def test_enqueue_and_dequeue(tmp_path):
    q = TaskQueue(db_path=tmp_path / "tasks.db")
    task_id = q.enqueue(Task(
        project="yolo", type="fix_bug", title="修复 yolo12n 零检出",
        priority=1, mode="confirm", agent_role="developer",
        context="zero detection on board"
    ))
    assert task_id > 0
    next_task = q.dequeue()
    assert next_task is not None
    assert next_task.project == "yolo"
    assert next_task.status == "running"


def test_priority_ordering(tmp_path):
    q = TaskQueue(db_path=tmp_path / "tasks.db")
    q.enqueue(Task(project="mediapipe", type="improve_perf", title="提升 FPS",
                   priority=3, mode="auto", agent_role="developer"))
    q.enqueue(Task(project="yolo", type="fix_bug", title="修复零检出",
                   priority=1, mode="confirm", agent_role="developer"))
    first = q.dequeue()
    assert first.priority == 1  # 低数字 = 高优先级


def test_no_duplicate_pending(tmp_path):
    q = TaskQueue(db_path=tmp_path / "tasks.db")
    q.enqueue(Task(project="yolo", type="fix_bug", title="修复零检出",
                   priority=1, mode="confirm", agent_role="developer"))
    # 同一 project+type+title 的任务已存在且 pending/running,不再重复入队
    task_id = q.enqueue(Task(project="yolo", type="fix_bug", title="修复零检出",
                             priority=1, mode="confirm", agent_role="developer"))
    assert task_id == -1  # -1 表示跳过


def test_mark_done(tmp_path):
    q = TaskQueue(db_path=tmp_path / "tasks.db")
    tid = q.enqueue(Task(project="yolo", type="fix_bug", title="修复",
                         priority=1, mode="auto", agent_role="developer"))
    q.dequeue()
    q.mark_done(tid, summary="修复成功")
    assert q.dequeue() is None  # 队列已空

Step 2: 运行确认失败

pytest tests/test_queue.py -v

Expected: FAIL

Step 3: 实现 src/rockchip_agents/core/queue.py

from __future__ import annotations

import sqlite3
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Optional

DEFAULT_DB = Path(__file__).parent.parent.parent.parent / "data" / "tasks.db"


@dataclass
class Task:
    project: str
    type: str        # fix_bug | improve_perf | add_feature | code_review | productize | architect
    title: str
    priority: int    # 1(严重)→ 5(低)
    mode: str        # auto | confirm | report
    agent_role: str  # developer | tester | productizer | architect
    context: str = ""
    id: int = 0
    status: str = "pending"
    result_summary: str = ""
    created_at: str = field(default_factory=lambda: datetime.utcnow().isoformat())


class TaskQueue:
    def __init__(self, db_path: Path | None = None) -> None:
        self._db = db_path or DEFAULT_DB
        self._db.parent.mkdir(parents=True, exist_ok=True)
        self._init_db()

    def _conn(self) -> sqlite3.Connection:
        conn = sqlite3.connect(self._db)
        conn.row_factory = sqlite3.Row
        return conn

    def _init_db(self) -> None:
        with self._conn() as c:
            c.execute("""
                CREATE TABLE IF NOT EXISTS tasks (
                    id           INTEGER PRIMARY KEY AUTOINCREMENT,
                    project      TEXT NOT NULL,
                    type         TEXT NOT NULL,
                    title        TEXT NOT NULL,
                    context      TEXT DEFAULT '',
                    priority     INTEGER DEFAULT 3,
                    mode         TEXT DEFAULT 'report',
                    agent_role   TEXT DEFAULT 'developer',
                    status       TEXT DEFAULT 'pending',
                    created_at   TEXT,
                    started_at   TEXT,
                    completed_at TEXT,
                    result_summary TEXT DEFAULT ''
                )
            """)

    def enqueue(self, task: Task) -> int:
        """返回 task id;若已存在同 project+type+title 的 pending/running 任务则返回 -1。"""
        with self._conn() as c:
            exists = c.execute(
                "SELECT id FROM tasks WHERE project=? AND type=? AND title=? AND status IN ('pending','running')",
                (task.project, task.type, task.title),
            ).fetchone()
            if exists:
                return -1
            cur = c.execute(
                "INSERT INTO tasks (project,type,title,context,priority,mode,agent_role,status,created_at) "
                "VALUES (?,?,?,?,?,?,?,?,?)",
                (task.project, task.type, task.title, task.context,
                 task.priority, task.mode, task.agent_role, "pending", task.created_at),
            )
            return cur.lastrowid

    def dequeue(self) -> Optional[Task]:
        """取出优先级最高的 pending 任务并标记为 running。"""
        with self._conn() as c:
            row = c.execute(
                "SELECT * FROM tasks WHERE status='pending' ORDER BY priority ASC, id ASC LIMIT 1"
            ).fetchone()
            if not row:
                return None
            c.execute(
                "UPDATE tasks SET status='running', started_at=? WHERE id=?",
                (datetime.utcnow().isoformat(), row["id"]),
            )
            t = Task(
                id=row["id"], project=row["project"], type=row["type"],
                title=row["title"], context=row["context"], priority=row["priority"],
                mode=row["mode"], agent_role=row["agent_role"], status="running",
            )
            return t

    def mark_done(self, task_id: int, summary: str = "") -> None:
        with self._conn() as c:
            c.execute(
                "UPDATE tasks SET status='done', completed_at=?, result_summary=? WHERE id=?",
                (datetime.utcnow().isoformat(), summary, task_id),
            )

    def mark_failed(self, task_id: int, reason: str = "") -> None:
        with self._conn() as c:
            c.execute(
                "UPDATE tasks SET status='failed', completed_at=?, result_summary=? WHERE id=?",
                (datetime.utcnow().isoformat(), reason, task_id),
            )

    def mark_waiting_confirm(self, task_id: int, diff: str = "") -> None:
        with self._conn() as c:
            c.execute(
                "UPDATE tasks SET status='waiting_confirm', result_summary=? WHERE id=?",
                (diff, task_id),
            )

Step 4: 创建 data/ 目录

mkdir -p /data/rockchip/agents/data

Step 5: 运行确认通过

pytest tests/test_queue.py -v

Expected: 4 passed


Task 4: 项目扫描器

Files:

  • Create: src/rockchip_agents/core/scanner.py
  • Create: tests/test_scanner.py

Step 1: 写失败测试 tests/test_scanner.py

from __future__ import annotations

import pytest
from pathlib import Path
from rockchip_agents.core.scanner import Scanner, ScanResult
from rockchip_agents.config import AgentsConfig, ProjectConfig, SchedulerConfig, ClaudeConfig, FeishuConfig


def _make_config(project_path: Path, mode: str = "auto") -> AgentsConfig:
    return AgentsConfig(
        projects={"test_proj": ProjectConfig(path=project_path, mode=mode)},
        scheduler=SchedulerConfig(),
        claude=ClaudeConfig(),
        feishu=FeishuConfig(),
        devices={},
    )


def test_scanner_detects_known_issues_from_readme(tmp_path):
    proj = tmp_path / "proj"
    proj.mkdir()
    (proj / "README.md").write_text(
        "## 已知问题\n- yolo12n: 零检出\n- Pose FPS 低于 80\n"
    )
    cfg = _make_config(proj)
    scanner = Scanner(cfg)
    results = scanner.scan_project("test_proj", cfg.projects["test_proj"])
    assert len(results) >= 1
    assert any("yolo12n" in r.title or "零检出" in r.title or "Pose" in r.title for r in results)


def test_scanner_detects_todo_in_code(tmp_path):
    proj = tmp_path / "proj"
    (proj / "src").mkdir(parents=True)
    (proj / "src" / "main.py").write_text(
        "def foo():\n    # TODO: fix edge case\n    pass\n"
    )
    cfg = _make_config(proj)
    scanner = Scanner(cfg)
    results = scanner.scan_project("test_proj", cfg.projects["test_proj"])
    assert any("TODO" in r.title or "todo" in r.title.lower() for r in results)


def test_scanner_uses_project_mode(tmp_path):
    proj = tmp_path / "proj"
    proj.mkdir()
    (proj / "README.md").write_text("## 已知问题\n- 问题 A\n")
    cfg = _make_config(proj, mode="confirm")
    scanner = Scanner(cfg)
    results = scanner.scan_project("test_proj", cfg.projects["test_proj"])
    assert all(r.mode == "confirm" for r in results)

Step 2: 运行确认失败

pytest tests/test_scanner.py -v

Expected: FAIL

Step 3: 实现 src/rockchip_agents/core/scanner.py

from __future__ import annotations

import logging
import re
from dataclasses import dataclass
from pathlib import Path

from rockchip_agents.config import AgentsConfig, ProjectConfig

logger = logging.getLogger(__name__)


@dataclass
class ScanResult:
    project: str
    type: str        # fix_bug | improve_perf | code_review
    title: str
    context: str
    priority: int
    mode: str
    agent_role: str = "developer"


class Scanner:
    def __init__(self, config: AgentsConfig) -> None:
        self._config = config

    def scan_all(self) -> list[ScanResult]:
        results: list[ScanResult] = []
        for name, proj_cfg in self._config.projects.items():
            if not proj_cfg.path.exists():
                logger.warning("项目路径不存在: %s", proj_cfg.path)
                continue
            results.extend(self.scan_project(name, proj_cfg))
        return results

    def scan_project(self, name: str, cfg: ProjectConfig) -> list[ScanResult]:
        results: list[ScanResult] = []
        results.extend(self._scan_readme(name, cfg))
        results.extend(self._scan_todos(name, cfg))
        results.extend(self._scan_claude_md(name, cfg))
        return results

    def _make(self, project: str, cfg: ProjectConfig, type_: str,
              title: str, context: str, priority: int) -> ScanResult:
        return ScanResult(
            project=project, type=type_, title=title,
            context=context, priority=priority, mode=cfg.mode,
        )

    def _scan_readme(self, project: str, cfg: ProjectConfig) -> list[ScanResult]:
        results = []
        readme = cfg.path / "README.md"
        if not readme.exists():
            return results
        text = readme.read_text(encoding="utf-8", errors="ignore")
        # 查找"已知问题"章节
        section = re.search(r"##\s*(已知问题|Known Issues?|known issues?)(.*?)(?=\n##|\Z)",
                            text, re.DOTALL | re.IGNORECASE)
        if not section:
            return results
        for line in section.group(2).splitlines():
            line = line.strip("- *\t ")
            if len(line) > 5:
                results.append(self._make(project, cfg, "fix_bug",
                                          f"[README] {line[:80]}", line, priority=2))
        return results

    def _scan_todos(self, project: str, cfg: ProjectConfig) -> list[ScanResult]:
        results = []
        src_dirs = [cfg.path / "src", cfg.path / "tools", cfg.path]
        pattern = re.compile(r"#\s*(TODO|FIXME|HACK)[:\s]+(.*)", re.IGNORECASE)
        seen: set[str] = set()
        for src_dir in src_dirs:
            if not src_dir.is_dir():
                continue
            for py_file in src_dir.rglob("*.py"):
                try:
                    for i, line in enumerate(py_file.read_text(errors="ignore").splitlines(), 1):
                        m = pattern.search(line)
                        if m:
                            key = m.group(2).strip()[:60]
                            if key not in seen:
                                seen.add(key)
                                rel = py_file.relative_to(cfg.path)
                                results.append(self._make(
                                    project, cfg, "code_review",
                                    f"TODO: {key}",
                                    f"{rel}:{i}{line.strip()}",
                                    priority=4,
                                ))
                except OSError:
                    pass
        return results

    def _scan_claude_md(self, project: str, cfg: ProjectConfig) -> list[ScanResult]:
        """从 CLAUDE.md 的关键技术点/关键约束中提取潜在优化任务。"""
        results = []
        cfile = cfg.path / "CLAUDE.md"
        if not cfile.exists():
            return results
        text = cfile.read_text(encoding="utf-8", errors="ignore")
        # 查找包含 ❌ 或 ⚠ 的行(表示有问题的项目)
        for line in text.splitlines():
            if ("❌" in line or "⚠" in line) and len(line.strip()) > 5:
                title = re.sub(r"[❌⚠️\|\s]+", " ", line).strip()[:80]
                if title:
                    results.append(self._make(project, cfg, "fix_bug",
                                              f"[CLAUDE.md] {title}", line, priority=2))
        return results

Step 4: 运行确认通过

pytest tests/test_scanner.py -v

Expected: 3 passed


Task 5: 飞书通知

Files:

  • Create: src/rockchip_agents/tools/feishu.py
  • Create: tests/test_feishu.py

Step 1: 写失败测试 tests/test_feishu.py

from __future__ import annotations

import pytest
import json
from unittest.mock import patch, MagicMock
from rockchip_agents.tools.feishu import FeishuNotifier


def test_send_text_formats_payload():
    notifier = FeishuNotifier(webhook_url="http://fake-webhook")
    with patch("rockchip_agents.tools.feishu.urllib.request.urlopen") as mock_open:
        mock_open.return_value.__enter__ = lambda s: s
        mock_open.return_value.__exit__ = MagicMock(return_value=False)
        mock_open.return_value.read.return_value = b'{"code":0}'
        notifier.send_text("任务完成", title="[yolo] 修复 yolo12n")
    call_args = mock_open.call_args
    request = call_args[0][0]
    payload = json.loads(request.data)
    assert payload["msg_type"] == "post"
    assert "yolo" in json.dumps(payload)


def test_send_text_no_crash_when_webhook_empty():
    notifier = FeishuNotifier(webhook_url="")
    # 空 webhook 静默跳过,不抛异常
    notifier.send_text("test")  # should not raise

Step 2: 运行确认失败

pytest tests/test_feishu.py -v

Expected: FAIL

Step 3: 实现 src/rockchip_agents/tools/feishu.py

from __future__ import annotations

import json
import logging
import urllib.request
from typing import Optional

logger = logging.getLogger(__name__)


class FeishuNotifier:
    def __init__(self, webhook_url: str, bot_token: str = "") -> None:
        self._webhook = webhook_url
        self._bot_token = bot_token

    def send_text(self, content: str, title: str = "Rockchip Agents") -> bool:
        """发送富文本通知到飞书群。"""
        if not self._webhook:
            logger.debug("飞书 Webhook 未配置,跳过通知")
            return False
        payload = {
            "msg_type": "post",
            "content": {
                "post": {
                    "zh_cn": {
                        "title": title,
                        "content": [[{"tag": "text", "text": content}]],
                    }
                }
            },
        }
        return self._post(payload)

    def send_task_result(self, project: str, task_title: str,
                         status: str, summary: str) -> bool:
        status_icon = {"done": "✅", "failed": "❌", "report": "📋"}.get(status, "️")
        title = f"{status_icon} [{project}] {task_title}"
        content = f"状态: {status}\n\n{summary}"
        return self.send_text(content, title=title)

    def send_confirm_card(self, task_id: int, project: str,
                          task_title: str, diff_summary: str,
                          callback_url: str) -> bool:
        """发送需要审批的交互卡片(confirm 模式)。"""
        if not self._webhook:
            return False
        payload = {
            "msg_type": "interactive",
            "card": {
                "header": {"title": {"tag": "plain_text", "content": f"[{project}] {task_title}"}},
                "elements": [
                    {"tag": "div", "text": {"tag": "lark_md", "content": diff_summary}},
                    {
                        "tag": "action",
                        "actions": [
                            {"tag": "button", "text": {"tag": "plain_text", "content": "批准执行"},
                             "type": "primary",
                             "url": f"{callback_url}/approve/{task_id}"},
                            {"tag": "button", "text": {"tag": "plain_text", "content": "拒绝"},
                             "type": "danger",
                             "url": f"{callback_url}/reject/{task_id}"},
                        ],
                    },
                ],
            },
        }
        return self._post(payload)

    def _post(self, payload: dict) -> bool:
        data = json.dumps(payload).encode()
        req = urllib.request.Request(
            self._webhook, data=data,
            headers={"Content-Type": "application/json"},
        )
        try:
            with urllib.request.urlopen(req, timeout=10) as resp:
                result = json.loads(resp.read())
                if result.get("code", 0) != 0:
                    logger.warning("飞书返回错误: %s", result)
                    return False
                return True
        except Exception as e:
            logger.error("飞书通知失败: %s", e)
            return False

Step 4: 运行确认通过

pytest tests/test_feishu.py -v

Expected: 2 passed


Task 6: Developer AgentClaude API 集成)

Files:

  • Create: src/rockchip_agents/agents/developer.py
  • Create: src/rockchip_agents/tools/file_tools.py
  • Create: tests/test_developer.py

Step 1: 实现文件操作工具 src/rockchip_agents/tools/file_tools.py

from __future__ import annotations

import subprocess
from pathlib import Path
from typing import Optional


def read_file(path: str) -> str:
    p = Path(path)
    if not p.exists():
        return f"[ERROR] 文件不存在: {path}"
    try:
        return p.read_text(encoding="utf-8", errors="replace")
    except OSError as e:
        return f"[ERROR] 读取失败: {e}"


def write_file(path: str, content: str) -> str:
    p = Path(path)
    p.parent.mkdir(parents=True, exist_ok=True)
    p.write_text(content, encoding="utf-8")
    return f"[OK] 已写入: {path} ({len(content)} bytes)"


def list_files(directory: str, pattern: str = "**/*.py") -> str:
    d = Path(directory)
    if not d.is_dir():
        return f"[ERROR] 目录不存在: {directory}"
    files = sorted(d.glob(pattern))
    return "\n".join(str(f.relative_to(d)) for f in files[:50])


def run_command(cmd: str, cwd: Optional[str] = None,
                timeout: int = 60) -> str:
    """在指定目录运行 shell 命令,返回 stdout+stderr。"""
    try:
        result = subprocess.run(
            cmd, shell=True, capture_output=True, text=True,
            cwd=cwd, timeout=timeout,
        )
        output = result.stdout + result.stderr
        return f"[returncode={result.returncode}]\n{output[:4000]}"
    except subprocess.TimeoutExpired:
        return f"[ERROR] 命令超时({timeout}s: {cmd}"
    except Exception as e:
        return f"[ERROR] 命令执行失败: {e}"

Step 2: 写失败测试 tests/test_developer.py

from __future__ import annotations

import pytest
from pathlib import Path
from unittest.mock import patch, MagicMock
from rockchip_agents.agents.developer import DeveloperAgent
from rockchip_agents.config import AgentsConfig, ProjectConfig, SchedulerConfig, ClaudeConfig, FeishuConfig
from rockchip_agents.core.queue import Task


def _cfg(tmp_path: Path) -> AgentsConfig:
    return AgentsConfig(
        projects={"yolo": ProjectConfig(path=tmp_path / "yolo", mode="auto")},
        scheduler=SchedulerConfig(),
        claude=ClaudeConfig(api_key="fake-key"),
        feishu=FeishuConfig(),
        devices={},
    )


def test_developer_agent_report_mode_no_file_write(tmp_path):
    """report 模式下 Developer Agent 不应写文件。"""
    task = Task(project="yolo", type="fix_bug", title="分析零检出",
                priority=1, mode="report", agent_role="developer", id=1)
    cfg = _cfg(tmp_path)
    agent = DeveloperAgent(cfg)

    mock_response = MagicMock()
    mock_response.content = [MagicMock(type="text", text="分析完成:根本原因是 sigmoid 缺失")]
    mock_response.stop_reason = "end_turn"

    with patch("rockchip_agents.agents.developer.anthropic.Anthropic") as MockClient:
        mock_client = MockClient.return_value
        mock_client.messages.create.return_value = mock_response
        result = agent.run(task)

    assert result.status == "done"
    assert "分析" in result.summary or len(result.summary) > 0


def test_developer_agent_returns_summary(tmp_path):
    """auto 模式下应返回执行摘要。"""
    (tmp_path / "yolo").mkdir()
    task = Task(project="yolo", type="code_review", title="检查代码质量",
                priority=3, mode="auto", agent_role="developer", id=2)
    cfg = _cfg(tmp_path)
    agent = DeveloperAgent(cfg)

    mock_msg = MagicMock()
    mock_msg.type = "text"
    mock_msg.text = "代码审查完成,发现 2 处改进点"
    mock_response = MagicMock()
    mock_response.content = [mock_msg]
    mock_response.stop_reason = "end_turn"

    with patch("rockchip_agents.agents.developer.anthropic.Anthropic") as MockClient:
        mock_client = MockClient.return_value
        mock_client.messages.create.return_value = mock_response
        result = agent.run(task)

    assert result.summary != ""

Step 3: 运行确认失败

pytest tests/test_developer.py -v

Expected: FAIL

Step 4: 实现 src/rockchip_agents/agents/developer.py

from __future__ import annotations

import logging
from dataclasses import dataclass
from typing import Any

import anthropic

from rockchip_agents.config import AgentsConfig
from rockchip_agents.core.queue import Task
from rockchip_agents.tools.file_tools import read_file, write_file, list_files, run_command

logger = logging.getLogger(__name__)

TOOLS = [
    {
        "name": "read_file",
        "description": "读取项目文件内容",
        "input_schema": {"type": "object", "properties": {
            "path": {"type": "string", "description": "文件绝对路径"}
        }, "required": ["path"]},
    },
    {
        "name": "write_file",
        "description": "写入/修改文件(仅 auto 模式可用)",
        "input_schema": {"type": "object", "properties": {
            "path": {"type": "string"}, "content": {"type": "string"}
        }, "required": ["path", "content"]},
    },
    {
        "name": "list_files",
        "description": "列出目录下的 Python 文件",
        "input_schema": {"type": "object", "properties": {
            "directory": {"type": "string"}, "pattern": {"type": "string"}
        }, "required": ["directory"]},
    },
    {
        "name": "run_command",
        "description": "在项目目录执行 shell 命令(如 pytest",
        "input_schema": {"type": "object", "properties": {
            "cmd": {"type": "string"}, "cwd": {"type": "string"}
        }, "required": ["cmd"]},
    },
]


@dataclass
class AgentResult:
    status: str   # done | failed
    summary: str


class DeveloperAgent:
    MAX_TURNS = 10

    def __init__(self, config: AgentsConfig) -> None:
        self._config = config
        self._client = anthropic.Anthropic(api_key=config.claude.api_key)

    def run(self, task: Task) -> AgentResult:
        proj_cfg = self._config.projects.get(task.project)
        proj_path = str(proj_cfg.path) if proj_cfg else ""
        mode = task.mode
        system = self._build_system_prompt(task, mode, proj_path)
        messages: list[dict[str, Any]] = [
            {"role": "user", "content": self._build_user_message(task)}
        ]
        for _ in range(self.MAX_TURNS):
            response = self._client.messages.create(
                model=self._config.claude.model,
                max_tokens=4096,
                system=system,
                tools=TOOLS if mode != "report" else [],
                messages=messages,
            )
            # 收集文本回复
            text_parts = [b.text for b in response.content if b.type == "text"]
            if response.stop_reason == "end_turn":
                return AgentResult(status="done", summary="\n".join(text_parts))
            # 处理 tool_use
            if response.stop_reason == "tool_use":
                messages.append({"role": "assistant", "content": response.content})
                tool_results = []
                for block in response.content:
                    if block.type != "tool_use":
                        continue
                    result = self._dispatch_tool(block.name, block.input, mode, proj_path)
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": result,
                    })
                messages.append({"role": "user", "content": tool_results})
                continue
            break
        return AgentResult(status="done", summary="(达到最大轮次)")

    def _dispatch_tool(self, name: str, inputs: dict,
                       mode: str, proj_path: str) -> str:
        if name == "read_file":
            return read_file(inputs["path"])
        if name == "list_files":
            return list_files(inputs["directory"], inputs.get("pattern", "**/*.py"))
        if name == "run_command":
            cwd = inputs.get("cwd") or proj_path
            return run_command(inputs["cmd"], cwd=cwd)
        if name == "write_file":
            if mode == "report":
                return "[BLOCKED] report 模式不允许写文件"
            return write_file(inputs["path"], inputs["content"])
        return f"[ERROR] 未知工具: {name}"

    def _build_system_prompt(self, task: Task, mode: str, proj_path: str) -> str:
        mode_desc = {
            "auto": "你可以直接读取和修改文件,运行测试。修复完成后报告结果。",
            "confirm": "分析问题并生成修改方案(diff 摘要),但不要实际写文件。",
            "report": "只分析问题,生成详细报告,不修改任何文件。",
        }[mode]
        return (
            f"你是 Rockchip 项目的 Developer Agent,专注于 RKNN NPU 推理优化。\n"
            f"项目路径: {proj_path}\n"
            f"当前模式: {mode}{mode_desc}\n"
            f"请用中文回复。保持简洁,聚焦问题。"
        )

    def _build_user_message(self, task: Task) -> str:
        return (
            f"任务类型: {task.type}\n"
            f"任务标题: {task.title}\n"
            f"上下文:\n{task.context}\n\n"
            f"请分析并处理这个任务。"
        )

Step 5: 运行确认通过

pytest tests/test_developer.py -v

Expected: 2 passed


Task 7: Executor(并发调度)

Files:

  • Create: src/rockchip_agents/core/executor.py
  • Create: tests/test_executor.py

Step 1: 写失败测试 tests/test_executor.py

from __future__ import annotations

import asyncio
import pytest
from pathlib import Path
from unittest.mock import MagicMock, patch
from rockchip_agents.core.executor import Executor
from rockchip_agents.core.queue import TaskQueue, Task
from rockchip_agents.config import AgentsConfig, ProjectConfig, SchedulerConfig, ClaudeConfig, FeishuConfig


def _cfg(tmp_path: Path) -> AgentsConfig:
    return AgentsConfig(
        projects={"yolo": ProjectConfig(path=tmp_path / "yolo", mode="report")},
        scheduler=SchedulerConfig(max_concurrent=2),
        claude=ClaudeConfig(api_key="fake"),
        feishu=FeishuConfig(),
        devices={},
    )


@pytest.mark.asyncio
async def test_run_next_marks_task_done(tmp_path):
    cfg = _cfg(tmp_path)
    q = TaskQueue(db_path=tmp_path / "tasks.db")
    tid = q.enqueue(Task(project="yolo", type="fix_bug", title="测试",
                         priority=1, mode="report", agent_role="developer"))
    executor = Executor(cfg, queue=q)
    with patch("rockchip_agents.core.executor.DeveloperAgent") as MockAgent:
        mock_agent = MockAgent.return_value
        mock_agent.run.return_value = MagicMock(status="done", summary="测试完成")
        await executor.run_next()
    # 任务应标记为 done
    with q._conn() as c:
        row = c.execute("SELECT status FROM tasks WHERE id=?", (tid,)).fetchone()
    assert row["status"] == "done"

Step 2: 运行确认失败

pytest tests/test_executor.py -v

Expected: FAIL

Step 3: 实现 src/rockchip_agents/core/executor.py

from __future__ import annotations

import asyncio
import logging
from typing import Optional

from rockchip_agents.agents.developer import DeveloperAgent
from rockchip_agents.config import AgentsConfig
from rockchip_agents.core.queue import TaskQueue, Task
from rockchip_agents.tools.feishu import FeishuNotifier

logger = logging.getLogger(__name__)


class Executor:
    def __init__(self, config: AgentsConfig,
                 queue: Optional[TaskQueue] = None) -> None:
        self._config = config
        self._queue = queue or TaskQueue()
        self._feishu = FeishuNotifier(
            webhook_url=config.feishu.webhook_url,
            bot_token=config.feishu.bot_token,
        )
        self._semaphore = asyncio.Semaphore(config.scheduler.max_concurrent)

    async def run_next(self) -> bool:
        """取出并执行下一个任务,返回是否有任务执行。"""
        task = self._queue.dequeue()
        if not task:
            logger.debug("任务队列为空")
            return False
        async with self._semaphore:
            await self._run_task(task)
        return True

    async def run_all_pending(self) -> int:
        """并发执行所有 pending 任务,返回执行数量。"""
        tasks_done = 0
        coros = []
        while True:
            task = self._queue.dequeue()
            if not task:
                break
            coros.append(self._run_task(task))
            tasks_done += 1
        if coros:
            await asyncio.gather(*coros, return_exceptions=True)
        return tasks_done

    async def _run_task(self, task: Task) -> None:
        logger.info("开始执行任务 [%s] %s", task.project, task.title)
        try:
            agent = DeveloperAgent(self._config)
            result = await asyncio.get_event_loop().run_in_executor(
                None, agent.run, task
            )
            self._queue.mark_done(task.id, result.summary)
            self._feishu.send_task_result(
                project=task.project, task_title=task.title,
                status=result.status, summary=result.summary,
            )
            logger.info("任务完成 [%s] %s", task.project, task.title)
        except Exception as e:
            logger.error("任务失败 [%s] %s: %s", task.project, task.title, e)
            self._queue.mark_failed(task.id, str(e))

Step 4: 运行确认通过

pytest tests/test_executor.py -v

Expected: 1 passed


Task 8: Scheduler(定时入口)

Files:

  • Create: src/rockchip_agents/core/scheduler.py

Step 1: 实现 src/rockchip_agents/core/scheduler.py

from __future__ import annotations

import asyncio
import logging

from apscheduler.schedulers.blocking import BlockingScheduler

from rockchip_agents.config import load_config
from rockchip_agents.core.executor import Executor
from rockchip_agents.core.queue import TaskQueue
from rockchip_agents.core.scanner import Scanner

logger = logging.getLogger(__name__)


def _scan_and_enqueue() -> None:
    cfg = load_config()
    q = TaskQueue()
    scanner = Scanner(cfg)
    results = scanner.scan_all()
    for r in results:
        from rockchip_agents.core.queue import Task
        tid = q.enqueue(Task(
            project=r.project, type=r.type, title=r.title,
            context=r.context, priority=r.priority,
            mode=r.mode, agent_role=r.agent_role,
        ))
        if tid > 0:
            logger.info("新任务入队 [%s] %s (priority=%d)", r.project, r.title, r.priority)


def _run_pending() -> None:
    cfg = load_config()
    executor = Executor(cfg)
    count = asyncio.run(executor.run_all_pending())
    logger.info("本轮执行任务数: %d", count)


def start() -> None:
    cfg = load_config()
    interval = cfg.scheduler.interval_hours
    scheduler = BlockingScheduler()
    scheduler.add_job(_scan_and_enqueue, "interval", hours=interval, id="scan")
    scheduler.add_job(_run_pending, "interval", hours=interval,
                      minutes=5, id="run")  # 扫描后 5 分钟开始执行
    logger.info("Scheduler 已启动,扫描间隔: %d 小时", interval)
    # 启动时立即扫描一次
    _scan_and_enqueue()
    scheduler.start()

Step 2: 验证启动入口

source venv/bin/activate
python scripts/run.py scan

Expected: 列出扫描到的任务(或提示项目路径不存在)


Task 9: Phase 1 集成测试

Files:

  • Create: tests/test_integration_p1.py

Step 1: 写集成测试

from __future__ import annotations

import pytest
from pathlib import Path
from unittest.mock import patch, MagicMock
from rockchip_agents.config import load_config, AgentsConfig, ProjectConfig, SchedulerConfig, ClaudeConfig, FeishuConfig
from rockchip_agents.core.scanner import Scanner
from rockchip_agents.core.queue import TaskQueue, Task
from rockchip_agents.core.executor import Executor
import asyncio


def test_full_scan_to_queue_to_execute(tmp_path):
    """完整流程:扫描 → 入队 → 执行 → 状态 done"""
    # 准备 mock 项目
    proj = tmp_path / "yolo"
    proj.mkdir()
    (proj / "README.md").write_text("## 已知问题\n- yolo12n 零检出\n")

    cfg = AgentsConfig(
        projects={"yolo": ProjectConfig(path=proj, mode="report")},
        scheduler=SchedulerConfig(max_concurrent=1),
        claude=ClaudeConfig(api_key="fake"),
        feishu=FeishuConfig(),
        devices={},
    )
    q = TaskQueue(db_path=tmp_path / "tasks.db")
    scanner = Scanner(cfg)
    results = scanner.scan_all()
    assert len(results) >= 1
    for r in results:
        q.enqueue(Task(project=r.project, type=r.type, title=r.title,
                       context=r.context, priority=r.priority,
                       mode=r.mode, agent_role=r.agent_role))

    mock_result = MagicMock(status="done", summary="分析完成")
    with patch("rockchip_agents.core.executor.DeveloperAgent") as MockAgent:
        MockAgent.return_value.run.return_value = mock_result
        asyncio.run(Executor(cfg, queue=q).run_all_pending())

    with q._conn() as c:
        done = c.execute("SELECT COUNT(*) FROM tasks WHERE status='done'").fetchone()[0]
    assert done >= 1

Step 2: 运行所有测试

pytest tests/ -v --tb=short

Expected: 全部通过


Phase 2Device Agent + Tester Agent + 三模式完整

Task 10: Device AgentSSH 设备管理)

Files:

  • Create: src/rockchip_agents/tools/device_agent.py
  • Create: tests/test_device_agent.py

Step 1: 写失败测试 tests/test_device_agent.py

from __future__ import annotations

import pytest
from unittest.mock import patch, MagicMock
from rockchip_agents.tools.device_agent import DeviceAgent
from rockchip_agents.config import DeviceConfig


def test_build_command_prepends_pre_cmd():
    dc = DeviceConfig(host="192.168.0.127", user="pi", password="123123",
                      workspace="/home/pi/Desktop",
                      pre_cmd="clashon && clashproxy on")
    agent = DeviceAgent(dc)
    cmd = agent._build_cmd("pytest tests/ -v", "/home/pi/Desktop/yolo")
    assert "clashon" in cmd
    assert "pytest" in cmd
    assert "/home/pi/Desktop/yolo" in cmd


def test_sync_builds_rsync_command(tmp_path):
    dc = DeviceConfig(host="192.168.0.127", user="pi", password="123123",
                      workspace="/home/pi/Desktop", pre_cmd="")
    agent = DeviceAgent(dc)
    cmd = agent._build_sync_cmd(str(tmp_path / "yolo"), "/home/pi/Desktop/yolo")
    assert "rsync" in cmd
    assert "192.168.0.127" in cmd


def test_run_returns_output():
    dc = DeviceConfig(host="192.168.0.127", user="pi", password="123123",
                      workspace="/home/pi/Desktop", pre_cmd="")
    agent = DeviceAgent(dc)
    with patch("rockchip_agents.tools.device_agent.paramiko.SSHClient") as MockSSH:
        mock_client = MockSSH.return_value.__enter__.return_value
        mock_stdout = MagicMock()
        mock_stdout.read.return_value = b"3 passed"
        mock_stdout.channel.recv_exit_status.return_value = 0
        mock_stderr = MagicMock()
        mock_stderr.read.return_value = b""
        mock_client.exec_command.return_value = (MagicMock(), mock_stdout, mock_stderr)
        result = agent.run_command("pytest tests/", "/home/pi/Desktop/yolo")
    assert "3 passed" in result

Step 2: 运行确认失败

pytest tests/test_device_agent.py -v

Step 3: 实现 src/rockchip_agents/tools/device_agent.py

from __future__ import annotations

import logging
import subprocess
from contextlib import contextmanager

import paramiko

from rockchip_agents.config import DeviceConfig

logger = logging.getLogger(__name__)


class DeviceAgent:
    def __init__(self, device: DeviceConfig) -> None:
        self._dev = device

    @contextmanager
    def _ssh(self):
        client = paramiko.SSHClient()
        client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        client.connect(self._dev.host, username=self._dev.user,
                       password=self._dev.password, timeout=30)
        try:
            yield client
        finally:
            client.close()

    def run_command(self, cmd: str, remote_cwd: str, timeout: int = 120) -> str:
        full_cmd = self._build_cmd(cmd, remote_cwd)
        try:
            with self._ssh() as client:
                _, stdout, stderr = client.exec_command(full_cmd, timeout=timeout)
                out = stdout.read().decode(errors="replace")
                err = stderr.read().decode(errors="replace")
                rc = stdout.channel.recv_exit_status()
                return f"[rc={rc}]\n{out}\n{err}".strip()
        except Exception as e:
            logger.error("SSH 命令失败: %s", e)
            return f"[ERROR] {e}"

    def sync(self, local_path: str, remote_workspace: str,
             excludes: list[str] | None = None) -> str:
        """用 rsync 将本地项目同步到设备。"""
        cmd = self._build_sync_cmd(local_path, remote_workspace, excludes)
        try:
            result = subprocess.run(cmd, shell=True, capture_output=True,
                                    text=True, timeout=120)
            return f"[rc={result.returncode}]\n{result.stdout}\n{result.stderr}".strip()
        except Exception as e:
            return f"[ERROR] rsync 失败: {e}"

    def _build_cmd(self, cmd: str, remote_cwd: str) -> str:
        pre = self._dev.pre_cmd
        parts = []
        if pre:
            parts.append(pre)
        parts.append(f"cd {remote_cwd}")
        parts.append(cmd)
        return " && ".join(parts)

    def _build_sync_cmd(self, local_path: str, remote_path: str,
                         excludes: list[str] | None = None) -> str:
        exc = " ".join(f"--exclude={e}" for e in (excludes or ["venv", "__pycache__", "*.pyc"]))
        return (
            f"rsync -avz --delete {exc} "
            f"{local_path}/ "
            f"{self._dev.user}@{self._dev.host}:{remote_path}/"
        )

Step 4: 运行确认通过

pytest tests/test_device_agent.py -v

Expected: 3 passed


Task 11: Tester Agent

Files:

  • Create: src/rockchip_agents/agents/tester.py

Step 1: 实现 src/rockchip_agents/agents/tester.py

from __future__ import annotations

import logging
from dataclasses import dataclass

from rockchip_agents.agents.developer import AgentResult
from rockchip_agents.config import AgentsConfig
from rockchip_agents.core.queue import Task
from rockchip_agents.tools.device_agent import DeviceAgent
from rockchip_agents.tools.file_tools import run_command

logger = logging.getLogger(__name__)


class TesterAgent:
    def __init__(self, config: AgentsConfig) -> None:
        self._config = config

    def run(self, task: Task) -> AgentResult:
        proj_cfg = self._config.projects.get(task.project)
        if not proj_cfg:
            return AgentResult(status="failed", summary=f"项目不存在: {task.project}")

        lines = []
        # 1. 本地测试
        local_result = run_command(
            "source venv/bin/activate && pytest tests/ -v --tb=short -q",
            cwd=str(proj_cfg.path),
        )
        lines.append(f"=== 本地测试 ===\n{local_result}")

        # 2. 设备端测试(如有配置)
        if proj_cfg.device_workspace and self._config.devices:
            dev_name = next(iter(self._config.devices))
            dev_cfg = self._config.devices[dev_name]
            device = DeviceAgent(dev_cfg)
            # 先同步
            sync_result = device.sync(str(proj_cfg.path), proj_cfg.device_workspace)
            lines.append(f"=== 设备同步 ===\n{sync_result}")
            # 然后运行板端测试
            board_result = device.run_command(
                "source venv/bin/activate && pytest tests/ -v --tb=short -q",
                proj_cfg.device_workspace,
            )
            lines.append(f"=== 板端测试 ({dev_name}) ===\n{board_result}")

        summary = "\n\n".join(lines)
        failed = "FAILED" in summary or "ERROR" in summary
        return AgentResult(
            status="failed" if failed else "done",
            summary=summary,
        )

Phase 3Productizer + Architect Agent(概要)

Task 12: Productizer Agent

Files:

  • Create: src/rockchip_agents/agents/productizer.py

评估项目成熟度:检查 README 完整性、API 文档、RELEASE.md、测试覆盖率。输出成熟度报告和待办清单。通过 Claude API 生成建议。


Task 13: Architect Agent

Files:

  • Create: src/rockchip_agents/agents/architect.py

跨项目分析:读取所有项目的 CLAUDE.md / README,识别能力重叠和整合机会。定期(每周)生成架构报告,提交预研任务到队列。

关键提示词方向:

  • "分析这些项目的功能,哪些可以整合成一个产品?"
  • "yolo + embedding 组合能做什么?"
  • "给出下一步预研建议"

Phase 4Manager Agent 全协调(概要)

Task 14: Manager Agent

Files:

  • Create: src/rockchip_agents/agents/manager.py

Manager 作为 Orchestrator

  • 根据任务 agent_role 字段分发到对应 Agent
  • 处理 confirm 模式的飞书审批回调
  • 监控各 Agent 状态,协调优先级
  • 更新 executor.pyExecutor._run_task 通过 Manager 路由

全量测试

source venv/bin/activate
pytest tests/ -v --tb=short

Expected: 全部 green

验证系统运行

# 手动触发一次扫描(不启动定时器)
source venv/bin/activate
python scripts/run.py scan

# 执行下一个任务
ANTHROPIC_API_KEY=sk-... python scripts/run.py run-next

# 启动定时调度
ANTHROPIC_API_KEY=sk-... python scripts/run.py start