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("msg_id", help="id входящего (m-NNN)")
|
||||||
p_tgr.add_argument("text")
|
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 = sub.add_parser("escalate", help="эскалация важного на уровень выше (min→hour→day)")
|
||||||
p_esc.add_argument("reason")
|
p_esc.add_argument("reason")
|
||||||
p_esc.add_argument("-f", "--from-level", default="min", choices=["min", "hour"],
|
p_esc.add_argument("-f", "--from-level", default="min", choices=["min", "hour"],
|
||||||
|
|
@ -85,8 +89,21 @@ def main():
|
||||||
elif args.cmd == "tg-reply":
|
elif args.cmd == "tg-reply":
|
||||||
es.init_db()
|
es.init_db()
|
||||||
from harness import tg_inbox
|
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)
|
mid = tg_inbox.send_reply(args.msg_id, args.text)
|
||||||
print(f"OK: ответ на {mid} отправлен")
|
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":
|
elif args.cmd == "escalate":
|
||||||
es.init_db()
|
es.init_db()
|
||||||
eid, to_level = commands.raise_escalation(
|
eid, to_level = commands.raise_escalation(
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ getUpdates offset=last+1. Идемпотентность: update_id → aggregat
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
@ -153,5 +154,60 @@ def poll_loop():
|
||||||
time.sleep(5)
|
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__":
|
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()
|
poll_loop()
|
||||||
Loading…
Reference in a new issue