stage-8: cron scheduler with APScheduler

Scheduler class, cron/jobs.json, Discord /cron commands, CLI cron subcommand, job lifecycle management. 88 new tests (281 total).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
MoltBot Service
2026-02-13 16:12:56 +00:00
parent 09d3de003a
commit 24a4d87f8c
8 changed files with 1640 additions and 1 deletions

View File

@@ -9,7 +9,8 @@ from pathlib import Path
from src.config import load_config
from src.secrets import get_secret
from src.adapters.discord_bot import create_bot
from src.adapters.discord_bot import create_bot, split_message
from src.scheduler import Scheduler
PROJECT_ROOT = Path(__file__).resolve().parent.parent
PID_FILE = PROJECT_ROOT / "echo-core.pid"
@@ -43,6 +44,26 @@ def main():
config = load_config()
client = create_bot(config)
# Scheduler setup
async def _send_to_channel(channel_alias: str, text: str) -> None:
"""Callback: resolve alias and send text to Discord channel."""
channels = config.get("channels", {})
ch_info = channels.get(channel_alias)
if not ch_info:
logger.warning("Cron: unknown channel alias '%s'", channel_alias)
return
channel_id = ch_info.get("id")
channel = client.get_channel(int(channel_id))
if channel is None:
logger.warning("Cron: channel %s not found in Discord cache", channel_id)
return
chunks = split_message(text)
for chunk in chunks:
await channel.send(chunk)
scheduler = Scheduler(send_callback=_send_to_channel, config=config)
client.scheduler = scheduler # type: ignore[attr-defined]
# PID file
PID_FILE.write_text(str(os.getpid()))
@@ -51,6 +72,7 @@ def main():
def handle_signal(sig, frame):
logger.info("Received signal %s, shutting down...", sig)
loop.create_task(scheduler.stop())
loop.create_task(client.close())
signal.signal(signal.SIGTERM, handle_signal)
@@ -59,6 +81,7 @@ def main():
try:
loop.run_until_complete(client.start(token))
except KeyboardInterrupt:
loop.run_until_complete(scheduler.stop())
loop.run_until_complete(client.close())
finally:
PID_FILE.unlink(missing_ok=True)