feat: add project file system with CRUD, upload, inline editing, and video attachment support

- Add new `/projects/{slug}/files` endpoint group for per-project filesystem operations including directory and file CRUD, upload, and inline editing with public read and owner write access
- Extend attachment system to support video formats (webm, ogv, mov, m4v) with proper file icons and MIME types
- Implement configurable allowed file types via `allowed_file_types` site setting, replacing hardcoded `ALLOWED_UPLOAD_TYPES` with dynamic `allowed_extensions()` and `is_extension_allowed()` functions
- Add `delete_all_project_files()` call in `delete_content_item()` to clean up project files when a project is deleted
- Create database indexes on `project_files` table for `(project_uid, path)` and `(project_uid, parent_path)` to optimize file lookups
- Introduce `docs_prose.py` module with `render_prose()` function that renders Markdown content inside `data-render` divs using mistune, enabling dynamic prose rendering in documentation pages
- Enhance docs search with Markdown-aware text stripping (`_demarkdown()`) and improved HTML/script/style sanitization for better search indexing
- Update documentation API samples to reflect new attachment response fields (`is_image`, `is_video`, `mime_type`) and note video format support
- Update README to document the new project files endpoint and clarify AI gateway attribution for guest Devii sessions
This commit is contained in:
2026-06-08 20:51:09 +00:00
parent e0535bb7c5
commit 97eb58fc19
110 changed files with 5534 additions and 603 deletions
+20 -11
View File
@@ -10,7 +10,7 @@ from fastapi.responses import JSONResponse, RedirectResponse
from devplacepy.database import get_int_setting
from devplacepy.services.manager import service_manager
from devplacepy.templating import templates
from devplacepy.utils import _user_from_api_key, _user_from_session, get_current_user
from devplacepy.utils import _user_from_api_key, _user_from_session, get_current_user, is_admin
logger = logging.getLogger("devii.router")
@@ -41,9 +41,9 @@ def _resolve_ws_owner(websocket: WebSocket):
if key:
user = _user_from_api_key(key)
if user:
return "user", user["uid"], user.get("username", ""), user.get("api_key", "")
return "user", user["uid"], user.get("username", ""), user.get("api_key", ""), is_admin(user)
guest_id = websocket.cookies.get(GUEST_COOKIE) or uuid_utils.uuid7().hex
return "guest", guest_id, "guest", ""
return "guest", guest_id, "guest", "", False
def _owner_from_request(request: Request):
@@ -110,15 +110,20 @@ async def devii_usage(request: Request):
svc = _service()
owner_kind, owner_id, _ = _owner_from_request(request)
if svc is None:
return JSONResponse({"spent_24h": 0.0, "limit": 0.0, "turns_today": 0})
return JSONResponse({"used_pct": 0.0, "turns_today": 0})
spent = svc.spent_24h(owner_kind, owner_id) if owner_id else 0.0
limit = svc.daily_limit_for(owner_kind)
turns = svc.hub().ledger.turns_24h(owner_kind, owner_id) if owner_id else 0
return JSONResponse({
"spent_24h": round(spent, 6),
"limit": svc.daily_limit_for(owner_kind),
used_pct = round(min(100.0, spent / limit * 100), 1) if limit > 0 else 0.0
payload = {
"used_pct": used_pct,
"turns_today": turns,
"owner_kind": owner_kind,
})
}
if is_admin(get_current_user(request)):
payload["spent_24h"] = round(spent, 6)
payload["limit"] = limit
return JSONResponse(payload)
@router.post("/clippy/ai/chat")
@@ -149,13 +154,15 @@ async def devii_ws(websocket: WebSocket):
if not service_manager.owns_lock():
await websocket.close(code=1013)
return
owner_kind, owner_id, username, api_key = _resolve_ws_owner(websocket)
owner_kind, owner_id, username, api_key, owner_is_admin = _resolve_ws_owner(websocket)
if owner_kind == "guest" and not svc.guests_enabled():
await websocket.send_json({"type": "error", "text": "Guest access to Devii is disabled."})
await websocket.close(code=1008)
return
session = svc.hub().get_or_create(owner_kind, owner_id, username, api_key, svc.instance_base_url())
session = svc.hub().get_or_create(
owner_kind, owner_id, username, api_key, svc.instance_base_url(), is_admin=owner_is_admin
)
session.attach(websocket)
try:
await session.send_bootstrap(websocket)
@@ -171,12 +178,14 @@ async def devii_ws(websocket: WebSocket):
if spent >= limit:
await websocket.send_json({
"type": "error",
"text": f"Daily limit reached (${spent:.4f} of ${limit:.2f}). Try again later.",
"text": "Daily AI quota reached (100%). Try again later.",
})
continue
session.spawn_turn(text)
elif kind == "reset":
await session.reset_conversation()
elif kind == "visibility":
session.set_visibility(websocket, bool(data.get("visible", True)), bool(data.get("focused", False)))
elif kind in ("avatar_result", "client_result"):
session.resolve_query(str(data.get("id", "")), data.get("result"))
except WebSocketDisconnect: