机器人显示名与站点标题可配;多段联运列表投影;协同群监听对账;工单附件落存;模板与工单后台补强
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -52,12 +52,98 @@ def mark_room_hot(chat_id: str, *, ttl_sec: float = HOT_WINDOW_SEC) -> None:
|
||||
|
||||
def has_hot_room() -> bool:
|
||||
"""是否还有热窗内的协同群。"""
|
||||
return bool(list_hot_rooms())
|
||||
|
||||
|
||||
def list_hot_rooms() -> list[str]:
|
||||
"""当前热窗内的协同群(刚建群或刚有人说话)。"""
|
||||
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())
|
||||
return [k for k, until in _hot_until.items() if until > now]
|
||||
|
||||
|
||||
def is_room_hot(chat_id: str) -> bool:
|
||||
"""这群是否还在热窗内。"""
|
||||
raw = (chat_id or "").strip()
|
||||
if not raw:
|
||||
return False
|
||||
now = time.time()
|
||||
with _hot_lock:
|
||||
until = _hot_until.get(raw) or 0.0
|
||||
if until <= now:
|
||||
_hot_until.pop(raw, None)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def plan_rooms_for_poll(
|
||||
rooms: list[str],
|
||||
*,
|
||||
max_cold: int = 120,
|
||||
redis: Optional[RedisClient] = None,
|
||||
) -> list[str]:
|
||||
"""
|
||||
本轮要扫的房间:热群全部优先,冷群按游标轮转。
|
||||
|
||||
注意:这里只读游标、不推进。真正扫过多少冷群后,由 advance_poll_cursor 推进,
|
||||
避免限频/超时提前停时把没扫到的群直接跳过。
|
||||
"""
|
||||
uniq: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for raw in rooms:
|
||||
room = (raw or "").strip()
|
||||
if not room or room in seen:
|
||||
continue
|
||||
seen.add(room)
|
||||
uniq.append(room)
|
||||
hot = [r for r in uniq if is_room_hot(r)]
|
||||
hot_set = set(hot)
|
||||
cold = [r for r in uniq if r not in hot_set]
|
||||
budget = max(0, int(max_cold))
|
||||
if not cold or budget <= 0:
|
||||
return hot
|
||||
start = load_poll_cursor(redis=redis) % len(cold)
|
||||
rotated = cold[start:] + cold[:start]
|
||||
take = rotated[:budget]
|
||||
# 热群在前,保证刚发过话/刚建的群本轮一定扫到。
|
||||
return hot + [r for r in take if r not in hot_set]
|
||||
|
||||
|
||||
def load_poll_cursor(*, redis: Optional[RedisClient] = None) -> int:
|
||||
"""冷群轮转游标。"""
|
||||
client = _client(redis)
|
||||
if client is None:
|
||||
return 0
|
||||
raw_cur = client.raw.get(client.key(WSHOTO_LAST_SEQ, "poll_room_cursor"))
|
||||
if isinstance(raw_cur, bytes):
|
||||
raw_cur = raw_cur.decode("utf-8")
|
||||
try:
|
||||
return int(raw_cur or 0)
|
||||
except (TypeError, ValueError):
|
||||
return 0
|
||||
|
||||
|
||||
def advance_poll_cursor(
|
||||
cold_polled: int,
|
||||
*,
|
||||
redis: Optional[RedisClient] = None,
|
||||
) -> None:
|
||||
"""
|
||||
按本轮实际扫过的冷群数推进游标。
|
||||
|
||||
限频或时间预算提前停时,只推进已扫到的,下轮从停住的地方继续。
|
||||
"""
|
||||
n = max(0, int(cold_polled))
|
||||
if n <= 0:
|
||||
return
|
||||
client = _client(redis)
|
||||
if client is None:
|
||||
return
|
||||
cur = load_poll_cursor(redis=client)
|
||||
client.raw.set(client.key(WSHOTO_LAST_SEQ, "poll_room_cursor"), str(int(cur + n)))
|
||||
|
||||
|
||||
def _client(redis: Optional[RedisClient] = None) -> Optional[RedisClient]:
|
||||
|
||||
Reference in New Issue
Block a user