# -*- coding: utf-8 -*- """ 通知渠道:飞书/企微群机器人 Webhook + 邮件(可选 SMTP) 事件:task_review(待审核)/ task_done(完成)/ task_failed(失败) budget_alert(预算告警)/ worker_alert(Worker 异常)/ wbs_ready(规划完成) """ import json import smtplib import requests import db EVENTS = { 'task_review': '任务待审核', 'task_done': '任务完成', 'task_failed': '任务失败', 'budget_alert': '预算告警', 'worker_alert': 'Worker 异常', 'wbs_ready': '规划完成', } def _channels_for(event): rows = db.q('SELECT * FROM notify_channels WHERE enabled=1') out = [] for r in rows: try: evs = json.loads(r['events'] or '[]') except Exception: evs = [] if event in evs: out.append(r) return out def _send_feishu(ch, title, text): payload = {'msg_type': 'text', 'content': {'text': f'【AI Worker 平台】{title}\n{text}'}} r = requests.post(ch['webhook'], json=payload, timeout=10) try: body = r.json() if body.get('code', 0) not in (0, None): raise ValueError(f'飞书返回错误: {body.get("msg", r.text[:100])}') except ValueError: raise except Exception: pass return r def _send_wecom(ch, title, text): payload = {'msgtype': 'text', 'text': {'content': f'【AI Worker 平台】{title}\n{text}'}} return requests.post(ch['webhook'], json=payload, timeout=10) def _send_email(ch, title, text): from config import EMAIL # 邮件配置在 config.py if not EMAIL.get('host'): return None from email.mime.text import MIMEText from email.utils import formataddr, formatdate, make_msgid msg = MIMEText(text, 'plain', 'utf-8') msg['From'] = formataddr((EMAIL.get('from_name', 'AI Worker 平台'), EMAIL['user'])) msg['To'] = ch['email'] msg['Subject'] = f'【AI Worker 平台】{title}' msg['Date'] = formatdate(localtime=True) msg['Message-ID'] = make_msgid(domain='tphai.com') try: s = smtplib.SMTP(EMAIL['host'], EMAIL['port'], timeout=30) if EMAIL.get('starttls'): s.starttls() if EMAIL.get('user'): s.login(EMAIL['user'], EMAIL['password']) s.sendmail(EMAIL['user'], [ch['email']], msg.as_string()) s.quit() return True except Exception as e: return f'邮件发送失败: {e}' def notify(event, title, text, save_alert=True, level='info', atype=None): """触发事件:写告警记录 + 推送所有订阅渠道""" if save_alert: db.w('INSERT INTO alerts (type, level, title, detail, read, created_at) ' 'VALUES (?,?,?,?,0,?)', (atype or event, level, title, text[:500], db.now())) ok, fail = [], [] for ch in _channels_for(event): try: if ch['type'] == 'feishu': r = _send_feishu(ch, title, text) (ok if r.status_code == 200 else fail).append(f'{ch["name"]}({r.status_code})') elif ch['type'] == 'wecom': r = _send_wecom(ch, title, text) (ok if r.status_code == 200 else fail).append(f'{ch["name"]}({r.status_code})') elif ch['type'] == 'email': r = _send_email(ch, title, text) (ok if r is True else fail).append(f'{ch["name"]}({r})') except Exception as e: fail.append(f'{ch["name"]}({e})') return {'ok': ok, 'fail': fail} def test_channel(ch): """渠道连通性测试""" text = '这是一条来自 AI Worker 项目管理平台的连通性测试消息 ✅' try: if ch['type'] == 'feishu': r = _send_feishu(ch, '连通测试', text) return r.status_code == 200, f'HTTP {r.status_code}' if ch['type'] == 'wecom': r = _send_wecom(ch, '连通测试', text) return r.status_code == 200, f'HTTP {r.status_code}' if ch['type'] == 'email': r = _send_email(ch, '连通测试', text) return r is True, str(r) except Exception as e: return False, str(e) return False, '未知渠道类型'