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>
This commit is contained in:
+28
@@ -0,0 +1,28 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.egg-info/
|
||||
venv/
|
||||
.venv/
|
||||
|
||||
# Runtime data
|
||||
data/
|
||||
logs/
|
||||
tmp/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
|
||||
# Dashboard build output
|
||||
dashboard/dist/
|
||||
dashboard/node_modules/
|
||||
|
||||
# Playwright
|
||||
.playwright-mcp/
|
||||
|
||||
# ArchVersion snapshots
|
||||
.arch_versions/
|
||||
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## 项目概述
|
||||
|
||||
Rockchip Agents:基于 Claude CLI 的 AI 项目自动化维护与调研系统。支持代码修复、板端测试、成熟度评估、架构整合、市场洞察与基础技术调研,运行在 RK3588 NPU 开发平台上。
|
||||
NMFS Agents:基于 Claude CLI 的 AI 项目自动化维护与调研系统。支持代码修复、板端测试、成熟度评估、架构整合、市场洞察与基础技术调研,运行在 RK3588 NPU 开发平台上。
|
||||
|
||||
## 目录约定
|
||||
|
||||
@@ -70,7 +70,7 @@ sqlite3 data/tasks.db "SELECT id,project,agent_role,title,status FROM tasks ORDE
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
src/rockchip_agents/
|
||||
src/nmfs_agents/
|
||||
├── config.py # 配置加载(AgentsConfig dataclass)
|
||||
├── agents/
|
||||
│ ├── developer.py # Developer Agent(claude --print 子进程,三模式)
|
||||
|
||||
+86
-5
@@ -1,4 +1,4 @@
|
||||
# Rockchip Agents 开发指南
|
||||
# NMFS Agents 开发指南
|
||||
|
||||
## 环境搭建
|
||||
|
||||
@@ -73,6 +73,35 @@ Scanner.scan_all() # 扫描 README/TODO/CLAUDE.md
|
||||
└─> FeishuNotifier.send_task_result()
|
||||
```
|
||||
|
||||
## Web Dashboard
|
||||
|
||||
Dashboard 是首选操作入口,访问地址:`http://localhost:9080`(Docker 环境同端口)。
|
||||
|
||||
### 主要功能
|
||||
|
||||
| 标签页 | 功能 |
|
||||
|--------|------|
|
||||
| 看板(Kanban) | 按项目查看任务状态(待处理/运行中/完成/失败),可拖拽调整优先级 |
|
||||
| 洞察(Insight) | 市场目标卡片(运动/适老/AI安全)+ 基础技术调研展示 + 历史记录 |
|
||||
| 配置(Config) | 查看/编辑全局和项目级 CLAUDE.md / README.md,触发 CLAUDE.md 刷新 |
|
||||
|
||||
### 常见操作
|
||||
|
||||
**提交开发需求**(点击右上角「提交需求」):
|
||||
- 选择项目 → 选择类型(修复问题/新功能/代码审查等)→ 填写描述 → 提交
|
||||
- 提交后自动路由到 `project_delivery` 队列
|
||||
|
||||
**发起技术攻关**:
|
||||
- 需求类型选「技术攻关」→ 选择角色(RTP/网络栈/内核调度/OS/硬件)→ 填写描述 → 提交
|
||||
- 自动路由到 `base_opt` 队列;调研结论写入 `__research__.*`,洞察页自动展示
|
||||
|
||||
**触发市场洞察**(洞察页):
|
||||
- 点击目标卡片的「触发洞察」按钮 → insight 队列入队 → market-pm 执行
|
||||
- 调研结论写入 `__market__.{goal}.*`,目标卡片下次刷新时自动更新
|
||||
|
||||
**刷新项目指导**(配置页,选中项目后可见):
|
||||
- 点击「🔄 刷新项目指导」→ 创建 `architect` 任务 → Agent 综合调研结论自动重写 CLAUDE.md
|
||||
|
||||
## 常用命令
|
||||
|
||||
```bash
|
||||
@@ -91,6 +120,9 @@ python scripts/run.py scan
|
||||
# 执行队列中下一个任务(须在普通终端执行)
|
||||
python scripts/run.py run-next
|
||||
|
||||
# 入队三专项调研任务(须在普通终端执行)
|
||||
python scripts/run.py research
|
||||
|
||||
# 全量测试
|
||||
pytest tests/ -q
|
||||
```
|
||||
@@ -115,9 +147,9 @@ sqlite3 data/insight_tasks.db "SELECT id,project,agent_role,title,status FROM ta
|
||||
```python
|
||||
source venv/bin/activate
|
||||
python - <<'EOF'
|
||||
from rockchip_agents.config import load_config
|
||||
from rockchip_agents.core.queue import Task, TaskQueue
|
||||
import asyncio, rockchip_agents.core.executor as ex
|
||||
from nmfs_agents.config import load_config
|
||||
from nmfs_agents.core.queue import Task, TaskQueue
|
||||
import asyncio, nmfs_agents.core.executor as ex
|
||||
|
||||
cfg = load_config()
|
||||
q = TaskQueue()
|
||||
@@ -174,7 +206,7 @@ projects:
|
||||
|
||||
## 添加新 Agent
|
||||
|
||||
1. 在 `src/rockchip_agents/agents/` 新建 `my_agent.py`,继承 `DeveloperAgent` 或独立实现 `run(task) -> AgentResult`
|
||||
1. 在 `src/nmfs_agents/agents/` 新建 `my_agent.py`,继承 `DeveloperAgent` 或独立实现 `run(task) -> AgentResult`
|
||||
2. 在 `executor.py` 的 `_make_agent()` 添加路由条目
|
||||
3. 在 `configs/agents.yaml` 的对应队列 `roles:` 中添加新角色名
|
||||
4. 在 `tests/` 添加测试文件
|
||||
@@ -242,6 +274,45 @@ devices:
|
||||
| `DB_PATH` | 任务数据库路径(默认 `data/tasks.db`) | 否 |
|
||||
| `DASHBOARD_PORT` | Dashboard 端口(默认 8080) | 否 |
|
||||
|
||||
## API 端点参考
|
||||
|
||||
Dashboard 后端 FastAPI,主要端点:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/api/tasks` | 获取任务列表(支持 `?project=` 过滤) |
|
||||
| POST | `/api/tasks` | 创建任务(`queue` 字段路由到不同数据库) |
|
||||
| PATCH | `/api/tasks/{id}` | 更新优先级 |
|
||||
| POST | `/api/tasks/{id}/retry` | 重试失败任务 |
|
||||
| POST | `/api/tasks/{id}/cancel` | 取消任务 |
|
||||
| POST | `/api/tasks/{id}/approve` | 审批 waiting_approval 任务 |
|
||||
| POST | `/api/tasks/{id}/reject` | 拒绝任务(附原因) |
|
||||
| GET | `/api/projects` | 获取项目列表 |
|
||||
| POST | `/api/projects/import` | 导入新项目 |
|
||||
| DELETE | `/api/projects/{name}` | 删除项目 |
|
||||
| GET | `/api/projects/{name}/config` | 获取项目 CLAUDE.md / README.md |
|
||||
| PUT | `/api/projects/{name}/config/{type}` | 保存配置文件(`claude`/`readme`) |
|
||||
| POST | `/api/projects/{name}/refresh-claude-md` | 触发 architect 更新 CLAUDE.md |
|
||||
| GET | `/api/config/global` | 获取全局 CLAUDE.md |
|
||||
| PUT | `/api/config/global` | 保存全局 CLAUDE.md |
|
||||
| GET | `/api/market/facts` | 获取市场洞察 facts(`__market__.*`) |
|
||||
| POST | `/api/market/trigger/{goal}` | 触发市场洞察任务 |
|
||||
| GET | `/api/market/history` | 获取洞察任务历史 |
|
||||
| GET | `/api/research/facts` | 获取三专项调研 facts(`__research__.*`) |
|
||||
| GET | `/api/executor/status` | 执行器状态 |
|
||||
| POST | `/api/executor/start` | 启动执行器 |
|
||||
| POST | `/api/executor/stop` | 停止执行器 |
|
||||
|
||||
### 队列路由规则
|
||||
|
||||
`POST /api/tasks` 的 `queue` 字段:
|
||||
|
||||
| queue 值 | 路由到 | 典型 agent_role |
|
||||
|----------|--------|-----------------|
|
||||
| `project_delivery`(默认)| `data/tasks.db` | developer / architect |
|
||||
| `base_opt` | `data/base_opt_tasks.db` | rtp-researcher / kernel-analyzer / hw-engineer |
|
||||
| `insight` | `data/insight_tasks.db` | market-pm / planner / productizer |
|
||||
|
||||
## 项目记忆系统
|
||||
|
||||
`data/project_memory.db` 分三层:
|
||||
@@ -252,6 +323,16 @@ devices:
|
||||
| facts(事实) | 任意 agent;architect 通过 `[记忆]` 标签写入 |
|
||||
| history(历史) | executor 每次任务完成/失败后自动写入 |
|
||||
|
||||
### 特殊 facts 命名约定
|
||||
|
||||
| key 前缀 | 写入者 | 用途 | Dashboard 展示位置 |
|
||||
|----------|--------|------|-------------------|
|
||||
| `__research__.rtp.*` | rtp-researcher | RTP/RTCP 调研结论 | 洞察页"基础技术调研" |
|
||||
| `__research__.net.*` | net-researcher | 网络栈调优结论 | 洞察页"基础技术调研" |
|
||||
| `__research__.kernel.*` | kernel-analyzer | 内核调度分析结论 | 洞察页"基础技术调研" |
|
||||
| `__market__.{goal}.*` | market-pm | 市场洞察结论(goal=运动/适老/AI安全) | 洞察页目标卡片 |
|
||||
| `__env__.*` | env-collector | 板端硬件/软件环境信息 | Agent 系统提示 |
|
||||
|
||||
```bash
|
||||
python scripts/run.py goals # 查看项目长期目标
|
||||
python scripts/run.py facts # 查看项目知识事实
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# ── Stage 1: 构建 React 前端 ─────────────────────────────────────────
|
||||
FROM node:20-alpine AS frontend-builder
|
||||
WORKDIR /frontend
|
||||
|
||||
COPY dashboard/package*.json ./
|
||||
RUN npm ci --prefer-offline
|
||||
|
||||
COPY dashboard/ ./
|
||||
RUN npm run build
|
||||
|
||||
# ── Stage 2: Python FastAPI 运行时 ───────────────────────────────────
|
||||
FROM python:3.10-slim AS runtime
|
||||
WORKDIR /app
|
||||
|
||||
# 安装系统工具(rsync/sshpass 用于板端同步测试,bash 用于 source 命令)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
sshpass rsync openssh-client \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 安装 Python 依赖
|
||||
COPY requirements-dashboard.txt ./
|
||||
RUN pip install --no-cache-dir -r requirements-dashboard.txt
|
||||
|
||||
# 复制源码并安装包
|
||||
COPY src/ ./src/
|
||||
COPY pyproject.toml ./
|
||||
RUN pip install --no-cache-dir -e . --no-deps
|
||||
COPY scripts/dashboard.py ./scripts/
|
||||
|
||||
# 复制前端构建产物
|
||||
COPY --from=frontend-builder /frontend/dist ./dashboard/dist/
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENV PYTHONPATH=/app/src
|
||||
|
||||
CMD ["python", "scripts/dashboard.py"]
|
||||
@@ -0,0 +1,28 @@
|
||||
.PHONY: install test test-one clean scan run status start
|
||||
|
||||
install:
|
||||
python3.10 -m venv venv
|
||||
venv/bin/pip install -e ".[dev]"
|
||||
|
||||
test:
|
||||
venv/bin/pytest tests/ -q
|
||||
|
||||
test-one:
|
||||
venv/bin/pytest $(FILE) -v
|
||||
|
||||
clean:
|
||||
rm -rf data/tasks.db __pycache__ src/rockchip_agents.egg-info
|
||||
find . -name "*.pyc" -delete
|
||||
find . -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
|
||||
|
||||
scan:
|
||||
venv/bin/python scripts/run.py scan
|
||||
|
||||
run:
|
||||
venv/bin/python scripts/run.py run
|
||||
|
||||
status:
|
||||
venv/bin/python scripts/run.py status
|
||||
|
||||
start:
|
||||
venv/bin/python scripts/run.py start
|
||||
@@ -1,4 +1,4 @@
|
||||
# Rockchip Agents
|
||||
# NMFS Agents
|
||||
|
||||
基于 Claude CLI 的 AI 项目自动化维护与调研系统,运行在 RK3588 NPU 开发平台上。
|
||||
支持代码修复、板端测试、成熟度评估、架构整合、市场洞察与基础技术调研。
|
||||
@@ -93,12 +93,33 @@ python scripts/run.py status # 查看任务状态
|
||||
|
||||
## 工作原理
|
||||
|
||||
系统有两条触发路径,均汇入同一个三队列执行引擎:
|
||||
|
||||
```
|
||||
APScheduler(多队列,1h/4h/6h)
|
||||
└─> Scanner 扫描项目(README 已知问题 / TODO / CLAUDE.md 标记)
|
||||
└─> TaskQueue 入队(SQLite,去重,优先级排序)
|
||||
└─> Executor 并发分发(asyncio Semaphore 限流)
|
||||
└─> Agent.run(task) → claude --print 子进程
|
||||
┌─ Web Dashboard(主操作入口,http://localhost:9080)────────────────────────┐
|
||||
│ 提交需求(修复/重构/文档) → project_delivery 队列 → developer/architect│
|
||||
│ 技术攻关(RTP/网络/内核) → base_opt 队列 → 专项研究 Agent │
|
||||
│ 触发市场洞察 → insight 队列 → market-pm/planner │
|
||||
│ 刷新项目指导(CLAUDE.md) → project_delivery 队列 → architect 分析写入 │
|
||||
└────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─ APScheduler(定时自动)───────────────────────────────────────────────────┐
|
||||
│ Scanner 扫描 README/TODO/CLAUDE.md → 自动入对应队列(1h/6h 间隔) │
|
||||
└────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
↓ 三队列并发执行
|
||||
|
||||
project_delivery(tasks.db) ─> developer / architect / tester
|
||||
base_opt(base_opt_tasks.db) ─> 算法专项 / 调研 / 硬件优化 Agent
|
||||
insight(insight_tasks.db) ─> market-pm / planner / productizer
|
||||
|
||||
↓ 调研结论写入 ProjectMemory
|
||||
|
||||
__research__.rtp.* / __research__.net.* / __research__.kernel.*
|
||||
└─> Dashboard"基础技术调研"区块自动展示
|
||||
|
||||
__market__.{goal}.*(运动 / 适老 / AI安全)
|
||||
└─> Dashboard"市场洞察"目标卡片自动更新
|
||||
```
|
||||
|
||||
## 执行模式
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
- **Dashboard Web UI**:FastAPI + React 看板,WebSocket 实时推送,支持 KanbanBoard / InsightView / ConfigView / VisualView
|
||||
- **市场洞察(InsightView)**:planner/productizer/vision-analyst/media-producer 角色,前端触发
|
||||
- **三专项 Agent**:RtpResearcherAgent / NetResearcherAgent / KernelAnalyzerAgent
|
||||
- **Dashboard Docker 部署**:`rockchip-agents-dashboard:latest`,端口 9080
|
||||
- **Dashboard Docker 部署**:`nmfs-agents-dashboard:latest`,端口 9080
|
||||
- **rk3566 设备接入**:算法任务默认用 rk3566(192.168.123.183)验证,rk3588 作为高性能备选
|
||||
|
||||
### 修复
|
||||
|
||||
@@ -39,6 +39,7 @@ queues:
|
||||
- dev-embedded
|
||||
- os-engineer
|
||||
- system-tester
|
||||
- tester
|
||||
- architect
|
||||
- market-pm
|
||||
- rtp-researcher
|
||||
|
||||
+28
-5
@@ -97,28 +97,51 @@ roles:
|
||||
|
||||
productizer:
|
||||
- sequential-thinking # 成熟度评分推理、维度分析
|
||||
- fetch # 行业标准、同类产品成熟度参考
|
||||
- sqlite-memory # 查询项目目标和能力事实作为评分依据
|
||||
|
||||
vision-analyst:
|
||||
- sequential-thinking # 视觉质量分析推理
|
||||
- fetch # 视觉效果参考、UI 设计规范
|
||||
- playwright # 板端推理界面截图、视觉交互验证
|
||||
|
||||
market-pm:
|
||||
- sequential-thinking # 市场文档推理、方案书结构规划
|
||||
- fetch # 市场资料、竞品信息、行业数据抓取
|
||||
- context7 # 行业标准和规范文档
|
||||
- sqlite-memory # 查询本平台能力事实作为竞品对比基准
|
||||
|
||||
media-producer:
|
||||
- sequential-thinking # 媒体生产规划、效果图合成逻辑推理
|
||||
- fetch # 行业内容参考、风格灵感
|
||||
|
||||
# ── 兼容旧角色(保持向后兼容)────────────────────────────────────────────
|
||||
tester: [sequential-thinking, fetch, playwright]
|
||||
tester: [sequential-thinking, fetch, playwright, sqlite-memory]
|
||||
dev-embedded: [sequential-thinking, fetch, context7]
|
||||
dev-backend: [sequential-thinking, fetch, context7]
|
||||
dev-ml: [sequential-thinking, fetch, context7]
|
||||
dev-test: [sequential-thinking, playwright]
|
||||
dev-refactor: [sequential-thinking]
|
||||
dev-test: [sequential-thinking, fetch, playwright]
|
||||
dev-refactor: [sequential-thinking, fetch]
|
||||
arch-system: [sequential-thinking, fetch, context7]
|
||||
arch-security: [sequential-thinking, fetch]
|
||||
arch-reviewer: [sequential-thinking]
|
||||
pm: [sequential-thinking, fetch]
|
||||
arch-reviewer: [sequential-thinking, fetch]
|
||||
pm: [sequential-thinking, fetch, sqlite-memory]
|
||||
|
||||
# ── 专项调研(project_delivery 队列)────────────────────────────────────
|
||||
rtp-researcher:
|
||||
- sequential-thinking
|
||||
- fetch # RTP/RTCP RFC 文档、媒体传输最佳实践
|
||||
- context7 # 协议库文档
|
||||
- sqlite-memory # 读取已有调研结论
|
||||
|
||||
net-researcher:
|
||||
- sequential-thinking
|
||||
- fetch # 网络栈文档、内核网络子系统资料
|
||||
- context7 # Linux 网络编程文档
|
||||
- sqlite-memory
|
||||
|
||||
kernel-analyzer:
|
||||
- sequential-thinking
|
||||
- fetch # 内核文档、调度器源码参考
|
||||
- context7 # Linux 内核 API 文档
|
||||
- sqlite-memory
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
# 扫描任务标题/上下文中的关键词,路由到对应 agent_role
|
||||
role_keywords:
|
||||
dev-kernel:
|
||||
- 驱动
|
||||
- 内核
|
||||
- kernel module
|
||||
- linux driver
|
||||
- platform_driver
|
||||
- kernel
|
||||
- dts
|
||||
- device tree
|
||||
- insmod
|
||||
- modprobe
|
||||
- kconfig
|
||||
dev-lowlevel:
|
||||
- bsp
|
||||
- uboot
|
||||
- bootloader
|
||||
- 裸机
|
||||
- rtos
|
||||
- 引导
|
||||
- 启动
|
||||
- u-boot
|
||||
- spl
|
||||
dev-embedded:
|
||||
- rknn
|
||||
- npu
|
||||
- 推理
|
||||
- 板端
|
||||
- rk3588
|
||||
- lite2
|
||||
- rknn_lite
|
||||
- opencv
|
||||
- mediapipe
|
||||
dev-ml:
|
||||
- 模型
|
||||
- 精度
|
||||
- 量化
|
||||
- 训练
|
||||
- onnx
|
||||
- accuracy
|
||||
- map
|
||||
- calibration
|
||||
dev-backend:
|
||||
- api
|
||||
- fastapi
|
||||
- 数据库
|
||||
- 服务
|
||||
- http
|
||||
- redis
|
||||
- django
|
||||
- flask
|
||||
hw-engineer:
|
||||
- 选型
|
||||
- 硬件
|
||||
- 芯片
|
||||
- 外设
|
||||
- 接口
|
||||
- bom
|
||||
- 功耗
|
||||
- datasheet
|
||||
- schematic
|
||||
dev-test:
|
||||
- 测试覆盖
|
||||
- coverage
|
||||
- 缺少测试
|
||||
- test case
|
||||
- pytest
|
||||
dev-refactor:
|
||||
- 重构
|
||||
- 技术债
|
||||
- 代码质量
|
||||
- 冗余
|
||||
- refactor
|
||||
|
||||
# 未匹配时的默认角色
|
||||
default_role: dev-embedded
|
||||
@@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
dist
|
||||
.git
|
||||
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,73 @@
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## React Compiler
|
||||
|
||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||
|
||||
```js
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
|
||||
// Remove tseslint.configs.recommended and replace with this
|
||||
tseslint.configs.recommendedTypeChecked,
|
||||
// Alternatively, use this for stricter rules
|
||||
tseslint.configs.strictTypeChecked,
|
||||
// Optionally, add this for stylistic rules
|
||||
tseslint.configs.stylisticTypeChecked,
|
||||
|
||||
// Other configs...
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactX from 'eslint-plugin-react-x'
|
||||
import reactDom from 'eslint-plugin-react-dom'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
// Enable lint rules for React
|
||||
reactX.configs['recommended-typescript'],
|
||||
// Enable lint rules for React DOM
|
||||
reactDom.configs.recommended,
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
@@ -0,0 +1,23 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
reactHooks.configs.flat.recommended,
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
},
|
||||
])
|
||||
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>dashboard</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+5961
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "dashboard",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xyflow/react": "^12.10.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.577.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"zustand": "^5.0.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"autoprefixer": "^10.4.27",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"globals": "^16.5.0",
|
||||
"postcss": "^8.5.8",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.48.0",
|
||||
"vite": "^7.3.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,42 @@
|
||||
#root {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||
}
|
||||
|
||||
@keyframes logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
a:nth-of-type(2) .logo {
|
||||
animation: logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -0,0 +1,196 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTaskStore } from '../store/tasks'
|
||||
import type { Task } from '../types'
|
||||
|
||||
const STATUS_ORDER: Record<string, number> = {
|
||||
running: 0, waiting_approval: 1, pending: 2, failed: 3, done: 4, vetoed: 5,
|
||||
}
|
||||
|
||||
const STATUS_CLS: Record<string, string> = {
|
||||
running: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
|
||||
waiting_approval: 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200',
|
||||
pending: 'bg-slate-100 text-slate-600 dark:bg-gray-800 dark:text-gray-400',
|
||||
failed: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
|
||||
done: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
||||
vetoed: 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200',
|
||||
}
|
||||
|
||||
const STATUS_LABEL: Record<string, string> = {
|
||||
running: '运行中', waiting_approval: '待审批',
|
||||
pending: '排队中', failed: '失败', done: '完成', vetoed: '已否决',
|
||||
}
|
||||
|
||||
const ROLE_ABBR: Record<string, string> = {
|
||||
developer: 'Dev', architect: 'Arch', tester: 'Test',
|
||||
'market-pm': 'Mkt', planner: 'Plan', productizer: 'Prod',
|
||||
'rtp-researcher': 'RTP', 'net-researcher': 'Net', 'kernel-analyzer': 'Kern',
|
||||
'os-engineer': 'OS', 'hw-engineer': 'HW', 'dev-kernel': 'KDev',
|
||||
'base-architect': 'BArch', 'base-validator': 'BVal',
|
||||
'vision-analyst': 'Vision', 'media-producer': 'Media',
|
||||
}
|
||||
|
||||
type FilterStatus = 'active' | 'all' | 'done'
|
||||
|
||||
function elapsed(since: string | null): string {
|
||||
if (!since) return ''
|
||||
const secs = Math.floor((Date.now() - new Date(since).getTime()) / 1000)
|
||||
if (secs < 60) return `${secs}s`
|
||||
if (secs < 3600) return `${Math.floor(secs / 60)}m`
|
||||
return `${Math.floor(secs / 3600)}h${Math.floor((secs % 3600) / 60)}m`
|
||||
}
|
||||
|
||||
export function ActiveTasksView() {
|
||||
const tasksMap = useTaskStore(s => s.tasks)
|
||||
const setSelectedTask = useTaskStore(s => s.setSelectedTask)
|
||||
const [filter, setFilter] = useState<FilterStatus>('active')
|
||||
const [search, setSearch] = useState('')
|
||||
|
||||
const tasks = useMemo(() => {
|
||||
let arr = Array.from(tasksMap.values())
|
||||
if (filter === 'active') {
|
||||
arr = arr.filter(t => ['running', 'waiting_approval', 'pending'].includes(t.status))
|
||||
} else if (filter === 'done') {
|
||||
arr = arr.filter(t => ['done', 'failed', 'vetoed'].includes(t.status))
|
||||
.sort((a, b) => (b.completed_at ?? '').localeCompare(a.completed_at ?? ''))
|
||||
.slice(0, 50)
|
||||
}
|
||||
if (search.trim()) {
|
||||
const q = search.toLowerCase()
|
||||
arr = arr.filter(t =>
|
||||
t.title.toLowerCase().includes(q) ||
|
||||
t.project.toLowerCase().includes(q) ||
|
||||
t.agent_role.toLowerCase().includes(q)
|
||||
)
|
||||
}
|
||||
return arr.sort((a, b) =>
|
||||
(STATUS_ORDER[a.status] ?? 9) - (STATUS_ORDER[b.status] ?? 9) ||
|
||||
a.priority - b.priority ||
|
||||
b.id - a.id
|
||||
)
|
||||
}, [tasksMap, filter, search])
|
||||
|
||||
const counts = useMemo(() => {
|
||||
const all = Array.from(tasksMap.values())
|
||||
return {
|
||||
running: all.filter(t => t.status === 'running').length,
|
||||
waiting: all.filter(t => t.status === 'waiting_approval').length,
|
||||
pending: all.filter(t => t.status === 'pending').length,
|
||||
}
|
||||
}, [tasksMap])
|
||||
|
||||
return (
|
||||
<div className="p-4 max-w-5xl mx-auto">
|
||||
{/* 概览计数条 */}
|
||||
<div className="flex items-center gap-4 mb-4 text-xs">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="w-2 h-2 rounded-full bg-blue-400 animate-pulse inline-block" />
|
||||
<span className="text-slate-600 dark:text-slate-400">运行中 <strong className="text-slate-900 dark:text-white">{counts.running}</strong></span>
|
||||
</div>
|
||||
{counts.waiting > 0 && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="w-2 h-2 rounded-full bg-orange-400 inline-block" />
|
||||
<span className="text-slate-600 dark:text-slate-400">待审批 <strong className="text-orange-500">{counts.waiting}</strong></span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="w-2 h-2 rounded-full bg-slate-400 inline-block" />
|
||||
<span className="text-slate-600 dark:text-slate-400">排队中 <strong className="text-slate-900 dark:text-white">{counts.pending}</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 过滤栏 */}
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="flex gap-1 bg-slate-100 dark:bg-gray-800 rounded-lg p-0.5">
|
||||
{(['active', 'all', 'done'] as FilterStatus[]).map(f => (
|
||||
<button key={f}
|
||||
onClick={() => setFilter(f)}
|
||||
className={`px-3 py-1 text-xs rounded-md transition-colors
|
||||
${filter === f
|
||||
? 'bg-white dark:bg-gray-700 text-slate-900 dark:text-white shadow-sm'
|
||||
: 'text-slate-500 dark:text-gray-500 hover:text-slate-800 dark:hover:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
{{ active: '进行中', all: '全部', done: '已完成' }[f]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<input
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
placeholder="搜索任务/项目/角色..."
|
||||
className="flex-1 max-w-xs bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700
|
||||
rounded-lg px-3 py-1 text-xs text-slate-700 dark:text-gray-300
|
||||
focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-xs text-slate-400">{tasks.length} 条</span>
|
||||
</div>
|
||||
|
||||
{/* 任务列表 */}
|
||||
<div className="bg-white dark:bg-gray-900 rounded-xl border border-slate-200 dark:border-gray-700 overflow-hidden">
|
||||
{tasks.length === 0 ? (
|
||||
<div className="text-center py-12 text-sm text-slate-400 dark:text-gray-500">
|
||||
{filter === 'active' ? '当前没有进行中的任务' : '没有匹配的任务'}
|
||||
</div>
|
||||
) : (
|
||||
tasks.map(task => <TaskRow key={task.id} task={task} onClick={() => setSelectedTask(task.id)} />)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TaskRow({ task, onClick }: { task: Task; onClick: () => void }) {
|
||||
const isRunning = task.status === 'running'
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="w-full px-4 py-2.5 border-b border-slate-100 dark:border-gray-800 last:border-0
|
||||
flex items-center gap-3 text-left hover:bg-slate-50 dark:hover:bg-gray-800/50
|
||||
transition-colors group"
|
||||
>
|
||||
{/* 状态 badge */}
|
||||
<span className={`text-[10px] px-1.5 py-0.5 rounded-full shrink-0 font-medium
|
||||
${STATUS_CLS[task.status] ?? STATUS_CLS.pending}`}>
|
||||
{STATUS_LABEL[task.status] ?? task.status}
|
||||
</span>
|
||||
|
||||
{/* 项目 badge */}
|
||||
<span className="text-[10px] px-1.5 py-0.5 bg-slate-100 dark:bg-gray-800
|
||||
text-slate-500 dark:text-gray-400 rounded font-mono shrink-0">
|
||||
{task.project}
|
||||
</span>
|
||||
|
||||
{/* 标题 */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className={`text-sm truncate ${
|
||||
isRunning ? 'text-slate-900 dark:text-white' : 'text-slate-700 dark:text-slate-300'
|
||||
}`}>
|
||||
{isRunning && (
|
||||
<span className="inline-block w-1.5 h-1.5 rounded-full bg-blue-400 animate-pulse mr-1.5 align-middle" />
|
||||
)}
|
||||
{task.title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 角色 */}
|
||||
<span className="text-[10px] text-slate-400 dark:text-gray-500 shrink-0 font-mono">
|
||||
{ROLE_ABBR[task.agent_role] ?? task.agent_role}
|
||||
</span>
|
||||
|
||||
{/* P 级 */}
|
||||
<span className="text-[10px] text-slate-400 dark:text-gray-500 shrink-0">
|
||||
P{task.priority}
|
||||
</span>
|
||||
|
||||
{/* 计时 */}
|
||||
{(task.started_at || task.completed_at) && (
|
||||
<span className="text-[10px] text-slate-400 dark:text-gray-500 shrink-0 w-10 text-right">
|
||||
{task.status === 'running'
|
||||
? elapsed(task.started_at)
|
||||
: task.completed_at?.slice(5, 16)
|
||||
}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
import {
|
||||
fetchGlobalConfig, updateGlobalConfig,
|
||||
fetchProjectConfig, updateProjectConfig,
|
||||
refreshClaudeMd,
|
||||
} from '../lib/api'
|
||||
|
||||
interface Props {
|
||||
activeProject: string | null
|
||||
}
|
||||
|
||||
interface FileSection {
|
||||
label: string
|
||||
badge: string
|
||||
badgeColor: string
|
||||
content: string
|
||||
path: string
|
||||
exists: boolean
|
||||
editable: boolean
|
||||
warningOnEdit?: string
|
||||
onSave?: (content: string) => Promise<void>
|
||||
}
|
||||
|
||||
export function ConfigView({ activeProject }: Props) {
|
||||
const [sections, setSections] = useState<FileSection[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
const [refreshMsg, setRefreshMsg] = useState('')
|
||||
|
||||
async function handleRefreshClaudeMd() {
|
||||
if (!activeProject) return
|
||||
setRefreshing(true)
|
||||
setRefreshMsg('')
|
||||
try {
|
||||
const task = await refreshClaudeMd(activeProject)
|
||||
setRefreshMsg(`✓ 已创建刷新任务 #${task.id},AI 将根据最新调研结论更新 CLAUDE.md`)
|
||||
} catch (e) {
|
||||
setRefreshMsg(`✕ 失败:${e}`)
|
||||
} finally {
|
||||
setRefreshing(false)
|
||||
}
|
||||
}
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
try {
|
||||
const result: FileSection[] = []
|
||||
|
||||
// 全局 CLAUDE.md
|
||||
try {
|
||||
const g = await fetchGlobalConfig()
|
||||
result.push({
|
||||
label: '全局 CLAUDE.md',
|
||||
badge: 'Global',
|
||||
badgeColor: 'bg-purple-700 text-purple-100',
|
||||
content: g.content,
|
||||
path: g.path,
|
||||
exists: g.exists,
|
||||
editable: true,
|
||||
warningOnEdit: '⚠ 这是全局配置,修改将影响所有项目。确认要继续吗?',
|
||||
onSave: updateGlobalConfig,
|
||||
})
|
||||
} catch {
|
||||
result.push({
|
||||
label: '全局 CLAUDE.md',
|
||||
badge: 'Global',
|
||||
badgeColor: 'bg-purple-700 text-purple-100',
|
||||
content: '',
|
||||
path: '未配置(容器未挂载 ~/.claude)',
|
||||
exists: false,
|
||||
editable: false,
|
||||
})
|
||||
}
|
||||
|
||||
// 项目级配置(仅在选中项目时显示)
|
||||
if (activeProject) {
|
||||
try {
|
||||
const p = await fetchProjectConfig(activeProject)
|
||||
result.push({
|
||||
label: `${activeProject} / CLAUDE.md`,
|
||||
badge: '项目',
|
||||
badgeColor: 'bg-blue-700 text-blue-100',
|
||||
content: p.claude.content,
|
||||
path: p.claude.path,
|
||||
exists: p.claude.exists,
|
||||
editable: true,
|
||||
onSave: (c) => updateProjectConfig(activeProject, 'claude', c),
|
||||
})
|
||||
result.push({
|
||||
label: `${activeProject} / README.md`,
|
||||
badge: '项目',
|
||||
badgeColor: 'bg-green-700 text-green-100',
|
||||
content: p.readme.content,
|
||||
path: p.readme.path,
|
||||
exists: p.readme.exists,
|
||||
editable: true,
|
||||
onSave: (c) => updateProjectConfig(activeProject, 'readme', c),
|
||||
})
|
||||
} catch (e) {
|
||||
setError(String(e))
|
||||
}
|
||||
}
|
||||
|
||||
setSections(result)
|
||||
} catch (e) {
|
||||
setError(String(e))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [activeProject])
|
||||
|
||||
useEffect(() => { load() }, [load])
|
||||
|
||||
if (loading) return <div className="p-6 text-slate-500 dark:text-gray-400 text-sm">加载中...</div>
|
||||
|
||||
return (
|
||||
<div className="p-4 space-y-4 max-w-4xl mx-auto">
|
||||
{!activeProject && (
|
||||
<div className="text-xs text-slate-400 dark:text-gray-500 bg-slate-100/60 dark:bg-gray-800/60 rounded-lg px-3 py-2">
|
||||
选择左上角项目标签后,可查看并编辑该项目的 CLAUDE.md 和 README.md。当前仅显示全局配置。
|
||||
</div>
|
||||
)}
|
||||
{activeProject && (
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={handleRefreshClaudeMd}
|
||||
disabled={refreshing}
|
||||
className="px-4 py-1.5 text-xs bg-amber-700 hover:bg-amber-600 text-white rounded-lg disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{refreshing ? '创建任务中...' : '🔄 刷新项目指导'}
|
||||
</button>
|
||||
{refreshMsg && (
|
||||
<span className={`text-xs ${refreshMsg.startsWith('✓') ? 'text-green-400' : 'text-red-400'}`}>
|
||||
{refreshMsg}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{error && <div className="text-xs text-red-400 bg-red-900/20 rounded px-3 py-2">{error}</div>}
|
||||
{sections.map((s, i) => (
|
||||
<SectionCard key={i} section={s} onReload={load} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SectionCard({ section, onReload }: { section: FileSection; onReload: () => void }) {
|
||||
const [editing, setEditing] = useState(false)
|
||||
const [draft, setDraft] = useState(section.content)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [saveError, setSaveError] = useState('')
|
||||
|
||||
// keep draft in sync when section reloads
|
||||
useEffect(() => { setDraft(section.content); setEditing(false) }, [section.content])
|
||||
|
||||
async function handleEdit() {
|
||||
if (section.warningOnEdit && !confirm(section.warningOnEdit)) return
|
||||
setEditing(true)
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!section.onSave) return
|
||||
setSaving(true)
|
||||
setSaveError('')
|
||||
try {
|
||||
await section.onSave(draft)
|
||||
setEditing(false)
|
||||
onReload()
|
||||
} catch (e) {
|
||||
setSaveError(String(e))
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-900 border border-slate-200 dark:border-gray-700 rounded-xl overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2 px-4 py-2.5 border-b border-slate-200 dark:border-gray-700 bg-slate-100/50 dark:bg-gray-800/50">
|
||||
<span className={`text-[10px] font-semibold px-2 py-0.5 rounded-full ${section.badgeColor}`}>
|
||||
{section.badge}
|
||||
</span>
|
||||
<span className="text-sm font-medium text-slate-800 dark:text-gray-200">{section.label}</span>
|
||||
<span className="text-xs text-slate-400 dark:text-gray-500 ml-1 truncate">{section.path}</span>
|
||||
{!section.exists && (
|
||||
<span className="text-xs text-yellow-600 ml-auto">(文件不存在)</span>
|
||||
)}
|
||||
<div className="ml-auto flex gap-2">
|
||||
{editing ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => { setEditing(false); setDraft(section.content) }}
|
||||
className="text-xs text-slate-500 dark:text-gray-400 hover:text-slate-800 dark:hover:text-gray-200 px-2 py-1"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="text-xs bg-blue-700 hover:bg-blue-600 text-white px-3 py-1 rounded disabled:opacity-50"
|
||||
>
|
||||
{saving ? '保存中...' : '保存'}
|
||||
</button>
|
||||
</>
|
||||
) : section.editable ? (
|
||||
<button
|
||||
onClick={handleEdit}
|
||||
className="text-xs text-slate-500 dark:text-gray-400 hover:text-slate-800 dark:hover:text-gray-200 px-2 py-1 rounded hover:bg-slate-200 dark:hover:bg-gray-700"
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{saveError && (
|
||||
<div className="px-4 py-2 text-xs text-red-400 bg-red-900/20">{saveError}</div>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<div className="relative">
|
||||
{editing ? (
|
||||
<textarea
|
||||
value={draft}
|
||||
onChange={e => setDraft(e.target.value)}
|
||||
className="w-full bg-slate-50 dark:bg-gray-950 text-slate-800 dark:text-gray-200 text-xs font-mono p-4 min-h-[300px] resize-y
|
||||
focus:outline-none focus:ring-1 focus:ring-blue-600"
|
||||
spellCheck={false}
|
||||
/>
|
||||
) : (
|
||||
<pre className="text-xs text-slate-700 dark:text-gray-300 font-mono p-4 overflow-x-auto whitespace-pre-wrap
|
||||
max-h-[400px] overflow-y-auto">
|
||||
{section.content || <span className="text-slate-300 dark:text-gray-600 italic">(空文件)</span>}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
import { useState } from 'react'
|
||||
import { importProject, fetchProjects } from '../lib/api'
|
||||
import { useTaskStore } from '../store/tasks'
|
||||
|
||||
interface Props { onClose: () => void }
|
||||
|
||||
const MODES = ['auto', 'confirm', 'report']
|
||||
|
||||
const TASK_TEMPLATE = [
|
||||
{ priority: 'P1', label: '全量代码审查', role: 'developer', desc: '架构设计与技术债务识别' },
|
||||
{ priority: 'P1', label: '测试覆盖率补全', role: 'tester', desc: '关键路径 + mock 无依赖 CI' },
|
||||
{ priority: 'P2', label: '模块重构', role: 'developer', desc: '接口规范化与解耦' },
|
||||
{ priority: 'P2', label: '性能分析', role: 'developer', desc: '推理瓶颈识别 + 向量化优化' },
|
||||
{ priority: 'P2', label: '板端集成测试', role: 'tester', desc: 'RKNN 部署验证(如含 RKNN)' },
|
||||
{ priority: 'P2', label: '跨项目数据对接', role: 'developer', desc: '关联项目数据流集成(若填写)' },
|
||||
{ priority: 'P3', label: '架构评估', role: 'architect', desc: '耦合度与扩展性路线图' },
|
||||
{ priority: 'P3', label: '文档完善', role: 'developer', desc: 'README/CLAUDE.md 更新' },
|
||||
]
|
||||
|
||||
const ROLE_COLOR: Record<string, string> = {
|
||||
developer: 'text-blue-400',
|
||||
tester: 'text-green-400',
|
||||
architect: 'text-purple-400',
|
||||
}
|
||||
|
||||
export function ImportProjectModal({ onClose }: Props) {
|
||||
const setProjects = useTaskStore(s => s.setProjects)
|
||||
const [form, setForm] = useState({
|
||||
path: '',
|
||||
name: '',
|
||||
mode: 'auto',
|
||||
description: '',
|
||||
scan_now: true,
|
||||
linked_project: '',
|
||||
})
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [result, setResult] = useState<{ tasks_created: number; tasks: { title: string }[] } | null>(null)
|
||||
const [showTemplate, setShowTemplate] = useState(false)
|
||||
|
||||
function handlePathChange(val: string) {
|
||||
const derived = val.replace(/\/$/, '').split('/').pop() ?? ''
|
||||
setForm(f => ({ ...f, path: val, name: f.name || derived }))
|
||||
}
|
||||
|
||||
async function submit(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
if (!form.path.trim()) { setError('目录路径不能为空'); return }
|
||||
setLoading(true)
|
||||
setError('')
|
||||
try {
|
||||
const res = await importProject({
|
||||
path: form.path.trim(),
|
||||
name: form.name.trim() || undefined,
|
||||
mode: form.mode,
|
||||
description: form.description.trim(),
|
||||
scan_now: form.scan_now,
|
||||
linked_project: form.linked_project.trim() || undefined,
|
||||
})
|
||||
const projects = await fetchProjects()
|
||||
setProjects(projects)
|
||||
if (res.tasks_created > 0) {
|
||||
setResult(res)
|
||||
} else {
|
||||
onClose()
|
||||
}
|
||||
} catch (e) {
|
||||
setError(String(e))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 成功后显示任务创建结果
|
||||
if (result) {
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center">
|
||||
<div className="bg-white dark:bg-gray-900 border border-slate-200 dark:border-gray-700 rounded-xl p-6 w-[520px] shadow-2xl">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="text-green-400 text-lg">✓</span>
|
||||
<h2 className="text-base font-semibold text-slate-900 dark:text-gray-100">
|
||||
项目已导入,{result.tasks_created} 个任务已入队
|
||||
</h2>
|
||||
</div>
|
||||
<div className="space-y-1 max-h-60 overflow-y-auto mb-4">
|
||||
{result.tasks.map((t, i) => (
|
||||
<div key={i} className="text-xs text-slate-700 dark:text-gray-300 bg-slate-100 dark:bg-gray-800 rounded px-3 py-1.5">
|
||||
{t.title}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 dark:text-gray-500 mb-4">
|
||||
点击 TopBar 的「开始执行」按钮启动自动执行,或在 Kanban 中查看任务状态。
|
||||
</p>
|
||||
<div className="flex justify-end">
|
||||
<button onClick={onClose}
|
||||
className="px-4 py-1.5 text-sm bg-blue-700 hover:bg-blue-600 text-white rounded">
|
||||
完成
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center">
|
||||
<div className="bg-white dark:bg-gray-900 border border-slate-200 dark:border-gray-700 rounded-xl p-6 w-[540px] shadow-2xl">
|
||||
<h2 className="text-base font-semibold text-slate-900 dark:text-gray-100 mb-1">导入项目目录</h2>
|
||||
<p className="text-xs text-slate-400 dark:text-gray-500 mb-4">
|
||||
自动检测项目特征,生成适合软硬件协同研发的默认任务集
|
||||
</p>
|
||||
|
||||
<form onSubmit={submit} className="space-y-3">
|
||||
<Row label="目录路径">
|
||||
<input
|
||||
value={form.path}
|
||||
onChange={e => handlePathChange(e.target.value)}
|
||||
className={inputCls}
|
||||
placeholder="/data/rockchip/my-project"
|
||||
/>
|
||||
</Row>
|
||||
<Row label="项目名称">
|
||||
<input
|
||||
value={form.name}
|
||||
onChange={e => setForm(f => ({ ...f, name: e.target.value }))}
|
||||
className={inputCls}
|
||||
placeholder="自动从路径推断"
|
||||
/>
|
||||
</Row>
|
||||
<Row label="模式">
|
||||
<select
|
||||
value={form.mode}
|
||||
onChange={e => setForm(f => ({ ...f, mode: e.target.value }))}
|
||||
className={inputCls}
|
||||
>
|
||||
{MODES.map(m => (
|
||||
<option key={m} value={m}>
|
||||
{m === 'auto' ? 'auto — 全自动持续开发(推荐)' :
|
||||
m === 'confirm' ? 'confirm — 每步需审批' :
|
||||
'report — 仅分析不修改'}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Row>
|
||||
<Row label="关联项目">
|
||||
<input
|
||||
value={form.linked_project}
|
||||
onChange={e => setForm(f => ({ ...f, linked_project: e.target.value }))}
|
||||
className={inputCls}
|
||||
placeholder="/data/test-platform/logo-collect(可选,生成集成任务)"
|
||||
/>
|
||||
</Row>
|
||||
<Row label="描述">
|
||||
<input
|
||||
value={form.description}
|
||||
onChange={e => setForm(f => ({ ...f, description: e.target.value }))}
|
||||
className={inputCls}
|
||||
placeholder="项目简短描述(可选)"
|
||||
/>
|
||||
</Row>
|
||||
|
||||
{/* 任务模板开关 */}
|
||||
<div className="flex items-center gap-2 pl-[108px]">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="scan_now"
|
||||
checked={form.scan_now}
|
||||
onChange={e => setForm(f => ({ ...f, scan_now: e.target.checked }))}
|
||||
className="accent-blue-500"
|
||||
/>
|
||||
<label htmlFor="scan_now" className="text-xs text-slate-500 dark:text-gray-400 cursor-pointer">
|
||||
生成默认任务集(根据项目特征自动适配)
|
||||
</label>
|
||||
{form.scan_now && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowTemplate(v => !v)}
|
||||
className="text-xs text-blue-400 hover:text-blue-300 ml-1"
|
||||
>
|
||||
{showTemplate ? '收起' : '预览任务'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 任务模板预览 */}
|
||||
{form.scan_now && showTemplate && (
|
||||
<div className="ml-[108px] bg-slate-100/60 dark:bg-gray-800/60 rounded-lg p-3 space-y-1.5">
|
||||
{TASK_TEMPLATE.map((t, i) => (
|
||||
<div key={i} className="flex items-start gap-2 text-xs">
|
||||
<span className={`text-[10px] font-mono px-1.5 py-0.5 rounded shrink-0
|
||||
${t.priority === 'P1' ? 'bg-red-900/60 text-red-300' :
|
||||
t.priority === 'P2' ? 'bg-yellow-900/60 text-yellow-300' :
|
||||
'bg-slate-200 dark:bg-gray-700 text-slate-500 dark:text-gray-400'}`}>
|
||||
{t.priority}
|
||||
</span>
|
||||
<span className="text-slate-800 dark:text-gray-200 font-medium shrink-0">{t.label}</span>
|
||||
<span className={`shrink-0 ${ROLE_COLOR[t.role]}`}>[{t.role}]</span>
|
||||
<span className="text-slate-400 dark:text-gray-500">{t.desc}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && <p className="text-xs text-red-400">{error}</p>}
|
||||
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<button type="button" onClick={onClose}
|
||||
className="px-4 py-1.5 text-sm text-slate-500 dark:text-gray-400 hover:text-slate-800 dark:hover:text-gray-200">
|
||||
取消
|
||||
</button>
|
||||
<button type="submit" disabled={loading}
|
||||
className="px-4 py-1.5 text-sm bg-blue-700 hover:bg-blue-600 text-white
|
||||
rounded disabled:opacity-50">
|
||||
{loading ? '导入中...' : '导入并生成任务'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const inputCls = 'w-full bg-slate-100 dark:bg-gray-800 border border-slate-300 dark:border-gray-600 rounded px-2 py-1.5 text-xs text-slate-800 dark:text-gray-200'
|
||||
|
||||
function Row({ label, children }: { label: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="grid grid-cols-[100px_1fr] items-start gap-2">
|
||||
<label className="text-xs text-slate-500 dark:text-gray-400 pt-1.5">{label}</label>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import {
|
||||
fetchMarketFacts, fetchMarketHistory, triggerMarketInsight,
|
||||
type MarketFacts, type MarketHistoryItem,
|
||||
fetchResearchFacts, fetchTaskLog,
|
||||
type MarketFacts, type MarketHistoryItem, type ResearchFacts,
|
||||
} from '../lib/api'
|
||||
|
||||
const GOAL_META: Record<string, { icon: string; label: string; color: string }> = {
|
||||
@@ -53,6 +56,12 @@ function GoalCard({ goal, facts, onTrigger }: {
|
||||
{facts.trends}
|
||||
</div>
|
||||
)}
|
||||
{facts.opportunities && (
|
||||
<div>
|
||||
<span className="font-medium text-slate-500 dark:text-slate-400">机会 </span>
|
||||
{facts.opportunities}
|
||||
</div>
|
||||
)}
|
||||
{facts.updated_at && (
|
||||
<div className="text-slate-400 dark:text-slate-500 text-right">
|
||||
更新于 {facts.updated_at.slice(0, 16)}
|
||||
@@ -69,6 +78,23 @@ function GoalCard({ goal, facts, onTrigger }: {
|
||||
}
|
||||
|
||||
function HistoryRow({ item }: { item: MarketHistoryItem }) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [log, setLog] = useState<string[]>([])
|
||||
const [logLoading, setLogLoading] = useState(false)
|
||||
|
||||
async function toggle() {
|
||||
if (!expanded && log.length === 0) {
|
||||
setLogLoading(true)
|
||||
try {
|
||||
const r = await fetchTaskLog(item.id)
|
||||
setLog(r.lines)
|
||||
} catch { /* ignore */ } finally {
|
||||
setLogLoading(false)
|
||||
}
|
||||
}
|
||||
setExpanded(e => !e)
|
||||
}
|
||||
|
||||
const statusCls: Record<string, string> = {
|
||||
done: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
||||
failed: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
|
||||
@@ -76,34 +102,105 @@ function HistoryRow({ item }: { item: MarketHistoryItem }) {
|
||||
pending: 'bg-slate-100 text-slate-600',
|
||||
}
|
||||
return (
|
||||
<div className="px-4 py-3 border-b border-slate-100 dark:border-gray-800 flex items-start gap-3">
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full shrink-0 ${statusCls[item.status] ?? statusCls.pending}`}>
|
||||
{item.status}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm text-slate-800 dark:text-slate-200 truncate">{item.title}</div>
|
||||
{item.result_summary && (
|
||||
<div className="text-xs text-slate-500 dark:text-slate-400 mt-0.5 line-clamp-2">
|
||||
{item.result_summary}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{item.completed_at && (
|
||||
<span className="text-xs text-slate-400 shrink-0">{item.completed_at.slice(0, 10)}</span>
|
||||
<div className="border-b border-slate-100 dark:border-gray-800">
|
||||
<button
|
||||
onClick={toggle}
|
||||
className="w-full px-4 py-3 flex items-start gap-3 text-left hover:bg-slate-50 dark:hover:bg-gray-800/50 transition-colors"
|
||||
>
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full shrink-0 mt-0.5 ${statusCls[item.status] ?? statusCls.pending}`}>
|
||||
{item.status}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm text-slate-800 dark:text-slate-200 truncate">{item.title}</div>
|
||||
{item.result_summary && !expanded && (
|
||||
<div className="text-xs text-slate-500 dark:text-slate-400 mt-0.5 line-clamp-2">
|
||||
{item.result_summary}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
{item.completed_at && (
|
||||
<span className="text-xs text-slate-400">{item.completed_at.slice(0, 10)}</span>
|
||||
)}
|
||||
<span className="text-xs text-slate-400">{expanded ? '▲' : '▼'}</span>
|
||||
</div>
|
||||
</button>
|
||||
{expanded && (
|
||||
<div className="px-4 pb-4">
|
||||
{logLoading ? (
|
||||
<div className="text-xs text-slate-400 py-2">加载中...</div>
|
||||
) : (() => {
|
||||
const content = log.length > 0 ? log.join('\n') : item.result_summary
|
||||
return content ? (
|
||||
<div className="max-h-[32rem] overflow-y-auto bg-slate-50 dark:bg-gray-950 rounded-lg p-4
|
||||
prose prose-sm dark:prose-invert max-w-none
|
||||
prose-table:text-xs prose-td:py-1 prose-th:py-1
|
||||
prose-headings:text-slate-800 dark:prose-headings:text-slate-100
|
||||
prose-p:text-slate-700 dark:prose-p:text-slate-300
|
||||
prose-code:text-xs prose-code:bg-slate-200 dark:prose-code:bg-gray-800
|
||||
prose-code:px-1 prose-code:rounded">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-xs text-slate-400 py-2">暂无内容</div>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const DOMAIN_META: Record<string, { icon: string; label: string }> = {
|
||||
rtp: { icon: '📡', label: 'RTP/RTCP 实时传输' },
|
||||
net: { icon: '🌐', label: '网络栈调优' },
|
||||
kernel: { icon: '⚙️', label: '内核调度分析' },
|
||||
}
|
||||
|
||||
function ResearchSection({ research }: { research: ResearchFacts }) {
|
||||
const domains = Object.entries(research.domains)
|
||||
if (domains.length === 0) {
|
||||
return (
|
||||
<div className="text-xs text-slate-400 dark:text-gray-500 text-center py-6">
|
||||
暂无调研数据,基础技术组完成调研后自动展示
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{domains.map(([domain, facts]) => {
|
||||
const meta = DOMAIN_META[domain] ?? { icon: '🔬', label: domain }
|
||||
return (
|
||||
<div key={domain}
|
||||
className="rounded-xl border border-slate-200 dark:border-gray-700 bg-white dark:bg-gray-900 p-4 space-y-2">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="text-lg">{meta.icon}</span>
|
||||
<span className="text-sm font-semibold text-slate-900 dark:text-white">{meta.label}</span>
|
||||
</div>
|
||||
{Object.entries(facts).slice(0, 6).map(([k, v]) => (
|
||||
<div key={k} className="text-xs">
|
||||
<span className="font-medium text-slate-500 dark:text-slate-400 capitalize">{k.replace(/_/g, ' ')} </span>
|
||||
<span className="text-slate-700 dark:text-slate-300 line-clamp-2">{v}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function InsightView() {
|
||||
const [data, setData] = useState<MarketFacts>({ goals: ['运动', '适老', 'AI安全'], facts: {} })
|
||||
const [history, setHistory] = useState<MarketHistoryItem[]>([])
|
||||
const [research, setResearch] = useState<ResearchFacts>({ domains: {} })
|
||||
const [_triggering, setTriggering] = useState<string | null>(null)
|
||||
const [toast, setToast] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
fetchMarketFacts().then(setData).catch(console.warn)
|
||||
fetchMarketHistory().then(setHistory).catch(console.warn)
|
||||
fetchResearchFacts().then(setResearch).catch(console.warn)
|
||||
}, [])
|
||||
|
||||
async function handleTrigger(goal: string) {
|
||||
@@ -142,6 +239,11 @@ export function InsightView() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<h2 className="text-sm font-semibold text-slate-600 dark:text-slate-400 mb-3">基础技术调研</h2>
|
||||
<ResearchSection research={research} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-slate-600 dark:text-slate-400 mb-3">洞察任务历史</h2>
|
||||
<div className="bg-white dark:bg-gray-900 rounded-xl border border-slate-200 dark:border-gray-700 overflow-hidden">
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
import { useMemo, useState, useCallback, useRef, useEffect } from 'react'
|
||||
import { useTaskStore } from '../store/tasks'
|
||||
import { clearProjectTasks, deleteProject } from '../lib/api'
|
||||
import { KanbanColumn } from './KanbanColumn'
|
||||
import { STATUS_COLUMNS } from '../types'
|
||||
import type { Task } from '../types'
|
||||
|
||||
interface Props {
|
||||
project?: string // undefined = 全览(所有项目合并)
|
||||
onImport?: () => void
|
||||
onNewTask?: () => void
|
||||
onBack?: () => void // 返回概览
|
||||
}
|
||||
|
||||
export function KanbanBoard({ project, onImport, onNewTask, onBack }: Props) {
|
||||
const tasksMap = useTaskStore(s => s.tasks)
|
||||
const projects = useTaskStore(s => s.projects)
|
||||
const [collapsed, setCollapsed] = useState<Set<string>>(new Set())
|
||||
const [search, setSearch] = useState('')
|
||||
const [menuOpen, setMenuOpen] = useState<string | null>(null) // which project's menu is open
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const tasksByProject = useMemo(() => {
|
||||
const result = new Map<string, Task[]>()
|
||||
for (const task of tasksMap.values()) {
|
||||
const arr = result.get(task.project) ?? []
|
||||
arr.push(task)
|
||||
result.set(task.project, arr)
|
||||
}
|
||||
return result
|
||||
}, [tasksMap])
|
||||
|
||||
const projectsToShow = project
|
||||
? [project]
|
||||
: Array.from(tasksByProject.keys()).sort()
|
||||
|
||||
const getTasksByStatus = useCallback((proj: string | undefined): Map<string, Task[]> => {
|
||||
const source = proj ? (tasksByProject.get(proj) ?? []) : Array.from(tasksMap.values())
|
||||
const q = search.trim().toLowerCase()
|
||||
const filtered = q ? source.filter(t => t.title.toLowerCase().includes(q)) : source
|
||||
const result = new Map<string, Task[]>()
|
||||
STATUS_COLUMNS.forEach(s => result.set(s, []))
|
||||
filtered.forEach(t => {
|
||||
const key = STATUS_COLUMNS.includes(t.status as any) ? t.status : 'pending'
|
||||
result.get(key)?.push(t)
|
||||
})
|
||||
return result
|
||||
}, [tasksByProject, tasksMap, search])
|
||||
|
||||
function toggleCollapse(proj: string) {
|
||||
setCollapsed(prev => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(proj)) next.delete(proj)
|
||||
else next.add(proj)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
function handleClick(e: MouseEvent) {
|
||||
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
||||
setMenuOpen(null)
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClick)
|
||||
return () => document.removeEventListener('mousedown', handleClick)
|
||||
}, [])
|
||||
|
||||
async function handleClearTasks(proj: string) {
|
||||
setMenuOpen(null)
|
||||
const n = [...useTaskStore.getState().tasks.values()].filter(t => t.project === proj).length
|
||||
if (!confirm(`确认清空项目「${proj}」的全部 ${n} 条任务记录?\n(此操作不可恢复)`)) return
|
||||
try {
|
||||
await clearProjectTasks(proj)
|
||||
useTaskStore.getState().removeProjectTasks(proj)
|
||||
} catch (e) {
|
||||
alert('清空失败:' + e)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteProject(proj: string) {
|
||||
setMenuOpen(null)
|
||||
if (!confirm(`确认删除项目「${proj}」配置并清空所有任务?\n(此操作不可恢复)`)) return
|
||||
try {
|
||||
await deleteProject(proj)
|
||||
useTaskStore.getState().removeProjectTasks(proj)
|
||||
useTaskStore.getState().setProjects(
|
||||
useTaskStore.getState().projects.filter(p => p.name !== proj)
|
||||
)
|
||||
} catch (e) {
|
||||
alert('删除失败:' + e)
|
||||
}
|
||||
}
|
||||
|
||||
if (projectsToShow.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-64 gap-4">
|
||||
<p className="text-slate-400 dark:text-gray-600 text-sm">暂无任务数据</p>
|
||||
<div className="flex gap-2">
|
||||
{onImport && (
|
||||
<button
|
||||
onClick={onImport}
|
||||
className="px-4 py-2 text-xs bg-slate-100 dark:bg-gray-800 hover:bg-slate-200 dark:hover:bg-gray-700 text-slate-700 dark:text-gray-300 rounded-lg"
|
||||
>
|
||||
导入项目
|
||||
</button>
|
||||
)}
|
||||
{onNewTask && projects.length > 0 && (
|
||||
<button
|
||||
onClick={onNewTask}
|
||||
className="px-4 py-2 text-xs bg-blue-800 hover:bg-blue-700 text-blue-100 rounded-lg"
|
||||
>
|
||||
提交需求
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 p-4">
|
||||
{/* 面包屑:在特定项目模式下显示 */}
|
||||
{project && (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="text-xs text-slate-400 dark:text-gray-500 hover:text-slate-700 dark:hover:text-gray-300 transition-colors"
|
||||
>
|
||||
← 所有项目
|
||||
</button>
|
||||
<span className="text-slate-500 dark:text-gray-700">/</span>
|
||||
<span className="text-xs font-medium text-slate-800 dark:text-gray-200">{project}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 搜索过滤 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
placeholder="搜索任务..."
|
||||
className="bg-slate-100 dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-lg px-3 py-1.5 text-xs
|
||||
text-slate-800 dark:text-gray-200 placeholder-slate-400 dark:placeholder-gray-600 w-56 focus:outline-none focus:border-slate-400 dark:focus:border-gray-500"
|
||||
/>
|
||||
{search && (
|
||||
<button
|
||||
onClick={() => setSearch('')}
|
||||
className="text-xs text-slate-400 dark:text-gray-500 hover:text-slate-700 dark:hover:text-gray-300"
|
||||
>
|
||||
清除
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{projectsToShow.map(proj => {
|
||||
const byStatus = getTasksByStatus(proj)
|
||||
const isCollapsed = collapsed.has(proj)
|
||||
const runningCount = byStatus.get('running')?.length ?? 0
|
||||
const waitingCount = byStatus.get('waiting_approval')?.length ?? 0
|
||||
const failedCount = byStatus.get('failed')?.length ?? 0
|
||||
|
||||
return (
|
||||
<div key={proj}>
|
||||
{/* 项目标题栏 */}
|
||||
<div className="flex items-center gap-3 mb-3 group">
|
||||
<div
|
||||
className="flex items-center gap-3 cursor-pointer select-none flex-1"
|
||||
onClick={() => toggleCollapse(proj)}
|
||||
>
|
||||
<span className="text-slate-500 dark:text-gray-400 text-xs">{isCollapsed ? '▶' : '▼'}</span>
|
||||
<span className="text-slate-800 dark:text-gray-200 font-semibold text-sm group-hover:text-blue-700 dark:group-hover:text-white transition-colors">
|
||||
{proj}
|
||||
</span>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
{runningCount > 0 && (
|
||||
<span className="flex items-center gap-1 text-blue-600 dark:text-blue-400">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-blue-500 dark:bg-blue-400 animate-pulse inline-block" />
|
||||
{runningCount} 运行中
|
||||
</span>
|
||||
)}
|
||||
{waitingCount > 0 && (
|
||||
<span className="text-orange-600 dark:text-orange-400">⚠ {waitingCount} 待确认</span>
|
||||
)}
|
||||
{failedCount > 0 && (
|
||||
<span className="text-red-600 dark:text-red-400">✕ {failedCount} 失败</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 项目操作菜单 */}
|
||||
<div className="relative" ref={menuOpen === proj ? menuRef : undefined}>
|
||||
<button
|
||||
onClick={e => { e.stopPropagation(); setMenuOpen(menuOpen === proj ? null : proj) }}
|
||||
className="text-slate-400 dark:text-gray-600 hover:text-slate-700 dark:hover:text-gray-300 px-1.5 py-0.5 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
title="项目操作"
|
||||
>
|
||||
⋮
|
||||
</button>
|
||||
{menuOpen === proj && (
|
||||
<div className="absolute right-0 top-6 z-50 bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-lg shadow-lg py-1 min-w-[140px]">
|
||||
<button
|
||||
onClick={() => handleClearTasks(proj)}
|
||||
className="w-full text-left px-3 py-2 text-xs text-orange-600 dark:text-orange-400 hover:bg-orange-50 dark:hover:bg-orange-900/20 transition-colors"
|
||||
>
|
||||
🗑 清空所有任务
|
||||
</button>
|
||||
<div className="border-t border-slate-100 dark:border-gray-700 my-1" />
|
||||
<button
|
||||
onClick={() => handleDeleteProject(proj)}
|
||||
className="w-full text-left px-3 py-2 text-xs text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
|
||||
>
|
||||
✕ 删除项目
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Kanban 列 */}
|
||||
{!isCollapsed && (
|
||||
<div className="flex gap-4 overflow-x-auto pb-2">
|
||||
{STATUS_COLUMNS.map(status => (
|
||||
<KanbanColumn
|
||||
key={status}
|
||||
status={status}
|
||||
tasks={byStatus.get(status) ?? []}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useState } from 'react'
|
||||
import type { Task } from '../types'
|
||||
import { STATUS_LABELS } from '../types'
|
||||
import { TaskCard } from './TaskCard'
|
||||
|
||||
interface Props {
|
||||
status: string
|
||||
tasks: Task[]
|
||||
}
|
||||
|
||||
const COLUMN_HEADER_COLORS: Record<string, string> = {
|
||||
pending: 'text-slate-500 dark:text-gray-400 border-slate-300 dark:border-gray-600',
|
||||
running: 'text-blue-400 border-blue-500',
|
||||
done: 'text-green-400 border-green-600',
|
||||
failed: 'text-red-400 border-red-600',
|
||||
waiting_approval: 'text-orange-400 border-orange-500',
|
||||
vetoed: 'text-slate-400 dark:text-gray-500 border-slate-300 dark:border-gray-600',
|
||||
}
|
||||
|
||||
// 完成列默认只显示最近 5 条
|
||||
const DONE_PREVIEW_COUNT = 5
|
||||
|
||||
export function KanbanColumn({ status, tasks }: Props) {
|
||||
const [showAll, setShowAll] = useState(false)
|
||||
const label = STATUS_LABELS[status] ?? status
|
||||
const headerColor = COLUMN_HEADER_COLORS[status] ?? 'text-slate-500 dark:text-gray-400 border-slate-300 dark:border-gray-600'
|
||||
|
||||
// 完成列截断
|
||||
const isDone = status === 'done'
|
||||
const displayTasks = isDone && !showAll
|
||||
? tasks.slice(0, DONE_PREVIEW_COUNT)
|
||||
: tasks
|
||||
const hiddenCount = isDone ? tasks.length - DONE_PREVIEW_COUNT : 0
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-w-[240px] w-[240px]">
|
||||
{/* 列头 */}
|
||||
<div className={`flex items-center justify-between mb-3 pb-2 border-b ${headerColor}`}>
|
||||
<span className="text-sm font-semibold">{label}</span>
|
||||
<span className="text-xs bg-slate-200/60 dark:bg-gray-700/60 rounded-full px-2 py-0.5 font-mono">{tasks.length}</span>
|
||||
</div>
|
||||
|
||||
{/* 任务卡片 */}
|
||||
<div className="flex flex-col gap-2 overflow-y-auto max-h-[calc(100vh-280px)]">
|
||||
{displayTasks.length === 0 ? (
|
||||
<p className="text-xs text-slate-500 dark:text-gray-700 text-center py-4">—</p>
|
||||
) : (
|
||||
displayTasks.map(t => <TaskCard key={t.id} task={t} />)
|
||||
)}
|
||||
|
||||
{/* 显示更多 / 收起 */}
|
||||
{isDone && hiddenCount > 0 && !showAll && (
|
||||
<button
|
||||
onClick={() => setShowAll(true)}
|
||||
className="text-xs text-slate-400 dark:text-gray-600 hover:text-slate-600 dark:hover:text-gray-400 text-center py-2 transition-colors"
|
||||
>
|
||||
查看更早的 {hiddenCount} 条
|
||||
</button>
|
||||
)}
|
||||
{isDone && showAll && tasks.length > DONE_PREVIEW_COUNT && (
|
||||
<button
|
||||
onClick={() => setShowAll(false)}
|
||||
className="text-xs text-slate-400 dark:text-gray-600 hover:text-slate-600 dark:hover:text-gray-400 text-center py-2 transition-colors"
|
||||
>
|
||||
收起
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
import { useState } from 'react'
|
||||
import { createTask } from '../lib/api'
|
||||
import { useTaskStore } from '../store/tasks'
|
||||
|
||||
interface Props { onClose: () => void }
|
||||
|
||||
// 用户友好的需求类型(隐藏技术实现细节)
|
||||
const GOAL_TYPES = [
|
||||
{ value: 'fix_bug', label: '修复一个问题', desc: '描述您遇到的 Bug 或异常行为' },
|
||||
{ value: 'new_feature', label: '添加新功能', desc: '描述您希望新增的能力' },
|
||||
{ value: 'improve_perf', label: '提升性能', desc: '描述需要优化的性能瓶颈' },
|
||||
{ value: 'code_review', label: '代码审查', desc: '检查代码质量和潜在问题' },
|
||||
{ value: 'refactor', label: '代码优化', desc: '改善代码结构和可维护性' },
|
||||
{ value: 'documentation','label': '完善文档', desc: '更新 README 或补充注释' },
|
||||
{ value: 'tech_support', label: '技术攻关', desc: '向基础技术组请求专项调研或优化支持' },
|
||||
]
|
||||
|
||||
const TECH_ROLES = [
|
||||
{ value: 'rtp-researcher', label: 'RTP/RTCP 调研', desc: '实时传输协议、低延迟音视频' },
|
||||
{ value: 'net-researcher', label: '网络栈调优', desc: 'QoS/拥塞控制/多链路' },
|
||||
{ value: 'kernel-analyzer', label: '内核调度分析', desc: 'CPUFreq/NPU/多核调度' },
|
||||
{ value: 'os-engineer', label: 'OS 系统优化', desc: '系统层配置与驱动调优' },
|
||||
{ value: 'hw-engineer', label: '硬件工程', desc: '硬件接口/外设/信号分析' },
|
||||
]
|
||||
|
||||
export function NewTaskModal({ onClose }: Props) {
|
||||
const projects = useTaskStore(s => s.projects)
|
||||
const applyUpdate = useTaskStore(s => s.applyUpdate)
|
||||
const [form, setForm] = useState({
|
||||
project: projects[0]?.name ?? '',
|
||||
type: 'fix_bug',
|
||||
title: '',
|
||||
context: '',
|
||||
initiator: '',
|
||||
discussion: '',
|
||||
techRole: 'rtp-researcher',
|
||||
})
|
||||
const isTechSupport = form.type === 'tech_support'
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const selectedType = GOAL_TYPES.find(t => t.value === form.type)
|
||||
|
||||
async function submit(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
if (!form.title.trim()) { setError('请描述您的需求'); return }
|
||||
if (!form.project) { setError('请选择项目'); return }
|
||||
setLoading(true)
|
||||
try {
|
||||
const task = await createTask({
|
||||
project: form.project,
|
||||
type: form.type,
|
||||
title: form.title.trim(),
|
||||
context: form.context.trim(),
|
||||
priority: 2,
|
||||
mode: projects.find(p => p.name === form.project)?.mode ?? 'auto',
|
||||
agent_role: isTechSupport ? form.techRole : 'developer',
|
||||
initiator: form.initiator.trim() || 'user',
|
||||
discussion: form.discussion.trim(),
|
||||
queue: isTechSupport ? 'base_opt' : 'project_delivery',
|
||||
})
|
||||
applyUpdate(task)
|
||||
onClose()
|
||||
} catch (e) {
|
||||
setError(String(e))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (projects.length === 0) {
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center">
|
||||
<div className="bg-white dark:bg-gray-900 border border-slate-200 dark:border-gray-700 rounded-xl p-6 w-[420px] shadow-2xl text-center">
|
||||
<p className="text-slate-500 dark:text-gray-400 mb-4">请先导入一个项目</p>
|
||||
<button onClick={onClose}
|
||||
className="px-4 py-2 text-sm bg-slate-200 dark:bg-gray-700 hover:bg-slate-300 dark:hover:bg-gray-600 text-slate-800 dark:text-gray-200 rounded-lg">
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center">
|
||||
<div className="bg-white dark:bg-gray-900 border border-slate-200 dark:border-gray-700 rounded-xl p-6 w-[520px] shadow-2xl">
|
||||
<h2 className="text-base font-semibold text-slate-900 dark:text-gray-100 mb-1">提交开发需求</h2>
|
||||
<p className="text-xs text-slate-400 dark:text-gray-500 mb-5">
|
||||
AI 将根据您的描述自动规划并完成开发工作
|
||||
</p>
|
||||
|
||||
<form onSubmit={submit} className="space-y-4">
|
||||
{/* 项目选择 */}
|
||||
{projects.length > 1 && (
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 dark:text-gray-400 mb-1.5 block">选择项目</label>
|
||||
<select
|
||||
value={form.project}
|
||||
onChange={e => setForm(f => ({ ...f, project: e.target.value }))}
|
||||
className={inputCls}
|
||||
>
|
||||
{projects.map(p => (
|
||||
<option key={p.name} value={p.name}>{p.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 需求类型(图标化选择) */}
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 dark:text-gray-400 mb-2 block">需求类型</label>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{GOAL_TYPES.map(t => (
|
||||
<button
|
||||
key={t.value}
|
||||
type="button"
|
||||
onClick={() => setForm(f => ({ ...f, type: t.value }))}
|
||||
className={`px-3 py-2 text-xs rounded-lg text-left transition-colors border
|
||||
${form.type === t.value
|
||||
? 'bg-blue-900/50 border-blue-600 text-blue-200'
|
||||
: 'bg-slate-100 dark:bg-gray-800 border-slate-200 dark:border-gray-700 text-slate-500 dark:text-gray-400 hover:border-slate-300 dark:hover:border-gray-600'
|
||||
}`}
|
||||
>
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 技术角色(仅技术攻关) */}
|
||||
{isTechSupport && (
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 dark:text-gray-400 mb-2 block">指派给</label>
|
||||
<div className="grid grid-cols-1 gap-1.5">
|
||||
{TECH_ROLES.map(r => (
|
||||
<button
|
||||
key={r.value}
|
||||
type="button"
|
||||
onClick={() => setForm(f => ({ ...f, techRole: r.value }))}
|
||||
className={`px-3 py-2 text-xs rounded-lg text-left transition-colors border flex items-center gap-2
|
||||
${form.techRole === r.value
|
||||
? 'bg-amber-900/50 border-amber-600 text-amber-200'
|
||||
: 'bg-slate-100 dark:bg-gray-800 border-slate-200 dark:border-gray-700 text-slate-500 dark:text-gray-400 hover:border-slate-300 dark:hover:border-gray-600'
|
||||
}`}
|
||||
>
|
||||
<span className="font-medium">{r.label}</span>
|
||||
<span className="text-slate-400 dark:text-gray-600">{r.desc}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 需求描述 */}
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 dark:text-gray-400 mb-1.5 block">
|
||||
需求描述
|
||||
{selectedType && (
|
||||
<span className="text-slate-300 dark:text-gray-600 ml-1">— {selectedType.desc}</span>
|
||||
)}
|
||||
</label>
|
||||
<input
|
||||
value={form.title}
|
||||
onChange={e => setForm(f => ({ ...f, title: e.target.value }))}
|
||||
className={inputCls}
|
||||
placeholder="用一句话描述您的需求..."
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 发起人 */}
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 dark:text-gray-400 mb-1.5 block">
|
||||
发起人 <span className="text-slate-300 dark:text-gray-600">(您的姓名)</span>
|
||||
</label>
|
||||
<input
|
||||
value={form.initiator}
|
||||
onChange={e => setForm(f => ({ ...f, initiator: e.target.value }))}
|
||||
className={inputCls}
|
||||
placeholder="请填写您的姓名..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 沟通纪要 */}
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 dark:text-gray-400 mb-1.5 block">
|
||||
沟通纪要 <span className="text-slate-300 dark:text-gray-600">(可选,任务背景与决策依据)</span>
|
||||
</label>
|
||||
<textarea
|
||||
value={form.discussion}
|
||||
onChange={e => setForm(f => ({ ...f, discussion: e.target.value }))}
|
||||
rows={2}
|
||||
className={inputCls + ' resize-none'}
|
||||
placeholder="记录相关讨论、背景原因或决策依据..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 详细背景(可选) */}
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 dark:text-gray-400 mb-1.5 block">
|
||||
补充说明 <span className="text-slate-300 dark:text-gray-600">(可选)</span>
|
||||
</label>
|
||||
<textarea
|
||||
value={form.context}
|
||||
onChange={e => setForm(f => ({ ...f, context: e.target.value }))}
|
||||
rows={3}
|
||||
className={inputCls + ' resize-none'}
|
||||
placeholder="如有需要,可补充复现步骤、期望结果、相关文件路径等..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && <p className="text-xs text-red-400">{error}</p>}
|
||||
|
||||
<div className="flex justify-end gap-2 pt-1">
|
||||
<button type="button" onClick={onClose}
|
||||
className="px-4 py-2 text-sm text-slate-500 dark:text-gray-400 hover:text-slate-800 dark:hover:text-gray-200">
|
||||
取消
|
||||
</button>
|
||||
<button type="submit" disabled={loading}
|
||||
className="px-5 py-2 text-sm bg-blue-700 hover:bg-blue-600 text-white
|
||||
rounded-lg disabled:opacity-50 font-medium">
|
||||
{loading ? '提交中...' : '提交需求'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const inputCls = 'w-full bg-slate-100 dark:bg-gray-800 border border-slate-300 dark:border-gray-600 rounded-lg px-3 py-2 text-xs text-slate-800 dark:text-gray-200'
|
||||
@@ -0,0 +1,312 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useTaskStore } from '../store/tasks'
|
||||
import { approveTask, rejectTask, deleteProject, fetchProjects } from '../lib/api'
|
||||
import type { Task, Project } from '../types'
|
||||
|
||||
interface Props {
|
||||
onImport: () => void
|
||||
onSelectProject: (name: string) => void
|
||||
}
|
||||
|
||||
function timeAgo(iso: string | null): string {
|
||||
if (!iso) return ''
|
||||
const sec = Math.floor((Date.now() - new Date(iso + 'Z').getTime()) / 1000)
|
||||
if (sec < 60) return '刚刚'
|
||||
if (sec < 3600) return `${Math.floor(sec / 60)} 分钟前`
|
||||
if (sec < 86400) return `${Math.floor(sec / 3600)} 小时前`
|
||||
return `${Math.floor(sec / 86400)} 天前`
|
||||
}
|
||||
|
||||
export function ProjectOverview({ onImport, onSelectProject }: Props) {
|
||||
const tasksMap = useTaskStore(s => s.tasks)
|
||||
const projects = useTaskStore(s => s.projects)
|
||||
const applyUpdate = useTaskStore(s => s.applyUpdate)
|
||||
|
||||
const setProjects = useTaskStore(s => s.setProjects)
|
||||
|
||||
const { projectStats, approvalTasks } = useMemo(() => {
|
||||
const all = Array.from(tasksMap.values())
|
||||
const byProj = new Map<string, Task[]>()
|
||||
for (const t of all) {
|
||||
const arr = byProj.get(t.project) ?? []
|
||||
arr.push(t)
|
||||
byProj.set(t.project, arr)
|
||||
}
|
||||
|
||||
const projectStats = projects.map(p => {
|
||||
const tasks = byProj.get(p.name) ?? []
|
||||
const done = tasks.filter(t => t.status === 'done').length
|
||||
const failed = tasks.filter(t => t.status === 'failed').length
|
||||
const running = tasks.filter(t => t.status === 'running').length
|
||||
const pending = tasks.filter(t => t.status === 'pending').length
|
||||
const waiting = tasks.filter(t => t.status === 'waiting_approval').length
|
||||
const total = tasks.length
|
||||
const progress = total > 0 ? Math.round((done / total) * 100) : 0
|
||||
// 最近活动时间
|
||||
const lastTs = tasks
|
||||
.map(t => t.completed_at || t.started_at || t.created_at)
|
||||
.filter(Boolean)
|
||||
.sort()
|
||||
.pop() ?? null
|
||||
return { project: p, tasks, done, failed, running, pending, waiting, total, progress, lastTs }
|
||||
})
|
||||
|
||||
const approvalTasks = all.filter(t => t.status === 'waiting_approval')
|
||||
return { projectStats, approvalTasks }
|
||||
}, [tasksMap, projects])
|
||||
|
||||
// 空状态
|
||||
if (projects.length === 0) {
|
||||
return <EmptyState onImport={onImport} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-6xl mx-auto space-y-6">
|
||||
{/* 待审批横幅 */}
|
||||
{approvalTasks.length > 0 && (
|
||||
<ApprovalBanner
|
||||
tasks={approvalTasks}
|
||||
onApprove={async (id) => {
|
||||
const t = await approveTask(id)
|
||||
applyUpdate(t)
|
||||
}}
|
||||
onReject={async (id) => {
|
||||
const t = await rejectTask(id, '用户拒绝')
|
||||
applyUpdate(t)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 统计栏 */}
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
<StatCard label="项目总数" value={projects.length} color="text-blue-400" />
|
||||
<StatCard label="今日完成" value={projectStats.reduce((s, p) => s + p.done, 0)} color="text-green-400" />
|
||||
<StatCard label="进行中" value={projectStats.reduce((s, p) => s + p.running, 0)} color="text-sky-400" />
|
||||
<StatCard label="待处理" value={projectStats.reduce((s, p) => s + p.failed, 0)} color="text-red-400" />
|
||||
</div>
|
||||
|
||||
{/* 项目卡片网格 */}
|
||||
<div>
|
||||
<h2 className="text-sm font-medium text-slate-500 dark:text-gray-400 mb-3">所有项目</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{projectStats.map(s => (
|
||||
<ProjectCard
|
||||
key={s.project.name}
|
||||
data={s}
|
||||
onClick={() => onSelectProject(s.project.name)}
|
||||
onDelete={async () => {
|
||||
if (!confirm(`确认删除项目「${s.project.name}」?\n(任务记录不会被删除)`)) return
|
||||
await deleteProject(s.project.name)
|
||||
const updated = await fetchProjects()
|
||||
setProjects(updated)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{/* 添加项目 */}
|
||||
<button
|
||||
onClick={onImport}
|
||||
className="h-40 rounded-xl border-2 border-dashed border-slate-200 dark:border-gray-700 hover:border-slate-400 dark:hover:border-gray-500
|
||||
flex flex-col items-center justify-center gap-2 text-slate-400 dark:text-gray-600
|
||||
hover:text-slate-600 dark:hover:text-gray-400 transition-colors"
|
||||
>
|
||||
<span className="text-2xl">+</span>
|
||||
<span className="text-sm">添加项目</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── 空状态引导页 ──────────────────────────────────────────────────────
|
||||
|
||||
function EmptyState({ onImport }: { onImport: () => void }) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-[calc(100vh-120px)] gap-6 px-4">
|
||||
<div className="text-6xl opacity-30">🤖</div>
|
||||
<div className="text-center">
|
||||
<h2 className="text-xl font-semibold text-slate-800 dark:text-gray-200 mb-2">还没有项目</h2>
|
||||
<p className="text-sm text-slate-400 dark:text-gray-500 max-w-sm">
|
||||
导入一个本地项目目录,AI 将自动分析代码、提出改进方案并持续优化
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onImport}
|
||||
className="px-6 py-2.5 bg-blue-700 hover:bg-blue-600 text-white rounded-xl
|
||||
text-sm font-medium transition-colors shadow-lg shadow-blue-900/30"
|
||||
>
|
||||
导入第一个项目
|
||||
</button>
|
||||
<div className="flex gap-6 text-xs text-slate-400 dark:text-gray-600 mt-2">
|
||||
<span>✓ 自动代码审查</span>
|
||||
<span>✓ 持续优化改进</span>
|
||||
<span>✓ 实时进度跟踪</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── 项目卡片 ──────────────────────────────────────────────────────────
|
||||
|
||||
interface ProjectStat {
|
||||
project: Project
|
||||
done: number; failed: number; running: number; pending: number; waiting: number
|
||||
total: number; progress: number; lastTs: string | null
|
||||
}
|
||||
|
||||
function ProjectCard({ data, onClick, onDelete }: {
|
||||
data: ProjectStat; onClick: () => void; onDelete: () => void
|
||||
}) {
|
||||
const { project, done, failed, running, pending, waiting, progress, lastTs } = data
|
||||
const isActive = running > 0 || pending > 0
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="bg-white dark:bg-gray-900 border border-slate-200/60 dark:border-gray-700/60 hover:border-slate-400 dark:hover:border-gray-500 rounded-xl p-4
|
||||
text-left transition-all hover:shadow-lg hover:shadow-slate-300/50 dark:shadow-black/30 group"
|
||||
>
|
||||
{/* 头部:名称 + 状态 + 删除 */}
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="text-sm font-semibold text-slate-900 dark:text-gray-100 group-hover:text-blue-700 dark:group-hover:text-white truncate">
|
||||
{project.name}
|
||||
</h3>
|
||||
{project.description && (
|
||||
<p className="text-xs text-slate-400 dark:text-gray-500 mt-0.5 line-clamp-1">{project.description}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 shrink-0 ml-2">
|
||||
<StatusPill running={running} waiting={waiting} isActive={isActive} />
|
||||
<button
|
||||
onClick={e => { e.stopPropagation(); onDelete() }}
|
||||
className="text-slate-300 dark:text-gray-600 hover:text-red-400 transition-colors text-sm leading-none opacity-0 group-hover:opacity-100"
|
||||
title="删除项目"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 进度条 */}
|
||||
<div className="mb-3">
|
||||
<div className="flex justify-between text-xs text-slate-400 dark:text-gray-500 mb-1">
|
||||
<span>完成进度</span>
|
||||
<span>{progress}%</span>
|
||||
</div>
|
||||
<div className="h-1.5 bg-slate-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-blue-500 rounded-full transition-all duration-500"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计数字 */}
|
||||
<div className="flex gap-3 text-xs">
|
||||
<span className="text-green-400">{done} 完成</span>
|
||||
{failed > 0 && <span className="text-red-400">{failed} 失败</span>}
|
||||
{pending > 0 && <span className="text-slate-400 dark:text-gray-500">{pending} 待执行</span>}
|
||||
{waiting > 0 && <span className="text-orange-400 font-medium">⚠ {waiting} 待审批</span>}
|
||||
</div>
|
||||
|
||||
{/* 最近活动 */}
|
||||
{lastTs && (
|
||||
<p className="text-xs text-slate-400 dark:text-gray-600 mt-2">{timeAgo(lastTs)}</p>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function StatusPill({ running, waiting, isActive }: {
|
||||
running: number; waiting: number; isActive: boolean
|
||||
}) {
|
||||
if (waiting > 0) {
|
||||
return (
|
||||
<span className="text-[10px] px-2 py-0.5 rounded-full bg-orange-100 text-orange-700 border border-orange-300 dark:bg-orange-900/50 dark:text-orange-300 dark:border-orange-700/50 shrink-0">
|
||||
待审批
|
||||
</span>
|
||||
)
|
||||
}
|
||||
if (running > 0) {
|
||||
return (
|
||||
<span className="text-[10px] px-2 py-0.5 rounded-full bg-blue-100 text-blue-700 border border-blue-300 dark:bg-blue-900/50 dark:text-blue-300 dark:border-blue-700/50 shrink-0 flex items-center gap-1">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-blue-500 dark:bg-blue-400 animate-pulse inline-block" />
|
||||
运行中
|
||||
</span>
|
||||
)
|
||||
}
|
||||
if (isActive) {
|
||||
return (
|
||||
<span className="text-[10px] px-2 py-0.5 rounded-full bg-slate-200 dark:bg-gray-700 text-slate-600 dark:text-gray-400 shrink-0">
|
||||
排队中
|
||||
</span>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<span className="text-[10px] px-2 py-0.5 rounded-full bg-slate-100 dark:bg-gray-800 text-slate-400 dark:text-gray-600 shrink-0">
|
||||
待机
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
// ── 待审批横幅 ─────────────────────────────────────────────────────────
|
||||
|
||||
function ApprovalBanner({ tasks, onApprove, onReject }: {
|
||||
tasks: Task[]
|
||||
onApprove: (id: number) => Promise<void>
|
||||
onReject: (id: number) => Promise<void>
|
||||
}) {
|
||||
const first = tasks[0]
|
||||
return (
|
||||
<div className="bg-orange-50 border border-orange-200 dark:bg-orange-950/40 dark:border-orange-700/50 rounded-xl p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-orange-500 dark:text-orange-400 text-base">⚠</span>
|
||||
<span className="text-sm font-medium text-orange-700 dark:text-orange-200">
|
||||
{tasks.length} 个任务需要您的确认
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-slate-400 dark:text-gray-500">{first.project}</span>
|
||||
</div>
|
||||
<p className="text-sm text-slate-700 dark:text-gray-300 mb-3 line-clamp-2">{first.title}</p>
|
||||
{first.result_summary && (
|
||||
<p className="text-xs text-slate-500 dark:text-gray-400 bg-slate-100/60 dark:bg-gray-900/60 rounded p-2 mb-3 line-clamp-3">
|
||||
{first.result_summary}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => onApprove(first.id)}
|
||||
className="px-4 py-1.5 text-sm bg-green-800 hover:bg-green-700 text-green-100
|
||||
rounded-lg transition-colors"
|
||||
>
|
||||
允许执行
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onReject(first.id)}
|
||||
className="px-4 py-1.5 text-sm bg-slate-100 dark:bg-gray-800 hover:bg-slate-200 dark:hover:bg-gray-700 text-slate-700 dark:text-gray-300
|
||||
rounded-lg transition-colors"
|
||||
>
|
||||
拒绝
|
||||
</button>
|
||||
{tasks.length > 1 && (
|
||||
<span className="self-center text-xs text-slate-400 dark:text-gray-500 ml-1">
|
||||
还有 {tasks.length - 1} 个待确认
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── 统计卡片 ──────────────────────────────────────────────────────────
|
||||
|
||||
function StatCard({ label, value, color }: { label: string; value: number; color: string }) {
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-900 border border-slate-200/60 dark:border-gray-700/60 rounded-xl px-4 py-3">
|
||||
<div className={`text-2xl font-bold ${color}`}>{value}</div>
|
||||
<div className="text-xs text-slate-400 dark:text-gray-500 mt-0.5">{label}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import type { Task } from '../types'
|
||||
import { STATUS_COLORS, ROLE_COLORS } from '../types'
|
||||
import { useTaskStore } from '../store/tasks'
|
||||
|
||||
interface Props { task: Task }
|
||||
|
||||
function elapsed(startedAt: string | null): string {
|
||||
if (!startedAt) return ''
|
||||
const sec = Math.floor((Date.now() - new Date(startedAt + 'Z').getTime()) / 1000)
|
||||
if (sec < 60) return `${sec}s`
|
||||
if (sec < 3600) return `${Math.floor(sec / 60)}m`
|
||||
return `${Math.floor(sec / 3600)}h ${Math.floor((sec % 3600) / 60)}m`
|
||||
}
|
||||
|
||||
const TYPE_LABELS: Record<string, string> = {
|
||||
fix_bug: '修复问题',
|
||||
improve_perf: '性能优化',
|
||||
code_review: '代码审查',
|
||||
new_feature: '新功能',
|
||||
arch_optimize: '架构优化',
|
||||
refactor: '代码重构',
|
||||
test_improvement: '测试完善',
|
||||
documentation: '文档更新',
|
||||
performance: '性能分析',
|
||||
integration: '项目集成',
|
||||
architecture: '架构评估',
|
||||
device_test: '设备测试',
|
||||
architect: '架构分析',
|
||||
}
|
||||
|
||||
const PRIORITY_BADGE: Record<number, { label: string; cls: string }> = {
|
||||
1: { label: 'P1', cls: 'bg-red-500/20 text-red-400 border border-red-500/30' },
|
||||
2: { label: 'P2', cls: 'bg-orange-500/20 text-orange-400 border border-orange-500/30' },
|
||||
3: { label: 'P3', cls: 'bg-yellow-500/20 text-yellow-500 border border-yellow-500/30' },
|
||||
4: { label: 'P4', cls: 'bg-blue-500/20 text-blue-400 border border-blue-500/30' },
|
||||
5: { label: 'P5', cls: 'bg-slate-500/20 text-slate-400 border border-slate-500/30' },
|
||||
}
|
||||
|
||||
export function TaskCard({ task }: Props) {
|
||||
const setSelected = useTaskStore(s => s.setSelectedTask)
|
||||
const colorClass = STATUS_COLORS[task.status] ?? 'border-slate-300 dark:border-gray-600 bg-slate-300/10 dark:bg-gray-600/10'
|
||||
|
||||
const typeLabel = TYPE_LABELS[task.type] ?? task.type
|
||||
const isRunning = task.status === 'running'
|
||||
const needsApproval = task.status === 'waiting_approval'
|
||||
const pri = PRIORITY_BADGE[task.priority] ?? PRIORITY_BADGE[3]
|
||||
const roleCls = ROLE_COLORS[task.agent_role] ?? 'bg-slate-500'
|
||||
const initiatorShort = (task.initiator || 'system').split(':').pop()?.slice(0, 8) ?? 'system'
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`rounded-lg border p-3 cursor-pointer hover:opacity-90 transition-opacity ${colorClass}`}
|
||||
onClick={() => setSelected(task.id)}
|
||||
>
|
||||
{/* 行1: 优先级 + 类型 + 待确认标记 */}
|
||||
<div className="flex items-center gap-1.5 mb-2">
|
||||
<span className={`text-[10px] font-bold px-1.5 py-0.5 rounded font-mono ${pri.cls}`}>
|
||||
{pri.label}
|
||||
</span>
|
||||
<span className="text-[10px] text-slate-400 dark:text-gray-500 bg-slate-200/60 dark:bg-gray-700/60 px-1.5 py-0.5 rounded">
|
||||
{typeLabel}
|
||||
</span>
|
||||
{needsApproval && (
|
||||
<span className="text-[10px] text-orange-700 bg-orange-100 dark:text-orange-300 dark:bg-orange-900/40 px-1.5 py-0.5 rounded font-medium ml-auto">
|
||||
待确认
|
||||
</span>
|
||||
)}
|
||||
{task.retry_count > 0 && (
|
||||
<span className="text-[10px] text-slate-400 dark:text-gray-500 ml-auto">×{task.retry_count}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 行2: 标题 */}
|
||||
<p className="text-sm text-slate-800 dark:text-gray-200 font-medium line-clamp-2 mb-2 leading-snug">
|
||||
{task.title}
|
||||
</p>
|
||||
|
||||
{/* 行3: 沟通纪要摘要(如有) */}
|
||||
{task.discussion && (
|
||||
<p className="text-[10px] text-slate-400 dark:text-gray-600 italic line-clamp-1 mb-2 border-l-2 border-slate-300 dark:border-gray-700 pl-1.5">
|
||||
{task.discussion}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* 结果预览(done 状态) */}
|
||||
{task.status === 'done' && task.result_summary && (
|
||||
<p className="text-xs text-slate-400 dark:text-gray-500 line-clamp-1 mb-2">
|
||||
{task.result_summary}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* 行4: 发起人 → 执行者 + 计时 */}
|
||||
<div className="flex items-center justify-between mt-1">
|
||||
<div className="flex items-center gap-1 text-[10px] text-slate-400 dark:text-gray-600">
|
||||
<span className="font-mono">{initiatorShort}</span>
|
||||
<span>→</span>
|
||||
<span className={`px-1.5 py-0.5 rounded text-white text-[10px] font-mono ${roleCls}`}>
|
||||
{task.agent_role}
|
||||
</span>
|
||||
</div>
|
||||
{isRunning && task.started_at && (
|
||||
<div className="flex items-center gap-1 text-[10px] text-blue-400">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-blue-400 animate-pulse inline-block" />
|
||||
<span>{elapsed(task.started_at)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useTaskStore } from '../store/tasks'
|
||||
import type { Toast as ToastItem } from '../store/tasks'
|
||||
|
||||
const TOAST_DURATION = 5000
|
||||
|
||||
function ToastBubble({ toast }: { toast: ToastItem }) {
|
||||
const removeToast = useTaskStore(s => s.removeToast)
|
||||
const setSelected = useTaskStore(s => s.setSelectedTask)
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => removeToast(toast.id), TOAST_DURATION)
|
||||
return () => clearTimeout(timer)
|
||||
}, [toast.id, removeToast])
|
||||
|
||||
const colorCls =
|
||||
toast.type === 'success' ? 'bg-green-900/90 border-green-700 text-green-100' :
|
||||
toast.type === 'error' ? 'bg-red-900/90 border-red-700 text-red-100' :
|
||||
toast.type === 'warning' ? 'bg-orange-900/90 border-orange-700 text-orange-100' :
|
||||
'bg-slate-100/90 dark:bg-gray-800/90 border-slate-300 dark:border-gray-600 text-slate-900 dark:text-gray-100'
|
||||
|
||||
function handleClick() {
|
||||
if (toast.taskId != null) setSelected(toast.taskId)
|
||||
removeToast(toast.id)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center justify-between gap-3 px-4 py-2.5 rounded-lg border
|
||||
text-xs shadow-lg cursor-pointer animate-in slide-in-from-right ${colorCls}`}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<span className="leading-snug">{toast.message}</span>
|
||||
<button
|
||||
onClick={e => { e.stopPropagation(); removeToast(toast.id) }}
|
||||
className="opacity-60 hover:opacity-100 text-base leading-none shrink-0"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ToastContainer() {
|
||||
const toasts = useTaskStore(s => s.toasts)
|
||||
if (toasts.length === 0) return null
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-4 right-4 z-[100] flex flex-col gap-2 max-w-[360px]">
|
||||
{toasts.map(t => <ToastBubble key={t.id} toast={t} />)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
fetchVisualRuns, fetchVisualCases, approveVisualCase, rejectVisualCase,
|
||||
type VisualRun, type VisualCase,
|
||||
} from '../lib/api'
|
||||
|
||||
const STATUS_BADGE: Record<string, string> = {
|
||||
passed: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
||||
failed: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
|
||||
needs_review: 'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200',
|
||||
running: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
|
||||
pending: 'bg-slate-100 text-slate-600 dark:bg-gray-800 dark:text-slate-400',
|
||||
skipped: 'bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-400',
|
||||
done: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
||||
interrupted: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
|
||||
}
|
||||
|
||||
function RunRow({ run, selected, onClick }: {
|
||||
run: VisualRun
|
||||
selected: boolean
|
||||
onClick: () => void
|
||||
}) {
|
||||
const pct = run.total > 0 ? Math.round(run.passed / run.total * 100) : 0
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={`cursor-pointer px-4 py-3 border-b border-slate-200 dark:border-gray-700
|
||||
hover:bg-slate-50 dark:hover:bg-gray-800 ${selected ? 'bg-blue-50 dark:bg-blue-900/20' : ''}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium text-sm">{run.project}</span>
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full ${STATUS_BADGE[run.status] ?? ''}`}>
|
||||
{run.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 flex items-center gap-3 text-xs text-slate-500 dark:text-slate-400">
|
||||
<span>{run.started_at.slice(0, 16)}</span>
|
||||
<span>{run.passed}/{run.total} passed</span>
|
||||
{run.pending_review > 0 && (
|
||||
<span className="text-amber-600 dark:text-amber-400">⚠ {run.pending_review} 待审批</span>
|
||||
)}
|
||||
</div>
|
||||
{run.total > 0 && (
|
||||
<div className="mt-1.5 h-1 bg-slate-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
||||
<div className="h-full bg-green-500" style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type ImageKey = 'baseline_path' | 'screenshot_path' | 'diff_path'
|
||||
|
||||
function CaseRow({ c, onApprove, onReject }: {
|
||||
c: VisualCase
|
||||
onApprove: (id: number) => void
|
||||
onReject: (id: number) => void
|
||||
}) {
|
||||
const imageKeys: ImageKey[] = ['baseline_path', 'screenshot_path', 'diff_path']
|
||||
const labels = ['基线', '当前截图', '差异']
|
||||
|
||||
return (
|
||||
<div className="border border-slate-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<span className="text-sm font-medium">{c.name}</span>
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400 ml-2">{c.url}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{c.diff_pct != null && (
|
||||
<span className="text-xs text-slate-500">diff {c.diff_pct.toFixed(2)}%</span>
|
||||
)}
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full ${STATUS_BADGE[c.status] ?? ''}`}>
|
||||
{c.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{imageKeys.map((key, i) => {
|
||||
const path = c[key]
|
||||
return (
|
||||
<div key={key} className="text-center">
|
||||
<div className="text-xs text-slate-500 dark:text-slate-400 mb-1">{labels[i]}</div>
|
||||
{path ? (
|
||||
<img
|
||||
src={`/api/visual/image?path=${encodeURIComponent(path)}`}
|
||||
alt={labels[i]}
|
||||
className="w-full h-32 object-cover rounded border border-slate-200 dark:border-gray-700"
|
||||
onError={e => { (e.target as HTMLImageElement).style.display = 'none' }}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-32 bg-slate-100 dark:bg-gray-800 rounded flex items-center justify-center text-xs text-slate-400">
|
||||
无
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{c.status === 'needs_review' && (
|
||||
<div className="flex gap-2 mt-3">
|
||||
<button
|
||||
onClick={() => onApprove(c.id)}
|
||||
className="flex-1 py-1.5 text-xs font-medium bg-green-600 hover:bg-green-700 text-white rounded transition-colors"
|
||||
>
|
||||
✓ 批准(更新基线)
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onReject(c.id)}
|
||||
className="flex-1 py-1.5 text-xs font-medium bg-red-600 hover:bg-red-700 text-white rounded transition-colors"
|
||||
>
|
||||
✗ 拒绝(保留旧基线)
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function VisualView() {
|
||||
const [runs, setRuns] = useState<VisualRun[]>([])
|
||||
const [selectedRun, setSelectedRun] = useState<string | null>(null)
|
||||
const [cases, setCases] = useState<VisualCase[]>([])
|
||||
|
||||
const pendingReviewTotal = runs.reduce((s, r) => s + r.pending_review, 0)
|
||||
|
||||
const loadRuns = () => fetchVisualRuns().then(setRuns).catch(console.warn)
|
||||
|
||||
useEffect(() => {
|
||||
loadRuns()
|
||||
}, [])
|
||||
|
||||
async function loadCases(runId: string) {
|
||||
setSelectedRun(runId)
|
||||
const cs = await fetchVisualCases(runId)
|
||||
setCases(cs)
|
||||
}
|
||||
|
||||
async function handleApprove(caseId: number) {
|
||||
await approveVisualCase(caseId)
|
||||
if (selectedRun) await loadCases(selectedRun)
|
||||
loadRuns()
|
||||
}
|
||||
|
||||
async function handleReject(caseId: number) {
|
||||
await rejectVisualCase(caseId)
|
||||
if (selectedRun) await loadCases(selectedRun)
|
||||
loadRuns()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full">
|
||||
{/* 左侧:运行列表 */}
|
||||
<div className="w-72 flex-shrink-0 border-r border-slate-200 dark:border-gray-700 overflow-y-auto">
|
||||
<div className="sticky top-0 bg-white dark:bg-gray-950 px-4 py-3 border-b border-slate-200 dark:border-gray-700">
|
||||
<h2 className="text-sm font-semibold">视觉测试运行</h2>
|
||||
{pendingReviewTotal > 0 && (
|
||||
<p className="text-xs text-amber-600 dark:text-amber-400 mt-0.5">
|
||||
⚠ {pendingReviewTotal} 个截图待审批
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{runs.length === 0 ? (
|
||||
<div className="p-4 text-sm text-slate-400">暂无运行记录</div>
|
||||
) : (
|
||||
runs.map(r => (
|
||||
<RunRow
|
||||
key={r.id}
|
||||
run={r}
|
||||
selected={selectedRun === r.id}
|
||||
onClick={() => loadCases(r.id)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 右侧:case 列表 */}
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{!selectedRun ? (
|
||||
<div className="flex items-center justify-center h-full text-slate-400 text-sm">
|
||||
选择左侧运行记录查看测试详情
|
||||
</div>
|
||||
) : cases.length === 0 ? (
|
||||
<div className="p-4 text-sm text-slate-400">该次运行暂无测试用例</div>
|
||||
) : (
|
||||
<div className="grid gap-4">
|
||||
{cases.map(c => (
|
||||
<CaseRow key={c.id} c={c} onApprove={handleApprove} onReject={handleReject} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
background-color: #f8fafc;
|
||||
color: #1e293b;
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
.dark body {
|
||||
background-color: #0f172a;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
@keyframes pulse-ring {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.running-pulse {
|
||||
animation: pulse-ring 1.5s ease-in-out infinite;
|
||||
}
|
||||
@@ -16,7 +16,7 @@ export async function fetchProjects() {
|
||||
export async function createTask(body: {
|
||||
project: string; type: string; title: string;
|
||||
priority?: number; mode?: string; agent_role?: string; context?: string;
|
||||
initiator?: string; discussion?: string
|
||||
initiator?: string; discussion?: string; queue?: string
|
||||
}) {
|
||||
const res = await fetch(`${BASE}/tasks`, {
|
||||
method: 'POST',
|
||||
@@ -243,3 +243,22 @@ export async function fetchMarketHistory(): Promise<MarketHistoryItem[]> {
|
||||
if (!res.ok) throw new Error(`fetchMarketHistory failed: ${res.status}`)
|
||||
return res.json()
|
||||
}
|
||||
|
||||
// ── Research Facts API ─────────────────────────────────────────────
|
||||
export interface ResearchFacts {
|
||||
domains: Record<string, Record<string, string>> // domain -> {sub_key -> value}
|
||||
}
|
||||
|
||||
export async function fetchResearchFacts(): Promise<ResearchFacts> {
|
||||
const res = await fetch(`${BASE}/research/facts`)
|
||||
if (!res.ok) throw new Error(`fetchResearchFacts failed: ${res.status}`)
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export async function refreshClaudeMd(project: string): Promise<{ id: number; title: string }> {
|
||||
const res = await fetch(`${BASE}/projects/${encodeURIComponent(project)}/refresh-claude-md`, {
|
||||
method: 'POST',
|
||||
})
|
||||
if (!res.ok) throw new Error(await res.text())
|
||||
return res.json()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useTaskStore } from '../store/tasks'
|
||||
|
||||
const WS_URL = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/ws`
|
||||
|
||||
let ws: WebSocket | null = null
|
||||
let retryDelay = 1000
|
||||
|
||||
export function connectWS() {
|
||||
const store = useTaskStore.getState()
|
||||
|
||||
ws = new WebSocket(WS_URL)
|
||||
store.setWsStatus('connecting')
|
||||
|
||||
ws.onopen = () => {
|
||||
store.setWsStatus('connected')
|
||||
retryDelay = 1000
|
||||
}
|
||||
|
||||
ws.onmessage = (ev) => {
|
||||
try {
|
||||
const msg = JSON.parse(ev.data)
|
||||
if (msg.type === 'snapshot') {
|
||||
store.applySnapshot(msg.tasks)
|
||||
} else if (msg.type === 'task_update' || msg.type === 'task_create') {
|
||||
store.applyUpdate(msg.task)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('WS parse error', e)
|
||||
}
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
store.setWsStatus('reconnecting')
|
||||
setTimeout(() => {
|
||||
retryDelay = Math.min(retryDelay * 2, 30000)
|
||||
connectWS()
|
||||
}, retryDelay)
|
||||
}
|
||||
|
||||
ws.onerror = () => {
|
||||
store.setWsStatus('offline')
|
||||
ws?.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
@@ -0,0 +1,94 @@
|
||||
import { create } from 'zustand'
|
||||
import type { Task, Project } from '../types'
|
||||
|
||||
export interface Toast {
|
||||
id: string
|
||||
message: string
|
||||
type: 'success' | 'error' | 'warning' | 'info'
|
||||
taskId?: number
|
||||
}
|
||||
|
||||
interface TaskStore {
|
||||
tasks: Map<number, Task>
|
||||
projects: Project[]
|
||||
wsStatus: 'connecting' | 'connected' | 'reconnecting' | 'offline'
|
||||
selectedTaskId: number | null
|
||||
activeProject: string | null // null = 全览
|
||||
toasts: Toast[]
|
||||
theme: 'dark' | 'light'
|
||||
|
||||
// actions
|
||||
applySnapshot: (tasks: Task[]) => void
|
||||
applyUpdate: (task: Task) => void
|
||||
setProjects: (projects: Project[]) => void
|
||||
setWsStatus: (s: TaskStore['wsStatus']) => void
|
||||
setSelectedTask: (id: number | null) => void
|
||||
setActiveProject: (p: string | null) => void
|
||||
removeProjectTasks: (project: string) => void
|
||||
removeToast: (id: string) => void
|
||||
toggleTheme: () => void
|
||||
}
|
||||
|
||||
const _savedTheme = (localStorage.getItem('theme') as 'dark' | 'light') || 'dark'
|
||||
document.documentElement.classList.toggle('dark', _savedTheme === 'dark')
|
||||
|
||||
export const useTaskStore = create<TaskStore>((set) => ({
|
||||
tasks: new Map(),
|
||||
projects: [],
|
||||
wsStatus: 'connecting',
|
||||
selectedTaskId: null,
|
||||
activeProject: null,
|
||||
toasts: [],
|
||||
theme: _savedTheme,
|
||||
|
||||
applySnapshot: (tasks) => {
|
||||
const m = new Map<number, Task>()
|
||||
tasks.forEach(t => m.set(t.id, t))
|
||||
set({ tasks: m })
|
||||
},
|
||||
|
||||
applyUpdate: (task) => {
|
||||
set(state => {
|
||||
const prev = state.tasks.get(task.id)
|
||||
const m = new Map(state.tasks)
|
||||
m.set(task.id, task)
|
||||
|
||||
// 检测状态转换,生成通知
|
||||
const toasts = [...state.toasts]
|
||||
if (prev && prev.status !== task.status) {
|
||||
const id = `${task.id}-${task.status}-${Date.now()}`
|
||||
const name = task.title.length > 30
|
||||
? task.title.slice(0, 30) + '…'
|
||||
: task.title
|
||||
if (task.status === 'done') {
|
||||
toasts.push({ id, type: 'success', message: `✓ 完成:${name}`, taskId: task.id })
|
||||
} else if (task.status === 'failed') {
|
||||
toasts.push({ id, type: 'error', message: `✕ 失败:${name}`, taskId: task.id })
|
||||
} else if (task.status === 'waiting_approval') {
|
||||
toasts.push({ id, type: 'warning', message: `⚠ 待审批:${name}`, taskId: task.id })
|
||||
}
|
||||
}
|
||||
|
||||
return { tasks: m, toasts }
|
||||
})
|
||||
},
|
||||
|
||||
setProjects: (projects) => set({ projects }),
|
||||
setWsStatus: (wsStatus) => set({ wsStatus }),
|
||||
setSelectedTask: (selectedTaskId) => set({ selectedTaskId }),
|
||||
setActiveProject: (activeProject) => set({ activeProject }),
|
||||
removeProjectTasks: (project) => set(state => {
|
||||
const m = new Map(state.tasks)
|
||||
for (const [id, task] of m) {
|
||||
if (task.project === project) m.delete(id)
|
||||
}
|
||||
return { tasks: m, selectedTaskId: null }
|
||||
}),
|
||||
removeToast: (id) => set(s => ({ toasts: s.toasts.filter(t => t.id !== id) })),
|
||||
toggleTheme: () => set(s => {
|
||||
const next = s.theme === 'dark' ? 'light' : 'dark'
|
||||
document.documentElement.classList.toggle('dark', next === 'dark')
|
||||
localStorage.setItem('theme', next)
|
||||
return { theme: next }
|
||||
}),
|
||||
}))
|
||||
@@ -0,0 +1,74 @@
|
||||
export interface Task {
|
||||
id: number
|
||||
project: string
|
||||
type: string
|
||||
title: string
|
||||
context: string
|
||||
priority: number
|
||||
mode: string
|
||||
agent_role: string
|
||||
status: 'pending' | 'running' | 'done' | 'failed' | 'waiting_approval' |
|
||||
'vetoed' | 'adversarial_pending' | 'waiting_confirm'
|
||||
created_at: string
|
||||
started_at: string | null
|
||||
completed_at: string | null
|
||||
result_summary: string
|
||||
retry_count: number
|
||||
parent_task_id: number
|
||||
constraint_type: string
|
||||
awaiting_role: string
|
||||
initiator: string
|
||||
discussion: string
|
||||
}
|
||||
|
||||
export interface Project {
|
||||
name: string
|
||||
mode: string
|
||||
kind: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export const STATUS_COLUMNS = [
|
||||
'pending', 'running', 'done', 'failed', 'waiting_approval', 'vetoed'
|
||||
] as const
|
||||
|
||||
export const STATUS_LABELS: Record<string, string> = {
|
||||
pending: '待执行',
|
||||
running: '运行中',
|
||||
done: '完成',
|
||||
failed: '失败',
|
||||
waiting_approval: '待审批',
|
||||
vetoed: '已否决',
|
||||
adversarial_pending: '对抗中',
|
||||
waiting_confirm: '等确认',
|
||||
}
|
||||
|
||||
export const STATUS_COLORS: Record<string, string> = {
|
||||
pending: 'border-gray-500 bg-gray-500/10',
|
||||
running: 'border-blue-500 bg-blue-500/10 running-pulse',
|
||||
done: 'border-green-500/50 bg-green-500/5 opacity-70',
|
||||
failed: 'border-red-500 bg-red-500/10',
|
||||
waiting_approval: 'border-orange-500 bg-orange-500/10',
|
||||
vetoed: 'border-purple-500 bg-purple-500/10',
|
||||
adversarial_pending: 'border-yellow-500 bg-yellow-500/10',
|
||||
waiting_confirm: 'border-cyan-500 bg-cyan-500/10',
|
||||
}
|
||||
|
||||
export const ROLE_COLORS: Record<string, string> = {
|
||||
'developer': 'bg-blue-600',
|
||||
'dev-embedded': 'bg-blue-500',
|
||||
'dev-kernel': 'bg-indigo-600',
|
||||
'dev-lowlevel': 'bg-indigo-500',
|
||||
'dev-ml': 'bg-violet-600',
|
||||
'dev-backend': 'bg-cyan-600',
|
||||
'dev-test': 'bg-teal-600',
|
||||
'dev-refactor': 'bg-slate-600',
|
||||
'hw-engineer': 'bg-amber-600',
|
||||
'tester': 'bg-teal-500',
|
||||
'arch-system': 'bg-rose-600',
|
||||
'arch-security':'bg-red-600',
|
||||
'arch-reviewer':'bg-pink-600',
|
||||
'pm': 'bg-fuchsia-600',
|
||||
'productizer': 'bg-emerald-600',
|
||||
'architect': 'bg-rose-500',
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./index.html', './src/**/*.{ts,tsx}'],
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
pending: '#6b7280',
|
||||
running: '#3b82f6',
|
||||
done: '#22c55e',
|
||||
failed: '#ef4444',
|
||||
waiting: '#f97316',
|
||||
vetoed: '#a855f7',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require('@tailwindcss/typography')],
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"types": ["vite/client"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': 'http://localhost:8080',
|
||||
'/ws': { target: 'ws://localhost:8080', ws: true },
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,40 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
dashboard:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.dashboard
|
||||
image: nmfs-agents-dashboard:latest
|
||||
container_name: agents-dashboard
|
||||
ports:
|
||||
- "9080:8080"
|
||||
volumes:
|
||||
- ./data:/app/data # 共享 tasks.db(SQLite WAL 模式并发安全)
|
||||
- ./configs:/app/configs # 读取项目配置
|
||||
- /data:/data # 宿主机项目目录(用于读写项目 CLAUDE.md/README.md)
|
||||
- /home/qiurui/.claude:/home/qiurui/.claude # 全局 CLAUDE.md + OAuth 认证
|
||||
- /home/qiurui/.claude.json:/home/qiurui/.claude.json # Claude CLI 配置
|
||||
- /home/qiurui/.local:/home/qiurui/.local # claude CLI 二进制
|
||||
- /data/company/visual-tester:/app/visual-tester
|
||||
environment:
|
||||
- DB_PATH=/app/data/tasks.db
|
||||
- CONFIG_DIR=/app/configs
|
||||
- DASHBOARD_PORT=8080
|
||||
- CLAUDE_GLOBAL_DIR=/home/qiurui/.claude
|
||||
- HOME=/home/qiurui
|
||||
- PATH=/home/qiurui/.local/bin:/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin
|
||||
- http_proxy=http://172.17.0.1:8889/
|
||||
- https_proxy=http://172.17.0.1:8889/
|
||||
- HTTP_PROXY=http://172.17.0.1:8889/
|
||||
- HTTPS_PROXY=http://172.17.0.1:8889/
|
||||
- ALL_PROXY=socks://172.17.0.1:1089/
|
||||
- all_proxy=socks://172.17.0.1:1089/
|
||||
- NO_PROXY=localhost,127.0.0.1,172.17.0.0/16
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "-c",
|
||||
"import urllib.request; urllib.request.urlopen('http://localhost:8080/api/tasks')"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
@@ -0,0 +1,123 @@
|
||||
# AI安全领域市场深度分析报告
|
||||
|
||||
> 更新时间:2026-03-10 | 分析人:Market-PM Agent | 平台:RK3588 NPU
|
||||
|
||||
---
|
||||
|
||||
## 一、市场规模
|
||||
|
||||
| 维度 | 数据 |
|
||||
|------|------|
|
||||
| 全球 AI 视频监控市场(2025) | USD 59.8 亿(另一口径:$65.1亿) |
|
||||
| 全球 AI 视频监控市场(2030 预测) | USD 117.6–287.6 亿,CAGR 14.46%–30.6% |
|
||||
| 全球视频监控总市场(2025) | USD 387.4 亿,2026 年 $425.4 亿 |
|
||||
| 全球视频监控总市场(2035 预测) | USD 2,673.9 亿,CAGR 12.26% |
|
||||
| 全球边缘计算市场(2025) | USD 2,650 亿,2029 年接近翻倍 |
|
||||
| 中国智能安防市场(2028 预测) | 突破 2,000 亿元,AI 安防占比>50% |
|
||||
| 中国安防市场(海康+大华合计) | 国内市占超 70% |
|
||||
| 亚太地区 | 增速最高,中国/日本/韩国领跑 |
|
||||
|
||||
---
|
||||
|
||||
## 二、竞品全景(≥5 家)
|
||||
|
||||
### 硬件/平台层
|
||||
|
||||
| # | 厂商 | 产品 | 定价 | 核心技术 | 市场地位 |
|
||||
|---|------|------|------|---------|---------|
|
||||
| 1 | **海康威视** | DeepinView AI 摄像头/边缘盒子 | 消费摄像头¥250–1,000;AI 深眸¥5,000+ | 全球市占 23%,海思 Hi3516DV300 NPU 集成,20+ 风险行为实时预警 | 全球第一 |
|
||||
| 2 | **大华股份** | WizMind/WizSense 系列 | 商用定价 | 全球市占 10.5%,AI 算法+前端智能摄像机 | 全球第二 |
|
||||
| 3 | **宇视科技** | AI Smart 系列 | 商用定价 | 全栈安防产品,与阿里云深度合作 | 国内第三 |
|
||||
| 4 | **萤石(海康子公司)** | 家用 AI 摄像头 | ¥149–299 | 消费级人形识别、跌倒检测、一键呼叫 | 国内家用第一 |
|
||||
| 5 | **小米生态链** | 米家摄像头 | ¥199–499 | 人脸识别、人形预警、AI 降噪 | 中低端市占领先 |
|
||||
|
||||
### 算法/AI 层
|
||||
|
||||
| # | 厂商 | 定位 | 核心能力 |
|
||||
|---|------|------|---------|
|
||||
| 1 | **商汤科技** | 视觉 AI 算法 | SenseVideo 城市级视频分析、大模型支持 |
|
||||
| 2 | **旷视科技** | 人脸/视频 AI | 人脸识别全球领先,Brain++平台 |
|
||||
| 3 | **云从科技** | 金融/安防算法 | 行为分析、轨迹追踪,与海康深度合作 |
|
||||
| 4 | **华为云(Ascend)** | AI 算力+安防生态 | 商汤/旷视/依图均是华为安防生态伙伴 |
|
||||
|
||||
### 边缘计算芯片/盒子层
|
||||
|
||||
| 方案 | 算力 | 定价 | 场景 |
|
||||
|------|------|------|------|
|
||||
| 海思 Hi3516DV300 | ~0.5 TOPS | 模组¥50–200 | 端侧摄像头 NPU |
|
||||
| NVIDIA Jetson Nano | ~0.5 TOPS | $149 | 开发者/高端摄像头 |
|
||||
| NVIDIA Jetson TX1 | ~1 TOPS | $299+ | 高端 AI 摄像头(海康深眸¥5000+) |
|
||||
| **RK3588(本平台)** | **6 TOPS INT8** | 模块 ¥800–1,200 | NPU 算力是海思方案 10x+ |
|
||||
|
||||
---
|
||||
|
||||
## 三、近期动态(2025年9月–2026年3月)
|
||||
|
||||
| 时间 | 事件 | 影响 |
|
||||
|------|------|------|
|
||||
| 2026.05(预告) | 杭州安博会发布《2026-2028 全球智能安防产业发展白皮书》 | 行业趋势指向大模型+边缘 AI 融合 |
|
||||
| 2026.01 | 中国智能安防市场预测 2028 年突破 2000 亿,AI 占比>50% | AI 渗透安防进入加速拐点 |
|
||||
| 2025 | 边缘 AI 渗透率仍不足 1%,巨头加速投入 | 蓝海窗口期,先发优势明显 |
|
||||
| 2025 | 全球边缘计算 $2650 亿,AI 驱动近翻倍增长(IDC) | 边缘算力需求量级提升 |
|
||||
| 2023 | 安博会:大模型应用扎堆,AI 摄像头满场 | 行业已完成 AI 化初步认知教育 |
|
||||
|
||||
---
|
||||
|
||||
## 四、用户痛点
|
||||
|
||||
| 痛点 | 类型 | 描述 |
|
||||
|------|------|------|
|
||||
| **算法授权费高昂** | B/G 端 | 商汤/旷视算法 SaaS 按通道数收费,中小客户难以承受 |
|
||||
| **私有化部署难** | B 端 | 云端方案数据安全顾虑;工厂/机构不允许数据出厂 |
|
||||
| **边缘算力不足** | 技术 | 主流边缘摄像头 NPU ≤1 TOPS,复杂场景精度差 |
|
||||
| **多厂商不兼容** | 运维 | 海康/大华/宇视协议碎片化,统一管理成本高 |
|
||||
| **实施周期长** | B/G 端 | 传统安防项目动辄 3–6 个月,快速交付需求未满足 |
|
||||
| **误报率高** | 终端用户 | AI 行为分析误报导致报警疲劳,运维人力浪费 |
|
||||
|
||||
---
|
||||
|
||||
## 五、RK3588 NPU 差异化机会
|
||||
|
||||
### 平台算力优势
|
||||
| 对比维度 | RK3588 NPU | 海思 Hi3516DV300 | Jetson TX1 |
|
||||
|---------|-----------|----------------|-----------|
|
||||
| NPU 算力 | **6 TOPS INT8** | ~0.5 TOPS | ~1 TOPS |
|
||||
| 成本(模块) | ¥800–1,200 | ¥50–200 | $299+ |
|
||||
| 算力/成本比 | ★★★★★ | ★★★ | ★★★ |
|
||||
| 已有模型 | YOLO+MediaPipe+Embedding | - | - |
|
||||
|
||||
### 3 大切入机会
|
||||
|
||||
#### 机会 1:中小企业私有化 AI 安防盒子(最高优先级)
|
||||
- **痛点**:海康/大华私有化 AI 盒子 ¥5,000–50,000,算法 SaaS 按通道收费贵
|
||||
- **方案**:RK3588 + 已有 YOLO + 行为分析算法,一次性硬件 ¥3,000–8,000 包含 5 路分析
|
||||
- **竞争优势**:算力 10x(vs 海思方案),无月费,私有化本地部署
|
||||
- **目标客户**:中小工厂、仓储、商超、物业
|
||||
|
||||
#### 机会 2:适老场景 AI 安全监控(与适老领域协同)
|
||||
- **方案**:养老院/社区 AI 跌倒检测+异常行为分析,合规隐私保护(深度图而非 RGB)
|
||||
- **差异化**:专项适老算法(骨骼+行为),与适老领域产品协同,一盒多用
|
||||
|
||||
#### 机会 3:运动场馆安防+分析一体(与运动领域协同)
|
||||
- **方案**:运动场馆 AI 摄像头,安防+动作分析双功能,单台设备实现双价值
|
||||
- **差异化**:安防厂商只做安全,本方案额外输出训练数据,提升客户价值
|
||||
|
||||
### 技术路径
|
||||
```
|
||||
摄像头 RTSP → RK3588 NPU 推理 → YOLO(多目标检测)
|
||||
→ 行为异常分析(本地规则引擎)
|
||||
→ MediaPipe(姿态检测,适老/运动)
|
||||
→ Embedding(人员 Re-ID)
|
||||
→ 本地告警 + HTTP API → 监控平台
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、切入建议
|
||||
|
||||
1. **算力降维打击**:以 6 TOPS vs 竞品 0.5 TOPS 作为核心卖点
|
||||
2. **私有化本地**:无月费、无云依赖、一次性交付,打破算法 SaaS 订阅壁垒
|
||||
3. **横向协同**:运动+适老+安全三场景共用一套推理平台,降低边际成本
|
||||
4. **先做 B 端**:中小工厂/物业/养老院,避开海康/大华的渠道体系
|
||||
|
||||
*参考来源:Fortune Business Insights、Grand View Research、Mordor Intelligence、佰泰盛世科技、智东西*
|
||||
@@ -0,0 +1,112 @@
|
||||
# 适老领域市场深度分析报告
|
||||
|
||||
> 更新时间:2026-03-10 | 分析人:Market-PM Agent | 平台:RK3588 NPU
|
||||
|
||||
---
|
||||
|
||||
## 一、市场规模
|
||||
|
||||
| 维度 | 数据 |
|
||||
|------|------|
|
||||
| 全球 AI 养老市场规模(2025) | USD 56.78 亿 |
|
||||
| 全球 AI 养老市场规模(2035 预测) | USD 387.52 亿,CAGR 21.3% |
|
||||
| 中国智能养老设备市场(2024) | 1,560 亿元 |
|
||||
| 中国智能养老设备市场(2029 预测) | 3,420 亿元,CAGR 17%(2024-2029) |
|
||||
| 中国养老机器人市场(2024) | 79 亿元,预计 2025 年 91 亿元 |
|
||||
| 智能护理机器人(2025 预测) | 突破 300 亿元,年均增长>20%;2035 年突破万亿元 |
|
||||
| 中国 60 岁以上人口 | >3 亿(占总人口 22%),65岁以上跌倒率 28~42% |
|
||||
| 中国独居老人数量 | 约 3,000 万 |
|
||||
| 全球跌倒检测设备市场(2025) | USD 14.1 亿,2034 年 USD 41.8 亿 |
|
||||
|
||||
---
|
||||
|
||||
## 二、竞品全景(≥5 家)
|
||||
|
||||
### 国内竞品
|
||||
|
||||
| # | 厂商 | 产品/方案 | 定价区间 | 技术特点 | 目标用户 |
|
||||
|---|------|----------|---------|---------|---------|
|
||||
| 1 | **华为** | WATCH D2/全屋 AI 康养传感器 | 手表¥1,099–3,299;传感器未公开 | ECG+血压+鸿蒙闭环;全屋传感器卫生间跌倒/卧室坠床检测;与优必选/智元联合推康养人形机器人(2025.05) | 银发+全年龄,24.5% 市场份额 |
|
||||
| 2 | **小米** | 小米手环 H1(适老款) | ¥399–799 | 一键 SOS、跌倒检测、血压/心率异常预警 | 60 岁以上独居老人 |
|
||||
| 3 | **爱牵挂** | 老人定位手环/手表 | ¥199–599 设备 + 年服务费 | GPS 定位+SOS+健康监测,SaaS 平台 | 居家/机构老人 |
|
||||
| 4 | **德心小宝** | 5D 毫米波 + AI 养老机器人 | ¥2 万+ | 非接触式,5D 毫米波+AI,申报 CFDA;目标 2025 年营收 1 亿,2026 年冲刺港股 | 养老机构/社区 |
|
||||
| 5 | **猎户星空(豹小秘)** | 豹小秘 Mini 养老机器人 | ¥2–5 万 | 视觉+4D 雷达融合,3秒跌倒预警,FHIR 医疗数据链路 | 医疗机构/高端养老 |
|
||||
| 6 | **麦迪科技** | 康养护理机器人(联合优必选) | 未公开(面向机构) | 7 大功能:情感陪伴/苏醒刺激/健康监测等;傅利叶/优必选机器人+麦迪临床数据 | 医院/康复/养老院 |
|
||||
| 7 | **傅利叶智能** | GR-1(科研)/ GR-3 Care-bot(陪伴) | ¥3–14 万 | 通用人形机器人,康复机器人+外骨骼助行;占养老机器人市场 55% 康复方向 | 康复机构/养老机构 |
|
||||
|
||||
### 国际竞品
|
||||
|
||||
| # | 厂商 | 产品 | 定价 | 特点 |
|
||||
|---|------|------|------|------|
|
||||
| 1 | **Apple Watch S10** | 适老跌倒检测 | USD 399+ | 碰撞+跌倒检测,AI 升级 S10 芯片,误报率降低 |
|
||||
| 2 | **Teton.ai(美国)** | AI 预测性护理平台 | SaaS 机构定价 | 2025.09 Series A $2000 万 |
|
||||
| 3 | **Panasonic** | 居家 AI 跌倒检测系统 | 商用定价 | 2026.01 发布,日本先发 |
|
||||
|
||||
---
|
||||
|
||||
## 三、近期动态(2025年9月–2026年3月)
|
||||
|
||||
| 时间 | 事件 | 影响 |
|
||||
|------|------|------|
|
||||
| 2026.01 | Panasonic 推出居家 AI 跌倒检测系统(日本) | 国际巨头入场,证明场景可行性 |
|
||||
| 2025.11 | 中国适老化智能手环销售额同比增速超 200%;智能手表超 350% | 银发适老硬件已进入爆发增长期 |
|
||||
| 2025.05 | 华为+优必选+智元机器人+中坚科技合作康养人形机器人 | 头部企业联合布局,行业加速整合 |
|
||||
| 2025.09 | Teton.ai Series A $2000 万(AI 预测性老年护理) | 海外资本验证智能看护 SaaS 模式 |
|
||||
| 2025.10 | 阿里《2025 银发+AI 应用趋势报告》:老年人深度参与 AI | 老年用户对 AI 接受度大幅提升 |
|
||||
| 2025.07 | 田园之家(居家智慧养老)完成 A 轮融资 | 居家养老 SaaS 赛道融资活跃 |
|
||||
|
||||
---
|
||||
|
||||
## 四、用户痛点
|
||||
|
||||
| 痛点 | 影响人群 | 现有方案局限 |
|
||||
|------|---------|------------|
|
||||
| **跌倒安全风险** | 65岁以上跌倒率 28~42% | 可穿戴接受度低(老人不愿戴);摄像头侵犯隐私 |
|
||||
| **独居无人陪伴** | 3000 万独居老人 | 定时呼叫成本高;子女无法实时感知 |
|
||||
| **穿戴设备难用** | 高龄老人 | 操作复杂、字体小、充电遗忘 |
|
||||
| **指纹门锁失效** | 皮肤干燥老人 | 指纹磨损误识率高,掌静脉/人脸成刚需 |
|
||||
| **隐私数据泄露** | 全量老人 | 云端上传健康数据监管风险 |
|
||||
| **慢病管理断层** | 高血压患者(2.45亿,60岁以上占50%+) | 数据孤岛,医疗-家庭无法联通 |
|
||||
| **设备成本过高** | 机构采购方 | 毫米波方案 ¥2万+,高端机器人 ¥3–20万,性价比不足 |
|
||||
|
||||
---
|
||||
|
||||
## 五、RK3588 NPU 差异化机会
|
||||
|
||||
### 平台能力基准
|
||||
| 能力 | 参数 |
|
||||
|------|------|
|
||||
| NPU 算力 | 6 TOPS INT8,三核并行最高 1.9x |
|
||||
| 姿态估计(mediapipe) | 33 关键点骨架,板端实时推理 |
|
||||
| 视觉推理 | YOLO 多人检测 30+ FPS |
|
||||
| 成本优势 | NanoPC-T6(RK3588)模块 ¥800–1,200 |
|
||||
|
||||
### 差异化机会矩阵
|
||||
|
||||
| 场景 | 痛点 | RK3588 方案 | 优势 |
|
||||
|------|------|------------|------|
|
||||
| **非接触跌倒检测** | 摄像头泄露隐私,毫米波成本高(¥2万+) | 深度摄像头+mediapipe 骨骼估计,边缘推理,数据不出设备 | 隐私保护 + 成本 ¥2,000–5,000 |
|
||||
| **卧室/卫生间监护** | 传统摄像头无法合规部署 | 深度图+骨骼检测(无 RGB 画面传输) | 合规可部署私密场所 |
|
||||
| **睡眠监测** | 毫米波精度差,商用设备贵 | 毫米波+视觉融合,RK3588 并行推理 | 多模态精度,成本可控 |
|
||||
| **摔倒后自动呼叫** | 需要手动 SOS | NPU 实时姿态异常→本地拨号 | 端侧自动化,无网络依赖 |
|
||||
| **异常行为检测** | 需 7×24 人工看护 | 长时间不动/异常行为时序分析,本地告警 | 替代人工看护 |
|
||||
|
||||
### 产品 BOM 估算
|
||||
```
|
||||
[居家适老 AI 盒子] = RK3588 + 深度摄像头 + 本地推理
|
||||
- RK3588 核心板(NanoPC-T6 类):¥800–1,200
|
||||
- 深度摄像头(OAK-D Lite 类):¥600–900
|
||||
- 外壳 + 电源:¥100–200
|
||||
- 合计 BOM:¥1,500–2,300(量产可降至 ¥1,000 以内)
|
||||
- 竞品对比:德心小宝(毫米波方案)¥2万+,猎户豹小秘 ¥2–5万
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、切入建议
|
||||
|
||||
1. **主打"隐私保护+边缘推理"**,避开华为/小米可穿戴红海
|
||||
2. **B 端先行**(养老机构/社区),积累数据与 CFDA 资质 → C 端扩展
|
||||
3. **技术 MVP**:基于现有 mediapipe + RK3588,快速验证跌倒检测准确率(目标 >95%)
|
||||
|
||||
*参考来源:AgeClub、新浪财经、东方财富、OFweek 可穿戴网、Precedence Research*
|
||||
@@ -0,0 +1,134 @@
|
||||
# 适老领域 AI 市场深度分析报告
|
||||
|
||||
> 生成日期:2026-03-09 | 作者:Market-PM Agent | 版本:v1.0
|
||||
|
||||
---
|
||||
|
||||
## 1. 市场概况
|
||||
|
||||
| 维度 | 数据 |
|
||||
|------|------|
|
||||
| 全球 AI 养老市场规模(2025) | USD 56.78 亿 |
|
||||
| 全球 AI 养老市场规模(2035 预测) | USD 387.52 亿 |
|
||||
| CAGR(2026-2035) | 21.30% |
|
||||
| 中国 60 岁以上人口 | >3 亿(占总人口 22%) |
|
||||
| 中国独居老人数量 | 约 3000 万 |
|
||||
| 全球跌倒检测设备市场(2025) | USD 14.1 亿 |
|
||||
| 全球跌倒检测设备市场(2034 预测) | USD 41.8 亿 |
|
||||
| 中国智能腕戴市场(2025) | 7100 万台,同比 +13.1% |
|
||||
|
||||
---
|
||||
|
||||
## 2. 竞品全景(≥5 家)
|
||||
|
||||
### 2.1 国内竞品
|
||||
|
||||
| # | 厂商 | 产品/方案 | 定价区间 | 技术特点 | 目标用户 |
|
||||
|---|------|----------|---------|---------|---------|
|
||||
| 1 | **华为** | WATCH D2 / Watch GT 系列 | ¥1,099~3,299 | ECG+血压+鸿蒙生态闭环,"腕戴-手机-家居-医疗"全场景 | 银发 + 全年龄段,24.5% 市场份额 |
|
||||
| 2 | **小米** | 小米手环 H1(适老款) | ¥399~799 | 一键 SOS,跌倒检测,血压/心率异常预警 | 60 岁以上独居老人 |
|
||||
| 3 | **爱牵挂** | 老人定位手环/手表 | ¥199~599/年服务费 | GPS 定位 + SOS + 健康监测,SaaS 平台 | 居家/机构老人 |
|
||||
| 4 | **德心小宝** | 5D 毫米波 + AI 养老机器人 | 约 ¥2 万+ | 5D 毫米波雷达+AI 算法,非接触式,2025 年申报 CFDA | 养老机构 / 社区 |
|
||||
| 5 | **云米科技** | 心肺监测雷达 | ¥1,500~3,000 | 24GHz 毫米波,无感呼吸/心率监测,无需穿戴 | 家庭安装 |
|
||||
| 6 | **猎户星空(豹小秘)** | 豹小秘 Mini 养老机器人 | ¥2~5 万 | 视觉+4D雷达融合,3秒跌倒预警,FHIR 医疗数据链路 | 医疗机构/高端养老 |
|
||||
| 7 | **国脉科技** | AI 居家养老智能体 | 政府采购定价 | 大模型+居家服务调度,主打 G 端 | 政府/街道养老站 |
|
||||
|
||||
### 2.2 国际竞品
|
||||
|
||||
| # | 厂商 | 产品 | 定价(美元) | 特点 |
|
||||
|---|------|------|------------|------|
|
||||
| 1 | **Teton.ai** | AI 预测性护理平台 | SaaS 机构定价 | 2025.09 获 Series A $2000万 |
|
||||
| 2 | **Panasonic** | AI 跌倒检测系统 | 商用定价 | 2026.01 发布,家用版本 |
|
||||
| 3 | **Cera Care(英国)** | 家庭医疗 AI | 订阅制 | 看护人员调度+AI 监测 |
|
||||
| 4 | **Apple Watch** | Series 10 血糖监测 | USD 399+ | 无创血糖(误差<5%),AI 跌倒检测 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 近 6 个月重要动态(2025.09 ~ 2026.03)
|
||||
|
||||
### 3.1 技术发布
|
||||
- **2026.01** Panasonic 推出居家 AI 跌倒检测系统(日本市场先发)
|
||||
- **2025.11** 中国智能腕戴全年预计 7100 万台,银发适老化产品增速 200~350%
|
||||
- **2025.10** 阿里巴巴发布《2025 银发+AI 应用趋势报告》,老年人 AI 使用深度参与者比例上升
|
||||
- **2025.09** 掌静脉门锁因适老优势销售额同比增长 150%,指纹识别被替代趋势明显
|
||||
|
||||
### 3.2 融资动态
|
||||
- **2025.09** Teton.ai(美国)Series A 融资 $2000 万,专注 AI 预测性老年护理
|
||||
- **2025年** 欧盟拨款 €5000 万用于养老机构 AI 整合项目
|
||||
- **2025.07** 田园之家(居家智慧养老)完成 A 轮,金沙江联合资本领投
|
||||
- **德心小宝** 目标 2025 年营收 1 亿,2026 年冲刺港股 Pre-IPO
|
||||
|
||||
---
|
||||
|
||||
## 4. 用户痛点调研
|
||||
|
||||
### 4.1 核心痛点(重要度排序)
|
||||
|
||||
| 痛点 | 影响人群 | 现有方案局限 |
|
||||
|------|---------|------------|
|
||||
| **跌倒安全风险** | 65岁以上跌倒率 28~42% | 可穿戴接受度低(老人不愿戴);摄像头侵犯隐私 |
|
||||
| **独居无人陪伴** | 3000 万独居老人 | 定时呼叫人力成本高;子女无法实时感知 |
|
||||
| **穿戴设备难用** | 认知能力下降的高龄老人 | 操作复杂、字体小、充电难(遗忘) |
|
||||
| **指纹门锁失效** | 皮肤干燥、指纹磨损老人 | 指纹锁误识率高 → 掌静脉/人脸成刚需 |
|
||||
| **隐私数据泄露** | 全量老人 | 云端上传健康数据存在风险(监管加强) |
|
||||
| **慢病管理断层** | 高血压患者(2.45亿,60岁以上占50%+) | 数据孤岛,医疗-家庭无法联通 |
|
||||
| **认知障碍走失** | 阿尔茨海默症老人 | GPS 定位精度不足室内使用 |
|
||||
|
||||
### 4.2 社区/论坛调研发现
|
||||
- 子女最关注:**跌倒实时报警** > 健康数据查看 > 位置确认
|
||||
- 老人本人最关注:**不想被监视感**(隐私)、操作简单、不额外充电
|
||||
- 机构采购关注:数据合规、CFDA 资质、与现有护理系统集成
|
||||
|
||||
---
|
||||
|
||||
## 5. RK3588 NPU 差异化机会分析
|
||||
|
||||
### 5.1 平台能力基准(基于已有项目事实)
|
||||
|
||||
| 能力 | 参数 |
|
||||
|------|------|
|
||||
| NPU 算力 | 6 TOPS(INT8),三核并行最高 1.9x 加速 |
|
||||
| 视觉推理 | YOLO v8n 30+ FPS,姿态估计(mediapipe)实时 |
|
||||
| 向量检索 | embedding 本地化,<10ms 延迟 |
|
||||
| 内存/算力比 | 远优于树莓派 4B,接近工作站级嵌入式 |
|
||||
|
||||
### 5.2 差异化机会矩阵
|
||||
|
||||
| 场景 | 现有痛点 | RK3588 方案 | 竞争优势 |
|
||||
|------|---------|------------|---------|
|
||||
| **非接触跌倒检测** | 摄像头泄露隐私,毫米波成本高 | 深度摄像头+骨骼估计(mediapipe)边缘推理,数据不出设备 | 隐私保护 + 成本低于毫米波方案 |
|
||||
| **卧室/卫生间监护** | 传统摄像头无法部署 | 深度图+骨骼检测(无 RGB 画面传输) | 合规可部署私密场所 |
|
||||
| **掌静脉门禁** | 老人指纹识别失败率高 | 红外+静脉纹路 CNN,NPU 加速推理<50ms | 替代方案,无需云端验证 |
|
||||
| **睡眠监测** | 单纯毫米波精度差,商用设备贵 | 毫米波+视觉融合,RK3588 并行推理 | 多模态精度高,成本可控 |
|
||||
| **摔倒后自动呼叫** | 需要手动 SOS | NPU 实时姿态分析→异常触发→本地拨号 | 端侧自动化,无网络依赖 |
|
||||
| **独居老人异常行为** | 需要 7×24 人工看护 | 行为时序分析(长时间不动/异常)本地触发告警 | 替代人工看护,成本极低 |
|
||||
|
||||
### 5.3 可行产品形态
|
||||
|
||||
```
|
||||
[居家适老 AI 盒子] = RK3588 + 深度摄像头 + 本地推理
|
||||
├── 跌倒检测(mediapipe 骨骼估计)→ 误报率 <5%
|
||||
├── 睡眠呼吸监测(可选毫米波模块)
|
||||
├── 异常行为检测(长时间不动 / 徘徊)
|
||||
├── 隐私保护:所有推理本地,不上传视频
|
||||
└── 告警通道:本地 WiFi → 子女 App / 护理平台
|
||||
|
||||
BOM 估算(初版):
|
||||
- RK3588 核心板(NanoPC-T6 类):¥800~1200
|
||||
- 深度摄像头(OAK-D Lite 类):¥600~900
|
||||
- 外壳 + 电源:¥100~200
|
||||
- 合计 BOM:¥1500~2300(量产可降至 ¥1000 以内)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 结论与建议
|
||||
|
||||
1. **市场时机成熟**:银发经济政策加持 + 人口结构红利,适老 AI 设备进入快速增长期
|
||||
2. **差异化定位**:主打"隐私保护+边缘推理",避开华为/小米的可穿戴红海
|
||||
3. **切入路径**:B 端先行(养老机构/社区)→ 积累数据与 CFDA 资质 → C 端扩展
|
||||
4. **技术 MVP**:基于现有 mediapipe + RK3588,快速验证跌倒检测准确率(目标 >95%)
|
||||
|
||||
---
|
||||
|
||||
*参考来源:AgeClub、OFweek 可穿戴网、前瞻研究院、InsightAce Analytics、Precedence Research*
|
||||
@@ -0,0 +1,96 @@
|
||||
# 运动领域市场深度分析报告
|
||||
|
||||
> 更新时间:2026-03-10 | 分析人:Market-PM Agent | 平台:RK3588 NPU
|
||||
|
||||
---
|
||||
|
||||
## 一、竞品全景(7 家核心玩家)
|
||||
|
||||
| 品牌/产品 | 定价区间 | 用户规模 | 核心技术特点 | 盈利模式 |
|
||||
|---------|--------|--------|------------|--------|
|
||||
| **Apple Watch S10/Ultra2** | $399–$799(国内¥2999起),Ultra2 $799 | 全球可穿戴市占 23% | S10/S9 芯片、ECG/血氧/摔倒检测、AI Siri 整合、跑步功率 | 硬件+Fitness+ 订阅 |
|
||||
| **Garmin Fenix 8** | $999–$1,199(国内¥6800–8700) | 2026年Q2运动线收入$6.05亿(YoY +41%) | 专业运动模式 50+、TopoActive 地图、VO2 Max/体力预测、Elevate Gen5 传感器 | 纯硬件高毛利 |
|
||||
| **华为 Watch GT5 Pro** | ¥2,699–3,999 | 全球可穿戴市占 17%(Q1 2025),出货同比+36% | 连续血氧+ECG+盘古大模型健康问答、鸿蒙生态闭环,心率追踪接近 Garmin Fenix 8 水准 | 硬件(生态锁定) |
|
||||
| **Amazfit T-Rex Ultra 2** | $299–$399 | 快速增长,TikTok 营销覆盖 800万次曝光 | Garmin 专业功能约一半价格,耐用性强,运动姿态分析 | 纯硬件 |
|
||||
| **WHOOP 5.0** | 硬件免费 + $30/月订阅 | 约 500 万付费会员 | 持续 HRV/睡眠/恢复评分;Advanced Labs 血液生物标志物(65项)整合;无屏幕极简 | 纯 SaaS 订阅 |
|
||||
| **Oura Ring 4** | 硬件$349 + $5.99/月 | 累计 550 万枚;2025 年营收$10亿(YoY +100%) | 睡眠/经期/准备度评分;AI 顾问聊天机器人;Dexcom 血糖整合 | 硬件+轻订阅 |
|
||||
| **Keep(Kinetic.ai)** | 会员¥25/月;硬件¥599–2,999 | 月活 3000 万+;2024 年营收¥20.66亿,H1 2025 扭亏为盈 | 垂直运动大模型;AI 教练"卡卡"姿态识别+个性化计划;线上课 | 订阅+硬件+广告 |
|
||||
|
||||
### 补充玩家
|
||||
- **小米 Redmi Band/Watch**:Q1 2025 出货 870 万台(全球第一),¥199–599,主打下沉市场
|
||||
- **COROS/Suunto/Polar**:专业耐力运动(铁三/越野跑),$300–700,GPS 精度和续航领先
|
||||
- **RingConn**:智能戒指,70% 品牌知名度(2025.01),舒适佩戴,专注健康监测
|
||||
|
||||
---
|
||||
|
||||
## 二、近期重要动态(2025年9月–2026年3月)
|
||||
|
||||
| 时间 | 事件 | 影响 |
|
||||
|------|------|------|
|
||||
| 2026.02 | Garmin 运动线 Q2 收入 $6.05亿,同比 +41% | 专业运动市场快速扩张,抢夺 Apple 高端份额 |
|
||||
| 2025.11 | 中国智能腕戴全年达 7100 万台(历史新高) | 国内运动健康穿戴持续大规模增长 |
|
||||
| 2025.09 | WHOOP 上线 Advanced Labs(65项血液生物标志物+AI整合) | 运动健康进入医疗级生物标志物赛道 |
|
||||
| 2025.08 | Keep H1 扭亏为盈,调整净利润¥1035万,毛利率 52.2% | AI 驱动运动 SaaS 模式已跑通盈利 |
|
||||
| 2025.03 | Keep 上线 AI 教练"卡卡",基于 Kinetic.ai 大模型 | 国内运动 AI 教练首发,3000万月活覆盖 |
|
||||
| 2025 | Oura 累计融资超$5亿,估值约$50亿 | 轻订阅可穿戴已是十亿美元级产品 |
|
||||
|
||||
## 市场规模
|
||||
- 全球运动手表市场(2024):**$97.55 亿**,预计 2031 年 $173 亿(CAGR 8.6%)
|
||||
- 中国智能健身市场(2025 预测):**¥820 亿**
|
||||
- 全球智能手表市场(2025):**$320 亿**,预计 2033 年 $795 亿
|
||||
|
||||
---
|
||||
|
||||
## 三、用户痛点
|
||||
|
||||
| 痛点 | 严重程度 | 描述 |
|
||||
|------|---------|------|
|
||||
| **续航 vs 功能取舍** | ★★★★★ | 开 GPS 后 Garmin 续航仅 15h,Apple Watch 不足 24h |
|
||||
| **AI/传感器精准度不足** | ★★★★☆ | 心率较准,血氧/血压仅供参考,ECG 无法诊断 |
|
||||
| **专项运动识别偏差** | ★★★★☆ | 骑行距离偏差大,球类运动(羽毛球/篮球)识别率低 |
|
||||
| **个性化训练不足** | ★★★☆☆ | 建议通用化,不考虑疲劳状态和历史水平 |
|
||||
| **订阅费用叠加** | ★★★☆☆ | 设备本身价高,完整功能还需额外订阅 |
|
||||
| **专业分析系统价格门槛高** | ★★★★★ | 商用 AI 运动分析系统(商汤/海康)¥10万+,中小场馆无法承受 |
|
||||
| **云端隐私顾虑** | ★★★☆☆ | 云端视频分析让用户担忧运动数据安全 |
|
||||
|
||||
---
|
||||
|
||||
## 四、RK3588 NPU 差异化机会分析
|
||||
|
||||
### 平台核心能力(实测数据)
|
||||
| 能力 | 规格 |
|
||||
|------|------|
|
||||
| NPU 算力 | 6 TOPS INT8(三核并行 1.3~1.9x 加速) |
|
||||
| MediaPipe Pose | 已完成板端部署(/data/company/mediapipe),33 关键点实时骨架 |
|
||||
| YOLO 目标检测 | 已完成板端部署(/data/company/yolo),多人实时检测 |
|
||||
| Embedding 向量检索 | 已完成(/data/company/embedding),动作序列比对 |
|
||||
| 视频处理 | 硬解码 8K@60fps,H.265/H.264 |
|
||||
|
||||
### 4 大差异化机会
|
||||
|
||||
#### 机会 1:中小场馆智能运动分析终端(最高优先级)
|
||||
- **痛点**:商用方案 ¥10万+,中小场馆负担不起;云端有隐私风险
|
||||
- **方案**:RK3588 本地推理(MediaPipe 关键点 + YOLO 多人检测),设备成本 ¥3,000–8,000/台
|
||||
- **竞争壁垒**:全本地、低延迟(<100ms)、无订阅、无隐私风险
|
||||
- **目标客户**:社区球场、中学体育馆、健身房、球类俱乐部
|
||||
|
||||
#### 机会 2:视觉辅助纠姿系统(高优先级)
|
||||
- **痛点**:手环传感器无法捕捉动作质量,实时反馈延迟大
|
||||
- **方案**:固定摄像头 + RK3588 实时姿态分析,与穿戴数据融合(心率+骨架联合分析)
|
||||
- **差异化**:弥补纯手环无视觉盲区,提供"教练级"实时动作反馈
|
||||
|
||||
#### 机会 3:本地 AI 运动教练盒子(中优先级)
|
||||
- **痛点**:Keep 等 App 需联网、订阅费高
|
||||
- **方案**:RK3588 离线推理盒,含标准化动作库(150+ 动作),家庭/企业健身房使用
|
||||
- **商业模式**:硬件一次性购买 + 动作包扩展
|
||||
|
||||
#### 机会 4:运动视频自动精彩剪辑(中优先级)
|
||||
- **方案**:RK3588 本地视频事件检测(YOLO + 事件识别),自动导出精彩片段
|
||||
- **对标**:影石 AI 高光助手(依赖手机),本方案完全本地化
|
||||
|
||||
---
|
||||
|
||||
## 五、可复用模块
|
||||
`摄像头RTSP` → `YOLO 多人检测` → `MediaPipe 骨架提取` → `Embedding 动作分析` → `本地展示/API`
|
||||
|
||||
*参考来源:洛图科技、Tom's Guide、TechRadar、新浪财经、Keep 财报*
|
||||
@@ -0,0 +1,222 @@
|
||||
# Rockchip Agents 系统设计文档
|
||||
|
||||
**日期**: 2026-02-27
|
||||
**状态**: 已批准,待实施
|
||||
|
||||
---
|
||||
|
||||
## 1. 目标
|
||||
|
||||
构建一个 AI Agent 系统,通过管理 `/data/rockchip/` 下的模型相关项目(yolo / mediapipe / embedding / info-privacy / logodetect-x86),使用 Claude API 持续进行综合优化(bug 修复、性能提升、功能迭代、代码质量),并支持飞书通知与交互。
|
||||
|
||||
---
|
||||
|
||||
## 2. 整体架构
|
||||
|
||||
### 2.1 核心循环
|
||||
|
||||
```
|
||||
APScheduler(每小时)
|
||||
└─→ Scanner 扫描所有项目(CLAUDE.md / 测试结果 / TODO)
|
||||
└─→ 生成任务 → 写入 SQLite 任务队列
|
||||
└─→ Manager Agent 调度
|
||||
├─→ asyncio 并发分发(最多 N 个并发)
|
||||
└─→ 专门 Agent 执行(Developer / Tester / ...)
|
||||
└─→ 飞书通知结果
|
||||
```
|
||||
|
||||
### 2.2 Agent 角色体系
|
||||
|
||||
```
|
||||
Manager Agent(项目经理)
|
||||
│ 协调工作、分配任务、跟踪进度、飞书汇报
|
||||
│
|
||||
├── Developer Agent(开发者)
|
||||
│ 修复 bug、实现功能、重构代码
|
||||
│ 工具:read_file, write_file, run_tests
|
||||
│
|
||||
├── Tester Agent(测试者)
|
||||
│ 验证修复、运行基准、板端部署测试
|
||||
│ 工具:run_local_tests, ssh_device, collect_benchmark
|
||||
│
|
||||
├── Productizer Agent(产品化)
|
||||
│ 评估成熟度、完善文档/API/配置、打包发布
|
||||
│ 工具:read_project, write_docs, check_release_readiness
|
||||
│
|
||||
└── Architect Agent(架构师)
|
||||
跨项目分析 → 识别整合机会 → 定义统一产品架构
|
||||
安排预研任务(如:yolo + embedding → 视觉语义搜索)
|
||||
工具:analyze_all_projects, create_research_task, write_architecture_doc
|
||||
```
|
||||
|
||||
每个角色 = 不同的 System Prompt + 工具集 + 决策权限。
|
||||
|
||||
---
|
||||
|
||||
## 3. 组件设计
|
||||
|
||||
### 3.1 目录结构
|
||||
|
||||
```
|
||||
src/rockchip_agents/
|
||||
├── agents/
|
||||
│ ├── manager.py # Manager Agent(调度协调)
|
||||
│ ├── developer.py # Developer Agent(代码修改)
|
||||
│ ├── tester.py # Tester Agent(测试验证)
|
||||
│ ├── productizer.py # Productizer Agent(产品化)
|
||||
│ └── architect.py # Architect Agent(架构整合)
|
||||
├── core/
|
||||
│ ├── scheduler.py # APScheduler 定时入口
|
||||
│ ├── scanner.py # 项目扫描器
|
||||
│ ├── queue.py # 任务队列(SQLite)
|
||||
│ └── executor.py # 并发执行协调(asyncio)
|
||||
├── tools/
|
||||
│ ├── file_tools.py # read_file / write_file
|
||||
│ ├── test_tools.py # run_tests / collect_results
|
||||
│ ├── device_agent.py # SSH 设备管理(部署/板端测试)
|
||||
│ └── feishu.py # 飞书通知 + 交互卡片
|
||||
└── config.py # 配置加载
|
||||
configs/
|
||||
├── projects.yaml # 被管理项目列表及设置
|
||||
├── agents.yaml # 全局配置(API Key、飞书、并发数)
|
||||
└── devices.yaml # 设备 SSH 配置
|
||||
tests/
|
||||
scripts/
|
||||
└── run.py # 启动入口
|
||||
```
|
||||
|
||||
### 3.2 任务队列(SQLite)
|
||||
|
||||
```sql
|
||||
CREATE TABLE tasks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
project TEXT NOT NULL,
|
||||
type TEXT NOT NULL, -- fix_bug | improve_perf | add_feature | code_review | productize | architect
|
||||
title TEXT NOT NULL,
|
||||
context TEXT, -- 相关代码/日志片段
|
||||
priority INTEGER, -- 1(严重)→ 5(低)
|
||||
mode TEXT, -- auto | confirm | report
|
||||
agent_role TEXT, -- developer | tester | productizer | architect
|
||||
status TEXT DEFAULT 'pending', -- pending | running | done | failed | waiting_confirm
|
||||
created_at DATETIME,
|
||||
started_at DATETIME,
|
||||
completed_at DATETIME,
|
||||
result_summary TEXT
|
||||
);
|
||||
```
|
||||
|
||||
### 3.3 三种执行模式
|
||||
|
||||
| 模式 | Agent 行为 | 飞书消息 |
|
||||
|------|-----------|---------|
|
||||
| `auto` | 直接写代码 → 跑测试 → 完成 | 结果通知 |
|
||||
| `confirm` | 生成 diff 草案 → 发飞书卡片等待审批 → 执行 | 交互卡片(批准/拒绝) |
|
||||
| `report` | 只输出分析报告,不修改代码 | 报告摘要 |
|
||||
|
||||
### 3.4 Device Agent(双侧同步)
|
||||
|
||||
```python
|
||||
# 同步工作流
|
||||
x86: Developer Agent 修复 → 本地测试通过
|
||||
↓
|
||||
DeviceAgent.sync(project, device) # rsync
|
||||
↓
|
||||
DeviceAgent.run(device, "pytest tests/ -v")
|
||||
↓
|
||||
收集板端 benchmark 结果 → 回传 → 飞书通知
|
||||
```
|
||||
|
||||
支持多设备配置,当前默认 `pi@192.168.0.127`(RK3588),可扩展 RV1106 等。
|
||||
|
||||
---
|
||||
|
||||
## 4. 飞书集成
|
||||
|
||||
### 4.1 通知消息(单向 Webhook)
|
||||
|
||||
- 任务完成摘要
|
||||
- 板端验证结果(FPS / 精度对比)
|
||||
- 错误告警
|
||||
- 架构师定期跨项目分析报告
|
||||
|
||||
### 4.2 交互卡片(`confirm` 模式,需飞书 Bot)
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────┐
|
||||
│ [yolo] 修复 yolo12n 零检出问题 │
|
||||
│ │
|
||||
│ 修改文件: inference/postprocess.py │
|
||||
│ 变更摘要: 补充 sigmoid 后处理... │
|
||||
│ │
|
||||
│ [批准执行] [查看 Diff] [拒绝] │
|
||||
└──────────────────────────────────────┘
|
||||
```
|
||||
|
||||
需要简单 HTTP callback server(`configs/agents.yaml` 中配置 `callback_port`)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 项目配置示例
|
||||
|
||||
```yaml
|
||||
# configs/projects.yaml
|
||||
projects:
|
||||
yolo:
|
||||
path: /data/rockchip/yolo
|
||||
device_workspace: /home/pi/Desktop/yolo
|
||||
mode: confirm
|
||||
mediapipe:
|
||||
path: /data/rockchip/mediapipe
|
||||
device_workspace: /home/pi/Desktop/mediapipe
|
||||
mode: auto
|
||||
embedding:
|
||||
path: /data/rockchip/embedding
|
||||
device_workspace: /home/pi/Desktop/embed-db
|
||||
mode: report
|
||||
info-privacy:
|
||||
path: /data/rockchip/info-privacy
|
||||
device_workspace: /home/pi/Desktop/info-privacy
|
||||
mode: confirm
|
||||
logodetect-x86:
|
||||
path: /data/rockchip/logodetect-x86
|
||||
mode: report # 纯 x86 训练项目,无设备同步
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Architect Agent 整合场景
|
||||
|
||||
| 当前独立项目 | 整合机会识别 | 目标产品方向 |
|
||||
|---|---|---|
|
||||
| yolo + embedding | 图像语义检索 | 「看图找图」本地搜索引擎 |
|
||||
| mediapipe + info-privacy | 人脸检测 + 隐私遮罩 | 会议/监控隐私保护系统 |
|
||||
| logodetect + embedding | 图标检测 + 向量匹配 | 桌面 UI 自动化测试 |
|
||||
|
||||
Architect Agent 定期(每周)对所有项目做一次全局分析,输出:
|
||||
- 各项目成熟度评估
|
||||
- 整合机会及优先级
|
||||
- 预研任务建议(提交至任务队列,`type=architect`)
|
||||
|
||||
---
|
||||
|
||||
## 7. 测试策略
|
||||
|
||||
```
|
||||
tests/
|
||||
├── test_scanner.py # Scanner 正确识别已知问题(Mock 项目文件)
|
||||
├── test_queue.py # 任务入队/出队/优先级
|
||||
├── test_worker.py # Agent 模式逻辑(Mock Claude API)
|
||||
├── test_device_agent.py # SSH 命令生成(Mock SSH)
|
||||
└── test_feishu.py # Webhook 消息格式(Mock HTTP)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 实施阶段规划
|
||||
|
||||
| 阶段 | 内容 | 产出 |
|
||||
|------|------|------|
|
||||
| Phase 1 | 基础框架:Scanner + Queue + Developer Agent + 飞书通知 | 能扫描项目、生成任务、执行修复、通知 |
|
||||
| Phase 2 | Device Agent + Tester Agent + 三模式完整支持 | 双侧同步、板端验证、confirm 交互 |
|
||||
| Phase 3 | Productizer + Architect Agent | 成熟度评估、跨项目整合分析 |
|
||||
| Phase 4 | Manager Agent 全面协调 + 并发执行 | 完整多角色团队运行 |
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,969 @@
|
||||
# Project Generalization & Memory Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** 消除 agent 提示词中的平台硬编码,建立分层项目记忆(SQLite facts/history + 向量后端接口),使 agents 每次处理同一项目时能读取并利用历史记忆。
|
||||
|
||||
**Architecture:** ProjectMemory 扩展两张新表(project_facts / project_history);MemoryBackend Protocol 预留向量后端接口;DeveloperAgent 和 ArchitectAgent 移除 Rockchip 硬编码,改为动态读取记忆;Executor 在任务完成后自动写入 history;Architect 通过 `[记忆]` 标签写入 facts。
|
||||
|
||||
**Tech Stack:** Python 3.10, SQLite, dataclasses, Protocol (typing), pytest
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 扩展 ProjectMemory — facts + history 表
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/rockchip_agents/tools/project_memory.py`
|
||||
- Test: `tests/test_project_memory.py`
|
||||
|
||||
**背景:** 当前 ProjectMemory 只有 `project_goals` 一张表。需要新增:
|
||||
- `project_facts`:key-value 事实知识库,任意 agent 可写(无角色限制)
|
||||
- `project_history`:任务完成记录,executor 自动写入
|
||||
|
||||
**Step 1: 写失败测试**
|
||||
|
||||
在 `tests/test_project_memory.py` 末尾追加:
|
||||
|
||||
```python
|
||||
def test_set_and_get_fact(tmp_path):
|
||||
mem = _mem(tmp_path)
|
||||
mem.set_fact("yolo", "test_cmd", "pytest tests/", role="developer")
|
||||
facts = mem.get_facts("yolo")
|
||||
assert facts["test_cmd"] == "pytest tests/"
|
||||
|
||||
|
||||
def test_set_fact_overwrites_existing(tmp_path):
|
||||
mem = _mem(tmp_path)
|
||||
mem.set_fact("yolo", "lang", "Python", role="developer")
|
||||
mem.set_fact("yolo", "lang", "Python/C++", role="architect")
|
||||
assert mem.get_facts("yolo")["lang"] == "Python/C++"
|
||||
|
||||
|
||||
def test_get_facts_returns_empty_when_absent(tmp_path):
|
||||
mem = _mem(tmp_path)
|
||||
assert mem.get_facts("nonexistent") == {}
|
||||
|
||||
|
||||
def test_add_and_get_history(tmp_path):
|
||||
mem = _mem(tmp_path)
|
||||
mem.add_history("yolo", "修复零检出", "fix_bug", "done", "已修复推理配置")
|
||||
history = mem.get_recent_history("yolo")
|
||||
assert len(history) == 1
|
||||
assert history[0]["task_title"] == "修复零检出"
|
||||
assert history[0]["status"] == "done"
|
||||
|
||||
|
||||
def test_history_limit(tmp_path):
|
||||
mem = _mem(tmp_path)
|
||||
for i in range(8):
|
||||
mem.add_history("yolo", f"任务{i}", "code_review", "done", f"摘要{i}")
|
||||
history = mem.get_recent_history("yolo", limit=5)
|
||||
assert len(history) == 5
|
||||
# 最新的在前
|
||||
assert history[0]["task_title"] == "任务7"
|
||||
|
||||
|
||||
def test_context_includes_facts_and_history(tmp_path):
|
||||
mem = _mem(tmp_path)
|
||||
mem.set_goal("yolo", "实时目标检测", role="productizer")
|
||||
mem.set_fact("yolo", "lang", "Python", role="developer")
|
||||
mem.add_history("yolo", "修复零检出", "fix_bug", "done", "摘要")
|
||||
ctx = mem.get_context_for_prompt("yolo")
|
||||
assert "项目长期目标" in ctx
|
||||
assert "lang" in ctx
|
||||
assert "Python" in ctx
|
||||
assert "近期任务" in ctx
|
||||
assert "修复零检出" in ctx
|
||||
```
|
||||
|
||||
**Step 2: 运行验证测试失败**
|
||||
|
||||
```bash
|
||||
cd /data/rockchip/agents && source venv/bin/activate
|
||||
pytest tests/test_project_memory.py::test_set_and_get_fact -v
|
||||
```
|
||||
预期: FAIL — `AttributeError: 'ProjectMemory' object has no attribute 'set_fact'`
|
||||
|
||||
**Step 3: 实现新方法**
|
||||
|
||||
完整替换 `src/rockchip_agents/tools/project_memory.py`:
|
||||
|
||||
```python
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional, Protocol, runtime_checkable
|
||||
|
||||
DEFAULT_DB_PATH = Path(__file__).parent.parent.parent.parent / "data" / "project_memory.db"
|
||||
|
||||
WRITE_ROLES: frozenset[str] = frozenset({"productizer", "architect"})
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class MemoryBackend(Protocol):
|
||||
"""Layer-2 vector backend interface. Implement to enable semantic search."""
|
||||
|
||||
def search(self, query: str, project: str, top_k: int = 5) -> list[str]: ...
|
||||
def upsert(self, project: str, key: str, text: str) -> None: ...
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProjectGoal:
|
||||
project: str
|
||||
goal_text: str
|
||||
updated_by: str
|
||||
updated_at: str
|
||||
|
||||
|
||||
class ProjectMemory:
|
||||
"""Persistent project knowledge store.
|
||||
|
||||
Layer 1 — SQLite: goals, facts (key-value), task history.
|
||||
Layer 2 — Optional MemoryBackend: vector semantic search (future).
|
||||
|
||||
Facts can be written by any agent; goals only by WRITE_ROLES.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
db_path: Path | None = None,
|
||||
vector_backend: Optional[MemoryBackend] = None,
|
||||
) -> None:
|
||||
self._db_path = db_path or DEFAULT_DB_PATH
|
||||
self._db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self._vector = vector_backend
|
||||
self._init_db()
|
||||
|
||||
# ── goals (write-restricted) ──────────────────────────────────────────────
|
||||
|
||||
def get_goal(self, project: str) -> Optional[ProjectGoal]:
|
||||
conn = self._conn()
|
||||
try:
|
||||
row = conn.execute(
|
||||
"SELECT project, goal_text, updated_by, updated_at "
|
||||
"FROM project_goals WHERE project = ?",
|
||||
(project,),
|
||||
).fetchone()
|
||||
return ProjectGoal(**dict(row)) if row else None
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def set_goal(self, project: str, goal_text: str, role: str) -> None:
|
||||
if role not in WRITE_ROLES:
|
||||
raise PermissionError(f"角色 {role!r} 无权修改项目目标(需要 {WRITE_ROLES})")
|
||||
conn = self._conn()
|
||||
try:
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO project_goals "
|
||||
"(project, goal_text, updated_by, updated_at) VALUES (?, ?, ?, ?)",
|
||||
(project, goal_text, role, datetime.now().isoformat()),
|
||||
)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def list_goals(self) -> dict[str, ProjectGoal]:
|
||||
conn = self._conn()
|
||||
try:
|
||||
rows = conn.execute(
|
||||
"SELECT project, goal_text, updated_by, updated_at FROM project_goals"
|
||||
).fetchall()
|
||||
return {r["project"]: ProjectGoal(**dict(r)) for r in rows}
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
# ── facts (open write) ────────────────────────────────────────────────────
|
||||
|
||||
def set_fact(self, project: str, key: str, value: str, role: str) -> None:
|
||||
"""Store a project fact. Any agent role may write."""
|
||||
conn = self._conn()
|
||||
try:
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO project_facts "
|
||||
"(project, key, value, updated_by, updated_at) VALUES (?, ?, ?, ?, ?)",
|
||||
(project, key, value, role, datetime.now().isoformat()),
|
||||
)
|
||||
conn.commit()
|
||||
if self._vector is not None:
|
||||
self._vector.upsert(project, key, f"{key}: {value}")
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def get_facts(self, project: str) -> dict[str, str]:
|
||||
conn = self._conn()
|
||||
try:
|
||||
rows = conn.execute(
|
||||
"SELECT key, value FROM project_facts WHERE project = ? ORDER BY key",
|
||||
(project,),
|
||||
).fetchall()
|
||||
return {r["key"]: r["value"] for r in rows}
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
# ── history (auto-written by executor) ───────────────────────────────────
|
||||
|
||||
def add_history(
|
||||
self,
|
||||
project: str,
|
||||
task_title: str,
|
||||
task_type: str,
|
||||
status: str,
|
||||
summary: str,
|
||||
) -> None:
|
||||
conn = self._conn()
|
||||
try:
|
||||
conn.execute(
|
||||
"INSERT INTO project_history "
|
||||
"(project, task_title, task_type, status, summary, created_at) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?)",
|
||||
(
|
||||
project,
|
||||
task_title[:200],
|
||||
task_type,
|
||||
status,
|
||||
summary[:500],
|
||||
datetime.now().isoformat(),
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def get_recent_history(self, project: str, limit: int = 5) -> list[dict]:
|
||||
conn = self._conn()
|
||||
try:
|
||||
rows = conn.execute(
|
||||
"SELECT task_title, task_type, status, summary, created_at "
|
||||
"FROM project_history WHERE project = ? "
|
||||
"ORDER BY id DESC LIMIT ?",
|
||||
(project, limit),
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
# ── composite context builder ─────────────────────────────────────────────
|
||||
|
||||
def get_context_for_prompt(self, project: str) -> str:
|
||||
"""Build a context block to inject into agent system prompts.
|
||||
|
||||
Includes goal, facts, and recent task history. If a vector backend
|
||||
is configured, appends semantic search results too.
|
||||
"""
|
||||
parts: list[str] = []
|
||||
|
||||
goal = self.get_goal(project)
|
||||
if goal:
|
||||
parts.append(
|
||||
f"【项目长期目标(由 {goal.updated_by} 设定,{goal.updated_at[:10]})】\n"
|
||||
f"{goal.goal_text}"
|
||||
)
|
||||
|
||||
facts = self.get_facts(project)
|
||||
if facts:
|
||||
facts_lines = "\n".join(f" {k}: {v}" for k, v in facts.items())
|
||||
parts.append(f"【项目知识库】\n{facts_lines}")
|
||||
|
||||
history = self.get_recent_history(project)
|
||||
if history:
|
||||
hist_lines = "\n".join(
|
||||
f" [{h['status']}] {h['task_title'][:60]}" for h in history
|
||||
)
|
||||
parts.append(f"【近期任务记录(最近 {len(history)} 条)】\n{hist_lines}")
|
||||
|
||||
if self._vector is not None:
|
||||
extra = self._vector.search(project, project, top_k=3)
|
||||
if extra:
|
||||
parts.append("【语义检索相关记忆】\n" + "\n".join(f" {e}" for e in extra))
|
||||
|
||||
return "\n\n".join(parts) + "\n" if parts else ""
|
||||
|
||||
# ── db helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
def _init_db(self) -> None:
|
||||
conn = self._conn()
|
||||
try:
|
||||
conn.executescript("""
|
||||
CREATE TABLE IF NOT EXISTS project_goals (
|
||||
project TEXT PRIMARY KEY,
|
||||
goal_text TEXT NOT NULL,
|
||||
updated_by TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS project_facts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
project TEXT NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
updated_by TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
UNIQUE(project, key)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS project_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
project TEXT NOT NULL,
|
||||
task_title TEXT NOT NULL,
|
||||
task_type TEXT NOT NULL DEFAULT '',
|
||||
status TEXT NOT NULL,
|
||||
summary TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
""")
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def _conn(self) -> sqlite3.Connection:
|
||||
conn = sqlite3.connect(str(self._db_path))
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
```
|
||||
|
||||
**Step 4: 运行所有新测试**
|
||||
|
||||
```bash
|
||||
pytest tests/test_project_memory.py -v
|
||||
```
|
||||
预期: 全部 PASS(原有 10 个 + 新增 6 个 = 16 个)
|
||||
|
||||
**Step 5: 运行全量测试确认无回归**
|
||||
|
||||
```bash
|
||||
pytest tests/ -q
|
||||
```
|
||||
预期: 73 passed(数量与之前一致)
|
||||
|
||||
---
|
||||
|
||||
### Task 2: DeveloperAgent 去硬编码,注入记忆
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/rockchip_agents/agents/developer.py`
|
||||
- Test: `tests/test_developer.py`
|
||||
|
||||
**背景:** `_build_context()` 在 line 95 硬编码了 "Rockchip RKNN NPU 推理项目";且 DeveloperAgent 没有读取 ProjectMemory。修改后,context 从项目的 description 字段和 ProjectMemory 动态构建。
|
||||
|
||||
**Step 1: 写失败测试**
|
||||
|
||||
读取 `tests/test_developer.py` 找到现有测试的结构,然后追加:
|
||||
|
||||
```python
|
||||
def test_developer_context_uses_description_not_hardcoded(tmp_path):
|
||||
"""_build_context must not mention 'Rockchip RKNN' when description differs."""
|
||||
from rockchip_agents.config import AgentsConfig, ProjectConfig, SchedulerConfig
|
||||
from rockchip_agents.config import ClaudeConfig, FeishuConfig
|
||||
from rockchip_agents.agents.developer import DeveloperAgent
|
||||
from rockchip_agents.core.queue import Task
|
||||
|
||||
cfg = AgentsConfig(
|
||||
projects={"myproj": ProjectConfig(
|
||||
path=tmp_path, mode="report",
|
||||
description="通用 Web 后端服务"
|
||||
)},
|
||||
scheduler=SchedulerConfig(),
|
||||
claude=ClaudeConfig(model="claude-sonnet-4-6"),
|
||||
feishu=FeishuConfig(),
|
||||
devices={},
|
||||
)
|
||||
agent = DeveloperAgent(cfg)
|
||||
task = Task(project="myproj", type="fix_bug", title="修复登录",
|
||||
context="", priority=2, mode="report")
|
||||
ctx = agent._build_context(task)
|
||||
assert "通用 Web 后端服务" in ctx
|
||||
assert "Rockchip RKNN" not in ctx
|
||||
assert "myproj" in ctx
|
||||
|
||||
|
||||
def test_developer_context_includes_memory(tmp_path):
|
||||
"""_build_context includes ProjectMemory context when available."""
|
||||
from rockchip_agents.config import AgentsConfig, ProjectConfig, SchedulerConfig
|
||||
from rockchip_agents.config import ClaudeConfig, FeishuConfig
|
||||
from rockchip_agents.agents.developer import DeveloperAgent
|
||||
from rockchip_agents.tools.project_memory import ProjectMemory
|
||||
from rockchip_agents.core.queue import Task
|
||||
|
||||
mem = ProjectMemory(db_path=tmp_path / "mem.db")
|
||||
mem.set_goal("myproj", "稳定高性能后端", role="productizer")
|
||||
mem.set_fact("myproj", "test_cmd", "pytest tests/", role="developer")
|
||||
|
||||
cfg = AgentsConfig(
|
||||
projects={"myproj": ProjectConfig(path=tmp_path, mode="auto",
|
||||
description="通用 Web 后端服务")},
|
||||
scheduler=SchedulerConfig(),
|
||||
claude=ClaudeConfig(model="claude-sonnet-4-6"),
|
||||
feishu=FeishuConfig(),
|
||||
devices={},
|
||||
)
|
||||
agent = DeveloperAgent(cfg, memory=mem)
|
||||
task = Task(project="myproj", type="fix_bug", title="修复登录",
|
||||
context="", priority=2, mode="auto")
|
||||
ctx = agent._build_context(task)
|
||||
assert "稳定高性能后端" in ctx
|
||||
assert "test_cmd" in ctx
|
||||
```
|
||||
|
||||
**Step 2: 验证测试失败**
|
||||
|
||||
```bash
|
||||
pytest tests/test_developer.py::test_developer_context_uses_description_not_hardcoded -v
|
||||
```
|
||||
预期: FAIL — `assert "Rockchip RKNN" not in ctx` 失败(因为还没改)
|
||||
|
||||
**Step 3: 修改 DeveloperAgent**
|
||||
|
||||
完整替换 `src/rockchip_agents/agents/developer.py`:
|
||||
|
||||
```python
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from rockchip_agents.config import AgentsConfig
|
||||
from rockchip_agents.core.queue import Task
|
||||
from rockchip_agents.tools.project_memory import ProjectMemory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
TIMEOUT = 300
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentResult:
|
||||
status: str # done | failed
|
||||
summary: str
|
||||
|
||||
|
||||
def _parse_cli_result(r: subprocess.CompletedProcess) -> AgentResult:
|
||||
"""解析 claude --output-format json 的输出。"""
|
||||
if r.returncode != 0:
|
||||
return AgentResult(status="failed", summary=r.stderr or r.stdout or "未知错误")
|
||||
try:
|
||||
data = json.loads(r.stdout)
|
||||
summary = data.get("result", r.stdout)
|
||||
is_error = data.get("is_error", False)
|
||||
return AgentResult(status="failed" if is_error else "done", summary=summary)
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
return AgentResult(status="done", summary=r.stdout)
|
||||
|
||||
|
||||
class DeveloperAgent:
|
||||
def __init__(
|
||||
self,
|
||||
config: AgentsConfig,
|
||||
memory: Optional[ProjectMemory] = None,
|
||||
) -> None:
|
||||
self._config = config
|
||||
self._memory = memory or ProjectMemory()
|
||||
|
||||
def run(self, task: Task) -> AgentResult:
|
||||
proj_cfg = self._config.projects.get(task.project)
|
||||
proj_path = str(proj_cfg.path) if proj_cfg else None
|
||||
cmd = self._build_cmd(task)
|
||||
env = self._build_env()
|
||||
try:
|
||||
r = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True, text=True,
|
||||
timeout=TIMEOUT,
|
||||
cwd=proj_path,
|
||||
env=env,
|
||||
)
|
||||
return _parse_cli_result(r)
|
||||
except subprocess.TimeoutExpired:
|
||||
return AgentResult(status="failed", summary=f"执行超时({TIMEOUT}s)")
|
||||
except Exception as e:
|
||||
logger.error("DeveloperAgent 执行失败: %s", e)
|
||||
return AgentResult(status="failed", summary=str(e))
|
||||
|
||||
def _build_cmd(self, task: Task) -> list[str]:
|
||||
cmd = [
|
||||
"claude", "--print",
|
||||
"--output-format", "json",
|
||||
"--no-session-persistence",
|
||||
"--model", self._config.claude.model,
|
||||
"--append-system-prompt", self._build_context(task),
|
||||
]
|
||||
if task.mode == "report":
|
||||
cmd += ["--allowedTools", "Read,Glob,Grep"]
|
||||
elif task.mode == "confirm":
|
||||
cmd += ["--allowedTools", "Read,Glob,Grep,Bash"]
|
||||
else: # auto
|
||||
cmd.append("--dangerously-skip-permissions")
|
||||
cmd.append(self._build_prompt(task))
|
||||
return cmd
|
||||
|
||||
def _build_env(self) -> dict:
|
||||
env = os.environ.copy()
|
||||
if self._config.claude.api_key:
|
||||
env["ANTHROPIC_API_KEY"] = self._config.claude.api_key
|
||||
env.pop("CLAUDECODE", None)
|
||||
env.pop("CLAUDE_CODE_ENTRYPOINT", None)
|
||||
return env
|
||||
|
||||
def _build_context(self, task: Task) -> str:
|
||||
proj_cfg = self._config.projects.get(task.project)
|
||||
proj_path = str(proj_cfg.path) if proj_cfg else ""
|
||||
proj_desc = (proj_cfg.description if proj_cfg and proj_cfg.description
|
||||
else task.project)
|
||||
mode_desc = {
|
||||
"auto": "直接修复文件并运行测试验证。",
|
||||
"confirm": "只分析并给出修改方案摘要,不实际写文件。",
|
||||
"report": "只读取和分析,输出详细报告,不做任何修改。",
|
||||
}.get(task.mode, "")
|
||||
memory_ctx = self._memory.get_context_for_prompt(task.project)
|
||||
return (
|
||||
f"你是 {task.project} 项目的 Developer Agent。\n"
|
||||
f"项目路径: {proj_path}。\n"
|
||||
f"项目描述: {proj_desc}。\n"
|
||||
f"当前模式: {task.mode} — {mode_desc}\n"
|
||||
f"{memory_ctx}"
|
||||
f"请用中文回复,保持简洁,聚焦问题。"
|
||||
)
|
||||
|
||||
def _build_prompt(self, task: Task) -> str:
|
||||
return (
|
||||
f"任务类型: {task.type}\n"
|
||||
f"任务标题: {task.title}\n"
|
||||
f"上下文:\n{task.context}\n\n"
|
||||
f"请分析并处理这个任务。"
|
||||
)
|
||||
```
|
||||
|
||||
**Step 4: 运行新测试**
|
||||
|
||||
```bash
|
||||
pytest tests/test_developer.py -v
|
||||
```
|
||||
预期: 全部 PASS
|
||||
|
||||
**Step 5: 全量测试**
|
||||
|
||||
```bash
|
||||
pytest tests/ -q
|
||||
```
|
||||
预期: 所有测试通过
|
||||
|
||||
---
|
||||
|
||||
### Task 3: ArchitectAgent 去硬编码 + [记忆] 标签支持
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/rockchip_agents/agents/architect.py`
|
||||
- Test: `tests/test_architect.py`
|
||||
|
||||
**背景:** `ARCHITECT_SYSTEM` 在 line 21-32 硬编码了 "Rockchip AI 平台"、"RKNN NPU 推理项目" 以及 YOLO/embedding/MediaPipe 等具体集成示例。需要改成通用架构师角色。同时新增 `[记忆]` 标签解析,让 architect 可以写入项目 facts。
|
||||
|
||||
**Step 1: 写失败测试**
|
||||
|
||||
在 `tests/test_architect.py` 末尾追加:
|
||||
|
||||
```python
|
||||
def test_architect_parses_memory_tag_and_writes_fact(tmp_path):
|
||||
"""[记忆] tag in report should call memory.set_fact for each key=value pair."""
|
||||
from unittest.mock import MagicMock, patch
|
||||
from rockchip_agents.agents.architect import ArchitectAgent
|
||||
from rockchip_agents.tools.project_memory import ProjectMemory
|
||||
from rockchip_agents.core.queue import TaskQueue
|
||||
|
||||
cfg = _make_cfg(tmp_path)
|
||||
mem = ProjectMemory(db_path=tmp_path / "mem.db")
|
||||
queue = TaskQueue(db_path=tmp_path / "q.db")
|
||||
agent = ArchitectAgent(cfg, queue=queue, memory=mem)
|
||||
|
||||
report = (
|
||||
"架构分析完成。\n"
|
||||
"[记忆] yolo: test_cmd=pytest tests/, main_lang=Python\n"
|
||||
"[优化] yolo: 优化推理管道\n"
|
||||
)
|
||||
agent._extract_and_enqueue(report, requester="test")
|
||||
|
||||
facts = mem.get_facts("yolo")
|
||||
assert facts.get("test_cmd") == "pytest tests/"
|
||||
assert facts.get("main_lang") == "Python"
|
||||
|
||||
|
||||
def test_architect_system_prompt_is_generic():
|
||||
"""ARCHITECT_SYSTEM must not contain hardcoded project names."""
|
||||
from rockchip_agents.agents.architect import ARCHITECT_SYSTEM
|
||||
for hardcoded in ["Rockchip", "RKNN", "YOLO", "MediaPipe", "embedding"]:
|
||||
assert hardcoded not in ARCHITECT_SYSTEM, (
|
||||
f"ARCHITECT_SYSTEM 不应包含硬编码词语: {hardcoded!r}"
|
||||
)
|
||||
```
|
||||
|
||||
**Step 2: 验证测试失败**
|
||||
|
||||
```bash
|
||||
pytest tests/test_architect.py::test_architect_system_prompt_is_generic -v
|
||||
```
|
||||
预期: FAIL — `assert "Rockchip" not in ARCHITECT_SYSTEM` 失败
|
||||
|
||||
**Step 3: 修改 architect.py**
|
||||
|
||||
替换 `ARCHITECT_SYSTEM` 常量(line 21-32):
|
||||
|
||||
```python
|
||||
ARCHITECT_SYSTEM = """你是一位技术架构师,负责分析多个软件项目,识别整合机会,提出架构改进建议。
|
||||
|
||||
分析维度:
|
||||
- 功能互补性:哪些项目的能力可以组合,产生新价值
|
||||
- 技术共性:是否有可复用的基础设施或数据管道
|
||||
- 产品可能性:整合后能解决什么实际问题
|
||||
|
||||
请用中文输出结构化分析报告,末尾按格式列出任务(每行一条):
|
||||
- [预研] 任务名称 ← 跨项目探索、新功能研究
|
||||
- [优化] <项目名>: 优化描述 ← 对特定项目的架构改进(项目名须在上面列出的项目中)
|
||||
- [目标] <项目名>: 目标描述 ← 更新项目长期目标(仅当需要修订时)
|
||||
- [记忆] <项目名>: key=value, key2=value2 ← 记录项目关键技术事实
|
||||
"""
|
||||
```
|
||||
|
||||
在 `_extract_and_enqueue()` 方法末尾(`[目标]` 解析块之后)追加 `[记忆]` 解析:
|
||||
|
||||
```python
|
||||
# [记忆] <project>: key=value, key2=value2 → update project facts
|
||||
for m in re.finditer(r"\[记忆\]\s+([\w-]+)\s*[::]\s*(.+)", report_text):
|
||||
project = m.group(1).strip()
|
||||
kv_text = m.group(2).strip()
|
||||
if project not in self._config.projects:
|
||||
logger.warning("记忆更新项目 %r 不在配置中,跳过", project)
|
||||
continue
|
||||
for kv in kv_text.split(","):
|
||||
kv = kv.strip()
|
||||
if "=" in kv:
|
||||
key, _, value = kv.partition("=")
|
||||
key = key.strip()[:50]
|
||||
value = value.strip()[:200]
|
||||
if key:
|
||||
try:
|
||||
self._memory.set_fact(project, key, value, "architect")
|
||||
logger.info("项目事实已记录 [%s] %s=%s", project, key, value)
|
||||
except Exception as e:
|
||||
logger.warning("[%s] 写入项目事实失败: %s", project, e)
|
||||
```
|
||||
|
||||
**Step 4: 运行新测试**
|
||||
|
||||
```bash
|
||||
pytest tests/test_architect.py -v
|
||||
```
|
||||
预期: 全部 PASS
|
||||
|
||||
**Step 5: 全量测试**
|
||||
|
||||
```bash
|
||||
pytest tests/ -q
|
||||
```
|
||||
预期: 所有测试通过
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Executor 自动写任务历史 + 传递 ProjectMemory 给 agents
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/rockchip_agents/core/executor.py`
|
||||
- Test: `tests/test_executor.py`
|
||||
|
||||
**背景:** 每次任务完成(done 或 failed)后,应把结果写入 `project_history`,让下一轮 agent 处理同一项目时看到历史记录。同时 `_make_agent()` 需要接受 `memory` 参数,将同一 ProjectMemory 实例传递给所有 agents。
|
||||
|
||||
**Step 1: 写失败测试**
|
||||
|
||||
在 `tests/test_executor.py` 末尾追加(或找到该文件后追加):
|
||||
|
||||
```python
|
||||
def test_executor_writes_history_after_task(tmp_path):
|
||||
"""Executor must write task result to ProjectMemory history after completion."""
|
||||
import asyncio
|
||||
from unittest.mock import patch, MagicMock
|
||||
from rockchip_agents.core.executor import Executor
|
||||
from rockchip_agents.core.queue import TaskQueue, Task
|
||||
from rockchip_agents.tools.project_memory import ProjectMemory
|
||||
from rockchip_agents.agents.developer import AgentResult
|
||||
|
||||
mem = ProjectMemory(db_path=tmp_path / "mem.db")
|
||||
queue = TaskQueue(db_path=tmp_path / "q.db")
|
||||
queue.enqueue(Task(project="yolo", type="fix_bug", title="修复零检出",
|
||||
context="", priority=2, mode="report", agent_role="developer"))
|
||||
|
||||
cfg = _make_cfg(tmp_path) # reuse helper from existing tests
|
||||
executor = Executor(cfg, queue=queue, memory=mem)
|
||||
|
||||
mock_result = AgentResult(status="done", summary="已修复")
|
||||
with patch("rockchip_agents.agents.developer.DeveloperAgent.run",
|
||||
return_value=mock_result):
|
||||
asyncio.run(executor.run_next())
|
||||
|
||||
history = mem.get_recent_history("yolo")
|
||||
assert len(history) == 1
|
||||
assert history[0]["status"] == "done"
|
||||
assert history[0]["task_title"] == "修复零检出"
|
||||
```
|
||||
|
||||
**Step 2: 验证测试失败**
|
||||
|
||||
```bash
|
||||
pytest tests/test_executor.py::test_executor_writes_history_after_task -v
|
||||
```
|
||||
预期: FAIL — `Executor.__init__` 不接受 `memory` 参数,或 history 为空
|
||||
|
||||
**Step 3: 修改 executor.py**
|
||||
|
||||
完整替换 `src/rockchip_agents/core/executor.py`:
|
||||
|
||||
```python
|
||||
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
|
||||
from rockchip_agents.tools.project_memory import ProjectMemory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _make_agent(config: AgentsConfig, task: Task, memory: ProjectMemory):
|
||||
"""根据 agent_role 路由到对应 Agent,传入共享 memory 实例。"""
|
||||
role = task.agent_role
|
||||
if role == "tester":
|
||||
from rockchip_agents.agents.tester import TesterAgent
|
||||
return TesterAgent(config)
|
||||
if role == "productizer":
|
||||
from rockchip_agents.agents.productizer import ProductizerAgent
|
||||
return ProductizerAgent(config, memory=memory)
|
||||
if role == "architect":
|
||||
from rockchip_agents.agents.architect import ArchitectAgent
|
||||
return ArchitectAgent(config, memory=memory)
|
||||
return DeveloperAgent(config, memory=memory)
|
||||
|
||||
|
||||
class Executor:
|
||||
def __init__(
|
||||
self,
|
||||
config: AgentsConfig,
|
||||
queue: Optional[TaskQueue] = None,
|
||||
memory: Optional[ProjectMemory] = None,
|
||||
) -> None:
|
||||
self._config = config
|
||||
self._queue = queue or TaskQueue()
|
||||
self._memory = memory or ProjectMemory()
|
||||
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:
|
||||
tasks_to_run: list[Task] = []
|
||||
while True:
|
||||
task = self._queue.dequeue()
|
||||
if not task:
|
||||
break
|
||||
tasks_to_run.append(task)
|
||||
|
||||
async def _run_with_sem(t: Task) -> None:
|
||||
async with self._semaphore:
|
||||
await self._run_task(t)
|
||||
|
||||
if tasks_to_run:
|
||||
await asyncio.gather(
|
||||
*[_run_with_sem(t) for t in tasks_to_run],
|
||||
return_exceptions=True,
|
||||
)
|
||||
return len(tasks_to_run)
|
||||
|
||||
async def _run_task(self, task: Task) -> None:
|
||||
logger.info(
|
||||
"开始执行任务 [%s] %s (role=%s, mode=%s)",
|
||||
task.project, task.title, task.agent_role, task.mode,
|
||||
)
|
||||
try:
|
||||
agent = _make_agent(self._config, task, self._memory)
|
||||
result = await asyncio.get_running_loop().run_in_executor(
|
||||
None, agent.run, task
|
||||
)
|
||||
if task.mode == "confirm" and result.status == "done":
|
||||
callback_url = f"http://localhost:{self._config.feishu.callback_port}"
|
||||
self._feishu.send_confirm_card(
|
||||
task_id=task.id,
|
||||
project=task.project,
|
||||
task_title=task.title,
|
||||
diff_summary=result.summary,
|
||||
callback_url=callback_url,
|
||||
)
|
||||
self._queue.mark_waiting_confirm(task.id, diff=result.summary)
|
||||
logger.info("任务等待确认 [%s] %s", task.project, task.title)
|
||||
else:
|
||||
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)
|
||||
|
||||
# Write history so future agents can learn from this run
|
||||
try:
|
||||
self._memory.add_history(
|
||||
project=task.project,
|
||||
task_title=task.title,
|
||||
task_type=task.type,
|
||||
status=result.status,
|
||||
summary=result.summary[:300],
|
||||
)
|
||||
except Exception as hist_err:
|
||||
logger.warning("写入任务历史失败: %s", hist_err)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("任务失败 [%s] %s: %s", task.project, task.title, e)
|
||||
self._queue.mark_failed(task.id, str(e))
|
||||
try:
|
||||
self._memory.add_history(
|
||||
project=task.project,
|
||||
task_title=task.title,
|
||||
task_type=task.type,
|
||||
status="failed",
|
||||
summary=str(e)[:300],
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
```
|
||||
|
||||
**Step 4: 运行新测试**
|
||||
|
||||
```bash
|
||||
pytest tests/test_executor.py -v
|
||||
```
|
||||
预期: 全部 PASS
|
||||
|
||||
**Step 5: 全量测试**
|
||||
|
||||
```bash
|
||||
pytest tests/ -q
|
||||
```
|
||||
预期: 所有测试通过(原 73 个 + 新增若干 = N passed)
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 新增 CLI 命令 + 更新文档
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/run.py`
|
||||
- Modify: `DEVELOP.md`
|
||||
|
||||
**背景:** 新增 `facts` 命令,方便查看各项目的知识库事实。同时更新 DEVELOP.md 里的记忆架构说明。
|
||||
|
||||
**Step 1: 在 scripts/run.py 中新增 `facts` 子命令**
|
||||
|
||||
在 `sub.add_parser("goals", ...)` 行之后追加:
|
||||
|
||||
```python
|
||||
sub.add_parser("facts", help="查看所有项目的知识库事实(由 agents 自动记录)")
|
||||
p_sf = sub.add_parser("set-fact", help="手动写入项目事实")
|
||||
p_sf.add_argument("project", help="项目名")
|
||||
p_sf.add_argument("key", help="事实键名,如 test_cmd")
|
||||
p_sf.add_argument("value", help="事实值")
|
||||
```
|
||||
|
||||
在 `elif args.cmd == "goals":` 块之后追加:
|
||||
|
||||
```python
|
||||
elif args.cmd == "facts":
|
||||
from rockchip_agents.tools.project_memory import ProjectMemory
|
||||
mem = ProjectMemory()
|
||||
from rockchip_agents.config import load_config
|
||||
cfg = load_config()
|
||||
any_found = False
|
||||
for proj_name in sorted(cfg.projects):
|
||||
facts = mem.get_facts(proj_name)
|
||||
if facts:
|
||||
any_found = True
|
||||
print(f"\n[{proj_name}]")
|
||||
for k, v in facts.items():
|
||||
print(f" {k}: {v}")
|
||||
if not any_found:
|
||||
print("暂无项目事实记录(运行 architect 任务后自动写入)")
|
||||
|
||||
elif args.cmd == "set-fact":
|
||||
from rockchip_agents.tools.project_memory import ProjectMemory
|
||||
mem = ProjectMemory()
|
||||
mem.set_fact(args.project, args.key, args.value, role="developer")
|
||||
print(f"已写入: [{args.project}] {args.key} = {args.value}")
|
||||
```
|
||||
|
||||
**Step 2: 手动验证命令可用**
|
||||
|
||||
```bash
|
||||
source venv/bin/activate
|
||||
python scripts/run.py facts
|
||||
python scripts/run.py set-fact memory-analyze test_cmd "cd test_cases && python test_runner.py"
|
||||
python scripts/run.py facts
|
||||
```
|
||||
预期: 正确打印 memory-analyze 的 test_cmd 事实
|
||||
|
||||
**Step 3: 更新 DEVELOP.md 记忆章节**
|
||||
|
||||
在 DEVELOP.md 中找到"项目记忆"相关章节(或在末尾追加),写入以下内容:
|
||||
|
||||
```markdown
|
||||
## 项目记忆系统
|
||||
|
||||
ProjectMemory 采用分层设计:
|
||||
|
||||
| 层 | 存储 | 读写规则 |
|
||||
|----|------|---------|
|
||||
| Layer 1a — goals | SQLite project_goals | 只有 productizer/architect 可写 |
|
||||
| Layer 1b — facts | SQLite project_facts | 任意 agent 可写;architect 通过 [记忆] 标签写入 |
|
||||
| Layer 1c — history | SQLite project_history | executor 自动写入,每次任务完成后记录 |
|
||||
| Layer 2 — vector | MemoryBackend Protocol | 可选;实现接口后自动启用语义检索 |
|
||||
|
||||
### Agent 写入记忆的方式
|
||||
|
||||
- **Developer**: 任务执行后由 executor 自动写 history
|
||||
- **Architect**: 分析报告中添加 `[记忆] <项目名>: key=value` 标签写 facts
|
||||
- **Productizer**: 评估后自动调用 `set_goal()` 更新项目目标
|
||||
|
||||
### 查看项目记忆
|
||||
|
||||
```bash
|
||||
python scripts/run.py goals # 项目长期目标
|
||||
python scripts/run.py facts # 项目知识库事实
|
||||
```
|
||||
|
||||
### 接入向量后端(Layer 2)
|
||||
|
||||
实现 `MemoryBackend` Protocol,传入 `ProjectMemory(vector_backend=your_backend)`:
|
||||
|
||||
```python
|
||||
class MyVectorBackend:
|
||||
def search(self, query: str, project: str, top_k: int = 5) -> list[str]: ...
|
||||
def upsert(self, project: str, key: str, text: str) -> None: ...
|
||||
```
|
||||
```
|
||||
|
||||
**Step 4: 全量测试最终确认**
|
||||
|
||||
```bash
|
||||
pytest tests/ -q
|
||||
```
|
||||
预期: 所有测试通过,0 warnings
|
||||
@@ -0,0 +1,217 @@
|
||||
# Agent 全面升级设计文档
|
||||
|
||||
**日期**: 2026-03-06
|
||||
**状态**: 已批准,待实施
|
||||
**约束**: 仅使用 `claude --print` CLI,零额外 token,MCP 工具在单次调用内生效
|
||||
|
||||
---
|
||||
|
||||
## 一、Agent 角色扩展(4 → 13 个)
|
||||
|
||||
### 开发者层(5 个,并行执行)
|
||||
|
||||
| 角色 | 职责 |
|
||||
|------|------|
|
||||
| `dev-embedded` | RKNN/NPU/板端推理、AI 应用层 |
|
||||
| `dev-backend` | Python 服务、API、数据管道(x86)|
|
||||
| `dev-ml` | 模型量化、精度优化、训练脚本 |
|
||||
| `dev-test` | 专门生成/补全测试代码,TDD 驱动 |
|
||||
| `dev-refactor` | 重构、技术债、代码质量 |
|
||||
|
||||
### 内核与底层层(2 个)
|
||||
|
||||
| 角色 | 职责 |
|
||||
|------|------|
|
||||
| `dev-kernel` | Linux 内核模块、驱动开发、内核配置(Kconfig/Device Tree)|
|
||||
| `dev-lowlevel` | BSP、Bootloader(U-Boot)、裸机/RTOS、硬件 bring-up |
|
||||
|
||||
### 硬件工程师(1 个)
|
||||
|
||||
| 角色 | 职责 |
|
||||
|------|------|
|
||||
| `hw-engineer` | 芯片选型(RK3588/RV1106/等)、外设接口设计、BOM 评审、功耗分析、Datasheet 查阅 |
|
||||
|
||||
### 增强测试工程师(1 个,多维度)
|
||||
|
||||
| 角色 | 测试维度 |
|
||||
|------|---------|
|
||||
| `tester` | ① 本地 pytest + 覆盖率报告 → ② 自动补全缺失测试 → ③ rsync 同步嵌入式设备 → ④ 板端实际运行(dmesg/崩溃日志/内存占用)→ ⑤ Playwright 验证 Web UI |
|
||||
|
||||
### 架构师层(3 个,相互制约)
|
||||
|
||||
| 角色 | 职责 | 制约关系 |
|
||||
|------|------|---------|
|
||||
| `arch-system` | 系统设计、性能、可扩展性 | 被 arch-security 和 pm 制约 |
|
||||
| `arch-security` | 安全审查、权限、数据合规 | 被 arch-system 和 pm 制约 |
|
||||
| `arch-reviewer` | 专门 review 其他 arch 提案 | 持有否决权 |
|
||||
|
||||
### 管理层(1 个)
|
||||
|
||||
| 角色 | 职责 |
|
||||
|------|------|
|
||||
| `pm` | 产品经理:用户价值、需求对齐、否决架构决策 |
|
||||
|
||||
---
|
||||
|
||||
## 二、MCP 工具矩阵
|
||||
|
||||
### 工具注册(`configs/mcp_tools.yaml`,支持热更新)
|
||||
|
||||
```yaml
|
||||
registry:
|
||||
sequential-thinking: "npx -y @modelcontextprotocol/server-sequential-thinking"
|
||||
fetch: "npx -y @modelcontextprotocol/server-fetch"
|
||||
playwright: "npx -y @playwright/mcp@latest"
|
||||
context7: "npx -y @upstash/context7-mcp@latest"
|
||||
|
||||
roles:
|
||||
dev-embedded: [sequential-thinking, fetch, context7]
|
||||
dev-backend: [sequential-thinking, fetch, context7]
|
||||
dev-ml: [sequential-thinking, fetch, context7]
|
||||
dev-test: [sequential-thinking, playwright]
|
||||
dev-refactor: [sequential-thinking]
|
||||
dev-kernel: [sequential-thinking, fetch]
|
||||
dev-lowlevel: [sequential-thinking, fetch]
|
||||
hw-engineer: [sequential-thinking, fetch, playwright]
|
||||
tester: [sequential-thinking, fetch, playwright]
|
||||
arch-system: [sequential-thinking, fetch, context7]
|
||||
arch-security: [sequential-thinking, fetch]
|
||||
arch-reviewer: [sequential-thinking]
|
||||
pm: [sequential-thinking, fetch]
|
||||
```
|
||||
|
||||
### Plan-First 系统提示(所有角色统一前缀)
|
||||
|
||||
```
|
||||
在执行任何写文件/命令操作前,必须先使用 sequentialthinking 工具完成:
|
||||
1. 问题拆解(≥3 步)
|
||||
2. 风险识别
|
||||
3. 方案选择(列出 2 种,说明选择理由)
|
||||
只有 sequentialthinking 调用完成后才能调用 Write/Edit/Bash。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、制约工作流(D 方案混合)
|
||||
|
||||
### 触发路由
|
||||
|
||||
| 任务类型 | 制约模式 |
|
||||
|---------|---------|
|
||||
| `fix_bug` / `improve_perf` / `code_review` | C 异步否决 |
|
||||
| `arch_optimize` / `security_review` / `kernel_change` / `hw_selection` | A 串行审批链 |
|
||||
| `new_feature` / `brainstorm` | B 对抗式提案 |
|
||||
|
||||
### 模式 C:异步否决(日常改动)
|
||||
|
||||
```
|
||||
dev-* 完成任务
|
||||
└─ [SPAWN:arch-reviewer] 自动注入
|
||||
├─ [VETO] 原因 → 父任务 vetoed,重入队(retry_count+1,上限 2)
|
||||
└─ [APPROVE] → 保持 done
|
||||
无响应 48h → 自动通过
|
||||
```
|
||||
|
||||
### 模式 A:串行审批链(重大变更)
|
||||
|
||||
```
|
||||
arch-system / dev-kernel 提案
|
||||
└─ [AWAIT:arch-security] → waiting_approval
|
||||
├─ [REJECT] 原因 → 返回提案者重试
|
||||
└─ [APPROVE]
|
||||
└─ [AWAIT:pm] → waiting_approval
|
||||
├─ [REJECT] → 返回提案者重试
|
||||
└─ [APPROVE] → dev-* 实施
|
||||
```
|
||||
|
||||
### 模式 B:对抗式提案(新功能)
|
||||
|
||||
```
|
||||
pm 接收 new_feature
|
||||
├─ [SPAWN:arch-system] → [PROPOSAL:arch-system] 方案 A
|
||||
└─ [SPAWN:arch-security] → [PROPOSAL:arch-security] 方案 B
|
||||
└─ arch-reviewer 汇总
|
||||
└─ [SELECT:arch-system 或 arch-security] → dev-* 实施
|
||||
```
|
||||
|
||||
### 结构化输出标签
|
||||
|
||||
| 标签 | 效果 |
|
||||
|------|------|
|
||||
| `[SPAWN:role] 描述` | 入队新任务,指定 agent_role |
|
||||
| `[AWAIT:role]` | 当前任务进入 waiting_approval,入队审查任务 |
|
||||
| `[APPROVE]` | 解除 waiting_approval,链路推进 |
|
||||
| `[REJECT] 原因` | 任务重入队,原因注入上下文 |
|
||||
| `[VETO] 原因` | 已完成任务回退为 vetoed,重入队 |
|
||||
| `[PROPOSAL:role] 内容` | 对抗式提案,等待 SELECT |
|
||||
| `[SELECT:role]` | 选中提案,触发 dev-* 实施 |
|
||||
|
||||
---
|
||||
|
||||
## 四、Scanner 升级 + Executor 改造
|
||||
|
||||
### Scanner 智能角色路由(`configs/role_routing.yaml`)
|
||||
|
||||
```yaml
|
||||
role_keywords:
|
||||
dev-kernel: [驱动, 内核, driver, kernel, dts, device tree, insmod]
|
||||
dev-lowlevel: [bsp, uboot, bootloader, 裸机, rtos, 引导, 启动]
|
||||
dev-embedded: [rknn, npu, 推理, 板端, rk3588, lite2, rknn_lite]
|
||||
dev-ml: [模型, 精度, 量化, 训练, onnx, accuracy, mAP]
|
||||
dev-backend: [api, fastapi, 数据库, 服务, http, redis]
|
||||
hw-engineer: [选型, 硬件, 芯片, 外设, 接口, bom, 功耗, datasheet]
|
||||
dev-test: [测试覆盖, coverage, 缺少测试, test case]
|
||||
dev-refactor: [重构, 技术债, 代码质量, 冗余]
|
||||
# 未匹配 → dev-embedded(嵌入式项目默认)
|
||||
```
|
||||
|
||||
### 新增 Scanner 信号源
|
||||
|
||||
| 来源 | 提取内容 | 任务类型 |
|
||||
|------|---------|---------|
|
||||
| `CLAUDE.md` 的 `## 规划功能` 章节 | 新功能描述 | `new_feature` |
|
||||
| 设备 dmesg(SSH 拉取) | kernel panic / oops | `kernel_change` |
|
||||
| `README.md` 的 `## 硬件` 章节 | 选型待定标记 | `hw_selection` |
|
||||
| `git log --since=7d` 失败 commit | revert 记录 | `fix_bug` + 失败上下文 |
|
||||
|
||||
### 任务状态机扩展
|
||||
|
||||
```
|
||||
pending → running → done ✅
|
||||
→ failed ❌ (retry ≤2,携带失败上下文)
|
||||
→ waiting_approval ⏳ (等待串行链审批,不占 Semaphore)
|
||||
→ vetoed 🚫 (被否决,重入队 retry)
|
||||
→ adversarial_pending ⚔️ (等待 SELECT)
|
||||
```
|
||||
|
||||
### Task 新增字段
|
||||
|
||||
```python
|
||||
retry_count: int = 0 # 最多重试 2 次
|
||||
parent_task_id: int = 0 # 追踪任务血缘
|
||||
constraint_type: str = "" # "veto_pending"|"approval_pending"|"adversarial"
|
||||
awaiting_role: str = "" # 串行链中等待哪个角色
|
||||
```
|
||||
|
||||
### Retry with Context 继承
|
||||
|
||||
失败/否决时重入队,前缀注入原因:
|
||||
```
|
||||
⚠️ 上次执行失败(第 N 次重试)
|
||||
原因:<reason>
|
||||
|
||||
--- 原始任务上下文 ---
|
||||
<original context>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、实施顺序(建议)
|
||||
|
||||
1. **基础设施**:Task 新字段 + 新状态 + `mcp_tools.yaml` + `role_routing.yaml`
|
||||
2. **MCP 注入**:`_build_cmd()` 按角色加载工具,Plan-First system prompt
|
||||
3. **新角色实现**:`dev-kernel`, `dev-lowlevel`, `hw-engineer` agent 文件
|
||||
4. **标签解析引擎**:Executor `_parse_agent_tags()` + `_handle_tags()`
|
||||
5. **制约状态机**:`waiting_approval`, `vetoed`, `adversarial_pending` 状态处理
|
||||
6. **Scanner 升级**:智能路由 + 新信号源
|
||||
7. **测试补全**:每个新 agent 和新状态的 pytest 覆盖
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,218 @@
|
||||
# Rockchip Agents Dashboard 设计文档
|
||||
|
||||
**日期**: 2026-03-06
|
||||
**状态**: 已批准,待实施
|
||||
|
||||
---
|
||||
|
||||
## 一、目标
|
||||
|
||||
构建一个独立的实时可视化看板,展示所有 Agent 任务的状态流转,支持多项目并行展示,并提供完整的操作控制台(手动入队、审批、重试、取消)。
|
||||
|
||||
---
|
||||
|
||||
## 二、架构总览
|
||||
|
||||
```
|
||||
scripts/dashboard.py ← 独立启动入口
|
||||
└─ FastAPI (port 8080)
|
||||
├─ GET /api/tasks 所有任务(可按 project 过滤)
|
||||
├─ POST /api/tasks 手动入队新任务
|
||||
├─ PATCH /api/tasks/{id} 修改优先级
|
||||
├─ POST /api/tasks/{id}/retry 重试 failed/vetoed 任务
|
||||
├─ POST /api/tasks/{id}/cancel 取消 running 任务(→ failed)
|
||||
├─ POST /api/tasks/{id}/approve APPROVE waiting_approval
|
||||
├─ POST /api/tasks/{id}/reject REJECT waiting_approval(附 reason)
|
||||
├─ GET /api/projects 已配置项目列表
|
||||
├─ WS /ws 实时推送(WebSocket)
|
||||
└─ GET / 静态托管 dashboard/dist/
|
||||
|
||||
dashboard/ ← React + Vite + Tailwind + shadcn/ui
|
||||
src/
|
||||
components/
|
||||
KanbanBoard.tsx 多项目 Kanban(按 project 分组)
|
||||
TaskCard.tsx 任务卡片(角色徽章、状态颜色、操作按钮)
|
||||
TaskDetail.tsx 右侧滑出详情面板
|
||||
DagView.tsx DAG 关系图(React Flow)
|
||||
NewTaskModal.tsx 手动入队对话框
|
||||
store/
|
||||
tasks.ts Zustand store
|
||||
lib/
|
||||
ws.ts WebSocket 管理(断线重连)
|
||||
api.ts REST API 封装
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、UI 布局
|
||||
|
||||
### 主布局
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────┐
|
||||
│ 🤖 Rockchip Agents Dashboard [+ 新任务] │
|
||||
│ ● WS connected 3 运行中 | 2 待审批 | 1 失败 │
|
||||
├──────────────────────────────────────────────────────────────────────┤
|
||||
│ [全览] [yolo] [claude-wx] [memory-analyze] …(项目标签页) │
|
||||
├──────────────────────────────────────────────────────────────────────┤
|
||||
│ [Kanban 看板] [DAG 关系图] │
|
||||
├──────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ▼ yolo (auto) ───────────────────────────────────────────────── │
|
||||
│ ┌─────────┬─────────┬──────┬──────┬────────────────┬──────────┐ │
|
||||
│ │ PENDING │ RUNNING │ DONE │ FAIL │ WAIT_APPROVAL │ VETOED │ │
|
||||
│ │ (2) │ (1) │ (8) │ (1) │ (1) ⚠️ │ (0) │ │
|
||||
│ │ [卡片] │ [卡片] │[卡片]│[卡片]│ [卡片] │ │ │
|
||||
│ │ │ │ │[重试]│ [APPROVE][REJ] │ │ │
|
||||
│ └─────────┴─────────┴──────┴──────┴────────────────┴──────────┘ │
|
||||
│ │
|
||||
│ ▼ claude-wx (auto) ────────────────────────────────────────────── │
|
||||
│ ┌─────────┬─────────┬──────┬──────┬────────────────┬──────────┐ │
|
||||
│ │ (0) │ (1) │ (5) │ (0) │ (0) │ (0) │ │
|
||||
│ └─────────┴─────────┴──────┴──────┴────────────────┴──────────┘ │
|
||||
└──────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 任务卡片
|
||||
|
||||
```
|
||||
┌──────────────────────────────┐
|
||||
│ 🔵 dev-embedded auto P:1 │ ← 角色徽章 / 模式 / 优先级
|
||||
│ 修复零检出率(模型精度问题) │ ← 标题(截断 60 字)
|
||||
│ ⏱ 4m 32s 🔄 retry: 0 │ ← 执行时长 / 重试次数
|
||||
│ parent: #12 │ ← 父任务链路(可点击跳转)
|
||||
└──────────────────────────────┘
|
||||
```
|
||||
|
||||
**状态颜色规则:**
|
||||
- `running`:蓝色边框 + 脉冲动画
|
||||
- `waiting_approval`:橙色边框 + ⚠️ 高亮
|
||||
- `failed`:红色边框
|
||||
- `vetoed`:紫色边框
|
||||
- `done`:绿色(半透明降调)
|
||||
- `pending`:灰色边框
|
||||
|
||||
### 右侧详情面板(点击卡片打开)
|
||||
|
||||
- 完整字段:title / type / context / result_summary / created_at / started_at / completed_at
|
||||
- parent_task_id 链接 → 点击跳转到父任务卡片
|
||||
- SPAWN 子任务列表(当前任务派生的所有子任务)
|
||||
- 操作按钮:按当前状态动态显示(Retry / Cancel / Approve / Reject)
|
||||
- 优先级滑块(1-5)
|
||||
|
||||
---
|
||||
|
||||
## 四、数据流 + 实时更新
|
||||
|
||||
### WebSocket 事件格式
|
||||
|
||||
```json
|
||||
// 初次连接:全量快照
|
||||
{ "type": "snapshot", "tasks": [...] }
|
||||
|
||||
// 任务状态变化
|
||||
{ "type": "task_update", "task": { "id": 42, "project": "yolo", "status": "running", ... } }
|
||||
|
||||
// 新任务入队
|
||||
{ "type": "task_create", "task": { ... } }
|
||||
```
|
||||
|
||||
### 后端 Watcher 线程
|
||||
|
||||
```
|
||||
后台线程(每 1 秒):
|
||||
SELECT * FROM tasks ORDER BY id DESC LIMIT 500
|
||||
→ 对比上次结果的哈希值(dict{id: hash(row)})
|
||||
→ 检测到 UPDATE/INSERT → broadcast 给所有 WS 连接
|
||||
→ 无变化 → 跳过
|
||||
```
|
||||
|
||||
### 前端状态管理(Zustand)
|
||||
|
||||
```typescript
|
||||
interface TaskStore {
|
||||
tasks: Map<number, Task>
|
||||
wsStatus: 'connected' | 'reconnecting' | 'offline'
|
||||
// 初次 snapshot 填充;后续 task_update/task_create 合并
|
||||
}
|
||||
```
|
||||
|
||||
断线自动重连(指数退避:1s → 2s → 4s → 8s),重连后服务端自动发送 snapshot。
|
||||
|
||||
### REST 操作(乐观更新)
|
||||
|
||||
点击 Retry/Approve 等 → 立即更新本地 UI → POST 到后端 → SQLite 写入 → Watcher 检测 → WS 广播确认。
|
||||
|
||||
---
|
||||
|
||||
## 五、Docker 部署
|
||||
|
||||
### Dockerfile.dashboard
|
||||
|
||||
```dockerfile
|
||||
# Stage 1: 构建 React 前端
|
||||
FROM node:20-alpine AS frontend-builder
|
||||
WORKDIR /frontend
|
||||
COPY dashboard/package*.json ./
|
||||
RUN npm ci
|
||||
COPY dashboard/ ./
|
||||
RUN npm run build
|
||||
|
||||
# Stage 2: Python FastAPI 运行时
|
||||
FROM python:3.10-slim AS runtime
|
||||
WORKDIR /app
|
||||
COPY requirements-dashboard.txt ./
|
||||
RUN pip install --no-cache-dir -r requirements-dashboard.txt
|
||||
COPY src/ ./src/
|
||||
COPY scripts/dashboard.py ./scripts/
|
||||
COPY --from=frontend-builder /frontend/dist ./dashboard/dist/
|
||||
EXPOSE 8080
|
||||
CMD ["python", "scripts/dashboard.py"]
|
||||
```
|
||||
|
||||
### docker-compose.yml 新增服务
|
||||
|
||||
```yaml
|
||||
services:
|
||||
dashboard:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.dashboard
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./data:/app/data # 共享 tasks.db(SQLite WAL 模式)
|
||||
- ./configs:/app/configs # 读取项目配置
|
||||
environment:
|
||||
- DB_PATH=/app/data/tasks.db
|
||||
- CONFIG_DIR=/app/configs
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、技术栈
|
||||
|
||||
| 层 | 技术 |
|
||||
|---|---|
|
||||
| 后端 | FastAPI + uvicorn + websockets |
|
||||
| 前端框架 | React 18 + TypeScript + Vite |
|
||||
| UI 组件 | shadcn/ui + Tailwind CSS v3 |
|
||||
| 状态管理 | Zustand |
|
||||
| DAG 图 | React Flow |
|
||||
| 部署 | Docker 多阶段构建 + docker-compose |
|
||||
|
||||
---
|
||||
|
||||
## 七、实施顺序
|
||||
|
||||
1. 后端 FastAPI 服务(`scripts/dashboard.py` + `src/rockchip_agents/dashboard/api.py`)
|
||||
2. WebSocket Watcher 线程
|
||||
3. React 前端脚手架(Vite + Tailwind + shadcn/ui)
|
||||
4. KanbanBoard + TaskCard 组件
|
||||
5. WebSocket 连接 + Zustand store
|
||||
6. TaskDetail 滑出面板 + REST 操作
|
||||
7. NewTaskModal 入队对话框
|
||||
8. DagView(React Flow)
|
||||
9. Dockerfile.dashboard + docker-compose 集成
|
||||
10. 构建验证 + 端到端测试
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,385 @@
|
||||
# Framework V2 设计文档
|
||||
|
||||
**日期**: 2026-03-08
|
||||
**状态**: 已确认
|
||||
|
||||
## 背景
|
||||
|
||||
当前框架(3 队列 × 15 角色)存在六个核心缺口:边缘验证无结构化、视觉测试单帧无基线、选题无人工门控、算法单线程、OS 覆盖过宽、设备接入仅 SSH。本文档描述 V2 框架的完整升级方案。
|
||||
|
||||
---
|
||||
|
||||
## §1 设备抽象层
|
||||
|
||||
### 1.1 问题
|
||||
|
||||
现有 `DeviceAgent` 仅封装 SSH,无法支持 ESP32/Nordic 等 MCU 设备(串口通信)及混合拓扑(部分 MCU 接开发机,部分接 RK3588)。
|
||||
|
||||
### 1.2 设备类型与连接方式
|
||||
|
||||
| 设备类型 | 连接方式 | 说明 |
|
||||
|---------|---------|------|
|
||||
| linux | ssh | RK3588、全志等 Linux 设备 |
|
||||
| esp32 | serial | 直连开发机 `/dev/ttyUSB*` |
|
||||
| nordic | serial | 直连开发机 `/dev/ttyACM*` |
|
||||
| esp32 | gateway | 经 RK3588 中转,RK3588 上的串口 |
|
||||
|
||||
### 1.3 `devices.yaml` 扩展
|
||||
|
||||
```yaml
|
||||
devices:
|
||||
rk3588:
|
||||
type: linux
|
||||
connect: ssh
|
||||
host: 192.168.123.181
|
||||
user: pi
|
||||
password: "123123"
|
||||
workspace: /home/pi/Desktop
|
||||
pre_cmd: "clashon && clashproxy on"
|
||||
|
||||
esp32-direct: # 直连开发机
|
||||
type: esp32
|
||||
connect: serial
|
||||
port: /dev/ttyUSB0
|
||||
baud: 115200
|
||||
workspace: /tmp/esp32
|
||||
|
||||
esp32-via-rk3588: # 接在 RK3588 上
|
||||
type: esp32
|
||||
connect: gateway
|
||||
via: rk3588
|
||||
port: /dev/ttyUSB1 # RK3588 上的串口号
|
||||
baud: 115200
|
||||
|
||||
nordic-dev:
|
||||
type: nordic
|
||||
connect: serial
|
||||
port: /dev/ttyACM0
|
||||
baud: 115200
|
||||
```
|
||||
|
||||
### 1.4 `DeviceManager` 统一接口
|
||||
|
||||
新建 `tools/device_manager.py`,替代 `tools/device_agent.py`:
|
||||
|
||||
```python
|
||||
class DeviceAdapter(Protocol):
|
||||
def run_test(self, cmd: str, cwd: str, timeout: int = 120) -> TestResult: ...
|
||||
def get_metrics(self) -> DeviceMetrics: ...
|
||||
def send_file(self, local: Path, remote: str) -> bool: ...
|
||||
def read_logs(self, n: int = 50) -> list[str]: ...
|
||||
def flash(self, firmware: Path) -> bool: ... # MCU 专用
|
||||
|
||||
class TestResult:
|
||||
rc: int
|
||||
stdout: str
|
||||
stderr: str
|
||||
duration_s: float
|
||||
pass_: bool # 解析自输出(PASS/FAIL/ERROR)
|
||||
|
||||
class DeviceMetrics:
|
||||
device: str
|
||||
timestamp: str
|
||||
# Linux 设备
|
||||
cpu_pct: float | None
|
||||
mem_used_mb: float | None
|
||||
npu_util_pct: float | None # RKNN 专用
|
||||
fps: float | None # 从推理日志解析
|
||||
# MCU 设备
|
||||
inference_latency_ms: float | None # 从 UART 输出解析
|
||||
ram_used_kb: int | None # 从编译报告或 UART 解析
|
||||
flash_used_kb: int | None
|
||||
```
|
||||
|
||||
**适配器实现:**
|
||||
|
||||
- `SSHAdapter`:复用现有 paramiko 逻辑,封装为统一接口
|
||||
- `SerialAdapter`:`pyserial` + 命令/响应解析(行尾 `\r\n`)
|
||||
- ESP32:`esptool.py` 烧录,`AT` 命令或自定义协议测试
|
||||
- Nordic:`nrfjprog` / `west flash` 烧录
|
||||
- `GatewayAdapter`:SSH 到中转设备,再通过其串口操作目标 MCU
|
||||
|
||||
**`DeviceConfig` 扩展(`config.py`):**
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class DeviceConfig:
|
||||
host: str = ""
|
||||
user: str = ""
|
||||
password: str = ""
|
||||
workspace: str = ""
|
||||
pre_cmd: str = ""
|
||||
type: str = "linux" # linux | esp32 | nordic
|
||||
connect: str = "ssh" # ssh | serial | gateway
|
||||
via: str = "" # gateway 时的中转设备名
|
||||
port: str = "" # 串口路径
|
||||
baud: int = 115200
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## §2 EdgeValidatorService
|
||||
|
||||
### 2.1 定位
|
||||
|
||||
部署在 RK3588 上的轻量 FastAPI 服务(`services/edge_validator/`),负责:
|
||||
- 向开发机 agents 暴露 HTTP API(无需 agents 直接 SSH 操作)
|
||||
- 管理 RK3588 本机测试执行
|
||||
- 管理接在 RK3588 上的 MCU 设备(串口中转)
|
||||
|
||||
### 2.2 API 端点
|
||||
|
||||
```
|
||||
GET /health
|
||||
GET /devices # 列出所有已识别的串口设备
|
||||
GET /metrics/{device_name} # 实时指标(linux: /proc; mcu: UART 解析)
|
||||
POST /run-test/{device_name} # 执行测试命令
|
||||
body: {cmd, cwd, timeout}
|
||||
resp: {rc, stdout, stderr, duration_s, pass_}
|
||||
POST /flash/{device_name} # 烧录固件(MCU 专用)
|
||||
body: {firmware_b64, offset}
|
||||
POST /screenshot # 截取 RK3588 显示内容(需 X11 或 framebuffer)
|
||||
resp: {image_b64, width, height}
|
||||
GET /logs/{device_name}/{n} # 最近 N 行推理日志
|
||||
```
|
||||
|
||||
### 2.3 部署
|
||||
|
||||
```bash
|
||||
# RK3588 上
|
||||
cd /home/pi/Desktop/edge_validator
|
||||
pip install fastapi uvicorn pyserial
|
||||
uvicorn main:app --host 0.0.0.0 --port 8899
|
||||
# systemd 自启
|
||||
```
|
||||
|
||||
### 2.4 全员边端感知
|
||||
|
||||
`developer.py` 的 `_build_device_context()` 升级,所有 agents 系统提示包含:
|
||||
|
||||
```
|
||||
【边端设备访问】
|
||||
本公司产品目标为边端落地,请优先验证边端实施效果。
|
||||
|
||||
设备列表:
|
||||
- rk3588 [linux/ssh]:pi@192.168.123.181
|
||||
EdgeValidatorService: http://192.168.123.181:8899
|
||||
fetch GET http://192.168.123.181:8899/metrics/rk3588
|
||||
fetch POST http://192.168.123.181:8899/run-test/rk3588
|
||||
|
||||
- esp32-direct [esp32/serial]:/dev/ttyUSB0 @115200
|
||||
pyserial 直接操作,烧录:esptool.py --port /dev/ttyUSB0 ...
|
||||
|
||||
- esp32-via-rk3588 [esp32/gateway]:
|
||||
fetch POST http://192.168.123.181:8899/run-test/esp32-via-rk3588
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## §3 选题门控机制
|
||||
|
||||
### 3.1 标签分类与审批规则
|
||||
|
||||
| 标签 | 适用场景 | 处理方式 |
|
||||
|------|---------|---------|
|
||||
| `[需求] project: desc` | 现有项目的优化/修复 | 直接 `pending`,无需审批 |
|
||||
| `[集成] desc` | 现有能力整合 | 直接 `pending`,无需审批 |
|
||||
| `[选题] desc` | **新方向/新课题** | `topic_pending`,Dashboard 审批 |
|
||||
| `[市场情报] key=value` | 竞品/趋势信息 | 写入 `__market__` facts,无需审批 |
|
||||
|
||||
**[选题] 使用条件(系统提示中明确):**
|
||||
- ✓ 当前项目列表中不存在的全新技术领域
|
||||
- ✓ 需要新建项目的方向
|
||||
- ✓ 超出平台目标范围的探索性方向
|
||||
- ✗ 现有项目的功能/性能优化 → 用 `[需求]`
|
||||
- ✗ 现有能力的组合整合 → 用 `[集成]`
|
||||
|
||||
### 3.2 Task 状态扩展
|
||||
|
||||
```python
|
||||
# queue.py 新增状态
|
||||
# pending → running → done/failed(现有)
|
||||
# topic_pending → (用户确认) → pending → running → done
|
||||
# → (用户否决) → vetoed
|
||||
```
|
||||
|
||||
```sql
|
||||
-- topic_pending 任务不被 dequeue() 取出,直到用户手动确认
|
||||
-- 新增 status CHECK 约束
|
||||
status TEXT CHECK(status IN (
|
||||
'pending', 'running', 'done', 'failed',
|
||||
'vetoed', 'waiting_approval',
|
||||
'topic_pending' -- 新增
|
||||
)) DEFAULT 'pending'
|
||||
```
|
||||
|
||||
### 3.3 Dashboard 操作界面
|
||||
|
||||
在现有任务列表中新增"待确认选题"分区:
|
||||
|
||||
```
|
||||
┌─ 待确认选题 (topic_pending) ──────────────────────────────────┐
|
||||
│ #47 [选题] 面向工业质检的边端视觉方案 planner 2026-03-08 │
|
||||
│ 价值分析:当前平台 YOLO 精度满足工业质检需求,市场空白... │
|
||||
│ [✅ 确认执行] [❌ 否决] [💬 修改意见...] │
|
||||
└────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Dashboard API 新增:
|
||||
```
|
||||
POST /api/tasks/{id}/approve-topic → status: pending
|
||||
POST /api/tasks/{id}/reject-topic body: {reason} → status: vetoed
|
||||
```
|
||||
|
||||
### 3.4 飞书通知(可选,Dashboard 设置开关)
|
||||
|
||||
```
|
||||
飞书通知内容(单向 webhook POST,无回调):
|
||||
📌 新选题待确认 #47
|
||||
方向:面向工业质检的边端视觉方案
|
||||
提出人:planner
|
||||
→ 在 Dashboard 中审批:http://<dev-machine>:8080/topics
|
||||
```
|
||||
|
||||
Dashboard 设置页新增:`飞书选题通知:[ 开启 / 关闭 ]`(默认关闭)
|
||||
|
||||
### 3.5 Market-PM 长期情报积累
|
||||
|
||||
- `scheduler.py` 新增 12h 定时任务触发 market-pm 情报收集
|
||||
- 收集输出:`[市场情报] __market__.competitor.rk3588=xxx`
|
||||
- 写入 ProjectMemory `__market__.*` facts(WRITE_ROLES 新增 `market-pm`)
|
||||
- 积累 ≥5 条情报后,market-pm 可生成 `[选题]` 供人工确认
|
||||
|
||||
---
|
||||
|
||||
## §4 算法角色三专项化
|
||||
|
||||
### 4.1 角色分工
|
||||
|
||||
| 角色 | 专项领域 | 边端验证设备 |
|
||||
|------|---------|------------|
|
||||
| `algo-antishake` | EIS/OIS、陀螺仪融合、光流防抖 | RK3588 (RKNN) + ESP32 (IMU) |
|
||||
| `algo-position` | UWB/BLE RSSI、ToF、多点定位 | Nordic + ESP32 |
|
||||
| `algo-nav` | GPS+IMU 融合、Kalman 滤波、姿态解算 | ESP32 (GPS+IMU模组) |
|
||||
|
||||
### 4.2 实现
|
||||
|
||||
三者均继承 `AlgoResearcherAgent`,追加领域专项系统提示,均注入:
|
||||
- `__hardware__` facts(硬件约束)
|
||||
- EdgeValidatorService URL(边端验证)
|
||||
- 对应设备串口/SSH 信息
|
||||
|
||||
### 4.3 base_opt 队列并发调整
|
||||
|
||||
```yaml
|
||||
base_opt:
|
||||
max_concurrent: 5 # 3 (原) → 5
|
||||
roles: [algo-antishake, algo-position, algo-nav, base-validator, base-architect]
|
||||
# 移除 os-engineer(os-engineer 移至 project_delivery 队列,按需触发)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## §5 OS Engineer 性能专项化
|
||||
|
||||
### 5.1 新定位
|
||||
|
||||
`os-engineer` 不再负责 RTOS/Linux 基础搭建(由 `dev-kernel`/`dev-lowlevel` 承接),专注**系统性能瓶颈识别与优化建议**。
|
||||
|
||||
### 5.2 专项领域
|
||||
|
||||
| 领域 | 工具 | 输出 |
|
||||
|------|------|------|
|
||||
| 内存 | `smem`, `valgrind --leak-check`, `/proc/meminfo`, OOM 溯源 | 内存泄漏报告、OOM 触发链 |
|
||||
| 网络 | `iperf3`, `ss`, `tc qdisc`, `tcpdump` | 带宽/延迟瓶颈报告 |
|
||||
| 视觉库 | OpenCV NEON/SIMD 利用率、Mat 内存开销时序 | 帧处理优化建议 |
|
||||
| AI 算子 | RKNN profiler、NPU 算子耗时、算子融合分析 | 推理优化路径 |
|
||||
|
||||
输出:
|
||||
- `docs/perf_reports/YYYY-MM-DD-{project}.md` 火焰图 + 瓶颈分析
|
||||
- `[记忆] __hardware__: perf.{category}.bottleneck=xxx`
|
||||
- `[需求] project: 基于剖析结果的具体优化任务`
|
||||
|
||||
### 5.3 队列归属
|
||||
|
||||
移入 `project_delivery` 队列(按需触发,不占 base_opt 常驻资源):
|
||||
```yaml
|
||||
project_delivery:
|
||||
roles: [developer, dev-kernel, dev-lowlevel, system-tester, architect, os-engineer]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## §6 边缘验证结构化
|
||||
|
||||
### 6.1 base-validator 增强
|
||||
|
||||
```
|
||||
输出标签:
|
||||
[验证结果] project=yolo fps=15.3 precision=0.82 pass=true
|
||||
[验证退化] project=yolo metric=fps current=12.1 baseline=15.3 drop_pct=20.9
|
||||
|
||||
退化阈值:fps drop >10% 或 precision drop >5% → 自动生成 [需求]
|
||||
|
||||
ProjectMemory facts 写入(WRITE_ROLES 含 base-validator):
|
||||
__hardware__: yolo.fps_baseline=15.3
|
||||
__hardware__: yolo.precision_baseline=0.82
|
||||
__hardware__: yolo.last_validated=2026-03-08T10:00:00
|
||||
```
|
||||
|
||||
### 6.2 vision-analyst 增强
|
||||
|
||||
```
|
||||
多帧分析流程(default 5帧,间隔 2s):
|
||||
1. 调用 EdgeValidatorService /screenshot × 5
|
||||
2. 逐帧 Claude Vision 分析:bbox 合理性、FPS 读取、漏检/误检
|
||||
3. 跨帧分析:检测稳定性、bbox 抖动程度
|
||||
4. 存档:docs/visual_tests/YYYY-MM-DD-{project}-{seq}.png
|
||||
|
||||
输出标签:
|
||||
[视觉结果] fps=15.3 stability=HIGH detected=3 false_pos=0 pass=true
|
||||
[视觉退化] fps_trend=降 bbox_jitter=高 → [需求] yolo: 检测框抖动需平滑
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## §7 完整变更清单(按优先级)
|
||||
|
||||
| P | 模块 | 变更说明 |
|
||||
|---|------|---------|
|
||||
| P0 | `configs/devices.yaml` | 扩展 type/connect/via/port/baud 字段 |
|
||||
| P0 | `src/.../config.py` | `DeviceConfig` 扩展对应字段 |
|
||||
| P0 | `src/.../tools/device_manager.py` | 新建,SSHAdapter/SerialAdapter/GatewayAdapter |
|
||||
| P0 | `services/edge_validator/` | 新建 FastAPI 服务 + systemd 配置 |
|
||||
| P1 | `src/.../core/queue.py` | 新增 `topic_pending` status;`execution_host` 字段 |
|
||||
| P1 | `src/.../agents/planner.py` | `[选题]` 标签解析;写入 `topic_pending` 任务 |
|
||||
| P1 | `src/.../agents/market_pm.py` | `[市场情报]` 标签;12h 定时情报收集 |
|
||||
| P1 | `src/.../core/scheduler.py` | market-pm 12h job;os-engineer 移入 project_delivery |
|
||||
| P1 | `src/.../dashboard/api.py` | approve-topic / reject-topic 端点;topic 列表页 |
|
||||
| P1 | `src/.../agents/developer.py` | `_build_device_context()` 多设备+EdgeValidatorService |
|
||||
| P2 | `src/.../agents/algo_antishake.py` | 新建,继承 AlgoResearcherAgent |
|
||||
| P2 | `src/.../agents/algo_position.py` | 新建 |
|
||||
| P2 | `src/.../agents/algo_nav.py` | 新建 |
|
||||
| P2 | `src/.../agents/os_engineer.py` | 系统提示重写,聚焦性能剖析 |
|
||||
| P2 | `src/.../agents/base_validator.py` | 结构化验证结果 + 退化检测 |
|
||||
| P2 | `src/.../agents/vision_analyst.py` | 多帧分析 + EdgeValidatorService 截图 |
|
||||
| P2 | `src/.../core/executor.py` | `_make_agent()` 新增三 algo 角色路由 |
|
||||
| P2 | `configs/agents.yaml` | base_opt max_concurrent 3→5;project_delivery 加 os-engineer |
|
||||
| P2 | `configs/mcp_tools.yaml` | 三 algo 角色工具配置 |
|
||||
| P3 | `src/.../tools/project_memory.py` | WRITE_ROLES 加 `base-validator`, `market-pm` |
|
||||
| P3 | 飞书通知设置 | Dashboard 设置页新增"选题飞书通知"开关 |
|
||||
|
||||
---
|
||||
|
||||
## 决策汇总
|
||||
|
||||
| 维度 | 决策 |
|
||||
|------|------|
|
||||
| 多端协同 | EdgeValidatorService (FastAPI) + 全员 HTTP 调用 |
|
||||
| 设备协议 | SSHAdapter / SerialAdapter / GatewayAdapter 统一抽象 |
|
||||
| 拓扑 | 混合:MCU 可接开发机或 RK3588 |
|
||||
| 选题审批 | Dashboard 操作 + 可选飞书单向通知(无回调) |
|
||||
| 选题范围 | 仅新方向/新课题 [选题];日常优化 [需求] 直通 |
|
||||
| 算法分工 | 三专项角色替换通用 algo-researcher |
|
||||
| OS 定位 | 性能瓶颈剖析专项,移入 project_delivery |
|
||||
| 边端感知 | 所有 agents 系统提示含边端设备 + EdgeValidatorService 信息 |
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,184 @@
|
||||
# PlannerAgent 设计文档
|
||||
|
||||
**日期**: 2026-03-08
|
||||
**状态**: 已确认
|
||||
|
||||
## 背景
|
||||
|
||||
rockchip-agents 目前缺少"需求侧"驱动力:Architect Agent 做跨项目分析,Developer Agent 执行任务,但没有角色持续评估平台差距、自主生成高价值需求。PlannerAgent 填补这一空白。
|
||||
|
||||
核心理念:agents 是一个 **RK3588 生态协作团队**,不是传统项目组。部分角色(planner/architect)需要全局视野,其他角色(developer/tester)聚焦项目但具备平台方向感。
|
||||
|
||||
---
|
||||
|
||||
## §1 架构 & 触发机制
|
||||
|
||||
### 角色定位
|
||||
|
||||
PlannerAgent 扮演"产品经理":
|
||||
|
||||
- 持续读取三层目标(平台→项目→任务历史)
|
||||
- 评估现状与目标的差距
|
||||
- 生成高价值需求入队
|
||||
|
||||
### 双触发机制
|
||||
|
||||
| 触发点 | 配置 | 说明 |
|
||||
|--------|------|------|
|
||||
| Scheduler 定时 | `interval_hours: 6`(独立于主调度的 1h) | 低优先级后台扫描 |
|
||||
| Executor 队列空 | `on_queue_empty: true` | 无任务时主动补充需求 |
|
||||
|
||||
### 并发控制
|
||||
|
||||
- `agent_role = "planner"`,不占用 developer/tester 并发槽
|
||||
- `max_concurrent_planner: 1`(同时只有一个 PlannerAgent 运行)
|
||||
|
||||
---
|
||||
|
||||
## §2 输入上下文 & 系统提示
|
||||
|
||||
### 三层上下文结构
|
||||
|
||||
```
|
||||
[平台目标]
|
||||
构建 RK3588 NPU 平台上的模块化 AI 推理生态……
|
||||
|
||||
[项目概况]
|
||||
yolo: 目标检测(当前目标:修复板端零检出)
|
||||
mediapipe: 关键点检测(当前目标:提升 Pose FPS)
|
||||
embedding: 向量检索(目标:完善 API)
|
||||
……
|
||||
|
||||
[近期任务历史 - 最近 20 条]
|
||||
done: yolo - 修复 INT8 量化精度损失
|
||||
failed: ui-tars - RKNN 模型转换超时
|
||||
……
|
||||
|
||||
[当前待处理任务数: N]
|
||||
```
|
||||
|
||||
### 系统提示词(PLANNER_SYSTEM)
|
||||
|
||||
```
|
||||
你是 RK3588 AI 生态团队的产品经理。
|
||||
|
||||
你的职责:
|
||||
1. 分析平台目标与当前项目状态的差距
|
||||
2. 识别跨模块整合机会
|
||||
3. 提出高价值的下一步需求
|
||||
|
||||
输出格式(报告末尾,每行一条):
|
||||
- [需求] <项目名>: 具体需求描述
|
||||
- [集成] 跨模块整合需求描述
|
||||
- [目标] <项目名>: 更新后的项目目标
|
||||
- [平台目标] 更新后的平台整体目标(仅当需要修订时)
|
||||
|
||||
去重原则:若相似需求已在队列中,请勿重复生成。
|
||||
```
|
||||
|
||||
### 输出标签解析
|
||||
|
||||
| 标签 | 目标 | 任务类型 | 优先级 |
|
||||
|------|------|---------|--------|
|
||||
| `[需求]` | 指定项目 | `feature` | 3 |
|
||||
| `[集成]` | `research`(跨项目) | `architect` | 3 |
|
||||
| `[目标]` | 指定项目 | — | 写入 ProjectMemory |
|
||||
| `[平台目标]` | `__platform__` | — | 写入 ProjectMemory |
|
||||
|
||||
---
|
||||
|
||||
## §3 三层目标体系
|
||||
|
||||
### 存储
|
||||
|
||||
`ProjectMemory.project_goals` 表,`project` 字段新增特殊值 `__platform__`:
|
||||
|
||||
| project 值 | 含义 |
|
||||
|-----------|------|
|
||||
| `__platform__` | 平台级目标(整个 RK3588 生态方向) |
|
||||
| `yolo` / `embedding` / … | 项目级目标 |
|
||||
|
||||
### 写入权限
|
||||
|
||||
`WRITE_ROLES = {"productizer", "architect", "planner"}`(新增 `planner`)
|
||||
|
||||
### 角色上下文注入分层
|
||||
|
||||
```
|
||||
planner / architect(全局视野)
|
||||
├─ platform_goal(完整)
|
||||
├─ 所有 project_goal(完整)
|
||||
└─ 所有 project_facts(精简)
|
||||
|
||||
developer / tester(项目聚焦)
|
||||
├─ platform_goal(首句摘要,锚定方向感)
|
||||
├─ 当前 project_goal(完整)
|
||||
└─ 当前 project_facts(完整)
|
||||
|
||||
productizer(跨项目评分)
|
||||
├─ platform_goal(首句摘要)
|
||||
└─ 所有 project_goal(摘要)
|
||||
```
|
||||
|
||||
### API 变更
|
||||
|
||||
```python
|
||||
# 新增 role 参数
|
||||
def get_context_for_prompt(
|
||||
self,
|
||||
project: str,
|
||||
role: str = "developer",
|
||||
) -> str
|
||||
```
|
||||
|
||||
### 平台目标初始化
|
||||
|
||||
PlannerAgent 首次运行时,若 `__platform__` 无记录,自动写入:
|
||||
|
||||
```
|
||||
构建 RK3588 NPU 平台上的模块化 AI 推理生态,
|
||||
持续提升各推理模块成熟度,探索跨模块整合产品价值。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## §4 错误处理 & 测试
|
||||
|
||||
### 错误处理
|
||||
|
||||
| 场景 | 处理方式 |
|
||||
|------|---------|
|
||||
| Claude CLI 不可达 | 返回 failed,不阻塞 executor |
|
||||
| 输出无有效标签 | 正常返回(已达目标是合理结果) |
|
||||
| 重复需求 | 比对 pending+running 任务 title 前 40 字,存在则跳过 |
|
||||
| `__platform__` 未初始化 | 首次运行自动写入默认值 |
|
||||
| platform_goal 写入失败 | 降级:只注入项目级上下文,不中断 |
|
||||
|
||||
### 测试
|
||||
|
||||
```
|
||||
tests/test_planner.py
|
||||
├── test_planner_dedup # 重复标题不重复入队
|
||||
├── test_planner_platform_init # 首次运行自动初始化 platform_goal
|
||||
├── test_planner_parse_tags # 四种标签解析正确
|
||||
├── test_planner_context_layers # role="planner" 包含三层上下文
|
||||
└── test_planner_empty_output # 无输出时不崩溃
|
||||
|
||||
tests/test_project_memory.py(新增)
|
||||
├── test_platform_goal_write # set_goal("__platform__") 存储正确
|
||||
└── test_context_role_filter # developer role 只获得平台摘要+项目完整
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 决策汇总
|
||||
|
||||
| 维度 | 决策 |
|
||||
|------|------|
|
||||
| 触发 | Scheduler 6h + executor 队列空双触发 |
|
||||
| 去重 | title 前 40 字 + pending 任务扫描混合 |
|
||||
| 输出标签 | `[需求]` / `[集成]` / `[目标]` / `[平台目标]` |
|
||||
| 存储 | ProjectMemory,`__platform__` 特殊 project |
|
||||
| 角色视野 | planner/architect 全三层;developer/tester 平台摘要+项目完整 |
|
||||
| 写权限 | `WRITE_ROLES` 新增 `planner`,可写 platform_goal |
|
||||
| 实现模式 | 复用 `ArchitectAgent._analyze` 模式(claude --print + report 工具集) |
|
||||
@@ -0,0 +1,734 @@
|
||||
# PlannerAgent Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** 实现 PlannerAgent(产品经理角色),基于三层目标体系(platform/project/task)持续评估差距并自主生成高价值需求入队。
|
||||
|
||||
**Architecture:**
|
||||
1. `ProjectMemory.get_context_for_prompt` 新增 `role` 参数,实现分层上下文注入
|
||||
2. `WRITE_ROLES` 新增 `"planner"`,支持写入 `__platform__` 级目标
|
||||
3. 新建 `agents/planner.py`,复用 `ArchitectAgent._analyze` 模式(`claude --print --output-format json`)
|
||||
4. Scheduler 新增独立 6h planner 定时任务;Executor 队列空时触发 planner
|
||||
|
||||
**Tech Stack:** Python 3.10, SQLite, `claude --print` subprocess, APScheduler, pytest
|
||||
|
||||
---
|
||||
|
||||
### Task 1: ProjectMemory — 分层上下文 + planner 写权限
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/rockchip_agents/tools/project_memory.py`
|
||||
- Test: `tests/test_project_memory.py`
|
||||
|
||||
**Step 1: 写失败测试**
|
||||
|
||||
在 `tests/test_project_memory.py` 末尾追加:
|
||||
|
||||
```python
|
||||
def test_platform_goal_write_and_read(tmp_path):
|
||||
mem = ProjectMemory(db_path=tmp_path / "mem.db")
|
||||
mem.set_goal("__platform__", "RK3588 生态目标", role="planner")
|
||||
g = mem.get_goal("__platform__")
|
||||
assert g is not None
|
||||
assert g.goal_text == "RK3588 生态目标"
|
||||
assert g.updated_by == "planner"
|
||||
|
||||
|
||||
def test_context_role_planner_includes_platform(tmp_path):
|
||||
mem = ProjectMemory(db_path=tmp_path / "mem.db")
|
||||
mem.set_goal("__platform__", "平台总目标", role="planner")
|
||||
mem.set_goal("yolo", "yolo 项目目标", role="architect")
|
||||
ctx = mem.get_context_for_prompt("yolo", role="planner")
|
||||
assert "平台总目标" in ctx
|
||||
assert "yolo 项目目标" in ctx
|
||||
|
||||
|
||||
def test_context_role_developer_gets_platform_summary(tmp_path):
|
||||
mem = ProjectMemory(db_path=tmp_path / "mem.db")
|
||||
mem.set_goal("__platform__", "平台总目标,后面很长很长的内容不应出现", role="planner")
|
||||
mem.set_goal("yolo", "yolo 项目详细目标", role="architect")
|
||||
ctx = mem.get_context_for_prompt("yolo", role="developer")
|
||||
assert "平台总目标" in ctx # 首句出现
|
||||
assert "yolo 项目详细目标" in ctx # 项目完整
|
||||
```
|
||||
|
||||
**Step 2: 运行确认失败**
|
||||
|
||||
```bash
|
||||
cd /data/rockchip/agents && source venv/bin/activate
|
||||
pytest tests/test_project_memory.py::test_platform_goal_write_and_read \
|
||||
tests/test_project_memory.py::test_context_role_planner_includes_platform \
|
||||
tests/test_project_memory.py::test_context_role_developer_gets_platform_summary -v
|
||||
```
|
||||
|
||||
预期:3 个 FAIL(`set_goal` 拒绝 planner / `get_context_for_prompt` 无 role 参数)
|
||||
|
||||
**Step 3: 修改 project_memory.py**
|
||||
|
||||
修改 `WRITE_ROLES`(第 14 行):
|
||||
|
||||
```python
|
||||
WRITE_ROLES: frozenset[str] = frozenset({"productizer", "architect", "planner"})
|
||||
```
|
||||
|
||||
修改 `get_context_for_prompt` 签名(第 165 行):
|
||||
|
||||
```python
|
||||
def get_context_for_prompt(
|
||||
self,
|
||||
project: str,
|
||||
role: str = "developer",
|
||||
query: str | None = None,
|
||||
) -> str:
|
||||
```
|
||||
|
||||
在方法体开头,`parts: list[str] = []` 之后,在读取 goal 之前插入平台目标逻辑:
|
||||
|
||||
```python
|
||||
# 平台目标注入(role 分层)
|
||||
_GLOBAL_ROLES = frozenset({"planner", "architect"})
|
||||
platform_goal = self.get_goal("__platform__")
|
||||
if platform_goal:
|
||||
if role in _GLOBAL_ROLES:
|
||||
parts.append(
|
||||
f"【平台长期目标({platform_goal.updated_at[:10]})】\n"
|
||||
f"{platform_goal.goal_text}"
|
||||
)
|
||||
else:
|
||||
# developer/tester/productizer 只获得首句摘要,锚定方向感
|
||||
first_sentence = platform_goal.goal_text.split(",")[0].split("。")[0][:60]
|
||||
parts.append(f"【平台方向】{first_sentence}")
|
||||
|
||||
# planner/architect 还需要汇总所有项目目标
|
||||
if role in _GLOBAL_ROLES and project != "__platform__":
|
||||
all_goals = self.list_goals()
|
||||
proj_summaries = [
|
||||
f" {name}: {g.goal_text[:80]}"
|
||||
for name, g in all_goals.items()
|
||||
if name != "__platform__"
|
||||
]
|
||||
if proj_summaries:
|
||||
parts.append("【各项目目标概览】\n" + "\n".join(proj_summaries))
|
||||
```
|
||||
|
||||
**Step 4: 运行测试**
|
||||
|
||||
```bash
|
||||
pytest tests/test_project_memory.py -v
|
||||
```
|
||||
|
||||
预期:所有测试 PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/rockchip_agents/tools/project_memory.py tests/test_project_memory.py
|
||||
git commit -m "feat(memory): add role-based layered context + planner write permission"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: PlannerAgent — 核心实现
|
||||
|
||||
**Files:**
|
||||
- Create: `src/rockchip_agents/agents/planner.py`
|
||||
- Test: `tests/test_planner.py`
|
||||
|
||||
**Step 1: 写失败测试**
|
||||
|
||||
创建 `tests/test_planner.py`:
|
||||
|
||||
```python
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from rockchip_agents.agents.planner import PlannerAgent, PLANNER_SYSTEM
|
||||
from rockchip_agents.agents.developer import AgentResult
|
||||
from rockchip_agents.config import AgentsConfig, ClaudeConfig, SchedulerConfig
|
||||
from rockchip_agents.core.queue import Task, TaskQueue
|
||||
from rockchip_agents.tools.project_memory import ProjectMemory
|
||||
|
||||
|
||||
def _make_config():
|
||||
cfg = MagicMock(spec=AgentsConfig)
|
||||
cfg.projects = {"yolo": MagicMock(description="目标检测", mode="confirm")}
|
||||
cfg.claude = ClaudeConfig(model="claude-sonnet-4-6", api_key=None)
|
||||
return cfg
|
||||
|
||||
|
||||
def _make_task():
|
||||
return Task(
|
||||
project="research", type="planner", title="PlannerAgent 周期分析",
|
||||
context="", priority=5, mode="report", agent_role="planner",
|
||||
)
|
||||
|
||||
|
||||
def test_planner_parse_tags(tmp_path):
|
||||
"""[需求]/[集成]/[目标]/[平台目标] 均被正确解析"""
|
||||
mem = ProjectMemory(db_path=tmp_path / "m.db")
|
||||
q = TaskQueue(db_path=tmp_path / "t.db")
|
||||
agent = PlannerAgent(_make_config(), queue=q, memory=mem)
|
||||
|
||||
report = (
|
||||
"分析报告内容...\n"
|
||||
"- [需求] yolo: 支持多目标追踪\n"
|
||||
"- [集成] embedding+yolo 联合检索\n"
|
||||
"- [目标] yolo: 提升板端 FPS 至 30\n"
|
||||
"- [平台目标] 成为 RK3588 推理生态标准方案\n"
|
||||
)
|
||||
agent._extract_and_enqueue(report)
|
||||
|
||||
tasks = [dict(r) for r in q._conn().execute(
|
||||
"SELECT project, type, title FROM tasks"
|
||||
).fetchall()]
|
||||
titles = [t["title"] for t in tasks]
|
||||
assert any("多目标追踪" in t for t in titles), "需求任务未入队"
|
||||
assert any("embedding+yolo" in t for t in titles), "集成任务未入队"
|
||||
|
||||
yolo_goal = mem.get_goal("yolo")
|
||||
assert yolo_goal is not None
|
||||
assert "FPS" in yolo_goal.goal_text
|
||||
|
||||
platform_goal = mem.get_goal("__platform__")
|
||||
assert platform_goal is not None
|
||||
assert "标准方案" in platform_goal.goal_text
|
||||
|
||||
|
||||
def test_planner_dedup(tmp_path):
|
||||
"""相似标题(前 40 字匹配)不重复入队"""
|
||||
mem = ProjectMemory(db_path=tmp_path / "m.db")
|
||||
q = TaskQueue(db_path=tmp_path / "t.db")
|
||||
agent = PlannerAgent(_make_config(), queue=q, memory=mem)
|
||||
|
||||
report1 = "- [需求] yolo: 支持多目标追踪以提升产品能力\n"
|
||||
report2 = "- [需求] yolo: 支持多目标追踪以提升产品能力(重复)\n"
|
||||
agent._extract_and_enqueue(report1)
|
||||
agent._extract_and_enqueue(report2)
|
||||
|
||||
count = q._conn().execute("SELECT count(*) FROM tasks").fetchone()[0]
|
||||
assert count == 1, "重复需求被重复入队"
|
||||
|
||||
|
||||
def test_planner_platform_init(tmp_path):
|
||||
"""首次运行自动初始化 platform_goal"""
|
||||
mem = ProjectMemory(db_path=tmp_path / "m.db")
|
||||
q = TaskQueue(db_path=tmp_path / "t.db")
|
||||
agent = PlannerAgent(_make_config(), queue=q, memory=mem)
|
||||
assert mem.get_goal("__platform__") is None
|
||||
|
||||
agent._ensure_platform_goal()
|
||||
|
||||
g = mem.get_goal("__platform__")
|
||||
assert g is not None
|
||||
assert len(g.goal_text) > 10
|
||||
|
||||
|
||||
def test_planner_empty_output(tmp_path):
|
||||
"""Claude 无有效输出时不崩溃"""
|
||||
mem = ProjectMemory(db_path=tmp_path / "m.db")
|
||||
q = TaskQueue(db_path=tmp_path / "t.db")
|
||||
agent = PlannerAgent(_make_config(), queue=q, memory=mem)
|
||||
# 不应抛异常
|
||||
agent._extract_and_enqueue("")
|
||||
agent._extract_and_enqueue("这是一段没有标签的分析报告。")
|
||||
```
|
||||
|
||||
**Step 2: 运行确认失败**
|
||||
|
||||
```bash
|
||||
pytest tests/test_planner.py -v
|
||||
```
|
||||
|
||||
预期:ImportError(planner.py 不存在)
|
||||
|
||||
**Step 3: 创建 planner.py**
|
||||
|
||||
创建 `src/rockchip_agents/agents/planner.py`:
|
||||
|
||||
```python
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
from rockchip_agents.agents.developer import (
|
||||
AgentResult, _parse_cli_result, _make_drop_root_preexec,
|
||||
_run_with_log, _task_log_path,
|
||||
)
|
||||
from rockchip_agents.config import AgentsConfig
|
||||
from rockchip_agents.core.queue import Task, TaskQueue
|
||||
from rockchip_agents.tools.project_memory import ProjectMemory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
TIMEOUT = 600 # planner 是 report 模式,只读分析
|
||||
|
||||
_DEFAULT_PLATFORM_GOAL = (
|
||||
"构建 RK3588 NPU 平台上的模块化 AI 推理生态,"
|
||||
"持续提升各推理模块成熟度,探索跨模块整合产品价值。"
|
||||
)
|
||||
|
||||
PLANNER_SYSTEM = """你是 RK3588 AI 生态团队的产品经理。
|
||||
|
||||
你的职责:
|
||||
1. 分析平台目标与当前项目状态的差距
|
||||
2. 识别跨模块整合机会,提出高价值的下一步需求
|
||||
3. 不重复已经在执行或等待中的任务
|
||||
|
||||
输出格式(报告末尾,每行一条):
|
||||
- [需求] <项目名>: 具体需求描述
|
||||
- [集成] 跨模块整合需求描述
|
||||
- [目标] <项目名>: 更新后的项目目标(仅当需要修订时)
|
||||
- [平台目标] 更新后的平台整体目标(仅当需要修订时)
|
||||
|
||||
去重原则:若相似需求已在队列中,请勿重复生成。
|
||||
请用中文输出,保持简洁。
|
||||
"""
|
||||
|
||||
|
||||
class PlannerAgent:
|
||||
def __init__(
|
||||
self,
|
||||
config: AgentsConfig,
|
||||
queue: Optional[TaskQueue] = None,
|
||||
memory: Optional[ProjectMemory] = None,
|
||||
) -> None:
|
||||
self._config = config
|
||||
self._queue = queue or TaskQueue()
|
||||
self._memory = memory or ProjectMemory()
|
||||
|
||||
def run(self, task: Task) -> AgentResult:
|
||||
self._ensure_platform_goal()
|
||||
prompt = self._build_prompt()
|
||||
cmd = [
|
||||
"claude", "--print",
|
||||
"--output-format", "json",
|
||||
"--no-session-persistence",
|
||||
"--model", self._config.claude.model,
|
||||
"--allowedTools", "Read,Glob,Grep",
|
||||
"--append-system-prompt", PLANNER_SYSTEM,
|
||||
"--", prompt,
|
||||
]
|
||||
try:
|
||||
env = self._build_env()
|
||||
result = _run_with_log(
|
||||
cmd, None, env, TIMEOUT, _task_log_path(task.id),
|
||||
preexec_fn=_make_drop_root_preexec(env),
|
||||
)
|
||||
if result.status == "done":
|
||||
self._extract_and_enqueue(result.summary)
|
||||
logger.info("PlannerAgent 分析完成,报告 %d 字", len(result.summary))
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("PlannerAgent 运行失败: %s", e)
|
||||
return AgentResult(status="failed", summary=str(e))
|
||||
|
||||
# ── helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
def _ensure_platform_goal(self) -> None:
|
||||
"""首次运行时初始化平台目标。"""
|
||||
if not self._memory.get_goal("__platform__"):
|
||||
try:
|
||||
self._memory.set_goal("__platform__", _DEFAULT_PLATFORM_GOAL, role="planner")
|
||||
logger.info("平台目标已初始化")
|
||||
except Exception as e:
|
||||
logger.warning("平台目标初始化失败: %s", e)
|
||||
|
||||
def _build_prompt(self) -> str:
|
||||
platform_ctx = self._memory.get_context_for_prompt("__platform__", role="planner")
|
||||
proj_names = "、".join(self._config.projects.keys())
|
||||
lines = [
|
||||
f"以下是当前 RK3588 AI 生态团队的状态(可用项目:{proj_names}):\n",
|
||||
platform_ctx.strip(),
|
||||
"",
|
||||
]
|
||||
for name, proj_cfg in self._config.projects.items():
|
||||
proj_ctx = self._memory.get_context_for_prompt(name, role="planner")
|
||||
desc = proj_cfg.description if hasattr(proj_cfg, "description") else ""
|
||||
lines.append(f"**{name}**: {desc}")
|
||||
if proj_ctx.strip():
|
||||
lines.append(proj_ctx.strip())
|
||||
# 附上当前队列摘要(防重复生成)
|
||||
pending_titles = self._get_pending_titles()
|
||||
if pending_titles:
|
||||
lines.append(f"\n当前待处理任务({len(pending_titles)} 条,避免重复):")
|
||||
for t in pending_titles[:15]:
|
||||
lines.append(f" - {t}")
|
||||
lines.append(
|
||||
"\n请分析平台目标与当前项目状态的差距,提出 3~5 条最高价值的需求或整合机会。"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
def _get_pending_titles(self) -> list[str]:
|
||||
try:
|
||||
conn = self._queue._conn()
|
||||
rows = conn.execute(
|
||||
"SELECT title FROM tasks WHERE status IN ('pending','running') ORDER BY id DESC LIMIT 30"
|
||||
).fetchall()
|
||||
conn.close()
|
||||
return [r["title"] for r in rows]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def _is_duplicate(self, title: str) -> bool:
|
||||
"""模糊去重:与任何 pending/running 任务 title 前 40 字相同则视为重复。"""
|
||||
key = title[:40]
|
||||
for existing in self._get_pending_titles():
|
||||
if existing[:40] == key:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _extract_and_enqueue(self, report_text: str) -> None:
|
||||
# [需求] <project>: <desc>
|
||||
for m in re.finditer(r"\[需求\]\s+([\w-]+)\s*[::]\s*(.+)", report_text):
|
||||
project = m.group(1).strip()
|
||||
desc = m.group(2).strip()[:120]
|
||||
if project not in self._config.projects:
|
||||
logger.warning("需求任务项目 %r 不在配置中,跳过", project)
|
||||
continue
|
||||
proj_cfg = self._config.projects[project]
|
||||
title = f"[需求] {desc}"
|
||||
if self._is_duplicate(title):
|
||||
logger.info("需求重复跳过: %s", title)
|
||||
continue
|
||||
self._queue.enqueue(Task(
|
||||
project=project, type="feature", title=title,
|
||||
context=f"由 PlannerAgent 从差距分析中提取\n{desc}",
|
||||
priority=3, mode=proj_cfg.mode,
|
||||
agent_role="developer", initiator="planner",
|
||||
discussion="PlannerAgent 基于平台目标-项目现状差距分析生成",
|
||||
))
|
||||
logger.info("需求任务入队: [%s] %s", project, title)
|
||||
|
||||
# [集成] <desc> (跨项目,无 project 前缀)
|
||||
for m in re.finditer(r"\[集成\]\s+(.+)", report_text):
|
||||
desc = m.group(1).strip()[:120]
|
||||
title = f"[集成] {desc}"
|
||||
if self._is_duplicate(title):
|
||||
logger.info("集成需求重复跳过: %s", title)
|
||||
continue
|
||||
self._queue.enqueue(Task(
|
||||
project="research", type="architect", title=title,
|
||||
context=f"由 PlannerAgent 提出的跨模块整合机会\n{desc}",
|
||||
priority=3, mode="report",
|
||||
agent_role="architect", initiator="planner",
|
||||
discussion="PlannerAgent 跨项目整合分析",
|
||||
))
|
||||
logger.info("集成任务入队: %s", title)
|
||||
|
||||
# [目标] <project>: <goal>
|
||||
for m in re.finditer(r"\[目标\]\s+([\w-]+)\s*[::]\s*(.+)", report_text):
|
||||
project = m.group(1).strip()
|
||||
goal_text = m.group(2).strip()[:300]
|
||||
if project not in self._config.projects:
|
||||
logger.warning("目标更新项目 %r 不在配置中,跳过", project)
|
||||
continue
|
||||
try:
|
||||
self._memory.set_goal(project, goal_text, role="planner")
|
||||
logger.info("项目目标已更新 by planner: [%s]", project)
|
||||
except Exception as e:
|
||||
logger.warning("[%s] 写入项目目标失败: %s", project, e)
|
||||
|
||||
# [平台目标] <goal>
|
||||
for m in re.finditer(r"\[平台目标\]\s+(.+)", report_text):
|
||||
goal_text = m.group(1).strip()[:300]
|
||||
try:
|
||||
self._memory.set_goal("__platform__", goal_text, role="planner")
|
||||
logger.info("平台目标已更新 by planner")
|
||||
except Exception as e:
|
||||
logger.warning("写入平台目标失败: %s", e)
|
||||
|
||||
def _build_env(self) -> dict:
|
||||
env = os.environ.copy()
|
||||
if self._config.claude.api_key:
|
||||
env["ANTHROPIC_API_KEY"] = self._config.claude.api_key
|
||||
env.pop("CLAUDECODE", None)
|
||||
env.pop("CLAUDE_CODE_ENTRYPOINT", None)
|
||||
return env
|
||||
```
|
||||
|
||||
**Step 4: 运行测试**
|
||||
|
||||
```bash
|
||||
pytest tests/test_planner.py -v
|
||||
```
|
||||
|
||||
预期:4 个 PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/rockchip_agents/agents/planner.py tests/test_planner.py
|
||||
git commit -m "feat(agents): add PlannerAgent with 3-layer goal context and dedup"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Executor — 注册 planner 路由
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/rockchip_agents/core/executor.py:42-63`
|
||||
- Test: `tests/test_executor.py`(已有,检查路由)
|
||||
|
||||
**Step 1: 写失败测试**
|
||||
|
||||
在 `tests/test_executor.py` 末尾追加(若文件存在):
|
||||
|
||||
```python
|
||||
def test_executor_routes_planner(mock_config, tmp_path):
|
||||
from rockchip_agents.core.executor import _make_agent
|
||||
from rockchip_agents.agents.planner import PlannerAgent
|
||||
from rockchip_agents.core.queue import Task
|
||||
from rockchip_agents.tools.project_memory import ProjectMemory
|
||||
|
||||
task = Task(
|
||||
project="research", type="planner", title="test",
|
||||
context="", priority=5, mode="report", agent_role="planner",
|
||||
)
|
||||
mem = ProjectMemory(db_path=tmp_path / "m.db")
|
||||
agent = _make_agent(mock_config, task, mem)
|
||||
assert isinstance(agent, PlannerAgent)
|
||||
```
|
||||
|
||||
**Step 2: 运行确认失败**
|
||||
|
||||
```bash
|
||||
pytest tests/test_executor.py::test_executor_routes_planner -v
|
||||
```
|
||||
|
||||
预期:FAIL(planner 路由不存在,返回 DeveloperAgent)
|
||||
|
||||
**Step 3: 修改 executor.py**
|
||||
|
||||
在 `_make_agent` 函数(第 42 行)的 `if role == "architect":` 块之后添加:
|
||||
|
||||
```python
|
||||
if role == "planner":
|
||||
from rockchip_agents.agents.planner import PlannerAgent
|
||||
return PlannerAgent(config, queue=TaskQueue(), memory=memory)
|
||||
```
|
||||
|
||||
注意:需要在文件顶部确认 `TaskQueue` 已导入(已有)。
|
||||
|
||||
**Step 4: 运行测试**
|
||||
|
||||
```bash
|
||||
pytest tests/test_executor.py -v
|
||||
```
|
||||
|
||||
预期:全部 PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/rockchip_agents/core/executor.py tests/test_executor.py
|
||||
git commit -m "feat(executor): route planner agent_role to PlannerAgent"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Scheduler — 6h 定时触发 + 队列空触发
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/rockchip_agents/core/scheduler.py`
|
||||
- Test: `tests/test_scheduler.py`(新增简单验证)
|
||||
|
||||
**Step 1: 写失败测试**
|
||||
|
||||
创建 `tests/test_scheduler.py`(或追加):
|
||||
|
||||
```python
|
||||
def test_planner_task_enqueue(tmp_path, monkeypatch):
|
||||
"""_enqueue_planner_task 创建 agent_role=planner 的任务"""
|
||||
from rockchip_agents.core.queue import TaskQueue
|
||||
from rockchip_agents.core import scheduler as sched_module
|
||||
|
||||
q = TaskQueue(db_path=tmp_path / "t.db")
|
||||
monkeypatch.setattr(sched_module, "_get_queue", lambda: q)
|
||||
|
||||
sched_module._enqueue_planner_task()
|
||||
|
||||
rows = q._conn().execute(
|
||||
"SELECT agent_role, type FROM tasks WHERE agent_role='planner'"
|
||||
).fetchall()
|
||||
assert len(rows) == 1
|
||||
assert rows[0]["type"] == "planner"
|
||||
```
|
||||
|
||||
**Step 2: 运行确认失败**
|
||||
|
||||
```bash
|
||||
pytest tests/test_scheduler.py::test_planner_task_enqueue -v
|
||||
```
|
||||
|
||||
预期:FAIL(`_enqueue_planner_task` 不存在)
|
||||
|
||||
**Step 3: 修改 scheduler.py**
|
||||
|
||||
在现有 `_scan_and_enqueue` 函数之后,`_run_pending` 之前,添加:
|
||||
|
||||
```python
|
||||
def _get_queue() -> "TaskQueue":
|
||||
"""可被测试 monkeypatch 替换的工厂函数。"""
|
||||
return TaskQueue()
|
||||
|
||||
|
||||
def _enqueue_planner_task() -> None:
|
||||
"""将 planner 分析任务入队(同标题去重,不重复触发)。"""
|
||||
from rockchip_agents.core.queue import Task
|
||||
q = _get_queue()
|
||||
tid = q.enqueue(Task(
|
||||
project="research",
|
||||
type="planner",
|
||||
title="PlannerAgent 周期差距分析",
|
||||
context="由 Scheduler 定时触发,分析平台目标与项目现状差距",
|
||||
priority=5,
|
||||
mode="report",
|
||||
agent_role="planner",
|
||||
initiator="scheduler",
|
||||
discussion="6h 定时 planner 分析",
|
||||
))
|
||||
if tid > 0:
|
||||
logger.info("PlannerAgent 任务入队 #%d", tid)
|
||||
else:
|
||||
logger.debug("PlannerAgent 任务已在队列中,跳过")
|
||||
```
|
||||
|
||||
在 `start()` 函数的 `scheduler.add_job` 部分,追加 planner 定时任务:
|
||||
|
||||
```python
|
||||
scheduler.add_job(
|
||||
_enqueue_planner_task, "interval", hours=6, id="planner",
|
||||
next_run_time=datetime.now() + timedelta(minutes=10),
|
||||
)
|
||||
```
|
||||
|
||||
**Step 4: 修改 `_run_pending` 支持队列空触发 planner**
|
||||
|
||||
将 `_run_pending` 修改为:
|
||||
|
||||
```python
|
||||
def _run_pending() -> None:
|
||||
cfg = load_config()
|
||||
executor = Executor(cfg)
|
||||
count = asyncio.run(executor.run_all_pending())
|
||||
logger.info("本轮执行任务数: %d", count)
|
||||
if count == 0:
|
||||
# 队列空时触发 planner 补充需求
|
||||
_enqueue_planner_task()
|
||||
logger.info("队列为空,已触发 PlannerAgent 差距分析")
|
||||
```
|
||||
|
||||
**Step 5: 运行测试**
|
||||
|
||||
```bash
|
||||
pytest tests/test_scheduler.py -v
|
||||
pytest tests/ -q # 全量回归
|
||||
```
|
||||
|
||||
预期:全部 PASS
|
||||
|
||||
**Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/rockchip_agents/core/scheduler.py tests/test_scheduler.py
|
||||
git commit -m "feat(scheduler): add 6h planner job + queue-empty trigger"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: get_context_for_prompt 调用方适配
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/rockchip_agents/agents/developer.py:252`
|
||||
- Modify: `src/rockchip_agents/agents/architect.py:190`
|
||||
|
||||
确保传入 `role` 参数,让不同 agent 获得对应层级上下文。
|
||||
|
||||
**Step 1: 修改 developer.py**
|
||||
|
||||
第 252 行 `_build_context` 中:
|
||||
|
||||
```python
|
||||
# 旧
|
||||
memory_ctx = self._memory.get_context_for_prompt(task.project)
|
||||
|
||||
# 新
|
||||
memory_ctx = self._memory.get_context_for_prompt(task.project, role=task.agent_role)
|
||||
```
|
||||
|
||||
**Step 2: 修改 architect.py**
|
||||
|
||||
第 190 行 `_build_prompt` 中:
|
||||
|
||||
```python
|
||||
# 旧
|
||||
goal_ctx = self._memory.get_context_for_prompt(name)
|
||||
|
||||
# 新
|
||||
goal_ctx = self._memory.get_context_for_prompt(name, role="architect")
|
||||
```
|
||||
|
||||
**Step 3: 全量测试**
|
||||
|
||||
```bash
|
||||
pytest tests/ -q
|
||||
```
|
||||
|
||||
预期:137+ PASS,0 FAIL
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/rockchip_agents/agents/developer.py src/rockchip_agents/agents/architect.py
|
||||
git commit -m "feat(agents): pass role to get_context_for_prompt for layered context"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Dashboard — planner 任务在 UI 展示
|
||||
|
||||
**Files:**
|
||||
- Modify: `dashboard/src/components/TaskList.tsx`(或相关列表组件)
|
||||
|
||||
PlannerAgent 的任务 `project="research"、agent_role="planner"`,确认 Dashboard 任务列表能正常展示(无需特殊处理,只需 UI 显示 `[planner]` 标签即可)。
|
||||
|
||||
**Step 1: 检查 TaskList 是否有 agent_role 展示**
|
||||
|
||||
```bash
|
||||
grep -n "agent_role\|planner" dashboard/src/components/TaskList.tsx 2>/dev/null | head -20
|
||||
```
|
||||
|
||||
若已有角色标签展示,无需修改。若无,在任务标题旁添加 `{task.agent_role}` badge。
|
||||
|
||||
**Step 2: 全量测试(前端构建验证)**
|
||||
|
||||
```bash
|
||||
cd dashboard && npm run build 2>&1 | tail -5
|
||||
```
|
||||
|
||||
**Step 3: Commit(如有修改)**
|
||||
|
||||
```bash
|
||||
git add dashboard/src/
|
||||
git commit -m "feat(dashboard): show planner role badge in task list"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 验收标准
|
||||
|
||||
1. `pytest tests/ -q` 全部通过(包括新增的 `test_planner.py` 4 条 + `test_project_memory.py` 3 条新测试)
|
||||
2. 手动验证:`python scripts/run.py scan` 后队列中出现 `PlannerAgent 周期差距分析` 任务
|
||||
3. `python scripts/run.py goals` 展示 `__platform__` 目标
|
||||
4. Dashboard 任务列表能看到 `agent_role=planner` 的任务
|
||||
|
||||
## 注意事项
|
||||
|
||||
- PlannerAgent 只用 `claude --print --allowedTools Read,Glob,Grep`(report 模式),不修改任何代码文件
|
||||
- `enqueue()` 本身已有 `project+type+title` 精确去重,PlannerAgent 内部额外做 title 前 40 字模糊去重,两层配合防止语义重复任务
|
||||
- `__platform__` 不在 `self._config.projects` 中,写目标时直接调用 `memory.set_goal`,不做项目名校验
|
||||
@@ -0,0 +1,275 @@
|
||||
# 三组 15 角色团队架构设计文档
|
||||
|
||||
**日期**: 2026-03-08
|
||||
**状态**: 已确认
|
||||
|
||||
## 背景
|
||||
|
||||
当前系统为单队列 8 角色,缺乏明确的组织分工。随着业务扩展至多平台(RK3588/ESP32/全志/Nordic)、多产品线(方案级+整机出货),需要引入三组架构,覆盖基础技术研发、项目研发交付、产品洞察与市场落地三个维度。
|
||||
|
||||
---
|
||||
|
||||
## 业务上下文
|
||||
|
||||
- **产品形态**:嵌入式 AI 方案(软硬全包含)为主 + 部分整机出货
|
||||
- **平台**:Rockchip(RK3588/RV1106/RK3562)、ESP32/Nordic(TinyML)、全志
|
||||
- **AI 能力**:RKNN 推理(板端)+ TinyML(微控制器)
|
||||
- **OS 栈**:Linux + RTOS 双栈
|
||||
|
||||
---
|
||||
|
||||
## 三组架构总览
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 🔬 基础技术研发组 base_opt_tasks.db 并发:3 周期:4h │
|
||||
│ hw-engineer / os-engineer / algo-researcher │
|
||||
│ base-validator / base-architect │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ 🚀 项目研发与交付组 tasks.db(现有) 并发:3 周期:1h │
|
||||
│ developer / dev-kernel / dev-lowlevel │
|
||||
│ system-tester / architect │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ 💡 产品洞察与市场组 insight_tasks.db 并发:2 事件触发│
|
||||
│ planner / productizer / vision-analyst │
|
||||
│ market-pm / media-producer │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## §1 基础技术研发组
|
||||
|
||||
**队列**: `data/base_opt_tasks.db`
|
||||
**并发**: 3
|
||||
**扫描周期**: 4h(慢周期,长时间基线任务)
|
||||
**目标**: 建立平台级能力基线,输出能力事实供项目组消费
|
||||
|
||||
### 角色定义
|
||||
|
||||
#### `hw-engineer`(现有,增强)
|
||||
- 芯片选型(算力/功耗/接口/成本/供货)
|
||||
- BOM 评审与替代方案
|
||||
- 接口信号完整性、ESD 保护设计
|
||||
- **写入 `__hardware__` facts**:硬件规格约束,供全组读取
|
||||
- 模式:confirm(硬件决策需人工审批)
|
||||
- Skills:`fetch`、`sequential-thinking`
|
||||
|
||||
#### `os-engineer`(新建,继承 LowlevelDev + KernelDev 能力)
|
||||
- RTOS 任务调度、中断优先级、驱动框架
|
||||
- Linux 内核基础层:Kconfig/DTS 模板、驱动框架
|
||||
- U-Boot / Bootloader 基础配置
|
||||
- 模式:auto/confirm
|
||||
- Skills:`ssh-device`、`sequential-thinking`
|
||||
|
||||
#### `algo-researcher`(新建)
|
||||
- 防抖算法研究与基线实现
|
||||
- 短距离定位/测距算法
|
||||
- GPS + 惯导(IMU 融合)算法
|
||||
- **受硬件约束**:读取 `__hardware__` facts,确认算法在目标平台可行
|
||||
- 产出:算法能力事实写入 ProjectMemory
|
||||
- 模式:auto/confirm
|
||||
- Skills:`ssh-device`、`sequential-thinking`、`fetch`
|
||||
|
||||
#### `base-validator`(新建,增强 TesterAgent)
|
||||
测试能力覆盖四层:
|
||||
|
||||
| 层次 | 内容 |
|
||||
|------|------|
|
||||
| **精度测试** | 检出率、误检率、漏检率,基线对比 |
|
||||
| **性能测试** | FPS、推理延迟、NPU 利用率、CPU/内存占用 |
|
||||
| **稳定性测试** | 长时间压力运行、内存泄漏检测、异常恢复验证 |
|
||||
| **硬件约束验证** | 读取 `__hardware__` facts,确认算法在目标平台满足约束 |
|
||||
|
||||
- 基线退化超阈值 → 自动触发 `[需求]` 任务入队
|
||||
- 测试结果写入 ProjectMemory facts
|
||||
- Skills:`ssh-device`、`sequential-thinking`
|
||||
|
||||
#### `base-architect`(新建,继承 ArchitectAgent)
|
||||
- 分析基础技术栈(OS/算法/硬件)技术债和瓶颈
|
||||
- 跨方向整合建议(如"防抖算法与 GPS 惯导融合")
|
||||
- 输出 `[优化]` 任务给 `algo-researcher` / `os-engineer`
|
||||
- **区别于项目组 architect**:聚焦平台能力本身,不关注项目交付
|
||||
- 模式:report
|
||||
|
||||
---
|
||||
|
||||
## §2 项目研发与交付组
|
||||
|
||||
**队列**: `data/tasks.db`(现有,保持兼容)
|
||||
**并发**: 3
|
||||
**扫描周期**: 1h(快周期)
|
||||
**目标**: 消费基础组能力事实,完成面向客户的项目交付
|
||||
|
||||
### 角色定义
|
||||
|
||||
#### `developer`(现有)
|
||||
- 项目功能开发、Bug 修复
|
||||
- 读取基础组能力事实作为实现依据
|
||||
- 模式:auto/confirm
|
||||
|
||||
#### `dev-kernel`(现有)
|
||||
- 项目级 Linux 驱动适配、DTS、Kconfig
|
||||
- 模式:auto/confirm
|
||||
|
||||
#### `dev-lowlevel`(现有)
|
||||
- 项目级 BSP、Bootloader 适配、硬件 bring-up
|
||||
- 模式:auto/confirm
|
||||
|
||||
#### `architect`(现有)
|
||||
- 跨项目架构分析 + arch_optimize
|
||||
- 消费基础组能力事实,制定项目架构方向
|
||||
- 模式:report + auto(optimize 时)
|
||||
|
||||
#### `system-tester`(替换现有 tester,大幅增强)
|
||||
测试能力矩阵:
|
||||
|
||||
| 能力 | 实现 |
|
||||
|------|------|
|
||||
| **功能测试** | pytest 全套,SSH 板端自动执行 |
|
||||
| **视觉测试** | Playwright 截图 → 传 vision-analyst 分析判断 |
|
||||
| **性能测试** | FPS/延迟/内存自动测量,写入 facts |
|
||||
| **跨平台** | RK3588/全志/ESP32 多平台 SSH 调度 |
|
||||
| **报告生成** | 结构化报告 → `docs/test_reports/` |
|
||||
| **回归闸门** | 测试失败自动触发 `[需求]` 入队 |
|
||||
|
||||
- Skills:`playwright`、`ssh-device`、`sequential-thinking`、`fetch`
|
||||
|
||||
---
|
||||
|
||||
## §3 产品洞察与市场落地组
|
||||
|
||||
**队列**: `data/insight_tasks.db`(新建)
|
||||
**并发**: 2
|
||||
**触发**: 事件驱动(planner 6h 定时 + 队列空触发)
|
||||
**目标**: 产品方向把控 + 市场文档 + 视觉质量把关
|
||||
|
||||
### 角色定义
|
||||
|
||||
#### `planner`(现有)
|
||||
- 平台级差距分析,跨三组需求生成
|
||||
- 三层目标体系(`__platform__` → project → task)
|
||||
- 模式:report
|
||||
|
||||
#### `productizer`(现有)
|
||||
- 成熟度评分(100分制规则引擎)
|
||||
- 模式:report
|
||||
|
||||
#### `vision-analyst`(新建)
|
||||
- **Playwright 截图** 板端推理界面/监控大屏
|
||||
- **Claude Vision(LLM多模态)** 分析:检测框合理性、漏检、FPS 数值读取
|
||||
- 定量补充:对接推理日志(JSON/txt)
|
||||
- 输出:视觉分析报告 + `[需求]` 任务入项目组队列
|
||||
- 触发:system-tester 完成后自动触发 / planner 主动下发
|
||||
- Skills:`playwright`、`sequential-thinking`
|
||||
- 模式:report
|
||||
|
||||
#### `market-pm`(新建)
|
||||
- 读取项目目标、技术规格、能力事实
|
||||
- 生成:方案书、技术白皮书、BOM 估算(Markdown → `docs/market/`)
|
||||
- 协助 planner 提出新项目需求
|
||||
- 模式:report
|
||||
- Skills:`fetch`、`sequential-thinking`
|
||||
|
||||
#### `media-producer`(新建)
|
||||
- **随时触发**(不依赖测试结果)
|
||||
- 技术侧:推理结果图标注(检测框/置信度/平台水印)
|
||||
- 市场侧:产品展示效果图(硬件渲染+算法效果合成)
|
||||
- 工具:OpenCV、ffmpeg、Pillow
|
||||
- 输出:`assets/` 目录
|
||||
- 模式:auto
|
||||
- Skills:`sequential-thinking`
|
||||
|
||||
---
|
||||
|
||||
## §4 信息流架构
|
||||
|
||||
```
|
||||
hw-engineer
|
||||
└─写入──▶ __hardware__ facts(硬件规格约束)
|
||||
│
|
||||
▼
|
||||
algo-researcher / os-engineer
|
||||
└─读取约束──▶ 实现算法/OS
|
||||
└─写入──▶ 能力事实(algo.antishake.fps=60, os.rtos.latency=1ms)
|
||||
│
|
||||
base-validator──▶ 精度/性能/稳定性验证──▶ 写入验证通过事实
|
||||
base-architect──▶ 跨方向整合建议──▶ [优化] 任务
|
||||
|
||||
│(能力事实传递)
|
||||
▼
|
||||
developer / dev-kernel / dev-lowlevel
|
||||
└─消费能力事实──▶ 项目实现
|
||||
system-tester
|
||||
└─Playwright截图──▶ vision-analyst──▶ 视觉通过/失败
|
||||
└─性能测试──▶ 写入项目 facts
|
||||
architect──▶ 架构优化建议
|
||||
|
||||
│(项目状态传递)
|
||||
▼
|
||||
planner──▶ 差距分析──▶ 跨组需求入队
|
||||
productizer──▶ 成熟度评分
|
||||
vision-analyst──▶ 视觉质量报告
|
||||
market-pm──▶ 方案书/白皮书
|
||||
media-producer──▶ 演示图/效果图(随时触发)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## §5 Skills 授权配置(mcp_tools.yaml)
|
||||
|
||||
```yaml
|
||||
roles:
|
||||
hw-engineer: [fetch, sequential-thinking]
|
||||
os-engineer: [ssh-device, sequential-thinking]
|
||||
algo-researcher: [ssh-device, sequential-thinking, fetch]
|
||||
base-validator: [ssh-device, sequential-thinking]
|
||||
base-architect: []
|
||||
developer: [sequential-thinking, ssh-device]
|
||||
dev-kernel: [ssh-device, sequential-thinking]
|
||||
dev-lowlevel: [ssh-device, sequential-thinking]
|
||||
system-tester: [playwright, ssh-device, sequential-thinking, fetch]
|
||||
architect: []
|
||||
planner: []
|
||||
productizer: []
|
||||
vision-analyst: [playwright, sequential-thinking]
|
||||
market-pm: [fetch, sequential-thinking]
|
||||
media-producer: [sequential-thinking]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## §6 队列映射与调度配置
|
||||
|
||||
```yaml
|
||||
queues:
|
||||
base_opt:
|
||||
db: data/base_opt_tasks.db
|
||||
max_concurrent: 3
|
||||
scan_interval_hours: 4
|
||||
roles: [hw-engineer, os-engineer, algo-researcher, base-validator, base-architect]
|
||||
|
||||
project_delivery:
|
||||
db: data/tasks.db
|
||||
max_concurrent: 3
|
||||
scan_interval_hours: 1
|
||||
roles: [developer, dev-kernel, dev-lowlevel, system-tester, architect]
|
||||
|
||||
insight:
|
||||
db: data/insight_tasks.db
|
||||
max_concurrent: 2
|
||||
trigger: event_driven # planner 6h + 队列空
|
||||
roles: [planner, productizer, vision-analyst, market-pm, media-producer]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 实现优先级
|
||||
|
||||
| 优先级 | 内容 |
|
||||
|--------|------|
|
||||
| P0 | 三队列调度架构(Scheduler + Executor 扩展) |
|
||||
| P1 | `base-validator`、`system-tester` 增强(测试能力核心) |
|
||||
| P2 | `vision-analyst`(Playwright + LLM 视觉分析) |
|
||||
| P3 | `os-engineer`、`algo-researcher`、`base-architect`(新角色) |
|
||||
| P4 | `market-pm`、`media-producer`(文档和媒体产出) |
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,138 @@
|
||||
# EnvCollector 设计文档
|
||||
|
||||
## 概述
|
||||
|
||||
自动收集本机环境变量中的模型路径、SSH 远端设备的模型文件 + 磁盘空间 + 关键包版本,
|
||||
持久化存储到 ProjectMemory,供所有 Agent 查询,避免重复下载模型。
|
||||
|
||||
---
|
||||
|
||||
## 1. 数据模型 & 存储结构
|
||||
|
||||
结果全部写入 `ProjectMemory` 的 `__env__` 虚拟项目命名空间(类似现有 `__hardware__`)。
|
||||
|
||||
### Key 规范
|
||||
|
||||
```
|
||||
# 本机环境变量(含 MODEL/RKNN/ONNX/WEIGHT/PATH 关键字的变量)
|
||||
__env__.local.envvar.<VAR_NAME> = "/path/to/model.rknn"
|
||||
|
||||
# 本机文件系统扫描(/data/ 下递归,max_depth=5)
|
||||
__env__.local.models.rknn = "/data/rockchip/yolo/model.rknn,/data/rockchip/embedding/emb.rknn"
|
||||
__env__.local.models.onnx = "/data/rockchip/yolo/model.onnx"
|
||||
__env__.local.models.pt = ""
|
||||
|
||||
# SSH 设备模型文件
|
||||
__env__.{device_name}.models.rknn = "/home/pi/Desktop/yolo/model.rknn,/home/pi/models/det.rknn"
|
||||
__env__.{device_name}.models.onnx = ""
|
||||
|
||||
# SSH 设备磁盘空间
|
||||
__env__.{device_name}.disk = "Filesystem /dev/root Size=29G Used=8.2G Avail=19G"
|
||||
|
||||
# SSH 设备已安装关键包
|
||||
__env__.{device_name}.packages = "rknn-toolkit-lite2==2.3.2,opencv-python==4.13.0,numpy==2.4.2"
|
||||
|
||||
# 设备离线时错误记录
|
||||
__env__.{device_name}.error = "offline at 2026-03-09T10:30:00"
|
||||
|
||||
# 收集时间戳
|
||||
__env__.collected_at = "2026-03-09T10:30:00"
|
||||
```
|
||||
|
||||
多值用逗号分隔,兼容现有 `get_facts()` 返回 dict 的结构,无需改表结构。
|
||||
|
||||
---
|
||||
|
||||
## 2. 收集逻辑 & 触发机制
|
||||
|
||||
### EnvCollector 核心类(tools/env_collector.py)
|
||||
|
||||
```python
|
||||
class EnvCollector:
|
||||
def collect_local(self, scan_dirs: list[str] = None) -> dict[str, str]:
|
||||
# 1. 扫描 os.environ,过滤含 MODEL/RKNN/ONNX/WEIGHT/PATH 的 key
|
||||
# 2. 文件系统:在 scan_dirs(默认 ["/data/"])下 find *.rknn/*.onnx/*.pt/*.pb
|
||||
# 跳过 venv/__pycache__/.git,max_depth=5
|
||||
|
||||
def collect_device(self, name: str, device: DeviceConfig) -> dict[str, str]:
|
||||
# 复用 DeviceAgent.run_command() 执行:
|
||||
# 1. find $workspace -maxdepth 4 -name "*.rknn" -o -name "*.onnx" ...
|
||||
# 2. df -h | grep -v tmpfs | head -5
|
||||
# 3. pip list 2>/dev/null | grep -E "rknn|torch|opencv|numpy|pillow"
|
||||
# 设备离线时捕获异常,写 error key
|
||||
|
||||
def save(self, facts: dict[str, str], memory: ProjectMemory) -> None:
|
||||
# set_fact("__env__", key, value, role="env-collector")
|
||||
# 最后写 collected_at
|
||||
```
|
||||
|
||||
### 触发机制
|
||||
|
||||
| 触发方式 | 实现位置 | 说明 |
|
||||
|---------|---------|------|
|
||||
| **手动** | `scripts/run.py collect-env` | 立即执行,输出收集摘要到 stdout |
|
||||
| **启动时** | `scheduler.start()` 末尾 daemon thread | 不阻塞调度器启动 |
|
||||
| **每日定时** | APScheduler `interval hours=24` | 后台安静更新,失败不影响主流程 |
|
||||
|
||||
SSH 扫描失败(设备离线)时:静默跳过,记录 `__env__.{name}.error` 到 ProjectMemory。
|
||||
|
||||
---
|
||||
|
||||
## 3. Agent 使用方式
|
||||
|
||||
### system prompt 注入(_build_device_context 扩展)
|
||||
|
||||
`DeveloperAgent._build_device_context()` 末尾追加"已知模型"摘要块:
|
||||
|
||||
```
|
||||
【已知模型文件(避免重复下载)】
|
||||
本机:
|
||||
*.rknn: /data/rockchip/yolo/model.rknn, /data/rockchip/embedding/emb.rknn
|
||||
RKNN_MODEL_PATH=/data/rockchip/models(来自环境变量)
|
||||
|
||||
rk3588 (192.168.123.181):
|
||||
*.rknn: /home/pi/Desktop/yolo/model.rknn
|
||||
磁盘: /dev/root 29G 已用8.2G 剩余19G
|
||||
已装: rknn-toolkit-lite2==2.3.2, opencv-python==4.13.0
|
||||
|
||||
(更新于 2026-03-09 10:30,执行 `python scripts/run.py collect-env` 可刷新)
|
||||
```
|
||||
|
||||
只在 memory 不为 None 且 `__env__` 有数据时注入(无数据时静默跳过)。
|
||||
|
||||
### ProjectMemory 精确查询
|
||||
|
||||
其他 Agent(base-validator、vision-analyst)可直接:
|
||||
|
||||
```python
|
||||
facts = self._memory.get_facts("__env__")
|
||||
rk3588_models = facts.get("rk3588.models.rknn", "").split(",")
|
||||
disk_info = facts.get("rk3588.disk", "unknown")
|
||||
```
|
||||
|
||||
### CLI 输出示例
|
||||
|
||||
```bash
|
||||
$ python scripts/run.py collect-env
|
||||
|
||||
[本机]
|
||||
ENV: RKNN_MODEL_PATH=/data/rockchip/models (1 条)
|
||||
*.rknn: 3 个文件
|
||||
*.onnx: 1 个文件
|
||||
|
||||
[rk3588 @ 192.168.123.181]
|
||||
*.rknn: 2 个文件
|
||||
磁盘: 剩余 19G / 29G
|
||||
关键包: rknn-toolkit-lite2==2.3.2, opencv-python==4.13.0
|
||||
|
||||
已保存到 ProjectMemory (__env__)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 实施原则
|
||||
|
||||
- **TDD**:先写失败测试,再实现
|
||||
- **YAGNI**:不做 AI 分析模型版本、不做模型哈希校验
|
||||
- **轻依赖**:只用 paramiko(已有)+ pathlib + os.environ,无额外依赖
|
||||
- **容错优先**:任意设备离线或扫描失败不影响主流程
|
||||
@@ -0,0 +1,721 @@
|
||||
# EnvCollector Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** 自动收集本机环境变量模型路径 + SSH 设备模型文件/磁盘/包信息,持久化至 ProjectMemory `__env__` 命名空间,注入 DeveloperAgent 上下文,避免重复下载模型。
|
||||
|
||||
**Architecture:** 独立 `EnvCollector` 类(`tools/env_collector.py`),直接复用现有 `DeviceAgent._ssh()` + `ProjectMemory.set_fact()`;CLI / scheduler / developer 三处集成均为薄封装,不引入新依赖。
|
||||
|
||||
**Tech Stack:** pathlib + os.environ + paramiko(已有)+ APScheduler(已有)+ ProjectMemory(已有)
|
||||
|
||||
---
|
||||
|
||||
### Task 1: tools/env_collector.py — 核心收集类
|
||||
|
||||
**Files:**
|
||||
- Create: `src/rockchip_agents/tools/env_collector.py`
|
||||
- Modify: `src/rockchip_agents/tools/project_memory.py`(WRITE_ROLES 添加 "env-collector")
|
||||
- Test: `tests/test_env_collector.py`
|
||||
|
||||
**Step 1: 写失败测试**
|
||||
|
||||
```python
|
||||
# tests/test_env_collector.py
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
from rockchip_agents.tools.env_collector import EnvCollector
|
||||
from rockchip_agents.tools.project_memory import ProjectMemory
|
||||
|
||||
|
||||
def test_collect_local_env_vars(tmp_path):
|
||||
"""collect_local 应收集含 MODEL/RKNN/ONNX 关键词的环境变量。"""
|
||||
env = {
|
||||
"RKNN_MODEL_PATH": "/data/rockchip/models",
|
||||
"HOME": "/home/pi", # 不含关键词,不应收集
|
||||
"ONNX_WEIGHT_DIR": "/data/weights",
|
||||
}
|
||||
with patch.dict(os.environ, env, clear=True):
|
||||
col = EnvCollector()
|
||||
facts = col.collect_local(scan_dirs=[])
|
||||
assert facts.get("local.envvar.RKNN_MODEL_PATH") == "/data/rockchip/models"
|
||||
assert facts.get("local.envvar.ONNX_WEIGHT_DIR") == "/data/weights"
|
||||
assert "local.envvar.HOME" not in facts
|
||||
|
||||
|
||||
def test_collect_local_file_scan(tmp_path):
|
||||
"""collect_local 文件扫描应找到 *.rknn / *.onnx / *.pt 文件。"""
|
||||
(tmp_path / "model.rknn").touch()
|
||||
(tmp_path / "sub").mkdir()
|
||||
(tmp_path / "sub" / "det.onnx").touch()
|
||||
col = EnvCollector()
|
||||
facts = col.collect_local(scan_dirs=[str(tmp_path)])
|
||||
assert "/model.rknn" in facts["local.models.rknn"]
|
||||
assert "/det.onnx" in facts["local.models.onnx"]
|
||||
assert facts.get("local.models.pt", "") == ""
|
||||
|
||||
|
||||
def test_collect_local_skips_venv(tmp_path):
|
||||
"""collect_local 应跳过 venv / __pycache__ 目录。"""
|
||||
venv = tmp_path / "venv" / "lib"
|
||||
venv.mkdir(parents=True)
|
||||
(venv / "ignore.rknn").touch()
|
||||
col = EnvCollector()
|
||||
facts = col.collect_local(scan_dirs=[str(tmp_path)])
|
||||
assert facts.get("local.models.rknn", "") == ""
|
||||
|
||||
|
||||
def test_collect_device_success():
|
||||
"""collect_device 成功时应返回 models/disk/packages facts。"""
|
||||
from rockchip_agents.config import DeviceConfig
|
||||
dev = DeviceConfig(host="1.2.3.4", user="pi", password="pi",
|
||||
workspace="/home/pi", type="linux", connect="ssh")
|
||||
|
||||
def fake_run(cmd, remote_cwd="", timeout=60):
|
||||
if "find" in cmd:
|
||||
return "[rc=0]\n/home/pi/model.rknn\n"
|
||||
if "df" in cmd:
|
||||
return "[rc=0]\n/dev/root 29G 8.2G 19G\n"
|
||||
if "pip" in cmd:
|
||||
return "[rc=0]\nrknn-toolkit-lite2 2.3.2\nopencv-python 4.13.0\n"
|
||||
return "[rc=0]\n"
|
||||
|
||||
col = EnvCollector()
|
||||
with patch.object(col, "_run_on_device", side_effect=fake_run):
|
||||
facts = col.collect_device("rk3588", dev)
|
||||
assert "/home/pi/model.rknn" in facts["rk3588.models.rknn"]
|
||||
assert "29G" in facts["rk3588.disk"]
|
||||
assert "rknn-toolkit-lite2" in facts["rk3588.packages"]
|
||||
|
||||
|
||||
def test_collect_device_offline():
|
||||
"""设备离线时应写入 error key,不抛异常。"""
|
||||
from rockchip_agents.config import DeviceConfig
|
||||
dev = DeviceConfig(host="1.2.3.4", user="pi", password="pi",
|
||||
workspace="/home/pi", type="linux", connect="ssh")
|
||||
col = EnvCollector()
|
||||
with patch.object(col, "_run_on_device", side_effect=Exception("Connection refused")):
|
||||
facts = col.collect_device("rk3588", dev)
|
||||
assert "offline" in facts.get("rk3588.error", "")
|
||||
|
||||
|
||||
def test_save_writes_to_memory(tmp_path):
|
||||
"""save() 应将 facts 写入 ProjectMemory __env__ 命名空间。"""
|
||||
mem = ProjectMemory(db_path=tmp_path / "m.db")
|
||||
col = EnvCollector()
|
||||
col.save({"local.models.rknn": "/data/a.rknn"}, mem)
|
||||
stored = mem.get_facts("__env__")
|
||||
assert stored["local.models.rknn"] == "/data/a.rknn"
|
||||
assert "collected_at" in stored
|
||||
|
||||
|
||||
def test_save_requires_env_collector_role(tmp_path):
|
||||
"""只有 env-collector role 才能写 __env__ facts(需先确认 WRITE_ROLES 已扩展)。"""
|
||||
mem = ProjectMemory(db_path=tmp_path / "m.db")
|
||||
# 不应抛 PermissionError
|
||||
mem.set_fact("__env__", "test.key", "val", role="env-collector")
|
||||
assert mem.get_facts("__env__")["test.key"] == "val"
|
||||
```
|
||||
|
||||
**Step 2: 运行确认失败**
|
||||
|
||||
```bash
|
||||
source venv/bin/activate
|
||||
pytest tests/test_env_collector.py -v
|
||||
# Expected: ImportError / AttributeError — 模块不存在
|
||||
```
|
||||
|
||||
**Step 3: 更新 WRITE_ROLES(project_memory.py)**
|
||||
|
||||
在 `src/rockchip_agents/tools/project_memory.py` 找到 `WRITE_ROLES`,添加 `"env-collector"`:
|
||||
|
||||
```python
|
||||
# 修改前
|
||||
WRITE_ROLES: frozenset[str] = frozenset({"productizer", "architect"})
|
||||
|
||||
# 修改后
|
||||
WRITE_ROLES: frozenset[str] = frozenset({"productizer", "architect", "env-collector"})
|
||||
```
|
||||
|
||||
**Step 4: 实现 env_collector.py**
|
||||
|
||||
```python
|
||||
# src/rockchip_agents/tools/env_collector.py
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from rockchip_agents.config import DeviceConfig
|
||||
from rockchip_agents.tools.project_memory import ProjectMemory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 文件扫描跳过这些目录
|
||||
_SKIP_DIRS = {"venv", "__pycache__", ".git", "node_modules", ".tox"}
|
||||
# 环境变量关键词过滤
|
||||
_ENV_KEYWORDS = {"MODEL", "RKNN", "ONNX", "WEIGHT", "PATH"}
|
||||
# 扫描的模型扩展名
|
||||
_EXTENSIONS = {".rknn", ".onnx", ".pt", ".pb"}
|
||||
|
||||
|
||||
class EnvCollector:
|
||||
"""收集本机环境变量 + SSH 设备模型文件信息,写入 ProjectMemory __env__。"""
|
||||
|
||||
def collect_local(self, scan_dirs: list[str] | None = None) -> dict[str, str]:
|
||||
"""收集本机环境变量(过滤关键词)+ 文件系统模型文件。"""
|
||||
facts: dict[str, str] = {}
|
||||
|
||||
# 1. 环境变量扫描
|
||||
for key, val in os.environ.items():
|
||||
if any(kw in key.upper() for kw in _ENV_KEYWORDS):
|
||||
facts[f"local.envvar.{key}"] = val
|
||||
|
||||
# 2. 文件系统扫描
|
||||
dirs = scan_dirs if scan_dirs is not None else ["/data/"]
|
||||
found: dict[str, list[str]] = {ext: [] for ext in _EXTENSIONS}
|
||||
for base in dirs:
|
||||
base_path = Path(base)
|
||||
if not base_path.exists():
|
||||
continue
|
||||
self._scan_dir(base_path, found, depth=0)
|
||||
|
||||
for ext, paths in found.items():
|
||||
key = f"local.models.{ext.lstrip('.')}"
|
||||
facts[key] = ",".join(sorted(set(paths)))
|
||||
|
||||
return facts
|
||||
|
||||
def _scan_dir(self, path: Path, found: dict[str, list[str]], depth: int) -> None:
|
||||
if depth > 5:
|
||||
return
|
||||
try:
|
||||
for entry in path.iterdir():
|
||||
if entry.is_dir():
|
||||
if entry.name not in _SKIP_DIRS:
|
||||
self._scan_dir(entry, found, depth + 1)
|
||||
elif entry.is_file() and entry.suffix in _EXTENSIONS:
|
||||
found[entry.suffix].append(str(entry))
|
||||
except PermissionError:
|
||||
pass
|
||||
|
||||
def collect_device(self, name: str, device: DeviceConfig) -> dict[str, str]:
|
||||
"""连接 SSH 设备,收集模型文件/磁盘/包信息。"""
|
||||
facts: dict[str, str] = {}
|
||||
try:
|
||||
ws = device.workspace
|
||||
|
||||
# 模型文件
|
||||
find_cmd = (
|
||||
f'find {ws} -maxdepth 4 \\( '
|
||||
f'-name "*.rknn" -o -name "*.onnx" -o -name "*.pt" -o -name "*.pb" '
|
||||
f'\\) 2>/dev/null'
|
||||
)
|
||||
out = self._run_on_device(find_cmd, ws)
|
||||
for ext in (".rknn", ".onnx", ".pt", ".pb"):
|
||||
key_name = ext.lstrip(".")
|
||||
paths = [l for l in out.splitlines() if l.endswith(ext) and not l.startswith("[")]
|
||||
facts[f"{name}.models.{key_name}"] = ",".join(paths)
|
||||
|
||||
# 磁盘空间
|
||||
disk_out = self._run_on_device("df -h | grep -v tmpfs | head -5", ws)
|
||||
lines = [l for l in disk_out.splitlines() if not l.startswith("[")]
|
||||
facts[f"{name}.disk"] = " | ".join(lines).strip()
|
||||
|
||||
# 已安装关键包
|
||||
pkg_out = self._run_on_device(
|
||||
'pip list 2>/dev/null | grep -iE "rknn|torch|opencv|numpy|pillow"', ws
|
||||
)
|
||||
pkgs = []
|
||||
for line in pkg_out.splitlines():
|
||||
if line.startswith("["):
|
||||
continue
|
||||
parts = line.split()
|
||||
if len(parts) >= 2:
|
||||
pkgs.append(f"{parts[0]}=={parts[1]}")
|
||||
facts[f"{name}.packages"] = ",".join(pkgs)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning("设备 %s 收集失败: %s", name, e)
|
||||
ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S")
|
||||
facts[f"{name}.error"] = f"offline at {ts}"
|
||||
|
||||
return facts
|
||||
|
||||
def _run_on_device(self, cmd: str, remote_cwd: str, timeout: int = 60) -> str:
|
||||
"""内部 SSH 执行(可被测试 monkeypatch)。"""
|
||||
# 不在这里 import DeviceAgent,避免循环依赖
|
||||
raise NotImplementedError("需在运行时传入 DeviceAgent 或 override")
|
||||
|
||||
def save(self, facts: dict[str, str], memory: ProjectMemory) -> None:
|
||||
"""将 facts 写入 ProjectMemory __env__ 命名空间。"""
|
||||
for key, val in facts.items():
|
||||
memory.set_fact("__env__", key, val, role="env-collector")
|
||||
ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S")
|
||||
memory.set_fact("__env__", "collected_at", ts, role="env-collector")
|
||||
```
|
||||
|
||||
> **注意:** `collect_device` 不直接依赖 `DeviceAgent`——由上层 `run_collection()` 函数组合,避免耦合:
|
||||
|
||||
```python
|
||||
def run_collection(cfg, memory: ProjectMemory) -> dict[str, str]:
|
||||
"""顶层编排:收集本机 + 所有 SSH 设备,保存到 memory。"""
|
||||
from rockchip_agents.tools.device_agent import DeviceAgent
|
||||
|
||||
col = EnvCollector()
|
||||
all_facts: dict[str, str] = {}
|
||||
|
||||
# 本机收集
|
||||
local = col.collect_local()
|
||||
all_facts.update(local)
|
||||
|
||||
# SSH 设备收集
|
||||
for name, dev in (cfg.devices or {}).items():
|
||||
if dev.connect != "ssh":
|
||||
continue
|
||||
agent = DeviceAgent(dev)
|
||||
|
||||
def _run(cmd, remote_cwd="", timeout=60, _agent=agent):
|
||||
return _agent.run_command(cmd, remote_cwd or dev.workspace, timeout)
|
||||
|
||||
col._run_on_device = _run
|
||||
device_facts = col.collect_device(name, dev)
|
||||
all_facts.update(device_facts)
|
||||
|
||||
col.save(all_facts, memory)
|
||||
return all_facts
|
||||
```
|
||||
|
||||
将 `run_collection` 也放入 `env_collector.py`。
|
||||
|
||||
**Step 5: 运行测试**
|
||||
|
||||
```bash
|
||||
pytest tests/test_env_collector.py -v
|
||||
# Expected: 6/6 PASSED
|
||||
```
|
||||
|
||||
**Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/rockchip_agents/tools/env_collector.py \
|
||||
src/rockchip_agents/tools/project_memory.py \
|
||||
tests/test_env_collector.py
|
||||
git commit -m "feat: add EnvCollector core class with local+SSH collection"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: scripts/run.py — collect-env 子命令
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/run.py`
|
||||
- Test: `tests/test_env_collector.py`(扩展 CLI 测试)
|
||||
|
||||
**Step 1: 写失败测试**
|
||||
|
||||
```python
|
||||
# 追加到 tests/test_env_collector.py
|
||||
|
||||
def test_run_collection_prints_summary(tmp_path, capsys):
|
||||
"""run_collection 完整运行后应返回包含 local/device 信息的 facts dict。"""
|
||||
from rockchip_agents.tools.env_collector import run_collection
|
||||
from rockchip_agents.config import AgentsConfig, ClaudeConfig, SchedulerConfig, FeishuConfig
|
||||
|
||||
cfg = AgentsConfig(
|
||||
projects={},
|
||||
scheduler=SchedulerConfig(),
|
||||
claude=ClaudeConfig(api_key="fake"),
|
||||
feishu=FeishuConfig(),
|
||||
devices={},
|
||||
)
|
||||
mem = ProjectMemory(db_path=tmp_path / "m.db")
|
||||
with patch.dict(os.environ, {"RKNN_MODEL_PATH": "/data/m"}, clear=False):
|
||||
facts = run_collection(cfg, mem)
|
||||
assert "local.envvar.RKNN_MODEL_PATH" in facts
|
||||
assert "collected_at" in mem.get_facts("__env__")
|
||||
```
|
||||
|
||||
**Step 2: 运行确认失败**
|
||||
|
||||
```bash
|
||||
pytest tests/test_env_collector.py::test_run_collection_prints_summary -v
|
||||
# Expected: FAIL — run_collection 函数未集成完整
|
||||
```
|
||||
|
||||
**Step 3: 在 scripts/run.py 添加 collect-env 子命令**
|
||||
|
||||
在 `sub.add_parser("projects", ...)` 之后添加:
|
||||
|
||||
```python
|
||||
sub.add_parser("collect-env", help="收集本机+SSH设备环境信息(模型路径/磁盘/包)")
|
||||
```
|
||||
|
||||
在 `elif args.cmd == "projects":` 之后添加:
|
||||
|
||||
```python
|
||||
elif args.cmd == "collect-env":
|
||||
from rockchip_agents.config import load_config
|
||||
from rockchip_agents.tools.env_collector import run_collection
|
||||
from rockchip_agents.tools.project_memory import ProjectMemory
|
||||
cfg = load_config()
|
||||
mem = ProjectMemory()
|
||||
facts = run_collection(cfg, mem)
|
||||
|
||||
# 打印摘要
|
||||
env_vars = {k: v for k, v in facts.items() if k.startswith("local.envvar.")}
|
||||
print(f"\n[本机]")
|
||||
if env_vars:
|
||||
print(f" ENV: {len(env_vars)} 条含模型关键词变量")
|
||||
for k, v in list(env_vars.items())[:3]:
|
||||
print(f" {k.split('.')[-1]}={v}")
|
||||
for ext in ("rknn", "onnx", "pt", "pb"):
|
||||
val = facts.get(f"local.models.{ext}", "")
|
||||
if val:
|
||||
cnt = len(val.split(","))
|
||||
print(f" *.{ext}: {cnt} 个文件")
|
||||
|
||||
for key in sorted(facts):
|
||||
if ".error" in key:
|
||||
name = key.split(".")[0]
|
||||
print(f"\n[{name}]")
|
||||
print(f" ⚠ {facts[key]}")
|
||||
elif ".disk" in key and not key.startswith("local"):
|
||||
name = key.split(".")[0]
|
||||
host = ""
|
||||
from rockchip_agents.config import load_config as _lc
|
||||
_cfg = _lc()
|
||||
dev = _cfg.devices.get(name)
|
||||
if dev:
|
||||
host = f" @ {dev.host}"
|
||||
print(f"\n[{name}{host}]")
|
||||
rknn_val = facts.get(f"{name}.models.rknn", "")
|
||||
if rknn_val:
|
||||
print(f" *.rknn: {len(rknn_val.split(','))} 个文件")
|
||||
print(f" 磁盘: {facts[key][:60]}")
|
||||
pkgs = facts.get(f"{name}.packages", "")
|
||||
if pkgs:
|
||||
print(f" 关键包: {pkgs[:80]}")
|
||||
|
||||
ts = facts.get("collected_at", "")
|
||||
print(f"\n已保存到 ProjectMemory (__env__) [{ts}]")
|
||||
```
|
||||
|
||||
**Step 4: 运行测试**
|
||||
|
||||
```bash
|
||||
pytest tests/test_env_collector.py -v
|
||||
# Expected: 7/7 PASSED
|
||||
```
|
||||
|
||||
**Step 5: 手动验证 CLI**
|
||||
|
||||
```bash
|
||||
python scripts/run.py collect-env
|
||||
# Expected: 输出 [本机] ENV: N 条含模型关键词变量 + 文件统计
|
||||
```
|
||||
|
||||
**Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add scripts/run.py tests/test_env_collector.py
|
||||
git commit -m "feat: add collect-env CLI subcommand"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: scheduler.py — 24h 定时 job + 启动后台线程
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/rockchip_agents/core/scheduler.py`
|
||||
- Test: `tests/test_scheduler.py`(已有,追加测试)
|
||||
|
||||
**Step 1: 写失败测试**
|
||||
|
||||
```python
|
||||
# 追加到 tests/test_scheduler.py(或新建 tests/test_env_scheduler.py)
|
||||
|
||||
def test_scheduler_has_env_collect_job(monkeypatch):
|
||||
"""start() 添加的 APScheduler jobs 应包含 env_collect(id='env_collect')。"""
|
||||
import types
|
||||
added_jobs = []
|
||||
|
||||
class FakeScheduler:
|
||||
def add_job(self, fn, trigger, **kwargs):
|
||||
added_jobs.append(kwargs.get("id"))
|
||||
def start(self):
|
||||
raise KeyboardInterrupt # 立即退出
|
||||
|
||||
monkeypatch.setattr(
|
||||
"rockchip_agents.core.scheduler.BlockingScheduler",
|
||||
lambda: FakeScheduler()
|
||||
)
|
||||
# stub 其他副作用
|
||||
monkeypatch.setattr("rockchip_agents.core.scheduler._scan_and_enqueue", lambda: None)
|
||||
monkeypatch.setattr("rockchip_agents.core.scheduler.TaskQueue", MagicMock())
|
||||
monkeypatch.setattr("rockchip_agents.core.scheduler.Watchdog", MagicMock())
|
||||
monkeypatch.setattr("rockchip_agents.core.scheduler.load_config", MagicMock(
|
||||
return_value=MagicMock(scheduler=MagicMock(interval_hours=1), devices={}, queues={})
|
||||
))
|
||||
|
||||
from rockchip_agents.core.scheduler import start
|
||||
try:
|
||||
start()
|
||||
except (KeyboardInterrupt, Exception):
|
||||
pass
|
||||
|
||||
assert "env_collect" in added_jobs, f"未找到 env_collect job,jobs={added_jobs}"
|
||||
```
|
||||
|
||||
**Step 2: 运行确认失败**
|
||||
|
||||
```bash
|
||||
pytest tests/test_scheduler.py::test_scheduler_has_env_collect_job -v
|
||||
# Expected: AssertionError — env_collect not in added_jobs
|
||||
```
|
||||
|
||||
**Step 3: 修改 scheduler.py**
|
||||
|
||||
在 `start()` 函数中,导入并添加 `_collect_env` 包装函数(在函数顶部附近添加):
|
||||
|
||||
```python
|
||||
def _collect_env() -> None:
|
||||
"""后台静默收集环境信息,失败不影响主流程。"""
|
||||
try:
|
||||
from rockchip_agents.config import load_config
|
||||
from rockchip_agents.tools.env_collector import run_collection
|
||||
from rockchip_agents.tools.project_memory import ProjectMemory
|
||||
cfg = load_config()
|
||||
run_collection(cfg, ProjectMemory())
|
||||
logger.info("EnvCollector 完成收集")
|
||||
except Exception:
|
||||
logger.exception("EnvCollector 收集失败(静默跳过)")
|
||||
```
|
||||
|
||||
在 `start()` 函数内 `scheduler.add_job(_enqueue_market_intel_task, ...)` 之后添加:
|
||||
|
||||
```python
|
||||
scheduler.add_job(
|
||||
_collect_env, "interval", hours=24, id="env_collect",
|
||||
next_run_time=datetime.now() + timedelta(minutes=30),
|
||||
)
|
||||
```
|
||||
|
||||
在 `_scan_and_enqueue()` 调用(启动时立即扫描一次)之后,启动一个 daemon 后台线程触发首次收集(不阻塞调度器):
|
||||
|
||||
```python
|
||||
# 启动时后台触发一次环境收集(daemon,不阻塞)
|
||||
import threading
|
||||
t = threading.Thread(target=_collect_env, daemon=True, name="env-collector-init")
|
||||
t.start()
|
||||
```
|
||||
|
||||
**Step 4: 运行测试**
|
||||
|
||||
```bash
|
||||
pytest tests/test_scheduler.py -v
|
||||
# Expected: 原有测试 + 新增测试均 PASSED
|
||||
```
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/rockchip_agents/core/scheduler.py tests/test_scheduler.py
|
||||
git commit -m "feat: add EnvCollector 24h scheduler job + startup daemon thread"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: developer.py — _build_device_context 注入已知模型摘要
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/rockchip_agents/agents/developer.py`
|
||||
- Test: `tests/test_developer.py`(已有,追加测试)
|
||||
|
||||
**Step 1: 写失败测试**
|
||||
|
||||
```python
|
||||
# 追加到 tests/test_developer.py
|
||||
|
||||
def test_build_device_context_injects_env_models(tmp_path):
|
||||
"""_build_device_context 在 __env__ 有数据时应注入已知模型摘要。"""
|
||||
from rockchip_agents.config import AgentsConfig, ClaudeConfig, SchedulerConfig, FeishuConfig
|
||||
from rockchip_agents.agents.developer import DeveloperAgent
|
||||
from rockchip_agents.tools.project_memory import ProjectMemory
|
||||
|
||||
cfg = AgentsConfig(
|
||||
projects={},
|
||||
scheduler=SchedulerConfig(),
|
||||
claude=ClaudeConfig(api_key="fake"),
|
||||
feishu=FeishuConfig(),
|
||||
devices={},
|
||||
)
|
||||
mem = ProjectMemory(db_path=tmp_path / "m.db")
|
||||
mem.set_fact("__env__", "local.models.rknn", "/data/a.rknn,/data/b.rknn", role="env-collector")
|
||||
mem.set_fact("__env__", "rk3588.models.rknn", "/home/pi/model.rknn", role="env-collector")
|
||||
mem.set_fact("__env__", "rk3588.disk", "/dev/root 29G 8G 19G", role="env-collector")
|
||||
mem.set_fact("__env__", "collected_at", "2026-03-09T10:00:00", role="env-collector")
|
||||
|
||||
agent = DeveloperAgent(cfg, memory=mem)
|
||||
ctx = agent._build_device_context()
|
||||
|
||||
assert "已知模型" in ctx
|
||||
assert "/data/a.rknn" in ctx or "2 个" in ctx
|
||||
assert "rk3588" in ctx
|
||||
assert "19G" in ctx or "29G" in ctx
|
||||
|
||||
|
||||
def test_build_device_context_no_env_data(tmp_path):
|
||||
"""__env__ 无数据时,_build_device_context 不注入模型摘要(不崩溃)。"""
|
||||
from rockchip_agents.config import AgentsConfig, ClaudeConfig, SchedulerConfig, FeishuConfig
|
||||
from rockchip_agents.agents.developer import DeveloperAgent
|
||||
from rockchip_agents.tools.project_memory import ProjectMemory
|
||||
|
||||
cfg = AgentsConfig(
|
||||
projects={}, scheduler=SchedulerConfig(),
|
||||
claude=ClaudeConfig(api_key="fake"), feishu=FeishuConfig(), devices={},
|
||||
)
|
||||
mem = ProjectMemory(db_path=tmp_path / "m.db")
|
||||
agent = DeveloperAgent(cfg, memory=mem)
|
||||
ctx = agent._build_device_context() # 不应抛异常
|
||||
assert "已知模型" not in ctx
|
||||
```
|
||||
|
||||
**Step 2: 运行确认失败**
|
||||
|
||||
```bash
|
||||
pytest tests/test_developer.py::test_build_device_context_injects_env_models \
|
||||
tests/test_developer.py::test_build_device_context_no_env_data -v
|
||||
# Expected: FAIL — AssertionError: "已知模型" not in ctx
|
||||
```
|
||||
|
||||
**Step 3: 修改 developer.py _build_device_context()**
|
||||
|
||||
在 `_build_device_context()` 方法末尾 `return "\n".join(lines)` 之前,追加以下代码:
|
||||
|
||||
```python
|
||||
# 注入已知模型摘要(从 ProjectMemory __env__ 读取)
|
||||
if self._memory is not None:
|
||||
env_facts = self._memory.get_facts("__env__")
|
||||
if env_facts:
|
||||
lines.append("【已知模型文件(避免重复下载)】")
|
||||
|
||||
# 本机
|
||||
local_rknn = env_facts.get("local.models.rknn", "")
|
||||
local_onnx = env_facts.get("local.models.onnx", "")
|
||||
local_env_vars = {
|
||||
k.split(".")[-1]: v
|
||||
for k, v in env_facts.items()
|
||||
if k.startswith("local.envvar.")
|
||||
}
|
||||
lines.append("本机:")
|
||||
if local_rknn:
|
||||
lines.append(f" *.rknn: {local_rknn}")
|
||||
if local_onnx:
|
||||
lines.append(f" *.onnx: {local_onnx}")
|
||||
for var_name, var_val in local_env_vars.items():
|
||||
lines.append(f" {var_name}={var_val}(来自环境变量)")
|
||||
|
||||
# 各 SSH 设备
|
||||
device_names = set()
|
||||
for key in env_facts:
|
||||
parts = key.split(".")
|
||||
if len(parts) >= 2 and parts[0] not in ("local", "collected"):
|
||||
device_names.add(parts[0])
|
||||
|
||||
for dev_name in sorted(device_names):
|
||||
dev_cfg = self._config.devices.get(dev_name)
|
||||
host_info = f" ({dev_cfg.host})" if dev_cfg else ""
|
||||
lines.append(f"\n{dev_name}{host_info}:")
|
||||
error = env_facts.get(f"{dev_name}.error", "")
|
||||
if error:
|
||||
lines.append(f" ⚠ {error}")
|
||||
continue
|
||||
rknn = env_facts.get(f"{dev_name}.models.rknn", "")
|
||||
if rknn:
|
||||
lines.append(f" *.rknn: {rknn}")
|
||||
disk = env_facts.get(f"{dev_name}.disk", "")
|
||||
if disk:
|
||||
lines.append(f" 磁盘: {disk[:60]}")
|
||||
pkgs = env_facts.get(f"{dev_name}.packages", "")
|
||||
if pkgs:
|
||||
lines.append(f" 已装: {pkgs[:80]}")
|
||||
|
||||
collected_at = env_facts.get("collected_at", "")
|
||||
if collected_at:
|
||||
lines.append(f"\n(更新于 {collected_at},执行 `python scripts/run.py collect-env` 可刷新)")
|
||||
lines.append("")
|
||||
```
|
||||
|
||||
**Step 4: 运行测试**
|
||||
|
||||
```bash
|
||||
pytest tests/test_developer.py -v
|
||||
# Expected: 全部 PASSED(含两个新测试)
|
||||
```
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/rockchip_agents/agents/developer.py tests/test_developer.py
|
||||
git commit -m "feat: inject known model summary into DeveloperAgent device context"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 全量验证
|
||||
|
||||
**Step 1: 运行全量测试**
|
||||
|
||||
```bash
|
||||
pytest tests/ -q
|
||||
# Expected: 全部通过(含 test_env_collector.py 的 8+ 个测试)
|
||||
```
|
||||
|
||||
**Step 2: 检查测试覆盖关键路径**
|
||||
|
||||
确认以下场景有测试:
|
||||
- [ ] `collect_local` 过滤环境变量关键词
|
||||
- [ ] `collect_local` 递归扫描 + 跳过 venv
|
||||
- [ ] `collect_device` SSH 成功(mock)
|
||||
- [ ] `collect_device` SSH 离线 → error key
|
||||
- [ ] `save()` 写入 ProjectMemory
|
||||
- [ ] `env-collector` WRITE_ROLES 权限
|
||||
- [ ] scheduler 有 `env_collect` job
|
||||
- [ ] `_build_device_context` 注入模型摘要
|
||||
- [ ] `_build_device_context` 无 __env__ 数据时静默
|
||||
|
||||
**Step 3: 手动冒烟测试 CLI**
|
||||
|
||||
```bash
|
||||
# 应能正常运行并打印摘要
|
||||
python scripts/run.py collect-env
|
||||
|
||||
# 验证数据写入
|
||||
python scripts/run.py facts
|
||||
# Expected: 显示 __env__ 下的 local.models.* / local.envvar.* 等条目
|
||||
```
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "test: verify EnvCollector full integration"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 实施注意事项
|
||||
|
||||
1. **`_run_on_device` 占位符**:`EnvCollector.collect_device()` 内部调用 `self._run_on_device()`,这个方法在 `EnvCollector` 类中 raise `NotImplementedError`。`run_collection()` 函数在调用前通过 monkeypatching(`col._run_on_device = _run`)注入真实的 `DeviceAgent.run_command`。测试同样用此机制 mock。
|
||||
|
||||
2. **WRITE_ROLES 扩展**:`project_memory.py` 中 `WRITE_ROLES` 需添加 `"env-collector"`,否则 `set_fact()` 会抛 `PermissionError`。
|
||||
|
||||
3. **`_memory` 字段检查**:`DeveloperAgent` 的 `__init__` 接收 `memory: ProjectMemory | None = None`,注入代码需先检查 `if self._memory is not None`。
|
||||
|
||||
4. **scheduler import**:`_collect_env()` 函数内部延迟 import(`from rockchip_agents.tools.env_collector import run_collection`),避免在模块加载时引入不必要依赖。
|
||||
|
||||
5. **设备 workspace 容错**:`find` 命令的 `remote_cwd` 用 `dev.workspace`,若设备 workspace 不存在,`find` 会返回错误但不影响其他命令执行。
|
||||
@@ -0,0 +1,170 @@
|
||||
# Visual Tester 设计文档
|
||||
|
||||
## 概述
|
||||
|
||||
基于 Playwright 截图对比的 UI 视觉回归测试系统,支持多级断点恢复,集成至 Dashboard 看板,自动发现 `/data/company` 下所有 Web UI。
|
||||
|
||||
---
|
||||
|
||||
## 1. 目录结构 & /data/company 迁移
|
||||
|
||||
### /data/company 目录布局
|
||||
|
||||
```
|
||||
/data/company/
|
||||
├── visual-tester/ # 新建独立服务
|
||||
│ ├── src/visual_tester/
|
||||
│ │ ├── runner.py # Playwright 执行引擎 + checkpoint
|
||||
│ │ ├── differ.py # PIL/pixelmatch 差异计算
|
||||
│ │ ├── discovery.py # 自动发现 Web UI
|
||||
│ │ └── db.py # SQLite 数据层
|
||||
│ ├── baselines/ # {project}/{name}.png 基线截图
|
||||
│ ├── diffs/ # {run_id}/{case_id}_diff.png 差异图
|
||||
│ ├── checkpoints/ # {run_id}.json 会话恢复文件
|
||||
│ ├── data/visual.db
|
||||
│ ├── requirements.txt
|
||||
│ └── README.md
|
||||
├── agents/ → symlink /data/rockchip/agents
|
||||
├── yolo/ → symlink /data/rockchip/yolo
|
||||
└── ...(其他项目符号链接)
|
||||
```
|
||||
|
||||
### configs/projects.yaml 迁移
|
||||
|
||||
- `scan_dirs` 从 `/data/rockchip` 更改为 `/data/company`
|
||||
- 删除旧的显式 `projects:` 条目(由 scan_dirs 自动发现替代)
|
||||
- `data/tasks.db` 历史数据保留(不清除),仅新扫描基于新路径
|
||||
|
||||
---
|
||||
|
||||
## 2. 数据模型 & 断点恢复机制
|
||||
|
||||
### SQLite 表结构(visual.db)
|
||||
|
||||
```sql
|
||||
CREATE TABLE test_runs (
|
||||
id TEXT PRIMARY KEY,
|
||||
project TEXT NOT NULL,
|
||||
started_at TEXT NOT NULL,
|
||||
completed_at TEXT,
|
||||
status TEXT NOT NULL, -- running | done | interrupted
|
||||
total INTEGER DEFAULT 0,
|
||||
passed INTEGER DEFAULT 0,
|
||||
failed INTEGER DEFAULT 0,
|
||||
pending_review INTEGER DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE test_cases (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
run_id TEXT NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
status TEXT NOT NULL, -- pending | running | passed | failed | needs_review | skipped
|
||||
screenshot_path TEXT,
|
||||
baseline_path TEXT,
|
||||
diff_path TEXT,
|
||||
diff_pct REAL,
|
||||
FOREIGN KEY (run_id) REFERENCES test_runs(id)
|
||||
);
|
||||
|
||||
CREATE TABLE baselines (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
project TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
commit_hash TEXT DEFAULT '',
|
||||
created_at TEXT NOT NULL,
|
||||
is_current INTEGER DEFAULT 1
|
||||
);
|
||||
|
||||
CREATE TABLE approvals (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
case_id INTEGER NOT NULL,
|
||||
action TEXT NOT NULL, -- approve | reject
|
||||
reason TEXT DEFAULT '',
|
||||
approved_by TEXT DEFAULT 'human',
|
||||
approved_at TEXT NOT NULL,
|
||||
FOREIGN KEY (case_id) REFERENCES test_cases(id)
|
||||
);
|
||||
```
|
||||
|
||||
### 四级断点恢复
|
||||
|
||||
| 级别 | 触发场景 | 恢复策略 |
|
||||
|------|---------|---------|
|
||||
| **测试套件恢复** | 进程崩溃 / 手动中断 | 跳过 status≠pending 的 case,重置 running→pending |
|
||||
| **基线版本管理** | 误操作覆盖基线 | `baselines.is_current=0` 软删除,可随时切回 |
|
||||
| **差异审批流** | 截图差异需人工确认 | status=needs_review 暂停队列,Dashboard 审批后继续 |
|
||||
| **Claude 会话恢复** | 断网 / 关机 / token 耗尽 | `checkpoints/{run_id}.json` 记录 last_case_id + progress |
|
||||
|
||||
Checkpoint JSON 格式:
|
||||
```json
|
||||
{
|
||||
"run_id": "abc123",
|
||||
"last_case_id": 42,
|
||||
"progress": "15/30",
|
||||
"status": "interrupted",
|
||||
"claude_session_id": "sess_xxx",
|
||||
"paused_at": "2026-03-09T10:30:00"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 自动发现 & Runner 工作流 & Dashboard 集成
|
||||
|
||||
### 自动发现优先级(discovery.py)
|
||||
|
||||
1. `CLAUDE.md` 中 `## 服务端口` / `## Web UI` 段落提取 URL
|
||||
2. `README.md` 中 `http://localhost:XXXX` 正则扫描
|
||||
3. `docker-compose.yml` 中 `ports: ["XXXX:XXXX"]` 提取宿主机端口
|
||||
|
||||
发现结果缓存至 `visual.db`,支持手动覆盖。
|
||||
|
||||
### Runner 工作流(runner.py)
|
||||
|
||||
```
|
||||
runner.run(project, resume=run_id)
|
||||
→ 加载/创建 test_run
|
||||
→ 筛选 pending cases(断点恢复跳过已完成)
|
||||
→ for each case:
|
||||
screenshot via Playwright → differ.compare()
|
||||
diff_pct < threshold → passed
|
||||
diff_pct > threshold AND has baseline → needs_review(暂停等待审批)
|
||||
no baseline → auto-set as baseline (passed)
|
||||
timeout/error → failed
|
||||
写 checkpoint
|
||||
→ 更新 run status → done / interrupted
|
||||
```
|
||||
|
||||
差异阈值:默认 `diff_threshold=0.5%`,可按项目配置。
|
||||
|
||||
### Dashboard 新增 Visual 标签页
|
||||
|
||||
**新增 API 端点:**
|
||||
|
||||
```
|
||||
GET /api/visual/runs # 最近 20 条运行记录
|
||||
GET /api/visual/runs/{run_id}/cases # 该次运行的所有 case
|
||||
POST /api/visual/runs # 触发新一轮测试(异步)
|
||||
POST /api/visual/cases/{id}/approve # 批准差异 → 更新基线,status=passed
|
||||
POST /api/visual/cases/{id}/reject # 拒绝差异 → status=failed,保留旧基线
|
||||
GET /api/visual/baselines/{project} # 列出该项目所有基线(含历史)
|
||||
POST /api/visual/runs/{run_id}/resume # 从断点继续
|
||||
```
|
||||
|
||||
**前端视图:**
|
||||
- 运行列表:项目 / 时间 / 进度条 / 状态徽章
|
||||
- Case 列表:三栏对比(基线 | 当前 | 差异)+ 审批按钮
|
||||
- 需审批的 case 在顶部 "⚠ N 个截图待审批" 聚合
|
||||
|
||||
---
|
||||
|
||||
## 实施原则
|
||||
|
||||
- **TDD**:先写失败测试,再实现
|
||||
- **YAGNI**:不提前实现 AI 差异分析、多分辨率适配等
|
||||
- **断点即一等公民**:每个 case 完成后立即 flush checkpoint
|
||||
- **轻依赖**:PIL(pillow)+ Playwright,无额外服务依赖
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
fastapi>=0.110.0
|
||||
uvicorn[standard]>=0.27.0
|
||||
websockets>=12.0
|
||||
paramiko>=3.0.0
|
||||
pyyaml>=6.0
|
||||
pillow>=10.0
|
||||
numpy>=1.24
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rockchip Agents Dashboard 独立服务"""
|
||||
"""NMFS Agents Dashboard 独立服务"""
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess, sys as _sys
|
||||
@@ -70,7 +70,7 @@ def _watcher(app_state, loop: asyncio.AbstractEventLoop) -> None:
|
||||
|
||||
|
||||
def main() -> None:
|
||||
from rockchip_agents.dashboard.api import make_app
|
||||
from nmfs_agents.dashboard.api import make_app
|
||||
|
||||
app = make_app(db_path=DB_PATH, config_dir=CONFIG_DIR,
|
||||
global_claude_dir=GLOBAL_CLAUDE_DIR)
|
||||
|
||||
+25
-25
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rockchip Agents 启动入口"""
|
||||
"""NMFS Agents 启动入口"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
@@ -9,7 +9,7 @@ logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Rockchip Agents")
|
||||
parser = argparse.ArgumentParser(description="NMFS Agents")
|
||||
sub = parser.add_subparsers(dest="cmd")
|
||||
sub.add_parser("scan", help="扫描所有项目,打印发现的任务(不执行)")
|
||||
sub.add_parser("run", help="扫描 → 入队 → 执行所有任务(一键完成)")
|
||||
@@ -33,8 +33,8 @@ def main() -> None:
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.cmd == "scan":
|
||||
from rockchip_agents.core.scanner import Scanner
|
||||
from rockchip_agents.config import load_config
|
||||
from nmfs_agents.core.scanner import Scanner
|
||||
from nmfs_agents.config import load_config
|
||||
cfg = load_config()
|
||||
tasks = Scanner(cfg).scan_all()
|
||||
if not tasks:
|
||||
@@ -44,23 +44,23 @@ def main() -> None:
|
||||
|
||||
elif args.cmd == "run":
|
||||
import asyncio
|
||||
from rockchip_agents.agents.manager import ManagerAgent
|
||||
from rockchip_agents.config import load_config
|
||||
from nmfs_agents.agents.manager import ManagerAgent
|
||||
from nmfs_agents.config import load_config
|
||||
cfg = load_config()
|
||||
stats = asyncio.run(ManagerAgent(cfg).run_cycle())
|
||||
print(f"扫描入队: {stats['enqueued']} 个,执行完成: {stats['executed']} 个")
|
||||
|
||||
elif args.cmd == "run-next":
|
||||
import asyncio
|
||||
from rockchip_agents.core.executor import Executor
|
||||
from rockchip_agents.config import load_config
|
||||
from nmfs_agents.core.executor import Executor
|
||||
from nmfs_agents.config import load_config
|
||||
cfg = load_config()
|
||||
ran = asyncio.run(Executor(cfg).run_next())
|
||||
print("已执行" if ran else "队列为空")
|
||||
|
||||
elif args.cmd == "status":
|
||||
from rockchip_agents.agents.manager import ManagerAgent
|
||||
from rockchip_agents.config import load_config
|
||||
from nmfs_agents.agents.manager import ManagerAgent
|
||||
from nmfs_agents.config import load_config
|
||||
cfg = load_config()
|
||||
status = ManagerAgent(cfg).get_status()
|
||||
if not status:
|
||||
@@ -70,12 +70,12 @@ def main() -> None:
|
||||
print(f" {state}: {count}")
|
||||
|
||||
elif args.cmd == "start":
|
||||
from rockchip_agents.core.scheduler import start
|
||||
from nmfs_agents.core.scheduler import start
|
||||
start()
|
||||
|
||||
elif args.cmd == "arch-versions":
|
||||
from rockchip_agents.config import load_config
|
||||
from rockchip_agents.tools.arch_version import ArchVersion
|
||||
from nmfs_agents.config import load_config
|
||||
from nmfs_agents.tools.arch_version import ArchVersion
|
||||
cfg = load_config()
|
||||
proj_cfg = cfg.projects.get(args.project)
|
||||
if not proj_cfg:
|
||||
@@ -93,8 +93,8 @@ def main() -> None:
|
||||
print(f" {v.version_id}{mark} [{v.status:12s}] [{passed}] {v.created_at[:16]} {v.description}")
|
||||
|
||||
elif args.cmd == "arch-restore":
|
||||
from rockchip_agents.config import load_config
|
||||
from rockchip_agents.tools.arch_version import ArchVersion
|
||||
from nmfs_agents.config import load_config
|
||||
from nmfs_agents.tools.arch_version import ArchVersion
|
||||
cfg = load_config()
|
||||
proj_cfg = cfg.projects.get(args.project)
|
||||
if not proj_cfg:
|
||||
@@ -105,7 +105,7 @@ def main() -> None:
|
||||
print(f"已将 {args.project} 恢复到 {args.version_id}")
|
||||
|
||||
elif args.cmd == "goals":
|
||||
from rockchip_agents.tools.project_memory import ProjectMemory
|
||||
from nmfs_agents.tools.project_memory import ProjectMemory
|
||||
goals = ProjectMemory().list_goals()
|
||||
if not goals:
|
||||
print("暂无项目目标记录(运行 productizer 或 architect 任务后自动写入)")
|
||||
@@ -115,8 +115,8 @@ def main() -> None:
|
||||
print(f" {g.goal_text}")
|
||||
|
||||
elif args.cmd == "facts":
|
||||
from rockchip_agents.tools.project_memory import ProjectMemory
|
||||
from rockchip_agents.config import load_config
|
||||
from nmfs_agents.tools.project_memory import ProjectMemory
|
||||
from nmfs_agents.config import load_config
|
||||
mem = ProjectMemory()
|
||||
cfg = load_config()
|
||||
any_found = False
|
||||
@@ -131,13 +131,13 @@ def main() -> None:
|
||||
print("暂无项目事实记录(运行 architect 任务后自动写入)")
|
||||
|
||||
elif args.cmd == "set-fact":
|
||||
from rockchip_agents.tools.project_memory import ProjectMemory
|
||||
from nmfs_agents.tools.project_memory import ProjectMemory
|
||||
mem = ProjectMemory()
|
||||
mem.set_fact(args.project, args.key, args.value, role="developer")
|
||||
print(f"已写入: [{args.project}] {args.key} = {args.value}")
|
||||
|
||||
elif args.cmd == "projects":
|
||||
from rockchip_agents.config import load_config
|
||||
from nmfs_agents.config import load_config
|
||||
cfg = load_config()
|
||||
print(f"{'项目名':20s} {'类型':8s} {'模式':8s} 路径")
|
||||
print("-" * 70)
|
||||
@@ -146,9 +146,9 @@ def main() -> None:
|
||||
print(f"{name:20s} {p.kind:8s} {p.mode:8s} {p.path}{dw}")
|
||||
|
||||
elif args.cmd == "collect-env":
|
||||
from rockchip_agents.config import load_config
|
||||
from rockchip_agents.tools.env_collector import run_collection
|
||||
from rockchip_agents.tools.project_memory import ProjectMemory
|
||||
from nmfs_agents.config import load_config
|
||||
from nmfs_agents.tools.env_collector import run_collection
|
||||
from nmfs_agents.tools.project_memory import ProjectMemory
|
||||
cfg = load_config()
|
||||
mem = ProjectMemory()
|
||||
facts = run_collection(cfg, mem)
|
||||
@@ -196,8 +196,8 @@ def main() -> None:
|
||||
print(f"\n已保存到 ProjectMemory (__env__) [{ts}]")
|
||||
|
||||
elif args.cmd == "research":
|
||||
from rockchip_agents.config import load_config
|
||||
from rockchip_agents.core.queue import TaskQueue, Task
|
||||
from nmfs_agents.config import load_config
|
||||
from nmfs_agents.core.queue import TaskQueue, Task
|
||||
cfg = load_config()
|
||||
q = TaskQueue()
|
||||
research_tasks = [
|
||||
|
||||
@@ -0,0 +1,359 @@
|
||||
"""
|
||||
edge-platform 全链路任务种子脚本。
|
||||
|
||||
向三个队列注入初始任务,覆盖所有 Agent 角色,建立完整的前后依赖上下文。
|
||||
|
||||
使用方法:
|
||||
source venv/bin/activate
|
||||
python scripts/seed_edge_platform.py [--dry-run]
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
||||
|
||||
from nmfs_agents.core.queue import Task, TaskQueue
|
||||
from nmfs_agents.tools.project_memory import ProjectMemory
|
||||
|
||||
PROJECT = "edge-platform"
|
||||
PLATFORM_DESC = "边端 AI 推理服务平台(RK3588 NPU + RTP 流媒体),目标:FPS 60+,延迟 <80ms"
|
||||
|
||||
# ── 初始项目目标(productizer 写入,其他 agent 读取)────────────────────────
|
||||
INITIAL_GOALS = {
|
||||
"productizer": "边端 AI 推理服务平台达到生产可用:内存稳定、NPU三核并行、RTP延迟<80ms",
|
||||
"architect": "整合 YOLO/MPP/RTP 技术栈,实现从摄像头到客户端的完整推理链路",
|
||||
}
|
||||
|
||||
# ── 初始知识库(agent 可直接读取)───────────────────────────────────────────
|
||||
INITIAL_FACTS = {
|
||||
"fps_baseline": "28",
|
||||
"rtp_latency_baseline_ms": "180",
|
||||
"npu_core_mode": "NPU_CORE_0(单核,需改为 NPU_CORE_ALL)",
|
||||
"known_bugs": "内存泄漏,错误处理缺失,模型热加载缺失,RTP延迟高",
|
||||
"test_cmd": "pytest tests/ -v",
|
||||
"model_path": "/data/models/yolo11n.rknn",
|
||||
}
|
||||
|
||||
|
||||
def seed_memory(mem: ProjectMemory) -> None:
|
||||
"""写入初始项目目标和知识库,为 agent 提供起点上下文。"""
|
||||
for role, goal in INITIAL_GOALS.items():
|
||||
try:
|
||||
mem.set_goal(PROJECT, goal, role=role)
|
||||
except Exception:
|
||||
pass # 已存在则跳过
|
||||
for key, val in INITIAL_FACTS.items():
|
||||
try:
|
||||
mem.set_fact(PROJECT, key, val, role="productizer")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def build_tasks() -> list[tuple[str, list[Task]]]:
|
||||
"""构建所有队列的初始任务列表。
|
||||
|
||||
返回 [(queue_name, [Task, ...]), ...]
|
||||
"""
|
||||
# ── project_delivery 队列任务 ──────────────────────────────────────────
|
||||
delivery_tasks = [
|
||||
# 1. Productizer 先做成熟度评估,设定长期目标
|
||||
Task(
|
||||
project=PROJECT,
|
||||
type="productize",
|
||||
title="初始成熟度评估:建立产品基线",
|
||||
context=(
|
||||
f"{PLATFORM_DESC}\n\n"
|
||||
"请评估当前项目成熟度(0-100分),重点关注:\n"
|
||||
"- 功能完整性:推理 API/RTP 推流/错误处理\n"
|
||||
"- 性能基线:FPS 28(目标 60),延迟 180ms(目标 80ms)\n"
|
||||
"- 测试覆盖:test_api.py / test_inference.py / test_streamer.py\n"
|
||||
"输出 [记忆] edge-platform: maturity_score=N,设定产品目标。"
|
||||
),
|
||||
priority=1,
|
||||
mode="report",
|
||||
agent_role="productizer",
|
||||
initiator="seed-script",
|
||||
),
|
||||
|
||||
# 2. Architect 做跨项目分析,派发开发任务
|
||||
Task(
|
||||
project="ALL",
|
||||
type="architect",
|
||||
title="全局架构分析:edge-platform 与现有项目整合",
|
||||
context=(
|
||||
"重点分析新加入的 edge-platform 项目与现有项目的整合机会:\n"
|
||||
"- edge-platform(推理服务)+ yolo(检测模型)的集成路径\n"
|
||||
"- edge-platform + embedding(向量检索)的语义搜索扩展\n"
|
||||
"- edge-platform + claude-wx(飞书通知)的告警接入\n"
|
||||
"- RTP 延迟问题与 os-base 调研的关联\n\n"
|
||||
"必须输出以下任务:\n"
|
||||
"[开发] edge-platform: 修复推理引擎内存泄漏——将模型加载移至 __init__,添加 release()\n"
|
||||
"[开发] edge-platform: 修复 NPU 单核问题——改 NPU_CORE_ALL 并测试 FPS\n"
|
||||
"[开发] edge-platform: 添加 /infer 错误处理——try/except base64/reshape 异常返回 400\n"
|
||||
"[测试] edge-platform: 运行全量测试套件并报告失败的 test_api.py 断言\n"
|
||||
"[记忆] edge-platform: arch_integration=yolo+embedding+claude-wx 三路整合方案已分析"
|
||||
),
|
||||
priority=2,
|
||||
mode="report",
|
||||
agent_role="architect",
|
||||
initiator="seed-script",
|
||||
),
|
||||
|
||||
# 3. Developer 修复 P0 内存泄漏(最高优先级)
|
||||
Task(
|
||||
project=PROJECT,
|
||||
type="fix_bug",
|
||||
title="[修复] P0 内存泄漏:推理引擎模型重复加载",
|
||||
context=(
|
||||
"文件:src/inference.py\n\n"
|
||||
"问题:RknnInferenceEngine.infer() 每次调用都执行 _load_model(),"
|
||||
"导致 RKNN 对象未释放,长时运行内存持续增长(500次请求 +200MB)。\n\n"
|
||||
"修复方案:\n"
|
||||
"1. 在 __init__ 中调用 _load_model() 并保存到 self._rknn\n"
|
||||
"2. infer() 直接使用 self._rknn\n"
|
||||
"3. 添加 __del__ 或 close() 方法释放 rknn 对象\n"
|
||||
"4. 运行 tests/test_inference.py::test_memory_no_growth_over_requests 验证\n\n"
|
||||
"完成后输出 [SPAWN:tester] 验证: 内存泄漏修复验证,运行 test_memory_no_growth"
|
||||
),
|
||||
priority=1,
|
||||
mode="auto",
|
||||
agent_role="developer",
|
||||
initiator="seed-script",
|
||||
discussion=f"来自 seed-script 初始化:{PLATFORM_DESC},P0 内存泄漏需优先修复",
|
||||
),
|
||||
|
||||
# 4. Developer 修复 NPU 单核问题
|
||||
Task(
|
||||
project=PROJECT,
|
||||
type="fix_bug",
|
||||
title="[修复] P1 NPU 单核 → 三核并行,目标 FPS 60+",
|
||||
context=(
|
||||
"文件:src/inference.py\n\n"
|
||||
"问题:init_runtime(core_mask=RKNNLite.NPU_CORE_0) 只使用单核,"
|
||||
"三核空闲,FPS 仅 28(目标 60+)。\n\n"
|
||||
"修复方案:\n"
|
||||
"1. 改为 core_mask=RKNNLite.NPU_CORE_ALL\n"
|
||||
"2. 在 infer() 末尾记录 fps 并更新 [记忆] edge-platform: fps_actual=N\n"
|
||||
"3. 更新 FPS_TARGET=60 断言\n\n"
|
||||
"完成后输出 [SPAWN:system-tester] 验证: 板端 FPS 回归测试,验证 NPU_CORE_ALL 效果"
|
||||
),
|
||||
priority=2,
|
||||
mode="auto",
|
||||
agent_role="developer",
|
||||
initiator="seed-script",
|
||||
),
|
||||
|
||||
# 5. Developer 修复错误处理
|
||||
Task(
|
||||
project=PROJECT,
|
||||
type="fix_bug",
|
||||
title="[修复] P1 /infer 端点错误处理——base64/reshape 异常返回 400",
|
||||
context=(
|
||||
"文件:src/server.py\n\n"
|
||||
"问题:/infer 端点无 try/except,无效 base64 或尺寸不符直接 500。\n\n"
|
||||
"修复方案:\n"
|
||||
"1. 用 try/except 包装 base64.b64decode,抛出 HTTPException(400)\n"
|
||||
"2. 用 try/except 包装 np.reshape,抛出 HTTPException(400)\n"
|
||||
"3. 在 infer() 调用处捕获推理异常,返回 HTTPException(422)\n"
|
||||
"4. 运行 tests/test_api.py 所有用例(含 test_infer_invalid_base64_returns_error)\n\n"
|
||||
"完成后输出 [SPAWN:tester] 验证: 错误处理修复验证,运行 test_api.py"
|
||||
),
|
||||
priority=2,
|
||||
mode="auto",
|
||||
agent_role="developer",
|
||||
initiator="seed-script",
|
||||
),
|
||||
|
||||
# 6. System Tester 全系统测试(含视觉推理验证)
|
||||
Task(
|
||||
project=PROJECT,
|
||||
type="test",
|
||||
title="全系统测试:功能+性能+板端验证",
|
||||
context=(
|
||||
f"{PLATFORM_DESC}\n\n"
|
||||
"测试清单:\n"
|
||||
"1. 功能测试:pytest tests/ -v(本机),重点关注失败用例\n"
|
||||
"2. 性能测试:连续调用 /infer 100 次,统计 avg_fps 和 p95 延迟\n"
|
||||
"3. 板端测试(可选):ssh pi@192.168.123.181 运行 pytest tests/\n"
|
||||
"4. 内存稳定性:验证 500 次请求后内存无增长\n\n"
|
||||
"发现问题请输出 [需求] edge-platform: <具体问题描述>\n"
|
||||
"性能基线请输出 [记忆] edge-platform: fps_system_test=N,latency_p95_ms=N"
|
||||
),
|
||||
priority=3,
|
||||
mode="auto",
|
||||
agent_role="system-tester",
|
||||
initiator="seed-script",
|
||||
),
|
||||
]
|
||||
|
||||
# ── insight 队列任务 ───────────────────────────────────────────────────
|
||||
insight_tasks = [
|
||||
# Planner 制定市场洞察计划
|
||||
Task(
|
||||
project=PROJECT,
|
||||
type="plan",
|
||||
title="边端 AI 推理服务市场洞察规划",
|
||||
context=(
|
||||
f"{PLATFORM_DESC}\n\n"
|
||||
"请制定市场洞察调研计划:\n"
|
||||
"- 边端 AI 推理服务(Edge Inference as a Service)市场规模\n"
|
||||
"- RK3588 平台在工业/安防/运动分析场景的竞争格局\n"
|
||||
"- 实时视频推理服务的付费意愿调研\n"
|
||||
"输出结构化调研任务给 market-pm 执行。"
|
||||
),
|
||||
priority=3,
|
||||
mode="auto",
|
||||
agent_role="planner",
|
||||
initiator="seed-script",
|
||||
),
|
||||
|
||||
# Market PM 分析市场机会
|
||||
Task(
|
||||
project=PROJECT,
|
||||
type="market_research",
|
||||
title="边端 AI 推理服务市场分析:运动/工业/安防三方向",
|
||||
context=(
|
||||
f"{PLATFORM_DESC}\n\n"
|
||||
"请分析以下三个目标市场的机会:\n\n"
|
||||
"1. 【运动】实时动作识别(跑步/球类)+ 教练反馈,B 端体育机构\n"
|
||||
" - 竞品:Camera IQ、Hudl,RK3588 本地化的差异点\n"
|
||||
" - 付费场景:赛事直播增强、训练质量评估\n\n"
|
||||
"2. 【工业/安防】边端目标检测,私有部署无需云端\n"
|
||||
" - 客户痛点:数据隐私、网络依赖、云计算费用\n"
|
||||
" - 定价逻辑:一次性硬件+软件授权 vs 订阅\n\n"
|
||||
"3. 【AI 安全/隐私】端侧推理保证数据不出设备\n"
|
||||
" - 与 info-privacy 项目的结合点\n\n"
|
||||
"输出格式:\n"
|
||||
"[调研报告] __market__.运动.opportunity=<市场机会摘要>\n"
|
||||
"[调研报告] __market__.工业.opportunity=<市场机会摘要>\n"
|
||||
"[调研报告] __market__.AI安全.opportunity=<市场机会摘要>"
|
||||
),
|
||||
priority=3,
|
||||
mode="auto",
|
||||
agent_role="market-pm",
|
||||
initiator="seed-script",
|
||||
),
|
||||
]
|
||||
|
||||
# ── base_opt 队列任务 ──────────────────────────────────────────────────
|
||||
base_opt_tasks = [
|
||||
# RTP 调研(直接关联 edge-platform 的 RTP 延迟问题)
|
||||
Task(
|
||||
project="rtp-research",
|
||||
type="research",
|
||||
title="RTP 延迟优化调研:针对 edge-platform MPP+RTP 瓶颈",
|
||||
context=(
|
||||
"关联项目:edge-platform(src/streamer.py)\n"
|
||||
"当前瓶颈:MPP 编码器每帧重新初始化,端到端延迟 ~180ms,目标 <80ms。\n\n"
|
||||
"请重点调研:\n"
|
||||
"1. MPP 编码器会话保持(mpp_enc_cfg_init/mpp_enc_ref_cfg_init 复用)\n"
|
||||
"2. RTP 包分片策略(MTU=1500 下 H264 NAL 单元分割)\n"
|
||||
"3. SO_PRIORITY + SO_SNDBUF 对 UDP 延迟的影响\n"
|
||||
"4. 零拷贝 DMA buffer → RTP payload 路径\n\n"
|
||||
"输出 [优化任务] os-engineer: <具体 sysctl/MPP API 优化步骤>"
|
||||
),
|
||||
priority=2,
|
||||
mode="auto",
|
||||
agent_role="rtp-researcher",
|
||||
initiator="seed-script",
|
||||
discussion=f"edge-platform RTP 延迟 180ms,超过目标 80ms",
|
||||
),
|
||||
|
||||
# Net 调研(API 服务网络调优)
|
||||
Task(
|
||||
project="net-research",
|
||||
type="research",
|
||||
title="网络栈调优调研:edge-platform 推理 API 响应时间优化",
|
||||
context=(
|
||||
"关联项目:edge-platform(src/server.py,FastAPI on uvicorn)\n"
|
||||
"当前问题:高并发下 API 响应时间不稳定(p99 > 200ms)。\n\n"
|
||||
"请调研:\n"
|
||||
"1. uvicorn worker 数量与 RK3588 CPU 核心绑定策略\n"
|
||||
"2. TCP_NODELAY + SO_REUSEPORT 对 FastAPI 延迟的影响\n"
|
||||
"3. 推理服务与 RTP 推流共用网卡时的 QoS 优先级划分\n"
|
||||
"4. 内核 net.core.somaxconn / net.ipv4.tcp_tw_reuse 参数建议\n\n"
|
||||
"输出具体 sysctl 命令和 uvicorn 启动参数建议\n"
|
||||
"输出 [优化任务] os-engineer: <网络参数配置步骤>"
|
||||
),
|
||||
priority=2,
|
||||
mode="auto",
|
||||
agent_role="net-researcher",
|
||||
initiator="seed-script",
|
||||
),
|
||||
|
||||
# Kernel 调研(NPU + API 并发调度)
|
||||
Task(
|
||||
project="kernel-analysis",
|
||||
type="research",
|
||||
title="内核调度调研:NPU 推理与 RTP 推流的资源竞争",
|
||||
context=(
|
||||
"关联项目:edge-platform(推理 API + RTP 推流并发运行)\n"
|
||||
"当前问题:NPU 推理(大核 A76)与 RTP 编码(MPP/VPU)争抢内存带宽,"
|
||||
"高负载下 FPS 从 28 降至 15。\n\n"
|
||||
"请调研:\n"
|
||||
"1. RK3588 NPU/VPU/ISP 内存带宽分配(IOMMU/CMA 配置)\n"
|
||||
"2. isolcpus=4-7(A76 大核)专用于推理进程\n"
|
||||
"3. SCHED_FIFO 实时优先级给 RTP 推流线程\n"
|
||||
"4. CPUFreq governor=performance 对 NPU 推理 FPS 的影响\n\n"
|
||||
"输出 [优化任务] dev-kernel: <内核参数/Kconfig 建议>"
|
||||
),
|
||||
priority=2,
|
||||
mode="auto",
|
||||
agent_role="kernel-analyzer",
|
||||
initiator="seed-script",
|
||||
),
|
||||
]
|
||||
|
||||
return [
|
||||
("project_delivery", delivery_tasks),
|
||||
("insight", insight_tasks),
|
||||
("base_opt", base_opt_tasks),
|
||||
]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="seed edge-platform tasks")
|
||||
parser.add_argument("--dry-run", action="store_true", help="只显示,不写入")
|
||||
args = parser.parse_args()
|
||||
|
||||
# 初始化 memory
|
||||
mem = ProjectMemory()
|
||||
|
||||
# 构建并注入初始队列任务
|
||||
queue_configs = [
|
||||
("project_delivery", "data/tasks.db"),
|
||||
("insight", "data/insight_tasks.db"),
|
||||
("base_opt", "data/base_opt_tasks.db"),
|
||||
]
|
||||
task_map = dict(build_tasks())
|
||||
|
||||
total = 0
|
||||
for queue_name, db_path in queue_configs:
|
||||
tasks = task_map.get(queue_name, [])
|
||||
if not tasks:
|
||||
continue
|
||||
print(f"\n[{queue_name}] ({db_path}) — {len(tasks)} 个任务:")
|
||||
for t in tasks:
|
||||
print(f" [{t.agent_role:16s}] {t.title[:60]}")
|
||||
if not args.dry_run:
|
||||
q = TaskQueue(db_path=Path(db_path))
|
||||
for t in tasks:
|
||||
q.enqueue(t)
|
||||
total += len(tasks)
|
||||
|
||||
if not args.dry_run:
|
||||
# 写入初始 memory
|
||||
seed_memory(mem)
|
||||
print(f"\n✓ 已写入 {total} 个初始任务")
|
||||
print("✓ 已写入 edge-platform 项目目标和知识库")
|
||||
print("\n启动调度器执行:bash scripts/start_daemon.sh")
|
||||
print("监控进度: bash scripts/watch_daemon.sh")
|
||||
else:
|
||||
print(f"\n[dry-run] 共 {sum(len(v) for v in task_map.values())} 个任务,未写入")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+51
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
# NMFS Agents 后台守护启动脚本
|
||||
# 用法:bash scripts/start_daemon.sh [--hours 24]
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
LOG_DIR="$PROJECT_ROOT/logs"
|
||||
PID_FILE="$PROJECT_ROOT/logs/daemon.pid"
|
||||
LOG_FILE="$LOG_DIR/daemon_$(date '+%Y%m%d_%H%M%S').log"
|
||||
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
# ── 检查是否已在运行 ────────────────────────────────────────────
|
||||
if [[ -f "$PID_FILE" ]]; then
|
||||
OLD_PID=$(cat "$PID_FILE")
|
||||
if kill -0 "$OLD_PID" 2>/dev/null; then
|
||||
echo "⚠️ 守护进程已在运行(PID=$OLD_PID),如需重启请先执行:"
|
||||
echo " kill $OLD_PID && rm $PID_FILE"
|
||||
exit 1
|
||||
else
|
||||
echo "旧 PID 文件残留,已清理"
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── 激活虚拟环境 ────────────────────────────────────────────────
|
||||
VENV="$PROJECT_ROOT/venv"
|
||||
if [[ ! -f "$VENV/bin/activate" ]]; then
|
||||
echo "❌ 未找到虚拟环境: $VENV/bin/activate"
|
||||
echo "请先执行: python3.10 -m venv venv && pip install -r requirements.txt"
|
||||
exit 1
|
||||
fi
|
||||
source "$VENV/bin/activate"
|
||||
|
||||
# ── 启动调度器(后台 nohup)────────────────────────────────────
|
||||
echo "🚀 启动 NMFS Agents 守护进程"
|
||||
echo " 日志文件: $LOG_FILE"
|
||||
echo " PID 文件: $PID_FILE"
|
||||
|
||||
nohup python "$PROJECT_ROOT/scripts/run.py" start \
|
||||
>> "$LOG_FILE" 2>&1 &
|
||||
|
||||
DAEMON_PID=$!
|
||||
echo "$DAEMON_PID" > "$PID_FILE"
|
||||
echo "✅ 守护进程已启动(PID=$DAEMON_PID)"
|
||||
echo ""
|
||||
echo "查看日志: tail -f $LOG_FILE"
|
||||
echo "查看状态: $VENV/bin/python $PROJECT_ROOT/scripts/run.py status"
|
||||
echo "查看目标: $VENV/bin/python $PROJECT_ROOT/scripts/run.py goals"
|
||||
echo "停止进程: kill $DAEMON_PID"
|
||||
@@ -0,0 +1,400 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
24小时项目自动化运行测试 - 基于 Playwright 浏览器自动化
|
||||
|
||||
用法:
|
||||
python scripts/test_24h.py --hours 24 --project /data/rockchip/logodetect-x86
|
||||
python scripts/test_24h.py --hours 1 --project /data/rockchip/logodetect-x86 --dry-run
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import time
|
||||
import urllib.request
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
DASHBOARD_URL = "http://localhost:9080"
|
||||
REPORT_DIR = Path("tmp/test_24h_report")
|
||||
CHECK_INTERVAL = 900 # 每 15 分钟一次
|
||||
|
||||
|
||||
# ── 辅助函数 ─────────────────────────────────────────────────────────
|
||||
|
||||
def _api(method: str, path: str, body: dict | None = None) -> dict:
|
||||
url = DASHBOARD_URL + path
|
||||
data = json.dumps(body).encode() if body else None
|
||||
headers = {"Content-Type": "application/json"} if data else {}
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=10) as r:
|
||||
return json.loads(r.read())
|
||||
|
||||
|
||||
def _get_project_tasks(project_name: str) -> list[dict]:
|
||||
return _api("GET", f"/api/tasks?project={project_name}")
|
||||
|
||||
|
||||
def _task_stats(tasks: list[dict]) -> dict:
|
||||
statuses = [t["status"] for t in tasks]
|
||||
return {
|
||||
"total": len(tasks),
|
||||
"pending": statuses.count("pending"),
|
||||
"running": statuses.count("running"),
|
||||
"done": statuses.count("done"),
|
||||
"failed": statuses.count("failed"),
|
||||
"vetoed": statuses.count("vetoed"),
|
||||
"waiting": statuses.count("waiting_approval"),
|
||||
}
|
||||
|
||||
|
||||
# ── Phase 1: 浏览器 UI 操作(Playwright)─────────────────────────────
|
||||
|
||||
def setup_via_browser(
|
||||
page, # playwright Page
|
||||
project_path: str,
|
||||
linked_project: str,
|
||||
project_name: str,
|
||||
) -> dict:
|
||||
"""通过浏览器 UI 完整设置项目(导入 + 生成任务 + 启动执行器)"""
|
||||
from playwright.sync_api import expect
|
||||
|
||||
print(f"[setup] 打开 Dashboard: {DASHBOARD_URL}")
|
||||
page.goto(DASHBOARD_URL)
|
||||
page.wait_for_timeout(2000)
|
||||
|
||||
# 截图:初始状态
|
||||
_screenshot(page, "00_initial")
|
||||
|
||||
# 点击「导入项目」
|
||||
print("[setup] 点击「导入项目」")
|
||||
page.click("button:has-text('导入项目')")
|
||||
page.wait_for_timeout(500)
|
||||
|
||||
# 填写路径(触发项目名自动推断)
|
||||
print(f"[setup] 填写路径: {project_path}")
|
||||
# 第 1 个 input = 目录路径
|
||||
page.locator("input").nth(0).fill(project_path)
|
||||
page.wait_for_timeout(500)
|
||||
|
||||
# 填写关联项目(第 3 个 input,placeholder 含 logo-collect)
|
||||
if linked_project:
|
||||
print(f"[setup] 填写关联项目: {linked_project}")
|
||||
page.locator("input").nth(2).fill(linked_project)
|
||||
|
||||
# 确认 mode = auto
|
||||
mode_select = page.locator("select")
|
||||
mode_select.select_option("auto")
|
||||
|
||||
# 点击「预览任务」展开模板
|
||||
try:
|
||||
page.click("button:has-text('预览任务')")
|
||||
page.wait_for_timeout(300)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
_screenshot(page, "01_import_modal_filled")
|
||||
|
||||
# 点击「导入并生成任务」
|
||||
print("[setup] 提交导入表单")
|
||||
page.click("button:has-text('导入并生成任务')")
|
||||
page.wait_for_timeout(3000)
|
||||
|
||||
_screenshot(page, "02_import_success")
|
||||
|
||||
# 点击「完成」关闭成功弹窗
|
||||
try:
|
||||
page.click("button:has-text('完成')")
|
||||
page.wait_for_timeout(1000)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 切换到目标项目 Tab
|
||||
print(f"[setup] 切换到项目: {project_name}")
|
||||
try:
|
||||
page.click(f"button:has-text('{project_name}')")
|
||||
page.wait_for_timeout(500)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
_screenshot(page, "03_project_kanban")
|
||||
|
||||
# 启动执行器(点击「开始执行」)
|
||||
print("[setup] 启动执行器")
|
||||
try:
|
||||
page.click("button:has-text('开始执行')")
|
||||
page.wait_for_timeout(2000)
|
||||
except Exception as e:
|
||||
print(f"[WARN] 启动执行器按钮未找到: {e}")
|
||||
|
||||
_screenshot(page, "04_executor_started")
|
||||
|
||||
# 收集初始任务统计
|
||||
tasks = _get_project_tasks(project_name)
|
||||
stats = _task_stats(tasks)
|
||||
print(f"[setup] 初始任务统计: {stats}")
|
||||
return stats
|
||||
|
||||
|
||||
# ── Phase 2: 24 小时监控循环 ─────────────────────────────────────────
|
||||
|
||||
def monitoring_loop(
|
||||
page,
|
||||
project_name: str,
|
||||
total_seconds: int,
|
||||
dry_run: bool = False,
|
||||
) -> list[dict]:
|
||||
"""每 CHECK_INTERVAL 秒检查一次,记录任务进展"""
|
||||
metrics: list[dict] = []
|
||||
start_time = time.time()
|
||||
check_num = 0
|
||||
interval = 60 if dry_run else CHECK_INTERVAL # dry-run 用 1 分钟
|
||||
|
||||
print(f"\n[monitor] 开始监控,总时长 {total_seconds/3600:.1f}h,间隔 {interval}s")
|
||||
|
||||
while time.time() - start_time < total_seconds:
|
||||
check_num += 1
|
||||
elapsed_s = time.time() - start_time
|
||||
elapsed_h = elapsed_s / 3600
|
||||
|
||||
print(f"\n[monitor] 第 {check_num} 次检查 ({elapsed_h:.2f}h / {total_seconds/3600:.1f}h)")
|
||||
|
||||
# 刷新页面,截图
|
||||
try:
|
||||
page.goto(DASHBOARD_URL)
|
||||
try:
|
||||
page.click(f"button:has-text('{project_name}')")
|
||||
except Exception:
|
||||
pass
|
||||
page.wait_for_timeout(2000)
|
||||
_screenshot(page, f"monitor_{check_num:04d}_{elapsed_h:.1f}h")
|
||||
except Exception as e:
|
||||
print(f"[WARN] 截图失败: {e}")
|
||||
|
||||
# 收集任务统计
|
||||
try:
|
||||
tasks = _get_project_tasks(project_name)
|
||||
stats = _task_stats(tasks)
|
||||
exec_status = _api("GET", "/api/executor/status")
|
||||
|
||||
record = {
|
||||
"check": check_num,
|
||||
"elapsed_h": round(elapsed_h, 2),
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"tasks": stats,
|
||||
"executor": exec_status,
|
||||
"done_titles": [t["title"] for t in tasks if t["status"] == "done"],
|
||||
"failed_titles": [t["title"] for t in tasks if t["status"] == "failed"],
|
||||
}
|
||||
metrics.append(record)
|
||||
print(f" 任务: {stats}")
|
||||
print(f" 执行器: running={exec_status.get('running')}, "
|
||||
f"active={exec_status.get('active')}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[WARN] 指标收集失败: {e}")
|
||||
|
||||
# 检查执行器是否停止,若停止则重启
|
||||
try:
|
||||
exec_st = _api("GET", "/api/executor/status")
|
||||
if not exec_st.get("running") and exec_st.get("pending", 0) > 0:
|
||||
print("[monitor] 执行器已停止,重新启动...")
|
||||
_api("POST", "/api/executor/start")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 等待下次检查
|
||||
next_check = start_time + check_num * interval
|
||||
wait_s = max(0, next_check - time.time())
|
||||
if wait_s > 0 and time.time() - start_time < total_seconds:
|
||||
print(f" 下次检查:{wait_s:.0f}s 后")
|
||||
time.sleep(wait_s)
|
||||
|
||||
return metrics
|
||||
|
||||
|
||||
# ── Phase 3: 评估报告生成 ─────────────────────────────────────────────
|
||||
|
||||
def generate_report(
|
||||
project_name: str,
|
||||
project_path: str,
|
||||
linked_project: str,
|
||||
metrics: list[dict],
|
||||
total_hours: float,
|
||||
page,
|
||||
) -> Path:
|
||||
"""生成 Markdown 评估报告"""
|
||||
if not metrics:
|
||||
print("[report] 无指标数据")
|
||||
return REPORT_DIR / "report.md"
|
||||
|
||||
first = metrics[0]["tasks"]
|
||||
last = metrics[-1]["tasks"]
|
||||
|
||||
tasks_done = last["done"]
|
||||
tasks_total = last["total"]
|
||||
tasks_failed = last["failed"]
|
||||
completion_pct = round(tasks_done / max(tasks_total, 1) * 100, 1)
|
||||
|
||||
# 收集所有 done 任务标题
|
||||
all_done = []
|
||||
for m in metrics:
|
||||
for title in m.get("done_titles", []):
|
||||
if title not in all_done:
|
||||
all_done.append(title)
|
||||
|
||||
all_failed = []
|
||||
for m in metrics:
|
||||
for title in m.get("failed_titles", []):
|
||||
if title not in all_failed:
|
||||
all_failed.append(title)
|
||||
|
||||
# 执行器在线率
|
||||
online_checks = sum(1 for m in metrics if m.get("executor", {}).get("running"))
|
||||
online_pct = round(online_checks / max(len(metrics), 1) * 100, 1)
|
||||
|
||||
report_lines = [
|
||||
f"# {total_hours:.0f}h 自动化运行评估报告",
|
||||
f"",
|
||||
f"**项目**: `{project_name}` ({project_path})",
|
||||
f"**关联项目**: `{linked_project or '无'}`",
|
||||
f"**报告时间**: {datetime.now().strftime('%Y-%m-%d %H:%M')}",
|
||||
f"**运行时长**: {total_hours:.1f} 小时",
|
||||
f"",
|
||||
f"---",
|
||||
f"",
|
||||
f"## 执行摘要",
|
||||
f"",
|
||||
f"| 指标 | 数值 |",
|
||||
f"|------|------|",
|
||||
f"| 总任务数 | {tasks_total} |",
|
||||
f"| 完成任务 | {tasks_done} ({completion_pct}%) |",
|
||||
f"| 失败任务 | {tasks_failed} |",
|
||||
f"| 待执行 | {last['pending']} |",
|
||||
f"| 执行器在线率 | {online_pct}% |",
|
||||
f"",
|
||||
f"## 已完成任务",
|
||||
f"",
|
||||
]
|
||||
for t in all_done:
|
||||
report_lines.append(f"- ✅ {t}")
|
||||
|
||||
if all_failed:
|
||||
report_lines += ["", "## 失败任务", ""]
|
||||
for t in all_failed:
|
||||
report_lines.append(f"- ❌ {t}")
|
||||
|
||||
report_lines += [
|
||||
"",
|
||||
"## 进度时间线",
|
||||
"",
|
||||
"| 时间点 | 完成 | 失败 | 待执行 |",
|
||||
"|--------|------|------|--------|",
|
||||
]
|
||||
for m in metrics[::4] + ([metrics[-1]] if metrics else []): # 每4条取1条
|
||||
s = m["tasks"]
|
||||
report_lines.append(
|
||||
f"| {m['elapsed_h']:.1f}h | {s['done']} | {s['failed']} | {s['pending']} |"
|
||||
)
|
||||
|
||||
report_lines += [
|
||||
"",
|
||||
"## 截图目录",
|
||||
f"",
|
||||
f"见 `{REPORT_DIR}/` 目录,共 {len(list(REPORT_DIR.glob('*.png')))} 张截图。",
|
||||
"",
|
||||
"---",
|
||||
f"*由 `scripts/test_24h.py` 自动生成*",
|
||||
]
|
||||
|
||||
report_path = REPORT_DIR / "report.md"
|
||||
report_path.write_text("\n".join(report_lines), encoding="utf-8")
|
||||
print(f"\n[report] 报告已生成: {report_path}")
|
||||
return report_path
|
||||
|
||||
|
||||
# ── 主入口 ────────────────────────────────────────────────────────────
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="24h 项目自动化运行测试")
|
||||
parser.add_argument("--project", default="/data/rockchip/logodetect-x86",
|
||||
help="项目目录路径")
|
||||
parser.add_argument("--linked", default="/data/test-platform/logo-collect",
|
||||
help="关联项目路径(用于集成任务)")
|
||||
parser.add_argument("--hours", type=float, default=24.0,
|
||||
help="运行时长(小时),默认 24")
|
||||
parser.add_argument("--dry-run", action="store_true",
|
||||
help="快速测试模式(1 分钟间隔,5 分钟总时长)")
|
||||
parser.add_argument("--headless", action="store_true", default=True,
|
||||
help="无头模式运行浏览器(默认开启)")
|
||||
parser.add_argument("--no-headless", dest="headless", action="store_false",
|
||||
help="显示浏览器窗口(需要显示器)")
|
||||
args = parser.parse_args()
|
||||
|
||||
project_path = args.project
|
||||
project_name = Path(project_path).name
|
||||
linked_project = args.linked
|
||||
total_hours = 5 / 60 if args.dry_run else args.hours # dry-run = 5 分钟
|
||||
total_seconds = int(total_hours * 3600)
|
||||
|
||||
REPORT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
metrics_path = REPORT_DIR / "metrics.jsonl"
|
||||
|
||||
print("=" * 60)
|
||||
print(f"NMFS Agents 自动化测试")
|
||||
print(f" 项目: {project_name} ({project_path})")
|
||||
print(f" 关联: {linked_project}")
|
||||
print(f" 时长: {total_hours:.2f}h ({'dry-run' if args.dry_run else '正式'})")
|
||||
print(f" 报告: {REPORT_DIR}/")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
from playwright.sync_api import sync_playwright
|
||||
except ImportError:
|
||||
print("[ERROR] playwright 未安装,运行: pip install playwright && playwright install chromium")
|
||||
raise SystemExit(1)
|
||||
|
||||
with sync_playwright() as pw:
|
||||
browser = pw.chromium.launch(headless=args.headless)
|
||||
ctx = browser.new_context(viewport={"width": 1440, "height": 900})
|
||||
page = ctx.new_page()
|
||||
|
||||
try:
|
||||
# Phase 1: 设置
|
||||
print("\n=== Phase 1: 浏览器 UI 设置 ===")
|
||||
setup_via_browser(page, project_path, linked_project, project_name)
|
||||
|
||||
# Phase 2: 监控
|
||||
print("\n=== Phase 2: 监控循环 ===")
|
||||
metrics = monitoring_loop(page, project_name, total_seconds, args.dry_run)
|
||||
|
||||
# 保存原始指标
|
||||
with open(metrics_path, "w") as f:
|
||||
for m in metrics:
|
||||
f.write(json.dumps(m, ensure_ascii=False) + "\n")
|
||||
|
||||
# Phase 3: 生成报告
|
||||
print("\n=== Phase 3: 生成评估报告 ===")
|
||||
_screenshot(page, "final_state")
|
||||
report_path = generate_report(
|
||||
project_name, project_path, linked_project,
|
||||
metrics, total_hours, page,
|
||||
)
|
||||
print(f"\n完成!评估报告: {report_path}")
|
||||
|
||||
finally:
|
||||
browser.close()
|
||||
|
||||
|
||||
def _screenshot(page, name: str) -> None:
|
||||
try:
|
||||
path = REPORT_DIR / f"{name}.png"
|
||||
page.screenshot(path=str(path), full_page=False)
|
||||
print(f" [screenshot] {path.name}")
|
||||
except Exception as e:
|
||||
print(f" [WARN] 截图失败 {name}: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+54
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
# 实时监控 NMFS Agents 守护进程
|
||||
# 用法:bash scripts/watch_daemon.sh
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
PID_FILE="$PROJECT_ROOT/logs/daemon.pid"
|
||||
VENV="$PROJECT_ROOT/venv"
|
||||
|
||||
if [[ ! -f "$PID_FILE" ]]; then
|
||||
echo "守护进程未运行(PID 文件不存在)"
|
||||
echo "启动方法: bash scripts/start_daemon.sh"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
PID=$(cat "$PID_FILE")
|
||||
if ! kill -0 "$PID" 2>/dev/null; then
|
||||
echo "守护进程已停止(PID=$PID 不存在)"
|
||||
rm -f "$PID_FILE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "═══════════════════════════════════════════"
|
||||
echo " NMFS Agents 监控 PID=$PID"
|
||||
echo " $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo "═══════════════════════════════════════════"
|
||||
|
||||
# 任务队列状态
|
||||
echo ""
|
||||
echo "── 任务队列状态 ─────────────────────────────"
|
||||
source "$VENV/bin/activate"
|
||||
python "$PROJECT_ROOT/scripts/run.py" status 2>/dev/null || echo "(获取失败)"
|
||||
|
||||
# 项目目标
|
||||
echo ""
|
||||
echo "── 项目目标记忆 ─────────────────────────────"
|
||||
python "$PROJECT_ROOT/scripts/run.py" goals 2>/dev/null || echo "(获取失败)"
|
||||
|
||||
# memory-analyze 版本历史
|
||||
echo ""
|
||||
echo "── memory-analyze 架构版本 ──────────────────"
|
||||
python "$PROJECT_ROOT/scripts/run.py" arch-versions memory-analyze 2>/dev/null || echo "(获取失败)"
|
||||
|
||||
# 最新日志
|
||||
echo ""
|
||||
echo "── 最新日志(最近 30 行)────────────────────"
|
||||
LATEST_LOG=$(ls -t "$PROJECT_ROOT/logs"/daemon_*.log 2>/dev/null | head -1)
|
||||
if [[ -n "$LATEST_LOG" ]]; then
|
||||
echo " 文件: $LATEST_LOG"
|
||||
tail -30 "$LATEST_LOG"
|
||||
else
|
||||
echo "(暂无日志文件)"
|
||||
fi
|
||||
@@ -0,0 +1,14 @@
|
||||
[Unit]
|
||||
Description=EdgeValidatorService for RK3588
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=pi
|
||||
WorkingDirectory=/home/pi/Desktop/edge_validator
|
||||
ExecStart=/home/pi/Desktop/edge_validator/venv/bin/uvicorn main:app --host 0.0.0.0 --port 8899
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,207 @@
|
||||
"""EdgeValidatorService: 部署在 RK3588 上,为开发机 agents 提供 HTTP 接口。
|
||||
|
||||
启动:uvicorn main:app --host 0.0.0.0 --port 8899
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import glob
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI(title="EdgeValidatorService", version="1.0.0")
|
||||
app.add_middleware(CORSMiddleware, allow_origins=["*"],
|
||||
allow_methods=["*"], allow_headers=["*"])
|
||||
|
||||
# 设备配置(本机 + 接入的 MCU)
|
||||
SERIAL_DEVICES: dict[str, dict] = {}
|
||||
|
||||
# 推理日志路径
|
||||
LOG_PATHS = {
|
||||
"default": "/tmp/infer.log",
|
||||
"yolo": "/tmp/yolo_infer.log",
|
||||
}
|
||||
|
||||
|
||||
class RunTestRequest(BaseModel):
|
||||
cmd: str
|
||||
cwd: str = "/home/pi/Desktop"
|
||||
timeout: int = 120
|
||||
|
||||
|
||||
class FlashRequest(BaseModel):
|
||||
firmware_b64: str
|
||||
offset: str = "0x0"
|
||||
port: str = "/dev/ttyUSB0"
|
||||
|
||||
|
||||
# ── 端点 ──────────────────────────────────────────────────────────
|
||||
|
||||
@app.get("/health")
|
||||
def health():
|
||||
return {"status": "ok", "timestamp": datetime.utcnow().isoformat()}
|
||||
|
||||
|
||||
@app.get("/devices")
|
||||
def list_devices():
|
||||
"""列出识别到的串口设备"""
|
||||
serial_ports = glob.glob("/dev/ttyUSB*") + glob.glob("/dev/ttyACM*")
|
||||
return {"rk3588": "linux/ssh", "serial_ports": serial_ports,
|
||||
"configured_mcus": list(SERIAL_DEVICES.keys())}
|
||||
|
||||
|
||||
@app.get("/metrics/{device_name}")
|
||||
def get_metrics(device_name: str):
|
||||
if device_name == "rk3588":
|
||||
return _linux_metrics()
|
||||
if device_name in SERIAL_DEVICES:
|
||||
return _mcu_metrics(SERIAL_DEVICES[device_name])
|
||||
raise HTTPException(404, f"未知设备: {device_name}")
|
||||
|
||||
|
||||
@app.post("/run-test/{device_name}")
|
||||
def run_test(device_name: str, req: RunTestRequest):
|
||||
if device_name == "rk3588":
|
||||
return _run_local(req)
|
||||
if device_name in SERIAL_DEVICES:
|
||||
return _run_serial(SERIAL_DEVICES[device_name], req)
|
||||
raise HTTPException(404, f"未知设备: {device_name}")
|
||||
|
||||
|
||||
@app.post("/screenshot")
|
||||
def screenshot():
|
||||
"""截取 RK3588 屏幕(需要 scrot 或 ffmpeg)"""
|
||||
import tempfile
|
||||
import os
|
||||
tmp = tempfile.mktemp(suffix=".png")
|
||||
r = subprocess.run(["scrot", tmp], capture_output=True, timeout=10)
|
||||
if r.returncode != 0:
|
||||
r = subprocess.run(
|
||||
f"cat /dev/fb0 | ffmpeg -vcodec rawvideo -f rawvideo "
|
||||
f"-pix_fmt rgb32 -s 1920x1080 -i - -f image2 -vcodec png {tmp} -y",
|
||||
shell=True, capture_output=True, timeout=15,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
raise HTTPException(500, "截图失败,请确认 scrot 或 ffmpeg 已安装")
|
||||
with open(tmp, "rb") as f:
|
||||
img_b64 = base64.b64encode(f.read()).decode()
|
||||
os.unlink(tmp)
|
||||
return {"image_b64": img_b64, "format": "png"}
|
||||
|
||||
|
||||
@app.get("/logs/{device_name}/{n}")
|
||||
def get_logs(device_name: str, n: int = 50):
|
||||
log_path = LOG_PATHS.get(device_name, LOG_PATHS["default"])
|
||||
try:
|
||||
r = subprocess.run(["tail", f"-{n}", log_path],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
return {"lines": r.stdout.splitlines(), "file": log_path}
|
||||
except Exception as e:
|
||||
return {"lines": [], "error": str(e), "file": log_path}
|
||||
|
||||
|
||||
# ── 内部函数 ──────────────────────────────────────────────────────
|
||||
|
||||
def _run_local(req: RunTestRequest) -> dict:
|
||||
t0 = time.monotonic()
|
||||
r = subprocess.run(
|
||||
req.cmd, shell=True, capture_output=True, text=True,
|
||||
cwd=req.cwd, timeout=req.timeout,
|
||||
)
|
||||
duration = time.monotonic() - t0
|
||||
stdout = r.stdout
|
||||
pass_ = r.returncode == 0 and not re.search(r"\b(FAILED|ERROR)\b", stdout + r.stderr)
|
||||
return {"rc": r.returncode, "stdout": stdout, "stderr": r.stderr,
|
||||
"duration_s": round(duration, 2), "pass_": pass_}
|
||||
|
||||
|
||||
def _run_serial(dev: dict, req: RunTestRequest) -> dict:
|
||||
try:
|
||||
import serial # type: ignore
|
||||
except ImportError:
|
||||
return {"rc": 1, "stdout": "", "stderr": "pyserial 未安装",
|
||||
"duration_s": 0, "pass_": False}
|
||||
t0 = time.monotonic()
|
||||
lines: list[str] = []
|
||||
try:
|
||||
ser = serial.Serial(dev["port"], dev.get("baud", 115200), timeout=req.timeout)
|
||||
ser.write((req.cmd + "\r\n").encode())
|
||||
deadline = time.monotonic() + req.timeout
|
||||
while time.monotonic() < deadline:
|
||||
line = ser.readline().decode(errors="replace").strip()
|
||||
if line:
|
||||
lines.append(line)
|
||||
if line.startswith(("PASS", "FAIL", "ERROR")):
|
||||
break
|
||||
ser.close()
|
||||
except Exception as e:
|
||||
return {"rc": 1, "stdout": "\n".join(lines), "stderr": str(e),
|
||||
"duration_s": time.monotonic() - t0, "pass_": False}
|
||||
stdout = "\n".join(lines)
|
||||
rc = 0 if any(l.startswith("PASS") for l in lines) else 1
|
||||
return {"rc": rc, "stdout": stdout, "stderr": "",
|
||||
"duration_s": round(time.monotonic() - t0, 2), "pass_": rc == 0}
|
||||
|
||||
|
||||
def _linux_metrics() -> dict:
|
||||
m: dict = {"device": "rk3588", "timestamp": datetime.utcnow().isoformat()}
|
||||
try:
|
||||
with open("/proc/stat") as f:
|
||||
parts = f.readline().split()
|
||||
user, nice, sys_, idle = int(parts[1]), int(parts[2]), int(parts[3]), int(parts[4])
|
||||
total = user + nice + sys_ + idle
|
||||
m["cpu_pct"] = round(100 * (user + sys_) / total, 1) if total else 0
|
||||
except Exception:
|
||||
m["cpu_pct"] = None
|
||||
try:
|
||||
r = subprocess.run(["free", "-m"], capture_output=True, text=True, timeout=3)
|
||||
for line in r.stdout.splitlines():
|
||||
if line.startswith("Mem:"):
|
||||
parts = line.split()
|
||||
m["mem_used_mb"] = float(parts[2])
|
||||
break
|
||||
except Exception:
|
||||
m["mem_used_mb"] = None
|
||||
try:
|
||||
r = subprocess.run(
|
||||
"tail -10 /tmp/infer.log 2>/dev/null | grep -oP 'fps=\\K[0-9.]+' | tail -1",
|
||||
shell=True, capture_output=True, text=True, timeout=3,
|
||||
)
|
||||
raw = r.stdout.strip()
|
||||
m["fps"] = float(raw) if raw else None
|
||||
except Exception:
|
||||
m["fps"] = None
|
||||
return m
|
||||
|
||||
|
||||
def _mcu_metrics(dev: dict) -> dict:
|
||||
m: dict = {"device": dev.get("name", "mcu"),
|
||||
"timestamp": datetime.utcnow().isoformat()}
|
||||
try:
|
||||
import serial # type: ignore
|
||||
ser = serial.Serial(dev["port"], dev.get("baud", 115200), timeout=3)
|
||||
ser.write(b"GET_METRICS\r\n")
|
||||
for _ in range(20):
|
||||
line = ser.readline().decode(errors="replace").strip()
|
||||
if line.startswith("METRICS:"):
|
||||
for kv in line[8:].split():
|
||||
k, _, v = kv.partition("=")
|
||||
if k == "fps":
|
||||
m["fps"] = float(v)
|
||||
elif k == "latency_ms":
|
||||
m["inference_latency_ms"] = float(v)
|
||||
elif k == "ram_kb":
|
||||
m["ram_used_kb"] = int(v)
|
||||
break
|
||||
ser.close()
|
||||
except Exception as e:
|
||||
m["error"] = str(e)
|
||||
return m
|
||||
@@ -0,0 +1,3 @@
|
||||
fastapi>=0.110.0
|
||||
uvicorn[standard]>=0.29.0
|
||||
pyserial>=3.5
|
||||
@@ -0,0 +1,418 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from nmfs_agents.agents.developer import (
|
||||
AgentResult, _parse_cli_result, _make_drop_root_preexec,
|
||||
_run_with_log, _task_log_path,
|
||||
)
|
||||
from nmfs_agents.config import AgentsConfig
|
||||
from nmfs_agents.core.queue import Task, TaskQueue
|
||||
from nmfs_agents.tools.arch_version import ArchVersion
|
||||
from nmfs_agents.tools.project_memory import ProjectMemory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
TIMEOUT = 3600
|
||||
TEST_TIMEOUT = 120
|
||||
|
||||
ARCHITECT_SYSTEM = """你是一位技术架构师,负责分析多个软件项目,识别整合机会,提出架构改进建议。
|
||||
|
||||
分析维度:
|
||||
- 功能互补性:哪些项目的能力可以组合,产生新价值
|
||||
- 技术共性:是否有可复用的基础设施或数据管道
|
||||
- 产品可能性:整合后能解决什么实际问题
|
||||
|
||||
请用中文输出结构化分析报告,末尾按格式列出任务(每行一条):
|
||||
- [预研] 任务名称 ← 跨项目探索、新功能研究
|
||||
- [优化] <项目名>: 优化描述 ← 对特定项目的架构改进(项目名须在上面列出的项目中)
|
||||
- [开发] <项目名>: 任务描述 ← 派发具体开发任务给 developer(可多条,多开发者并行执行)
|
||||
- [测试] <项目名>: 验证要点 ← 派发集成测试任务给 tester
|
||||
- [重构] <项目名>: 重构内容 ← 派发代码重构任务给 dev-refactor
|
||||
- [内核开发] <项目名>: 任务 ← 派发内核驱动开发任务给 dev-kernel
|
||||
- [目标] <项目名>: 目标描述 ← 更新项目长期目标(仅当需要修订时)
|
||||
- [记忆] <项目名>: key=value, key2=value2 ← 记录项目关键技术事实
|
||||
- [后续] 自动续链: 描述 ← 基于本次分析发现,列出1-3条应立即跟进的技术调研或验证任务
|
||||
|
||||
## 技能配置能力(Skills)
|
||||
如分析中发现 Agent 工具/能力需要调整,可直接修改配置文件:
|
||||
- MCP 工具配置:`/data/company/agents/configs/mcp_tools.yaml`(角色工具授权)
|
||||
- Agent 配置:`/data/company/agents/configs/agents.yaml`(队列、并发、角色列表)
|
||||
- 每次修改配置文件后,在 [记忆] 中记录变更原因:key=config_update, value=<变更摘要>
|
||||
"""
|
||||
|
||||
|
||||
class ArchitectAgent:
|
||||
def __init__(self, config: AgentsConfig,
|
||||
queue: Optional[TaskQueue] = None,
|
||||
memory: Optional[ProjectMemory] = None) -> None:
|
||||
self._config = config
|
||||
self._queue = queue or TaskQueue()
|
||||
self._memory = memory or ProjectMemory()
|
||||
|
||||
# ── entry point ───────────────────────────────────────────────────────────
|
||||
|
||||
def run(self, task: Task) -> AgentResult:
|
||||
"""Route to optimize flow or cross-project analysis based on task type."""
|
||||
if not self._verify_claude_binary():
|
||||
return AgentResult(status="failed", summary="claude CLI 不可达,bootstrap 验证失败")
|
||||
if task.type == "arch_optimize":
|
||||
return self._optimize(task)
|
||||
return self._analyze(task)
|
||||
|
||||
@staticmethod
|
||||
def _verify_claude_binary() -> bool:
|
||||
"""验证 claude CLI 可达性(bootstrap 检查)。"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["claude", "--version"],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
logger.info("claude CLI 可达: %s", result.stdout.strip())
|
||||
return True
|
||||
logger.warning("claude --version 返回非零码 %d: %s", result.returncode, result.stderr.strip())
|
||||
return False
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired) as e:
|
||||
logger.error("claude CLI 不可达: %s", e)
|
||||
return False
|
||||
|
||||
# ── cross-project analysis ────────────────────────────────────────────────
|
||||
|
||||
def _analyze(self, task: Task) -> AgentResult:
|
||||
"""Generate architecture report and enqueue [预研]/[优化] tasks."""
|
||||
summaries = self._collect_summaries()
|
||||
cmd = [
|
||||
"claude", "--print",
|
||||
"--output-format", "json",
|
||||
"--no-session-persistence",
|
||||
"--model", self._config.claude.model,
|
||||
"--allowedTools", "Read,Glob,Grep",
|
||||
"--append-system-prompt", ARCHITECT_SYSTEM,
|
||||
"--", self._build_prompt(summaries),
|
||||
]
|
||||
try:
|
||||
_env = self._build_env()
|
||||
result = _run_with_log(
|
||||
cmd, None, _env, TIMEOUT, _task_log_path(task.id),
|
||||
preexec_fn=_make_drop_root_preexec(_env),
|
||||
)
|
||||
if result.status == "done":
|
||||
self._extract_and_enqueue(result.summary, task.project)
|
||||
logger.info("架构分析完成,已生成 %d 字报告", len(result.summary))
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("ArchitectAgent 分析失败: %s", e)
|
||||
return AgentResult(status="failed", summary=str(e))
|
||||
|
||||
# ── iterative optimization with version control ───────────────────────────
|
||||
|
||||
def _optimize(self, task: Task) -> AgentResult:
|
||||
"""Apply an architectural optimization with snapshot/test/rollback cycle."""
|
||||
proj_cfg = self._config.projects.get(task.project)
|
||||
if not proj_cfg:
|
||||
return AgentResult(status="failed",
|
||||
summary=f"项目 {task.project!r} 未在配置中找到")
|
||||
|
||||
arch_v = ArchVersion(proj_cfg.path)
|
||||
version_id = arch_v.snapshot(task.title)
|
||||
|
||||
# Apply optimization
|
||||
cmd = [
|
||||
"claude", "--print",
|
||||
"--output-format", "json",
|
||||
"--no-session-persistence",
|
||||
"--model", self._config.claude.model,
|
||||
"--dangerously-skip-permissions",
|
||||
"--append-system-prompt", ARCHITECT_SYSTEM,
|
||||
"--", self._build_optimize_prompt(task),
|
||||
]
|
||||
try:
|
||||
_env = self._build_env()
|
||||
apply_result = _run_with_log(
|
||||
cmd, str(proj_cfg.path), _env, TIMEOUT, _task_log_path(task.id),
|
||||
preexec_fn=_make_drop_root_preexec(_env),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("arch_optimize 执行失败: %s", e)
|
||||
try:
|
||||
arch_v.rollback(version_id, str(e))
|
||||
except Exception:
|
||||
pass
|
||||
return AgentResult(status="failed", summary=str(e))
|
||||
|
||||
if apply_result.status == "failed":
|
||||
arch_v.rollback(version_id, apply_result.summary)
|
||||
return AgentResult(
|
||||
status="failed",
|
||||
summary=f"[{version_id}] 优化执行失败,已回滚:\n{apply_result.summary}",
|
||||
)
|
||||
|
||||
# Validate with tests
|
||||
test_res = self._run_tests(proj_cfg.path)
|
||||
if test_res["passed"]:
|
||||
arch_v.promote(version_id, test_res["output"])
|
||||
return AgentResult(
|
||||
status="done",
|
||||
summary=(
|
||||
f"[{version_id}] 优化成功并晋升\n"
|
||||
f"{apply_result.summary}\n\n"
|
||||
f"测试: {test_res['output'][:300]}"
|
||||
),
|
||||
)
|
||||
else:
|
||||
arch_v.rollback(version_id, test_res["output"])
|
||||
return AgentResult(
|
||||
status="failed",
|
||||
summary=(
|
||||
f"[{version_id}] 优化后测试失败,已回滚\n"
|
||||
f"{test_res['output'][:400]}"
|
||||
),
|
||||
)
|
||||
|
||||
# ── helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
def _collect_summaries(self) -> dict[str, str]:
|
||||
return {
|
||||
name: self._read_project_summary(proj_cfg.path)
|
||||
for name, proj_cfg in self._config.projects.items()
|
||||
}
|
||||
|
||||
def _read_project_summary(self, path: Path) -> str:
|
||||
for fname in ("README.md", "CLAUDE.md"):
|
||||
f = path / fname
|
||||
if not f.exists():
|
||||
continue
|
||||
lines = [l.strip() for l in f.read_text(errors="ignore").splitlines()
|
||||
if l.strip() and not l.startswith("#")]
|
||||
if lines:
|
||||
return " ".join(lines[:3])[:300]
|
||||
return f"({path.name},无说明文档)"
|
||||
|
||||
def _build_prompt(self, summaries: dict[str, str]) -> str:
|
||||
proj_names = "、".join(summaries.keys())
|
||||
lines = [f"以下是当前所有项目的功能描述(可用项目名:{proj_names}):\n"]
|
||||
for name, desc in summaries.items():
|
||||
goal_ctx = self._memory.get_context_for_prompt(name, role="architect")
|
||||
lines.append(f"**{name}**: {desc}")
|
||||
if goal_ctx.strip():
|
||||
lines.append(goal_ctx.strip())
|
||||
lines.append(
|
||||
"\n请分析这些项目的能力,识别整合机会,给出产品架构建议。\n"
|
||||
"在报告末尾按以下格式列出任务(每行一条):\n"
|
||||
"- [预研] 任务名称\n"
|
||||
f"- [优化] <项目名>: 优化描述 (项目名须为:{proj_names} 之一)\n"
|
||||
f"- [开发] <项目名>: 具体开发任务描述 (可多条,并行派发给 developer)\n"
|
||||
f"- [测试] <项目名>: 集成测试验证要点 (派发给 tester)\n"
|
||||
f"- [重构] <项目名>: 重构目标和范围 (派发给 dev-refactor)\n"
|
||||
f"- [内核开发] <项目名>: 内核/驱动任务 (派发给 dev-kernel)\n"
|
||||
f"- [目标] <项目名>: 更新后的项目长期目标描述 (可选,仅当目标需要修订时)"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
def _build_optimize_prompt(self, task: Task) -> str:
|
||||
proj_cfg = self._config.projects.get(task.project)
|
||||
proj_desc = proj_cfg.description if proj_cfg else ""
|
||||
return (
|
||||
f"你是 {task.project} 项目的架构优化 Agent。\n"
|
||||
f"项目: {task.project},描述: {proj_desc}\n"
|
||||
f"优化任务: {task.title}\n"
|
||||
f"背景: {task.context}\n\n"
|
||||
f"请对该项目进行架构层面的优化:\n"
|
||||
f"1. 分析现有代码结构和性能瓶颈\n"
|
||||
f"2. 实施具体的架构改进(直接修改代码文件)\n"
|
||||
f"3. 确保改动向后兼容,不破坏现有接口\n"
|
||||
f"4. 输出改动摘要\n"
|
||||
f"请直接修改代码文件,不要只输出报告。"
|
||||
)
|
||||
|
||||
def _build_env(self) -> dict:
|
||||
env = os.environ.copy()
|
||||
if self._config.claude.api_key:
|
||||
env["ANTHROPIC_API_KEY"] = self._config.claude.api_key
|
||||
env.pop("CLAUDECODE", None)
|
||||
env.pop("CLAUDE_CODE_ENTRYPOINT", None)
|
||||
return env
|
||||
|
||||
def _run_tests(self, project_path: Path) -> dict:
|
||||
"""Run pytest in the project directory. Returns {passed, output}."""
|
||||
tests_dir = project_path / "tests"
|
||||
if not tests_dir.exists():
|
||||
return {"passed": True, "output": "(无测试目录,跳过验证)"}
|
||||
|
||||
for pytest_bin in [str(project_path / "venv" / "bin" / "pytest"), "pytest"]:
|
||||
try:
|
||||
r = subprocess.run(
|
||||
[pytest_bin, "tests/", "-q", "--tb=short"],
|
||||
capture_output=True, text=True,
|
||||
timeout=TEST_TIMEOUT, cwd=str(project_path),
|
||||
)
|
||||
return {
|
||||
"passed": r.returncode == 0,
|
||||
"output": (r.stdout + r.stderr)[:1000],
|
||||
}
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
except subprocess.TimeoutExpired:
|
||||
return {"passed": False, "output": f"测试超时({TEST_TIMEOUT}s)"}
|
||||
return {"passed": False, "output": "未找到 pytest 可执行文件"}
|
||||
|
||||
def _extract_and_enqueue(self, report_text: str, requester: str) -> None:
|
||||
"""Parse [预研], [优化], [目标], and [记忆] tags from report."""
|
||||
# [预研] → cross-project research task (report mode)
|
||||
for m in re.finditer(r"\[预研\]\s+(.+)", report_text):
|
||||
title = m.group(1).strip()[:100]
|
||||
self._queue.enqueue(Task(
|
||||
project="research", type="architect", title=title,
|
||||
context=f"由 Architect Agent 从架构分析中提取(来源: {requester})",
|
||||
priority=3, mode="report", agent_role="architect",
|
||||
initiator="architect",
|
||||
discussion=f"架构分析报告中的预研项(来源项目: {requester})",
|
||||
))
|
||||
logger.info("预研任务入队: %s", title)
|
||||
|
||||
# [优化] <project>: <desc> → arch_optimize task
|
||||
for m in re.finditer(r"\[优化\]\s+([\w-]+)\s*[::]\s*(.+)", report_text):
|
||||
project = m.group(1).strip()
|
||||
description = m.group(2).strip()[:100]
|
||||
if project not in self._config.projects:
|
||||
logger.warning("优化任务项目 %r 不在配置中,跳过", project)
|
||||
continue
|
||||
proj_cfg = self._config.projects[project]
|
||||
self._queue.enqueue(Task(
|
||||
project=project,
|
||||
type="arch_optimize",
|
||||
title=f"[架构优化] {description}",
|
||||
context=f"由 Architect Agent 提出(来源: {requester})\n{description}",
|
||||
priority=2,
|
||||
mode=proj_cfg.mode,
|
||||
agent_role="architect",
|
||||
initiator="architect",
|
||||
discussion=f"Architect Agent 在分析 {requester} 时提出的优化建议",
|
||||
))
|
||||
logger.info("架构优化任务入队: [%s] %s", project, description)
|
||||
|
||||
# [开发] <project>: <desc> → developer task (multiple allowed for parallel dispatch)
|
||||
for m in re.finditer(r"\[开发\]\s+([\w-]+)\s*[::]\s*(.+)", report_text):
|
||||
project = m.group(1).strip()
|
||||
description = m.group(2).strip()[:100]
|
||||
if project not in self._config.projects:
|
||||
logger.warning("开发任务项目 %r 不在配置中,跳过", project)
|
||||
continue
|
||||
proj_cfg = self._config.projects[project]
|
||||
self._queue.enqueue(Task(
|
||||
project=project, type="fix_bug",
|
||||
title=f"[开发] {description}",
|
||||
context=f"由 Architect 指派(来源分析: {requester})\n{description}",
|
||||
priority=2, mode=proj_cfg.mode, agent_role="developer",
|
||||
initiator="architect",
|
||||
discussion=f"Architect 分析 {requester} 时指派的开发任务:\n{description}",
|
||||
))
|
||||
logger.info("开发任务入队: [%s] %s", project, description)
|
||||
|
||||
# [测试] <project>: <desc> → tester task
|
||||
for m in re.finditer(r"\[测试\]\s+([\w-]+)\s*[::]\s*(.+)", report_text):
|
||||
project = m.group(1).strip()
|
||||
description = m.group(2).strip()[:100]
|
||||
if project not in self._config.projects:
|
||||
logger.warning("测试任务项目 %r 不在配置中,跳过", project)
|
||||
continue
|
||||
proj_cfg = self._config.projects[project]
|
||||
self._queue.enqueue(Task(
|
||||
project=project, type="test_improvement",
|
||||
title=f"[测试] {description}",
|
||||
context=f"由 Architect 指派(来源分析: {requester})\n{description}",
|
||||
priority=2, mode=proj_cfg.mode, agent_role="tester",
|
||||
initiator="architect",
|
||||
discussion=f"Architect 分析 {requester} 时指派的测试任务:\n{description}",
|
||||
))
|
||||
logger.info("测试任务入队: [%s] %s", project, description)
|
||||
|
||||
# [重构] <project>: <desc> → dev-refactor task
|
||||
for m in re.finditer(r"\[重构\]\s+([\w-]+)\s*[::]\s*(.+)", report_text):
|
||||
project = m.group(1).strip()
|
||||
description = m.group(2).strip()[:100]
|
||||
if project not in self._config.projects:
|
||||
logger.warning("重构任务项目 %r 不在配置中,跳过", project)
|
||||
continue
|
||||
proj_cfg = self._config.projects[project]
|
||||
self._queue.enqueue(Task(
|
||||
project=project, type="refactor",
|
||||
title=f"[重构] {description}",
|
||||
context=f"由 Architect 指派(来源分析: {requester})\n{description}",
|
||||
priority=3, mode=proj_cfg.mode, agent_role="dev-refactor",
|
||||
initiator="architect",
|
||||
discussion=f"Architect 分析 {requester} 时指派的重构任务:\n{description}",
|
||||
))
|
||||
logger.info("重构任务入队: [%s] %s", project, description)
|
||||
|
||||
# [内核开发] <project>: <desc> → dev-kernel task
|
||||
for m in re.finditer(r"\[内核开发\]\s+([\w-]+)\s*[::]\s*(.+)", report_text):
|
||||
project = m.group(1).strip()
|
||||
description = m.group(2).strip()[:100]
|
||||
if project not in self._config.projects:
|
||||
logger.warning("内核开发任务项目 %r 不在配置中,跳过", project)
|
||||
continue
|
||||
proj_cfg = self._config.projects[project]
|
||||
self._queue.enqueue(Task(
|
||||
project=project, type="fix_bug",
|
||||
title=f"[内核开发] {description}",
|
||||
context=f"由 Architect 指派(来源分析: {requester})\n{description}",
|
||||
priority=2, mode=proj_cfg.mode, agent_role="dev-kernel",
|
||||
initiator="architect",
|
||||
discussion=f"Architect 分析 {requester} 时指派的内核开发任务:\n{description}",
|
||||
))
|
||||
logger.info("内核开发任务入队: [%s] %s", project, description)
|
||||
|
||||
# [目标] <project>: <goal> → update project memory (architect can write)
|
||||
for m in re.finditer(r"\[目标\]\s+([\w-]+)\s*[::]\s*(.+)", report_text):
|
||||
project = m.group(1).strip()
|
||||
goal_text = m.group(2).strip()[:300]
|
||||
if project not in self._config.projects:
|
||||
logger.warning("目标更新项目 %r 不在配置中,跳过", project)
|
||||
continue
|
||||
try:
|
||||
self._memory.set_goal(project, goal_text, role="architect")
|
||||
logger.info("项目目标已更新 by architect: [%s]", project)
|
||||
except Exception as e:
|
||||
logger.warning("[%s] 写入项目目标失败: %s", project, e)
|
||||
|
||||
# [记忆] <project>: key=value, key2=value2 → update project facts
|
||||
for m in re.finditer(r"\[记忆\]\s+([\w-]+)\s*[::]\s*(.+)", report_text):
|
||||
project = m.group(1).strip()
|
||||
kv_text = m.group(2).strip()
|
||||
if project not in self._config.projects:
|
||||
logger.warning("记忆更新项目 %r 不在配置中,跳过", project)
|
||||
continue
|
||||
for kv in kv_text.split(","):
|
||||
kv = kv.strip()
|
||||
if "=" in kv:
|
||||
key, _, value = kv.partition("=")
|
||||
key = key.strip()[:50]
|
||||
value = value.strip()[:200]
|
||||
if key and value:
|
||||
try:
|
||||
self._memory.set_fact(project, key, value, "architect")
|
||||
logger.info("项目事实已记录 [%s] %s=%s", project, key, value)
|
||||
except Exception as e:
|
||||
logger.warning("[%s] 写入项目事实失败: %s", project, e)
|
||||
|
||||
# [后续] 自动续链: <desc> → 立即跟进的调研/验证任务(入 project_delivery,低优先级)
|
||||
for m in re.finditer(r"\[后续\][^\[::]*[::]\s*(.+)", report_text):
|
||||
desc = m.group(1).strip()[:120]
|
||||
self._queue.enqueue(Task(
|
||||
project=requester if requester in self._config.projects else "research",
|
||||
type="tech_support",
|
||||
title=f"[后续] {desc}",
|
||||
context=f"由 Architect 在分析中自动续链(来源: {requester})\n{desc}",
|
||||
priority=4,
|
||||
mode="auto",
|
||||
agent_role="architect",
|
||||
initiator="architect",
|
||||
))
|
||||
logger.info("后续任务入队: %s", desc)
|
||||
@@ -0,0 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from nmfs_agents.agents.architect import ArchitectAgent
|
||||
|
||||
BASE_ARCHITECT_SYSTEM = """你是基础技术研发组的架构师,专注平台级技术栈整体设计。
|
||||
|
||||
职责(区别于项目组 architect):
|
||||
- 分析 OS/算法/硬件 三个方向的技术债和瓶颈
|
||||
- 识别跨方向整合机会(如防抖算法与 GPS 惯导融合)
|
||||
- 评估新硬件对现有基础能力的影响
|
||||
- 为 os-engineer / algo-researcher 提出具体 [优化] 任务
|
||||
|
||||
输出格式:
|
||||
- [优化] os-engineer: 具体优化建议
|
||||
- [优化] algo-researcher: 具体优化建议
|
||||
- [记忆] __platform__: key=value(记录架构决策)
|
||||
"""
|
||||
|
||||
|
||||
class BaseArchitectAgent(ArchitectAgent):
|
||||
"""基础技术研发组:OS/算法/硬件跨方向架构分析"""
|
||||
|
||||
def _build_prompt(self, summaries: dict[str, str]) -> str:
|
||||
base = super()._build_prompt(summaries)
|
||||
return BASE_ARCHITECT_SYSTEM + "\n\n" + base
|
||||
@@ -0,0 +1,55 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from nmfs_agents.agents.developer import DeveloperAgent
|
||||
from nmfs_agents.config import AgentsConfig
|
||||
from nmfs_agents.core.queue import Task
|
||||
from nmfs_agents.tools.project_memory import ProjectMemory
|
||||
|
||||
BASE_VALIDATOR_SYSTEM = """你是基础技术研发组的验证工程师,负责算法和OS的基线验证。
|
||||
|
||||
验证维度:
|
||||
1. 精度测试:检出率、误检率、漏检率,与 __hardware__ 基线对比
|
||||
2. 性能测试:FPS、推理延迟(ms)、NPU利用率、CPU/内存占用
|
||||
3. 稳定性测试:长时间运行(≥1h)、内存泄漏检测、异常恢复
|
||||
4. 硬件约束验证:读取 __hardware__ facts,确认算法满足硬件限制
|
||||
|
||||
边端验证方式:
|
||||
- 通过 EdgeValidatorService HTTP API 执行测试(见上方设备信息)
|
||||
- 或通过 SSH 直接执行:sshpass -p '...' ssh pi@<host> "cd <workspace> && pytest ..."
|
||||
|
||||
输出格式:
|
||||
- [验证结果] project=<name> fps=<N> precision=<0.xx> pass=true/false
|
||||
- [验证退化] project=<name> metric=fps current=<N> baseline=<M> drop_pct=<P>
|
||||
- [记忆] __hardware__: <project>.fps_baseline=<N> (首次验证或基线更新时)
|
||||
- [需求] <project>: <具体优化需求> (当 fps drop >10% 或 precision drop >5% 时)
|
||||
|
||||
退化阈值(自动生成 [需求]):
|
||||
- FPS 下降超过 10%
|
||||
- 精度(precision/recall)下降超过 5 个百分点
|
||||
- 内存使用增长超过 20%
|
||||
|
||||
请用中文输出,数值精确,结论明确。
|
||||
"""
|
||||
|
||||
|
||||
class BaseValidatorAgent(DeveloperAgent):
|
||||
"""基础技术组:算法精度 + 性能 + 稳定性 + 硬件约束验证"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: AgentsConfig,
|
||||
memory: Optional[ProjectMemory] = None,
|
||||
) -> None:
|
||||
super().__init__(config, memory=memory)
|
||||
|
||||
def _build_context(self, task: Task) -> str:
|
||||
base = super()._build_context(task)
|
||||
hw_facts = self._memory.get_facts("__hardware__")
|
||||
if hw_facts:
|
||||
hw_lines = "\n".join(f" {k}: {v}" for k, v in hw_facts.items())
|
||||
hw_section = f"\n【硬件约束(来自 hw-engineer)】\n{hw_lines}\n"
|
||||
else:
|
||||
hw_section = "\n【硬件约束】暂无,请查阅 __hardware__ facts\n"
|
||||
return base + hw_section + self._build_device_context() + "\n" + BASE_VALIDATOR_SYSTEM
|
||||
@@ -0,0 +1,132 @@
|
||||
from __future__ import annotations
|
||||
|
||||
"""boss.py — L0 Boss Agent(CEO/战略决策者)。
|
||||
|
||||
职责:
|
||||
- 每周接收各部门长汇报,制定战略方向
|
||||
- 读取四域知识库(tech/market/management/delivery)做决策参考
|
||||
- 不介入日常开发细节,聚焦跨部门协作和长期规划
|
||||
- 接收 [越级汇报] 标签的重要信息(重大技术风险/市场机会)
|
||||
- 输出 [决策] [方向] [协调] 标签驱动后续行动
|
||||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
from nmfs_agents.agents.developer import AgentResult, DeveloperAgent
|
||||
from nmfs_agents.config import AgentsConfig
|
||||
from nmfs_agents.core.queue import Task, TaskQueue
|
||||
from nmfs_agents.tools.knowledge_base import KnowledgeBase
|
||||
from nmfs_agents.tools.project_memory import ProjectMemory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
BOSS_SYSTEM = """你是 NMFS AI Company 的 CEO/Boss(L0 战略决策层)。
|
||||
|
||||
## 职责定位
|
||||
- 接收基础技术部、项目交付部、市场洞察部三个部门长的周报
|
||||
- 制定公司技术战略和市场方向
|
||||
- 协调跨部门资源分配和优先级
|
||||
- 处理 [越级汇报] 重要信息(技术风险/市场机会/跨组织问题)
|
||||
- **不介入**具体代码、测试、日常开发细节
|
||||
|
||||
## 决策维度
|
||||
1. **技术战略**:基础技术积累是否与市场需求对齐?
|
||||
2. **市场机会**:当前研发方向是否抓住了最重要的市场窗口?
|
||||
3. **资源效率**:三个部门的投入产出比是否合理?
|
||||
4. **风险管控**:是否存在技术债务、人员短板、外部威胁?
|
||||
5. **长期护城河**:我们的核心壁垒是什么,如何持续加固?
|
||||
|
||||
## 输出格式(必须包含)
|
||||
- [决策] <具体决策或批准事项>
|
||||
- [方向] <调整优先级或战略方向>
|
||||
- [协调] <需要跨部门协作的事项,说明哪两个部门>
|
||||
- [记忆] kb.tech: 关键技术决策 或 kb.market: 市场判断 或 kb.management: 管理决策
|
||||
- [后续] <下周需要重点跟进的1-3件事>
|
||||
|
||||
## 工作模式
|
||||
1. 读取四域知识库最新动态(上方已注入)
|
||||
2. 综合分析三部门现状
|
||||
3. 识别最重要的2-3个战略问题
|
||||
4. 给出明确决策,不模糊,不推诿
|
||||
5. 记录关键决策到知识库
|
||||
|
||||
## 沟通原则
|
||||
- 与部门长通过任务队列异步沟通
|
||||
- 接受 [越级汇报] 但不鼓励绕过中间层(除非紧急)
|
||||
- 决策有依据,不独断专行
|
||||
"""
|
||||
|
||||
|
||||
class BossAgent(DeveloperAgent):
|
||||
"""L0 战略决策 Boss Agent。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: AgentsConfig,
|
||||
queue: Optional[TaskQueue] = None,
|
||||
memory: Optional[ProjectMemory] = None,
|
||||
knowledge_base: Optional[KnowledgeBase] = None,
|
||||
) -> None:
|
||||
super().__init__(config, memory=memory)
|
||||
self._queue = queue or TaskQueue()
|
||||
self._kb = knowledge_base or KnowledgeBase()
|
||||
|
||||
def run(self, task: Task) -> AgentResult:
|
||||
result = super().run(task)
|
||||
if result.status == "done":
|
||||
self._extract_kb_facts(result.summary)
|
||||
self._dispatch_coordination(result.summary, task)
|
||||
return result
|
||||
|
||||
def _build_context(self, task: Task) -> str:
|
||||
base = super()._build_context(task)
|
||||
# 注入四域知识库摘要
|
||||
kb_sections = []
|
||||
for domain in ("tech", "market", "management", "delivery"):
|
||||
snippet = self._kb.context_for_agent(domain, limit=5)
|
||||
kb_sections.append(snippet)
|
||||
kb_ctx = "\n\n".join(kb_sections)
|
||||
stats = self._kb.summary_by_domain()
|
||||
stats_str = " | ".join(f"{d}: {stats.get(d, 0)}条" for d in ("tech", "market", "management", "delivery"))
|
||||
return (
|
||||
base
|
||||
+ f"\n\n【知识库统计】{stats_str}\n\n"
|
||||
+ kb_ctx
|
||||
+ "\n\n"
|
||||
+ BOSS_SYSTEM
|
||||
)
|
||||
|
||||
def _extract_kb_facts(self, report_text: str) -> None:
|
||||
"""解析 [记忆] kb.{domain}: 内容,写入知识库。"""
|
||||
for m in re.finditer(r"\[记忆\]\s+kb\.(\w+):\s*(.+)", report_text):
|
||||
domain, content = m.group(1), m.group(2).strip()
|
||||
if domain in ("tech", "market", "management", "delivery"):
|
||||
self._kb.store(
|
||||
domain=domain,
|
||||
category="boss-decision",
|
||||
title=f"Boss决策 {__import__('datetime').datetime.now().strftime('%Y-%m-%d')}",
|
||||
content=content,
|
||||
source="boss",
|
||||
)
|
||||
logger.info("Boss 写入知识库 [%s]: %s", domain, content[:80])
|
||||
|
||||
def _dispatch_coordination(self, report_text: str, task: Task) -> None:
|
||||
"""解析 [协调] 标签,创建跨部门协调任务。"""
|
||||
for m in re.finditer(r"\[协调\]\s*(.+)", report_text):
|
||||
desc = m.group(1).strip()[:200]
|
||||
logger.info("Boss 协调任务: %s", desc)
|
||||
# 协调任务写入 project_delivery 队列,交部门长处理
|
||||
self._queue.enqueue(Task(
|
||||
project="company",
|
||||
type="coordination",
|
||||
title=f"[Boss协调] {desc[:60]}",
|
||||
context=f"Boss 战略协调指令:{desc}",
|
||||
priority=2,
|
||||
mode=task.mode,
|
||||
agent_role="tech-dept-leader",
|
||||
parent_task_id=task.id,
|
||||
initiator="boss",
|
||||
discussion=f"来自 Boss 周报决策",
|
||||
))
|
||||
@@ -11,9 +11,9 @@ from typing import Optional
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from rockchip_agents.config import AgentsConfig
|
||||
from rockchip_agents.core.queue import Task
|
||||
from rockchip_agents.tools.project_memory import ProjectMemory
|
||||
from nmfs_agents.config import AgentsConfig
|
||||
from nmfs_agents.core.queue import Task
|
||||
from nmfs_agents.tools.project_memory import ProjectMemory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -185,7 +185,7 @@ class DeveloperAgent:
|
||||
|
||||
def _build_cmd(self, task: Task) -> tuple[list[str], list[str]]:
|
||||
"""返回 (命令列表, 需要清理的临时文件列表)"""
|
||||
from rockchip_agents.config import load_mcp_tools
|
||||
from nmfs_agents.config import load_mcp_tools
|
||||
mcp_cfg = load_mcp_tools()
|
||||
registry: dict[str, str] = mcp_cfg.get("registry", {})
|
||||
role_tools: list[str] = mcp_cfg.get("roles", {}).get(task.agent_role, [])
|
||||
@@ -200,7 +200,7 @@ class DeveloperAgent:
|
||||
tmp_files: list[str] = []
|
||||
|
||||
if task.mode == "report":
|
||||
cmd += ["--allowedTools", "Read,Glob,Grep"]
|
||||
cmd += ["--allowedTools", "Read,Glob,Grep,WebFetch,WebSearch"]
|
||||
else:
|
||||
# auto/confirm 模式:跳过 claude 内部权限对话框
|
||||
cmd += ["--dangerously-skip-permissions"]
|
||||
@@ -258,13 +258,26 @@ class DeveloperAgent:
|
||||
"只有 sequentialthinking 调用完成后才能调用 Write/Edit/Bash。\n\n"
|
||||
)
|
||||
|
||||
spawn_hint = "" if task.mode == "report" else (
|
||||
"【工作流协作】完成代码修改并确认有效变更后,在输出末尾追加:\n"
|
||||
"[SPAWN:tester] 验证: <简述改动要点和测试重点>\n"
|
||||
"(若无实质代码改动,则不输出此标签)\n\n"
|
||||
)
|
||||
|
||||
discussion_section = (
|
||||
f"【任务背景/沟通纪要】\n{task.discussion.strip()}\n\n"
|
||||
if task.discussion and task.discussion.strip() else ""
|
||||
)
|
||||
|
||||
return (
|
||||
f"{plan_first}"
|
||||
f"{spawn_hint}"
|
||||
f"你是 {task.project} 项目的 {task.agent_role} Agent。\n"
|
||||
f"项目路径: {proj_path}。\n"
|
||||
f"项目描述: {proj_desc}。\n"
|
||||
f"当前模式: {task.mode} — {mode_desc}\n"
|
||||
f"{mem_section}"
|
||||
f"{discussion_section}"
|
||||
f"请用中文回复,保持简洁,聚焦问题。"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
from __future__ import annotations
|
||||
|
||||
"""group_leader.py — L2/L3 组长基类。
|
||||
|
||||
职责:
|
||||
- 协调组内成员工作,不直接写业务代码
|
||||
- 解析成员输出的 [越级汇报] 标签并转发
|
||||
- 周期性生成组内状态摘要,向 L1 汇报
|
||||
- 根据进展动态调整成员任务优先级
|
||||
- 组长自身通过 SPAWN 派发任务给具体成员
|
||||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
from nmfs_agents.agents.developer import AgentResult, DeveloperAgent
|
||||
from nmfs_agents.config import AgentsConfig
|
||||
from nmfs_agents.core.queue import Task, TaskQueue
|
||||
from nmfs_agents.tools.project_memory import ProjectMemory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
GROUP_LEADER_SYSTEM = """你是一个技术组的组长(L2 级别)。
|
||||
|
||||
## 职责范围
|
||||
- 协调组内成员(L3)工作,解决跨成员依赖和阻塞
|
||||
- 向上向 L1 部门长汇报组内进展
|
||||
- 识别并上报重大技术风险或机会(用 [越级汇报] 标签)
|
||||
- **不直接写业务代码**,但可以做技术方案评审
|
||||
|
||||
## 组内管理原则
|
||||
- 管理而非控制:给方向和支持,不微管理每行代码
|
||||
- 识别瓶颈:哪个成员被卡住了?原因是什么?
|
||||
- 资源调配:某个任务比其他任务更紧急时,调整优先级
|
||||
- 知识流通:成员 A 的发现对成员 B 有价值,主动传递
|
||||
|
||||
## 输出格式
|
||||
- [状态] 组内整体进展(1-3 句话)
|
||||
- [阻塞] <成员/任务>: <阻塞原因>(如有)
|
||||
- [调度] <成员角色>: <新的优先级任务>(通过 SPAWN 派发)
|
||||
- [向上汇报] <向 L1 部门长的关键信息>
|
||||
- [越级汇报] <重大风险或机会>(直达 Boss,谨慎使用)
|
||||
- [后续] <下轮组内重点>
|
||||
|
||||
## 工作模式
|
||||
1. 读取组内各成员最近记忆和任务结果
|
||||
2. 识别进展、阻塞、风险
|
||||
3. 调整优先级,派发协调任务
|
||||
4. 生成向上汇报摘要
|
||||
"""
|
||||
|
||||
|
||||
class GroupLeaderAgent(DeveloperAgent):
|
||||
"""L2/L3 组长 Agent 基类。子类设置 GROUP_NAME / MEMBER_ROLES / DEPT_LEADER_ROLE。"""
|
||||
|
||||
GROUP_NAME: str = "通用组"
|
||||
MEMBER_ROLES: list[str] = []
|
||||
DEPT_LEADER_ROLE: str = "tech-dept-leader"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: AgentsConfig,
|
||||
queue: Optional[TaskQueue] = None,
|
||||
memory: Optional[ProjectMemory] = None,
|
||||
) -> None:
|
||||
super().__init__(config, memory=memory)
|
||||
self._queue = queue or TaskQueue()
|
||||
|
||||
def run(self, task: Task) -> AgentResult:
|
||||
result = super().run(task)
|
||||
if result.status == "done":
|
||||
self._handle_escalation(result.summary, task)
|
||||
self._dispatch_spawned_tasks(result.summary, task)
|
||||
return result
|
||||
|
||||
def _build_context(self, task: Task) -> str:
|
||||
base = super()._build_context(task)
|
||||
# 注入组内成员最近记忆
|
||||
member_section = self._build_member_context()
|
||||
return base + member_section + "\n\n" + GROUP_LEADER_SYSTEM
|
||||
|
||||
def _build_member_context(self) -> str:
|
||||
"""汇总组内各成员的 ProjectMemory facts,供组长参考。"""
|
||||
lines = [f"\n【{self.GROUP_NAME} 成员状态汇总】"]
|
||||
for role in self.MEMBER_ROLES:
|
||||
facts = self._memory.get_facts(role) or {}
|
||||
if facts:
|
||||
fact_strs = [f"{k}={v}" for k, v in list(facts.items())[:5]]
|
||||
lines.append(f" {role}: {', '.join(fact_strs)}")
|
||||
else:
|
||||
lines.append(f" {role}: (暂无记忆记录)")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
def _handle_escalation(self, report_text: str, task: Task) -> None:
|
||||
"""将 [越级汇报] 内容转发至 Boss 队列。"""
|
||||
for m in re.finditer(r"\[越级汇报\]\s*(.+)", report_text):
|
||||
content = m.group(1).strip()[:300]
|
||||
logger.warning("[%s] 越级汇报触发: %s", self.GROUP_NAME, content[:80])
|
||||
self._queue.enqueue(Task(
|
||||
project="company",
|
||||
type="escalation",
|
||||
title=f"[越级汇报][{self.GROUP_NAME}] {content[:60]}",
|
||||
context=content,
|
||||
priority=1,
|
||||
mode=task.mode,
|
||||
agent_role="boss",
|
||||
parent_task_id=task.id,
|
||||
initiator=task.agent_role,
|
||||
discussion=f"来自 {self.GROUP_NAME} 组长越级汇报",
|
||||
))
|
||||
|
||||
def _dispatch_spawned_tasks(self, report_text: str, task: Task) -> None:
|
||||
"""解析 [调度] 标签,为指定成员入队任务。"""
|
||||
for m in re.finditer(r"\[调度\]\s*([a-zA-Z0-9_-]+):\s*(.+)", report_text):
|
||||
role, desc = m.group(1).strip(), m.group(2).strip()[:200]
|
||||
if role not in self.MEMBER_ROLES:
|
||||
logger.debug("[%s] 调度目标 %s 不在组内,跳过", self.GROUP_NAME, role)
|
||||
continue
|
||||
self._queue.enqueue(Task(
|
||||
project=task.project,
|
||||
type="code_improve",
|
||||
title=f"[{self.GROUP_NAME}组长调度] {desc[:60]}",
|
||||
context=desc,
|
||||
priority=task.priority,
|
||||
mode=task.mode,
|
||||
agent_role=role,
|
||||
parent_task_id=task.id,
|
||||
initiator=task.agent_role,
|
||||
discussion=f"{self.GROUP_NAME} 组长协调",
|
||||
))
|
||||
logger.info("[%s] 组长调度 %s: %s", self.GROUP_NAME, role, desc[:60])
|
||||
|
||||
|
||||
# ── 具体组长实现 ────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class OsBaseLeaderAgent(GroupLeaderAgent):
|
||||
"""OS-Base 组组长(短距通信/内核底层/视频子系统/网络优化)。"""
|
||||
|
||||
GROUP_NAME = "OS-Base"
|
||||
MEMBER_ROLES = ["rtp-researcher", "net-researcher", "kernel-analyzer",
|
||||
"dev-kernel", "os-engineer"]
|
||||
DEPT_LEADER_ROLE = "tech-dept-leader"
|
||||
|
||||
|
||||
class VisionAlgoLeaderAgent(GroupLeaderAgent):
|
||||
"""视觉算法组组长(EIS/光流/RKNN 推理优化)。"""
|
||||
|
||||
GROUP_NAME = "视觉算法"
|
||||
MEMBER_ROLES = ["algo-antishake", "algo-vision"]
|
||||
DEPT_LEADER_ROLE = "tech-dept-leader"
|
||||
@@ -0,0 +1,21 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from nmfs_agents.agents.developer import DeveloperAgent
|
||||
from nmfs_agents.core.queue import Task
|
||||
|
||||
|
||||
class HwEngineerAgent(DeveloperAgent):
|
||||
"""芯片选型、外设接口设计、BOM 评审、功耗分析、Datasheet 查阅专职 Agent"""
|
||||
|
||||
def _build_context(self, task: Task) -> str:
|
||||
base = super()._build_context(task)
|
||||
hw_guidance = (
|
||||
"\n【硬件工程规范】\n"
|
||||
"- 芯片选型:从算力(TOPS)、功耗(TDP)、接口(PCIe/USB/MIPI)、成本、供货稳定性五维评估\n"
|
||||
"- Rockchip 芯片系列:RK3588(8TOPS)、RV1106(0.5TOPS)、RK3562(1TOPS)\n"
|
||||
"- BOM 评审:关注关键器件替代方案,避免单一货源风险\n"
|
||||
"- 功耗分析:区分静态功耗与动态功耗,评估散热需求\n"
|
||||
"- Datasheet 查阅:优先使用 fetch 工具获取最新官方文档\n"
|
||||
"- 接口设计:注意信号完整性、阻抗匹配、ESD 保护\n"
|
||||
)
|
||||
return base + hw_guidance
|
||||
@@ -0,0 +1,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from nmfs_agents.agents.developer import DeveloperAgent
|
||||
from nmfs_agents.core.queue import Task
|
||||
|
||||
|
||||
class KernelDevAgent(DeveloperAgent):
|
||||
"""Linux 内核模块、驱动开发、Kconfig/Device Tree 专职 Agent"""
|
||||
|
||||
def _build_context(self, task: Task) -> str:
|
||||
base = super()._build_context(task)
|
||||
kernel_guidance = (
|
||||
"\n【内核开发规范】\n"
|
||||
"- 修改 DTS/DTSI 前必须了解上下游节点关系\n"
|
||||
"- 内核模块编译:make -C /lib/modules/$(uname -r)/build M=$(pwd) modules\n"
|
||||
"- 驱动调试:dmesg -T | tail -50;insmod/rmmod 前确认依赖\n"
|
||||
"- Kconfig 变更需同步更新 defconfig\n"
|
||||
"- 内存安全:检查 NULL 指针、use-after-free、buffer overflow\n"
|
||||
)
|
||||
return base + self._build_device_context() + kernel_guidance
|
||||
@@ -0,0 +1,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from nmfs_agents.agents.developer import DeveloperAgent
|
||||
from nmfs_agents.core.queue import Task
|
||||
|
||||
|
||||
class LowlevelDevAgent(DeveloperAgent):
|
||||
"""BSP、Bootloader(U-Boot)、裸机/RTOS、硬件 bring-up 专职 Agent"""
|
||||
|
||||
def _build_context(self, task: Task) -> str:
|
||||
base = super()._build_context(task)
|
||||
lowlevel_guidance = (
|
||||
"\n【底层开发规范】\n"
|
||||
"- U-Boot 编译:make CROSS_COMPILE=aarch64-linux-gnu- rockchip_rk3588_defconfig && make\n"
|
||||
"- 裸机调试优先使用 UART 串口输出,不依赖 OS\n"
|
||||
"- 地址空间:严格区分物理地址与虚拟地址,注意 MMU 开关时机\n"
|
||||
"- 硬件 bring-up:先验证电源时序,再验证时钟,最后验证外设\n"
|
||||
"- RTOS 任务优先级设计:中断 > 实时任务 > 普通任务\n"
|
||||
)
|
||||
return base + self._build_device_context() + lowlevel_guidance
|
||||
@@ -0,0 +1,71 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from nmfs_agents.config import AgentsConfig
|
||||
from nmfs_agents.core.executor import Executor
|
||||
from nmfs_agents.core.queue import TaskQueue
|
||||
from nmfs_agents.core.scanner import Scanner
|
||||
from nmfs_agents.tools.callback_server import CallbackServer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ManagerAgent:
|
||||
"""顶层协调器:扫描 → 入队 → 执行 → 汇报。"""
|
||||
|
||||
def __init__(self, config: AgentsConfig,
|
||||
queue: Optional[TaskQueue] = None) -> None:
|
||||
self._config = config
|
||||
self._queue = queue or TaskQueue()
|
||||
self._executor = Executor(config, queue=self._queue)
|
||||
self._callback_server: Optional[CallbackServer] = None
|
||||
|
||||
def start_callback_server(self) -> None:
|
||||
if not self._callback_server:
|
||||
self._callback_server = CallbackServer(
|
||||
port=self._config.feishu.callback_port,
|
||||
queue=self._queue,
|
||||
)
|
||||
self._callback_server.start()
|
||||
|
||||
def stop_callback_server(self) -> None:
|
||||
if self._callback_server:
|
||||
self._callback_server.stop()
|
||||
self._callback_server = None
|
||||
|
||||
def scan_and_enqueue(self) -> int:
|
||||
"""扫描所有项目,将发现的任务入队,返回新增任务数。"""
|
||||
scanner = Scanner(self._config)
|
||||
results = scanner.scan_all()
|
||||
count = 0
|
||||
for r in results:
|
||||
from nmfs_agents.core.queue import Task
|
||||
tid = self._queue.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:
|
||||
count += 1
|
||||
logger.info("本轮扫描新增任务: %d", count)
|
||||
return count
|
||||
|
||||
async def run_cycle(self) -> dict:
|
||||
"""执行一次完整循环,返回统计信息。"""
|
||||
enqueued = self.scan_and_enqueue()
|
||||
executed = await self._executor.run_all_pending()
|
||||
return {"enqueued": enqueued, "executed": executed}
|
||||
|
||||
def get_status(self) -> dict:
|
||||
"""返回任务队列各状态计数。"""
|
||||
conn = self._queue._conn()
|
||||
try:
|
||||
rows = conn.execute(
|
||||
"SELECT status, COUNT(*) as cnt FROM tasks GROUP BY status"
|
||||
).fetchall()
|
||||
finally:
|
||||
conn.close()
|
||||
return {row["status"]: row["cnt"] for row in rows}
|
||||
@@ -0,0 +1,32 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from nmfs_agents.agents.developer import DeveloperAgent
|
||||
from nmfs_agents.core.queue import Task
|
||||
|
||||
MEDIA_PRODUCER_SYSTEM = """你是产品洞察与市场落地组的媒体制作师,随时可被触发,兼顾技术演示和市场展示。
|
||||
|
||||
技术演示侧:
|
||||
- 使用 OpenCV(python3 + cv2)在推理结果图上绘制检测框、置信度、平台水印
|
||||
- 命令示例:python3 -c "import cv2; img=cv2.imread('input.jpg'); ..."
|
||||
- 输出至 assets/demo/
|
||||
|
||||
市场展示侧:
|
||||
- 生成产品效果图:Pillow 合成硬件外观图 + 算法效果标注
|
||||
- Demo 视频剪辑:ffmpeg -i input.mp4 -vf "drawtext=text='RK3588':..." output.mp4
|
||||
- 输出至 assets/market/
|
||||
|
||||
工作流:
|
||||
1. 确认输入素材路径(推理图/视频/产品图)
|
||||
2. 选择合适处理方式(cv2/ffmpeg/Pillow)
|
||||
3. 执行并保存输出文件
|
||||
4. 报告生成的文件路径和简要说明
|
||||
|
||||
注意:随时可触发,不依赖测试结果,同时服务技术和市场两种用途。
|
||||
"""
|
||||
|
||||
|
||||
class MediaProducerAgent(DeveloperAgent):
|
||||
"""产品洞察与市场落地组:技术演示图 + 产品效果图 + Demo 视频"""
|
||||
|
||||
def _build_context(self, task: Task) -> str:
|
||||
return super()._build_context(task) + "\n" + MEDIA_PRODUCER_SYSTEM
|
||||
@@ -0,0 +1,45 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from nmfs_agents.agents.developer import DeveloperAgent
|
||||
from nmfs_agents.core.queue import Task
|
||||
|
||||
OS_ENGINEER_SYSTEM = """你是项目研发与交付组的系统性能剖析工程师,专注识别和消除系统瓶颈。
|
||||
|
||||
职责:不做基础搭建,只做性能问题的深度剖析与优化建议。
|
||||
|
||||
四大剖析领域:
|
||||
|
||||
【内存分析】
|
||||
- 工具:smem -r(进程内存排行)、valgrind --leak-check=full、pmap -x <pid>
|
||||
- 目标:定位内存泄漏、OOM killer 触发链、大 allocation 热点
|
||||
- 输出:内存占用趋势图 + 泄漏函数调用链
|
||||
|
||||
【网络分析】
|
||||
- 工具:iperf3 -s/-c(吞吐测试)、ss -tulpn(连接状态)、tc qdisc show(排队策略)
|
||||
- 目标:带宽瓶颈、延迟尖峰、包重传率
|
||||
- 输出:吞吐/延迟基准数据 + 建议调整的内核参数
|
||||
|
||||
【视觉库分析】
|
||||
- 工具:OpenCV 帧处理计时(cv2.getTickCount)、NEON/SIMD 利用率(perf stat -e simd)
|
||||
- 目标:Mat 内存分配热点、colorspace 转换开销、resize/warp 算子效率
|
||||
- 输出:帧处理时序分解 + 高耗时算子替换建议
|
||||
|
||||
【AI 算子分析】
|
||||
- 工具:RKNN profiler(rknn_server --profile)、rknpu-debug、perf record + flamegraph
|
||||
- 目标:NPU 算子耗时排行、内存搬运开销(DDR ↔ NPU SRAM)、算子融合机会
|
||||
- 输出:算子热点火焰图 + 量化/融合优化建议
|
||||
|
||||
输出规范:
|
||||
- 报告:docs/perf_reports/YYYY-MM-DD-{project}.md(含测量命令 + 原始数据 + 建议)
|
||||
- 基线:[记忆] __hardware__: perf.{category}.bottleneck={desc}
|
||||
- 优化任务:[需求] {project}: 基于剖析结果的具体优化项
|
||||
|
||||
请用中文,数据精确,建议可操作。
|
||||
"""
|
||||
|
||||
|
||||
class OsEngineerAgent(DeveloperAgent):
|
||||
"""基础技术研发组:RTOS + Linux OS 基础层"""
|
||||
|
||||
def _build_context(self, task: Task) -> str:
|
||||
return super()._build_context(task) + self._build_device_context() + "\n" + OS_ENGINEER_SYSTEM
|
||||
@@ -0,0 +1,232 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
from nmfs_agents.agents.developer import (
|
||||
AgentResult, _make_drop_root_preexec,
|
||||
_run_with_log, _task_log_path,
|
||||
)
|
||||
from nmfs_agents.config import AgentsConfig
|
||||
from nmfs_agents.core.queue import Task, TaskQueue
|
||||
from nmfs_agents.tools.project_memory import ProjectMemory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
TIMEOUT = 600 # planner 是 report 模式,只读分析
|
||||
|
||||
_DEFAULT_PLATFORM_GOAL = (
|
||||
"构建 RK3588 NPU 平台上的模块化 AI 推理生态,"
|
||||
"持续提升各推理模块成熟度,探索跨模块整合产品价值。"
|
||||
)
|
||||
|
||||
PLANNER_SYSTEM = """你是 RK3588 AI 生态团队的产品经理。
|
||||
|
||||
你的职责:
|
||||
1. 分析平台目标与当前项目状态的差距
|
||||
2. 识别跨模块整合机会,提出高价值的下一步需求
|
||||
3. 不重复已经在执行或等待中的任务
|
||||
|
||||
输出格式(报告末尾,每行一条):
|
||||
- [需求] <项目名>: 具体需求描述(现有项目优化,直接入队执行)
|
||||
- [集成] 跨模块整合需求描述(直接入队执行)
|
||||
- [目标] <项目名>: 更新后的项目目标(仅当需要修订时)
|
||||
- [平台目标] 更新后的平台整体目标(仅当需要修订时)
|
||||
- [选题] 新方向描述:价值分析(≥30字)
|
||||
|
||||
[选题] 仅用于:
|
||||
✓ 当前项目列表中完全不存在的全新技术领域
|
||||
✓ 需要新建项目的方向
|
||||
✓ 超出现有平台目标范围的探索性方向
|
||||
✗ 现有项目的优化/修复 → 用 [需求]
|
||||
✗ 现有能力的组合整合 → 用 [集成]
|
||||
|
||||
去重原则:若相似需求已在队列中,请勿重复生成。
|
||||
请用中文输出,保持简洁。
|
||||
"""
|
||||
|
||||
|
||||
class PlannerAgent:
|
||||
def __init__(
|
||||
self,
|
||||
config: AgentsConfig,
|
||||
queue: Optional[TaskQueue] = None,
|
||||
memory: Optional[ProjectMemory] = None,
|
||||
) -> None:
|
||||
self._config = config
|
||||
self._queue = queue or TaskQueue()
|
||||
self._memory = memory or ProjectMemory()
|
||||
|
||||
def run(self, task: Task) -> AgentResult:
|
||||
self._ensure_platform_goal()
|
||||
prompt = self._build_prompt()
|
||||
cmd = [
|
||||
"claude", "--print",
|
||||
"--output-format", "json",
|
||||
"--no-session-persistence",
|
||||
"--model", self._config.claude.model,
|
||||
"--allowedTools", "Read,Glob,Grep",
|
||||
"--append-system-prompt", PLANNER_SYSTEM,
|
||||
"--", prompt,
|
||||
]
|
||||
try:
|
||||
env = self._build_env()
|
||||
result = _run_with_log(
|
||||
cmd, None, env, TIMEOUT, _task_log_path(task.id),
|
||||
preexec_fn=_make_drop_root_preexec(env),
|
||||
)
|
||||
if result.status == "done":
|
||||
self._extract_and_enqueue(result.summary)
|
||||
logger.info("PlannerAgent 分析完成,报告 %d 字", len(result.summary))
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("PlannerAgent 运行失败: %s", e)
|
||||
return AgentResult(status="failed", summary=str(e))
|
||||
|
||||
# ── helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
def _ensure_platform_goal(self) -> None:
|
||||
"""首次运行时初始化平台目标。"""
|
||||
if not self._memory.get_goal("__platform__"):
|
||||
try:
|
||||
self._memory.set_goal("__platform__", _DEFAULT_PLATFORM_GOAL, role="planner")
|
||||
logger.info("平台目标已初始化")
|
||||
except Exception as e:
|
||||
logger.warning("平台目标初始化失败: %s", e)
|
||||
|
||||
def _build_prompt(self) -> str:
|
||||
platform_ctx = self._memory.get_context_for_prompt("__platform__", role="planner")
|
||||
proj_names = "、".join(self._config.projects.keys())
|
||||
lines = [
|
||||
f"以下是当前 RK3588 AI 生态团队的状态(可用项目:{proj_names}):\n",
|
||||
platform_ctx.strip(),
|
||||
"",
|
||||
]
|
||||
for name, proj_cfg in self._config.projects.items():
|
||||
proj_ctx = self._memory.get_context_for_prompt(name, role="planner")
|
||||
desc = proj_cfg.description if hasattr(proj_cfg, "description") else ""
|
||||
lines.append(f"**{name}**: {desc}")
|
||||
if proj_ctx.strip():
|
||||
lines.append(proj_ctx.strip())
|
||||
# 附上当前队列摘要(防重复生成)
|
||||
pending_titles = self._get_pending_titles()
|
||||
if pending_titles:
|
||||
lines.append(f"\n当前待处理任务({len(pending_titles)} 条,避免重复):")
|
||||
for t in pending_titles[:15]:
|
||||
lines.append(f" - {t}")
|
||||
lines.append(
|
||||
"\n请分析平台目标与当前项目状态的差距,提出 3~5 条最高价值的需求或整合机会。"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
def _get_pending_titles(self) -> list[str]:
|
||||
try:
|
||||
conn = self._queue._conn()
|
||||
rows = conn.execute(
|
||||
"SELECT title FROM tasks WHERE status IN ('pending','running') ORDER BY id DESC LIMIT 30"
|
||||
).fetchall()
|
||||
conn.close()
|
||||
return [r["title"] for r in rows]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def _is_duplicate(self, title: str) -> bool:
|
||||
"""模糊去重:与任何 pending/running 任务 title 共同前缀超 40 字或完整标题相互包含则视为重复。"""
|
||||
key = title[:40]
|
||||
for existing in self._get_pending_titles():
|
||||
# 两个标题取各自前 40 字,若较短一方与另一方的前缀匹配则视为重复
|
||||
existing_key = existing[:40]
|
||||
min_len = min(len(key), len(existing_key))
|
||||
if min_len > 0 and key[:min_len] == existing_key[:min_len]:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _extract_and_enqueue(self, report_text: str) -> None:
|
||||
# [需求] <project>: <desc>
|
||||
for m in re.finditer(r"\[需求\]\s+([\w-]+)\s*[::]\s*(.+)", report_text):
|
||||
project = m.group(1).strip()
|
||||
desc = m.group(2).strip()[:120]
|
||||
if project not in self._config.projects:
|
||||
logger.warning("需求任务项目 %r 不在配置中,跳过", project)
|
||||
continue
|
||||
proj_cfg = self._config.projects[project]
|
||||
title = f"[需求] {desc}"
|
||||
if self._is_duplicate(title):
|
||||
logger.info("需求重复跳过: %s", title)
|
||||
continue
|
||||
self._queue.enqueue(Task(
|
||||
project=project, type="feature", title=title,
|
||||
context=f"由 PlannerAgent 从差距分析中提取\n{desc}",
|
||||
priority=3, mode=proj_cfg.mode,
|
||||
agent_role="developer", initiator="planner",
|
||||
discussion="PlannerAgent 基于平台目标-项目现状差距分析生成",
|
||||
))
|
||||
logger.info("需求任务入队: [%s] %s", project, title)
|
||||
|
||||
# [集成] <desc> (跨项目,无 project 前缀)
|
||||
for m in re.finditer(r"\[集成\]\s+(.+)", report_text):
|
||||
desc = m.group(1).strip()[:120]
|
||||
title = f"[集成] {desc}"
|
||||
if self._is_duplicate(title):
|
||||
logger.info("集成需求重复跳过: %s", title)
|
||||
continue
|
||||
self._queue.enqueue(Task(
|
||||
project="research", type="architect", title=title,
|
||||
context=f"由 PlannerAgent 提出的跨模块整合机会\n{desc}",
|
||||
priority=3, mode="report",
|
||||
agent_role="architect", initiator="planner",
|
||||
discussion="PlannerAgent 跨项目整合分析",
|
||||
))
|
||||
logger.info("集成任务入队: %s", title)
|
||||
|
||||
# [目标] <project>: <goal>
|
||||
for m in re.finditer(r"\[目标\]\s+([\w-]+)\s*[::]\s*(.+)", report_text):
|
||||
project = m.group(1).strip()
|
||||
goal_text = m.group(2).strip()[:300]
|
||||
if project not in self._config.projects:
|
||||
logger.warning("目标更新项目 %r 不在配置中,跳过", project)
|
||||
continue
|
||||
try:
|
||||
self._memory.set_goal(project, goal_text, role="planner")
|
||||
logger.info("项目目标已更新 by planner: [%s]", project)
|
||||
except Exception as e:
|
||||
logger.warning("[%s] 写入项目目标失败: %s", project, e)
|
||||
|
||||
# [平台目标] <goal>
|
||||
for m in re.finditer(r"\[平台目标\]\s+(.+)", report_text):
|
||||
goal_text = m.group(1).strip()[:300]
|
||||
try:
|
||||
self._memory.set_goal("__platform__", goal_text, role="planner")
|
||||
logger.info("平台目标已更新 by planner")
|
||||
except Exception as e:
|
||||
logger.warning("写入平台目标失败: %s", e)
|
||||
|
||||
# [选题] <desc>:新方向/新课题,写入 topic_pending 待人工审批
|
||||
for m in re.finditer(r"\[选题\]\s+(.+?)(?:[::]\s*(.+))?$", report_text, re.MULTILINE):
|
||||
desc = m.group(1).strip()[:120]
|
||||
context = (m.group(2) or "").strip()[:300]
|
||||
title = f"[选题] {desc}"
|
||||
# 检查是否已有同标题的 topic_pending(去重)
|
||||
existing = self._queue.get_topic_pending_tasks()
|
||||
if any(t.title == title for t in existing):
|
||||
logger.info("选题重复跳过: %s", title)
|
||||
continue
|
||||
tid = self._queue.enqueue_topic(Task(
|
||||
project="research", type="topic", title=title,
|
||||
context=context or f"由 PlannerAgent 提出的新课题方向\n{desc}",
|
||||
priority=3, mode="report",
|
||||
agent_role="planner", initiator="planner",
|
||||
discussion="PlannerAgent 识别为新方向/新课题,需人工确认后执行",
|
||||
))
|
||||
if tid > 0:
|
||||
logger.info("选题入队待审批 #%d: %s", tid, title)
|
||||
|
||||
def _build_env(self) -> dict:
|
||||
env = os.environ.copy()
|
||||
if self._config.claude.api_key:
|
||||
env["ANTHROPIC_API_KEY"] = self._config.claude.api_key
|
||||
env.pop("CLAUDECODE", None)
|
||||
env.pop("CLAUDE_CODE_ENTRYPOINT", None)
|
||||
return env
|
||||
@@ -0,0 +1,143 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from nmfs_agents.agents.developer import AgentResult
|
||||
from nmfs_agents.config import AgentsConfig
|
||||
from nmfs_agents.core.queue import Task
|
||||
from nmfs_agents.tools.project_memory import ProjectMemory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class MaturityItem:
|
||||
name: str
|
||||
score: int # 实际得分
|
||||
max_score: int # 满分
|
||||
present: bool
|
||||
note: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class MaturityReport:
|
||||
project: str
|
||||
score: int # 总分 0-100
|
||||
items: list[MaturityItem] = field(default_factory=list)
|
||||
suggestions: list[str] = field(default_factory=list)
|
||||
|
||||
def summary_text(self) -> str:
|
||||
lines = [f"# {self.project} 成熟度报告", f"总分: {self.score}/100", ""]
|
||||
lines.append("## 各项评分")
|
||||
for item in self.items:
|
||||
icon = "✅" if item.present else "❌"
|
||||
lines.append(f"{icon} {item.name}: {item.score}/{item.max_score}"
|
||||
+ (f" — {item.note}" if item.note else ""))
|
||||
if self.suggestions:
|
||||
lines.append("\n## 改进建议")
|
||||
for s in self.suggestions:
|
||||
lines.append(f"- {s}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
class ProductizerAgent:
|
||||
"""基于规则评估项目产品化成熟度,生成改进建议。"""
|
||||
|
||||
CHECKS: list[tuple[str, int]] = [
|
||||
("README.md", 20), # 项目说明文档
|
||||
("RELEASE.md", 15), # 发布记录
|
||||
("pyproject.toml", 10), # 打包配置
|
||||
("tests/", 20), # 测试目录
|
||||
("DEVELOP.md", 10), # 开发指南
|
||||
("src/", 10), # 标准源码结构
|
||||
("configs/", 5), # 配置文件
|
||||
("scripts/", 5), # 工具脚本
|
||||
("Makefile", 5), # 构建入口
|
||||
]
|
||||
|
||||
def __init__(self, config: AgentsConfig,
|
||||
memory: Optional[ProjectMemory] = None) -> None:
|
||||
self._config = config
|
||||
self._memory = memory or ProjectMemory()
|
||||
self.last_report: Optional[MaturityReport] = None
|
||||
|
||||
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}")
|
||||
|
||||
report = self._evaluate(task.project, proj_cfg.path)
|
||||
self.last_report = report
|
||||
summary = report.summary_text()
|
||||
logger.info("[%s] 成熟度评分: %d/100", task.project, report.score)
|
||||
|
||||
# Write initial project goal if not already set (productizer can always update)
|
||||
self._refresh_goal(task.project, proj_cfg.path, report)
|
||||
|
||||
return AgentResult(status="done", summary=summary)
|
||||
|
||||
def _evaluate(self, project: str, path: Path) -> MaturityReport:
|
||||
items: list[MaturityItem] = []
|
||||
total = 0
|
||||
|
||||
for name, max_score in self.CHECKS:
|
||||
target = path / name
|
||||
present = target.exists()
|
||||
score = max_score if present else 0
|
||||
|
||||
# 额外细粒度检查
|
||||
note = ""
|
||||
if present and name == "README.md":
|
||||
text = target.read_text(errors="ignore")
|
||||
has_quickstart = any(kw in text for kw in ["快速开始", "Quick Start", "quickstart", "## 安装", "## Install"])
|
||||
if not has_quickstart:
|
||||
score = max_score // 2
|
||||
note = "缺少快速开始章节"
|
||||
if present and name == "tests/":
|
||||
test_files = list(target.rglob("test_*.py")) + list(target.rglob("*_test.py"))
|
||||
if not test_files:
|
||||
score = max_score // 2
|
||||
note = "tests/ 目录为空"
|
||||
else:
|
||||
note = f"{len(test_files)} 个测试文件"
|
||||
|
||||
items.append(MaturityItem(name=name, score=score,
|
||||
max_score=max_score, present=present, note=note))
|
||||
total += score
|
||||
|
||||
suggestions = self._generate_suggestions(items, path)
|
||||
return MaturityReport(project=project, score=total,
|
||||
items=items, suggestions=suggestions)
|
||||
|
||||
def _generate_suggestions(self, items: list[MaturityItem], path: Path) -> list[str]:
|
||||
suggestions = []
|
||||
for item in items:
|
||||
if not item.present:
|
||||
suggestions.append(f"添加 {item.name}(可提升 {item.max_score} 分)")
|
||||
elif item.score < item.max_score:
|
||||
suggestions.append(f"完善 {item.name}:{item.note}")
|
||||
return suggestions
|
||||
|
||||
def _refresh_goal(self, project: str, path: Path,
|
||||
report: MaturityReport) -> None:
|
||||
"""Write a structured project goal to memory.
|
||||
|
||||
The goal captures the project's current purpose and maturity status so
|
||||
that future agents always work with an up-to-date objective.
|
||||
"""
|
||||
desc = self._config.projects[project].description
|
||||
score_line = f"当前成熟度评分 {report.score}/100"
|
||||
top_suggestions = ";".join(report.suggestions[:3]) if report.suggestions else "暂无改进建议"
|
||||
|
||||
if desc:
|
||||
goal_text = f"{desc}。{score_line}。近期改进方向:{top_suggestions}。"
|
||||
else:
|
||||
goal_text = f"{project} 项目。{score_line}。近期改进方向:{top_suggestions}。"
|
||||
|
||||
try:
|
||||
self._memory.set_goal(project, goal_text, role="productizer")
|
||||
except Exception as e:
|
||||
logger.warning("[%s] 写入项目目标失败: %s", project, e)
|
||||
@@ -0,0 +1,68 @@
|
||||
from __future__ import annotations
|
||||
|
||||
"""senior_dev.py — 高级研发 Agent(L3 Senior Developer)。
|
||||
|
||||
职责:
|
||||
- 平衡「高质量代码实现」与「架构视角」
|
||||
- 可主动提出架构改进建议([架构建议] 标签)
|
||||
- 技术决策有依据,关注先进性(通过 web search 跟踪技术前沿)
|
||||
- 普通任务以代码质量为主;发现架构问题时触发讨论
|
||||
- 可接受 senior-dev 任务,也可接受 developer 任务
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from nmfs_agents.agents.developer import DeveloperAgent
|
||||
from nmfs_agents.config import AgentsConfig
|
||||
from nmfs_agents.core.queue import Task, TaskQueue
|
||||
from nmfs_agents.tools.project_memory import ProjectMemory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SENIOR_DEV_SYSTEM = """你是一名高级研发工程师(Senior Developer,L3)。
|
||||
|
||||
## 与普通研发的区别
|
||||
- **架构视角**:不只写功能代码,还思考设计是否合理、可扩展
|
||||
- **技术先进性**:关注业界最新方案,通过 web search 验证是否有更好的实现
|
||||
- **代码质量**:类型注解、测试覆盖、可读性,高于普通标准
|
||||
- **主动发现**:遇到技术债务或架构隐患,主动提出而不忽视
|
||||
|
||||
## 工作模式
|
||||
1. **理解需求**:明确本次任务边界和验收标准
|
||||
2. **技术调研**(如需要):用 web search 看业界最优实践
|
||||
3. **实现**:写高质量代码,完整类型注解
|
||||
4. **自测**:写/完善 pytest 测试,覆盖边界情况
|
||||
5. **架构反思**:此次修改是否引入了新的技术债务?是否有更好的设计?
|
||||
|
||||
## 输出标签
|
||||
- [架构建议] <发现的架构问题或改进机会>(会触发讨论任务)
|
||||
- [技术讨论] <值得团队讨论的技术方案>
|
||||
- [越级汇报] <重大技术风险>(谨慎使用)
|
||||
- [记忆] <项目名>: key=value(记录关键技术事实)
|
||||
- [后续] <下一步建议>
|
||||
|
||||
## 编码规范(严格执行)
|
||||
- `from __future__ import annotations`
|
||||
- 完整类型注解;路径用 `pathlib.Path`
|
||||
- 日志用 `logging`,禁止 `print`
|
||||
- 新功能必须有对应 pytest 测试
|
||||
- 不引入未经评估的外部依赖
|
||||
"""
|
||||
|
||||
|
||||
class SeniorDevAgent(DeveloperAgent):
|
||||
"""高级研发 Agent:平衡代码实现与架构视角。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: AgentsConfig,
|
||||
queue: Optional[TaskQueue] = None,
|
||||
memory: Optional[ProjectMemory] = None,
|
||||
) -> None:
|
||||
super().__init__(config, memory=memory)
|
||||
self._queue = queue or TaskQueue()
|
||||
|
||||
def _build_context(self, task: Task) -> str:
|
||||
base = super()._build_context(task)
|
||||
return base + "\n\n" + SENIOR_DEV_SYSTEM
|
||||
@@ -0,0 +1,75 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
from nmfs_agents.agents.developer import AgentResult, DeveloperAgent
|
||||
from nmfs_agents.config import AgentsConfig
|
||||
from nmfs_agents.core.queue import Task, TaskQueue
|
||||
from nmfs_agents.tools.project_memory import ProjectMemory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SYSTEM_TESTER_SYSTEM = """你是项目研发与交付组的系统测试工程师。
|
||||
|
||||
测试五层能力:
|
||||
1. 功能测试:运行 pytest,SSH 板端自动执行,检查通过/失败
|
||||
2. 视觉测试:使用 Playwright 截图推理界面,分析检测框/FPS/误检,截图保存至 docs/test_reports/
|
||||
3. 性能测试:测量 FPS、推理延迟、内存占用,与目标值对比,写入 [记忆]
|
||||
4. 跨平台测试:RK3588/全志/ESP32 分平台调度执行,记录差异
|
||||
5. 测试报告:生成结构化 Markdown 报告至 docs/test_reports/YYYY-MM-DD-{project}.md
|
||||
|
||||
工具使用:
|
||||
- Playwright:browser_navigate → 推理演示页 → browser_take_screenshot → 分析截图
|
||||
- SSH 板端:通过 Bash 执行 ssh user@host "pytest tests/ -q"
|
||||
- 性能测量:time/perf_counter 或板端 top/free 命令
|
||||
|
||||
失败处理:
|
||||
- 测试失败 → 输出 [需求] <项目>: 具体问题描述,触发 developer 修复
|
||||
- 性能退化 → [记忆] <项目>: fps_baseline=N,记录当前基线
|
||||
|
||||
请用中文,数值精确,测试结论明确。
|
||||
"""
|
||||
|
||||
|
||||
class SystemTesterAgent(DeveloperAgent):
|
||||
"""项目交付组:多层系统测试(功能+视觉+性能+跨平台+报告)"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: AgentsConfig,
|
||||
queue: Optional[TaskQueue] = None,
|
||||
memory: Optional[ProjectMemory] = None,
|
||||
) -> None:
|
||||
super().__init__(config, memory=memory)
|
||||
self._queue = queue or TaskQueue()
|
||||
|
||||
def run(self, task: Task) -> AgentResult:
|
||||
result = super().run(task)
|
||||
if result.status == "done":
|
||||
self._dispatch_require_tasks(result.summary, task)
|
||||
return result
|
||||
|
||||
def _build_context(self, task: Task) -> str:
|
||||
base = super()._build_context(task)
|
||||
return base + self._build_device_context() + "\n" + SYSTEM_TESTER_SYSTEM
|
||||
|
||||
def _dispatch_require_tasks(self, report_text: str, task: Task) -> None:
|
||||
"""解析 [需求] 标签,将测试发现的问题派发给 developer 修复。"""
|
||||
for m in re.finditer(r"\[需求\]\s+([\w-]+)\s*[::]\s*(.+)", report_text):
|
||||
project = m.group(1).strip()
|
||||
desc = m.group(2).strip()[:120]
|
||||
child = Task(
|
||||
project=project,
|
||||
type="fix_bug",
|
||||
title=f"[修复] {desc}",
|
||||
agent_role="developer",
|
||||
priority=task.priority,
|
||||
mode=task.mode,
|
||||
parent_task_id=task.id,
|
||||
initiator="system-tester",
|
||||
discussion=f"系统测试发现问题(任务#{task.id} {task.title}):\n{desc}",
|
||||
)
|
||||
self._queue.enqueue(child)
|
||||
logger.info("系统测试派发修复任务: project=%s, title=%s", project, child.title)
|
||||
@@ -0,0 +1,51 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from nmfs_agents.agents.developer import AgentResult
|
||||
from nmfs_agents.config import AgentsConfig
|
||||
from nmfs_agents.core.queue import Task
|
||||
from nmfs_agents.tools.device_agent import DeviceAgent
|
||||
from nmfs_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(
|
||||
"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,
|
||||
)
|
||||
@@ -0,0 +1,141 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import tempfile
|
||||
from typing import Optional
|
||||
|
||||
from nmfs_agents.agents.developer import (
|
||||
AgentResult, _make_drop_root_preexec, _run_with_log, _task_log_path,
|
||||
)
|
||||
from nmfs_agents.config import AgentsConfig
|
||||
from nmfs_agents.core.queue import Task, TaskQueue
|
||||
from nmfs_agents.tools.project_memory import ProjectMemory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
TIMEOUT = 600
|
||||
|
||||
VISION_ANALYST_SYSTEM = """你是产品洞察与市场落地组的视觉分析师,基于 Claude Vision 进行多帧视觉质量分析。
|
||||
|
||||
工作流程(5帧分析):
|
||||
1. 调用 EdgeValidatorService 截图(优先)或 Playwright 截图
|
||||
- EdgeValidatorService: fetch POST http://<host>:8899/screenshot(见上方设备信息)
|
||||
- Playwright 备用: browser_navigate → browser_take_screenshot
|
||||
2. 连续截取 5 帧,间隔 2 秒
|
||||
3. 逐帧作为多模态 LLM 分析(你直接看截图):
|
||||
- 检测框位置合理性(有无明显偏移/错位)
|
||||
- FPS 数值读取(从 OSD/界面文字)
|
||||
- 漏检:应检测目标未出现检测框
|
||||
- 误检:背景被误识别
|
||||
4. 跨帧分析:检测稳定性(同一目标的框是否抖动/消失)
|
||||
5. 存档:docs/visual_tests/YYYY-MM-DD-{project}-{seq:02d}.png
|
||||
|
||||
输出格式:
|
||||
- [视觉结果] fps=<N> stability=HIGH/MED/LOW detected=<N> false_pos=<N> pass=true/false
|
||||
- [视觉退化] fps_trend=升/降 bbox_jitter=高/中/低
|
||||
- [需求] <项目名>: 具体视觉问题描述和改进方向
|
||||
|
||||
结合推理日志定量验证:
|
||||
- fetch GET http://<host>:8899/logs/yolo/20
|
||||
|
||||
请用中文,视觉描述精确,问题定位清晰。
|
||||
"""
|
||||
|
||||
|
||||
class VisionAnalystAgent:
|
||||
def __init__(
|
||||
self,
|
||||
config: AgentsConfig,
|
||||
queue: Optional[TaskQueue] = None,
|
||||
memory: Optional[ProjectMemory] = None,
|
||||
) -> None:
|
||||
self._config = config
|
||||
self._queue = queue or TaskQueue()
|
||||
self._memory = memory or ProjectMemory()
|
||||
|
||||
def run(self, task: Task) -> AgentResult:
|
||||
from nmfs_agents.config import load_mcp_tools
|
||||
mcp_cfg = load_mcp_tools()
|
||||
registry = mcp_cfg.get("registry", {})
|
||||
|
||||
mcp_servers: dict = {}
|
||||
for tool in ["playwright", "sequential-thinking"]:
|
||||
if tool in registry:
|
||||
parts = registry[tool].split()
|
||||
if shutil.which(parts[0]):
|
||||
mcp_servers[tool] = {"command": parts[0], "args": parts[1:]}
|
||||
|
||||
tmp_files: list[str] = []
|
||||
cmd = [
|
||||
"claude", "--print",
|
||||
"--output-format", "json",
|
||||
"--no-session-persistence",
|
||||
"--model", self._config.claude.model,
|
||||
"--dangerously-skip-permissions",
|
||||
"--append-system-prompt", VISION_ANALYST_SYSTEM,
|
||||
]
|
||||
if mcp_servers:
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".json", delete=False, prefix="mcp_vision_"
|
||||
) as f:
|
||||
json.dump({"mcpServers": mcp_servers}, f)
|
||||
tmp_files.append(f.name)
|
||||
cmd += ["--mcp-config", tmp_files[-1]]
|
||||
cmd += ["--", self._build_prompt(task)]
|
||||
|
||||
try:
|
||||
env = self._build_env()
|
||||
result = _run_with_log(
|
||||
cmd, None, env, TIMEOUT, _task_log_path(task.id),
|
||||
preexec_fn=_make_drop_root_preexec(env),
|
||||
)
|
||||
if result.status == "done":
|
||||
self._extract_needs(result.summary)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("VisionAnalystAgent 失败: %s", e)
|
||||
return AgentResult(status="failed", summary=str(e))
|
||||
finally:
|
||||
for f in tmp_files:
|
||||
try:
|
||||
os.unlink(f)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def _build_prompt(self, task: Task) -> str:
|
||||
proj_cfg = self._config.projects.get(task.project)
|
||||
desc = proj_cfg.description if proj_cfg else task.project
|
||||
return (
|
||||
f"项目: {task.project},描述: {desc}\n"
|
||||
f"任务: {task.title}\n"
|
||||
f"背景: {task.context}\n\n"
|
||||
f"请使用 Playwright 截图并分析推理效果,输出视觉分析报告。"
|
||||
)
|
||||
|
||||
def _extract_needs(self, report_text: str) -> None:
|
||||
for m in re.finditer(r"\[需求\]\s+([\w-]+)\s*[::]\s*(.+)", report_text):
|
||||
project = m.group(1).strip()
|
||||
desc = m.group(2).strip()[:120]
|
||||
if project not in self._config.projects:
|
||||
logger.warning("视觉分析需求项目 %r 不在配置中,跳过", project)
|
||||
continue
|
||||
proj_cfg = self._config.projects[project]
|
||||
self._queue.enqueue(Task(
|
||||
project=project, type="fix_bug",
|
||||
title=f"[视觉分析] {desc}",
|
||||
context=f"由 VisionAnalystAgent 发现\n{desc}",
|
||||
priority=2, mode=proj_cfg.mode,
|
||||
agent_role="developer", initiator="vision-analyst",
|
||||
discussion="VisionAnalystAgent 视觉分析发现的问题",
|
||||
))
|
||||
|
||||
def _build_env(self) -> dict:
|
||||
env = os.environ.copy()
|
||||
if self._config.claude.api_key:
|
||||
env["ANTHROPIC_API_KEY"] = self._config.claude.api_key
|
||||
env.pop("CLAUDECODE", None)
|
||||
env.pop("CLAUDE_CODE_ENTRYPOINT", None)
|
||||
return env
|
||||
@@ -0,0 +1,213 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import yaml
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_DIR = Path(__file__).parent.parent.parent / "configs"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProjectConfig:
|
||||
path: Path
|
||||
mode: str = "report" # auto | confirm | report
|
||||
kind: str = "host" # embedded | host
|
||||
device_workspace: Optional[str] = None
|
||||
description: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class QueueConfig:
|
||||
db: str # 相对于项目根目录的 DB 路径
|
||||
max_concurrent: int = 3
|
||||
scan_interval_hours: int = 1
|
||||
roles: frozenset[str] = field(default_factory=frozenset)
|
||||
|
||||
|
||||
@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 = ""
|
||||
type: str = "linux" # linux | esp32 | nordic
|
||||
connect: str = "ssh" # ssh | serial | gateway
|
||||
via: str = "" # gateway 时的中转设备名(对应 devices 中的 key)
|
||||
port: str = "" # 串口路径,如 /dev/ttyUSB0
|
||||
baud: int = 115200
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentsConfig:
|
||||
projects: dict[str, ProjectConfig]
|
||||
scheduler: SchedulerConfig
|
||||
claude: ClaudeConfig
|
||||
feishu: FeishuConfig
|
||||
devices: dict[str, DeviceConfig]
|
||||
claude_wx_url: str = "" # 可选:claude-wx 服务端 URL,用于 agent dashboard 通知
|
||||
queues: dict[str, "QueueConfig"] = field(default_factory=dict)
|
||||
|
||||
|
||||
def load_config(config_dir: Path | None = None) -> AgentsConfig:
|
||||
"""Load configuration, merging auto-discovered projects with explicit overrides.
|
||||
|
||||
projects.yaml may contain:
|
||||
- ``scan_dirs``: list of paths to scan for Claude-managed projects
|
||||
- ``projects``: explicit per-project config (overrides auto-discovered values;
|
||||
``path`` is optional if the project is found via scan_dirs)
|
||||
"""
|
||||
d = config_dir or CONFIG_DIR
|
||||
with open(d / "projects.yaml") as f:
|
||||
proj_data = yaml.safe_load(f) or {}
|
||||
with open(d / "agents.yaml") as f:
|
||||
agent_data = yaml.safe_load(f) or {}
|
||||
with open(d / "devices.yaml") as f:
|
||||
dev_data = yaml.safe_load(f) or {}
|
||||
|
||||
# Parse devices first (needed for device_workspace inference)
|
||||
devices: dict[str, DeviceConfig] = {
|
||||
name: DeviceConfig(
|
||||
host=v.get("host", ""),
|
||||
user=v.get("user", ""),
|
||||
password=str(v.get("password", "")),
|
||||
workspace=v.get("workspace", ""),
|
||||
pre_cmd=v.get("pre_cmd", ""),
|
||||
type=v.get("type", "linux"),
|
||||
connect=v.get("connect", "ssh"),
|
||||
via=v.get("via", ""),
|
||||
port=v.get("port", ""),
|
||||
baud=int(v.get("baud", 115200)),
|
||||
)
|
||||
for name, v in (dev_data.get("devices") or {}).items()
|
||||
}
|
||||
|
||||
# ── step 1: auto-discover from scan_dirs ──────────────────────────────────
|
||||
discovered: dict[str, ProjectConfig] = {}
|
||||
scan_dirs = proj_data.get("scan_dirs") or []
|
||||
if scan_dirs:
|
||||
from nmfs_agents.tools.project_discovery import ProjectDiscovery
|
||||
discovery = ProjectDiscovery(devices=devices)
|
||||
for raw_path in scan_dirs:
|
||||
target = Path(raw_path)
|
||||
for disc in discovery.scan(target):
|
||||
discovered[disc.name] = ProjectConfig(
|
||||
path=disc.path,
|
||||
mode=disc.mode,
|
||||
kind=disc.kind,
|
||||
device_workspace=disc.device_workspace,
|
||||
description=disc.description,
|
||||
)
|
||||
logger.debug("自动发现项目: %s (%s) at %s",
|
||||
disc.name, disc.kind, disc.path)
|
||||
|
||||
# ── step 1b: remove excluded projects ────────────────────────────────────
|
||||
exclude_set = set(proj_data.get("exclude_projects") or [])
|
||||
for name in list(discovered.keys()):
|
||||
if name in exclude_set:
|
||||
del discovered[name]
|
||||
logger.debug("已排除项目: %s", name)
|
||||
|
||||
# ── step 2: apply explicit overrides ─────────────────────────────────────
|
||||
explicit: dict[str, ProjectConfig] = {}
|
||||
for name, v in (proj_data.get("projects") or {}).items():
|
||||
base = discovered.get(name)
|
||||
# path is optional when project was already discovered
|
||||
raw_path = v.get("path")
|
||||
if raw_path:
|
||||
path = Path(raw_path)
|
||||
elif base:
|
||||
path = base.path
|
||||
else:
|
||||
logger.warning("项目 %r 无法确定路径(未在 scan_dirs 中发现,且未指定 path),跳过", name)
|
||||
continue
|
||||
explicit[name] = ProjectConfig(
|
||||
path=path,
|
||||
mode=v.get("mode", base.mode if base else "report"),
|
||||
kind=v.get("kind", base.kind if base else "host"),
|
||||
device_workspace=v.get("device_workspace",
|
||||
base.device_workspace if base else None),
|
||||
description=v.get("description", base.description if base else ""),
|
||||
)
|
||||
|
||||
# 只启用显式配置的项目;scan_dirs 仅用于路径解析,不自动引入未列出的项目
|
||||
projects = explicit
|
||||
|
||||
sched = agent_data.get("scheduler", {})
|
||||
claude_d = agent_data.get("claude", {})
|
||||
feishu_d = agent_data.get("feishu", {})
|
||||
|
||||
# 解析多队列配置
|
||||
queues: dict[str, QueueConfig] = {}
|
||||
for q_name, q_data in agent_data.get("queues", {}).items():
|
||||
queues[q_name] = QueueConfig(
|
||||
db=q_data.get("db", f"data/{q_name}_tasks.db"),
|
||||
max_concurrent=q_data.get("max_concurrent", 3),
|
||||
scan_interval_hours=q_data.get("scan_interval_hours", 1),
|
||||
roles=frozenset(q_data.get("roles", [])),
|
||||
)
|
||||
|
||||
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,
|
||||
claude_wx_url=agent_data.get("claude_wx_url", ""),
|
||||
queues=queues,
|
||||
)
|
||||
|
||||
|
||||
def load_mcp_tools(config_dir: Path | None = None) -> dict:
|
||||
"""加载 MCP 工具注册表和角色工具映射(支持热更新)"""
|
||||
d = config_dir or CONFIG_DIR
|
||||
path = d / "mcp_tools.yaml"
|
||||
if not path.exists():
|
||||
return {"registry": {}, "roles": {}}
|
||||
with open(path) as f:
|
||||
return yaml.safe_load(f) or {"registry": {}, "roles": {}}
|
||||
|
||||
|
||||
def load_role_routing(config_dir: Path | None = None) -> dict[str, list[str]]:
|
||||
"""加载角色关键词路由表(返回 role → [keywords] 映射)"""
|
||||
d = config_dir or CONFIG_DIR
|
||||
path = d / "role_routing.yaml"
|
||||
if not path.exists():
|
||||
return {}
|
||||
with open(path) as f:
|
||||
data = yaml.safe_load(f) or {}
|
||||
return data.get("role_keywords", {})
|
||||
|
||||
|
||||
def route_role(text: str, routing: dict[str, list[str]]) -> str:
|
||||
"""按关键词路由任务到合适的 agent_role,无匹配返回 dev-embedded"""
|
||||
text_lower = text.lower()
|
||||
for role, keywords in routing.items():
|
||||
if any(kw.lower() in text_lower for kw in keywords):
|
||||
return role
|
||||
return "dev-embedded"
|
||||
@@ -0,0 +1,356 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timedelta
|
||||
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 = ""
|
||||
# 制约工作流扩展字段
|
||||
retry_count: int = 0
|
||||
parent_task_id: int = 0
|
||||
constraint_type: str = "" # "veto_pending"|"approval_pending"|"adversarial"
|
||||
awaiting_role: str = "" # 串行链中等待哪个角色
|
||||
created_at: str = field(default_factory=lambda: datetime.utcnow().isoformat())
|
||||
# 任务溯源字段
|
||||
initiator: str = "system" # 发起人:scanner / architect / manager / user / system
|
||||
discussion: str = "" # 沟通纪要:任务背景、决策依据、相关讨论
|
||||
execution_host: str = "dev" # dev | edge-api
|
||||
|
||||
|
||||
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, isolation_level=None) # autocommit
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
def _init_db(self) -> None:
|
||||
with self._conn() as c:
|
||||
c.execute("PRAGMA journal_mode=WAL")
|
||||
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 '',
|
||||
retry_count INTEGER DEFAULT 0,
|
||||
parent_task_id INTEGER DEFAULT 0,
|
||||
constraint_type TEXT DEFAULT '',
|
||||
awaiting_role TEXT DEFAULT '',
|
||||
initiator TEXT DEFAULT 'system',
|
||||
discussion TEXT DEFAULT ''
|
||||
)
|
||||
""")
|
||||
# 迁移:为已存在的旧表添加新列(幂等)
|
||||
with self._conn() as c:
|
||||
for col, col_type, defval in [
|
||||
("retry_count", "INTEGER", "0"),
|
||||
("parent_task_id", "INTEGER", "0"),
|
||||
("constraint_type", "TEXT", "''"),
|
||||
("awaiting_role", "TEXT", "''"),
|
||||
("initiator", "TEXT", "'system'"),
|
||||
("discussion", "TEXT", "''"),
|
||||
("execution_host", "TEXT", "'dev'"),
|
||||
]:
|
||||
try:
|
||||
c.execute(f"ALTER TABLE tasks ADD COLUMN {col} {col_type} DEFAULT {defval}")
|
||||
except sqlite3.OperationalError:
|
||||
pass # 列已存在
|
||||
|
||||
def _row_to_task(self, row) -> Task:
|
||||
return 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=row["status"],
|
||||
retry_count=int(row["retry_count"] or 0),
|
||||
parent_task_id=row["parent_task_id"] or 0,
|
||||
constraint_type=row["constraint_type"] or "",
|
||||
awaiting_role=row["awaiting_role"] or "",
|
||||
initiator=row["initiator"] or "system",
|
||||
discussion=row["discussion"] or "",
|
||||
execution_host=row["execution_host"] if "execution_host" in row.keys() else "dev",
|
||||
)
|
||||
|
||||
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,"
|
||||
"retry_count,parent_task_id,constraint_type,awaiting_role,initiator,discussion,"
|
||||
"execution_host) "
|
||||
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
(task.project, task.type, task.title, task.context,
|
||||
task.priority, task.mode, task.agent_role, "pending", task.created_at,
|
||||
task.retry_count, task.parent_task_id, task.constraint_type, task.awaiting_role,
|
||||
task.initiator, task.discussion, task.execution_host),
|
||||
)
|
||||
return cur.lastrowid
|
||||
|
||||
def dequeue(
|
||||
self,
|
||||
exclude_projects: frozenset[str] | None = None,
|
||||
role_filter: frozenset[str] | None = None,
|
||||
) -> Optional[Task]:
|
||||
"""原子化取出优先级最高的 pending 任务并标记为 running。
|
||||
exclude_projects: 跳过这些项目的任务(用于项目级暂停)。
|
||||
role_filter: 只取这些角色的任务(用于多队列隔离)。
|
||||
"""
|
||||
conn = self._conn()
|
||||
try:
|
||||
conn.execute("BEGIN EXCLUSIVE")
|
||||
# 动态构建 WHERE 子句
|
||||
conditions = ["status='pending'"]
|
||||
params: list = []
|
||||
if exclude_projects:
|
||||
placeholders = ",".join("?" * len(exclude_projects))
|
||||
conditions.append(f"project NOT IN ({placeholders})")
|
||||
params.extend(tuple(exclude_projects))
|
||||
if role_filter:
|
||||
placeholders = ",".join("?" * len(role_filter))
|
||||
conditions.append(f"agent_role IN ({placeholders})")
|
||||
params.extend(tuple(role_filter))
|
||||
where = " AND ".join(conditions)
|
||||
row = conn.execute(
|
||||
f"SELECT * FROM tasks WHERE {where} ORDER BY priority ASC, id ASC LIMIT 1",
|
||||
params,
|
||||
).fetchone()
|
||||
if not row:
|
||||
conn.execute("COMMIT")
|
||||
return None
|
||||
conn.execute(
|
||||
"UPDATE tasks SET status='running', started_at=? WHERE id=?",
|
||||
(datetime.utcnow().isoformat(), row["id"]),
|
||||
)
|
||||
conn.execute("COMMIT")
|
||||
t = self._row_to_task(row)
|
||||
t.status = "running"
|
||||
return t
|
||||
except Exception:
|
||||
conn.execute("ROLLBACK")
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
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),
|
||||
)
|
||||
|
||||
def mark_waiting_approval(self, task_id: int, awaiting_role: str = "") -> None:
|
||||
"""进入串行审批链等待状态(不占 Semaphore)"""
|
||||
with self._conn() as c:
|
||||
c.execute(
|
||||
"UPDATE tasks SET status='waiting_approval', awaiting_role=? WHERE id=?",
|
||||
(awaiting_role, task_id),
|
||||
)
|
||||
|
||||
def mark_vetoed(self, task_id: int, reason: str = "") -> None:
|
||||
"""被否决:retry_count+1;第 3 次否决时标记为 failed(最多允许 2 次重试,retry_count 上限为 2)"""
|
||||
with self._conn() as c:
|
||||
row = c.execute("SELECT * FROM tasks WHERE id=?", (task_id,)).fetchone()
|
||||
if not row:
|
||||
return
|
||||
retry = int(row["retry_count"] or 0) + 1
|
||||
if retry > 2:
|
||||
c.execute(
|
||||
"UPDATE tasks SET status='failed', result_summary=? WHERE id=?",
|
||||
(f"否决次数超限: {reason}", task_id),
|
||||
)
|
||||
return
|
||||
new_context = (
|
||||
f"\u26a0\ufe0f 上次执行被否决(第 {retry} 次重试)\n"
|
||||
f"原因:{reason}\n\n"
|
||||
f"--- 原始任务上下文 ---\n"
|
||||
f"{row['context']}"
|
||||
)
|
||||
c.execute(
|
||||
"UPDATE tasks SET status='pending', retry_count=?, context=?, "
|
||||
"awaiting_role='', constraint_type='veto_pending' WHERE id=?",
|
||||
(retry, new_context, task_id),
|
||||
)
|
||||
|
||||
def mark_adversarial_pending(self, task_id: int) -> None:
|
||||
"""对抗式提案等待 SELECT"""
|
||||
with self._conn() as c:
|
||||
c.execute(
|
||||
"UPDATE tasks SET status='adversarial_pending', "
|
||||
"constraint_type='adversarial' WHERE id=?",
|
||||
(task_id,),
|
||||
)
|
||||
|
||||
def approve_waiting(self, task_id: int) -> None:
|
||||
"""审批通过:waiting_approval → done"""
|
||||
with self._conn() as c:
|
||||
c.execute(
|
||||
"UPDATE tasks SET status='done', awaiting_role='' WHERE id=?",
|
||||
(task_id,),
|
||||
)
|
||||
|
||||
def reset_stale_running(self, timeout_minutes: int = 120) -> int:
|
||||
"""将超时的 running 任务标记为 failed,返回处理数量。
|
||||
|
||||
timeout_minutes=0 表示重置所有 running 任务(用于进程重启后的恢复)。
|
||||
"""
|
||||
with self._conn() as c:
|
||||
if timeout_minutes == 0:
|
||||
rows = c.execute(
|
||||
"SELECT id FROM tasks WHERE status='running'"
|
||||
).fetchall()
|
||||
else:
|
||||
cutoff = (datetime.utcnow() - timedelta(minutes=timeout_minutes)).isoformat()
|
||||
rows = c.execute(
|
||||
"SELECT id FROM tasks WHERE status='running' AND started_at < ?",
|
||||
(cutoff,),
|
||||
).fetchall()
|
||||
if not rows:
|
||||
return 0
|
||||
ids = [r["id"] for r in rows]
|
||||
placeholders = ",".join("?" * len(ids))
|
||||
reason = "执行超时(Watchdog 检测)" if timeout_minutes > 0 else "进程重启恢复"
|
||||
c.execute(
|
||||
f"UPDATE tasks SET status='failed', completed_at=?, result_summary=? "
|
||||
f"WHERE id IN ({placeholders})",
|
||||
[datetime.utcnow().isoformat(), reason, *ids],
|
||||
)
|
||||
return len(ids)
|
||||
|
||||
def bump_stale_priorities(self, threshold_minutes: int = 120) -> int:
|
||||
"""将长时间等待的 pending 任务优先级提升(priority-1,最低为 1),返回处理数量。"""
|
||||
cutoff = (datetime.utcnow() - timedelta(minutes=threshold_minutes)).isoformat()
|
||||
with self._conn() as c:
|
||||
rows = c.execute(
|
||||
"SELECT id, priority FROM tasks "
|
||||
"WHERE status='pending' AND priority > 1 AND created_at < ?",
|
||||
(cutoff,),
|
||||
).fetchall()
|
||||
for r in rows:
|
||||
c.execute(
|
||||
"UPDATE tasks SET priority=? WHERE id=?",
|
||||
(r["priority"] - 1, r["id"]),
|
||||
)
|
||||
return len(rows)
|
||||
|
||||
def get_waiting_approval_tasks(self) -> list[Task]:
|
||||
"""获取所有 waiting_approval 状态的任务"""
|
||||
with self._conn() as c:
|
||||
rows = c.execute(
|
||||
"SELECT * FROM tasks WHERE status='waiting_approval'"
|
||||
).fetchall()
|
||||
return [self._row_to_task(r) for r in rows]
|
||||
|
||||
|
||||
def enqueue_topic(self, task: Task) -> int:
|
||||
"""将选题任务写入 topic_pending 状态(不走普通 pending 去重检查)。"""
|
||||
with self._conn() as c:
|
||||
exists = c.execute(
|
||||
"SELECT id FROM tasks WHERE project=? AND title=? AND status='topic_pending'",
|
||||
(task.project, task.title),
|
||||
).fetchone()
|
||||
if exists:
|
||||
return -1
|
||||
cur = c.execute(
|
||||
"INSERT INTO tasks "
|
||||
"(project,type,title,context,priority,mode,agent_role,status,created_at,"
|
||||
"initiator,discussion,execution_host) "
|
||||
"VALUES (?,?,?,?,?,?,?,'topic_pending',?,?,?,?)",
|
||||
(task.project, task.type, task.title, task.context,
|
||||
task.priority, task.mode, task.agent_role, task.created_at,
|
||||
task.initiator, task.discussion, task.execution_host),
|
||||
)
|
||||
return cur.lastrowid
|
||||
|
||||
def approve_topic(self, task_id: int) -> None:
|
||||
"""用户确认选题:topic_pending → pending"""
|
||||
with self._conn() as c:
|
||||
c.execute(
|
||||
"UPDATE tasks SET status='pending' WHERE id=? AND status='topic_pending'",
|
||||
(task_id,),
|
||||
)
|
||||
|
||||
def reject_topic(self, task_id: int, reason: str = "") -> None:
|
||||
"""用户否决选题:topic_pending → vetoed"""
|
||||
with self._conn() as c:
|
||||
c.execute(
|
||||
"UPDATE tasks SET status='vetoed', result_summary=? WHERE id=? AND status='topic_pending'",
|
||||
(reason, task_id),
|
||||
)
|
||||
|
||||
def get_topic_pending_tasks(self) -> list[Task]:
|
||||
"""返回所有待审批选题。"""
|
||||
with self._conn() as c:
|
||||
rows = c.execute(
|
||||
"SELECT * FROM tasks WHERE status='topic_pending' ORDER BY created_at DESC"
|
||||
).fetchall()
|
||||
return [self._row_to_task(r) for r in rows]
|
||||
|
||||
def clear_project_tasks(self, project: str, statuses: list[str] | None = None) -> int:
|
||||
"""删除指定项目的任务记录,默认删除全部状态。返回删除数量。"""
|
||||
with self._conn() as c:
|
||||
if statuses:
|
||||
placeholders = ",".join("?" * len(statuses))
|
||||
rows = c.execute(
|
||||
f"SELECT id FROM tasks WHERE project=? AND status IN ({placeholders})",
|
||||
[project, *statuses],
|
||||
).fetchall()
|
||||
else:
|
||||
rows = c.execute(
|
||||
"SELECT id FROM tasks WHERE project=?", (project,)
|
||||
).fetchall()
|
||||
if not rows:
|
||||
return 0
|
||||
ids = [r["id"] for r in rows]
|
||||
placeholders = ",".join("?" * len(ids))
|
||||
c.execute(f"DELETE FROM tasks WHERE id IN ({placeholders})", ids)
|
||||
return len(ids)
|
||||
@@ -0,0 +1,192 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from nmfs_agents.config import AgentsConfig, ProjectConfig
|
||||
from nmfs_agents.tools.project_memory import ProjectMemory
|
||||
|
||||
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"
|
||||
initiator: str = "scanner"
|
||||
discussion: str = ""
|
||||
|
||||
|
||||
class Scanner:
|
||||
def __init__(self, config: AgentsConfig,
|
||||
memory: ProjectMemory | None = None) -> None:
|
||||
self._config = config
|
||||
self._memory = memory or ProjectMemory()
|
||||
from nmfs_agents.config import load_role_routing
|
||||
self._role_routing = load_role_routing()
|
||||
|
||||
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))
|
||||
results.extend(self._scan_description(name, cfg))
|
||||
results.extend(self._scan_planned_features(name, cfg))
|
||||
results.extend(self._scan_periodic(name, cfg))
|
||||
# Append long-term goal to every task context so agents stay aligned
|
||||
goal_ctx = self._memory.get_context_for_prompt(name)
|
||||
if goal_ctx:
|
||||
for r in results:
|
||||
r.context = r.context + goal_ctx
|
||||
return results
|
||||
|
||||
def _make(self, project: str, cfg: ProjectConfig, type_: str,
|
||||
title: str, context: str, priority: int,
|
||||
agent_role: str | None = None) -> ScanResult:
|
||||
if agent_role is None:
|
||||
from nmfs_agents.config import route_role
|
||||
agent_role = route_role(f"{title} {context}", self._role_routing)
|
||||
return ScanResult(
|
||||
project=project, type=type_, title=title,
|
||||
context=context, priority=priority, mode=cfg.mode,
|
||||
agent_role=agent_role,
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
_SKIP_DIRS = frozenset({
|
||||
"venv", ".venv", ".git", "__pycache__", "node_modules",
|
||||
".tox", "build", "dist", "*.egg-info",
|
||||
})
|
||||
|
||||
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 self._iter_py_files(src_dir):
|
||||
try:
|
||||
rel = py_file.relative_to(cfg.path)
|
||||
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)
|
||||
results.append(self._make(
|
||||
project, cfg, "code_review",
|
||||
f"TODO: {key}",
|
||||
f"{rel}:{i} — {line.strip()}",
|
||||
priority=4,
|
||||
))
|
||||
except OSError:
|
||||
pass
|
||||
return results
|
||||
|
||||
def _iter_py_files(self, root: Path):
|
||||
"""递归遍历 .py 文件,跳过 venv/__pycache__ 等无关目录。"""
|
||||
for entry in root.iterdir():
|
||||
if entry.is_dir():
|
||||
if entry.name in self._SKIP_DIRS or entry.name.endswith(".egg-info"):
|
||||
continue
|
||||
yield from self._iter_py_files(entry)
|
||||
elif entry.suffix == ".py":
|
||||
yield entry
|
||||
|
||||
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
|
||||
|
||||
_BUG_KEYWORDS = frozenset(["待修复", "bug", "问题", "错误", "失败", "零检出", "报错"])
|
||||
_PERF_KEYWORDS = frozenset(["fps偏低", "fps 偏低", "性能偏低", "偏低", "延迟高", "速度慢"])
|
||||
|
||||
def _scan_description(self, project: str, cfg: ProjectConfig) -> list[ScanResult]:
|
||||
"""从 projects.yaml description 字段提取问题任务。"""
|
||||
desc = cfg.description.strip()
|
||||
if not desc:
|
||||
return []
|
||||
desc_lower = desc.lower()
|
||||
if any(kw in desc_lower for kw in self._BUG_KEYWORDS):
|
||||
return [self._make(project, cfg, "fix_bug",
|
||||
f"[描述] {desc[:80]}", desc, priority=2)]
|
||||
if any(kw in desc_lower for kw in self._PERF_KEYWORDS):
|
||||
return [self._make(project, cfg, "improve_perf",
|
||||
f"[描述] {desc[:80]}", desc, priority=3)]
|
||||
return []
|
||||
|
||||
def _scan_periodic(self, project: str, cfg: ProjectConfig) -> list[ScanResult]:
|
||||
"""为每个项目生成兜底定期代码巡检任务(优先级最低)。"""
|
||||
return [self._make(
|
||||
project, cfg, "code_review",
|
||||
"定期代码巡检",
|
||||
f"对 {project} 项目进行整体代码质量检查,识别潜在改进点。",
|
||||
priority=5,
|
||||
)]
|
||||
|
||||
def _scan_planned_features(self, project: str, cfg: ProjectConfig) -> list[ScanResult]:
|
||||
"""从 CLAUDE.md 的 ## 规划功能 章节提取 new_feature 任务"""
|
||||
results = []
|
||||
cfile = cfg.path / "CLAUDE.md"
|
||||
if not cfile.exists():
|
||||
return results
|
||||
text = cfile.read_text(encoding="utf-8", errors="ignore")
|
||||
section = re.search(
|
||||
r"##\s*规划功能(.*?)(?=\n##|\Z)", text, re.DOTALL
|
||||
)
|
||||
if not section:
|
||||
return results
|
||||
for line in section.group(1).splitlines():
|
||||
line = line.strip("- *\t ")
|
||||
if len(line) > 5:
|
||||
results.append(self._make(
|
||||
project, cfg, "new_feature",
|
||||
f"[规划] {line[:80]}", line, priority=3,
|
||||
))
|
||||
return results
|
||||
@@ -0,0 +1,64 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import threading
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from nmfs_agents.core.queue import TaskQueue
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
WATCHDOG_INTERVAL = 60 # 检查间隔(秒)
|
||||
TASK_TIMEOUT_MINUTES = 150 # running 任务超过此时长视为卡死(任务最大超时 120min + 30min 缓冲)
|
||||
PRIORITY_AGING_MINUTES = 120 # pending 任务等待超过此时长触发优先级提升
|
||||
|
||||
|
||||
class Watchdog(threading.Thread):
|
||||
"""后台看门狗线程:定期检测并修复卡死任务,防止队列长期阻塞。
|
||||
|
||||
功能:
|
||||
- 超时任务检测:running > TASK_TIMEOUT_MINUTES → failed(释放 semaphore 名额)
|
||||
- 优先级老化:pending > PRIORITY_AGING_MINUTES → priority -1(防止任务永久饥饿)
|
||||
- 心跳日志:每次 tick 写 DEBUG 日志,便于确认 watchdog 存活
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
queue: TaskQueue,
|
||||
interval: int = WATCHDOG_INTERVAL,
|
||||
task_timeout_minutes: int = TASK_TIMEOUT_MINUTES,
|
||||
) -> None:
|
||||
super().__init__(name="Watchdog", daemon=True)
|
||||
self._queue = queue
|
||||
self._interval = interval
|
||||
self._task_timeout = task_timeout_minutes
|
||||
self._stop_event = threading.Event()
|
||||
|
||||
def run(self) -> None:
|
||||
logger.info(
|
||||
"Watchdog 已启动(检查间隔=%ds,任务超时阈值=%dm)",
|
||||
self._interval, self._task_timeout,
|
||||
)
|
||||
# Event.wait(timeout) 在 set() 时立即返回 True,否则等满 timeout 秒返回 False
|
||||
while not self._stop_event.wait(self._interval):
|
||||
try:
|
||||
self._tick()
|
||||
except Exception as e:
|
||||
logger.error("Watchdog tick 异常: %s", e)
|
||||
logger.info("Watchdog 已停止")
|
||||
|
||||
def stop(self) -> None:
|
||||
"""通知 Watchdog 退出,调用后线程将在当前 sleep 结束时退出。"""
|
||||
self._stop_event.set()
|
||||
|
||||
def _tick(self) -> None:
|
||||
stale = self._queue.reset_stale_running(self._task_timeout)
|
||||
if stale:
|
||||
logger.warning("Watchdog: %d 个超时任务已重置为 failed", stale)
|
||||
|
||||
aged = self._queue.bump_stale_priorities(PRIORITY_AGING_MINUTES)
|
||||
if aged:
|
||||
logger.info("Watchdog: %d 个长期 pending 任务优先级已提升", aged)
|
||||
|
||||
logger.debug("Watchdog tick OK (stale=%d, aged=%d)", stale, aged)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user