v2.2 - Small improvement Interface change
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
"""Reset only the Hermes conversation ID used by Alfred.
|
||||
|
||||
Admin-protected, CSRF-protected, and concurrency-safe with ask_hermes().
|
||||
Does NOT delete the actual Hermes conversation history or change MCP/skills.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import fcntl
|
||||
import os
|
||||
from pathlib import Path
|
||||
import secrets
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from flask import Blueprint, jsonify
|
||||
from services import admin_console, hermes_client
|
||||
|
||||
|
||||
class HermesSessionBusy(RuntimeError):
|
||||
"""Hermes is currently processing a message."""
|
||||
|
||||
|
||||
def archive_current_session() -> bool:
|
||||
"""Return True when an existing session was archived.
|
||||
|
||||
The exact same thread lock and filesystem lock are used by ask_hermes.
|
||||
Both locks are non-blocking here to keep HTTP responsive.
|
||||
"""
|
||||
if os.getenv('HERMES_SESSION_MODE', 'persistent').strip().lower() == 'legacy':
|
||||
raise ValueError('Hermes est en mode legacy : les conversations sont déjà indépendantes.')
|
||||
|
||||
request_lock = hermes_client._REQUEST_LOCK
|
||||
if not request_lock.acquire(blocking=False):
|
||||
raise HermesSessionBusy('Hermes traite une demande. Réessaie lorsqu’il a terminé.')
|
||||
try:
|
||||
path = hermes_client._session_file()
|
||||
if path.is_symlink():
|
||||
raise ValueError('Fichier de session symbolique non autorisé.')
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
lock_path = path.with_name(path.name + '.lock')
|
||||
flags = os.O_CREAT | os.O_RDWR
|
||||
if hasattr(os, 'O_NOFOLLOW'):
|
||||
flags |= os.O_NOFOLLOW
|
||||
fd = os.open(lock_path, flags, 0o600)
|
||||
with os.fdopen(fd, 'r+') as lock_file:
|
||||
try:
|
||||
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except BlockingIOError as exc:
|
||||
raise HermesSessionBusy('Une conversation Hermes est en cours. Réessaie plus tard.') from exc
|
||||
try:
|
||||
if not path.exists():
|
||||
return False
|
||||
if not path.is_file():
|
||||
raise ValueError('État de session Hermes inattendu.')
|
||||
# Archive the file even if its JSON is invalid: this action is also a recovery path.
|
||||
archive_dir = path.parent / 'hermes_session_archives'
|
||||
if archive_dir.is_symlink():
|
||||
raise ValueError('Répertoire d’archives symbolique non autorisé.')
|
||||
archive_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
stem = datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')
|
||||
name = f'{path.stem}-{stem}-{secrets.token_hex(4)}.json'
|
||||
os.replace(path, archive_dir / name)
|
||||
print('[ALFRED] Nouvelle session Hermes demandée depuis l’interface administrateur.', flush=True)
|
||||
return True
|
||||
finally:
|
||||
fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
|
||||
finally:
|
||||
request_lock.release()
|
||||
|
||||
|
||||
def register_hermes_session_page(app):
|
||||
bp = Blueprint('alfred_hermes_session_button_v1', __name__)
|
||||
|
||||
@bp.post('/api/control/hermes/new-session')
|
||||
@admin_console._require_admin
|
||||
def new_session():
|
||||
try:
|
||||
archived = archive_current_session()
|
||||
except HermesSessionBusy as exc:
|
||||
return jsonify(ok=False, error=str(exc)), 409
|
||||
except (OSError, ValueError, RuntimeError) as exc:
|
||||
app.logger.warning('Nouvelle session Hermes impossible : %s', exc)
|
||||
return jsonify(ok=False, error='Impossible de réinitialiser la session : '+str(exc)), 503
|
||||
response = jsonify(ok=True, archived=archived,
|
||||
message=('Session précédente sauvegardée. La prochaine demande ouvrira une nouvelle session Hermes.'
|
||||
if archived else 'Aucune session précédente : Hermes en créera une à la prochaine demande.'))
|
||||
response.headers['Cache-Control'] = 'no-store, private'
|
||||
return response
|
||||
|
||||
app.register_blueprint(bp)
|
||||
Reference in New Issue
Block a user