178 lines
5.5 KiB
Python
178 lines
5.5 KiB
Python
"""
|
||
测服自己的微盛监听名单与 last_seq。
|
||
|
||
本文件职责:把协同群 chat_id、按席/按房间的查询游标写到 REDIS_KEY_PREFIX 下。
|
||
禁止:写现网 ytd:prod: 前缀;禁止把游标当六态。
|
||
线程:可被建群线程与 Worker 轮询同时调用,只做单 key 读写。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
import threading
|
||
import time
|
||
from typing import Optional
|
||
|
||
from agent.redis_coord import get_redis_runtime
|
||
from agent.redis_coord.client import RedisClient
|
||
from agent.redis_coord.keys import WSHOTO_LAST_SEQ, WSHOTO_ROOM_TICKET, WSHOTO_ROOMS
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# 建群后或刚听到群话:立刻唤醒轮询,并在热窗内更勤地扫。
|
||
_poll_wake = threading.Event()
|
||
_hot_until: dict[str, float] = {}
|
||
_hot_lock = threading.Lock()
|
||
HOT_WINDOW_SEC = 90.0
|
||
|
||
|
||
def wake_wshoto_poll() -> None:
|
||
"""打断轮询等待。建群线程可调;真正拉消息仍在 Worker 轮询线程。"""
|
||
_poll_wake.set()
|
||
|
||
|
||
def wait_wshoto_poll(timeout: float) -> bool:
|
||
"""等下一轮或被唤醒。返回是否被踢醒。"""
|
||
fired = _poll_wake.wait(timeout=max(0.1, float(timeout)))
|
||
if fired:
|
||
_poll_wake.clear()
|
||
return fired
|
||
|
||
|
||
def mark_room_hot(chat_id: str, *, ttl_sec: float = HOT_WINDOW_SEC) -> None:
|
||
"""这群接下来几十秒更勤扫:刚建群或刚有人说话。"""
|
||
raw = (chat_id or "").strip()
|
||
if not raw:
|
||
return
|
||
until = time.time() + max(5.0, float(ttl_sec))
|
||
with _hot_lock:
|
||
_hot_until[raw] = until
|
||
wake_wshoto_poll()
|
||
|
||
|
||
def has_hot_room() -> bool:
|
||
"""是否还有热窗内的协同群。"""
|
||
now = time.time()
|
||
with _hot_lock:
|
||
stale = [k for k, until in _hot_until.items() if until <= now]
|
||
for k in stale:
|
||
_hot_until.pop(k, None)
|
||
return any(until > now for until in _hot_until.values())
|
||
|
||
|
||
def _client(redis: Optional[RedisClient] = None) -> Optional[RedisClient]:
|
||
if redis is not None:
|
||
return redis
|
||
try:
|
||
return get_redis_runtime().client
|
||
except Exception:
|
||
logger.exception("微盛监听名单:Redis 不可用")
|
||
return None
|
||
|
||
|
||
def watch_collab_room(
|
||
chat_id: str,
|
||
*,
|
||
work_order_no: str = "",
|
||
redis: Optional[RedisClient] = None,
|
||
) -> None:
|
||
"""
|
||
建群成功后登记 chat_id,供按房间补拉。
|
||
|
||
可同时记下房间→工单号,主账按群查找暂时不可用时也能对上单。
|
||
副作用:改 Redis。失败只打日志,不挡销售侧建群。
|
||
"""
|
||
raw = (chat_id or "").strip()
|
||
if not raw:
|
||
return
|
||
client = _client(redis)
|
||
if client is None:
|
||
return
|
||
key = client.key(WSHOTO_ROOMS)
|
||
current = client.raw.get(key) or ""
|
||
if isinstance(current, bytes):
|
||
current = current.decode("utf-8")
|
||
rooms = {x.strip() for x in str(current).split(",") if x.strip()}
|
||
rooms.add(raw)
|
||
client.raw.set(key, ",".join(sorted(rooms)))
|
||
no = (work_order_no or "").strip()
|
||
if no:
|
||
client.raw.set(client.key(WSHOTO_ROOM_TICKET, raw), no)
|
||
logger.info("存档监听已登记群数=%s", len(rooms))
|
||
mark_room_hot(raw)
|
||
|
||
|
||
def ticket_no_for_room(chat_id: str, *, redis: Optional[RedisClient] = None) -> str:
|
||
"""用协同群 chat_id 找回工单号(测服 Redis 短索引,不是六态)。"""
|
||
raw = (chat_id or "").strip()
|
||
if not raw:
|
||
return ""
|
||
client = _client(redis)
|
||
if client is None:
|
||
return ""
|
||
val = client.raw.get(client.key(WSHOTO_ROOM_TICKET, raw))
|
||
if val is None:
|
||
return ""
|
||
if isinstance(val, bytes):
|
||
val = val.decode("utf-8")
|
||
return str(val).strip()
|
||
|
||
|
||
def unwatch_collab_room(chat_id: str, *, redis: Optional[RedisClient] = None) -> None:
|
||
"""协同结束后去掉监听,避免解散群还被当成活群补拉。"""
|
||
raw = (chat_id or "").strip()
|
||
if not raw:
|
||
return
|
||
client = _client(redis)
|
||
if client is None:
|
||
return
|
||
key = client.key(WSHOTO_ROOMS)
|
||
current = client.raw.get(key) or ""
|
||
if isinstance(current, bytes):
|
||
current = current.decode("utf-8")
|
||
rooms = {x.strip() for x in str(current).split(",") if x.strip() and x.strip() != raw}
|
||
client.raw.set(key, ",".join(sorted(rooms)))
|
||
client.raw.delete(client.key(WSHOTO_ROOM_TICKET, raw))
|
||
|
||
|
||
def list_watched_rooms(*, redis: Optional[RedisClient] = None) -> list[str]:
|
||
"""当前要按房间补拉的协同群。"""
|
||
client = _client(redis)
|
||
if client is None:
|
||
return []
|
||
current = client.raw.get(client.key(WSHOTO_ROOMS)) or ""
|
||
if isinstance(current, bytes):
|
||
current = current.decode("utf-8")
|
||
return [x.strip() for x in str(current).split(",") if x.strip()]
|
||
|
||
|
||
def load_last_seq(scope: str, *, redis: Optional[RedisClient] = None) -> int:
|
||
"""
|
||
读取本前缀下的 last_seq。
|
||
|
||
scope:seat:AI001 或 room:wrxxx。没有记录返回 0(调用方改走时间窗,避免从 0 翻全量历史)。
|
||
"""
|
||
client = _client(redis)
|
||
if client is None:
|
||
return 0
|
||
val = client.raw.get(client.key(WSHOTO_LAST_SEQ, scope))
|
||
if val is None:
|
||
return 0
|
||
try:
|
||
return int(val)
|
||
except (TypeError, ValueError):
|
||
return 0
|
||
|
||
|
||
def save_last_seq(scope: str, seq: int, *, redis: Optional[RedisClient] = None) -> None:
|
||
"""推进测服自己的查询游标。seq 必须单调不减。"""
|
||
if seq <= 0:
|
||
return
|
||
client = _client(redis)
|
||
if client is None:
|
||
return
|
||
prev = load_last_seq(scope, redis=client)
|
||
if seq < prev:
|
||
return
|
||
client.raw.set(client.key(WSHOTO_LAST_SEQ, scope), str(int(seq)))
|