forked from retoor/devplacepy
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:
+20
-11
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user