feat(bookslm): v2.2.0 - BooksLM chat AI contextuel par répertoire + 4 providers AI
CI / lint (push) Failing after 16s
CI / test (push) Skipped
CI / build (push) Skipped
CI / e2e (push) Skipped
CI / security (push) Successful in 21s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s

BooksLM (#76):
- Panneau chat slide-in 450px, clic-droit répertoire → 🧠 BooksLM
- Collecte récursive .md avec limites (200 fichiers, 200K chars)
- Cache SHA-256, redaction secrets, priorité README/index
- SSE streaming, badges sources cliquables, historique localStorage
- Commandes palette: BooksLM ouvrir/nouvelle conversation

Providers AI (4 nouveaux):
- NVIDIA (integrate.api.nvidia.com)
- QwenCloud (dashscope.aliyuncs.com)
- Xiaomi (api.xiaomi.com)
- Mistral (api.mistral.ai)
- Tous OpenAI-compatible, auto-listing modèles

Fix: dropdown config-select suit maintenant le thème (option bg/color)
27 tests BooksLM + 466 tests au total
This commit is contained in:
2026-09-06 19:42:40 -04:00
parent 6292bfd9cb
commit 524e6da591
14 changed files with 1595 additions and 11 deletions
+19 -4
View File
@@ -695,6 +695,7 @@ except Exception: # pragma: no cover - WeasyPrint/GTK missing
# Multi-format export (HTML / MD bundle / ePub) — pure Python, no heavy deps.
from backend.export import export_epub, export_html, export_md_bundle, ExportError # noqa: E402
from backend.ai_routes import router as ai_router
from backend.bookslm_routes import router as bookslm_router
from backend.saved_searches import delete_saved, get_saved, save_search
from backend.share import (
create_share,
@@ -714,6 +715,7 @@ from backend.webhooks import (
app.include_router(auth_router)
app.include_router(ai_router)
app.include_router(bookslm_router)
# Resolve frontend path relative to this file
FRONTEND_DIR = Path(__file__).resolve().parent.parent / "frontend"
@@ -3993,7 +3995,7 @@ async def api_get_ai_keys(current_user=Depends(require_admin)):
"""Return stored AI keys (values masked)."""
keys = _read_ai_keys()
masked = {}
for k in ["DEEPSEEK_API_KEY", "OPENROUTER_API_KEY", "GEMINI_API_KEY"]:
for k in ["DEEPSEEK_API_KEY", "OPENROUTER_API_KEY", "GEMINI_API_KEY", "NVIDIA_API_KEY", "QWENCLOUD_API_KEY", "XIAOMI_API_KEY", "MISTRAL_API_KEY"]:
val = keys.get(k, "") or os.environ.get(k, "")
if val:
masked[k] = val[:4] + "..." + val[-4:] if len(val) > 8 else "***"
@@ -4005,7 +4007,7 @@ async def api_get_ai_keys(current_user=Depends(require_admin)):
async def api_set_ai_keys(body: dict = Body(...), current_user=Depends(require_admin)):
"""Save AI keys. Pass {"DEEPSEEK_API_KEY":"sk-...","OPENROUTER_API_KEY":"...","GEMINI_API_KEY":"..."}"""
keys = _read_ai_keys()
for k in ["DEEPSEEK_API_KEY", "OPENROUTER_API_KEY", "GEMINI_API_KEY"]:
for k in ["DEEPSEEK_API_KEY", "OPENROUTER_API_KEY", "GEMINI_API_KEY", "NVIDIA_API_KEY", "QWENCLOUD_API_KEY", "XIAOMI_API_KEY", "MISTRAL_API_KEY"]:
if body.get(k):
keys[k] = body[k]
_write_ai_keys(keys)
@@ -4020,6 +4022,10 @@ async def api_test_ai_keys(current_user=Depends(require_admin)):
("DEEPSEEK_API_KEY", "deepseek", "https://api.deepseek.com/v1/models", "Authorization"),
("OPENROUTER_API_KEY", "openrouter", "https://openrouter.ai/api/v1/models", "Authorization"),
("GEMINI_API_KEY", "gemini", "https://generativelanguage.googleapis.com/v1beta/models?key={key}", None),
("NVIDIA_API_KEY", "nvidia", "https://integrate.api.nvidia.com/v1/models", "Authorization"),
("QWENCLOUD_API_KEY", "qwencloud", "https://dashscope.aliyuncs.com/compatible-mode/v1/models", "Authorization"),
("XIAOMI_API_KEY", "xiaomi", "https://api.xiaomi.com/v1/models", "Authorization"),
("MISTRAL_API_KEY", "mistral", "https://api.mistral.ai/v1/models", "Authorization"),
]:
key = get_ai_key(key_name)
if not key:
@@ -4047,7 +4053,8 @@ async def api_list_ai_models(provider: str = Query(...), current_user=Depends(re
"""List available models for a given AI provider."""
provider = provider.lower()
if provider not in ("deepseek", "openrouter", "gemini"):
all_providers = ("deepseek", "openrouter", "gemini", "nvidia", "qwencloud", "xiaomi", "mistral")
if provider not in all_providers:
return {"models": [], "error": f"Unknown provider: {provider}"}
key_name = f"{provider.upper()}_API_KEY"
@@ -4060,8 +4067,16 @@ async def api_list_ai_models(provider: str = Query(...), current_user=Depends(re
url = f"https://generativelanguage.googleapis.com/v1beta/models?key={key}"
elif provider == "deepseek":
url = "https://api.deepseek.com/v1/models"
else: # openrouter
elif provider == "openrouter":
url = "https://openrouter.ai/api/v1/models"
elif provider == "nvidia":
url = "https://integrate.api.nvidia.com/v1/models"
elif provider == "qwencloud":
url = "https://dashscope.aliyuncs.com/compatible-mode/v1/models"
elif provider == "xiaomi":
url = "https://api.xiaomi.com/v1/models"
elif provider == "mistral":
url = "https://api.mistral.ai/v1/models"
try:
if provider == "gemini":