落地海运协同群:测服靠会话存档听群话,报价单能带上协同字段。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
"""
|
||||
测服自己的微盛监听名单与 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)))
|
||||
Reference in New Issue
Block a user