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

183
lib/notify.py Normal file
View File

@@ -0,0 +1,183 @@
"""
飞书 / Lark Markdown 通知 + 任务结果汇总。
内置 lark-notice-api 与 mengya-mail-api无需外部 subprocess。
"""
from __future__ import annotations
import os
import sys
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import TYPE_CHECKING, Callable
if TYPE_CHECKING:
from runner import TaskContext
CRON_FEISHU_WEBHOOK_DEFAULT = (
"https://open.feishu.cn/open-apis/bot/v2/hook/1b13f977-f848-4a42-8e43-1b16ad592a34"
)
_VENDOR_ROOT = Path(__file__).resolve().parent / "vendor"
_LARK_SRC = _VENDOR_ROOT / "lark-notice-api" / "src"
_MAIL_SRC = _VENDOR_ROOT / "mengya-mail-api" / "src"
_MAIL_ENV_DEFAULT = _VENDOR_ROOT / "mengya-mail-api" / ".env"
def _ensure_vendor_imports() -> None:
for src in (_LARK_SRC, _MAIL_SRC):
text = str(src)
if src.is_dir() and text not in sys.path:
sys.path.insert(0, text)
@dataclass
class TaskResult:
"""任务执行结果,汇总 ok / skip / fail 三类条目。"""
ok: list[str] = field(default_factory=list)
skip: list[str] = field(default_factory=list)
fail: list[str] = field(default_factory=list)
def add_ok(self, item: str) -> None:
self.ok.append(item)
def add_skip(self, item: str) -> None:
self.skip.append(item)
def add_fail(self, item: str) -> None:
self.fail.append(item)
@property
def success(self) -> bool:
return not self.fail
@property
def status_cn(self) -> str:
return "成功" if self.success else "失败"
@property
def total(self) -> int:
return len(self.ok) + len(self.skip) + len(self.fail)
def markdown_list(items: list[str]) -> str:
"""将多行条目格式化为 Markdown 列表。"""
if not items:
return "- 无"
return "\n".join(f"- {x}" for x in items if x)
def markdown_fields(items: list[tuple[str, object]]) -> str:
"""将键值对格式化为更简洁的 Markdown 列表。"""
if not items:
return "- 无"
lines: list[str] = []
for key, value in items:
text = "-" if value is None or value == "" else str(value)
lines.append(f"- **{key}**{text}")
return "\n".join(lines)
def send_task_summary(
ctx: "TaskContext",
result: TaskResult,
start_time: str,
*,
log: Callable[[str], None],
extra_fields: list[tuple[str, object]] | None = None,
) -> None:
"""发送标准任务汇总通知(飞书 + 邮件降级)。"""
from runner import task_output_prefix
end_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
subject = f"{task_output_prefix(ctx.task_id)} {result.status_cn} {end_time}"
fields: list[tuple[str, object]] = [
("任务", ctx.task_id),
("开始", start_time),
("结束", end_time),
]
if extra_fields:
fields.extend(extra_fields)
fields.extend([
("总数", result.total),
("成功", len(result.ok)),
("跳过", len(result.skip)),
("失败", len(result.fail)),
])
md = "\n".join([
markdown_fields(fields),
"", "**成功列表**", markdown_list(result.ok),
"", "**跳过列表**", markdown_list(result.skip),
"", "**失败列表**", markdown_list(result.fail),
])
send_feishu_markdown(subject, md, log=log)
def send_feishu_markdown(
title: str,
markdown: str,
*,
log: Callable[[str], None],
) -> None:
"""发送飞书 Markdown失败时自动降级为邮件通知"""
if not title or not markdown:
log("飞书通知内容为空,跳过。")
return
webhook = os.environ.get("LARK_NOTICE_WEBHOOK", CRON_FEISHU_WEBHOOK_DEFAULT)
if not webhook:
log("未配置飞书 webhook跳过飞书通知。")
return
_ensure_vendor_imports()
from lark_notice_api.client import LarkNoticeError, LarkWebhookClient
try:
LarkWebhookClient(webhook).send_markdown(title, markdown)
return
except LarkNoticeError as exc:
log(f"发送飞书通知失败:{exc}")
except ImportError:
log("发送飞书通知失败:内置 lark-notice-api 不可用。")
log("飞书通知失败,尝试通过邮件发送…")
_send_mail_fallback(title, markdown, log)
def _send_mail_fallback(title: str, markdown: str, log: Callable[[str], None]) -> None:
"""通过内置 mengya-mail-api 发送邮件(失败仅记日志)。"""
enabled = os.environ.get("CRON_MAIL_ENABLED", "0")
if enabled != "1":
log("邮件通知未开启export CRON_MAIL_ENABLED=1 可启用),跳过。")
return
env_file = os.environ.get("MAIL_API_ENV_FILE", str(_MAIL_ENV_DEFAULT))
if env_file and os.path.isfile(env_file):
os.environ.setdefault("MENGYA_MAIL_ENV_FILE", env_file)
mail_to = os.environ.get("MAIL_TO", "mail@smyhub.com")
from_name = os.environ.get("MAIL_FROM_NAME", "cron")
_ensure_vendor_imports()
from mengya_mail_api.config import ConfigError, MailConfig
from mengya_mail_api.email_client import MailClient, MailClientError
try:
config = MailConfig.from_env()
MailClient(config).send_email(
to=mail_to,
subject=title,
html_body=markdown,
from_name=from_name,
)
log("邮件通知发送成功。")
except ConfigError as exc:
log(f"邮件配置错误:{exc}")
except MailClientError as exc:
log(f"邮件通知发送失败:{exc}")
except ImportError:
log("邮件通知发送失败:内置 mengya-mail-api 不可用。")