机器人显示名与站点标题可配;多段联运列表投影;协同群监听对账;工单附件落存;模板与工单后台补强

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
linmianxun
2026-10-19 09:41:59 +08:00
co-authored by Cursor
parent 277cdc1c28
commit 2b528758b4
119 changed files with 6760 additions and 3516 deletions
+87 -1
View File
@@ -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]: