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