00466a9801
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>
98 lines
3.3 KiB
Python
98 lines
3.3 KiB
Python
"""
|
|
Plugin manifest and settings endpoints for doc-service.
|
|
|
|
These are internal-only — they are called by the main backend's generic plugin
|
|
proxy, never directly by the browser. No authentication is applied here because
|
|
the backend enforces access control before forwarding the request.
|
|
|
|
Endpoints:
|
|
GET /plugin/manifest → static manifest with JSON Schema for settings
|
|
GET /plugin/settings → current storage config values
|
|
PATCH /plugin/settings → update storage config (partial update)
|
|
"""
|
|
from fastapi import APIRouter
|
|
from pydantic import BaseModel
|
|
|
|
from app.services.config_reader import get_storage_config, save_storage_config
|
|
|
|
router = APIRouter()
|
|
|
|
_MANIFEST: dict = {
|
|
"id": "doc-service",
|
|
"name": "Document Service",
|
|
"icon": "file-text",
|
|
"version": "1.0",
|
|
"access": {
|
|
"allow_superuser": True,
|
|
"required_groups": ["doc-service-admin"],
|
|
},
|
|
"settings_schema": {
|
|
"type": "object",
|
|
"title": "Storage & Watch",
|
|
"properties": {
|
|
"watch_enabled": {
|
|
"type": "boolean",
|
|
"title": "Enable file watching",
|
|
"description": (
|
|
"Automatically ingest PDF files added to the mounted watch directory. "
|
|
"Requires a service restart to take effect after toggling."
|
|
),
|
|
},
|
|
"watch_path": {
|
|
"type": "string",
|
|
"title": "Watch path",
|
|
"readOnly": True,
|
|
"description": "Configured via Docker volume mount — edit docker-compose to change.",
|
|
},
|
|
"ai_folder_suggestion": {
|
|
"type": "boolean",
|
|
"title": "AI folder suggestion",
|
|
"description": (
|
|
"AI suggests a category for each ingested document. "
|
|
"You must confirm the suggestion before it is applied."
|
|
),
|
|
},
|
|
"ai_folder_default": {
|
|
"type": "string",
|
|
"title": "Default import category",
|
|
"description": "Category assigned automatically when AI folder suggestion is disabled.",
|
|
},
|
|
"ai_rename_suggestion": {
|
|
"type": "boolean",
|
|
"title": "AI rename suggestion",
|
|
"description": (
|
|
"AI suggests a document title for each ingested file. "
|
|
"You must confirm before it is applied."
|
|
),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
class StorageSettingsUpdate(BaseModel):
|
|
watch_enabled: bool | None = None
|
|
ai_folder_suggestion: bool | None = None
|
|
ai_folder_default: str | None = None
|
|
ai_rename_suggestion: bool | None = None
|
|
# watch_path is intentionally excluded — it cannot be changed via API
|
|
|
|
|
|
@router.get("/manifest")
|
|
async def get_manifest() -> dict:
|
|
return _MANIFEST
|
|
|
|
|
|
@router.get("/settings")
|
|
async def get_settings() -> dict:
|
|
return await get_storage_config()
|
|
|
|
|
|
@router.patch("/settings")
|
|
async def update_settings(body: StorageSettingsUpdate) -> dict:
|
|
update = body.model_dump(exclude_none=True)
|
|
if "ai_folder_default" in update:
|
|
update["ai_folder_default"] = update["ai_folder_default"][:128].strip() or "imports"
|
|
await save_storage_config(update)
|
|
return await get_storage_config()
|