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:
2026-03-10 07:24:19 +08:00
co-authored by Claude Sonnet 4.6
parent 36985d0774
commit df59fc0c83
160 changed files with 31806 additions and 1707 deletions
+28
View File
@@ -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/
+2 -2
View File
@@ -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 Agentclaude --print 子进程,三模式)
+86 -5
View File
@@ -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(事实) | 任意 agentarchitect 通过 `[记忆]` 标签写入 |
| 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 # 查看项目知识事实
+37
View File
@@ -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"]
+28
View File
@@ -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
+27 -6
View File
@@ -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_deliverytasks.db ─> developer / architect / tester
base_optbase_opt_tasks.db ─> 算法专项 / 调研 / 硬件优化 Agent
insightinsight_tasks.db ─> market-pm / planner / productizer
↓ 调研结论写入 ProjectMemory
__research__.rtp.* / __research__.net.* / __research__.kernel.*
└─> Dashboard"基础技术调研"区块自动展示
__market__.{goal}.*(运动 / 适老 / AI安全)
└─> Dashboard"市场洞察"目标卡片自动更新
```
## 执行模式
+1 -1
View File
@@ -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 设备接入**:算法任务默认用 rk3566192.168.123.183)验证,rk3588 作为高性能备选
### 修复
+1
View File
@@ -39,6 +39,7 @@ queues:
- dev-embedded
- os-engineer
- system-tester
- tester
- architect
- market-pm
- rtp-researcher
+28 -5
View File
@@ -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
+77
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
node_modules
dist
.git
+24
View File
@@ -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?
+73
View File
@@ -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...
},
},
])
```
+23
View File
@@ -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,
},
},
])
+13
View File
@@ -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>
+5961
View File
File diff suppressed because it is too large Load Diff
+42
View File
@@ -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"
}
}
+6
View File
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
+1
View File
@@ -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

+42
View File
@@ -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;
}
+1
View File
@@ -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>
)
}
+241
View File
@@ -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>
)
}
+117 -15
View File
@@ -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">
+237
View File
@@ -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>
)
}
+71
View File
@@ -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>
)
}
+232
View File
@@ -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>
)
}
+110
View File
@@ -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>
)
}
+53
View File
@@ -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>
)
}
+195
View File
@@ -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>
)
}
+23
View File
@@ -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;
}
+20 -1
View File
@@ -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()
}
+44
View File
@@ -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()
}
}
+10
View File
@@ -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>,
)
+94
View File
@@ -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 }
}),
}))
+74
View File
@@ -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',
}
+18
View File
@@ -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')],
}
+28
View File
@@ -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"]
}
+7
View File
@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
+26
View File
@@ -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"]
}
+15
View File
@@ -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 },
},
},
})
+40
View File
@@ -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.dbSQLite 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
+123
View File
@@ -0,0 +1,123 @@
# AI安全领域市场深度分析报告
> 更新时间:2026-03-10 | 分析人:Market-PM Agent | 平台:RK3588 NPU
---
## 一、市场规模
| 维度 | 数据 |
|------|------|
| 全球 AI 视频监控市场(2025) | USD 59.8 亿(另一口径:$65.1亿) |
| 全球 AI 视频监控市场(2030 预测) | USD 117.6287.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,000AI 深眸¥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 | 模组¥50200 | 端侧摄像头 NPU |
| NVIDIA Jetson Nano | ~0.5 TOPS | $149 | 开发者/高端摄像头 |
| NVIDIA Jetson TX1 | ~1 TOPS | $299+ | 高端 AI 摄像头(海康深眸¥5000+) |
| **RK3588(本平台)** | **6 TOPS INT8** | 模块 ¥8001,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 |
| 成本(模块) | ¥8001,200 | ¥50200 | $299+ |
| 算力/成本比 | ★★★★★ | ★★★ | ★★★ |
| 已有模型 | YOLO+MediaPipe+Embedding | - | - |
### 3 大切入机会
#### 机会 1:中小企业私有化 AI 安防盒子(最高优先级)
- **痛点**:海康/大华私有化 AI 盒子 ¥5,00050,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、佰泰盛世科技、智东西*
+112
View File
@@ -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,0993,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-T6RK3588)模块 ¥8001,200 |
### 差异化机会矩阵
| 场景 | 痛点 | RK3588 方案 | 优势 |
|------|------|------------|------|
| **非接触跌倒检测** | 摄像头泄露隐私,毫米波成本高(¥2万+) | 深度摄像头+mediapipe 骨骼估计,边缘推理,数据不出设备 | 隐私保护 + 成本 ¥2,0005,000 |
| **卧室/卫生间监护** | 传统摄像头无法合规部署 | 深度图+骨骼检测(无 RGB 画面传输) | 合规可部署私密场所 |
| **睡眠监测** | 毫米波精度差,商用设备贵 | 毫米波+视觉融合,RK3588 并行推理 | 多模态精度,成本可控 |
| **摔倒后自动呼叫** | 需要手动 SOS | NPU 实时姿态异常→本地拨号 | 端侧自动化,无网络依赖 |
| **异常行为检测** | 需 7×24 人工看护 | 长时间不动/异常行为时序分析,本地告警 | 替代人工看护 |
### 产品 BOM 估算
```
[居家适老 AI 盒子] = RK3588 + 深度摄像头 + 本地推理
- RK3588 核心板(NanoPC-T6 类):¥8001,200
- 深度摄像头(OAK-D Lite 类):¥600900
- 外壳 + 电源:¥100200
- 合计 BOM:¥1,5002,300(量产可降至 ¥1,000 以内)
- 竞品对比:德心小宝(毫米波方案)¥2万+,猎户豹小秘 ¥2–5万
```
---
## 六、切入建议
1. **主打"隐私保护+边缘推理"**,避开华为/小米可穿戴红海
2. **B 端先行**(养老机构/社区),积累数据与 CFDA 资质 → C 端扩展
3. **技术 MVP**:基于现有 mediapipe + RK3588,快速验证跌倒检测准确率(目标 >95%)
*参考来源:AgeClub、新浪财经、东方财富、OFweek 可穿戴网、Precedence Research*
+134
View File
@@ -0,0 +1,134 @@
# 适老领域 AI 市场深度分析报告
> 生成日期:2026-03-09 | 作者:Market-PM Agent | 版本:v1.0
---
## 1. 市场概况
| 维度 | 数据 |
|------|------|
| 全球 AI 养老市场规模(2025 | USD 56.78 亿 |
| 全球 AI 养老市场规模(2035 预测) | USD 387.52 亿 |
| CAGR2026-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 TOPSINT8),三核并行最高 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*
+96
View File
@@ -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(国内¥68008700 | 2026年Q2运动线收入$6.05亿(YoY +41% | 专业运动模式 50+、TopoActive 地图、VO2 Max/体力预测、Elevate Gen5 传感器 | 纯硬件高毛利 |
| **华为 Watch GT5 Pro** | ¥2,6993,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 血糖整合 | 硬件+轻订阅 |
| **KeepKinetic.ai** | 会员¥25/月;硬件¥5992,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 Labs65项血液生物标志物+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 续航仅 15hApple 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@60fpsH.265/H.264 |
### 4 大差异化机会
#### 机会 1:中小场馆智能运动分析终端(最高优先级)
- **痛点**:商用方案 ¥10万+,中小场馆负担不起;云端有隐私风险
- **方案**RK3588 本地推理(MediaPipe 关键点 + YOLO 多人检测),设备成本 ¥3,0008,000/台
- **竞争壁垒**:全本地、低延迟(<100ms)、无订阅、无隐私风险
- **目标客户**:社区球场、中学体育馆、健身房、球类俱乐部
#### 机会 2:视觉辅助纠姿系统(高优先级)
- **痛点**:手环传感器无法捕捉动作质量,实时反馈延迟大
- **方案**:固定摄像头 + RK3588 实时姿态分析,与穿戴数据融合(心率+骨架联合分析)
- **差异化**:弥补纯手环无视觉盲区,提供"教练级"实时动作反馈
#### 机会 3:本地 AI 运动教练盒子(中优先级)
- **痛点**:Keep 等 App 需联网、订阅费高
- **方案**:RK3588 离线推理盒,含标准化动作库(150+ 动作),家庭/企业健身房使用
- **商业模式**:硬件一次性购买 + 动作包扩展
#### 机会 4:运动视频自动精彩剪辑(中优先级)
- **方案**RK3588 本地视频事件检测(YOLO + 事件识别),自动导出精彩片段
- **对标**:影石 AI 高光助手(依赖手机),本方案完全本地化
---
## 五、可复用模块
`摄像头RTSP``YOLO 多人检测``MediaPipe 骨架提取``Embedding 动作分析``本地展示/API`
*参考来源:洛图科技、Tom's Guide、TechRadar、新浪财经、Keep 财报*
+222
View File
@@ -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 在任务完成后自动写入 historyArchitect 通过 `[记忆]` 标签写入 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、BootloaderU-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` |
| 设备 dmesgSSH 拉取) | 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
+218
View File
@@ -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.dbSQLite 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. DagViewReact 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__.*` factsWRITE_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-engineeros-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 jobos-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→5project_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 工具集) |
+734
View File
@@ -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
```
预期:ImportErrorplanner.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
```
预期:FAILplanner 路由不存在,返回 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+ PASS0 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`,不做项目名校验
+275
View File
@@ -0,0 +1,275 @@
# 三组 15 角色团队架构设计文档
**日期**: 2026-03-08
**状态**: 已确认
## 背景
当前系统为单队列 8 角色,缺乏明确的组织分工。随着业务扩展至多平台(RK3588/ESP32/全志/Nordic)、多产品线(方案级+整机出货),需要引入三组架构,覆盖基础技术研发、项目研发交付、产品洞察与市场落地三个维度。
---
## 业务上下文
- **产品形态**:嵌入式 AI 方案(软硬全包含)为主 + 部分整机出货
- **平台**RockchipRK3588/RV1106/RK3562)、ESP32/NordicTinyML)、全志
- **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 + autooptimize 时)
#### `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 VisionLLM多模态)** 分析:检测框合理性、漏检、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__/.gitmax_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 精确查询
其他 Agentbase-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,无额外依赖
- **容错优先**:任意设备离线或扫描失败不影响主流程
+721
View File
@@ -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_ROLESproject_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_collectid='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 jobjobs={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
- **轻依赖**PILpillow+ Playwright,无额外服务依赖
File diff suppressed because it is too large Load Diff
+7
View File
@@ -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
+2 -2
View File
@@ -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
View File
@@ -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 = [
+359
View File
@@ -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=Nlatency_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、HudlRK3588 本地化的差异点\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-platformsrc/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-platformsrc/server.pyFastAPI 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-7A76 大核)专用于推理进程\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()
+51
View File
@@ -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"
+400
View File
@@ -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 个 inputplaceholder 含 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()
+54
View File
@@ -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
+207
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
fastapi>=0.110.0
uvicorn[standard]>=0.29.0
pyserial>=3.5
View File
View File
+418
View File
@@ -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)
+25
View File
@@ -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
+55
View File
@@ -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推理延迟msNPU利用率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
+132
View File
@@ -0,0 +1,132 @@
from __future__ import annotations
"""boss.py — L0 Boss AgentCEO/战略决策者)。
职责
- 每周接收各部门长汇报制定战略方向
- 读取四域知识库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/BossL0 战略决策层)。
## 职责定位
- 接收基础技术部项目交付部市场洞察部三个部门长的周报
- 制定公司技术战略和市场方向
- 协调跨部门资源分配和优先级
- 处理 [越级汇报] 重要信息技术风险/市场机会/跨组织问题
- **不介入**具体代码测试日常开发细节
## 决策维度
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"请用中文回复,保持简洁,聚焦问题。"
)
+152
View File
@@ -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"
+21
View File
@@ -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
+20
View File
@@ -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 -50insmod/rmmod 前确认依赖\n"
"- Kconfig 变更需同步更新 defconfig\n"
"- 内存安全:检查 NULL 指针、use-after-free、buffer overflow\n"
)
return base + self._build_device_context() + kernel_guidance
+20
View File
@@ -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、BootloaderU-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
+71
View File
@@ -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}
+32
View File
@@ -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 = """你是产品洞察与市场落地组的媒体制作师,随时可被触发,兼顾技术演示和市场展示。
技术演示侧
- 使用 OpenCVpython3 + 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
+45
View File
@@ -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=fullpmap -x <pid>
- 目标定位内存泄漏OOM killer 触发链 allocation 热点
- 输出内存占用趋势图 + 泄漏函数调用链
网络分析
- 工具iperf3 -s/-c吞吐测试ss -tulpn连接状态tc qdisc show排队策略
- 目标带宽瓶颈延迟尖峰包重传率
- 输出吞吐/延迟基准数据 + 建议调整的内核参数
视觉库分析
- 工具OpenCV 帧处理计时cv2.getTickCountNEON/SIMD 利用率perf stat -e simd
- 目标Mat 内存分配热点colorspace 转换开销resize/warp 算子效率
- 输出帧处理时序分解 + 高耗时算子替换建议
AI 算子分析
- 工具RKNN profilerrknn_server --profilerknpu-debugperf 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
+232
View File
@@ -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
+143
View File
@@ -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)
+68
View File
@@ -0,0 +1,68 @@
from __future__ import annotations
"""senior_dev.py — 高级研发 AgentL3 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 DeveloperL3)。
## 与普通研发的区别
- **架构视角**不只写功能代码还思考设计是否合理可扩展
- **技术先进性**关注业界最新方案通过 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
+75
View File
@@ -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. 功能测试运行 pytestSSH 板端自动执行检查通过/失败
2. 视觉测试使用 Playwright 截图推理界面分析检测框/FPS/误检截图保存至 docs/test_reports/
3. 性能测试测量 FPS推理延迟内存占用与目标值对比写入 [记忆]
4. 跨平台测试RK3588/全志/ESP32 分平台调度执行记录差异
5. 测试报告生成结构化 Markdown 报告至 docs/test_reports/YYYY-MM-DD-{project}.md
工具使用
- Playwrightbrowser_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)
+51
View File
@@ -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,
)
+141
View File
@@ -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
+213
View File
@@ -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"
View File
+356
View File
@@ -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)
+192
View File
@@ -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
+64
View File
@@ -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