From 78dc79ceabae775e8dbd9d8fd32d54c55643f05b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dieter=20Schl=C3=BCter?= Date: Sat, 13 Jun 2026 04:22:38 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20check-assets=20=E2=86=94=20KI=20gekoppe?= =?UTF-8?q?lt=20=E2=80=94=20byte-getauschte=20Bilder=20neu=20bewerten?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Da bekannte Bilder aus Effizienzgründen nicht jeden Scan neu geladen werden (url_hashes), prüfte die KI einen späteren Byte-Tausch eines bekannten Bildes nicht von selbst erneut. Lücke geschlossen: Erkennt die Datei-Prüfung (check-assets) eine geänderte Bilddatei, wird deren URL über BaselineManager.invalidate_ai_url_hashes aus der Hash-Erinnerung entfernt → nächster Scan lädt das Bild neu, neue Bytes = neuer Hash = Cache-Miss = die KI bewertet den Inhalt neu. Einhängung in cmd_scan (vor run_ai_analysis → Neubewertung noch im selben Scan) und cmd_check_assets. Integritäts- und Inhaltsprüfung greifen so ineinander. Tests: 260 grün (+2: invalidate entfernt nur die genannten URLs; geändertes Bild wird nach Invalidierung neu geladen und beanstandet). Co-Authored-By: Claude Opus 4.8 --- BEDIENUNGSANLEITUNG.md | 8 ++++++-- scanner/__main__.py | 15 ++++++++++++++ scanner/baseline.py | 16 +++++++++++++++ tests/test_ai_analyzer.py | 43 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 2 deletions(-) diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md index fc5000d..4752c27 100644 --- a/BEDIENUNGSANLEITUNG.md +++ b/BEDIENUNGSANLEITUNG.md @@ -1032,8 +1032,12 @@ einer ansonsten stabilen Seite). Deshalb gilt: akzeptiert. Da die Quittung am **Byte-Hash** hängt, wird ein später ausgetauschtes Bild (andere Bytes → neuer Hash) automatisch neu geprüft und nicht stillschweigend mitakzeptiert. -Hinweis zur Byte-Änderung bekannter Bilder auf stabilen Seiten: Diese deckt zusätzlich die -Datei-Prüfung (`check-assets`) ab — sie schlägt an, wenn sich der Inhalt einer bekannten Datei ändert. +**Byte-Tausch bekannter Bilder (z. B. ein bestehendes Logo wird heimlich gegen ein strafbares +ausgetauscht):** Bekannte Bilder werden aus Effizienzgründen nicht bei jedem Scan neu geladen +(URL→Byte-Hash gemerkt). Damit ein Austausch trotzdem nicht durchrutscht, ist die Datei-Prüfung +(`check-assets`) an die KI **gekoppelt**: Erkennt sie eine geänderte Bilddatei, wird deren URL aus +der Hash-Erinnerung entfernt → die KI lädt das Bild neu und **bewertet seinen Inhalt neu** (neue +Bytes → neuer Hash → Analyse). So greifen Integritäts- und Inhaltsprüfung ineinander. Audio- und Video-Prüfung sind als abschaltbare Erweiterungspunkte vorbereitet (`ai_analysis.audio` / `ai_analysis.video`, default deaktiviert) und können aktiviert werden, diff --git a/scanner/__main__.py b/scanner/__main__.py index 0cba4fc..91831c8 100644 --- a/scanner/__main__.py +++ b/scanner/__main__.py @@ -689,6 +689,17 @@ def _run_assets_core(cfg: dict, bm: BaselineManager, snap: dict) -> tuple[dict, return asset_diff, score_asset_diff(asset_diff, cfg), bool(baseline_hashes) +def _invalidate_ai_for_changed_assets(bm: BaselineManager, asset_diff: dict) -> None: + """Geänderte Bild-Assets (byte-getauscht) aus der KI-url_hashes-Erinnerung entfernen, + damit die KI sie neu lädt UND inhaltlich neu bewertet. Nicht-Bild-URLs sind nicht in + url_hashes → werden ignoriert.""" + changed_urls = [c["url"] for c in (asset_diff or {}).get("changed", []) if c.get("url")] + n = bm.invalidate_ai_url_hashes(changed_urls) + if n: + logging.getLogger("scanner.assets").info( + "KI-Cache für %d geänderte Bild(er) invalidiert → inhaltliche Neubewertung.", n) + + def cmd_check(args: argparse.Namespace, cfg: dict) -> int: logger = logging.getLogger("scanner.check") bm = BaselineManager(cfg["data_dir"]) @@ -762,6 +773,9 @@ def cmd_scan(args: argparse.Namespace, cfg: dict) -> int: levels.append(asset_assess["level"]) score_total += asset_assess.get("score", 0) bm.mark_check_run("assets") + # Geänderte Bilder → KI-Cache invalidieren, damit die KI sie inhaltlich neu + # bewertet (läuft hier vor run_ai_analysis → Neubewertung noch im selben Scan). + _invalidate_ai_for_changed_assets(bm, asset_diff) if pc.get("ext_links", True) and bm.is_check_due("ext_links", interval): print(" Wöchentliche Prüfung externer Links läuft mit ...") ext_links = _run_ext_links_core(cfg, snap) # informativ, hebt Level nicht an @@ -1148,6 +1162,7 @@ def cmd_check_assets(args: argparse.Namespace, cfg: dict) -> int: print("Prüfe die Dateien Ihrer Website (Bilder/Skripte/Stylesheets) ...") asset_diff, assessment, has_baseline = _run_assets_core(cfg, bm, snap) bm.mark_check_run("assets") + _invalidate_ai_for_changed_assets(bm, asset_diff) report = { "generated_at": datetime.now(timezone.utc).isoformat(), diff --git a/scanner/baseline.py b/scanner/baseline.py index 22ca7b9..f51da96 100644 --- a/scanner/baseline.py +++ b/scanner/baseline.py @@ -292,6 +292,22 @@ class BaselineManager: json.dumps(state, indent=2, ensure_ascii=False), encoding="utf-8" ) + def invalidate_ai_url_hashes(self, urls: list[str]) -> int: + """Entfernt URLs aus der url_hashes-Erinnerung. Folge: Die KI lädt diese Bilder + beim nächsten Lauf erneut und bewertet sie inhaltlich neu (geänderte Bytes → + neuer Hash → Cache-Miss → Analyse). Wird von der Datei-Prüfung (check-assets) + für geänderte Bild-Assets aufgerufen. Returns Anzahl entfernter Einträge.""" + if not urls: + return 0 + ledger = self.load_ai_ledger() + uh = ledger.get("url_hashes", {}) + removed = [u for u in urls if u in uh] + for u in removed: + del uh[u] + if removed: + self.save_ai_ledger(ledger) + return len(removed) + def dismiss_ai_entries(self, fingerprints: list[str] | None = None) -> int: """ Mark ledger entries as dismissed (false-positive acknowledgement). diff --git a/tests/test_ai_analyzer.py b/tests/test_ai_analyzer.py index 4373972..a0e7d28 100644 --- a/tests/test_ai_analyzer.py +++ b/tests/test_ai_analyzer.py @@ -287,6 +287,49 @@ class TestImages: m2.assert_not_called() +# --------------------------------------------------------------------------- +# check-assets ↔ KI-Kopplung: geänderte Bilder werden neu bewertet +# --------------------------------------------------------------------------- + +class TestAssetAiCoupling: + def _cfg_img(self): + cfg = _cfg() + cfg["ai_analysis"]["text"]["enabled"] = False + cfg["ai_analysis"]["image"]["enabled"] = True + return cfg + + def test_invalidate_removes_only_given_urls(self, tmp_path): + bm = BaselineManager(tmp_path) + bm.save_ai_ledger({"entries": {}, "url_hashes": {"a": "h1", "b": "h2", "c": "h3"}}) + assert bm.invalidate_ai_url_hashes(["b", "x"]) == 1 # x ist nicht vorhanden + assert set(bm.load_ai_ledger()["url_hashes"]) == {"a", "c"} + + def test_changed_image_reanalyzed_after_invalidation(self, tmp_path, monkeypatch): + """Byte-Tausch eines bekannten Bildes → invalidieren → KI lädt+bewertet neu.""" + monkeypatch.setenv("OPENROUTER_API_KEY", "test") + bm = BaselineManager(tmp_path) + cfg = self._cfg_img() + snap = _snap("kurz", img=[{"url": "https://x.de/logo.png", "class": "internal"}]) + + with patch("scanner.ai_analyzer.fetch_asset_hashes", + return_value={"https://x.de/logo.png": {"sha256": "OLD", "size": 1, "error": None}}), \ + patch("scanner.ai_analyzer._openrouter_chat", return_value=_verdict()): + run_ai_analysis(cfg, bm, snap, {}) + + # check-assets erkennt Byte-Änderung → Kopplung invalidiert die URL + assert bm.invalidate_ai_url_hashes(["https://x.de/logo.png"]) == 1 + + new = _verdict("pornography", "high", 0.95) + with patch("scanner.ai_analyzer.fetch_asset_hashes", + return_value={"https://x.de/logo.png": {"sha256": "NEW", "size": 1, "error": None}}) as f2, \ + patch("scanner.ai_analyzer._openrouter_chat", return_value=new) as c2: + res = run_ai_analysis(cfg, bm, snap, {}) + f2.assert_called() # neu geladen (URL war invalidiert) + c2.assert_called() # neue Bytes (NEW) → Cache-Miss → neu analysiert + assert res["api_calls"] == 1 + assert len(res["findings"]) == 1 # jetzt beanstandet + + # --------------------------------------------------------------------------- # Scoring: cap at yellow # ---------------------------------------------------------------------------