Compare commits

..
9 Commits
6 changed files with 563 additions and 124 deletions
+266
View File
@@ -0,0 +1,266 @@
<picture src="https://img.shields.io/badge/python-3.11%2B-blue" alt="Python" /><picture src="https://img.shields.io/badge/vue-3.4%2B-4FC08D" alt="Vue" /><picture src="https://img.shields.io/badge/license-MIT-green" alt="License" />
# TaskPulse — AI Agent Scheduled Task Monitor
> **Let your AI agents self-report. You just glance at the dashboard.**
TaskPulse is a tracking and monitoring platform designed for AI agents' scheduled tasks. Unlike traditional CRUD-heavy admin panels, TaskPulse is **AI-native**: give your agents an API spec, and they autonomously register themselves, report execution results, and update their status — no manual form-filling required.
[📖 中文文档](./README.md)
---
## Philosophy
```
┌─────────────────────┐
│ TaskPulse Dashboard│
│ Unified Status View│
└──────┬──────────┬───┘
│ │
┌─────────────────┘ └─────────────────┐
▼ ▼
┌─────────────────┐ ┌──────────────────────┐
│ AI Agent A │ register tasks → execute │ AI Agent B │
│ ├─ data sync │ ← report result + log │ ├─ order fetch │
│ └─ report gen │ │ └─ inventory sync │
└─────────────────┘ └──────────────────────┘
```
**AI-Native by Design**
Traditional schedulers require humans to fill forms and manage cron expressions. But AI agents can read API docs and act autonomously:
1. **Self-Register** — One API call to register both the agent and all its tasks
2. **Report Results** — After each execution, POST back the result with logs and duration
3. **Auto-Alert** — If a task misses its window, the system alerts automatically
The only thing humans need to do: **open the dashboard and see the big picture.**
---
## Features
### 🤖 AI-Native Onboarding
- **Batch Registration**: `POST /api/agents/register-with-tasks` — agent + tasks in one shot
- **API Key Auth**: Each agent gets its own key for reporting
- **Re-registration**: Deleted agents can re-register anytime
- **Agent Avatars**: First-letter + deterministic color avatars for quick visual identification
### 📊 Unified Dashboard
- **Summary Cards**: agent count, task count, 24h executions, pending alerts
- **Global Task View**: all tasks from all agents in one place
- **Task Tags**: label tasks (e.g. "data-sync", "critical"), filter by tags
- **Human-Readable Cron**: `*/5 * * * *` → "Every 5 minutes"
- **Localized Time**: all timestamps auto-converted from UTC to browser timezone
### ⏱ Execution Tracking
- **Live Reporting**: agents POST results immediately after execution
- **Execution History**: full timeline per task, paginated
- **Log Viewer**: dark terminal-style viewer for debugging
- **Search & Filter**: by task name, tags, and status
### 🚨 Timeout Alerts & Notifications
- **Auto-Scan**: background scheduler checks overdue tasks every 60s
- **Grace Window**: per-task tolerance to avoid false alerts
- **Multi-Channel**: Feishu (Lark) webhook, email SMTP, generic webhook
- **Alert Acknowledgment**: mark alerts as handled for closed-loop management
### 🎨 Modern UI
- **Dark Theme**: gradient backgrounds + neon accents
- **Full-Width Layout**: maximizes screen usage
- **Responsive**: adapts to different screen sizes
---
## Tech Stack
| Layer | Technology |
|---|---|
| Backend | Python FastAPI (async) |
| Production Server | Gunicorn + Uvicorn Worker |
| Frontend | Vue 3 + Element Plus |
| Build Tool | Vite |
| Database | MySQL 8.0 |
| Validation | Pydantic v2 |
| ORM | SQLAlchemy 2.0 (async) |
| Auth | JWT (python-jose) + bcrypt |
| Scheduler | APScheduler (async loop) |
| Deployment | Monolithic (FastAPI serves SPA) |
---
## Quick Start
### Local Development
```bash
# 1. Clone
git clone https://github.com/oventh/TaskPulse.git
cd TaskPulse
# 2. Backend
cd backend
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env # edit .env with your DB config
uvicorn app.main:app --reload --port 8000
# 3. Frontend (new terminal)
cd frontend
npm install
npm run dev # http://localhost:3000
```
> Frontend dev server proxies `/api` to `localhost:8000`.
### Production Deployment
```bash
# 1. Build frontend
cd frontend && npm install && npm run build
# 2. Deploy to server
rsync -avz --exclude='.venv' --exclude='node_modules' --exclude='.git' \
./ user@server:/home/app/taskpulse/
# 3. Install deps + start with Gunicorn
cd /home/app/taskpulse/backend
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
.venv/bin/gunicorn app.main:app \
-k uvicorn.workers.UvicornWorker \
-b 0.0.0.0:8000 -w 2 --preload --daemon
```
---
## Agent Integration
Let your AI agent self-register in 3 steps:
```python
import requests
BASE = "https://your-domain.com"
# 1. Register agent + all its tasks (one API call)
resp = requests.post(f"{BASE}/api/agents/register-with-tasks", json={
"name": "my-agent",
"description": "My AI Agent",
"tasks": [
{"name": "sync-data", "cron_expression": "*/5 * * * *"},
{"name": "daily-report", "cron_expression": "0 9 * * *"}
]
})
data = resp.json()
API_KEY = data["agent"]["api_key"] # ← save this
AGENT_ID = data["agent"]["id"]
# 2. Report execution result after each run
requests.post(f"{BASE}/api/tasks/{TASK_ID}/executions",
headers={"Authorization": f"Bearer {API_KEY}"},
json={"status": "success", "duration_ms": 1500, "log": "Task completed"})
```
> For curl, batch registration, and more examples, see the Chinese doc [AGENT_GUIDE.md](./AGENT_GUIDE.md).
---
## Configuration
| Variable | Default | Description |
|----------|---------|-------------|
| `TASKPULSE_DB_HOST` | localhost | MySQL host |
| `TASKPULSE_DB_PORT` | 3306 | MySQL port |
| `TASKPULSE_DB_USER` | dbuser | Database user |
| `TASKPULSE_DB_PASSWORD` | - | Database password |
| `TASKPULSE_DB_NAME` | taskpulse | Database name |
| `TASKPULSE_DEBUG` | true | Debug mode (false in production) |
| `TASKPULSE_SECRET_KEY` | - | JWT signing key |
| `TASKPULSE_SMTP_*` | - | Email notification config |
See `backend/.env.example` for the full list.
---
## API Overview
### Agents
| Method | Path | Description |
|--------|------|-------------|
| POST | `/api/agents` | Register an agent (returns API Key) |
| **POST** | **`/api/agents/register-with-tasks`** | **🌟 Batch register agent + tasks** |
| GET | `/api/agents` | List all agents |
| PUT | `/api/agents/{id}` | Update agent name/description |
| DELETE | `/api/agents/{id}` | Delete agent (cascades to tasks) |
### Tasks
| Method | Path | Description |
|--------|------|-------------|
| POST | `/api/tasks?agent_id=X` | Create a scheduled task |
| GET | `/api/tasks` | List tasks (`?q=&tags=&status=` filters) |
| PUT | `/api/tasks/{id}` | Update task name/tags/description |
| DELETE | `/api/tasks/{id}` | Delete a task |
### Execution Reporting
| Method | Path | Description |
|--------|------|-------------|
| POST | `/api/tasks/{id}/executions` | **Agent reports execution result** |
| GET | `/api/tasks/{id}/executions` | View execution history (paginated) |
| GET | `/api/tasks/{id}/executions/{eid}` | View single execution detail |
### Dashboard & Config
| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/dashboard/summary` | Dashboard summary stats |
| GET | `/api/dashboard/tasks` | Dashboard full task view |
| GET | `/api/system/config` | Get system config |
| PUT | `/api/system/config` | Update system config (Base URL) |
### Alerts & Notifications
| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/alerts` | List alerts |
| POST | `/api/alerts/{id}/acknowledge` | Acknowledge an alert |
| POST | `/api/notification-channels` | Add notification channel |
Full OpenAPI docs at `/docs` (Swagger UI).
---
## Project Structure
```
taskpulse/
├── backend/
│ ├── app/
│ │ ├── main.py # FastAPI entry + SPA static files + lifecycle
│ │ ├── config.py # Environment variables (pydantic-settings)
│ │ ├── database.py # Async MySQL connection
│ │ ├── models/ # SQLAlchemy models (6 tables)
│ │ ├── schemas/ # Pydantic request/response models
│ │ ├── api/ # REST API routes (7 modules)
│ │ └── services/ # Business logic layer
│ │ ├── scheduler.py # ⏰ Background scheduler (timeout detection)
│ │ └── notification.py # 📢 Feishu/Email/Webhook sender
│ ├── alembic/ # DB migrations
│ └── requirements.txt
├── frontend/
│ ├── src/
│ │ ├── views/ # 6 pages (dark theme)
│ │ ├── router/ # Route guards + login redirect
│ │ ├── api/ # Axios client + 401 interceptor
│ │ └── assets/ # Global styles + Element Plus overrides
│ └── package.json
├── AGENT_GUIDE.md # Agent API guide (Chinese)
├── README.md # Documentation (Chinese)
└── README.en.md # Documentation (English)
```
---
## License
MIT License © 2026
+191 -112
View File
@@ -1,28 +1,96 @@
# TaskPulse — AI Agent Task Monitor <picture src="https://img.shields.io/badge/python-3.11%2B-blue" alt="Python" /><picture src="https://img.shields.io/badge/vue-3.4%2B-4FC08D" alt="Vue" /><picture src="https://img.shields.io/badge/license-MIT-green" alt="License" />
统一看板,监控 AI Agent 定时任务的运行状态、执行历史,并在任务超时未执行时发送告警通知。 # TaskPulse — AI Agent 定时任务监控系统
**线上地址**: https://task.pags.cn > **让 AI Agent 自己汇报工作,你只需要打开看板看一眼。**
TaskPulse 是一个面向 AI Agent 的定时任务跟踪与监控平台。它不是传统的"你去填表创建任务"的后台管理系统,而是**给 AI Agent 一份 API 说明书,让 Agent 自主注册、自动汇报**——你只需要在统一的 Dashboard 上看全局状态。
## 系统截图
<img src="https://pub-44c5bd2a850e4bc7aab6f5f8701493fd.r2.dev/images/111.png"></img>
[🌐 English](./README.en.md)
---
## 设计理念
```
┌─────────────────────┐
│ TaskPulse Dashboard│
│ 统一状态看板 │
└──────┬──────────┬───┘
│ │
┌─────────────────┘ └─────────────────┐
▼ ▼
┌─────────────────┐ ┌──────────────────────┐
│ AI Agent A │ 注册任务 → 定时执行 │ AI Agent B │
│ └─ 数据同步 │ ← 汇报结果 + 日志 │ └─ 订单采集 │
│ └─ 报表生成 │ │ └─ 库存同步 │
└─────────────────┘ └──────────────────────┘
```
**核心理念:AI 原生接入**
传统定时任务管理需要用户手动填写 cron 表达式、配置参数。但 AI Agent 本身就具备理解和执行能力——只需给 Agent 一份 API 文档,它就能:
1. **自主注册** — 通过一条 API 把自己和所有定时任务登记到系统
2. **按时汇报** — 每次任务执行完毕,主动 POST 结果(含日志、耗时、状态)
3. **自动纠错** — 如果某个任务到点没执行,系统自动告警
人类要做的只有一件事:**打开看板,看一眼全局状态。**
--- ---
## 核心功能 ## 核心功能
- **AI 原生接入** — AI Agent 通过 API 一次性注册自己 + 所有定时任务,无需手动填写表单 ### 🤖 AI 原生接入
- **统一状态看板** — 所有定时任务的运行状态、最近运行时间、结果一目了然 - **一次性批量注册**`POST /api/agents/register-with-tasks` — Agent 在一分钟内完成注册 + 所有任务登记
- **执行汇报** — Agent 执行完任务后主动汇报结果,含详细日志 - **API Key 鉴权**:每个 Agent 独立密钥,汇报时自动校验
- **超时告警** — 任务未按时执行时自动告警 - **无缝重注册**:删除后可重新注册,获取新 Key 继续工作
- **多渠道通知** — 支持飞书 Webhook、邮件、通用 Webhook - **Agent 头像**:名称首字 + 确定性色彩头像,多 Agent 一目了然
- **Base URL 可配置** — 系统设置页面可配置公网访问地址,所有接入指令自动更新
### 📊 统一状态看板
- **概览卡片**:Agent 数量、任务总数、24h 执行量、待处理告警
- **全任务视图**:所有 Agent 的任务集中展示,状态、最近运行、结果一目了然
- **任务标签**:可对任务打标签(如"数据同步"、"重要"),按标签筛选
- **Cron 人类可读**`*/5 * * * *` 自动显示为 "每5分钟"
- **时间本地化**:所有时间自动从 UTC 转为浏览器本地时区
### ⏱ 执行汇报与追踪
- **实时汇报**Agent 执行完任务后 POST 结果,系统记录执行日志
- **执行历史**:每个任务都有完整的时间线,支持分页查看
- **日志查看器**:暗色终端风格,方便排查问题
- **搜索过滤**:按任务名称、标签、状态快速筛选
### 🚨 超时告警与通知
- **自动检测**:后台调度器每 60 秒扫描未按时执行的任务
- **宽容窗口**:每个任务可单独配置容忍时间,避免误报
- **多渠道通知**:飞书 Webhook、邮件 SMTP、通用 Webhook
- **告警确认**:已处理的告警可标记确认,闭环管理
### 🎨 现代化界面
- **暗色主题**:深色渐变背景 + 霓虹色点缀,降低视觉疲劳
- **全宽布局**:充分利用屏幕空间
- **响应式**:适配不同分辨率
---
## 技术栈 ## 技术栈
| 层 | 技术 | | 层 | 技术 |
|---|---| |---|---|
| 后端 | Python FastAPI (异步) + Gunicorn (生产) | | 后端框架 | Python FastAPI (异步) |
| 前端 | Vue3 + Element Plus (暗色主题) | | 生产服务 | Gunicorn + Uvicorn Worker |
| 前端框架 | Vue 3 + Element Plus |
| 构建工具 | Vite |
| 数据库 | MySQL 8.0 | | 数据库 | MySQL 8.0 |
| 部署 | 单体应用(FastAPI serve Vue3 构建产物) | | 数据校验 | Pydantic v2 |
| ORM | SQLAlchemy 2.0 (异步) |
| 认证 | JWT (python-jose) + bcrypt |
| 定时检测 | APScheduler (异步) |
| 部署形态 | 单体应用 (FastAPI serve 前端静态文件) |
---
## 快速开始 ## 快速开始
@@ -30,128 +98,138 @@
```bash ```bash
# 1. 克隆项目 # 1. 克隆项目
git clone https://gitea.tatta.cn/Tatta/TaskPulse.git git clone https://github.com/oventh/TaskPulse.git
cd TaskPulse cd TaskPulse
# 2. 后端 - 创建虚拟环境并安装依赖 # 2. 后端
cd backend cd backend
python -m venv .venv python -m venv .venv
# Windows: .venv\Scripts\activate source .venv/bin/activate # Windows: .venv\Scripts\activate
# Linux: source .venv/bin/activate
pip install -r requirements.txt pip install -r requirements.txt
cp .env.example .env # 编辑 .env 配置数据库
# 3. 复制环境变量并修改数据库配置
cp .env.example .env
# 编辑 .env,配置 MySQL 连接信息
# 4. 启动后端开发服务器
uvicorn app.main:app --reload --port 8000 uvicorn app.main:app --reload --port 8000
# 5. 前端(新终端) # 3. 前端(新终端)
cd frontend cd frontend
npm install npm install
npm run dev # 访问 http://localhost:3000 npm run dev # http://localhost:3000
``` ```
> 前端 dev server 自动代理 `/api` 请求到 `localhost:8000` > 前端 `npm run dev` 会代理 `/api` 到 `localhost:8000`
### 生产部署 ### 生产部署
```bash ```bash
# 1. 构建前端 # 1. 构建前端
cd frontend cd frontend && npm install && npm run build
npm install && npm run build
# 2. 部署到服务器 # 2. 部署到服务器
# 将整个项目复制到服务器(排除 .venv 和 node_modules
rsync -avz --exclude='.venv' --exclude='node_modules' --exclude='.git' \ rsync -avz --exclude='.venv' --exclude='node_modules' --exclude='.git' \
./ user@server:/home/openclaw/app/taskpulse/ ./ user@server:/home/app/taskpulse/
# 3. 在服务器上创建虚拟环境并安装依赖 # 3. 安装依赖 + Gunicorn 启动
cd /home/openclaw/app/taskpulse/backend cd /home/app/taskpulse/backend
python3 -m venv .venv python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
.venv/bin/pip install -r requirements.txt
# 4. 使用 Gunicorn 启动
.venv/bin/gunicorn app.main:app \ .venv/bin/gunicorn app.main:app \
-k uvicorn.workers.UvicornWorker \ -k uvicorn.workers.UvicornWorker \
-b 0.0.0.0:8000 \ -b 0.0.0.0:8000 -w 2 --preload --daemon
-w 2 \
--access-logfile /var/log/taskpulse/access.log \
--error-logfile /var/log/taskpulse/error.log \
--daemon
``` ```
## 环境变量 ---
复制 `backend/.env.example``backend/.env` 并按需修改: ## Agent 接入指南
| 变量 | 默认值 | 说明 | 让 AI Agent 自动接入只需 3 步:
|------|--------|------|
| TASKPULSE_DB_HOST | localhost | MySQL 主机 |
| TASKPULSE_DB_PORT | 3306 | MySQL 端口 |
| TASKPULSE_DB_USER | dbuser | 数据库用户 |
| TASKPULSE_DB_PASSWORD | - | 数据库密码 |
| TASKPULSE_DB_NAME | taskpulse | 数据库名 |
| TASKPULSE_DEBUG | true | 调试模式(生产环境设为 false) |
| TASKPULSE_FEISHU_WEBHOOK_URL | (空) | 飞书 Webhook 地址 |
| TASKPULSE_SMTP_HOST | (空) | SMTP 服务器 |
| TASKPULSE_SMTP_PORT | 587 | SMTP 端口 |
| TASKPULSE_SMTP_USER | (空) | SMTP 用户 |
| TASKPULSE_SMTP_PASSWORD | (空) | SMTP 密码 |
## Agent 接入
详细接入说明见 [AGENT_GUIDE.md](./AGENT_GUIDE.md)。
### 最小接入示例
```python ```python
import requests import requests
BASE = "https://task.pags.cn" # 替换为你的实际地址 BASE = "https://your-domain.com" # 替换为你的实际地址
# 1. AI Agent 注册自己 + 所有定时任务(一次性) # 1. Agent 一次性注册自己 + 所有定时任务
resp = requests.post(f"{BASE}/api/agents/register-with-tasks", json={ resp = requests.post(f"{BASE}/api/agents/register-with-tasks", json={
"name": "my-agent", "name": "my-agent",
"description": "我的 AI Agent", "description": "我的 AI Agent",
"tasks": [ "tasks": [
{"name": "sync-data", "cron_expression": "*/5 * * * *"}, {"name": "sync-data", "cron_expression": "*/5 * * * *"},
{"name": "daily-report", "cron_expression": "0 9 * * *"} {"name": "daily-report", "cron_expression": "0 9 * * *"}
] ]
}) })
agent = resp.json() data = resp.json()
API_KEY = agent["api_key"] # 保存此 Key API_KEY = data["agent"]["api_key"] # 保存此 Key
AGENT_ID = agent["agent"]["id"] AGENT_ID = data["agent"]["id"]
# 2. 每次执行汇报结果 # 2. 每次执行完任务后,汇报结果
requests.post(f"{BASE}/api/tasks/{TASK_ID}/executions", requests.post(f"{BASE}/api/tasks/{TASK_ID}/executions",
headers={"Authorization": f"Bearer {API_KEY}"}, headers={"Authorization": f"Bearer {API_KEY}"},
json={"status": "success", "duration_ms": 1200, "log": "done"}) json={"status": "success", "duration_ms": 1500, "log": "执行完毕"})
``` ```
> 完整接入说明见 [AGENT_GUIDE.md](./AGENT_GUIDE.md),含 curl / Python / 批量注册示例。
---
## 系统配置
| 变量 | 默认值 | 说明 |
|------|--------|------|
| `TASKPULSE_DB_HOST` | localhost | MySQL 主机 |
| `TASKPULSE_DB_PORT` | 3306 | MySQL 端口 |
| `TASKPULSE_DB_USER` | dbuser | 数据库用户 |
| `TASKPULSE_DB_PASSWORD` | - | 数据库密码 |
| `TASKPULSE_DB_NAME` | taskpulse | 数据库名 |
| `TASKPULSE_DEBUG` | true | 调试模式(生产 false |
| `TASKPULSE_SECRET_KEY` | - | JWT 签名密钥 |
| `TASKPULSE_SMTP_*` | - | 邮件通知配置 |
完整变量列表见 `backend/.env.example`
---
## API 概览 ## API 概览
### Agent 管理
| 方法 | 路径 | 说明 | | 方法 | 路径 | 说明 |
|------|------|------| |------|------|------|
| POST | /api/agents | 注册 Agent | | POST | `/api/agents` | 注册 Agent(返回 API Key |
| POST | /api/agents/register-with-tasks | **AI Agent 一次性注册自己 + 所有任务** | | **POST** | **`/api/agents/register-with-tasks`** | **🌟 Agent + 任务一次性批量注册** |
| GET | /api/agents | 列出所有 Agent | | GET | `/api/agents` | 列出所有 Agent |
| POST | /api/tasks?agent_id=X | 注册定时任务 | | PUT | `/api/agents/{id}` | 修改 Agent 名称/描述 |
| GET | /api/tasks | 列出所有任务 | | DELETE | `/api/agents/{id}` | 删除 Agent(关联任务一并删除) |
| DELETE | /api/tasks/{id} | 删除任务 |
| POST | /api/tasks/{id}/executions | 汇报执行结果 |
| GET | /api/tasks/{id}/executions | 查看执行历史 |
| GET | /api/dashboard/summary | 看板概览 |
| GET | /api/dashboard/tasks | 看板任务视图 |
| GET | /api/system/config | 读取系统配置(Base URL |
| PUT | /api/system/config | 更新系统配置 |
| GET | /api/alerts | 告警列表 |
| POST | /api/alerts/{id}/acknowledge | 确认告警 |
| POST | /api/notification-channels | 添加通知渠道 |
| GET | /api/notification-channels | 列出通知渠道 |
完整 API 文档见 `/docs`Swagger UI)。 ### 定时任务
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | `/api/tasks?agent_id=X` | 注册定时任务 |
| GET | `/api/tasks` | 任务列表(支持 `?q=&tags=&status=` 搜索) |
| PUT | `/api/tasks/{id}` | 修改任务名称/标签/描述 |
| DELETE | `/api/tasks/{id}` | 删除任务 |
### 执行汇报
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | `/api/tasks/{id}/executions` | Agent 汇报执行结果 |
| GET | `/api/tasks/{id}/executions` | 查看执行历史(分页) |
| GET | `/api/tasks/{id}/executions/{eid}` | 查看单次执行详情 |
### 看板与配置
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/dashboard/summary` | 看板概览统计数据 |
| GET | `/api/dashboard/tasks` | 看板全任务视图 |
| GET | `/api/system/config` | 获取系统配置 |
| PUT | `/api/system/config` | 更新系统配置(Base URL |
### 告警与通知
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/alerts` | 告警列表 |
| POST | `/api/alerts/{id}/acknowledge` | 确认告警 |
| POST | `/api/notification-channels` | 添加通知渠道(飞书/邮件/Webhook) |
完整 OpenAPI 文档在 `/docs`Swagger UI)查看。
---
## 项目结构 ## 项目结构
@@ -159,35 +237,36 @@ requests.post(f"{BASE}/api/tasks/{TASK_ID}/executions",
taskpulse/ taskpulse/
├── backend/ ├── backend/
│ ├── app/ │ ├── app/
│ │ ├── main.py # FastAPI 入口 + SPA 静态文件服务 │ │ ├── main.py # FastAPI 入口 + SPA 静态文件 + 生命周期
│ │ ├── config.py # 环境变量配置 │ │ ├── config.py # 环境变量配置 (pydantic-settings)
│ │ ├── database.py # 异步数据库连接 │ │ ├── database.py # 异步 MySQL 连接
│ │ ├── models/ # SQLAlchemy 数据模型 │ │ ├── models/ # SQLAlchemy 数据模型6 张表)
│ │ │ ├── agent.py # Agent(含 API Key 自动生成) │ │ │ ├── agent.py # Agent + API Key
│ │ │ ├── task.py # 定时任务cron、宽容窗口) │ │ │ ├── task.py # 定时任务 + cron + tags
│ │ │ ├── execution.py # 执行记录(日志、结果、耗时) │ │ │ ├── execution.py # 执行记录 + 日志
│ │ │ ├── notification.py # 通知渠道告警规则、告警历史 │ │ │ ├── notification.py # 通知渠道 + 告警规则 + 告警
│ │ │ ── system_config.py # 系统配置key-value │ │ │ ── system_config.py # 系统配置 (key-value)
│ │ ├── schemas/ # Pydantic 数据校验 │ │ │ └── user.py # 看板登录用户
│ │ ├── api/ # API 路由 │ │ ├── schemas/ # Pydantic 请求/响应模型
│ │ ├── agents.py # Agent CRUD + 批量注册 │ │ ├── api/ # REST API 路由(7 个模块)
│ │ │ ├── tasks.py # 任务 CRUD │ │ └── services/ # 业务逻辑层
│ │ ├── executions.py # 执行汇报 + 历史查询 │ │ ├── scheduler.py # ⏰ 后台调度器(超时检测)
│ │ ── notifications.py # 通知渠道 + 告警规则 + 告警确认 │ │ ── notification.py # 📢 飞书/邮件/Webhook 发送
│ │ │ ├── dashboard.py # 看板概览统计
│ │ │ └── system.py # 系统配置
│ │ └── services/ # 业务逻辑
│ │ ├── agent.py / task.py / execution.py
│ │ ├── scheduler.py # 后台扫描:检测超时任务→生成告警→通知
│ │ └── notification.py # 飞书 / 邮件 / Webhook 通知发送
│ ├── alembic/ # 数据库迁移 │ ├── alembic/ # 数据库迁移
│ └── requirements.txt │ └── requirements.txt
├── frontend/ ├── frontend/
│ ├── src/ │ ├── src/
│ │ ├── views/ # 页面组件(暗色主题) │ │ ├── views/ # 6 个页面(暗色主题)
│ │ ├── router/ # 路由 │ │ ├── router/ # 路由守卫 + 登录跳转
│ │ ── api/ # Axios API 客户端 │ │ ── api/ # Axios 客户端 + 401 拦截
│ │ └── assets/ # 全局样式 + Element Plus 覆盖
│ └── package.json │ └── package.json
├── AGENT_GUIDE.md # Agent 接入指南 ├── AGENT_GUIDE.md # Agent API 接入说明
└── README.md └── README.md
``` ```
---
## 许可证
MIT License © 2026
+26 -5
View File
@@ -1,15 +1,25 @@
"""API router — TaskExecution (reporting + query).""" """API router — TaskExecution (reporting + query)."""
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db from app.database import get_db
from app.schemas import ExecutionOut, ExecutionReport from app.schemas import ExecutionOut, ExecutionReport
from app.services import ExecutionService, TaskService, AgentService from app.services import ExecutionService, TaskService, AgentService
from app.models import TaskExecution
router = APIRouter(prefix="/api/tasks/{task_id}/executions", tags=["executions"]) router = APIRouter(prefix="/api/tasks/{task_id}/executions", tags=["executions"])
class PaginatedExecutions(BaseModel):
items: list[ExecutionOut]
total: int
page: int
page_size: int
@router.post("", response_model=ExecutionOut, status_code=201) @router.post("", response_model=ExecutionOut, status_code=201)
async def report_execution(task_id: int, body: ExecutionReport, async def report_execution(task_id: int, body: ExecutionReport,
db: AsyncSession = Depends(get_db)): db: AsyncSession = Depends(get_db)):
@@ -46,16 +56,27 @@ async def report_execution(task_id: int, body: ExecutionReport,
return ExecutionOut(**record.__dict__) return ExecutionOut(**record.__dict__)
@router.get("", response_model=list[ExecutionOut]) @router.get("", response_model=PaginatedExecutions)
async def list_executions( async def list_executions(
task_id: int, task_id: int,
limit: int = Query(50, ge=1, le=200), page: int = Query(1, ge=1),
offset: int = Query(0, ge=0), page_size: int = Query(20, ge=1, le=100),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
exec_svc = ExecutionService(db) exec_svc = ExecutionService(db)
records = await exec_svc.get_executions(task_id, limit=limit, offset=offset) offset = (page - 1) * page_size
return [ExecutionOut(**r.__dict__) for r in records] records = await exec_svc.get_executions(task_id, limit=page_size, offset=offset)
# Get total count
count_stmt = select(func.count()).select_from(TaskExecution).where(TaskExecution.task_id == task_id)
total = (await db.execute(count_stmt)).scalar() or 0
return PaginatedExecutions(
items=[ExecutionOut(**r.__dict__) for r in records],
total=total,
page=page,
page_size=page_size,
)
@router.get("/recent", response_model=list[ExecutionOut]) @router.get("/recent", response_model=list[ExecutionOut])
+1 -1
View File
@@ -42,7 +42,7 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { beijing } from '../utils/time.js' import dayjs from 'dayjs'
import { listAlerts, acknowledgeAlert } from '../api/index.js' import { listAlerts, acknowledgeAlert } from '../api/index.js'
const alerts = ref([]) const alerts = ref([])
+1 -1
View File
@@ -152,7 +152,7 @@
<script setup> <script setup>
import { ref, onMounted, computed } from 'vue' import { ref, onMounted, computed } from 'vue'
import { beijing } from '../utils/time.js' import dayjs from 'dayjs'
import { getDashboardSummary, getDashboardTasks, getSystemConfig } from '../api/index.js' import { getDashboardSummary, getDashboardTasks, getSystemConfig } from '../api/index.js'
const summary = ref({ const summary = ref({
+78 -5
View File
@@ -72,6 +72,20 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<!-- Pagination -->
<div class="pagination" v-if="total > pageSize && totalPages > 1">
<span class="page-info"> {{ total }} </span>
<div class="page-btns">
<button class="page-btn" :disabled="currentPage <= 1" @click="goPage(currentPage - 1)">上一页</button>
<template v-for="p in (totalPages || 1)" :key="p">
<button v-if="Math.abs(p - currentPage) <= 2 || p === 1 || p === totalPages"
class="page-btn" :class="{ active: p === currentPage }"
@click="goPage(p)">{{ p }}</button>
<span v-else-if="p === currentPage - 3 || p === currentPage + 3" class="page-ellipsis"></span>
</template>
<button class="page-btn" :disabled="currentPage >= totalPages" @click="goPage(currentPage + 1)">下一页</button>
</div>
</div>
</div> </div>
<!-- Log Dialog --> <!-- Log Dialog -->
@@ -90,9 +104,9 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted, computed } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { beijing } from '../utils/time.js' import dayjs from 'dayjs'
import { getTask, listExecutions } from '../api/index.js' import { getTask, listExecutions } from '../api/index.js'
const route = useRoute() const route = useRoute()
@@ -103,14 +117,37 @@ const logVisible = ref(false)
const currentLog = ref('') const currentLog = ref('')
const logExecution = ref(null) const logExecution = ref(null)
// Pagination
const currentPage = ref(1)
const pageSize = ref(20)
const total = ref(0)
const totalPages = computed(() => Math.ceil(total.value / pageSize.value) || 1)
const loadExecutions = async (page) => {
try {
const res = await listExecutions(taskId, { page, page_size: pageSize.value })
const data = res.data || {}
executions.value = data.items || []
total.value = data.total || 0
currentPage.value = data.page || page
} catch (e) {
console.error(e)
executions.value = []
}
}
const goPage = (p) => {
if (p < 1 || p > totalPages.value) return
loadExecutions(p)
}
onMounted(async () => { onMounted(async () => {
try { try {
const [taskRes, execRes] = await Promise.all([ const [taskRes] = await Promise.all([
getTask(taskId), getTask(taskId),
listExecutions(taskId, { limit: 100 }),
]) ])
task.value = taskRes.data task.value = taskRes.data
executions.value = execRes.data await loadExecutions(1)
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} }
@@ -243,4 +280,40 @@ const showLog = (row) => {
word-break: break-all; word-break: break-all;
margin: 0; margin: 0;
} }
/* ── Pagination ──────────────────────────── */
.pagination {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
border-top: 1px solid var(--border-color);
}
.page-info {
font-size: 12px;
color: var(--text-muted);
}
.page-btns {
display: flex;
align-items: center;
gap: 4px;
}
.page-btn {
padding: 4px 10px;
border-radius: 6px;
border: 1px solid var(--border-color);
background: transparent;
color: var(--text-secondary);
font-size: 12px;
cursor: pointer;
transition: all 0.15s;
}
.page-btn:hover { border-color: var(--accent-1); color: var(--accent-1); }
.page-btn.active {
background: var(--accent-1);
border-color: var(--accent-1);
color: #fff;
}
.page-btn:disabled { opacity: 0.3; cursor: not-allowed; }
.page-ellipsis { color: var(--text-muted); font-size: 12px; padding: 0 2px; }
</style> </style>