Nudge WhatsApp group ops asks that skip @mention.

When require_mention drops an ops short-intent, send one throttled @sender tip instead of silent ignore; casual chat stays quiet.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-08-10 23:41:48 +08:00
parent 94a1b5b26c
commit 6c5536b758
4 changed files with 201 additions and 0 deletions

View file

@ -3,6 +3,8 @@
from __future__ import annotations
import re
import threading
import time
from typing import Any
_BOT_MENTION_RE = re.compile(r"@\S+")
@ -108,9 +110,78 @@ def maybe_ops_short_intent_system_hint(*, text: str, lang: str = "en") -> str:
return build_ops_short_intent_hint(intent=intent, lang=lang)
def build_group_mention_nudge_text(
*,
intent: str | None = None,
lang: str = "en",
triggers: list[str] | None = None,
) -> str:
"""Tell field users why a group ops ask was ignored (require @bot / trigger)."""
extra = ""
trigs = [str(x or "").strip() for x in (triggers or []) if str(x or "").strip()]
if trigs:
shown = ", ".join(trigs[:3])
if str(lang or "").strip().lower().startswith("zh"):
extra = f"或发送触发词({shown})"
else:
extra = f" or use a trigger ({shown})"
label = str(intent or "").strip().replace("_", " ")
if str(lang or "").strip().lower().startswith("zh"):
topic = f"(识别到:{label})" if label else ""
return (
f"群里需要先 @我{extra} 才会处理运维请求{topic}。"
"请带上 @ 后重发(断纤/离线/告警/Excel/license 等)。"
)
topic = f" (detected: {label})" if label else ""
return (
f"In this group I only answer when @mentioned{extra}{topic}. "
"Please re-send with @me for ops asks (fiber / offline / alarms / excel / license)."
)
_MENTION_NUDGE_LOCK = threading.Lock()
_MENTION_NUDGE_LAST: dict[str, float] = {}
_MENTION_NUDGE_TTL_S = 12 * 60.0
def should_send_group_mention_nudge(
*,
account_id: str,
chat_id: str,
user_id: str,
now: float | None = None,
ttl_s: float | None = None,
) -> bool:
"""Throttle one nudge per sender/chat for a few minutes."""
key = f"{str(account_id or '').strip()}|{str(chat_id or '').strip()}|{str(user_id or '').strip()}"
if not key.strip("|"):
return False
ts = float(now if now is not None else time.monotonic())
window = float(ttl_s if ttl_s is not None else _MENTION_NUDGE_TTL_S)
with _MENTION_NUDGE_LOCK:
prev = _MENTION_NUDGE_LAST.get(key)
if prev is not None and (ts - float(prev)) < window:
return False
_MENTION_NUDGE_LAST[key] = ts
if len(_MENTION_NUDGE_LAST) > 512:
cutoff = ts - window
stale = [k for k, v in _MENTION_NUDGE_LAST.items() if float(v) < cutoff]
for k in stale[:128]:
_MENTION_NUDGE_LAST.pop(k, None)
return True
def reset_group_mention_nudge_throttle_for_tests() -> None:
with _MENTION_NUDGE_LOCK:
_MENTION_NUDGE_LAST.clear()
__all__ = [
"build_group_mention_nudge_text",
"build_ops_short_intent_hint",
"detect_ops_short_intent",
"maybe_ops_short_intent_system_hint",
"normalize_ops_user_text",
"reset_group_mention_nudge_throttle_for_tests",
"should_send_group_mention_nudge",
]