Causa: su Qt 5.6 (kit Sailfish 5.1) una QVariantList di QVariantMap
usata come model non fa risolvere i campi nei delegate QML
(model.title/message_count/updated_at = undefined -> righe vuote), pur
con payload corretto dal server (verificato con cattura reale del 12/09).
- Nuovo src/sessionsmodel.{h,cpp}: QAbstractListModel con ruoli
session_id/title/message_count/updated_at; api.sessions ora e' il
modello, api.sessionsCount per lo stato "nessuna sessione"
- /api/sessions con sidebar_source=webui&exclude_hidden=1 (come la
sidebar del browser: solo sessioni WebUI, niente CLI/cron/nascoste)
- SessionsPage: describe(message_count, updated_at) con parametri espliciti
- PROTOCOL.md: payload sessions verificato + nota del pitfall Qt 5.6
7.3 KiB
Protocollo hermes-webui ↔ client
Documentazione del protocollo HTTP/SSE usato da harbour-hermes.
Validato dal vivo il 12/09/2026 contro un'istanza reale di
nesquena/hermes-webui (v0.52.x, server.py, ThreadingHTTPServer).
Il protocollo non è documentato ufficialmente: è replicato dal frontend web del progetto (
static/*.jsè la reference implementation) e pinnato alla versione attuale. Un aggiornamento del WebUI può richiedere aggiornamenti al client.
Server di riferimento: in ascolto su 127.0.0.1:8787 dietro reverse proxy
(https://hermes.hackatoniclife.com). TLS obbligatorio in produzione.
1. Autenticazione
Cookie di sessione hermes_session = token.firma_hmac (TTL 30 giorni
di default; HERMES_WEBUI_SESSION_TTL per cambiarlo). Auth attiva solo se il
server ha una password impostata.
| Endpoint | Metodo | Body / Risposta |
|---|---|---|
/api/auth/login |
POST | {"password": "..."} → 200 {"ok": true} + Set-Cookie |
/api/auth/status |
GET | {"auth_enabled": bool, "logged_in": bool, "password_auth_enabled": bool, ...} |
/api/auth/logout |
POST | invalida il cookie |
- Errori:
401 {"error": "Authentication required"}; rate limit sul login. - Se
auth_enabledèfalse, tutte le API rispondono senza cookie. - Il client persiste SOLO il cookie (file
0600); la password non è mai salvata.
2. Sessioni
GET /api/sessions?sidebar_source=webui&exclude_hidden=1 → lista per la
sidebar (stessi parametri del frontend web: solo sessioni WebUI visibili;
senza parametri arrivano anche le CLI/cron e le nascoste — lista più lunga).
Cattura reale del 12/09/2026 (chiavi verificate):
{
"sessions": [
{
"session_id": "20260907_123845_c7e52d",
"title": "Diagnosi guasto YunoHost",
"workspace": "/home/kaneda/.hermes/profiles/carlo/home/workspace",
"model": "deepseek-v4-flash",
"message_count": 163,
"created_at": 1788707925.6,
"updated_at": 1788778530.726691,
"pinned": false, "archived": false, "project_id": null,
"profile": "default",
"source_tag": "webui", "raw_source": "webui",
"session_source": "webui", "source_label": "WebUI",
"parent_session_id": null
}
],
"sidebar_reference_sessions": [ "..." ],
"cli_count": 4, "archived_count": 0,
"archived_webui_count": 0, "archived_cli_count": 0,
"include_archived": false, "all_profiles": false,
"active_profile": "default"
}
⚠ Qt 5.6 (kit Sailfish 5.1): i campi di questi oggetti NON si risolvono nei
delegate QML quando il modello è una QVariantList (model.campo = undefined
→ righe vuote pur con dati corretti dal server). Il client usa un
QAbstractListModel C++ (SessionsModel) con ruoli session_id, title,
message_count, updated_at.
GET /api/session?session_id=ID → {"session": { ... }} con in più:
messages: array di{"role": "user"|"assistant", "content": "...", "timestamp": 1789188869.6, ...}(i messaggi assistente possono includerereasoning,finish_reason,id,_turnDuration,_turnTps,_firstTokenMs; i messaggi "solo tool" hannocontentvuoto)tool_calls,input_tokens,output_tokens,estimated_cost,read_only,active_stream_id,enabled_toolsets, ...
POST /api/session/new con body {"workspace": "/path"} (opzionale; il
workspace deve essere sotto la home dell'utente, nella lista salvata o sotto
il default) → {"session": {...}} (sessione vuota).
3. Chat con streaming
POST /api/chat/start:
{
"session_id": "f517dae7a2da",
"message": "Rispondi solo con la parola pong",
"model": "deepseek-v4-flash",
"model_provider": "deepseek",
"workspace": "/home/kaneda/.hermes/profiles/carlo/home/workspace",
"profile": "default"
}
Risposta:
{
"stream_id": "a1e0ec82169b4673a13bf9bd466afd3f",
"session_id": "f517dae7a2da",
"pending_started_at": 1789188858.196835,
"turn_id": "20260912T045418Z-ba85fcc8cebe",
"title": "Rispondi solo con la parola pong. Non usare strumenti.",
"effective_model_provider": "deepseek"
}
Errori noti: 404 sessione inesistente; 400 "Missing required field(s)";
conflitto se la sessione ha già uno stream attivo ("session already has an
active stream").
GET /api/chat/stream?stream_id=ID → SSE text/event-stream,
heartbeat ogni 5 s (: heartbeat), ogni evento ha id: <stream_id>:<seq>:
| Evento | Payload | Uso nel client |
|---|---|---|
context_status |
{session_id, prefill} |
ignorato |
token |
{"text": "..."} |
delta di testo della risposta |
metering |
{tps, ttft_ms, usage, ...} |
ignorato (v1) |
done |
{"session": {...con messages finali}, "usage": {...}} |
risincronizza il modello e chiude il turno |
title_status |
{session_id, status, title} |
ignorato |
title |
{session_id, "title": "..."} |
aggiorna il titolo |
stream_end |
{session_id} |
fine stream |
Altri eventi possono esistere (attività tool, approval/clarify) e vanno ignorati senza errore (il parser li scarta).
Ripresa: GET /api/chat/stream/status?stream_id=ID →
{"active": bool, "stream_id", "replay_available"}. Se GET /api/session
mostra active_stream_id non nullo, riagganciare lo stream con lo stesso URL.
Annullamento: GET /api/chat/cancel?stream_id=ID →
{"ok": true, "cancelled": bool, "stream_id"}.
4. Voce
Trascrizione (voce → testo)
POST /api/transcribe — multipart/form-data, campo file
(OGG/Opus, WebM, M4A, WAV...; il server converte con ffmpeg se serve):
{"ok": true, "transcript": "Questa è una prova di trascrizione vocale..."}
- Provider STT del server (qui:
local= faster-whisper). Latenza osservata: ~10 s per 6 s di audio (modellobase, CPU). - ⚠️ Lingua: con
stt.local.language: ''whisper può tradurre l'italiano in inglese; impostarestt.local.language: itin~/.hermes/config.yaml(richiede riavvio del WebUI). GET /api/transcribe/capability→{"ok": true, "available": true, "provider": "local"}.
Sintesi (testo → voce)
POST /api/tts — JSON:
{"text": "Ciao", "voice": "it-IT-ElsaNeural", "engine": "edge",
"rate": "+0%", "pitch": "+0Hz"}
→ 200 audio/mpeg (mp3) oppure 4xx {"error": "..."}.
- Limiti server: 5000 caratteri; rate limit 2 s per client;
allowlist voci hardcoded nella funzione
_handle_ttsdiapi/routes.py. Voci ammesse oggi: zh-CN (5), en-US (2), fr-CA (4), fr-FR (3), id-ID (1) + it-IT (4: ElsaNeural, DiegoNeural, IsabellaNeural, GiuseppeMultilingualNeural) — le italiane sono state aggiunte da noi (patch + test): il server va riavviato per attivarle. - Engine:
edge(default, gratis),elevenlabs,openai(richiedono chiavi configurate sul server).browserè solo client-side, non usabile qui.
5. Note operative
- Tutti gli errori API sono JSON
{"error": "..."}. - Il server non espone CORS: il client nativo non serve preflight.
- Endpoint utili non usati dalla v1:
/api/clarify/*e/api/approval/*(richieste interattive dell'agente: vanno gestite per non lasciare un run in attesa),/api/upload(allegati),/api/settings,/api/models,/api/sessions/events(SSE globale di invalidazione lista). - L'istanza di prova usata per la validazione era isolata (porta 8899,
HERMES_WEBUI_STATE_DIRdedicata, password temporanea) e non ha toccato né il server live né i dati utente.