mirror of
https://github.com/langgenius/dify.git
synced 2026-05-04 01:18:05 +08:00
WIP: huamninput email sending
This commit is contained in:
177
api/tasks/mail_human_input_delivery_task.py
Normal file
177
api/tasks/mail_human_input_delivery_task.py
Normal file
@ -0,0 +1,177 @@
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import click
|
||||
from celery import shared_task
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from configs import dify_config
|
||||
from core.workflow.nodes.human_input.entities import EmailDeliveryConfig, EmailDeliveryMethod
|
||||
from extensions.ext_database import db
|
||||
from extensions.ext_mail import mail
|
||||
from libs.email_template_renderer import render_email_template
|
||||
from models.human_input import (
|
||||
DeliveryMethodType,
|
||||
HumanInputDelivery,
|
||||
HumanInputForm,
|
||||
HumanInputFormRecipient,
|
||||
RecipientType,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _EmailRecipient:
|
||||
email: str
|
||||
token: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _EmailDeliveryJob:
|
||||
form_id: str
|
||||
workflow_run_id: str
|
||||
subject: str
|
||||
body: str
|
||||
form_content: str
|
||||
recipients: list[_EmailRecipient]
|
||||
|
||||
|
||||
def _build_form_link(token: str | None) -> str | None:
|
||||
if not token:
|
||||
return None
|
||||
base_url = dify_config.CONSOLE_WEB_URL
|
||||
if not base_url:
|
||||
return None
|
||||
return f"{base_url.rstrip('/')}/api/form/human_input/{token}"
|
||||
|
||||
|
||||
def _parse_recipient_payload(payload: str) -> tuple[str | None, RecipientType | None]:
|
||||
try:
|
||||
payload_dict: dict[str, Any] = json.loads(payload)
|
||||
except Exception:
|
||||
logger.exception("Failed to parse recipient payload")
|
||||
return None, None
|
||||
|
||||
return payload_dict.get("email"), payload_dict.get("TYPE")
|
||||
|
||||
|
||||
def _load_email_jobs(session: Session, form_id: str) -> list[_EmailDeliveryJob]:
|
||||
form = session.get(HumanInputForm, form_id)
|
||||
if form is None:
|
||||
logger.warning("Human input form not found, form_id=%s", form_id)
|
||||
return []
|
||||
|
||||
deliveries = session.scalars(
|
||||
select(HumanInputDelivery).where(
|
||||
HumanInputDelivery.form_id == form_id,
|
||||
HumanInputDelivery.delivery_method_type == DeliveryMethodType.EMAIL,
|
||||
)
|
||||
).all()
|
||||
jobs: list[_EmailDeliveryJob] = []
|
||||
for delivery in deliveries:
|
||||
delivery_config = EmailDeliveryMethod.model_validate_json(delivery.channel_payload)
|
||||
|
||||
recipients = session.scalars(
|
||||
select(HumanInputFormRecipient).where(HumanInputFormRecipient.delivery_id == delivery.id)
|
||||
).all()
|
||||
|
||||
recipient_entities: list[_EmailRecipient] = []
|
||||
for recipient in recipients:
|
||||
email, recipient_type = _parse_recipient_payload(recipient.recipient_payload)
|
||||
if recipient_type not in {RecipientType.EMAIL_MEMBER, RecipientType.EMAIL_EXTERNAL}:
|
||||
continue
|
||||
if not email:
|
||||
continue
|
||||
token = recipient.access_token
|
||||
if not token:
|
||||
continue
|
||||
recipient_entities.append(_EmailRecipient(email=email, token=token))
|
||||
|
||||
if not recipient_entities:
|
||||
continue
|
||||
|
||||
jobs.append(
|
||||
_EmailDeliveryJob(
|
||||
form_id=form_id,
|
||||
workflow_run_id=form.workflow_run_id,
|
||||
subject=delivery_config.config.subject,
|
||||
body=delivery_config.config.body,
|
||||
form_content=form.rendered_content,
|
||||
recipients=recipient_entities,
|
||||
)
|
||||
)
|
||||
return jobs
|
||||
|
||||
|
||||
def _render_subject(subject_template: str, substitutions: dict[str, str]) -> str:
|
||||
return render_email_template(subject_template, substitutions)
|
||||
|
||||
|
||||
def _render_body(body_template: str, substitutions: dict[str, str]) -> str:
|
||||
templated_body = EmailDeliveryConfig.replace_url_placeholder(body_template, substitutions.get("form_link"))
|
||||
return render_email_template(templated_body, substitutions)
|
||||
|
||||
|
||||
def _build_substitutions(
|
||||
*,
|
||||
job: _EmailDeliveryJob,
|
||||
recipient: _EmailRecipient,
|
||||
node_title: str | None,
|
||||
) -> dict[str, str]:
|
||||
raw_values: dict[str, str | None] = {
|
||||
"form_id": job.form_id,
|
||||
"workflow_run_id": job.workflow_run_id,
|
||||
"node_title": node_title,
|
||||
"form_token": recipient.token,
|
||||
"form_link": _build_form_link(recipient.token),
|
||||
"form_content": job.form_content,
|
||||
"recipient_email": recipient.email,
|
||||
}
|
||||
return {key: value or "" for key, value in raw_values.items()}
|
||||
|
||||
|
||||
def _open_session(session_factory: sessionmaker | Session | None):
|
||||
if session_factory is None:
|
||||
return Session(db.engine)
|
||||
if isinstance(session_factory, Session):
|
||||
return session_factory
|
||||
return session_factory()
|
||||
|
||||
|
||||
@shared_task(queue="mail")
|
||||
def dispatch_human_input_email_task(form_id: str, node_title: str | None = None, session_factory=None):
|
||||
if not mail.is_inited():
|
||||
return
|
||||
|
||||
logger.info(click.style(f"Start human input email delivery for form {form_id}", fg="green"))
|
||||
start_at = time.perf_counter()
|
||||
|
||||
try:
|
||||
with _open_session(session_factory) as session:
|
||||
jobs = _load_email_jobs(session, form_id)
|
||||
|
||||
for job in jobs:
|
||||
for recipient in job.recipients:
|
||||
substitutions = _build_substitutions(job=job, recipient=recipient, node_title=node_title)
|
||||
subject = _render_subject(job.subject, substitutions)
|
||||
body = _render_body(job.body, substitutions)
|
||||
|
||||
mail.send(
|
||||
to=recipient.email,
|
||||
subject=subject,
|
||||
html=body,
|
||||
)
|
||||
|
||||
end_at = time.perf_counter()
|
||||
logger.info(
|
||||
click.style(
|
||||
f"Human input email delivery succeeded for form {form_id}: latency: {end_at - start_at}", fg="green"
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Send human input email failed, form_id=%s", form_id)
|
||||
@ -5,42 +5,15 @@ from typing import Any
|
||||
|
||||
import click
|
||||
from celery import shared_task
|
||||
from flask import render_template_string
|
||||
from jinja2.runtime import Context
|
||||
from jinja2.sandbox import ImmutableSandboxedEnvironment
|
||||
|
||||
from configs import dify_config
|
||||
from configs.feature import TemplateMode
|
||||
from extensions.ext_mail import mail
|
||||
from libs.email_template_renderer import render_email_template
|
||||
from libs.email_i18n import get_email_i18n_service
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SandboxedEnvironment(ImmutableSandboxedEnvironment):
|
||||
def __init__(self, timeout: int, *args: Any, **kwargs: Any):
|
||||
self._timeout_time = time.time() + timeout
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def call(self, context: Context, obj: Any, *args: Any, **kwargs: Any) -> Any:
|
||||
if time.time() > self._timeout_time:
|
||||
raise TimeoutError("Template rendering timeout")
|
||||
return super().call(context, obj, *args, **kwargs)
|
||||
|
||||
|
||||
def _render_template_with_strategy(body: str, substitutions: Mapping[str, str]) -> str:
|
||||
mode = dify_config.MAIL_TEMPLATING_MODE
|
||||
timeout = dify_config.MAIL_TEMPLATING_TIMEOUT
|
||||
if mode == TemplateMode.UNSAFE:
|
||||
return render_template_string(body, **substitutions)
|
||||
if mode == TemplateMode.SANDBOX:
|
||||
tmpl = SandboxedEnvironment(timeout=timeout).from_string(body)
|
||||
return tmpl.render(substitutions)
|
||||
if mode == TemplateMode.DISABLED:
|
||||
return body
|
||||
raise ValueError(f"Unsupported mail templating mode: {mode}")
|
||||
|
||||
|
||||
@shared_task(queue="mail")
|
||||
def send_inner_email_task(to: list[str], subject: str, body: str, substitutions: Mapping[str, str]):
|
||||
if not mail.is_inited():
|
||||
@ -50,7 +23,7 @@ def send_inner_email_task(to: list[str], subject: str, body: str, substitutions:
|
||||
start_at = time.perf_counter()
|
||||
|
||||
try:
|
||||
html_content = _render_template_with_strategy(body, substitutions)
|
||||
html_content = render_email_template(body, substitutions)
|
||||
|
||||
email_service = get_email_i18n_service()
|
||||
email_service.send_raw_email(to=to, subject=subject, html_content=html_content)
|
||||
|
||||
Reference in New Issue
Block a user