fix(boxwart): Nachrichten-Job raeumt nach dem Versand auf, Hermes-Dashboard-Link repariert
- news-melden.sh verschiebt Text- und Sprechfassung nach dem Versand nach *.gesendet.*. Hermes' write_file ueberschreibt keine vorhandene Datei; der liegengebliebene Bericht vom Vortag liess jeden Morgenlauf 4 von 5 Schreibversuchen scheitern. Ein zweiter Aufruf desselben Laufs erkennt den schon verschickten Bericht weiterhin. - hermes_ui-Proxy: Seit Hermes v0.21 leitet das Dashboard auf /login um und die Anmeldeseite schickt an /auth/password-login — beides ohne /hermes-ui-Praefix. Der Knopf „Hermes-GUI oeffnen“ landete deshalb auf MC2s „Diese Seite gibt es nicht“. Umleitungen und Anmelde-Pfade werden jetzt umgeschrieben, mehrere Set-Cookie bleiben. - Tests fuer die Pfad-Umschreibung (12 Backend-Tests gesamt). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
12dadfe6ef
commit
3d2c9549fb
@@ -6,14 +6,19 @@ Hermes-GUI eine volle Single-Page-App: HTML + gehashte Assets + JSON-API + MEHRE
|
||||
(/api/ws, /api/console, /api/pty, /api/events). Deshalb hier ein GENERISCHER Reverse-Proxy für
|
||||
alles unter `/hermes-ui/*` (alle HTTP-Methoden + beliebige WebSockets).
|
||||
|
||||
Der Backend-Server läuft NUR auf Loopback (127.0.0.1:9119, `hermes serve --skip-build`, kein
|
||||
Login — gleiches LAN-Trust-Modell wie Konsole/Terminal) und wird über den ohnehin offenen
|
||||
MC2-Port (9001) same-origin durchgereicht → keine eigene Firewall-Freigabe, kein CORS.
|
||||
Der Backend-Server (`hermes dashboard`, :9119) wird über den MC2-Port (9001) same-origin
|
||||
durchgereicht → keine eigene Firewall-Freigabe, kein CORS.
|
||||
|
||||
Damit die absoluten Pfade der SPA (`/assets`, `/api`, `/api/ws`) nicht mit MC2s eigenen (`/assets`,
|
||||
`/api`) kollidieren, wird das Hermes-Bundle mit Vite-`base=/hermes-ui/` gebaut (deploy.sh) — dann
|
||||
liegen ALLE seine Pfade unter `/hermes-ui/`. Dieser Proxy streift das Präfix ab und leitet an
|
||||
`:9119/...` weiter.
|
||||
|
||||
Seit Hermes v0.21 hat das Dashboard eine eigene Anmeldung: `/` leitet auf `/login?next=…` um,
|
||||
und die Anmeldeseite schickt an `/auth/password-login`. Beide Pfade sind absolut und kennen
|
||||
das Präfix nicht — MC2 reichte die Umleitung unverändert weiter, und der Knopf „Hermes-GUI
|
||||
öffnen“ landete auf MC2s eigener „Diese Seite gibt es nicht“ (gefunden 23.09.2026). Deshalb
|
||||
schreibt der Proxy Umleitungen und diese Pfade im HTML auf `/hermes-ui/…` um.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@@ -91,6 +96,24 @@ async def _proxy_ws(ws: WebSocket, path: str) -> None:
|
||||
_BASE_PATH_RE = re.compile(rb'window\.__HERMES_BASE_PATH__\s*=\s*"[^"]*"')
|
||||
_BASE_PATH_SET = b'window.__HERMES_BASE_PATH__="/' + _BASE.encode() + b'"'
|
||||
_HEAD_INJECT = b"<head><script>" + _BASE_PATH_SET + b";</script>"
|
||||
# Absolute Anmelde-Pfade in der (server-gerenderten) Login-Seite: "/auth/…", "/login", "/logout".
|
||||
_ANMELDE_PFADE = re.compile(rb'(["\'(=])/(auth|login|logout)(?=[/?"\')\s])')
|
||||
|
||||
|
||||
def praefixiere_ort(ort: str) -> str:
|
||||
"""Umleitungsziel unter /hermes-ui/ holen. Fremde Ziele (http…, //host) bleiben."""
|
||||
if ort.startswith("/") and not ort.startswith(("//", f"/{_BASE}/")):
|
||||
return f"/{_BASE}{ort}"
|
||||
return ort
|
||||
|
||||
|
||||
def praefixiere_html(body: bytes) -> bytes:
|
||||
"""Basis-Pfad der SPA setzen und die absoluten Anmelde-Pfade unter /hermes-ui/ legen."""
|
||||
if _BASE_PATH_RE.search(body):
|
||||
body = _BASE_PATH_RE.sub(_BASE_PATH_SET, body)
|
||||
elif b"<head>" in body:
|
||||
body = body.replace(b"<head>", _HEAD_INJECT, 1)
|
||||
return _ANMELDE_PFADE.sub(rb"\1/" + _BASE.encode() + rb"/\2", body)
|
||||
|
||||
|
||||
async def _proxy_http(request: Request, path: str = "") -> Response:
|
||||
@@ -113,12 +136,18 @@ async def _proxy_http(request: Request, path: str = "") -> Response:
|
||||
)
|
||||
body = r.content
|
||||
if "text/html" in r.headers.get("content-type", "").lower():
|
||||
if _BASE_PATH_RE.search(body):
|
||||
body = _BASE_PATH_RE.sub(_BASE_PATH_SET, body)
|
||||
elif b"<head>" in body:
|
||||
body = body.replace(b"<head>", _HEAD_INJECT, 1)
|
||||
resp_headers = {k: v for k, v in r.headers.items() if k.lower() not in _DROP}
|
||||
return Response(content=body, status_code=r.status_code, headers=resp_headers)
|
||||
body = praefixiere_html(body)
|
||||
resp = Response(content=body, status_code=r.status_code)
|
||||
# multi_items statt dict: Die Anmeldung setzt Cookies, und mehrere Set-Cookie-Zeilen
|
||||
# dürfen nicht zu einer zusammenfallen.
|
||||
for k, v in r.headers.multi_items():
|
||||
kl = k.lower()
|
||||
if kl in _DROP:
|
||||
continue
|
||||
if kl == "location":
|
||||
v = praefixiere_ort(v)
|
||||
resp.raw_headers.append((kl.encode("latin-1"), v.encode("latin-1")))
|
||||
return resp
|
||||
|
||||
|
||||
@router.get(f"/{_BASE}")
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Tests für die Pfad-Umschreibung des Hermes-Dashboard-Proxys (Anmeldung seit Hermes v0.21)."""
|
||||
|
||||
from routers import hermes_ui
|
||||
|
||||
|
||||
def test_umleitung_auf_login_bekommt_praefix():
|
||||
assert hermes_ui.praefixiere_ort("/login?next=%2F") == "/hermes-ui/login?next=%2F"
|
||||
assert hermes_ui.praefixiere_ort("/") == "/hermes-ui/"
|
||||
|
||||
|
||||
def test_fremde_oder_schon_praefixierte_ziele_bleiben():
|
||||
assert hermes_ui.praefixiere_ort("/hermes-ui/sessions") == "/hermes-ui/sessions"
|
||||
assert hermes_ui.praefixiere_ort("https://example.org/x") == "https://example.org/x"
|
||||
assert hermes_ui.praefixiere_ort("//evil.example/x") == "//evil.example/x"
|
||||
|
||||
|
||||
def test_anmeldeseite_schickt_an_praefixierten_pfad():
|
||||
html = (b'<html><head><title>Login</title></head><body>'
|
||||
b'<form action="/auth/password-login" method="post"></form>'
|
||||
b'<script>fetch("/auth/password-login",{method:"POST"});location.href="/login?x=1"</script>'
|
||||
b'</body></html>')
|
||||
neu = hermes_ui.praefixiere_html(html)
|
||||
assert b'action="/hermes-ui/auth/password-login"' in neu
|
||||
assert b'fetch("/hermes-ui/auth/password-login"' in neu
|
||||
assert b'"/hermes-ui/login?x=1"' in neu
|
||||
assert b'window.__HERMES_BASE_PATH__="/hermes-ui"' in neu
|
||||
|
||||
|
||||
def test_andere_absolute_pfade_bleiben_unberuehrt():
|
||||
html = b'<head></head><a href="/assets/x.js">x</a><a href="/authors">y</a>'
|
||||
neu = hermes_ui.praefixiere_html(html)
|
||||
assert b'href="/assets/x.js"' in neu
|
||||
assert b'href="/authors"' in neu
|
||||
@@ -5,17 +5,17 @@ from datetime import datetime
|
||||
from services import modell_nutzung
|
||||
|
||||
ZEILEN = [
|
||||
'2026-09-23T03:12:44+02:00 box llama-swap[1]: [INFO] Request 192.168.178.28 '
|
||||
'"POST /v1/chat/completions HTTP/1.1" 200 10254 "Go-http-client/1.1" 1.2s',
|
||||
'2026-09-23T03:12:50+02:00 box llama-swap[1]: [INFO] Request 192.168.178.28 '
|
||||
'"POST /v1/chat/completions HTTP/1.1" 200 9000 "Go-http-client/1.1" 1.1s',
|
||||
'2026-09-23T07:01:00+02:00 box llama-swap[1]: [INFO] Request 127.0.0.1 '
|
||||
'"POST /v1/chat/completions HTTP/1.1" 200 800 "python-httpx/0.28.1" 3s',
|
||||
'2026-09-23T07:02:00+02:00 box llama-swap[1]: [INFO] Request 127.0.0.1 '
|
||||
'"POST /v1/embeddings HTTP/1.1" 200 800 "python-httpx/0.28.1" 20ms',
|
||||
('2026-09-23T03:12:44+02:00 box llama-swap[1]: [INFO] Request 192.168.178.28 '
|
||||
'"POST /v1/chat/completions HTTP/1.1" 200 10254 "Go-http-client/1.1" 1.2s'),
|
||||
('2026-09-23T03:12:50+02:00 box llama-swap[1]: [INFO] Request 192.168.178.28 '
|
||||
'"POST /v1/chat/completions HTTP/1.1" 200 9000 "Go-http-client/1.1" 1.1s'),
|
||||
('2026-09-23T07:01:00+02:00 box llama-swap[1]: [INFO] Request 127.0.0.1 '
|
||||
'"POST /v1/chat/completions HTTP/1.1" 200 800 "python-httpx/0.28.1" 3s'),
|
||||
('2026-09-23T07:02:00+02:00 box llama-swap[1]: [INFO] Request 127.0.0.1 '
|
||||
'"POST /v1/embeddings HTTP/1.1" 200 800 "python-httpx/0.28.1" 20ms'),
|
||||
"2026-09-17T19:59:00+02:00 box llama-swap[1]: [INFO] <Qwen3.8-27B> Health check passed on http://localhost:5805/health",
|
||||
'2026-09-10T03:00:00+02:00 box llama-swap[1]: [INFO] Request 192.168.178.28 '
|
||||
'"POST /v1/chat/completions HTTP/1.1" 200 1 "Go-http-client/1.1" 1s',
|
||||
('2026-09-10T03:00:00+02:00 box llama-swap[1]: [INFO] Request 192.168.178.28 '
|
||||
'"POST /v1/chat/completions HTTP/1.1" 200 1 "Go-http-client/1.1" 1s'),
|
||||
]
|
||||
|
||||
NAMEN = {"127.0.0.1": "Lucy und OpenChamber über MC2", "192.168.178.28": "NerdQuiz auf Arcane"}
|
||||
|
||||
@@ -10,14 +10,14 @@ PROJEKTE_SYNC_LOG = [
|
||||
]
|
||||
|
||||
ERRORS_LOG = [
|
||||
'2026-09-23 07:02:41,584 WARNING [cron_195e479e8e30_20260923_070017] agent.tool_executor: '
|
||||
('2026-09-23 07:02:41,584 WARNING [cron_195e479e8e30_20260923_070017] agent.tool_executor: '
|
||||
'Tool write_file returned error (0.00s): {"error": "Refusing to overwrite /tmp/news-text.md: '
|
||||
'/tmp/news-text.md exists"}',
|
||||
'2026-09-23 07:02:58,516 WARNING [cron_195e479e8e30_20260923_070017] agent.tool_executor: '
|
||||
'Tool write_file returned error (0.00s): {"error": "Refusing to overwrite /tmp/news-text.md"}',
|
||||
'/tmp/news-text.md exists"}'),
|
||||
('2026-09-23 07:02:58,516 WARNING [cron_195e479e8e30_20260923_070017] agent.tool_executor: '
|
||||
'Tool write_file returned error (0.00s): {"error": "Refusing to overwrite /tmp/news-text.md"}'),
|
||||
"2026-09-23 07:03:39,596 WARNING gateway.mirror: Mirror: no session found for telegram:1 thread=None",
|
||||
'2026-09-22 07:01:10,000 WARNING [cron_195e479e8e30_20260922_070017] agent.tool_executor: '
|
||||
'Tool web_extract returned error (0.10s): {"error": "timeout"}',
|
||||
('2026-09-22 07:01:10,000 WARNING [cron_195e479e8e30_20260922_070017] agent.tool_executor: '
|
||||
'Tool web_extract returned error (0.10s): {"error": "timeout"}'),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -43,6 +43,9 @@ if [ "${1:-}" = "--nur-stimme" ]; then
|
||||
--data "$(python3 -c 'import json,sys; print(json.dumps({"text": open(sys.argv[1], encoding="utf-8").read()[:1500]}))' "$SPRECHDATEI")" \
|
||||
-o "$TON" -w '%{http_code}' 2>/dev/null)
|
||||
|
||||
# Sprechfassung wegraeumen (Grund siehe Textfassung unten) — auch wenn die Stimme scheiterte.
|
||||
mv -f "$SPRECHDATEI" "${SPRECHDATEI%.txt}.gesendet.txt" 2>/dev/null || true
|
||||
|
||||
[ "$CODE" = "200" ] && [ -s "$TON" ] || exit 0
|
||||
|
||||
# Telegram zeigt eine ECHTE Sprachnachricht (runde Blase, Wellenform) nur bei
|
||||
@@ -57,8 +60,16 @@ fi
|
||||
# ------------------------------------------- Erster Durchgang: Text + Start ---
|
||||
TEXTDATEI="${1:-/tmp/news-text.md}"
|
||||
SPRECHDATEI="${2:-/tmp/news-sprich.txt}"
|
||||
GESENDET="${TEXTDATEI%.md}.gesendet.md"
|
||||
|
||||
if [ ! -s "$TEXTDATEI" ]; then
|
||||
# Zweiter Aufruf desselben Laufs, nachdem der erste den Bericht schon verschickt und
|
||||
# weggeraeumt hat? Dann ist nichts zu tun (die Doppelversand-Sperre unten sieht die
|
||||
# Datei nicht mehr).
|
||||
if [ -s "$GESENDET" ] && [ $(( $(date +%s) - $(stat -c %Y "$GESENDET" 2>/dev/null || echo 0) )) -lt 7200 ]; then
|
||||
echo "gemeldet (dieser Bericht ging bereits raus — nicht doppelt geschickt)"
|
||||
exit 0
|
||||
fi
|
||||
bash "$NOTIFY" -s "[News]" "Der Nachrichtenbericht ist leer geblieben ($TEXTDATEI). Da lief etwas schief."
|
||||
exit 1
|
||||
fi
|
||||
@@ -78,6 +89,11 @@ find /tmp -maxdepth 1 -name '.news-gesendet-*' -mtime +2 -delete 2>/dev/null
|
||||
|
||||
bash "$NOTIFY" -s "" "$(cat "$TEXTDATEI")"
|
||||
|
||||
# Bericht wegraeumen (23.09.2026): Hermes' write_file ueberschreibt keine vorhandene Datei.
|
||||
# Blieb der Bericht vom Vortag liegen, scheiterte jeder Morgenlauf erst an
|
||||
# „Refusing to overwrite /tmp/news-text.md“ (4 von 5 Versuchen, jeden Tag seit Wochen).
|
||||
mv -f "$TEXTDATEI" "$GESENDET" 2>/dev/null && touch "$GESENDET" 2>/dev/null || true
|
||||
|
||||
# Stimme abkoppeln: eigene Sitzung, kein Elternprozess, keine offenen Deskriptoren.
|
||||
# Damit ueberlebt sie das Ende dieses Skripts und blockiert es nicht.
|
||||
if [ -s "$SPRECHDATEI" ]; then
|
||||
|
||||
Reference in New Issue
Block a user