包含从项目创建至今的全部代码首次入库: 核心框架: - 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>
52 KiB
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 Agent;Phase 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 Agent(Claude 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 2:Device Agent + Tester Agent + 三模式完整
Task 10: Device Agent(SSH 设备管理)
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 3:Productizer + 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 4:Manager Agent 全协调(概要)
Task 14: Manager Agent
Files:
- Create:
src/rockchip_agents/agents/manager.py
Manager 作为 Orchestrator:
- 根据任务
agent_role字段分发到对应 Agent - 处理
confirm模式的飞书审批回调 - 监控各 Agent 状态,协调优先级
- 更新
executor.py让Executor._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