""" 测服自己的微盛监听名单与 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)))