- Add company module (3-tier org, CEO planning, parallel departments) - Add company orchestrator, knowledge extractor, presets, scheduler - Add company API endpoints, models, and frontend views - Add 今天吃啥 PWA app (69 dishes, real images, offline support) - Add team_projects output directory structure - Add unified manage.ps1 for service lifecycle - Add Windows startup guide v1.0 - Add TTS troubleshooting doc - Update frontend (AgentChat UX overhaul, new views) - Update backend (voice engine fix, multi-tenant, RBAC) - Remove deprecated startup scripts and old docs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
116 lines
3.6 KiB
Python
116 lines
3.6 KiB
Python
"""
|
|
公司定时调度服务 — 基于 APScheduler 周期执行公司项目
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
|
|
from apscheduler.schedulers.background import BackgroundScheduler
|
|
from apscheduler.triggers.cron import CronTrigger
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.models.company_schedule import CompanySchedule
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Global scheduler instance
|
|
_scheduler: Optional[BackgroundScheduler] = None
|
|
|
|
|
|
def get_scheduler() -> BackgroundScheduler:
|
|
"""获取全局调度器实例(懒初始化)。"""
|
|
global _scheduler
|
|
if _scheduler is None:
|
|
_scheduler = BackgroundScheduler()
|
|
_scheduler.start()
|
|
logger.info("CompanyScheduler started")
|
|
return _scheduler
|
|
|
|
|
|
def load_schedules(db: Session):
|
|
"""从数据库加载所有启用的调度并注册到调度器。"""
|
|
sched = get_scheduler()
|
|
schedules = db.query(CompanySchedule).filter(CompanySchedule.enabled.is_(True)).all()
|
|
|
|
for cs in schedules:
|
|
register_schedule(cs)
|
|
|
|
logger.info("Loaded %d company schedules from database", len(schedules))
|
|
|
|
|
|
def register_schedule(cs: CompanySchedule):
|
|
"""将单个调度注册到 APScheduler。"""
|
|
if not cs.cron_expression:
|
|
logger.warning("Schedule %s has no cron expression, skipping", cs.id)
|
|
return
|
|
|
|
sched = get_scheduler()
|
|
job_id = f"company_schedule_{cs.id}"
|
|
|
|
# Remove existing job if any
|
|
if sched.get_job(job_id):
|
|
sched.remove_job(job_id)
|
|
|
|
try:
|
|
trigger = CronTrigger.from_crontab(cs.cron_expression)
|
|
sched.add_job(
|
|
_execute_scheduled_project,
|
|
trigger=trigger,
|
|
id=job_id,
|
|
args=[cs.id],
|
|
name=cs.name,
|
|
replace_existing=True,
|
|
)
|
|
# Update next run time
|
|
job = sched.get_job(job_id)
|
|
if job and job.next_run_time:
|
|
cs.next_run_at = job.next_run_time
|
|
logger.info("Registered schedule: %s (cron: %s, next: %s)", cs.name, cs.cron_expression, cs.next_run_at)
|
|
except Exception as e:
|
|
logger.error("Failed to register schedule %s: %s", cs.id, e)
|
|
|
|
|
|
def unregister_schedule(schedule_id: str):
|
|
"""从调度器移除单个调度。"""
|
|
sched = get_scheduler()
|
|
job_id = f"company_schedule_{schedule_id}"
|
|
if sched.get_job(job_id):
|
|
sched.remove_job(job_id)
|
|
logger.info("Unregistered schedule: %s", schedule_id)
|
|
|
|
|
|
def shutdown_scheduler():
|
|
"""关闭调度器。"""
|
|
global _scheduler
|
|
if _scheduler:
|
|
_scheduler.shutdown(wait=False)
|
|
_scheduler = None
|
|
logger.info("CompanyScheduler shut down")
|
|
|
|
|
|
async def _execute_scheduled_project(schedule_id: str):
|
|
"""调度器回调:执行公司的定时项目。"""
|
|
from app.core.database import SessionLocal
|
|
from app.services.company_orchestrator import CompanyOrchestrator
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
cs = db.query(CompanySchedule).filter(CompanySchedule.id == schedule_id).first()
|
|
if not cs or not cs.enabled:
|
|
return
|
|
|
|
logger.info("CompanyScheduler: Executing schedule '%s' for company %s", cs.name, cs.company_id)
|
|
orchestrator = CompanyOrchestrator(db, cs.company_id, "system")
|
|
result = await orchestrator.execute(cs.project_goal)
|
|
|
|
cs.last_run_at = datetime.utcnow()
|
|
db.commit()
|
|
|
|
logger.info("CompanyScheduler: Schedule '%s' completed, success=%s", cs.name, result.get("success"))
|
|
except Exception as e:
|
|
logger.error("CompanyScheduler: Schedule '%s' failed: %s", schedule_id, e)
|
|
finally:
|
|
db.close()
|