diff --git a/api/monitor/models.py b/api/monitor/models.py index 6b50479..596d52d 100644 --- a/api/monitor/models.py +++ b/api/monitor/models.py @@ -107,9 +107,15 @@ class MonitorTask(MonitorBase): max_comments_count: Mapped[int] = mapped_column(Integer, nullable=False, default=50) run_timeout_seconds: Mapped[int] = mapped_column(Integer, nullable=False, default=3600) - # Push notifications are opt-in per task. A task list that all pushes to one - # webhook turns noisy fast, so silence is the default. + # 通知分成两类,因为它们的性质完全不同: + # + # * `notify_enabled` —— **推送新作品**。可能每轮都有,一条任务列表都推到同一个群 + # 会很快变吵,所以默认关。(列名是历史遗留:它早先是唯一的通知开关。) + # * `notify_failures` —— **推送异常**(登录失效 / 运行失败 / 没抓到数据)。频率低, + # 而且一旦发生就意味着这个任务从此**默默采不到任何东西**,你会一直不知道, + # 直到某天发现数据停在几周前。这正是最该被告知的情况,所以默认**开**。 notify_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) + notify_failures: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) # Scheduler state. Persisted so the schedule survives an API restart. next_run_at: Mapped[Optional[int]] = mapped_column(BigInteger, index=True) diff --git a/api/monitor/notify.py b/api/monitor/notify.py index 67d462a..608543a 100644 --- a/api/monitor/notify.py +++ b/api/monitor/notify.py @@ -107,14 +107,27 @@ async def build_run_message( task: MonitorTask, run: MonitorRun, ) -> Optional[str]: - """Compose one markdown summary for a finished run, or None if nothing to say.""" + """Compose one markdown summary for a finished run, or None if nothing to say. + + 事件按开关过滤:只勾了「新作品」的任务,不该因为一次失败被推消息,反之亦然 —— + 否则拆开这两个开关就没有意义了。 + """ + allowed = [] + if task.notify_enabled: + allowed.append(EVENT_NEW_NOTE) + if task.notify_failures: + allowed.extend([EVENT_AUTH_FAILURE, EVENT_RUN_FAILED, EVENT_NO_DATA]) + + if not allowed: + return None + events = list( ( await session.scalars( select(MonitorEvent) .where( MonitorEvent.run_id == run.id, - MonitorEvent.type.in_(NOTIFIABLE_EVENT_TYPES), + MonitorEvent.type.in_(allowed), ) .order_by(MonitorEvent.id) ) @@ -176,7 +189,8 @@ async def notify_run(session: AsyncSession, task: MonitorTask, run: MonitorRun) Returns the message that was sent, or None. Never raises. """ try: - if not task.notify_enabled: + # 两个开关是分开的:只开「异常」不该因为新作品而发消息,反之亦然。 + if not (task.notify_enabled or task.notify_failures): return None webhook_url = await get_webhook_url(session) diff --git a/api/monitor/service.py b/api/monitor/service.py index eaa8e04..1609a14 100644 --- a/api/monitor/service.py +++ b/api/monitor/service.py @@ -183,6 +183,7 @@ async def create_task(session: AsyncSession, payload: Dict[str, Any]) -> Monitor max_comments_count=payload.get("max_comments_count") or defaults["max_comments_count"], run_timeout_seconds=payload.get("run_timeout_seconds", 3600), notify_enabled=payload.get("notify_enabled", False), + notify_failures=payload.get("notify_failures", True), next_run_at=schedule.next_occurrence( mode=schedule_mode, interval_minutes=interval_minutes, @@ -240,6 +241,7 @@ async def update_task(session: AsyncSession, task_id: int, payload: Dict[str, An "max_comments_count", "run_timeout_seconds", "notify_enabled", + "notify_failures", ): if field in payload and payload[field] is not None: setattr(task, field, payload[field]) @@ -706,6 +708,7 @@ async def list_tasks( "max_comments_count": task.max_comments_count, "run_timeout_seconds": task.run_timeout_seconds, "notify_enabled": task.notify_enabled, + "notify_failures": task.notify_failures, "next_run_at": task.next_run_at, "last_run_at": task.last_run_at, "last_status": task.last_status, diff --git a/api/schemas/monitor.py b/api/schemas/monitor.py index f74c7a1..0ee94aa 100644 --- a/api/schemas/monitor.py +++ b/api/schemas/monitor.py @@ -60,9 +60,10 @@ class MonitorTaskCreate(BaseModel): max_comments_count: Optional[int] = Field(default=None, ge=1, le=500) run_timeout_seconds: int = Field(default=3600, ge=60, le=86400) enabled: bool = True - # Push a WeCom summary for runs that failed or found new works. Opt-in per - # task so a single webhook does not get flooded. + # 两类通知分开:新作品可能每轮都有(默认关,避免刷屏), + # 异常频率低且意味着任务已经停止工作(默认开,否则你会一直不知道)。 notify_enabled: bool = False + notify_failures: bool = True # Raw pasted values: full URLs or bare ids, in either form. targets: List[str] = Field(min_length=1) @@ -98,6 +99,7 @@ class MonitorTaskUpdate(BaseModel): max_comments_count: Optional[int] = Field(default=None, ge=1, le=500) run_timeout_seconds: Optional[int] = Field(default=None, ge=60, le=86400) notify_enabled: Optional[bool] = None + notify_failures: Optional[bool] = None # When present, replaces the whole target list. targets: Optional[List[str]] = None diff --git a/webui/src/components/monitor/TaskEditorDialog.tsx b/webui/src/components/monitor/TaskEditorDialog.tsx index 2718b12..16d5e61 100644 --- a/webui/src/components/monitor/TaskEditorDialog.tsx +++ b/webui/src/components/monitor/TaskEditorDialog.tsx @@ -118,6 +118,8 @@ export function TaskEditorDialog({ open, onOpenChange, task }: TaskEditorDialogP const [enableComments, setEnableComments] = useState(true) const [maxComments, setMaxComments] = useState('50') const [notifyEnabled, setNotifyEnabled] = useState(false) + // 异常推送默认开:失败意味着这个任务从此默默采不到东西,而你不会知道。 + const [notifyFailures, setNotifyFailures] = useState(true) const [targets, setTargets] = useState('') // Reset the form whenever the dialog is (re)opened. For a new task the @@ -142,6 +144,7 @@ export function TaskEditorDialog({ open, onOpenChange, task }: TaskEditorDialogP String(task?.max_comments_count ?? settings?.values['collect.default_max_comments'] ?? 50), ) setNotifyEnabled(task?.notify_enabled ?? false) + setNotifyFailures(task?.notify_failures ?? true) setTargets(task ? task.targets.map((t) => t.raw_value || t.external_id).join('\n') : '') }, [open, task, settings]) @@ -185,6 +188,7 @@ export function TaskEditorDialog({ open, onOpenChange, task }: TaskEditorDialogP run_timeout_seconds: 3600, enabled: true, notify_enabled: notifyEnabled, + notify_failures: notifyFailures, targets: targetList, } @@ -455,24 +459,55 @@ export function TaskEditorDialog({ open, onOpenChange, task }: TaskEditorDialogP -
- setNotifyEnabled(checked === true)} - /> -
- -

- 仅在本任务**采集失败 / 登录态失效**或**发现新作品**时推送, - 一轮只发一条汇总。需先在监控页配置 Webhook 地址。 -

+
+ {/* 两类通知的性质完全不同,所以分成两个开关: + 异常低频且意味着任务已经停止工作 —— 默认开; + 新作品可能每轮都有 —— 默认关,否则会刷屏。 */} +
+ setNotifyFailures(checked === true)} + /> +
+ +

+ 登录态失效、采集进程失败、一篇都没抓到时推送。 + + 异常意味着这个任务从此默默采不到任何东西 + + —— 关掉的话你不会知道,直到某天发现数据停在几周前。 +

+
+ +
+ setNotifyEnabled(checked === true)} + /> +
+ +

+ 发现新作品时推送。监控多个博主时可能每轮都有,容易刷屏,所以默认关闭。 +

+
+
+ +

+ 两者都是一轮只发一条汇总。需先在右上角「系统设置」里配置企业微信 Webhook 地址。 +

diff --git a/webui/src/types/monitor.ts b/webui/src/types/monitor.ts index e0353de..f2fecf5 100644 --- a/webui/src/types/monitor.ts +++ b/webui/src/types/monitor.ts @@ -56,8 +56,10 @@ export interface MonitorTask { enable_comments: boolean max_comments_count: number run_timeout_seconds: number - /** Opt-in per task so one webhook does not get flooded. */ + /** 推送**新作品**。可能每轮都有,默认关以免刷屏。 */ notify_enabled: boolean + /** 推送**异常**(登录失效/运行失败/没抓到数据)。默认开。 */ + notify_failures: boolean /** Epoch milliseconds. */ next_run_at: number | null last_run_at: number | null @@ -258,6 +260,7 @@ export interface TaskCreatePayload { run_timeout_seconds: number enabled: boolean notify_enabled: boolean + notify_failures: boolean targets: string[] }