diff --git a/gateway/scripts/xmpp_bot.py b/gateway/scripts/xmpp_bot.py index 132df95..d23e5b2 100644 --- a/gateway/scripts/xmpp_bot.py +++ b/gateway/scripts/xmpp_bot.py @@ -97,37 +97,54 @@ def _is_mam_recovery() -> bool: return _MAM_RECOVERY # ── Silence cooldown: when user says shut up, actually shut up ── -_SILENCE_UNTIL: float = 0.0 -_SILENCE_LOCK = threading.Lock() -_SHUTUP_PATTERNS = [ - "闭嘴", "住口", - "shut up", "shutup", -] +# ── Coordinator protocol (aligned with mohe/zhiwei/xiaoguo) ── +# All in-band XMPP signaling, no DB dependency. +_COORDINATOR: str = "mohe" # which agent moderates +_GRANTED: str | None = None # agent granted one-time speak permission +_REVOKED_UNTIL: float = 0.0 # timestamp until revoked agent can speak again +_SHUTUP_PATTERNS = ["闭嘴", "别说话", "安静", "shut", "stfu", "别说了", "停"] -def _is_silenced() -> bool: - """Check if bot is in silence mode. - If so, the caller should NOT process or respond to any message. - """ - with _SILENCE_LOCK: - if time.time() < _SILENCE_UNTIL: - return True - return False +def _process_coordinator_signals(nickname: str, body: str) -> bool: + """Parse coordinator/GRANT/REVOKE signals from group messages. + Returns True if the message was consumed as a control signal (no further processing).""" + global _COORDINATOR, _GRANTED, _REVOKED_UNTIL + + # 1. hmo switches coordinator + if nickname == 'hmo' and 'coordinator=' in body.lower(): + for name in ('mohe', 'zhiwei', 'xxm'): + if f'coordinator={name}' in body.lower(): + _COORDINATOR = name + _GRANTED = None + log(f"Coordinator switched to {name} by hmo") + return True + + # 2. GRANT signal (overrides REVOKE, one-time use) + gm = re.search(r'\[GRANT:(\w+)\]', body) + if gm: + _GRANTED = gm.group(1) + _REVOKED_UNTIL = 0 # lift revocation when granted + log(f"GRANT: {_GRANTED}") + return True # signal consumed, don't need to process further + + # 3. REVOKE signal (5min auto-restore) + rm = re.search(r'\[REVOKE:(\w+)\]', body) + if rm and rm.group(1) == 'xxm': + _REVOKED_UNTIL = time.time() + 300 + log(f"REVOKEd: xxm silenced for 5min") + return True + + # Not a control signal + return False def _check_shutup(body: str) -> bool: - """Check if the user is telling the bot to shut up. - Returns True and sets silence cooldown if so. - """ + """Check if hmo is telling the bot to shut up. 5-min silence (matching coordinator pattern).""" lower = body.lower().strip() - # Require minimum match: at least one shut-up keyword appears - # and the message is primarily about silencing (not a longer discussion) for pat in _SHUTUP_PATTERNS: if pat.lower() in lower: - # Set 30s silence - long enough to break the loop - with _SILENCE_LOCK: - _SILENCE_UNTIL = time.time() + 30 - log(f"(shutup detected: '{pat}' → 30s silence)") + _REVOKED_UNTIL = time.time() + 300 + log(f"(shutup: '{pat}' → 5min silence)") return True return False @@ -157,7 +174,7 @@ def on_message(msg): return # Shut-up check — hard silence before any processing - if _is_silenced(): + if time.time() < _REVOKED_UNTIL: log(f"(silenced) <{sender}> {body[:60]}... dropped") return if _check_shutup(body): @@ -552,14 +569,26 @@ def on_group_message(msg): # Record to message buffer for HTTP bridge monitoring _record_group_msg(nickname, body) - # Shut-up check — applies to group messages from others - if _is_silenced(): - log(f"(group silenced) {body[:60]}... dropped") + # ── Coordinator signals (GRANT/REVOKE/coordinator switch) ── + if _process_coordinator_signals(nickname, body): return + + # ── Revoke check: read-only mode (see but don't reply) ── + is_revoked = time.time() < _REVOKED_UNTIL + # GRANT overrides REVOKE + if is_revoked and _GRANTED == 'xxm': + _GRANTED = None + is_revoked = False + log("GRANTed: xxm can speak despite REVOKE") + + # ── Shut-up check ── if _check_shutup(body): - log(f"(group shutup detected)") return + # ── Read-only mode for revoked agents ── + if is_revoked: + body = f"【只读消息】你目前被收回发言权。只需了解内容。输出 __SILENT__。\n\n[核心群 {room}] {nickname} 说: {body}" + # Batch nearby messages (unless @mention → process immediately) if _batch_group_message(room, nickname, body): log(f"[Group][{room}] {nickname}: {body[:80]} (batched)")