Push scheduled-job failure summaries to WhatsApp and clarify pending access.

Enqueue a short English failure notice on job crashes, default scheduler lang to en, and tell unauthorized users their request id is pending admin YES.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-08-10 22:59:40 +08:00
parent d3f621b58b
commit 4646170743
6 changed files with 186 additions and 9 deletions

View file

@ -500,7 +500,7 @@ def handle_whatsapp_access(
{
"channel": "whatsapp",
"chat_id": inbound.external_chat_id,
"text": denied_reply_text(lang=lang),
"text": denied_reply_text(lang=lang, pending_id=str(pending_id or "")),
"attachments": [],
"metadata": reply_meta,
}

View file

@ -343,9 +343,21 @@ def coerce_whatsapp_access_target(value: str) -> str:
return normalize_whatsapp_target(normalize_whatsapp_phone(value))
def denied_reply_text(*, lang: str) -> str:
def denied_reply_text(*, lang: str, pending_id: str = "") -> str:
pid = str(pending_id or "").strip()
if str(lang or "").strip().lower().startswith("zh"):
if pid:
return (
f"访问申请已提交(编号 {pid})。管理员同意后即可使用;"
"请稍候,无需重复发送相同请求。"
)
return "无权限:您尚未获得使用此助手的授权。请联系管理员。"
if pid:
return (
f"Access pending (request {pid}): an administrator was notified. "
"You can use the assistant after they approve with YES. "
"No need to resend the same request."
)
return "Access denied: you are not authorized to use this assistant. Please contact an administrator."

View file

@ -76,7 +76,7 @@ def enqueue_scheduled_job_run(
trace_id = uuid.uuid4().hex
agent_run_id = uuid.uuid4().hex
prompt_text = str(getattr(job, "prompt_text", "") or "").strip()
lang = str(getattr(job, "lang", "") or "zh")
lang = str(getattr(job, "lang", "") or "en")
recipe = load_recipe_from_job(job)
playbook = recipe_has_playbook(recipe)
user_text = build_scheduled_turn_instruction(

View file

@ -5,13 +5,34 @@ from typing import Any
from runtime.scheduler.recipe import compile_playbook_instruction, recipe_has_playbook
def format_scheduled_user_reminder(prompt_text: str) -> str:
def format_scheduled_user_reminder(prompt_text: str, *, lang: str = "en") -> str:
body = str(prompt_text or "").strip()
if not body:
return ""
if body.startswith("⏰"):
return body
return f"⏰ 提醒:{body}"
if str(lang or "").lower().startswith("zh"):
return f"⏰ 提醒:{body}"
return f"⏰ Reminder: {body}"
def format_scheduled_failure_summary(
*,
job_name: str = "",
job_id: str = "",
error: str = "",
lang: str = "en",
) -> str:
"""Short user-facing failure notice for WhatsApp/WeChat scheduled delivery."""
name = str(job_name or "").strip() or str(job_id or "").strip() or "scheduled job"
err = " ".join(str(error or "").strip().split())
if len(err) > 240:
err = err[:237] + "..."
if not err:
err = "unknown error"
if str(lang or "").lower().startswith("zh"):
return f"[定时任务失败] {name}\n错误:{err}\n请检查任务配置或手动重试。"
return f"[Scheduled job failed] {name}\nError: {err}\nCheck the job config or retry manually."
def build_scheduled_turn_instruction(
@ -71,6 +92,7 @@ def scheduled_turn_system_suffix(*, lang: str, playbook: bool = False) -> str:
__all__ = [
"build_scheduled_turn_instruction",
"format_scheduled_failure_summary",
"format_scheduled_user_reminder",
"scheduled_turn_system_suffix",
]

View file

@ -6,7 +6,7 @@ from typing import Any
from runtime.orchestration.group_ingest import is_nonsend_channel_reply_text
from runtime.scheduler.turn_text import format_scheduled_user_reminder
from runtime.scheduler.turn_text import format_scheduled_failure_summary, format_scheduled_user_reminder
def resolve_scheduled_outbound_text(*, payload: dict[str, Any], reply_text: str) -> str:
@ -15,7 +15,8 @@ def resolve_scheduled_outbound_text(*, payload: dict[str, Any], reply_text: str)
return text
prompt = str(payload.get("prompt_text") or "").strip()
if prompt:
return format_scheduled_user_reminder(prompt)
lang = str(payload.get("lang") or "en")
return format_scheduled_user_reminder(prompt, lang=lang)
return ""
@ -158,9 +159,43 @@ def finalize_scheduled_turn_failure(
payload: dict[str, Any],
error: str,
) -> None:
from runtime.scheduler.channel_delivery import deliver_scheduled_reply
tenant_id = str(payload.get("tenant_id") or "")
job_id = str(payload.get("job_id") or "")
scheduled_run_id = str(payload.get("run_id_scheduled") or "")
err = str(error or "")[:500]
job = store.scheduled_job_get(job_id=job_id, tenant_id=tenant_id) if job_id else None
delivery_json = ""
if job:
delivery_json = str(getattr(job, "delivery_json", "") or "")
if not delivery_json:
delivery = payload.get("delivery") if isinstance(payload.get("delivery"), dict) else {}
delivery_json = json.dumps(delivery, ensure_ascii=False) if delivery else ""
lang = str(payload.get("lang") or getattr(job, "lang", "") or "en")
summary = format_scheduled_failure_summary(
job_name=str(getattr(job, "name", "") or ""),
job_id=job_id,
error=err,
lang=lang,
)
delivery_status: dict[str, Any] = {}
try:
delivery_status = deliver_scheduled_reply(
store,
tenant_id=tenant_id,
reply_text=summary,
delivery_json=delivery_json,
resolved_channel=str(payload.get("resolved_channel") or ""),
resolved_chat_id=str(payload.get("resolved_chat_id") or ""),
resolved_account_id=str(payload.get("resolved_account_id") or ""),
session_id=str(payload.get("session_id") or ""),
turn_uuid="",
)
except Exception as exc:
delivery_status = {"ok": False, "error": f"{type(exc).__name__}: {exc}"}
if scheduled_run_id:
store.scheduled_job_run_update(
run_id=scheduled_run_id,
@ -168,11 +203,13 @@ def finalize_scheduled_turn_failure(
patch={
"status": "failed",
"finished_at": datetime.now(timezone.utc).isoformat(),
"error": str(error or "")[:500],
"error": err,
"reply_text": summary,
"delivery_status": delivery_status,
"session_id": str(payload.get("session_id") or ""),
},
)
if job_id:
job = store.scheduled_job_get(job_id=job_id, tenant_id=tenant_id)
pause_after = bool(job and str(getattr(job, "schedule_kind", "") or "") == "once")
store.scheduled_job_mark_run(
job_id=job_id,