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
+1
-1
@@ -299,7 +299,7 @@ Docker CE 29.9 + Compose v5.6 из репо download.docker.com (в Debian ст
|
||||
- Проект `tg_export` (Telethon) перенесён с Mac: код, юниты и установка — [`stacks/rpi-tg-export/`](../stacks/rpi-tg-export/README.md). На малине — **второй аккаунт владельца**, докачка **только текста** раз в сутки (`tg-export.timer`, 04:30, `run --no-media --archived`), выгрузка в `/mnt/backup/tg/` (tgexport:astra 2750/640 — Astra читает). Старая выгрузка первого аккаунта (~14 ГБ) — на Mac, не переносилась (решение владельца).
|
||||
- Отдельный системный пользователь `tgexport` (без шелла): сессия `/var/lib/tg-export/tg.session` (600) = полный доступ к аккаунту — Astra её не видит; ключи приложения `/etc/tg-export/env` (root:tgexport 640, те же `api_id`/`api_hash`, что на Mac). Вход — владелец: `ssh -t rpi sudo -u tgexport tg-export login`.
|
||||
- Мониторинг — `backup-metrics backup_tg_export` → `BackupFailed`/`BackupStale` (>30 ч). ⚠️ Первый прогон может идти дольше суток из-за FloodWait.
|
||||
- **Поиск и сводка** (с 2026-10-18): докачка каждый час (было раз в сутки 04:30) + индекс `/mnt/backup/tg/index.db` (FTS5 + эмбеддинги potion-multilingual-128M, fastembed в venv, модели `/opt/tg-export/models`); `tg search` для Astra, темы `tg topic`, сводка `tg-report.timer` 08:00 Asia/Nicosia через Codex → очередь outbox. Навык и правила — astra-bot `11dd4fc`. Служба `tg-embed` (astra) держит модель эмбеддингов в памяти для быстрых запросов (~6 с вместо ~40). Первая полная выгрузка 2026-10-18 19:49: 12 чатов, 303 тыс. сообщений с 2024-10-18, индекс 85 580 кусков, 1,2 ГБ на HDD. Подробно — README стека. Вход во второй аккаунт (@mikpokrovsky) — 2026-10-18; выгружаются только 6 выбранных владельцем чатов (`--only`, см. README стека). ⚠️ 2026-10-18 18:45–18:51 по ошибке шла выгрузка всех групп и каналов (успела только группа Python, 35 500 сообщений) — остановлена, данные удалены по решению владельца. ⚠️ Таймер выгрузки **выключен** до вступления владельца в @cyprusdiy, @cymedic и Cyprus Car – Chat; сводка включена, без тем и индекса молчит.
|
||||
- **Поиск и сводка** (с 2026-10-18): докачка каждый час (было раз в сутки 04:30) + индекс `/mnt/backup/tg/index.db` (FTS5 + эмбеддинги potion-multilingual-128M, fastembed в venv, модели `/opt/tg-export/models`); `tg search` для Astra, темы `tg topic`, сводка `tg-report.timer` 08:00 Asia/Nicosia через Codex → очередь outbox. Навык и правила — astra-bot `11dd4fc`. Вечерний рекап группы целиком — `tg-digest.timer` 23:50 Asia/Nicosia (`tg digest`, сейчас @cyfasttrack). Служба `tg-embed` (astra) держит модель эмбеддингов в памяти для быстрых запросов (~6 с вместо ~40). Первая полная выгрузка 2026-10-18 19:49: 12 чатов, 303 тыс. сообщений с 2024-10-18, индекс 85 580 кусков, 1,2 ГБ на HDD. Подробно — README стека. Вход во второй аккаунт (@mikpokrovsky) — 2026-10-18; выгружаются только 6 выбранных владельцем чатов (`--only`, см. README стека). ⚠️ 2026-10-18 18:45–18:51 по ошибке шла выгрузка всех групп и каналов (успела только группа Python, 35 500 сообщений) — остановлена, данные удалены по решению владельца. ⚠️ Таймер выгрузки **выключен** до вступления владельца в @cyprusdiy, @cymedic и Cyprus Car – Chat; сводка включена, без тем и индекса молчит.
|
||||
- Доступ: тексты — только для владельца; Оле и Поле закрыто (правило в `codex/AGENTS.md` и скилл `tg`, astra-bot `27bf8e6`), как почта.
|
||||
|
||||
## Читалка books → Obsidian (с 2026-10-18)
|
||||
|
||||
@@ -58,3 +58,8 @@ sudo systemctl enable --now tg-export.timer
|
||||
не лимит Claude читалки) → очередь `/var/lib/astra-google/outbox/` → `gcal remind` шлёт владельцу.
|
||||
⚠️ Очередь шлёт одним `sendMessage` без разбиения: сводка режется на части ≤ 3800 знаков (иначе файл > 4096
|
||||
застрял бы и заблокировал очередь). Тем нет или индекса нет — ничего не шлёт. `tg report --dry` — посмотреть.
|
||||
- **Вечерний рекап группы целиком** (с 2026-10-18): `tg digest add @чат` (список — `/var/lib/tg-report/digests.json`),
|
||||
`tg-digest.timer` 23:50 Asia/Nicosia → `tg digest run`: все куски чата **с прошлого рекапа** (не календарный день —
|
||||
сообщения после последней ежечасной докачки уйдут в следующий рекап, без дыр), до 60 тыс. знаков (больше — последние),
|
||||
Codex пишет темы дня и «Полезное» со ссылками → очередь outbox. Сейчас: CY Fast Track Talks (@cyfasttrack).
|
||||
`tg digest run --dry --hours 24` — посмотреть.
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=Telegram: вечерний рекап выбранных групп целиком (Codex) → Telegram
|
||||
After=network-online.target tg-export.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=astra
|
||||
Group=astra
|
||||
ExecStart=/usr/local/bin/tg digest run
|
||||
TimeoutStartSec=30min
|
||||
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Telegram: рекап групп каждый день в 23:50 (Кипр)
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 23:50 Asia/Nicosia
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
Reference in New Issue
Block a user