feat: 多语言任务、WebUI 增强与 Agent MCP 集成

重构 lib 为扁平模块并支持 Windows schtasks;新增 JS/Bash/PowerShell 模板、WebUI 调度编辑,以及 Cursor Skill 与 MCP 工具供 Agent 管理定时任务。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-24 21:45:25 +08:00
parent 6c2db2dfa3
commit c10cacd5c6
59 changed files with 3989 additions and 800 deletions

11
.cursor/mcp.json Normal file
View File

@@ -0,0 +1,11 @@
{
"mcpServers": {
"sproutclaw-cron": {
"command": "python",
"args": ["mcp-server/server.py"],
"env": {
"SPROUTCLAW_CRON_ROOT": "${workspaceFolder}"
}
}
}
}

View File

@@ -0,0 +1,88 @@
---
name: sproutclaw-cron
description: >-
Manage SproutClaw Cron scheduled tasks: list/enable/disable/run tasks, read logs,
update schedules, create tasks from templates. Use when the user mentions cronctl,
sproutclaw-cron, scheduled tasks, cron jobs, task enable/disable, or creating
new cron tasks. Prefer MCP tools (cron_*) when available; fall back to cronctl.py CLI.
---
# SproutClaw Cron 定时任务技能
## 何时启用
用户提到:`cronctl`、定时任务、开关任务、试跑任务、新建 cron 任务、`sproutclaw-cron`、WebUI 管理面板,或需要查看任务日志/调度时。
## 优先顺序
1. **MCP 工具**(已配置 `.cursor/mcp.json``cron_list_tasks``cron_get_task``cron_run_task`
2. **CLI 兜底**`python cronctl.py <action> [task-id]`
3. **WebUI**`start-webui.bat`Windows`webui/start.sh`Linux
## 项目路径
- 根目录:仓库根(`cronctl.py` 所在目录)
- 可通过环境变量 `SPROUTCLAW_CRON_ROOT` 覆盖
- Linux 生产路径示例:`/shumengya/project/agent/sproutclaw-cron`
## MCP 工具速查
| 场景 | 工具 |
|------|------|
| 看有哪些任务 | `cron_list_tasks` |
| 看单个任务详情 | `cron_get_task` |
| 开/关/切换 | `cron_enable_task` / `cron_disable_task` / `cron_toggle_task` |
| 试跑 | `cron_run_task``cron_get_log` |
| 改调度 | `cron_update_schedule` |
| 新建任务 | `cron_list_templates``cron_create_task` |
| 同步系统 cron | `cron_sync_cron` |
## 新建任务流程
1. 确认 `task_id` 命名:`<主机名>-<功能描述>`(如 `smallmengya-gitea-repo-sync`
2. `cron_create_task(task_id=..., runtime="python")`**默认禁用**
3. 编辑 `<task-id>/run.py`(或对应入口)与 `schedule.cron`
4. `cron_run_task` 试跑,`cron_get_log` 确认
5. 用户明确要求后再 `cron_enable_task`
模板与约定详见根目录 [AGENTS.md](../../AGENTS.md)。
## CLI 兜底命令
```bash
python cronctl.py status
python cronctl.py status <task-id>
python cronctl.py enable|disable|toggle <task-id>
python cronctl.py run <task-id>
python cronctl.py sync-cron <task-id>
python cronctl.py enable all
```
Windows 将 `python` 换为实际 Python 路径Linux 可用 `python3`
## Agent 原则
- **默认新建任务保持禁用**,除非用户明确要求启用
- **不要修改** `_template*` 示例目录(除非用户要求)
- **schedule.cron** 调度入口保持 `cronctl.py run <task-id>` 形式
- Python 任务保留 `task_is_disabled` / `task_logging` / `acquire_cron_lock` 结构
- 非 Python 任务只改入口脚本;锁/日志/disable 由 `cronctl run` 统一处理
- 改完业务逻辑后先 `run``enable`
## 任务目录结构
```
<task-id>/
├── task.json # 可选runtime + entry + tags
├── run.py|run.js|run.sh|run.ps1
├── schedule.cron
├── logs/<task-id>.log
└── *.json # 可选配置
```
禁用任务位于 `.disabled/<task-id>/`
## 附加资源
- CLI 与 API 详情:[reference.md](reference.md)
- 环境检查脚本:[scripts/check_cron.py](scripts/check_cron.py)

View File

@@ -0,0 +1,83 @@
# SproutClaw Cron 参考
## cronctl.py 子命令
| 子命令 | 说明 | 示例 |
|--------|------|------|
| `status` | 列出或查看任务状态 | `cronctl.py status` |
| `enable` | 启用并同步 cron | `cronctl.py enable my-task` |
| `disable` | 禁用(移入 `.disabled/` | `cronctl.py disable my-task` |
| `toggle` | 切换状态 | `cronctl.py toggle my-task` |
| `run` | 同步执行一次 | `cronctl.py run my-task` |
| `sync-cron` | 仅同步 cron.d / schtasks | `cronctl.py sync-cron my-task` |
别名:`on``enable``off``disable`。批量操作传 `all`
## task.json
```json
{
"runtime": "python",
"entry": "run.py",
"tags": ["标签1", "标签2"]
}
```
`runtime``python` | `javascript` | `bash` | `powershell`
## schedule.cron 格式
- 首行注释为任务描述WebUI / MCP 会读取)
- cron 五段 + 用户 + 命令
- 命令应调用:`python3 <CRON_ROOT>/cronctl.py run <task-id>`
示例:
```cron
# 每天 08:00 同步仓库
0 8 * * * root /usr/bin/python3 /path/to/sproutclaw-cron/cronctl.py run my-task >/dev/null 2>&1
```
## WebUI APIFastAPI端口见 start 脚本)
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/tasks` | 任务列表 |
| GET | `/api/tasks/{id}` | 任务详情 |
| POST | `/api/tasks/{id}/enable` | 启用 |
| POST | `/api/tasks/{id}/disable` | 禁用 |
| POST | `/api/tasks/{id}/run` | 后台试跑 |
| GET | `/api/tasks/{id}/log?lines=200` | 日志 |
| PATCH | `/api/tasks/{id}/schedule` | 更新调度 |
## 语言模板
| 模板目录 | runtime | 入口 |
|----------|---------|------|
| `_template` | python | `run.py` |
| `_template-javascript` | javascript | `run.js` |
| `_template-bash` | bash | `run.sh` |
| `_template-powershell` | powershell | `run.ps1` |
## 公共库Python 任务)
`lib/runner.py` 导入:
- `TaskContext.from_task_id(task_id)`
- `task_is_disabled(ctx)` / `task_logging(ctx)` / `acquire_cron_lock(...)`
`lib/notify.py` 导入:
- `TaskResult``send_task_summary(...)`
## 开关机制
- 启用:`.disabled/<id>/``<id>/`
- 禁用:`<id>/``.disabled/<id>/`
- 禁用后 cron 仍可触发,但 `cronctl run` 会直接跳过
## MCP 服务器
- 入口:`mcp-server/server.py`
- 配置:`.cursor/mcp.json`
- 依赖:`pip install -r mcp-server/requirements.txt`

View File

@@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""SproutClaw Cron 环境检查(跨平台,仅标准库 + 可选 fastmcp"""
from __future__ import annotations
import json
import subprocess
import sys
from pathlib import Path
def main() -> int:
root = Path(__file__).resolve().parents[4]
lib = root / "lib"
cronctl = root / "cronctl.py"
mcp_server = root / "mcp-server" / "server.py"
checks: list[dict[str, object]] = []
def add(name: str, ok: bool, detail: str = "") -> None:
checks.append({"name": name, "ok": ok, "detail": detail})
add("cron_root", root.is_dir(), str(root))
add("cronctl.py", cronctl.is_file(), str(cronctl))
add("lib/", lib.is_dir(), str(lib))
add("mcp-server/server.py", mcp_server.is_file(), str(mcp_server))
try:
import fastmcp # noqa: F401
add("fastmcp", True, "已安装")
except ImportError:
add("fastmcp", False, "pip install -r mcp-server/requirements.txt")
if cronctl.is_file():
proc = subprocess.run(
[sys.executable, str(cronctl), "status"],
cwd=str(root),
capture_output=True,
text=True,
timeout=30,
)
add(
"cronctl status",
proc.returncode == 0,
(proc.stdout or proc.stderr).strip()[:500],
)
ok_all = all(c["ok"] for c in checks if c["name"] != "fastmcp")
print(json.dumps({"ok": ok_all, "checks": checks}, ensure_ascii=False, indent=2))
return 0 if ok_all else 1
if __name__ == "__main__":
raise SystemExit(main())