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
+57 -28
View File
@@ -97,37 +97,54 @@ def _is_mam_recovery() -> bool:
return _MAM_RECOVERY return _MAM_RECOVERY
# ── Silence cooldown: when user says shut up, actually shut up ── # ── Silence cooldown: when user says shut up, actually shut up ──
_SILENCE_UNTIL: float = 0.0 # ── Coordinator protocol (aligned with mohe/zhiwei/xiaoguo) ──
_SILENCE_LOCK = threading.Lock() # All in-band XMPP signaling, no DB dependency.
_SHUTUP_PATTERNS = [ _COORDINATOR: str = "mohe" # which agent moderates
"闭嘴", "住口", _GRANTED: str | None = None # agent granted one-time speak permission
"shut up", "shutup", _REVOKED_UNTIL: float = 0.0 # timestamp until revoked agent can speak again
] _SHUTUP_PATTERNS = ["闭嘴", "别说话", "安静", "shut", "stfu", "别说了", ""]
def _is_silenced() -> bool: def _process_coordinator_signals(nickname: str, body: str) -> bool:
"""Check if bot is in silence mode. """Parse coordinator/GRANT/REVOKE signals from group messages.
If so, the caller should NOT process or respond to any message. Returns True if the message was consumed as a control signal (no further processing)."""
""" global _COORDINATOR, _GRANTED, _REVOKED_UNTIL
with _SILENCE_LOCK:
if time.time() < _SILENCE_UNTIL: # 1. hmo switches coordinator
return True if nickname == 'hmo' and 'coordinator=' in body.lower():
return False 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: def _check_shutup(body: str) -> bool:
"""Check if the user is telling the bot to shut up. """Check if hmo is telling the bot to shut up. 5-min silence (matching coordinator pattern)."""
Returns True and sets silence cooldown if so.
"""
lower = body.lower().strip() 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: for pat in _SHUTUP_PATTERNS:
if pat.lower() in lower: if pat.lower() in lower:
# Set 30s silence - long enough to break the loop _REVOKED_UNTIL = time.time() + 300
with _SILENCE_LOCK: log(f"(shutup: '{pat}' → 5min silence)")
_SILENCE_UNTIL = time.time() + 30
log(f"(shutup detected: '{pat}' → 30s silence)")
return True return True
return False return False
@@ -157,7 +174,7 @@ def on_message(msg):
return return
# Shut-up check — hard silence before any processing # Shut-up check — hard silence before any processing
if _is_silenced(): if time.time() < _REVOKED_UNTIL:
log(f"(silenced) <{sender}> {body[:60]}... dropped") log(f"(silenced) <{sender}> {body[:60]}... dropped")
return return
if _check_shutup(body): if _check_shutup(body):
@@ -552,14 +569,26 @@ def on_group_message(msg):
# Record to message buffer for HTTP bridge monitoring # Record to message buffer for HTTP bridge monitoring
_record_group_msg(nickname, body) _record_group_msg(nickname, body)
# Shut-up check — applies to group messages from others # ── Coordinator signals (GRANT/REVOKE/coordinator switch) ──
if _is_silenced(): if _process_coordinator_signals(nickname, body):
log(f"(group silenced) {body[:60]}... dropped")
return 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): if _check_shutup(body):
log(f"(group shutup detected)")
return 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) # Batch nearby messages (unless @mention → process immediately)
if _batch_group_message(room, nickname, body): if _batch_group_message(room, nickname, body):
log(f"[Group][{room}] {nickname}: {body[:80]} (batched)") log(f"[Group][{room}] {nickname}: {body[:80]} (batched)")