feat: 配置中心-凭据(邮件推送AI表单/掩码交互/SMTP测试/推送探测端点)

- 后端 POST /tenants/{slug}/email/test:resolve_email_settings 当前配置
  真发测试邮件(主题 [all-kefu] SMTP 测试邮件+租户名+UTC+8 时间);
  回退后 host/user 仍空 → 400;成败如实返回 {sent, error},密钥不进响应
- 后端 POST /tenants/{slug}/push/test:连通性探测——与 _do_post 同款
  连接形态 POST 空 messages 批量(红线:绝不携带线索数据);不可达
  reason 分类 dns/timeout/connect(Windows 裸 ConnectError 文案兜底),
  auth_ok 三态(2xx 真 / 401·403 假 / 其余 null),单次无重试
- email_service 抽 _smtp_send 共用连接逻辑(_send 契约不变)+
  send_test_email(成败+错误串如实返回,不走吞异常 bool)
- 前端 CredsPane:邮件/推送/AI 三组凭据表单(数值/邮箱前端预检 +
  后端 400 双层);密钥哨兵交互(__SET__ 占位符/不动保存保持库值/
  清除按钮显式清空可撤销);SMTP 测试与推送探测三态内联展示
- 测试:pytest 新增 18(email/test 7 + push/test 11),全量 595 通过;
  vitest 新增 20(model 哨兵往返/预检边界 + API 调用形态),107 通过

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-13 20:55:28 +08:00
parent 39d72c53aa
commit bf923d9cf7
11 changed files with 2024 additions and 19 deletions
+45 -5
View File
@@ -1,5 +1,6 @@
import html
import logging
from datetime import datetime, timedelta, timezone
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
@@ -58,16 +59,23 @@ def _cc_views(contacts: dict, cc_info: dict | None) -> tuple[dict, dict]:
return body, subj
async def _smtp_send(msg: MIMEMultipart, cfg: dict) -> None:
"""底层连接发送(Phase 3 Task 7c 从 _send 抽出): 直调 aiosmtplib,
失败抛原始异常——需要错误串的调用方(配置中心测试邮件 send_test_email)
复用同一连接逻辑;_send 在此之上包"吞异常返 bool"契约,行为不变。"""
await aiosmtplib.send(
msg, hostname=cfg["host"], port=cfg["port"],
username=cfg["user"], password=cfg["password"],
use_tls=True,
)
async def _send(msg: MIMEMultipart, cfg: dict) -> bool:
if not all([cfg["host"], cfg["user"], cfg["notify_emails"]]):
logger.warning("SMTP not configured, skipping email")
return False
try:
await aiosmtplib.send(
msg, hostname=cfg["host"], port=cfg["port"],
username=cfg["user"], password=cfg["password"],
use_tls=True,
)
await _smtp_send(msg, cfg)
logger.info("Lead email sent to %s", cfg["notify_emails"])
return True
except Exception:
@@ -170,3 +178,35 @@ async def send_correction_email(
f"<table>{_contact_rows(body_contacts)}</table>"
f"</body></html>", "html", "utf-8"))
return await _send(msg, cfg)
async def send_test_email(
to: str, tenant_name: str = "",
config_data: dict | None = None,
) -> tuple[bool, str | None]:
"""配置中心测试邮件(Phase 3 Task 7c): 按当前生效邮件配置真发一封到
指定收件人——主题/正文钉契约("[all-kefu] SMTP 测试邮件"+租户名+时间,
时间与 lead_push_service 同款 +08:00 口径)。
与 _send 的吞异常 bool 契约解耦: 成败与错误串如实返回(探测端点直传
前端,凭据对不对让收件箱与错误串说话);连接逻辑复用 _smtp_send。
"SMTP 未配置"的 400 判定在端点层(本函数假定 cfg 已可用)。"""
cfg = resolve_email_settings(config_data)
now = datetime.now(timezone(timedelta(hours=8)))
msg = MIMEMultipart("alternative")
msg["Subject"] = "[all-kefu] SMTP 测试邮件"
msg["From"] = f"【{cfg['sender_name']}】 <{cfg['user']}>"
msg["To"] = to
msg.attach(MIMEText(
f"<html><body>"
f"<p>这是一封 SMTP 配置测试邮件——收到本邮件说明该租户的邮件配置"
f"可以正常发信。</p>"
f"<p>租户:{_esc(tenant_name)}</p>"
f"<p>时间:{now.strftime('%Y-%m-%d %H:%M:%S')}(UTC+8)</p>"
f"</body></html>", "html", "utf-8"))
try:
await _smtp_send(msg, cfg)
return True, None
except Exception as exc:
logger.exception("Test email to %s failed", to)
return False, repr(exc)