Notify WhatsApp requesters on access decisions and ship EN ops help.

After admin YES/NO, DM the requester a field guide; WA help/bind no longer returns the Chinese productivity todo menu.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-08-10 23:37:50 +08:00
parent f31ebeb30e
commit 94a1b5b26c
5 changed files with 179 additions and 5 deletions

View file

@ -100,7 +100,11 @@ def _build_admin_gateway_executor(
)
def _menu_text() -> str:
def _menu_text(*, channel: str = "") -> str:
if str(channel or "").strip().lower() == "whatsapp":
from runtime.extensions.whatsapp.access_control import whatsapp_ops_help_text
return whatsapp_ops_help_text(lang="en")
return (
"已绑定成功,常用命令:\n"
"1) 帮助 / 菜单\n"
@ -121,13 +125,14 @@ def _handle_productivity_commands(
session_id: str = "",
creator_external_user_id: str = "",
creator_push_name: str = "",
channel: str = "",
) -> str | None:
t = (text or "").strip()
t_low = t.lower()
if not t:
return None
if t in ("帮助", "菜单", "help", "/help"):
return _menu_text()
return _menu_text(channel=channel)
from svc.config.paths import db_path
from svc.persistence.sqlite_store import SqliteStore
@ -1045,7 +1050,16 @@ def process_inbound_payload(payload: dict[str, Any]) -> dict[str, Any]:
else None
),
)
reply = ("绑定成功。\n\n" + _menu_text()) if info else "绑定失败:无效或已使用的绑定码。"
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)
else:
reply = (
"Bind failed: invalid or already used code."
if channel_is_wa_bind
else "绑定失败:无效或已使用的绑定码。"
)
else:
if inbound.is_group and not should_process_group_inbound(
is_group=inbound.is_group,
@ -1238,6 +1252,7 @@ def process_inbound_payload(payload: dict[str, Any]) -> dict[str, Any]:
)
or ""
),
channel=str(inbound.channel or ""),
)
if cmd_reply is not None:
reply = cmd_reply
@ -1742,7 +1757,7 @@ def process_inbound_payload(payload: dict[str, Any]) -> dict[str, Any]:
if reply:
reply = f"{preface}\n\n{reply}"
else:
reply = f"{preface}\n\n{_menu_text()}"
reply = f"{preface}\n\n{_menu_text(channel=str(inbound.channel or ''))}"
ch_lower = str(inbound.channel or "").strip().lower()
if ch_lower in {"wechat", "weixin"}:

View file

@ -5,6 +5,8 @@ import uuid
from typing import Any
from runtime.extensions.whatsapp.access_control import (
access_denied_final_text,
access_granted_guide_text,
admin_approval_result_text,
admin_notify_text,
default_access_lang,
@ -24,7 +26,7 @@ from runtime.extensions.whatsapp.access_control import (
resolve_whatsapp_sender_jid,
whatsapp_sender_lookup_jids,
)
from runtime.extensions.whatsapp.api import is_whatsapp_user_target
from runtime.extensions.whatsapp.api import is_whatsapp_user_target, normalize_whatsapp_target
from runtime.extensions.whatsapp.tenant import resolve_whatsapp_tenant_id
@ -167,6 +169,22 @@ def _ensure_guest_identity(
)
def _requester_dm_chat_id(*, external_user_id: str, phone: str = "") -> str:
jid = str(external_user_id or "").strip()
if jid and is_whatsapp_user_target(jid):
try:
return normalize_whatsapp_target(jid)
except Exception:
return jid
ph = str(phone or "").strip() or phone_from_jid(jid)
if ph:
try:
return normalize_whatsapp_target(ph)
except Exception:
pass
return jid
def _notify_admins(
store: Any,
*,
@ -210,6 +228,42 @@ def _notify_admins(
)
def _notify_requester_access_decision(
store: Any,
*,
tenant_id: str,
account_id: str,
lang: str,
external_user_id: str,
phone: str,
pending_id: str,
approved: bool,
) -> None:
chat_id = _requester_dm_chat_id(external_user_id=external_user_id, phone=phone)
if not chat_id or not is_whatsapp_user_target(chat_id):
return
text = (
access_granted_guide_text(lang=lang)
if approved
else access_denied_final_text(lang=lang)
)
store.enqueue_channel_outbound_message(
channel="whatsapp",
chat_id=chat_id,
text=text,
tenant_id=tenant_id,
account_id=account_id,
source=json.dumps(
{
"kind": "whatsapp_access_decision",
"pending_id": str(pending_id or ""),
"approved": bool(approved),
},
ensure_ascii=False,
),
)
def _upsert_whatsapp_contact_profile(
store: Any,
*,
@ -347,6 +401,17 @@ def _handle_admin_message(
if callable(deleter):
deleter(pending_id=pending_id)
_notify_requester_access_decision(
store,
tenant_id=tenant_id,
account_id=account_id,
lang=lang,
external_user_id=target_jid,
phone=target_phone,
pending_id=pending_id,
approved=approved,
)
raw = meta.get("raw") if isinstance(meta.get("raw"), dict) else {}
reply_meta: dict[str, Any] = {
"quote_remote_jid": admin_chat_id,

View file

@ -343,6 +343,43 @@ def coerce_whatsapp_access_target(value: str) -> str:
return normalize_whatsapp_target(normalize_whatsapp_phone(value))
def whatsapp_ops_help_text(*, lang: str = "en") -> str:
"""Short field-ops guide for WhatsApp (English-first deployments)."""
if str(lang or "").strip().lower().startswith("zh"):
return (
"群聊请 @机器人 再提问。\n"
"常用示例:\n"
"- 断纤 / fiber cut 告警\n"
"- 离线网元清单\n"
"- 告警导出 Excel / alarm excel\n"
"- license / capacity 查询\n"
"长任务可回复 YES / continue。\n"
"绑定团队账号:bind <code>"
)
return (
"In groups, @mention the bot first.\n"
"Try:\n"
"- fiber cut alarms\n"
"- list offline NEs\n"
"- alarm excel report\n"
"- license / capacity check\n"
"During long tasks reply YES / continue.\n"
"Team account: bind <code>"
)
def access_granted_guide_text(*, lang: str = "en") -> str:
if str(lang or "").strip().lower().startswith("zh"):
return "访问已批准,可以使用助手了。\n\n" + whatsapp_ops_help_text(lang=lang)
return "Access approved — you can use the assistant now.\n\n" + whatsapp_ops_help_text(lang=lang)
def access_denied_final_text(*, lang: str = "en") -> str:
if str(lang or "").strip().lower().startswith("zh"):
return "访问申请未通过。如需使用请联系管理员。"
return "Your access request was not approved. Contact an administrator if you still need access."
def denied_reply_text(*, lang: str, pending_id: str = "", already_pending: bool = False) -> str:
pid = str(pending_id or "").strip()
if str(lang or "").strip().lower().startswith("zh"):