feat(xxm): implement coordinator protocol (aligned with mohe/zhiwei/xiaoguo)

- GRANT: [GRANT:xxm] one-time speak permission, overrides REVOKE
- REVOKE: [REVOKE:xxm] 5-min speak ban, read-only mode
- Coordinator switch: hmo can change with 'coordinator=xxm'
- Shut-up: hmo says keywords → 5-min silence (was 30s)
- Read-only mode: revoked agents see messages but output __SILENT__
- Removed old _is_silenced/_SILENCE_UNTIL, unified under _REVOKED_UNTIL
This commit is contained in:
hmo
2026-06-20 20:09:01 +08:00
parent efc4cd1a9e
commit b9df510f31
+55 -26
View File
@@ -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:
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)")