chore: auto-commit from dashboard

This commit is contained in:
2026-04-02 17:43:13 +00:00
parent 006123a63b
commit d9450ce70d
4 changed files with 164 additions and 0 deletions

View File

@@ -112,6 +112,27 @@ def main():
interval_min, hb_channel,
)
# Newsletter Cercetași checker (optional)
newsletter_config = config.get("newsletter_cercetasi", {})
if newsletter_config.get("enabled"):
from src.newsletter_cercetasi import check_and_send as check_newsletter
from apscheduler.triggers.cron import CronTrigger as _CronTrigger
async def _newsletter_tick() -> None:
try:
await check_newsletter(config, _send_to_channel)
except Exception as exc:
logger.error("Newsletter checker failed: %s", exc)
nl_cron = newsletter_config.get("cron", "0 9 * * *")
scheduler._scheduler.add_job(
_newsletter_tick,
trigger=_CronTrigger.from_crontab(nl_cron),
id="__newsletter_cercetasi__",
max_instances=1,
)
logger.info("Newsletter Cercetasi checker registered (cron: %s)", nl_cron)
# Telegram bot (optional — only if telegram_token exists)
telegram_token = get_secret("telegram_token")
telegram_app = None

134
src/newsletter_cercetasi.py Normal file
View File

@@ -0,0 +1,134 @@
"""Newsletter Cercetași checker — detects new editions and sends WhatsApp summaries."""
import asyncio
import json
import logging
import subprocess
from datetime import datetime, timezone
from pathlib import Path
from typing import Awaitable, Callable
import httpx
from src.claude_session import CLAUDE_BIN, PROJECT_ROOT, _safe_env, build_system_prompt
log = logging.getLogger("echo-core.newsletter-cercetasi")
NEWSLETTER_BASE_URL = "https://cercetaiis-newsletter.beehiiv.com/p/newsletter-{n}-din-{year}"
STATE_FILE = PROJECT_ROOT / "cron" / "newsletter-cercetasi-state.json"
KB_PROMPT_FILE = (
PROJECT_ROOT / "memory" / "kb" / "projects" / "grup-sprijin" / "prompt-newsletter-cercetasi.md"
)
CLAUDE_TIMEOUT = 120
def _read_state() -> dict:
try:
return json.loads(STATE_FILE.read_text())
except (FileNotFoundError, json.JSONDecodeError):
return {"last_sent": 0, "year": datetime.now(timezone.utc).year}
def _write_state(state: dict) -> None:
STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
STATE_FILE.write_text(json.dumps(state, indent=2, ensure_ascii=False) + "\n")
async def _newsletter_exists(n: int, year: int) -> bool:
"""Return True if newsletter #{n}/{year} returns HTTP 200."""
url = NEWSLETTER_BASE_URL.format(n=n, year=year)
try:
async with httpx.AsyncClient(follow_redirects=True) as client:
resp = await client.get(url, timeout=10)
return resp.status_code == 200
except Exception as e:
log.debug("Newsletter #%d/%d check failed: %s", n, year, e)
return False
def _generate_summary(n: int, year: int) -> str | None:
"""Run Claude CLI to generate summary for newsletter #{n}/{year}. Returns text or None."""
url = NEWSLETTER_BASE_URL.format(n=n, year=year)
try:
kb_prompt = KB_PROMPT_FILE.read_text()
except FileNotFoundError:
log.error("KB prompt file not found: %s", KB_PROMPT_FILE)
return None
prompt = (
f"Newsletter-ul Cercetașilor #{n}/{year} este disponibil la: {url}\n\n"
f"Urmează instrucțiunile de mai jos pentru a genera rezumatul:\n\n"
f"{kb_prompt}"
)
cmd = [
CLAUDE_BIN, "-p", prompt,
"--model", "sonnet",
"--output-format", "json",
"--allowedTools", "WebFetch",
]
try:
system_prompt = build_system_prompt()
cmd += ["--system-prompt", system_prompt]
except FileNotFoundError:
pass
try:
proc = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=CLAUDE_TIMEOUT,
env=_safe_env(),
cwd=PROJECT_ROOT,
)
if proc.returncode != 0:
log.error("Claude CLI error (exit %d): %s", proc.returncode, proc.stderr[:300])
return None
data = json.loads(proc.stdout)
return data.get("result", "").strip() or None
except subprocess.TimeoutExpired:
log.error("Claude CLI timed out for newsletter #%d", n)
return None
except (json.JSONDecodeError, Exception) as e:
log.error("Failed to generate newsletter summary: %s", e)
return None
async def check_and_send(config, send_callback) -> None:
"""Check for new newsletter; if found, generate summary and send via callback."""
state = _read_state()
current_year = datetime.now(timezone.utc).year
# New year → reset counter
if state.get("year", current_year) != current_year:
log.info("New year detected (%d%d), resetting newsletter counter", state["year"], current_year)
state = {"last_sent": 0, "year": current_year}
next_n = state["last_sent"] + 1
log.info("Checking for Cercetasi newsletter #%d/%d...", next_n, current_year)
if not await _newsletter_exists(next_n, current_year):
log.info("Newsletter #%d/%d not yet available", next_n, current_year)
return
log.info("Newsletter #%d/%d found! Generating summary...", next_n, current_year)
summary = await asyncio.to_thread(_generate_summary, next_n, current_year)
if not summary:
log.error("Failed to generate summary for newsletter #%d/%d", next_n, current_year)
return
channel = config.get("newsletter_cercetasi.channel", "echo-core")
try:
await send_callback(channel, summary)
state["last_sent"] = next_n
state["year"] = current_year
state["last_sent_at"] = datetime.now(timezone.utc).isoformat()
_write_state(state)
log.info("Newsletter #%d/%d summary sent to channel '%s'", next_n, current_year, channel)
except Exception as e:
log.error("Send failed for newsletter #%d/%d — will retry next check: %s", next_n, current_year, e)