from contextlib import asynccontextmanager
import io
import os
import sys
import logging
from dotenv import load_dotenv
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import uvicorn
from server.core.cancel_bus import start_cancel_listener, stop_cancel_listener
from server.core.database import Base, engine
from server.core.db_sync import run_database_sync
from server.core.runner_init import init_runner, shutdown_runner
from server.deepsearch.core.models.report_template import ReportTemplateDB
from server.deepsearch.core.models.web_search_engine_model import WebSearchEngineModel
from server.local_retrieval.models.knowledge_base import KnowledgeBaseDB
from server.local_retrieval.models.knowledge_base_document import KnowledgeBaseDocumentDB
from server.routers import register
logger = logging.getLogger(__name__)
backend_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if backend_dir not in sys.path:
sys.path.append(backend_dir)
project_root = backend_dir
load_dotenv(os.path.join(project_root, '.env'))
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
@asynccontextmanager
async def lifespan_func(input_app: FastAPI):
"""管理应用启动与关闭阶段的初始化和资源清理。"""
logger.info("🚀 Starting openJiuwen-DeepSearch Server...")
await init_runner()
await start_cancel_listener()
target_tables = [
WebSearchEngineModel.__table__,
ReportTemplateDB.__table__,
KnowledgeBaseDB.__table__,
KnowledgeBaseDocumentDB.__table__,
]
if engine.url.drivername == "sqlite":
renamed_count = 0
for table in target_tables:
if not hasattr(table, "indexes"):
logger.warning(f"Table {table.name} has no indexes attribute, skipping...")
continue
for idx in table.indexes:
old_idx_name = idx.name
idx.name = f"{old_idx_name}_{table.name}"
logger.info(f"{table.name}: Renamed index: {old_idx_name} ---> {idx.name}")
renamed_count += 1
logger.info(f"Duplicate index renaming completed. Total renamed indexes: {renamed_count}")
Base.metadata.create_all(
bind=engine,
tables=target_tables,
checkfirst=True
)
run_database_sync()
logger.info("✅ Database field sync completed")
yield
logger.info("🛑 Shutting down openJiuwen-DeepSearch Server...")
await stop_cancel_listener()
await shutdown_runner()
logger.info("✅ openJiuwen-DeepSearch Server shutdown completed")
app = FastAPI(
title="openJiuwen-DeepSearch API",
description="Backend API for openJiuwen-DeepSearch",
version="1.0.0",
docs_url="/api/docs",
redoc_url="/api/redoc",
openapi_url="/api/openapi.json",
lifespan=lifespan_func,
swagger_ui_init_oauth={
"usePkceWithAuthorizationCodeGrant": True,
"appName": "openJiuwen-DeepSearch API",
"clientId": "swagger-ui",
}
)
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000", "http://127.0.0.1:3000", "http://localhost:3069", "http://127.0.0.1:3069"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/")
async def root():
"""返回服务欢迎信息与基础入口地址。"""
return {
"message": "Welcome to openJiuwen-DeepSearch Server",
"docs": "/api/docs",
"health": "/api/health"
}
register.router_register(app)
def main():
"""读取运行配置并启动后端服务。"""
config = {
"host": os.getenv("HOST", "0.0.0.0"),
"port": int(os.getenv("BACKEND_PORT", "8000")),
"reload": False,
"log_level": "info",
"access_log": True,
"workers": int(os.getenv("WORKER_NUM", 1)),
}
logger.info("🚀 Starting openJiuwen-DeepSearch Server in development mode...")
logger.info(f"📍 Server will be available at: http://{config['host']}:{config['port']}")
logger.info(f"📚 API Documentation: http://{config['host']}:{config['port']}/api/docs")
logger.info(f"🔍 Health Check: http://{config['host']}:{config['port']}/api/health")
logger.info("🔄 Auto-reload enabled for development")
logger.info("⏹️ Press Ctrl+C to stop the server")
logger.info("-" * 60)
uvicorn.run(
"server.main:app",
loop="asyncio",
**config
)
if __name__ == "__main__":
main()