feat(tg): TG-visibility — уведомления о занятости уровней + срез лестницы по запросу (t-260929170039-aad557)
- tg_inbox.notify_chat(): отправка в чат харнеса без входящего-родителя (чат = последний MessageReceived от Alex) - tg_inbox.ladder_snapshot(): срез лестницы — занятость/тики/ошибки уровней, готово/в работе, открытые эскалации; read-only через run_projections() - scheduler._notify_busy(): уровень сообщает в TG о взятии задачи (анти-спам 15м на уровень) — TaskClaimed в scheduler.run_level_tick - cli.py: команда ladder + tg-notify; /лестница|/ladder|/статус|/status в tg-reply заменяется на срез лестницы — запрос из TG самодостаточен - guard в __main__ tg_inbox: единственный легитимный поллер — systemd-сервис (раньше python -m harness.tg_inbox с любыми аргументами поднимал конкурирующего поллера → 409 Conflict в легитимном)
This commit is contained in:
parent
935c75d078
commit
78ba888bb9
2 changed files with 73 additions and 0 deletions
17
cli.py
17
cli.py
|
|
@ -35,6 +35,10 @@ def main():
|
|||
p_tgr.add_argument("msg_id", help="id входящего (m-NNN)")
|
||||
p_tgr.add_argument("text")
|
||||
|
||||
p_lad = sub.add_parser("ladder", help="срез лестницы: уровни, занятость, задачи, эскалации")
|
||||
p_not = sub.add_parser("tg-notify", help="отправить сообщение в чат харнеса (уведомление)")
|
||||
p_not.add_argument("text")
|
||||
|
||||
p_esc = sub.add_parser("escalate", help="эскалация важного на уровень выше (min→hour→day)")
|
||||
p_esc.add_argument("reason")
|
||||
p_esc.add_argument("-f", "--from-level", default="min", choices=["min", "hour"],
|
||||
|
|
@ -85,8 +89,21 @@ def main():
|
|||
elif args.cmd == "tg-reply":
|
||||
es.init_db()
|
||||
from harness import tg_inbox
|
||||
low = args.text.strip().lower()
|
||||
if low in ("/лестница", "/ladder", "/статус", "/status", "лестница", "status"):
|
||||
# срез лестницы по запросу из TG — видимостью управляет сам харнес
|
||||
args.text = "📊 " + tg_inbox.ladder_snapshot()
|
||||
mid = tg_inbox.send_reply(args.msg_id, args.text)
|
||||
print(f"OK: ответ на {mid} отправлен")
|
||||
elif args.cmd == "ladder":
|
||||
es.init_db()
|
||||
from harness import tg_inbox
|
||||
print(tg_inbox.ladder_snapshot())
|
||||
elif args.cmd == "tg-notify":
|
||||
es.init_db()
|
||||
from harness import tg_inbox
|
||||
r = tg_inbox.notify_chat(args.text)
|
||||
print("OK: уведомление отправлено" if r else "SKIP: некому отправить (чат не известен)")
|
||||
elif args.cmd == "escalate":
|
||||
es.init_db()
|
||||
eid, to_level = commands.raise_escalation(
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ getUpdates offset=last+1. Идемпотентность: update_id → aggregat
|
|||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
|
|
@ -153,5 +154,60 @@ def poll_loop():
|
|||
time.sleep(5)
|
||||
|
||||
|
||||
def notify_chat(text: str):
|
||||
"""Отправить сообщение в чат харнеса (без входящего-родителя — уведомления).
|
||||
|
||||
Чат берём из последнего входящего MessageReceived (это личка Алекса);
|
||||
если входящих ещё не было — тихо пропускаем (некому показывать).
|
||||
"""
|
||||
_load_env()
|
||||
if not TG_TOKEN:
|
||||
return None
|
||||
model = es.run_projections()
|
||||
inbox = [m for m in (model.get("inbox") or {}).values()
|
||||
if m.get("chat_id") and not m.get("ignored")]
|
||||
if not inbox:
|
||||
return None # некому: чат ещё не известен
|
||||
chat_id = sorted(inbox, key=lambda m: m.get("ts") or "")[-1]["chat_id"]
|
||||
try:
|
||||
return send_message(chat_id, text)
|
||||
except Exception:
|
||||
return None # уведомление — best effort, не валить тик
|
||||
|
||||
|
||||
def ladder_snapshot() -> str:
|
||||
"""Срез лестницы для Telegram: уровни, занятость, задачи, эскалации."""
|
||||
model = es.run_projections()
|
||||
cs = model.get("cycle_state") or {}
|
||||
order = ["sec", "min", "hour", "day"]
|
||||
lines = []
|
||||
for name in order:
|
||||
st = cs.get(name) or {}
|
||||
busy = "🔧 занят" if st.get("busy") else "ожидает"
|
||||
lines.append(f"{'🔹' if name != 'sec' else '⚡'} {name}: {busy}, тиков {st.get('ticks', 0)}"
|
||||
+ (f", ошибок {st['errors']}" if st.get("errors") else ""))
|
||||
tasks = list((model.get("tasks") or {}).values())
|
||||
tasks.sort(key=lambda t: t.get("created") or "", reverse=True)
|
||||
board = [t for t in tasks if t.get("status") in ("ready", "running")]
|
||||
lines.append(f"📋 готово/в работе: {sum(1 for t in tasks if t['status'] == 'ready')}/"
|
||||
f"{sum(1 for t in tasks if t['status'] == 'running')}")
|
||||
for t in board[:8]:
|
||||
lines.append(f" [{t['size']}] {t['status']}: {t['title'][:60]}")
|
||||
open_esc = [e for e in (model.get("escalations") or {}).values()
|
||||
if isinstance(e, dict) and e.get("status") == "open"]
|
||||
lines.append(f"🚨 открытых эскалаций: {len(open_esc)}")
|
||||
for e in open_esc[:5]:
|
||||
lines.append(f" [{e.get('severity', 'info')}] {e.get('reason', '')[:80]}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Единственный легитимный поллер — systemd-сервис simbiot-harness-tg.
|
||||
# `python -m harness.tg_inbox` с любыми аргументами (--help, list, …)
|
||||
# раньше поднимал второго поллера → 409 Conflict в легитимном.
|
||||
if len(sys.argv) > 1:
|
||||
print("usage: python -m harness.tg_inbox # long-poll поллер (systemd)")
|
||||
print(" cli.py ladder # срез лестницы (текст)")
|
||||
print(" cli.py tg-notify <text> # отправка в чат харнеса")
|
||||
sys.exit(0)
|
||||
poll_loop()
|
||||
Loading…
Reference in a new issue