Files
ai-worker-platform/notify.py
T
hz4th_coder 7c4cbf8994 V3.4 负责人验收制+邮件修复+事件面板+真实网页交付
- 项目级验收:全部任务完成后进入「待验收」,邮件通知负责人,验收通过才算完成;公开验收链接 /review/<token> 免登录一键通过/打回(打回必填原因)
- 打回自动返工:自动生成含负责人意见的返工任务,AI主管立即重做并再次提交验收;平台内也可验收
- 真实网页交付:任务HTML产出自动落盘工作目录(剥离围栏/前置叙述),Demo展示真实页面;返工产出覆盖入口页;老项目已回填
- 事件记录面板:合并AI主管动态+任务日志+交付记录,可展开/收起(记忆状态),任务事件可点击定位
- 邮件通道修复:补 Date/Message-ID 头(amavisd 拒收 invalid header section 根因),打包附件/通知恢复送达;notify.py 改标准 MIME
- 规划重试:WBS 拆解失败自动重试3次,仍失败邮件通知负责人
- 项目状态新增 review(待验收),全端展示
2026-08-14 13:33:59 +08:00

118 lines
4.1 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.
# -*- coding: utf-8 -*-
"""
通知渠道:飞书/企微群机器人 Webhook + 邮件(可选 SMTP
事件:task_review(待审核)/ task_done(完成)/ task_failed(失败)
budget_alert(预算告警)/ worker_alertWorker 异常)/ 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, '未知渠道类型'