feat(ai): catalogue d'outils lecture & recherche (#79 phase C)
CI / lint (push) Successful in 59s
CI / security (push) Successful in 49s
CI / test (push) Successful in 1m24s
CI / build (push) Successful in 37s
CI / e2e (push) Successful in 10m24s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s
CI / lint (push) Successful in 59s
CI / security (push) Successful in 49s
CI / test (push) Successful in 1m24s
CI / build (push) Successful in 37s
CI / e2e (push) Successful in 10m24s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
"""Recent-files services shared by the REST route and the AI tool layer.
|
||||
|
||||
Provides ``list_recent`` (last opened files, falling back to last modified)
|
||||
and ``humanize_mtime``. The route ``/api/recent`` and the tool ``list_recent``
|
||||
both delegate here. Permission filtering is applied against the caller's
|
||||
vault list.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from backend.history import get_recent_opened, is_bookmarked
|
||||
from backend.indexer import find_file_in_index, index
|
||||
|
||||
|
||||
def humanize_mtime(mtime: float) -> str:
|
||||
"""Return a short, human-friendly French rendering of a timestamp."""
|
||||
delta = time.time() - mtime
|
||||
if delta < 60:
|
||||
return "à l'instant"
|
||||
if delta < 3600:
|
||||
return f"il y a {int(delta / 60)} min"
|
||||
if delta < 86400:
|
||||
return f"il y a {int(delta / 3600)} h"
|
||||
if delta < 604800:
|
||||
return f"il y a {int(delta / 86400)} j"
|
||||
return datetime.fromtimestamp(mtime).strftime("%d %b %Y")
|
||||
|
||||
|
||||
def _can_access(vault: str, user_vaults: list[str]) -> bool:
|
||||
return "*" in user_vaults or vault in user_vaults
|
||||
|
||||
|
||||
def list_recent(
|
||||
username: str | None,
|
||||
user_vaults: list[str] | None = None,
|
||||
*,
|
||||
vault: str | None = None,
|
||||
limit: int = 20,
|
||||
mode: str = "opened",
|
||||
) -> dict[str, Any]:
|
||||
"""Return the caller's recent files.
|
||||
|
||||
Args:
|
||||
username: Caller username (needed for the "opened" history mode).
|
||||
user_vaults: Vaults the caller may access (``["*"]`` = all).
|
||||
vault: Optional single-vault filter.
|
||||
limit: Maximum number of files to return.
|
||||
mode: ``"opened"`` to use the open history, anything else for the
|
||||
last-modified fallback.
|
||||
|
||||
Returns:
|
||||
Dict with ``files``, ``total``, ``limit`` and ``mode``.
|
||||
"""
|
||||
user_vaults = user_vaults or []
|
||||
|
||||
if mode == "opened" and username:
|
||||
history = get_recent_opened(username, vault_filter=vault, limit=limit)
|
||||
files_resp: list[dict[str, Any]] = []
|
||||
for item in history:
|
||||
v_name = item["vault"]
|
||||
if not _can_access(v_name, user_vaults):
|
||||
continue
|
||||
|
||||
f_idx = find_file_in_index(item["path"], v_name)
|
||||
if f_idx:
|
||||
files_resp.append({
|
||||
"path": f_idx["path"],
|
||||
"title": f_idx.get("title") or item["path"].split("/")[-1],
|
||||
"vault": v_name,
|
||||
"mtime": item["opened_at"],
|
||||
"mtime_human": humanize_mtime(item["opened_at"]),
|
||||
"size_bytes": f_idx.get("size", 0),
|
||||
"tags": [f"#{t}" for t in f_idx.get("tags", [])][:5],
|
||||
"preview": f_idx.get("content_preview", "")[:120],
|
||||
"bookmarked": is_bookmarked(username, v_name, f_idx["path"]),
|
||||
})
|
||||
else:
|
||||
files_resp.append({
|
||||
"path": item["path"],
|
||||
"title": item.get("title") or item["path"].split("/")[-1],
|
||||
"vault": v_name,
|
||||
"mtime": item["opened_at"],
|
||||
"mtime_human": humanize_mtime(item["opened_at"]),
|
||||
"tags": [],
|
||||
"preview": "",
|
||||
"bookmarked": is_bookmarked(username, v_name, item["path"]),
|
||||
})
|
||||
return {
|
||||
"files": files_resp,
|
||||
"total": len(files_resp),
|
||||
"limit": limit,
|
||||
"mode": "opened",
|
||||
}
|
||||
|
||||
all_files: list[tuple[str, dict[str, Any]]] = []
|
||||
for v_name, v_data in index.items():
|
||||
if vault and v_name != vault:
|
||||
continue
|
||||
if not _can_access(v_name, user_vaults):
|
||||
continue
|
||||
for f in v_data.get("files", []):
|
||||
all_files.append((v_name, f))
|
||||
|
||||
all_files.sort(key=lambda x: x[1].get("modified", ""), reverse=True)
|
||||
recent = all_files[:limit]
|
||||
|
||||
files_resp = []
|
||||
for v_name, f in recent:
|
||||
iso_modified = f.get("modified", "")
|
||||
try:
|
||||
mtime_dt = datetime.fromisoformat(iso_modified.replace("Z", "+00:00"))
|
||||
mtime_val = mtime_dt.timestamp()
|
||||
except Exception:
|
||||
mtime_val = time.time()
|
||||
|
||||
files_resp.append({
|
||||
"path": f["path"],
|
||||
"title": f["title"],
|
||||
"vault": v_name,
|
||||
"mtime": mtime_val,
|
||||
"mtime_human": humanize_mtime(mtime_val),
|
||||
"mtime_iso": iso_modified,
|
||||
"size_bytes": f.get("size", 0),
|
||||
"tags": [f"#{t}" for t in f.get("tags", [])][:5],
|
||||
"preview": f.get("content_preview", "")[:120],
|
||||
"bookmarked": is_bookmarked(username, v_name, f["path"]) if username else False,
|
||||
})
|
||||
|
||||
return {
|
||||
"files": files_resp,
|
||||
"total": len(all_files),
|
||||
"limit": limit,
|
||||
"mode": "modified",
|
||||
}
|
||||
Reference in New Issue
Block a user