rpi: tg digest — вечерний рекап группы целиком (23:50 Кипр, Codex), CY Fast Track Talks
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
dfe0fad728
commit
15a9e1d2de
@@ -6,6 +6,8 @@
|
||||
tg topic add "ТЕМА" [--query "что искать"] | list | del НОМЕР
|
||||
темы ежедневной сводки (query — поисковая формулировка, по умолчанию — сама тема)
|
||||
tg report [--hours 24] [--dry] сводка по темам за период → владельцу в Telegram (--dry — только напечатать)
|
||||
tg digest add @ЧАТ | list | del @ЧАТ чаты для ежедневного рекапа целиком (всё за день, а не по темам)
|
||||
tg digest run [--hours N] [--dry] рекап этих чатов с прошлого рекапа → владельцу в Telegram (таймер 23:50)
|
||||
tg stats сколько чатов/сообщений/кусков, когда обновлялось
|
||||
tg index (сервис tg-export, юзер tgexport) — дописать в индекс новые сообщения выгрузки
|
||||
tg serve (служба tg-embed, юзер astra) — держать модель в памяти, векторы запросов по unix-сокету
|
||||
@@ -377,6 +379,119 @@ REPORT_PROMPT = """Ты готовишь владельцу утреннюю с
|
||||
{data}"""
|
||||
|
||||
|
||||
DIGEST_PROMPT = """Ты готовишь владельцу вечерний рекап Telegram-группы «{title}» за период {period}.
|
||||
Ниже — вся переписка группы за этот период по кускам (время, ссылка на первое сообщение куска, текст «Автор: сообщение»).
|
||||
|
||||
Сделай рекап по-русски, простым текстом для Telegram без Markdown:
|
||||
- 3–8 главных тем дня, по каждой — 1–3 строки сути: о чём спорили, что выяснили, какие советы и ответы дали;
|
||||
- отдельно «Полезное»: конкретика — места, контакты, цены, сроки, ссылки, документы, лайфхаки — с ссылкой на сообщение;
|
||||
- если был важный вопрос без ответа — упомяни;
|
||||
- флуд, приветствия, рекламу и оффтоп пропусти. Ничего не выдумывай, только по переписке.
|
||||
Темы — строкой «▸ Тема», пункты — «• …». Без вступлений и выводов. Если переписки почти не было — одной строкой.
|
||||
|
||||
{data}"""
|
||||
DIGEST_MAX = 60_000 # знаков переписки в один запрос к Codex (больше — берём последние)
|
||||
|
||||
|
||||
def digests():
|
||||
f = REPORT / "digests.json"
|
||||
return json.loads(f.read_text()) if f.exists() else {"chats": [], "last": {}}
|
||||
|
||||
|
||||
def save_digests(d):
|
||||
REPORT.mkdir(parents=True, exist_ok=True)
|
||||
(REPORT / "digests.json").write_text(json.dumps(d, ensure_ascii=False, indent=1))
|
||||
|
||||
|
||||
def find_chat(c, needle):
|
||||
n = needle.lstrip("@").lower()
|
||||
for r in c.execute("SELECT id, title, username FROM chat"):
|
||||
if (r["username"] or "").lower() == n or str(r["id"]) == needle or n in (r["title"] or "").lower():
|
||||
return r
|
||||
return None
|
||||
|
||||
|
||||
def queue_text(text, tag):
|
||||
OUTBOX.mkdir(parents=True, exist_ok=True)
|
||||
parts, cur = [], ""
|
||||
for para in text.split("\n"):
|
||||
if len(cur) + len(para) + 1 > TG_MAX and cur:
|
||||
parts.append(cur)
|
||||
cur = ""
|
||||
cur += para[:TG_MAX] + "\n"
|
||||
if cur.strip():
|
||||
parts.append(cur)
|
||||
old = os.umask(0o007)
|
||||
try:
|
||||
stamp = dt.datetime.now().strftime("%Y%m%d%H%M%S%f")
|
||||
for i, p in enumerate(parts):
|
||||
(OUTBOX / f"{stamp}-{tag}{i:02d}.txt").write_text(p.strip())
|
||||
finally:
|
||||
os.umask(old)
|
||||
return len(parts)
|
||||
|
||||
|
||||
def cmd_digest(a):
|
||||
d = digests()
|
||||
if a.op == "list":
|
||||
for x in d["chats"]:
|
||||
last = d["last"].get(str(x["id"]))
|
||||
print(f"{x['title']} (@{x.get('username') or x['id']})" + (f" — последний рекап {dt.datetime.fromtimestamp(last, TZ):%d.%m %H:%M}" if last else ""))
|
||||
if not d["chats"]:
|
||||
print("Чатов для рекапа нет. Добавить: tg digest add @чат")
|
||||
return
|
||||
if a.op in ("add", "del"):
|
||||
c = db()
|
||||
r = find_chat(c, a.chat)
|
||||
if a.op == "add":
|
||||
if not r:
|
||||
raise SystemExit(f"{a.chat} нет в индексе — чат выгружается? (tg stats)")
|
||||
if all(x["id"] != r["id"] for x in d["chats"]):
|
||||
d["chats"].append({"id": r["id"], "title": r["title"], "username": r["username"]})
|
||||
print(f"Ежедневный рекап: {r['title']}")
|
||||
else:
|
||||
before = len(d["chats"])
|
||||
d["chats"] = [x for x in d["chats"] if not (r and x["id"] == r["id"]) and x.get("username") != a.chat.lstrip("@")]
|
||||
print("Убран." if len(d["chats"]) < before else "Такого чата в рекапе нет.")
|
||||
save_digests(d)
|
||||
return
|
||||
# run
|
||||
if not d["chats"]:
|
||||
print("Чатов для рекапа нет.")
|
||||
return
|
||||
if not DB.exists():
|
||||
print("Индекса ещё нет — рекап не делаю.")
|
||||
return
|
||||
c = db()
|
||||
now = int(time.time())
|
||||
for x in d["chats"]:
|
||||
since = now - a.hours * 3600 if a.hours else d["last"].get(str(x["id"]), now - 86400)
|
||||
rows = c.execute("SELECT * FROM chunk WHERE chat_id=? AND ts1 > ? ORDER BY ts0", (x["id"], since)).fetchall()
|
||||
chat = {"id": x["id"], "username": x.get("username")}
|
||||
blocks, total = [], 0
|
||||
for r in reversed(rows): # не влезает — берём последние
|
||||
b = f"── {dt.datetime.fromtimestamp(r['ts0'], TZ):%d.%m %H:%M} · {link(chat, r['first_id'])}\n{r['text']}"
|
||||
if total + len(b) > DIGEST_MAX:
|
||||
break
|
||||
blocks.append(b)
|
||||
total += len(b)
|
||||
blocks.reverse()
|
||||
period = f"{dt.datetime.fromtimestamp(since, TZ):%d.%m %H:%M} — {dt.datetime.fromtimestamp(now, TZ):%d.%m %H:%M}"
|
||||
n_msgs = sum(r["n"] for r in rows)
|
||||
if not rows:
|
||||
text = f"🗒 {x['title']} за {period}: сообщений не было."
|
||||
else:
|
||||
body = codex(DIGEST_PROMPT.format(title=x["title"], period=period, data="\n\n".join(blocks)))
|
||||
cut = f" (в рекапе последние {len(blocks)} из {len(rows)} кусков)" if len(blocks) < len(rows) else ""
|
||||
text = f"🗒 {x['title']} — рекап за {period}, сообщений {n_msgs}{cut}\n\n{body}"
|
||||
if a.dry:
|
||||
print(text)
|
||||
continue
|
||||
print(f"{x['title']}: в очереди {queue_text(text, 'dg')} сообщ.")
|
||||
d["last"][str(x["id"])] = now
|
||||
save_digests(d)
|
||||
|
||||
|
||||
def cmd_report(a):
|
||||
t = topics()
|
||||
if not t:
|
||||
@@ -396,25 +511,10 @@ def cmd_report(a):
|
||||
if a.dry:
|
||||
print(text)
|
||||
return
|
||||
OUTBOX.mkdir(parents=True, exist_ok=True)
|
||||
parts, cur = [], ""
|
||||
for para in text.split("\n"):
|
||||
if len(cur) + len(para) + 1 > TG_MAX and cur:
|
||||
parts.append(cur)
|
||||
cur = ""
|
||||
cur += para[:TG_MAX] + "\n"
|
||||
if cur.strip():
|
||||
parts.append(cur)
|
||||
old = os.umask(0o007)
|
||||
try:
|
||||
stamp = dt.datetime.now().strftime("%Y%m%d%H%M%S%f")
|
||||
for i, p in enumerate(parts):
|
||||
(OUTBOX / f"{stamp}-tg{i:02d}.txt").write_text(p.strip())
|
||||
finally:
|
||||
os.umask(old)
|
||||
n = queue_text(text, "tg")
|
||||
REPORT.mkdir(parents=True, exist_ok=True)
|
||||
(REPORT / "last-report.txt").write_text(text)
|
||||
print(f"сводка поставлена в очередь: {len(parts)} сообщ.")
|
||||
print(f"сводка поставлена в очередь: {n} сообщ.")
|
||||
|
||||
|
||||
def cmd_serve(a):
|
||||
@@ -451,6 +551,11 @@ def main():
|
||||
r = ts.add_parser("del"); r.add_argument("num")
|
||||
ts.add_parser("list")
|
||||
s = sub.add_parser("report"); s.add_argument("--hours", type=int, default=24); s.add_argument("--dry", action="store_true")
|
||||
s = sub.add_parser("digest"); ds = s.add_subparsers(dest="op", required=True)
|
||||
r = ds.add_parser("add"); r.add_argument("chat")
|
||||
r = ds.add_parser("del"); r.add_argument("chat")
|
||||
ds.add_parser("list")
|
||||
r = ds.add_parser("run"); r.add_argument("--hours", type=int, default=0); r.add_argument("--dry", action="store_true")
|
||||
sub.add_parser("stats")
|
||||
s = sub.add_parser("index"); s.add_argument("--quiet", action="store_true")
|
||||
sub.add_parser("serve")
|
||||
|
||||
Reference in New Issue
Block a user