Add generic plugin architecture and watch-directory feature

Introduces a manifest contract so feature containers self-describe their
settings (JSON Schema + access rules). Backend and frontend gain generic
plugin proxy and dynamic Extensions UI with zero feature-specific code.

Doc-service is the first plugin consumer: exposes /plugin/manifest and
/plugin/settings, adds a watchdog-based file watcher that auto-ingests
PDFs from a mounted directory, maps subfolders to categories, supports
AI-suggested folder/filename (user-confirmed), and enforces a no-remove
policy. Access is gated by is_superuser or doc-service-admin group.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-04-18 02:09:50 +02:00
parent 2d7207b62f
commit 00466a9801
29 changed files with 1373 additions and 52 deletions
+31 -1
View File
@@ -1,15 +1,45 @@
import asyncio
import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI
from app.core.config import settings
from app.routers import categories, documents
from app.routers import plugin as plugin_router
app = FastAPI(title=settings.PROJECT_NAME)
logger = logging.getLogger(__name__)
@asynccontextmanager
async def lifespan(app: FastAPI):
loop = asyncio.get_running_loop()
watcher = None
try:
from app.services.config_reader import get_storage_config
storage_config = await get_storage_config()
if storage_config.get("watch_enabled"):
from app.services.file_watcher import FileWatcherService
watcher = FileWatcherService(loop)
await watcher.start(storage_config["watch_path"], storage_config)
except Exception as exc:
logger.warning("[doc-service] File watcher could not start: %s", exc)
yield
if watcher is not None:
await watcher.stop()
app = FastAPI(title=settings.PROJECT_NAME, lifespan=lifespan)
# No CORS — this service is only reachable from the main backend on backend-net.
# All browser traffic goes through the main backend proxy.
app.include_router(documents.router, prefix="/documents", tags=["documents"])
app.include_router(categories.router, prefix="/categories", tags=["categories"])
app.include_router(plugin_router.router, prefix="/plugin", tags=["plugin"])
@app.get("/health")