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:
+55
-26
@@ -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)")
|
||||
|
||||
Reference in New Issue
Block a user