import logging from fastapi import FastAPI, Depends, HTTPException, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse from sqlalchemy.orm import Session from datetime import datetime from pathlib import Path from .database import engine, get_db, init_db from .models import Base from .api import topics, system, articles, publishing, auth, admin, audit, optimizer_logs, cases, task_logs, llm_configs, system_configs, topic_config, calendar, metrics, assets, tasks, platform_config, collector_mgmt, assistant from .initial_data import import_initial_data from .core.scheduler import scheduler app = FastAPI(title="宇之然内容创作平台", version="0.1.0") logger = logging.getLogger(__name__) @app.on_event("startup") def start_scheduler(): scheduler.start() logger.info("Background scheduler started") @app.on_event("shutdown") def stop_scheduler(): scheduler.shutdown() logger.info("Background scheduler shut down") # CORS - local-only origins _local_origins = [ "http://localhost:8001", "http://127.0.0.1:8001", "http://localhost:8000", "http://127.0.0.1:8000", ] app.add_middleware( CORSMiddleware, allow_origins=_local_origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # 初始化数据库 Base.metadata.create_all(bind=engine) init_db() import_initial_data() # 清理超时任务(运行超过 1 小时的任务视为故障) from datetime import datetime, timezone, timedelta from .database import SessionLocal from .models import ContentTask try: cleanup_db = SessionLocal() cutoff = datetime.now(timezone.utc) - timedelta(hours=1) stale = cleanup_db.query(ContentTask).filter( ContentTask.status == "running", ContentTask.started_at.isnot(None), ContentTask.started_at < cutoff ).all() for t in stale: t.status = "failed" t.error_msg = "任务超时(服务重启导致状态丢失)" if t.started_at: t.duration = int((datetime.now(timezone.utc) - t.started_at).total_seconds()) logger.warning(f"标记超时任务: {t.task_id}") if stale: cleanup_db.commit() logger.info(f"已清理 {len(stale)} 个超时任务") cleanup_db.close() except Exception as e: logger.warning(f"任务清理失败: {e}") # 注册 API 路由 app.include_router(topics.router) app.include_router(system.router) app.include_router(articles.router) app.include_router(publishing.router) app.include_router(auth.router) app.include_router(admin.router) app.include_router(audit.router) app.include_router(optimizer_logs.router) app.include_router(cases.router) app.include_router(task_logs.router) app.include_router(llm_configs.router) app.include_router(system_configs.router) app.include_router(topic_config.router) app.include_router(calendar.router) app.include_router(metrics.router) app.include_router(assets.router) app.include_router(tasks.router) app.include_router(platform_config.router) app.include_router(collector_mgmt.router) app.include_router(assistant.router) # 挂载前端 FRONTEND_DIR = Path(__file__).parent.parent.parent / "frontend" if FRONTEND_DIR.exists() and (FRONTEND_DIR / "index.html").exists(): app.mount("/", StaticFiles(directory=str(FRONTEND_DIR), html=True), name="frontend") logger.info(f"Frontend mounted at / from {FRONTEND_DIR}") # SW 特殊处理 sw_path = FRONTEND_DIR / "sw.js" if sw_path.exists(): @app.get("/sw.js") async def service_worker(): return FileResponse( sw_path, media_type="application/javascript", headers={"Cache-Control": "no-cache", "Service-Worker-Allowed": "/"} ) # 离线页面 offline_path = FRONTEND_DIR / "offline.html" if offline_path.exists(): @app.get("/offline.html") async def offline_page(): return FileResponse(offline_path, media_type="text/html") else: @app.get("/") def root(): return {"service": "API only", "docs": "/docs"} if __name__ == "__main__": import uvicorn uvicorn.run("app.main:app", host="0.0.0.0", port=8001)