|
|
@@ -16,6 +16,7 @@ import sys
|
|
|
from typing import Optional
|
|
|
|
|
|
from fastapi import APIRouter, Request, HTTPException
|
|
|
+from pydantic import BaseModel
|
|
|
|
|
|
from fastapi.responses import JSONResponse
|
|
|
|
|
|
@@ -153,6 +154,95 @@ async def setup_health():
|
|
|
}
|
|
|
|
|
|
|
|
|
+# ─────────────────────────────────────────────────────────────────────
|
|
|
+# FR-002 — data directory (first-run wizard shows + lets desktop users
|
|
|
+# choose where their research data lives)
|
|
|
+# ─────────────────────────────────────────────────────────────────────
|
|
|
+
|
|
|
+class DataDirRequest(BaseModel):
|
|
|
+ data_dir: str
|
|
|
+
|
|
|
+
|
|
|
+@router.get("/data-dir")
|
|
|
+async def get_data_dir(request: Request):
|
|
|
+ """Report the current data directory + its sub-dirs (FR-002).
|
|
|
+
|
|
|
+ Loopback-only — the data dir is a local-filesystem concern and there
|
|
|
+ is no reason a remote caller needs it. Surfaces enough for the wizard
|
|
|
+ to show "your data lives here" with the standard sub-folders.
|
|
|
+ """
|
|
|
+ _require_loopback(request)
|
|
|
+ from agentpaas.config import settings
|
|
|
+
|
|
|
+ return {
|
|
|
+ "data_dir": settings.data_dir,
|
|
|
+ "is_default": not bool(
|
|
|
+ os.getenv("AGENTPAAS_DATA_DIR") or _data_dir_override()
|
|
|
+ ),
|
|
|
+ "mode": settings.deployment_mode,
|
|
|
+ "subdirs": {
|
|
|
+ "instances": settings.instances_dir,
|
|
|
+ "knowledge_bases": settings.knowledge_bases_dir,
|
|
|
+ "logs": settings.logs_dir,
|
|
|
+ },
|
|
|
+ # Changing data_dir needs a restart because DB/dir handles are
|
|
|
+ # bound at process startup. The wizard tells the user this.
|
|
|
+ "change_requires_restart": True,
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+@router.put("/data-dir")
|
|
|
+async def set_data_dir(req: DataDirRequest, request: Request):
|
|
|
+ """Persist a chosen data directory to ~/.agentpaas/config.json (FR-002).
|
|
|
+
|
|
|
+ Loopback-only. Validates that the path's parent exists and is
|
|
|
+ writable, creates the dir if needed, then writes it to config.json so
|
|
|
+ `_default_data_dir()` picks it up on the next startup. Returns
|
|
|
+ change_requires_restart=true — the running process keeps using the
|
|
|
+ old dir until restarted (DB handle is already open).
|
|
|
+ """
|
|
|
+ _require_loopback(request)
|
|
|
+
|
|
|
+ target = os.path.abspath(os.path.expanduser(req.data_dir.strip()))
|
|
|
+ if not target:
|
|
|
+ raise HTTPException(status_code=400, detail="data_dir is empty")
|
|
|
+
|
|
|
+ parent = os.path.dirname(target)
|
|
|
+ if not os.path.isdir(parent):
|
|
|
+ raise HTTPException(
|
|
|
+ status_code=400,
|
|
|
+ detail=f"Parent directory does not exist: {parent}",
|
|
|
+ )
|
|
|
+ if not os.access(parent, os.W_OK):
|
|
|
+ raise HTTPException(
|
|
|
+ status_code=400,
|
|
|
+ detail=f"Parent directory is not writable: {parent}",
|
|
|
+ )
|
|
|
+
|
|
|
+ try:
|
|
|
+ os.makedirs(target, exist_ok=True)
|
|
|
+ except OSError as e:
|
|
|
+ raise HTTPException(status_code=400, detail=f"Cannot create dir: {e}")
|
|
|
+
|
|
|
+ cfg = _load_config()
|
|
|
+ cfg["data_dir"] = target
|
|
|
+ _save_config(cfg)
|
|
|
+
|
|
|
+ return {
|
|
|
+ "ok": True,
|
|
|
+ "data_dir": target,
|
|
|
+ "change_requires_restart": True,
|
|
|
+ "message": "数据目录已保存,重启服务后生效。",
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+def _data_dir_override() -> str:
|
|
|
+ """Helper mirroring config._data_dir_from_config_json for the is_default
|
|
|
+ check above (kept local to avoid importing a private from config)."""
|
|
|
+ cfg = _load_config()
|
|
|
+ return cfg.get("data_dir", "") or ""
|
|
|
+
|
|
|
+
|
|
|
@router.get("/mode")
|
|
|
async def get_deployment_mode():
|
|
|
"""Unauthenticated read of the current deployment_mode + display info.
|