Files
sproutclaw-cron/lib/notify.py
shumengya c10cacd5c6 feat: 多语言任务、WebUI 增强与 Agent MCP 集成
重构 lib 为扁平模块并支持 Windows schtasks;新增 JS/Bash/PowerShell 模板、WebUI 调度编辑,以及 Cursor Skill 与 MCP 工具供 Agent 管理定时任务。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 21:45:25 +08:00

184 lines
5.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
飞书 / 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 不可用。")