mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 01:50:44 +08:00
Productize scheduled success delivery, close WA pending, and clarify SQL scope grants.
Wrap successful scheduled jobs with a short WA header (+ attachment count), expire stale access pending and DM requesters on admin approve/deny, and add concrete next_steps for insufficient_scope:sql:query. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
dff8901ae3
commit
4898adf7d0
10 changed files with 334 additions and 12 deletions
|
|
@ -1052,8 +1052,12 @@ def process_inbound_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
|||
)
|
||||
channel_is_wa_bind = str(inbound.channel or "").strip().lower() == "whatsapp"
|
||||
if info:
|
||||
guide = _menu_text(channel=str(inbound.channel or ""))
|
||||
reply = ("Bound successfully.\n\n" + guide) if channel_is_wa_bind else ("绑定成功。\n\n" + guide)
|
||||
if channel_is_wa_bind:
|
||||
from runtime.extensions.whatsapp.access_control import access_granted_guide_text
|
||||
|
||||
reply = access_granted_guide_text(lang="en")
|
||||
else:
|
||||
reply = "绑定成功。\n\n" + _menu_text(channel=str(inbound.channel or ""))
|
||||
else:
|
||||
reply = (
|
||||
"Bind failed: invalid or already used code."
|
||||
|
|
|
|||
|
|
@ -264,6 +264,30 @@ def _notify_requester_access_decision(
|
|||
)
|
||||
|
||||
|
||||
def notify_whatsapp_access_decision(
|
||||
store: Any,
|
||||
*,
|
||||
tenant_id: str,
|
||||
account_id: str,
|
||||
lang: str = "en",
|
||||
external_user_id: str,
|
||||
phone: str = "",
|
||||
pending_id: str = "",
|
||||
approved: bool,
|
||||
) -> None:
|
||||
"""Public wrapper for admin/API approve paths to DM the requester."""
|
||||
_notify_requester_access_decision(
|
||||
store,
|
||||
tenant_id=tenant_id,
|
||||
account_id=account_id,
|
||||
lang=lang,
|
||||
external_user_id=external_user_id,
|
||||
phone=phone,
|
||||
pending_id=pending_id,
|
||||
approved=approved,
|
||||
)
|
||||
|
||||
|
||||
def _upsert_whatsapp_contact_profile(
|
||||
store: Any,
|
||||
*,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,40 @@ def format_scheduled_user_reminder(prompt_text: str, *, lang: str = "en") -> str
|
|||
return f"⏰ Reminder: {body}"
|
||||
|
||||
|
||||
def format_scheduled_success_summary(
|
||||
*,
|
||||
job_name: str = "",
|
||||
job_id: str = "",
|
||||
reply_text: str = "",
|
||||
attachment_count: int = 0,
|
||||
lang: str = "en",
|
||||
max_body_chars: int = 1200,
|
||||
) -> str:
|
||||
"""Short user-facing success notice; keeps body but caps length for WhatsApp."""
|
||||
name = str(job_name or "").strip() or str(job_id or "").strip() or "scheduled job"
|
||||
body = str(reply_text or "").strip()
|
||||
# Drop pure reminder fallbacks that just echo the job prompt.
|
||||
if body.startswith("⏰"):
|
||||
body = ""
|
||||
att_n = max(0, int(attachment_count or 0))
|
||||
cap = max(200, int(max_body_chars or 1200))
|
||||
if body and len(body) > cap:
|
||||
body = body[: cap - 3].rstrip() + "..."
|
||||
if str(lang or "").lower().startswith("zh"):
|
||||
head = f"[定时任务完成] {name}"
|
||||
if att_n:
|
||||
head += f"\n附件:{att_n} 个"
|
||||
if body:
|
||||
return f"{head}\n{body}"
|
||||
return head + ("\n(本次无文字摘要,请查看附件。)" if att_n else "\n(本次无摘要内容。)")
|
||||
head = f"[Scheduled job done] {name}"
|
||||
if att_n:
|
||||
head += f"\nAttachments: {att_n}"
|
||||
if body:
|
||||
return f"{head}\n{body}"
|
||||
return head + ("\n(No text summary; see attachment(s).)" if att_n else "\n(No summary content.)")
|
||||
|
||||
|
||||
def format_scheduled_failure_summary(
|
||||
*,
|
||||
job_name: str = "",
|
||||
|
|
@ -72,11 +106,14 @@ def scheduled_turn_system_suffix(*, lang: str, playbook: bool = False) -> str:
|
|||
"\n\n[Scheduled playbook mode] You are executing a recurring workflow for the user. "
|
||||
"Follow the playbook steps, use tools as needed, and deliver a useful update "
|
||||
"(including save_deliverable_attachment for generated files). "
|
||||
"Lead the final reply with a short English summary (3–8 lines: what ran, key counts, "
|
||||
"ok/failed highlights), then optional detail. "
|
||||
"Do not pretend the user just messaged you."
|
||||
)
|
||||
return (
|
||||
"\n\n【定时工作流模式】你正在执行周期性工作流。"
|
||||
"按 playbook 步骤完成任务,按需调用工具;若生成文件须 save_deliverable_attachment。"
|
||||
"最终回复先给 3–8 行摘要(做了什么、关键计数、成败),再写细节。"
|
||||
"不要假装用户刚刚发了消息,不要只回一句空提醒。"
|
||||
)
|
||||
if is_en:
|
||||
|
|
@ -93,6 +130,7 @@ def scheduled_turn_system_suffix(*, lang: str, playbook: bool = False) -> str:
|
|||
__all__ = [
|
||||
"build_scheduled_turn_instruction",
|
||||
"format_scheduled_failure_summary",
|
||||
"format_scheduled_success_summary",
|
||||
"format_scheduled_user_reminder",
|
||||
"scheduled_turn_system_suffix",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -6,7 +6,11 @@ from typing import Any
|
|||
|
||||
|
||||
from runtime.orchestration.group_ingest import is_nonsend_channel_reply_text
|
||||
from runtime.scheduler.turn_text import format_scheduled_failure_summary, format_scheduled_user_reminder
|
||||
from runtime.scheduler.turn_text import (
|
||||
format_scheduled_failure_summary,
|
||||
format_scheduled_success_summary,
|
||||
format_scheduled_user_reminder,
|
||||
)
|
||||
|
||||
|
||||
def resolve_scheduled_outbound_text(*, payload: dict[str, Any], reply_text: str) -> str:
|
||||
|
|
@ -101,11 +105,16 @@ def finalize_scheduled_turn_success(
|
|||
payload: dict[str, Any],
|
||||
base_result: dict[str, Any],
|
||||
) -> None:
|
||||
from runtime.scheduler.channel_delivery import deliver_scheduled_reply
|
||||
from runtime.scheduler.channel_delivery import (
|
||||
_collect_scheduled_turn_attachments,
|
||||
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 "")
|
||||
session_id = str(payload.get("session_id") or "")
|
||||
turn_uuid = str(base_result.get("turn_uuid") or payload.get("run_id") or "")
|
||||
reply_text = resolve_scheduled_outbound_text(payload=payload, reply_text=str(base_result.get("reply_text") or ""))
|
||||
delivery = payload.get("delivery") if isinstance(payload.get("delivery"), dict) else {}
|
||||
delivery_json = json.dumps(delivery, ensure_ascii=False)
|
||||
|
|
@ -114,10 +123,34 @@ def finalize_scheduled_turn_success(
|
|||
if job:
|
||||
delivery_json = str(getattr(job, "delivery_json", "") or delivery_json)
|
||||
|
||||
lang = str(payload.get("lang") or getattr(job, "lang", "") or "en")
|
||||
att_count = 0
|
||||
try:
|
||||
att_count = len(
|
||||
_collect_scheduled_turn_attachments(
|
||||
store=store,
|
||||
session_id=session_id,
|
||||
turn_uuid=turn_uuid,
|
||||
)
|
||||
or []
|
||||
)
|
||||
except Exception:
|
||||
att_count = 0
|
||||
|
||||
# Playbook / ops jobs: always wrap with a short success header for WA readability.
|
||||
if job_id or att_count:
|
||||
reply_text = format_scheduled_success_summary(
|
||||
job_name=str(getattr(job, "name", "") or ""),
|
||||
job_id=job_id,
|
||||
reply_text=reply_text,
|
||||
attachment_count=att_count,
|
||||
lang=lang,
|
||||
)
|
||||
|
||||
_persist_scheduled_assistant_reply(
|
||||
store,
|
||||
session_id=str(payload.get("session_id") or ""),
|
||||
turn_uuid=str(base_result.get("turn_uuid") or payload.get("run_id") or ""),
|
||||
session_id=session_id,
|
||||
turn_uuid=turn_uuid,
|
||||
reply_text=reply_text,
|
||||
)
|
||||
delivery_status = deliver_scheduled_reply(
|
||||
|
|
@ -128,8 +161,8 @@ def finalize_scheduled_turn_success(
|
|||
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=str(base_result.get("turn_uuid") or payload.get("run_id") or ""),
|
||||
session_id=session_id,
|
||||
turn_uuid=turn_uuid,
|
||||
)
|
||||
if scheduled_run_id:
|
||||
store.scheduled_job_run_update(
|
||||
|
|
|
|||
|
|
@ -88,16 +88,38 @@ def enrich_mcp_scope_error(result: dict[str, Any]) -> dict[str, Any]:
|
|||
]
|
||||
out["user_facing_hint"] = (
|
||||
"SQL query is not enabled for this bot token. "
|
||||
"I will use alarm aggregate/report tools instead, or an admin can grant sql:query."
|
||||
"I will use alarm aggregate/report tools instead. "
|
||||
"To enable SQL: ask a WhatsApp admin → Admin UI → netx MCP token scopes → grant sql:query."
|
||||
)
|
||||
out["next_steps"] = [
|
||||
"Use aggregateUmeAlarms / queryUmeAlarmsRaw / ume_alarm_xlsx_report instead of sqlQueryUme.",
|
||||
"Ask a WhatsApp admin to open Admin → MCP / netx token and grant scope sql:query.",
|
||||
"Do not retry sqlQueryUme until the scope is granted.",
|
||||
]
|
||||
out["admin_action"] = {
|
||||
"required_scope": "sql:query",
|
||||
"where": "Admin UI → MCP server / netx API token scopes",
|
||||
"ask": "WhatsApp access admin (whitelist contact with list_type=admin)",
|
||||
}
|
||||
else:
|
||||
out["hint"] = (
|
||||
f"Current netx token lacks scope {scope or '(unknown)'}. "
|
||||
"Do not retry the same tool; ask an admin to grant it, or use tools that do not need this scope."
|
||||
)
|
||||
out["user_facing_hint"] = (
|
||||
f"Permission missing ({scope or 'scope'}). An admin needs to grant this on the netx API token."
|
||||
f"Permission missing ({scope or 'scope'}). "
|
||||
"Ask a WhatsApp admin to grant this scope on the netx API token "
|
||||
"(Admin → MCP / netx token scopes)."
|
||||
)
|
||||
out["next_steps"] = [
|
||||
f"Ask a WhatsApp admin to grant scope {scope or '(unknown)'} on the netx MCP token.",
|
||||
"Retry only after the scope is granted; do not blind-retry this tool.",
|
||||
]
|
||||
out["admin_action"] = {
|
||||
"required_scope": scope or "",
|
||||
"where": "Admin UI → MCP server / netx API token scopes",
|
||||
"ask": "WhatsApp access admin (whitelist contact with list_type=admin)",
|
||||
}
|
||||
out["failure_class"] = "auth"
|
||||
out["retry_forbidden"] = True
|
||||
return out
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue