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
|
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
|
||||||
|
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
|
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
|
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)")
|
||||||
|
|||||||
Reference in New Issue
Block a user